提问者:小点点

无需调用刷新即可刷新JavaFX TableView()


如果我想在某个任意ObservableValue已更改但基础TableView数据未更改的情况下使TableView刷新(而不调用refresh()方法),那么对提取器回调方法进行自适应是否是一个好的解决方案?

这是一个使用TableView刷新()方法的实现示例。

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. 通过setViable()切换可见性关闭和打开;
  2. 从所需表示形式的原始对象创建派生的观察列表并使用setItems();
  3. Futz与节点的大小来触发刷新;
  4. 做一个getItems(),清除TableView,做一个setItems();和
  5. 刷新()方法。

我试过#2和#5,显然都很有效。选项1、3和4看起来很混乱。我不喜欢#2,因为它会浪费内存,我个人认为不应该更改底层数据来反映它是如何向用户表示的。

我看了观察列表中的fire Changed()方法,但是,它是受保护的,不是公开的。从哲学上讲,改变的不是数据,而是表示——所以我不喜欢这个解决方案。

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

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

我在想,用这种风格做点什么可能会引发一种更新,但我还没有详细探讨过这一点。

在上述解决方案中,我认为提取器回调是继续的方式。我确实认为我错过了显而易见的解决方案,因此任何见解都是非常感谢的。


共1个答案

匿名用户

由于kleopatra提供的见解和James\u D在过去发布的类似示例,我实现了一个不调用TableView的实现。refresh()方法。我在这里提供了一个解决方案,以防有人感兴趣——尽管所有的功劳都归kleopatra和James\u 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();
    }

}