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

JavaFX TreeView - repaint node when its underlying data changes

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

Nodeclass有一个字段:

private final CashflowSet cashflowSet = new CashflowSet();

并且 CashflowSet 又包含一个可观察列表 属性 of Cashflows:

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

(不要理会我为什么要这样嵌套;Node class 和 CashflowSet class 都有各种其他字段,这些字段与此处无关)

此外,我有一个用于树的自定义细胞工厂:

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 也渲染了其他东西,但是我还是把这里不相关的都删掉了)

现在,当我在我的数据模型中 add/remove 现金流时,我找到了适当的 Node 和 add/remove 现金流 to/from 其可观察的现金流列表,例如:

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

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

我知道我可以在树上调用 refresh(),但是树可以包含很多数据,我希望有良好的性能,并在单节点似乎是一个糟糕的解决方案。

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

谢谢

许多 GUI 框架使用观察者模式来仅更新需要更新的部分。如果你可以将每个节点定义为一个 Observer,并将节点的每条数据定义为一个 Observable,那么你需要做的就是将这些 Observers 订阅到它们对应的 Observable。这样,当数据发生变化时,它可以通知正在侦听的节点,以便只有它们可以更改。

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

希望对您有所帮助。

解决此问题的一种方法是将侦听器添加到 TreeCell 构造函数中的 'cashFlows' 列表。

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

在上面的代码中,您注册了一个侦听器,以侦听与每个单元格关联的项目的现金流量。所以每当列表(cashFlows)发生更新时,都会调用updateItem来re-evaluate显示。

[更新]:

根据评论和建议,我尝试将提取器实现包含到 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;
        }
    }
}

使用 Listeners 方法(不推荐,留作记录)

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

可能的解决方案:

  1. 更新数据时,要么:

    a) 更改 treeItem。

    b) 对现有的树项目发射 tree item value change event

2。子类树项目和 override its value property 具有自定义 属性 实现感知您的更改。

正如 James_D 在评论中指出的那样,选项 2 将不起作用,因为值 属性 是私有的,因此不能被覆盖。

为可能的方法 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;
        }
    }
}