【问题标题】:JavaFX: how to handle dragging an item from a TreeViewJavaFX:如何处理从 TreeView 拖动项目
【发布时间】:2015-09-03 21:30:17
【问题描述】:

对于我正在开发的应用程序,我有一个带有(我自己的)TreeItems 的 TreeView。这工作正常,我可以按预期显示项目。

我现在希望能够处理从 TreeView 拖动一个项目到应用程序窗口的另一部分并让它在那里执行一些操作。我现在面临两个(至少……)问题:

  1. 每当您在 TreeView 中单击时,该项目始终处于选中状态。可以预防吗?
  2. 在 TreeView 上添加 MouseEvent 侦听器时,我得到了能够检测拖动并对其做出响应的事件。但是,我无法确定鼠标事件对应的 TreeItem。当然,我需要知道确切的 TreeItem 才能使拖动工作。这可能吗?

我尝试过的一些事情:

  1. 我添加了自己的单元格工厂,即使在处理和使用单元格上的所有鼠标事件时,树中的项目仍处于选中状态???
  2. 如果我向每个单元格添加一个 MouseEvent 处理程序,我将能够管理拖放,但考虑到 TreeView 中可能有数千行(可能 >> 100,000,并非全部扩展),是这不是一个巨大的开销吗?为 TreeView 只有一个事件处理程序不是更好吗? (但是,我如何确定对应的 TreeItem 呢?)

TreeView 鼠标事件为我提供以下信息:

没有点击单元格:MouseEvent [source = TreeView[id=templateTreeView, styleClass=tree-view], target = TreeViewSkin$1@32a37c7a[styleClass=cell indexed-cell tree-cell]'null', eventType = MOUSE_PRESSED, consumed = false, x = 193.0, y = 289.0, z = 0.0, button = PRIMARY, primaryButtonDown, pickResult = PickResult [node = TreeViewSkin$1@32a37c7a[styleClass=cell indexed-cell tree-cell]'null', point = Point3D [x = 192.0, y = 8.0, z = 0.0], distance = 1492.820323027551]

单击带有文本“属性”的单元格:MouseEvent [source = TreeView[id=templateTreeView, styleClass=tree-view], target = TreeViewSkin$1@16aa9102[styleClass=cell indexed-cell tree-cell]'Attributes', eventType = MOUSE_PRESSED, consumed = false, x = 76.0, y = 34.0, z = 0.0, button = PRIMARY, primaryButtonDown, pickResult = PickResult [node = TreeViewSkin$1@16aa9102[styleClass=cell indexed-cell tree-cell]'Attributes', point = Point3D [x = 75.0, y = 13.0, z = 0.0], distance = 1492.820323027551]

我猜秘密是在 PickResult 的节点中的某个地方,但从那里我仍然无法看到如何到达 TreeItem。

希望有一个(简单的)答案...

【问题讨论】:

  • 非常有趣,我稍后会细读(我可能会需要它!)幸运的是,现在我正在拖动到画布上,为此我已经有了代码来确定我的目标????

标签: java treeview javafx-8


【解决方案1】:

你犯了过早优化的罪:)。

TreeCells 基本上只为TreeView 中的当前可见 项创建。当您展开或折叠树中的节点或滚动时,那些TreeCells 将被重用以显示不同的TreeItems。这就是updateItem(...)TreeCell 中类似方法的目的;当TreeCell 实例显示的项目发生变化时,它们会被调用。

我系统上的TreeCell 高约 1/4 英寸;显示 100,000 个TreeCells 需要一台超过 2,000 英尺/630 米高的显示器。到那时,您可能比一些额外的侦听器有更严重的内存分配问题......但无论如何,只有在该特定单元格上发生事件时才会调用侦听器,并且与单元本身,因此除非您有任何直接证据在单元上注册侦听器(正如您所观察到的,这会大大降低您的代码复杂性)会对性能产生不利影响,否则您应该使用“每个单元的侦听器”方法。

这是一个包含 1,000,000 个Integer 值的树项的树示例。它跟踪创建的TreeCells 的数量(在我的系统上,我设置的窗口大小似乎从未超过 20)。它还显示一个标签;您可以将值从树中拖到标签上,标签将显示掉在那里的值的总和。

import java.util.stream.IntStream;

import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.DataFormat;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class TreeViewNoSelection extends Application {


    private static int cellCount =  0 ;
    private final DataFormat objectDataFormat = new DataFormat("application/x-java-serialized-object");

    @Override
    public void start(Stage primaryStage) {
        TreeView<Integer> tree = new TreeView<>();
        tree.setShowRoot(false);

        Task<TreeItem<Integer>> buildTreeTask = new Task<TreeItem<Integer>>() {

            @Override
            protected TreeItem<Integer> call() throws Exception {
                TreeItem<Integer> treeRoot = new TreeItem<>(0);


                IntStream.range(1, 10).mapToObj(this::createItem)
                    .forEach(treeRoot.getChildren()::add);
                return treeRoot ;
            }
            private TreeItem<Integer> createItem(int value) {
                TreeItem<Integer> item = new TreeItem<>(value);
                if (value < 100_000) {
                    for (int i = 0; i < 10; i++) {
                        item.getChildren().add(createItem(value * 10 + i));
                    }
                }
                return item ;
            }

        };



        tree.setCellFactory(tv -> new TreeCell<Integer>() {

            {               
                System.out.println("Cells created: "+(++cellCount));

                setOnDragDetected(e -> {
                    if (! isEmpty()) {
                        Dragboard db = startDragAndDrop(TransferMode.COPY);
                        ClipboardContent cc = new ClipboardContent();
                        cc.put(objectDataFormat, getItem());
                        db.setContent(cc);
                        Label label = new Label(String.format("Add %,d", getItem()));
                        new Scene(label);
                        db.setDragView(label.snapshot(null, null));
                    }
                });
            }

            @Override
            public void updateItem(Integer value, boolean empty) {
                super.updateItem(value, empty);
                if (empty) {
                    setText(null);
                } else {
                    setText(String.format("%,d", value));
                }
            }
        });

        IntegerProperty total = new SimpleIntegerProperty();
        Label label = new Label();
        label.textProperty().bind(total.asString("Total: %,d"));

        label.setOnDragOver(e -> 
                e.acceptTransferModes(TransferMode.COPY));

        // in real life use a CSS pseudoclass and external CSS file for the background:
        label.setOnDragEntered(e -> label.setStyle("-fx-background-color: yellow;"));
        label.setOnDragExited(e -> label.setStyle(""));

        label.setOnDragDropped(e -> {
            Dragboard db = e.getDragboard();
            if (db.hasContent(objectDataFormat)) {
                Integer value = (Integer) db.getContent(objectDataFormat);
                total.set(total.get() + value);
                e.setDropCompleted(true);
            }
        });

        BorderPane.setMargin(label, new Insets(10));
        label.setMaxWidth(Double.MAX_VALUE);
        label.setAlignment(Pos.CENTER);

        BorderPane root = new BorderPane(new Label("Loading..."));

        buildTreeTask.setOnSucceeded(e -> {
            tree.setRoot(buildTreeTask.getValue());
            root.setCenter(tree);
            root.setBottom(label);
        });

        primaryStage.setScene(new Scene(root, 250, 400));
        primaryStage.show();

        Thread t = new Thread(buildTreeTask);
        t.setDaemon(true);
        t.start();

    }

    public static void main(String[] args) {
        launch(args);
    }
}

对于选择问题:我会质疑您为什么要这样做;它将创造一种不同寻常的用户体验。问题可能是管理选择的“烘焙”事件处理程序在您定义的处理程序之前被调用,因此当您使用事件时,选择已经更改。您可以尝试添加事件过滤器:

cell.addEventFilter(MouseEvent.MOUSE_PRESSED, Event::consume);

但这也会禁用展开/折叠树中的节点。

所以你可以试试这样的:

cell.addEventFilter(MouseEvent.MOUSE_PRESSED, e -> {
    if (getTreeItem() != null) {
         Object target = e.getTarget();
         if (target instanceof Node && ((Node)target).getStyleClass().contains("arrow")) {
             getTreeItem().setExpanded(! getTreeItem().isExpanded());
         }
     }
     e.consume();
});

从那时起,它开始看起来有点像黑客......

如果你想完全禁用选择,另一种选择可能是为树创建一个自定义选择模型,它总是返回一个空选择。

【讨论】:

  • 我整理了一个使用选择模型的示例,但我不确定完全禁用选择是否是您想要的。如果有帮助,我可以发布。
  • 太棒了,感谢您的广泛回答!我喜欢“犯过早优化的罪”!所以解决方案确实是用 TreeCells 来做。事实上,这样做很容易!我现在有一个工作拖放。它缺少对用户的视觉确认,即他/她正在拖动某些东西,但我想这应该很容易做到(有什么建议吗?)
  • 至于不希望选择发生,这是因为我有一个选择模型更改侦听器会触发我在这种情况下不想要的更改。但是现在我可以检测到从 MouseEvent 处理程序中选择的节点,我不需要这样做,我也可以让我的代码更干净!
  • 开始拖动时您应该会看到一个默认的拖动图标。如果需要,您可以在 dragDetected 处理程序中使用 dragboard.setDragView(..) 对其进行配置。
  • 好的,对。我现在理解的拖放事件并得到了这些工作。但是,我现在遇到了以下问题:我需要知道放置所在的确切(本地)坐标。但是,DRAG_DROPPED 事件不携带这些(?)。在单元处理程序的 MOUSE_RELEASED 上,我确实得到了它们,所以我正在考虑使用它们。但这里有一个问题:当我在 DRAG_DETECTED(单元格的)中设置内容 Dragboard 时,MOUSE_RELEASED 处的坐标会以某种方式偏移(实际上 PickResult 完全关闭)。这是一个错误还是有办法解决这个问题?
猜你喜欢
  • 1970-01-01
  • 2010-10-12
  • 2013-03-25
  • 2019-01-08
  • 2017-01-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多