不调用refresh()方法,如何刷新JavaFX TableView?

3

简述

如果我想在某一随意的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。

考虑的解决方案

简单的解决方案

  1. 通过setVisible()切换可见性;
  2. 从原始数据创建导出的ObservableList,并使用setItems();
  3. 通过更改节点的大小来触发刷新;
  4. 获取Items(),清除TableView,然后设置Items();和
  5. refresh()方法。

我尝试过#2和#5,它们都可以工作。选项1、3和4似乎很笨拙。我不喜欢#2,因为它浪费内存,而且我个人认为基础数据不应该更改以反映向用户的呈现方式。

其他无需refresh()的解决方案

fireChanged()

我查看了ObservableList中的fireChanged()方法,但它是受保护和非公共的。从哲学上讲,不是数据发生了变化,只是表示发生了变化-因此我不喜欢这个解决方案。

提取器回调

我认为可以使用这种方法来解决这个问题,但我还没有实现解决方案(这也是这个问题的理由)。

多个TableColumns

我可以为不同的表示创建TableColumns,并更改可见性以反映所需的配置,但这似乎很浪费(但它可能比refresh()更好-我不确定refresh()是否每次调用时都会创建所有单元格)。

样式操作

我在考虑使用样式来触发刷新,但我还没有详细研究过这个问题。

结尾

在上述解决方案中,我认为提取器回调是前进的方式。我认为我错过了明显的解决方案,因此非常感谢任何见解。


4
不需要使用这些东西。只需遵循标准的JavaFX属性模式,将可能发生更改的数据封装在属性中,它就会自动更新。如果它不能工作,请发布一个[mre]以便我们查看问题所在。 - James_D
1
不错的问题,附带示例 :) 我花了一些时间来深入挖掘其中的问题:你需要使用绑定来获取派生值,而不仅仅是查找。有了这个绑定,值将在切换时更改,并且可视化更新将自动进行。顺便说一句,最好不要混合使用值/单元格工厂,除非你想让自己和我困惑 <g> - kleopatra
1
请遵循函数命名规则:应该是 massProperty()(而不是 getMassProperty())。 - kleopatra
1个回答

2

感谢kleopatra提供的见解以及James_D过去发布的类似示例,我制作了一个不需要调用TableView.refresh()方法即可工作的实现方式。我在此提供解决方案,以防任何人感兴趣——尽管所有的功劳都归于kleopatra和James_D。

/**
 * Demonstrates a TableView that refreshes via the use of a binding for
 * the cell value.
 *
 * This approach will fire for each affected cell, which might be an
 * issue for large tables.
 */
package com.example.rtv3;

import java.text.DecimalFormat;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
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;

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 nameProperty() {
            return name;
        }

        public DoubleProperty massProperty() {
            return mass;
        }

        public DoubleProperty uncertaintyProperty() {
            return uncertainty;
        }
    }

    @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, Number>("Uncertainty");

        nameCol.setCellValueFactory(
                r -> r.getValue().nameProperty());
        massCol.setCellValueFactory(
                r -> r.getValue().massProperty().asObject());

        // Implement a CellValueFactory that uses a DoubleBinding to
        // dynamically change the representation of the data based
        // on an external property.
        // NOTE: Even though DoubleBinding has "Double" in the name,
        // it implements ObservableValue<Number> not ObservableValue<Double>,
        // thus the cell type for the column needs to be Number vice Double.
        // NOTE: More complexity can be achieved by extending the
        // DoubleBinding class instead of using the Binding.createDoubleBinding
        // method.  A listener will need to be added to the checkbox selected
        // property that calls the invalidate() method on the binding.
        uncCol.setCellValueFactory(
                (TableColumn.CellDataFeatures<Planet, Number> r) -> {
                    // Instantiate a DoubleBinding that uses a Callable
                    // to change the representation of the uncertainity
                    // and use the selected property from the checkbox
                    // as a dependency (the dependency will cause invalidate
                    // the binding).
                    return Bindings.createDoubleBinding(
                            // The Callable that computes the value
                            () -> {
                        var u = r.getValue().uncertaintyProperty().get();
                        if (toggle.selectedProperty().getValue()) {
                            var v = r.getValue().massProperty().get();
                            return 100.0 * u / v;
                        } else {
                            return u;
                        }
                    },
                            // The dependency that we want to watch
                            toggle.selectedProperty());
                });

        // Setup the formatting of the uncertainty column to change
        // based on the toggle
        var valueFormatter = new DecimalFormat("0.000E0");
        var percentFormatter = new DecimalFormat("0.0000");
        uncCol.setCellFactory(
                v -> {
                    return new TableCell<>() {
                @Override
                public void updateItem(Number item, boolean empty
                ) {
                    super.updateItem(item, empty);

                    if (empty) {
                        setText(null);
                    } else {
                        setText(toggle.selectedProperty().getValue()
                                ? percentFormatter.format(item)
                                : valueFormatter.format(item));
                    }
                }
            };
                });

        table.getColumns().addAll(nameCol, massCol, uncCol);
        table.setItems(planets);

        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();
    }

}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接