Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
626 views
in Technique[技术] by (71.8m points)

java - JavaFX: how to handle dragging an item from a TreeView

For an application I'm developing I have a TreeView with (my own type of) TreeItems. This is working fine, and I get the items to display as expected.

I now want to be able to handle dragging an item from this TreeView to another part of the app window and have it perform some action there. I am now faced with two (at least…) issues:

  1. Whenever you click in the TreeView, the item is always selected. Can this be prevented?
  2. When adding a MouseEvent listener on the TreeView, I get the events with which I would be able to detect dragging and respond to that. I have, however not been able to determine the corresponding TreeItem for the mouse event. I need to know the exact TreeItem, of course, for the drag to work. Is this possible?

Some things I have tried:

  1. I added my own cell factory and even when handling and consuming all mouse events on a cell, the item in the tree is still selected???
  2. If I add a MouseEvent Handler to each and every cell, I will be able to manage the drag and drop, but given there could be thousands (potentially >> 100,000, not all expanded tough) of rows in the TreeView, isn't this a tremendous overhead and would it not be better to have just one event handler for the TreeView? (but then, how do I determine the corresponding TreeItem?)

The TreeView mouse events give me the following info:

No cell clicked: 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]

Cell with text "Attributes" clicked: 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]

I guess the secret is somewhere in the Node of the PickResult, but from there I'm still unable to see how to get to the TreeItem.

Hope there is an (easy) answer to this...

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

You are committing the sin of premature optimization :).

TreeCells are essentially only created for the currently visible items in a TreeView. When you expand or collapse nodes in the tree, or when you scroll, those TreeCells are reused to display different TreeItems. This is the purpose of the updateItem(...) and similar methods in TreeCell; they are called when the item displayed by that TreeCell instance changes.

A TreeCell on my system is about 1/4 inch high; to display 100,000 TreeCells would take a monitor more than 2,000 feet / 630 meters tall. At that point, you probably have more serious memory allocation issues than some extra listeners.... But at any rate, a listener would only be invoked if an event occurs on that particular cell, and occupies a fairly small footprint in comparison to the cell itself, so unless you have any direct evidence registering listeners on the cells (which as you've observed, massively reduces your code complexity) adversely affects performance, you should use the "listener per cell" approach.

Here is an example of a tree that holds 1,000,000 Integer-valued tree items. It tracks the number of TreeCells created (on my system it never seems to exceed 20 with the window size I set). It also displays a label; you can drag the values from the tree to the label and the label will display a running total of the values dropped there.

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

For the selection issue: I would question why you want to do this; it would create an unusual user experience. The issue is probably that the "baked-in" event handlers which manage selection are being invoked before the handlers you define, so by the time you consume the event, selection has already been changed. You can try adding an event filter instead:

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

but this will also disable expanding/collapsing the nodes in the tree.

So you can try something like:

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

at which point it starts to look like something of a hack...

If you want to entirely disable selection, another option might be to create a custom selection model for the tree which just always returns an empty selection.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...