代码之家  ›  专栏  ›  技术社区  ›  user3237736

JavaFX TreeView-在基础数据更改时重新绘制节点

  •  -1
  • user3237736  · 技术社区  · 2 年前

    我有一个 TreeView<Node> ,包含 TreeItem<Node> s

    这个 Node 类有一个字段:

    private final CashflowSet cashflowSet = new CashflowSet();
    

    以及 CashflowSet 依次包含的可观察列表属性 Cashflow s:

    private final SimpleListProperty<Cashflow> cashflows = new SimpleListProperty<>(observableArrayList());
    

    (不用费心为什么要这样嵌套它;Node类和CashflowSet类都有各种其他字段,这些字段在这里不相关)

    此外,我还有一个树的自定义单元工厂:

    tree.setCellFactory(cell -> new NodeRenderer());
    

    显示每个节点上的现金流计数:

    @Override
    protected void updateItem(Node node, boolean empty) {
        super.updateItem(node, empty);
    
        if (node == null || empty) {
            setGraphic(null);
            setText(null);
        } else {
            int cashflowCount = node.getCashflowSet().getCashflows().size();
            label.setText(String.valueOf(cashflowCount));
            setGraphic(label);
        }
    }
    

    (cell factory也呈现了其他内容,但我再次遗漏了此处不相关的所有内容)

    现在,当我在数据模型中添加/删除现金流时,我发现 节点 并在其可观察现金流列表中添加/删除现金流,例如:

    treeItem.getValue().getCashflowSet().addCashflow(cashflow);
    

    我的问题: 在节点的现金流集中添加或删除现金流时,渲染器不会重新绘制节点,因此它仍然显示过期的现金流计数。只有当我强制树重新绘制时(例如通过折叠和展开节点),它才会显示更新的数据。我知道树不会自动重新绘制节点,因为它不会被通知底层数据的这些更改。我知道如何解决这个问题,例如ListView或TableView,其中项目绑定到一个可观察的列表,我可以在属性更改时触发的各种属性上定义提取器。但是TreeView的数据模型是不同的,我不确定这里有什么合适的解决方案。我必须在某个地方手动添加侦听器吗?甚至 bind() 这个 label 我的渲染器的 sizeProperty() 可观察的现金流列表?我不太了解这些细胞工厂是如何工作的,所以我不确定它是否适合这样的地方。

    我知道我可以打电话 refresh() 然而,在树上,树可以包含大量数据,我希望有良好的性能,每当单个节点发生任何更改时,刷新整个内容似乎是一个糟糕的解决方案。

    所以我的问题是: 如何让树在节点的基础现金流列表发生更改(即:删除或添加现金流)时触发特定节点的重新绘制。 (请注意,现金流对象本身不会改变,因此我只需要观察列表大小的变化,而不是列表元素的变化)

    谢谢

    1 回复  |  直到 2 年前
        1
  •  2
  •   jewelsea    2 年前

    潜在解决方案:

    1. 更新数据时,可以:

      a) 更改树项。

      b) 点火a tree item value change event 在现有树项目上。

    2、子类树项和 override its value property 使用了解您的更改的自定义属性实现。

    正如James\u D在评论中指出的那样,选项2不起作用,因为value属性是私有的,所以不能重写它。

    提供了潜在进近1b的示例代码。

    每当与树项的值关联的给定列表的大小更改时,此代码将对现有树项触发更改事件。

    Bindings.size(
        treeItem.getValue().getCashflowSet().getCashflows()
    ).addListener((o, old, new) -> 
        Event.fireEvent(
            treeItem, 
            new TreeItem.TreeModificationEvent<>(
                TreeItem.valueChangedEvent(), 
                treeItem, 
                treeItem.getValue()
            )
        )
    );
    

    当您希望使绑定无效(例如,树项值更改)时,还需要删除侦听器,并可能将该项与新绑定重新关联。

    实例

    示例代码基于Sai答案中的演示代码,并包含在与树项关联的值更改时删除过时绑定和创建新绑定的逻辑。

    import javafx.application.Application;
    import javafx.beans.binding.*;
    import javafx.beans.property.*;
    import javafx.beans.value.ChangeListener;
    import javafx.collections.*;
    import javafx.event.Event;
    import javafx.geometry.Insets;
    import javafx.scene.Scene;
    import javafx.scene.control.*;
    import javafx.scene.layout.VBox;
    import javafx.stage.Stage;
    
    import java.util.Random;
    
    public class TreeViewDemo extends Application {
    
        public static void main(String[] args) {
            launch(args);
        }
    
        private Task demoTask;
        private TreeItem<Task> demoTreeItem;
    
        @Override
        public void start(Stage primaryStage) {
            // BUILD DATA
            Random rnd = new Random();
            ObservableList<Task> tasks = FXCollections.observableArrayList();
            for (int i = 1; i < 10; i++) {
                Task sub1 = new Task("Sub Task A", rnd.nextBoolean());
                Task sub2 = new Task("Sub Task B", rnd.nextBoolean());
    
                Task tsk = new Task("Task " + i, rnd.nextBoolean());
                if (demoTask == null) {
                    tsk.setName("Demo Task");
                    demoTask = tsk;
                }
                tsk.getTasks().addAll(sub1, sub2);
                tasks.addAll(tsk);
            }
    
            // BUILD TREE ITEMS
            TreeItem<Task> rootItem = new MyTreeItem();
            rootItem.setExpanded(true);
            for (Task task : tasks) {
                TreeItem<Task> item = new MyTreeItem(task);
    
                for (Task subTask : task.getTasks()) {
                    TreeItem<Task> subItem = new MyTreeItem(subTask);
                    item.getChildren().add(subItem);
    
                    if (subTask == demoTask) {
                        demoTreeItem = subItem;
                    }
                }
    
                if (task == demoTask) {
                    demoTreeItem = item;
                }
    
                rootItem.getChildren().add(item);
            }
    
            TreeView<Task> treeView = new TreeView<>();
            treeView.setRoot(rootItem);
            treeView.setCellFactory(taskTreeView -> new MyTreeCell());
    
            Button addButton = new Button("Add");
            addButton.setOnAction(e -> demoTask.getCashFlows().add(1));
    
            Button changeButton = new Button("Change Task");
            changeButton.setOnAction(e -> {
                demoTask = createChangeTask();
                demoTreeItem.setValue(demoTask);
            });
    
            VBox root = new VBox(addButton, changeButton, treeView);
            root.setSpacing(10);
            root.setPadding(new Insets(10));
            primaryStage.setScene(new Scene(root));
            primaryStage.setTitle("TreeView Demo");
            primaryStage.show();
        }
    
        private Task createChangeTask() {
            Task changeTask = new Task("Change It", false);
    
            changeTask.getCashFlows().add(1);
            changeTask.getCashFlows().add(2);
    
            return changeTask;
        }
    
        class MyTreeItem extends TreeItem<Task> {
            public MyTreeItem() {
                super();
            }
    
            public MyTreeItem(Task value) {
                super(value);
                establishBindingForValueProperty();
                establishBindingForCashflowSize(null, value);
            }
    
            private void establishBindingForCashflowSize(Task oldValue, Task newValue) {
                // remove old size binding listener, so that if the cashflow associated with the old task changes,
                // it no longer triggers a value change event on this TreeItem.
                if (oldValue != null) {
                    sizeBinding.removeListener(sizeBindingListener);
                    sizeBinding = null;
                    sizeBindingListener = null;
                }
    
                // create a new size binding listener, so that when the cashflow associated with the task changes,
                // it triggers a value change event on this TreeItem.
                if (newValue != null) {
                    sizeBinding = Bindings.size(
                            getValue().getCashFlows()
                    );
    
                    sizeBindingListener = (observable1, oldValue1, newValue1) -> Event.fireEvent(
                            MyTreeItem.this,
                            new TreeModificationEvent<>(
                                    TreeItem.valueChangedEvent(),
                                    MyTreeItem.this,
                                    getValue()
                            )
                    );
    
                    sizeBinding.addListener(sizeBindingListener);
                }
            }
    
            private IntegerBinding sizeBinding;
            private ChangeListener<Number> sizeBindingListener;
            private void establishBindingForValueProperty() {
                valueProperty().addListener((observable, oldValue, newValue) ->
                        establishBindingForCashflowSize(oldValue, newValue)
                );
            }
        }
    
        class MyTreeCell extends TreeCell<Task> {
            @Override
            protected void updateItem(Task item, boolean empty) {
                super.updateItem(item, empty);
                if (item != null && !empty) {
                    setText(item.getName() + " (" + item.getCashFlows().size() + ")");
                } else {
                    setText(null);
                }
            }
        }
    
        class Task {
            StringProperty name = new SimpleStringProperty();
            BooleanProperty completed = new SimpleBooleanProperty();
            ObservableList<Task> tasks = FXCollections.observableArrayList();
    
            ObservableList<Integer> cashFlows = FXCollections.observableArrayList();
    
            public Task(String n, boolean c) {
                setName(n);
                setCompleted(c);
            }
    
            public String getName() {
                return name.get();
            }
    
            public StringProperty nameProperty() {
                return name;
            }
    
            public void setName(String name) {
                this.name.set(name);
            }
    
            public boolean isCompleted() {
                return completed.get();
            }
    
            public BooleanProperty completedProperty() {
                return completed;
            }
    
            public void setCompleted(boolean completed) {
                this.completed.set(completed);
            }
    
            public ObservableList<Task> getTasks() {
                return tasks;
            }
    
            public ObservableList<Integer> getCashFlows() {
                return cashFlows;
            }
        }
    }
    
        2
  •  2
  •   Sai Dandem    2 年前

    解决此问题的一种方法是将侦听器添加到TreeCell构造函数中的“现金流”列表中。

    public MyTreeCell() {
        ListChangeListener<? super Integer> listener = p -> updateItem(getItem(), false);
        itemProperty().addListener((obs, oldItem, newItem) -> {
            if (oldItem != null) {
                oldItem.getCashFlows().removeListener(listener);
            }
            if (newItem != null) {
                newItem.getCashFlows().addListener(listener);
            }
        });
    }
    

    在上面的代码中,您注册了一个监听器来监听与每个单元格关联的项目的现金流。因此,每当列表(现金流)上发生更新时,将调用updateItem来重新评估显示。

    [更新]:

    基于这些评论和建议,我尝试将提取器实现包含到TreeItem中,以触发值更改事件。而且效果很好:)。这就是@kleopatra和@jewelsea提到的实现。这样,您就可以列出要监视和更新单元格的所有可观察属性。

    Callback<Task, Observable[]> extractor = task -> new Observable[]{task.getCashFlows()};
    
    class MyTreeItem<T> extends TreeItem<T> {
            public MyTreeItem(Callback<T, Observable[]> extractor) {
                if (extractor == null) {
                    throw new NullPointerException("Extractor cannot be null");
                }
                final InvalidationListener listener = e -> Event.fireEvent(this, new TreeModificationEvent<>(TreeItem.<T>valueChangedEvent(), this, getValue()));
                valueProperty().addListener((obs, oldValue, newValue) -> {
                    if (oldValue != null) {
                        Stream.of(extractor.call(oldValue)).forEach(prop -> prop.removeListener(listener));
                    }
                    if (newValue != null) {
                        Stream.of(extractor.call(newValue)).forEach(prop -> prop.addListener(listener));
                    }
                });
            }
        }
    

    完整的工作演示如下:

    使用提取器方法

    import javafx.application.Application;
    import javafx.beans.InvalidationListener;
    import javafx.beans.Observable;
    import javafx.beans.property.BooleanProperty;
    import javafx.beans.property.SimpleBooleanProperty;
    import javafx.beans.property.SimpleStringProperty;
    import javafx.beans.property.StringProperty;
    import javafx.collections.FXCollections;
    import javafx.collections.ObservableList;
    import javafx.event.Event;
    import javafx.geometry.Insets;
    import javafx.scene.Scene;
    import javafx.scene.control.Button;
    import javafx.scene.control.TreeCell;
    import javafx.scene.control.TreeItem;
    import javafx.scene.control.TreeView;
    import javafx.scene.layout.VBox;
    import javafx.stage.Stage;
    import javafx.util.Callback;
    
    import java.util.Random;
    import java.util.stream.Stream;
    
    public class TreeViewDemo extends Application {
    
        public static void main(String[] args) {
            launch(args);
        }
    
        private Task demoTask;
    
        @Override
        public void start(Stage primaryStage) {
            // BUILD DATA
            Random rnd = new Random();
            ObservableList<Task> tasks = FXCollections.observableArrayList();
            for (int i = 1; i < 10; i++) {
                Task sub1 = new Task("Sub Task A", rnd.nextBoolean());
                Task sub2 = new Task("Sub Task B", rnd.nextBoolean());
    
                Task tsk = new Task("Task " + i, rnd.nextBoolean());
                if (demoTask == null) {
                    tsk.setName("Demo Task");
                    demoTask = tsk;
                }
                tsk.getTasks().addAll(sub1, sub2);
                tasks.addAll(tsk);
            }
    
            // BUILD TREE ITEMS
            TreeItem<Task> rootItem = new TreeItem<>();
            rootItem.setExpanded(true);
            final Callback<Task, Observable[]> extractor = task -> new Observable[]{task.getCashFlows()};
            for (Task task : tasks) {
                TreeItem<Task> item = new MyTreeItem(extractor);
                item.setValue(task);
                for (Task subTask : task.getTasks()) {
                    TreeItem<Task> subItem = new MyTreeItem(extractor);
                    subItem.setValue(subTask);
                    item.getChildren().add(subItem);
                }
                rootItem.getChildren().add(item);
            }
    
            TreeView<Task> treeView = new TreeView<>();
            treeView.setRoot(rootItem);
            treeView.setCellFactory(taskTreeView -> new TreeCell<Task>() {
                @Override
                protected void updateItem(Task item, boolean empty) {
                    super.updateItem(item, empty);
                    if (item != null && !empty) {
                        setText(item.getName() + " (" + item.getCashFlows().size() + ")");
                    } else {
                        setText(null);
                    }
                }
            });
    
            Button button = new Button("Add");
            button.setOnAction(e -> demoTask.getCashFlows().add(1));
    
            VBox root = new VBox(button, treeView);
            root.setSpacing(10);
            root.setPadding(new Insets(10));
            primaryStage.setScene(new Scene(root));
            primaryStage.setTitle("TreeView Demo");
            primaryStage.show();
        }
    
        class MyTreeItem<T> extends TreeItem<T> {
            public MyTreeItem(Callback<T, Observable[]> extractor) {
                if (extractor == null) {
                    throw new NullPointerException("Extractor cannot be null");
                }
                final InvalidationListener listener = e -> Event.fireEvent(this, new TreeModificationEvent<>(TreeItem.<T>valueChangedEvent(), this, getValue()));
                valueProperty().addListener((obs, oldValue, newValue) -> {
                    if (oldValue != null) {
                        Stream.of(extractor.call(oldValue)).forEach(prop -> prop.removeListener(listener));
                    }
                    if (newValue != null) {
                        Stream.of(extractor.call(newValue)).forEach(prop -> prop.addListener(listener));
                    }
                });
            }
        }
    
        class Task {
            StringProperty name = new SimpleStringProperty();
            BooleanProperty completed = new SimpleBooleanProperty();
            ObservableList<Task> tasks = FXCollections.observableArrayList();
    
            ObservableList<Integer> cashFlows = FXCollections.observableArrayList();
    
            public Task(String n, boolean c) {
                setName(n);
                setCompleted(c);
            }
    
            public String getName() {
                return name.get();
            }
    
            public StringProperty nameProperty() {
                return name;
            }
    
            public void setName(String name) {
                this.name.set(name);
            }
    
            public boolean isCompleted() {
                return completed.get();
            }
    
            public BooleanProperty completedProperty() {
                return completed;
            }
    
            public void setCompleted(boolean completed) {
                this.completed.set(completed);
            }
    
            public ObservableList<Task> getTasks() {
                return tasks;
            }
    
            public ObservableList<Integer> getCashFlows() {
                return cashFlows;
            }
        }
    }
    

    使用监听器方法(不推荐,保留以备记录)

    import javafx.application.Application;
    import javafx.beans.property.BooleanProperty;
    import javafx.beans.property.SimpleBooleanProperty;
    import javafx.beans.property.SimpleStringProperty;
    import javafx.beans.property.StringProperty;
    import javafx.collections.FXCollections;
    import javafx.collections.ListChangeListener;
    import javafx.collections.ObservableList;
    import javafx.geometry.Insets;
    import javafx.scene.Scene;
    import javafx.scene.control.Button;
    import javafx.scene.control.TreeCell;
    import javafx.scene.control.TreeItem;
    import javafx.scene.control.TreeView;
    import javafx.scene.layout.VBox;
    import javafx.stage.Stage;
    
    import java.util.Random;
    
    public class TreeViewDemo extends Application {
    
        public static void main(String[] args) {
            launch(args);
        }
    
        private Task demoTask;
    
        @Override
        public void start(Stage primaryStage) {
            // BUILD DATA
            Random rnd = new Random();
            ObservableList<Task> tasks = FXCollections.observableArrayList();
            for (int i = 1; i < 10; i++) {
                Task sub1 = new Task("Sub Task A", rnd.nextBoolean());
                Task sub2 = new Task("Sub Task B", rnd.nextBoolean());
    
                Task tsk = new Task("Task " + i, rnd.nextBoolean());
                if (demoTask == null) {
                    tsk.setName("Demo Task");
                    demoTask = tsk;
                }
                tsk.getTasks().addAll(sub1, sub2);
                tasks.addAll(tsk);
            }
            
            // BUILD TREE ITEMS
            TreeItem<Task> rootItem = new TreeItem<>();
            rootItem.setExpanded(true);
            for (Task task : tasks) {
                TreeItem<Task> item = new TreeItem(task);
                for (Task subTask : task.getTasks()) {
                    TreeItem<Task> subItem = new TreeItem(subTask);
                    item.getChildren().add(subItem);
                }
                rootItem.getChildren().add(item);
            }
    
            TreeView<Task> treeView = new TreeView<>();
            treeView.setRoot(rootItem);
            treeView.setCellFactory(taskTreeView -> new MyTreeCell());
    
            Button button = new Button("Add");
            button.setOnAction(e -> demoTask.getCashFlows().add(1));
    
            VBox root = new VBox(button, treeView);
            root.setSpacing(10);
            root.setPadding(new Insets(10));
            primaryStage.setScene(new Scene(root));
            primaryStage.setTitle("TreeView Demo");
            primaryStage.show();
        }
    
        class MyTreeCell extends TreeCell<Task> {
            public MyTreeCell() {
                ListChangeListener<? super Integer> listener = p -> updateItem(getItem(), false);
                itemProperty().addListener((obs, oldItem, newItem) -> {
                    if (oldItem != null) {
                        oldItem.getCashFlows().removeListener(listener);
                    }
                    if (newItem != null) {
                        newItem.getCashFlows().addListener(listener);
                    }
                });
            }
    
            @Override
            protected void updateItem(Task item, boolean empty) {
                super.updateItem(item, empty);
                if (item != null && !empty) {
                    setText(item.getName() + " (" + item.getCashFlows().size() + ")");
                } else {
                    setText(null);
                }
            }
        }
    
        class Task {
            StringProperty name = new SimpleStringProperty();
            BooleanProperty completed = new SimpleBooleanProperty();
            ObservableList<Task> tasks = FXCollections.observableArrayList();
    
            ObservableList<Integer> cashFlows = FXCollections.observableArrayList();
    
            public Task(String n, boolean c) {
                setName(n);
                setCompleted(c);
            }
    
            public String getName() {
                return name.get();
            }
    
            public StringProperty nameProperty() {
                return name;
            }
    
            public void setName(String name) {
                this.name.set(name);
            }
    
            public boolean isCompleted() {
                return completed.get();
            }
    
            public BooleanProperty completedProperty() {
                return completed;
            }
    
            public void setCompleted(boolean completed) {
                this.completed.set(completed);
            }
    
            public ObservableList<Task> getTasks() {
                return tasks;
            }
    
            public ObservableList<Integer> getCashFlows() {
                return cashFlows;
            }
        }
    }
    
        3
  •  -1
  •   Tomas    2 年前

    许多GUI框架使用Observer模式只更新需要更新的部分。如果可以将每个节点定义为观察者,将节点的每个数据段定义为可观察对象,那么只需将这些观察者订阅到其相应的可观察对象。这样,当数据更改时,它可以通知正在侦听的节点,以便只有它们可以更改。

    有关更多详细信息,请查看设计的观察者模式。

    希望这有帮助。