无法弄清楚为什么 TreeView 在 JavaFX 中构建为镜像

Cannot figure out why a TreeView builds as a mirror in JavaFX

提问人:Michael Sims 提问时间:11/12/2023 最后编辑:Michael Sims 更新时间:11/14/2023 访问量:50

问:

我在 JavaFX 中的 TreeView 遇到了一个奇怪的问题。

我使用它的应用程序从基于 Web 的数据构建分支,因此填充 TreeView 需要一些时间。但是,当它填充时,分支显示在顶部底部,但在底部,它只是文本,而不是实际的 TreeItems。

如果有人想自己尝试,我创建了最小的可重现代码。我将最少的代码发布到 GitHub,您可以从这里克隆它

此屏幕截图还将显示问题。

以下是将重现该问题的类:

主要:

public class Main extends Application {

    public static void main(String[] args) {
        launch(args);
    }
    
    private TreeItem<TreeNode> root;
    private Random random = new Random(System.currentTimeMillis());

    @Override
    public void start(Stage stage) throws Exception {
        root = new TreeItem<>(new TreeNode("Root", false));
        TreeView<TreeNode> treeView = new TreeView<>(root);
        treeView.setCellFactory(param -> new CellFactory());
        treeView.setShowRoot(false);
        ScrollPane scrollPane = new ScrollPane(treeView);
        double width = 500;
        double height = 800;
        AnchorPane ap = new AnchorPane(scrollPane);
        Scene scene = new Scene(ap);
        stage.setScene(scene);
        stage.setWidth(width);
        stage.setHeight(height);
        stage.setOnCloseRequest(e->{
            Platform.exit();
            System.exit(0);
        });
        treeView.setPrefWidth(width);
        treeView.setPrefHeight(height);
        ap.setPrefWidth(width);
        ap.setPrefHeight(height);
        AnchorPane.setRightAnchor(scrollPane, 0.0);
        AnchorPane.setLeftAnchor(scrollPane, 0.0);
        AnchorPane.setTopAnchor(scrollPane, 30.0);
        AnchorPane.setBottomAnchor(scrollPane, 0.0);
        stage.show();
        new Thread(buildTree()).start();
    }

    private String getName() {
        int low = 100;
        int high = 9999;
        return "Node(" + random.nextInt(low, high) + ")";
    }

    private Runnable buildTree() {
        return ()->{
            for (int x = 0; x < 100; x++) {
                boolean checkBoxNode = x % 2 == 0;
                root.getChildren().add(new TreeItem<>(new TreeNode(getName(), checkBoxNode)));
                sleep(500);
            }
        };
    }

    private void sleep(long time) {
        try {
            TimeUnit.MILLISECONDS.sleep(time);
        }
        catch (InterruptedException ignored) {
        }
    }
}

细胞工厂:

public class CellFactory extends TreeCell<TreeNode> {
    private CheckBox checkBox;

    public CellFactory() {
        checkBox = new CheckBox();
        checkBox.setOnAction(event -> {
            TreeNode item = getItem();
            if (item != null) {
                item.setSelected(checkBox.isSelected());
            }
        });

        itemProperty().addListener(((observable, oldValue, treeNode) -> {
            if (treeNode != null) {
                checkBox.setVisible(treeNode.isCheckBoxNode());
            }
        }));
    }

    @Override
    protected void updateItem(TreeNode treeNode, boolean empty) {
        super.updateItem(treeNode, empty);
        if (empty || treeNode == null) {
            setGraphic(null);
        }
        else {
            checkBox.setSelected(treeNode.isSelected());
            setText(treeNode.toString());
            setGraphic(checkBox);
        }
    }
}

树节点:

public class TreeNode {
    private String name;
    private boolean selected = true;
    private boolean checkBoxNode;

    public TreeNode(String name, boolean checkBoxNode) {
        this.name = name;
        this.checkBoxNode = checkBoxNode;
    }

    public boolean isSelected() {
        return selected;
    }

    public void setSelected(boolean selected) {
        this.selected = selected;
    }

    public boolean isCheckBoxNode() {
        return checkBoxNode;
    }

    @Override
    public String toString() {
        return name;
    }
}

有谁知道为什么会这样?

JavaFX 树视图

评论


答:

5赞 jewelsea 11/12/2023 #1

代码中的 bug 导致观察到的问题

单元格是重复使用的,并且必须在调用中正确更新。updateItem

当单元格不为空时,可以设置单元格的文本。但是,如果以前为空的单元格更新为空值,则不会取消设置文本(将其设置为 null)。

请参阅下面的固定代码:

protected void updateItem(TreeNode treeNode, boolean empty) {
    super.updateItem(treeNode, empty);
    if (empty || treeNode == null) {
        setText(null);  // <-- text for previous associated 
                        //     item needs to be cleared for empty cells.
        setGraphic(null);       
    } else {
        checkBox.setSelected(treeNode.isSelected());
        setText(treeNode.toString());  // <--- text has been set here.
        setGraphic(checkBox);
    }
}

示例中不相关的问题或令人困惑的事情

切勿从其他线程访问或修改与活动场景图关联的元素,而应使用该元素在 JavaFX 线程上添加新的 TreeItems。这不是问题的根源,但这是一个应该解决的问题。Platform.runLater

你的 CellFactory 名称令人困惑,它不是一个单元工厂,它是一个 Cell(你可以看到,因为它扩展了 TreeCell)。工厂是生成新单元格的 lambda 的完整回调。

TreeNode 类的名称令人困惑,它不是 JavaFX 节点。是的,它是数据模型中树的一个节点,但考虑到上下文,这让我感到非常困惑。对其他人来说可能没问题,我不知道。

常见问题

我不完全理解为什么以前的 TreeItem 中的文本会显示在 TreeView 的底部。

单元的创建和放置是内部实现的一部分。除非您想更改 JavaFX 库中的内部实现(您不想这样做),否则您不必了解内部细节是如何工作的。理解这一点的唯一方法是查看代码并从实现中对设计进行逆向工程。TreeViewTreeView

只需确保在支持单元格的项发生更改时正确更新单元格视觉对象。

我想,在内部,TreeView 可能正在移动单元格;有时创建新的,有时重用现有的。但它如何管理细胞并不重要;只要正确更新单元格,就会显示正确的值。

在您的问题示例中发生的奇怪行为是由于代码中的错误而不是 JavaFX 实现中的错误。TreeView

另外,你会用哪个类名而不是 和 ?CellFactoryTreeNode

可能与您的问题域有关(我不知道)。如果你想要一些非常通用的名字,那么:

  • CellFactory -> CustomTreeCellCheckManagementTreeCell
  • TreeNode -> TreeItemData.

评论

0赞 Michael Sims 11/12/2023
谢谢,这确实解决了问题。但我并没有因此而变得更好,因为我不完全理解为什么以前的 TreeItem 中的文本会显示在 TreeView 的底部。另外,你会用哪个类名而不是 CellFactory 和 TreeNode ?
0赞 jewelsea 11/12/2023
添加了常见问题解答部分。
0赞 Michael Sims 11/12/2023
谢谢你的解释。你是对的,当然,了解引擎盖下的工作原理是没有必要的......只需使用这些类,因为它们被设计为可以使用,一切都应该很好......但是好奇心......正如他们所说......☺
0赞 Michael Sims 11/14/2023
你觉得这个名字而不是?ItemClassTreeNode
0赞 jewelsea 11/14/2023
如前所述,这取决于您的域(我不知道)并且是主观的。如果它代表一个类别或类型或项目,也许是可以的(在这种情况下,我会称之为)。根据您对该领域的了解,选择并使用对您最有意义的名称。ItemClassItemCategory