简述
如果我想在某一随意的ObservableValue变化时刷新TableView(而不是调用refresh()方法)而表格的数据却没有变化,那么一个提取器回调方法的适配方案是一个好的解决方案吗?
这里有一个使用TableView refresh()方法的实现示例。
package com.example.rtv1;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Callback;
public class App extends Application {
/**
* A data class exemplar.
*/
public static class Planet {
private final StringProperty name;
private final DoubleProperty mass;
private final DoubleProperty uncertainty;
public Planet(String name, double mass, double uncertainty) {
this.name = new SimpleStringProperty(name);
this.mass = new SimpleDoubleProperty(mass);
this.uncertainty = new SimpleDoubleProperty(uncertainty);
}
public StringProperty getNameProperty() {
return name;
}
public DoubleProperty getMassProperty() {
return mass;
}
public DoubleProperty getUncertaintyProperty() {
return uncertainty;
}
}
/**
* Provides a CellFactory and CellValueFactory that use an external
* (to the TableView) property to control how data is represented.
*/
public class DerivedCell<S extends Planet> {
private final ObservableValue<Boolean> watch;
private final NumberFormat valueFormatter;
private final NumberFormat percentFormatter;
private final DoubleProperty value;
public DerivedCell(ObservableValue<Boolean> watch) {
this.watch = watch;
valueFormatter = new DecimalFormat("0.000E0");
percentFormatter = new DecimalFormat("0.0000");
value = new SimpleDoubleProperty(Double.NaN);
// I could put a listener here to invalidate value (perhaps set
// it to NAN) when the boolean property is toggled, but my concern
// is that could fire many listeners. Is that less overhead than
// calling TableView.refresh()?
}
/**
* Provides a CellFactory that will change formatting based on
* an external property.
*/
public Callback<TableColumn<S, Double>, TableCell<S, Double>> forCell() {
return list -> new TableCell<S, Double>() {
@Override
public void updateItem(Double item, boolean empty
) {
super.updateItem(item, empty);
if (empty) {
setText(null);
} else {
setText(watch.getValue()
? percentFormatter.format(item)
: valueFormatter.format(item));
}
}
};
}
/**
* Provides a CellValueFactory that will change the representation of
* the data based on an external property.
*/
public Callback<TableColumn.CellDataFeatures<S, Double>, ObservableValue<Double>> getValue() {
return r -> {
var u = r.getValue().getUncertaintyProperty().get();
if (watch.getValue()) {
var v = r.getValue().getMassProperty().get();
value.setValue(100.0 * u / v);
} else {
value.setValue(u);
}
return value.asObject();
};
}
}
@Override
public void start(Stage stage) throws Exception {
var planets = FXCollections.observableArrayList(
// From: https://en.wikipedia.org/wiki/List_of_Solar_System_objects_by_size
new Planet("Mercury", 330.11E21, 0.02E21),
new Planet("Venus", 4867.5E21, 0.2E21),
new Planet("Earth", 5972.4E21, 0.3E21),
new Planet("Mars", 641.71E21, 0.03E21),
new Planet("Jupiter", 1898187E21, 88E21),
new Planet("Saturn", 568317E21, 13E21),
new Planet("Uranus", 86813E21, 4E21),
new Planet("Neptune", 102413E21, 5E21),
new Planet("Pluto", 13.03E21, 0.03E21)
);
var layout = new VBox();
var toggle = new CheckBox("Uncertainty as %");
toggle.selectedProperty().setValue(true);
var table = new TableView<Planet>();
var nameCol = new TableColumn<Planet, String>("Name");
var massCol = new TableColumn<Planet, Double>("Mass");
var uncCol = new TableColumn<Planet, Double>("Uncertainty");
var derived = new DerivedCell<Planet>(toggle.selectedProperty());
nameCol.setCellValueFactory(
r -> r.getValue().getNameProperty());
massCol.setCellValueFactory(
r -> r.getValue().getMassProperty().asObject());
uncCol.setCellValueFactory(derived.getValue());
uncCol.setCellFactory(derived.forCell());
table.getColumns().addAll(nameCol, massCol, uncCol);
table.setItems(planets);
// Call the maligned refresh() TableView method when the CheckBox
// changes state. It would be fantastic if there was a way to
// call the fireChanged() method in the observable list...
toggle.selectedProperty().addListener(
(var ov, var t, var t1) -> {
table.refresh();
});
layout.getChildren().addAll(toggle, table);
var scene = new Scene(layout);
stage.setTitle("Refreshable TableView");
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
细节
当@kleopatra之前回答类似的问题时,他表示“不要使用刷新”。
我有一个情况,底层数据没有更改,只是在TableView中的呈现方式变化了。具体而言,我有两列数据,一个是测量值,另一个是不确定度。用户能够控制测量单位(例如kg)以及不确定度是显示为测量单位还是显示为百分比。
单元格工厂和单元格值工厂使用控制值和数字格式化的属性。当这些属性中的一个更改时,会触发更改侦听器并刷新TableView。
考虑的解决方案
简单的解决方案
- 通过setVisible()切换可见性;
- 从原始数据创建导出的ObservableList,并使用setItems();
- 通过更改节点的大小来触发刷新;
- 获取Items(),清除TableView,然后设置Items();和
- refresh()方法。
我尝试过#2和#5,它们都可以工作。选项1、3和4似乎很笨拙。我不喜欢#2,因为它浪费内存,而且我个人认为基础数据不应该更改以反映向用户的呈现方式。
其他无需refresh()的解决方案
fireChanged()
我查看了ObservableList中的fireChanged()方法,但它是受保护和非公共的。从哲学上讲,不是数据发生了变化,只是表示发生了变化-因此我不喜欢这个解决方案。
提取器回调
我认为可以使用这种方法来解决这个问题,但我还没有实现解决方案(这也是这个问题的理由)。
多个TableColumns
我可以为不同的表示创建TableColumns,并更改可见性以反映所需的配置,但这似乎很浪费(但它可能比refresh()更好-我不确定refresh()是否每次调用时都会创建所有单元格)。
样式操作
我在考虑使用样式来触发刷新,但我还没有详细研究过这个问题。
结尾
在上述解决方案中,我认为提取器回调是前进的方式。我认为我错过了明显的解决方案,因此非常感谢任何见解。
massProperty()
(而不是getMassProperty()
)。 - kleopatra