You are committing the sin of premature optimization :).
TreeCell
s 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 TreeCell
s are reused to display different TreeItem
s. 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 TreeCell
s 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 TreeCell
s 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.