I am trying to implement a search function for a TreeView
in JavaFX. I want to highlight all the matches when the user hits the enter key. So I added a boolean isHighlighted
to my TreeItem
and in my TreeCell
s updateItem
, I check whether the item isHighlighted
and if so I apply a certain CSS. Everything works fine with the items/cells not visible at the moment of the search -- when I scroll to them, they are properly highlighted. The problem is: How can I "repaint" the TreeCells visible at search so that they reflect whether their item isHighlighted
? My Controller does currently not have any reference to the TreeCells
the TreeView
creates.

- 625
- 1
- 7
- 20
1 Answers
This answer is based on this one, but adapted for TreeView
instead of TableView
, and updated to use JavaFX 8 functionality (greatly reducing the amount of code required).
One strategy for this is to maintain an ObservableSet
of TreeItems
that match the search (this is sometimes useful for other functionality you may want anyway). Use a CSS PseudoClass
and an external CSS file to highlight the required cells. You can create a BooleanBinding
in the cell factory that binds to the cell's treeItemProperty
and the ObservableSet
, evaluating to true
if the set contains the cell's current tree item. Then just register a listener with the binding and update the pseudoclass state of the cell when it changes.
Here's a SSCCE. It contains a tree whose items are Integer
-valued. It will update the search when you type in the search box, matching those whose value is a multiple of the value entered.
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class TreeWithSearchAndHighlight extends Application {
@Override
public void start(Stage primaryStage) {
TreeView<Integer> tree = new TreeView<>(createRandomTree(100));
// keep track of items that match our search:
ObservableSet<TreeItem<Integer>> searchMatches = FXCollections.observableSet(new HashSet<>());
// cell factory returns an instance of TreeCell implementation defined below.
// pass the cell implementation a reference to the set of search matches
tree.setCellFactory(tv -> new SearchHighlightingTreeCell(searchMatches));
// search text field:
TextField textField = new TextField();
// allow only numeric input:
textField.setTextFormatter(new TextFormatter<Integer>(change ->
change.getControlNewText().matches("\\d*")
? change
: null));
// when the text changes, update the search matches:
textField.textProperty().addListener((obs, oldText, newText) -> {
// clear search:
searchMatches.clear();
// if no text, or 0, just exit:
if (newText.isEmpty()) {
return ;
}
int searchValue = Integer.parseInt(newText);
if (searchValue == 0) {
return ;
}
// search for matching nodes and put them in searchMatches:
Set<TreeItem<Integer>> matches = new HashSet<>();
searchMatchingItems(tree.getRoot(), matches, searchValue);
searchMatches.addAll(matches);
});
BorderPane root = new BorderPane(tree, textField, null, null, null);
BorderPane.setMargin(textField, new Insets(5));
BorderPane.setMargin(tree, new Insets(5));
Scene scene = new Scene(root, 600, 600);
// stylesheet sets style for cells matching search by using the selector
// .tree-cell:search-match
// (specified in the initalization of the Pseudoclass at the top of the code)
scene.getStylesheets().add("tree-highlight-search.css");
primaryStage.setScene(scene);
primaryStage.show();
}
// find all tree items whose value is a multiple of the search value:
private void searchMatchingItems(TreeItem<Integer> searchNode, Set<TreeItem<Integer>> matches, int searchValue) {
if (searchNode.getValue() % searchValue == 0) {
matches.add(searchNode);
}
for (TreeItem<Integer> child : searchNode.getChildren()) {
searchMatchingItems(child, matches, searchValue);
}
}
// build a random tree with numNodes nodes (all nodes expanded):
private TreeItem<Integer> createRandomTree(int numNodes) {
List<TreeItem<Integer>> items = new ArrayList<>();
TreeItem<Integer> root = new TreeItem<>(1);
root.setExpanded(true);
items.add(root);
Random rng = new Random();
for (int i = 2 ; i <= numNodes ; i++) {
TreeItem<Integer> item = new TreeItem<>(i);
item.setExpanded(true);
TreeItem<Integer> parent = items.get(rng.nextInt(items.size()));
parent.getChildren().add(item);
items.add(item);
}
return root ;
}
public static class SearchHighlightingTreeCell extends TreeCell<Integer> {
// must keep reference to binding to prevent premature garbage collection:
private BooleanBinding matchesSearch ;
public SearchHighlightingTreeCell(ObservableSet<TreeItem<Integer>> searchMatches) {
// pseudoclass for highlighting state
// css can set style with selector
// .tree-cell:search-match { ... }
PseudoClass searchMatch = PseudoClass.getPseudoClass("search-match");
// initialize binding. Evaluates to true if searchMatches
// contains the current treeItem
// note the binding observes both the treeItemProperty and searchMatches,
// so it updates if either one changes:
matchesSearch = Bindings.createBooleanBinding(() ->
searchMatches.contains(getTreeItem()),
treeItemProperty(), searchMatches);
// update the pseudoclass state if the binding value changes:
matchesSearch.addListener((obs, didMatchSearch, nowMatchesSearch) ->
pseudoClassStateChanged(searchMatch, nowMatchesSearch));
}
// update the text when the item displayed changes:
@Override
protected void updateItem(Integer item, boolean empty) {
super.updateItem(item, empty);
setText(empty ? null : "Item "+item);
}
}
public static void main(String[] args) {
launch(args);
}
}
The CSS file tree-highlight-search.css just has to contain a style for the highlighted cells:
.tree-cell:search-match {
-fx-background: yellow ;
}
-
This works almost perfect. I had to replace a EventHandler
so that it will highlight TreeCells: `Platform.runLater(() -> { EventHandler super ScrollEvent> e = myTreeView.getChildrenUnmodifiable().get(0).getOnScroll(); myTreeView.getChildrenUnmodifiable().get(0).setOnScroll((event) -> { e.handle(event); search(s.SearchTextField.getText()); } }); });` – spilot Jan 22 '16 at 00:20 -
Anyway, thank you very much. Your solution not only does work very fine, but is also elegant. – spilot Jan 22 '16 at 00:23
-
I really don't understand why you need that additional hack, or what it really does. As long as you update the `ObservableSet` whenever you do a new search, the tree will automatically update. – James_D Jan 22 '16 at 00:39
-
In my implementation it wont. When the user scrolls, items that should match the current search term are not highlighted. So, I trigger search every time the user scrolls. – spilot Jan 22 '16 at 01:04
-
That makes no sense. Why would doing the search when the user scrolls update the cells, but doing the search when the user changes the text not update the cells? You have something else wrong somewhere (either you are not triggering the search, or your cell implementation is not binding properly to the `ObservableSet`.) – James_D Jan 22 '16 at 01:08
-
What makes you think the cells do not update when the User changes the text? They do. Changing the text results in the visible cells with appropriate content to be highlit. – spilot Jan 22 '16 at 02:01
-
When the user scrolls it the cells should update their highlight status when the items they show change, without the hack with the scroll listener. The point is that the `matchesSearch` binding observes the cell's `TreeItemProperty`, so when the new tree item is displayed in the cell, it should update. – James_D Jan 22 '16 at 02:48
-
It does not. The code in my `TreeCell`s Constructor looks like this: `matchesSearch = Bindings.createBooleanBinding(() -> searchMatches.contains(treeItemProperty(), searchMatches); matchesSearch.addListener((obs, didMatchSearch, nowMatchesSearch) -> { ObservableList
l = getStyleClass(); String s = myTreeStyle.CSS_NODE_HIGHLIGHTED; if (nowMatchesSearch) { if (!l.contains(s)) l.add(s); } else l.remove(s);` – spilot Jan 22 '16 at 08:56 -
Your `matchesSearch` binding looks wrong (maybe you mistyped it here, or maybe you have it wrong). The first argument to `createBooleanBinding` is the function which evaluates the boolean. Subsequent arguments are observables: if any of those observables changes, then this boolean binding becomes invalid and must be recomputed. The two things that can change that mean this needs recomputing are the set of search matches, or the tree item the cell is displaying. It looks like you do not have `treeItemProperty()` in that list of observables, so it won't update when the cell changes item. – James_D Jan 22 '16 at 13:31
-
Also note you are testing whether the set contains the `ObjectProperty
>` (`treeItemProperty()`), not the actual `TreeItem<...>` (`getTreeItem()`). The object returned by `treeItemProperty()` is always the same object; the object returned by `getTreeItem()` represents the actual `TreeItem` displayed in the cell, which will change when you scroll etc. Finally: does your highlighting work if you expand/collapse nodes in the tree? I would imagine you scroll listener will not be fired by that... – James_D Jan 22 '16 at 13:35 -
You're right, I mistyped it here. It actually looks like this: `matchesSearch = Bindings.createBooleanBinding(() -> searchMatches.contains(getTreeItem()), treeItemProperty(), searchMatches); ` – spilot Jan 23 '16 at 18:49
-
There was a bug in legacy code. Your solution is 100% correct. – spilot Jan 24 '16 at 10:10