JavaFX:如何处理从TreeView拖动项目

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

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

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

我试过的一些事情:

  1. 我添加了自己的单元工厂,甚至当处理和消耗单元格上的所有鼠标事件时,树中的项目仍然被选中???
  2. 如果我向每个单元格添加一个MouseEvent Handler,我将能够管理拖放,但是如果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。

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

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

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

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

以下是包含1,000,000个Integer数值树项的树的示例。 它跟踪创建的TreeCell的数量(在我的系统上,我设置的窗口大小似乎永远不会超过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 tree = new TreeView<>(); tree.setShowRoot(false); Task> buildTreeTask = new Task>() { @Override protected TreeItem call() throws Exception { TreeItem treeRoot = new TreeItem<>(0); IntStream.range(1, 10).mapToObj(this::createItem) .forEach(treeRoot.getChildren()::add); return treeRoot ; } private TreeItem createItem(int value) { TreeItem 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() { { 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); } } 

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

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

在这一点上它开始看起来像一个黑客……

如果要完全禁用选择,则另一个选项可能是为树创建自定义选择模型,该模型始终返回空选择。