8

I am would like to create elements looking and behaving as the one shown below.

enter image description here

There is a dark background and top list of 4 elements: "biblioteki", "Analiza" and so on. When we click on one of them, the list is expanded and this item and its children gets light dark backgorund. Additionally, the selected item from the child list gets a different font (bold and white). There can be only one item expanded at any time.

So I figure out that this is TreeView behaviour with proper styles applied. I get it working with the following code:

    TreeView<TabMenuElement> treeView = new TreeView<>(treeRoot);
    treeView.setCellFactory(tv -> new TreeCell<TabMenuElement>() {
        @Override
        public void updateItem(TabMenuElement item, boolean empty) {
            super.updateItem(item, empty);
            setDisclosureNode(null);

            if (empty) {
                setText("");
                setGraphic(null);
            } else {
                setText(item.getName()); // appropriate text for item
                if (item.getIv() != null) {
                    setGraphic(item.getIv());
                }
            }
        }
    });
    treeView.setShowRoot(false);

TabMenuElement has method getIV to obtain ImageView (icon) if it is defined for this element and getName to obtain text to be displayed. For now it looks like this

enter image description here

So I have following problems:

  1. how to change font only on selected item in TreeView?
  2. how to setup background on selected sub-tree?
  3. how to force that at most one subtree could be expanded
  4. how to set bigger size to top level items?

2 Answers 2

18

how to change font only on selected item in TreeView?

In your css file, just define the font for .tree-cell:selected:

.tree-cell:selected {
    -fx-font-weight: bold ;
}

how to setup background on selected sub-tree?

This one's a little tricky. You want the background of any expanded node, and any node whose parent is not the root, to be different (that's not how you phrased it but I think it is logically equivalent). An expanded node has a CSS pseudoclass already. For the "parent is not the root", you need to define your own pseudoclass:

PseudoClass subElementPseudoClass = PseudoClass.getPseudoClass("sub-tree-item");

Now observe the treeItem property of the cell, and update the pseudoclass state when it changes:

treeView.setCellFactory(tv -> {
    TreeCell<TabMenuElement> cell = new TreeCell<TabMenuElement>() {
        @Override
        public void updateItem(TabMenuElement item, boolean empty) {
            super.updateItem(item, empty);
            setDisclosureNode(null);

            if (empty) {
                setText("");
                setGraphic(null);
            } else {
                setText(item.getName()); // appropriate text for item
                if (item.getIv() != null) {
                    setGraphic(item.getIv());
                }
            }
        }
    };

    cell.treeItemProperty().addListener((obs, oldTreeItem, newTreeItem) -> {
        cell.pseudoClassStateChanged(subElementPseudoClass,
            newTreeItem != null && newTreeItem.getParent() != cell.getTreeView().getRoot());
    }

    return cell ;
});

Now in your CSS file you can do

.tree-cell:expanded, .tree-cell:sub-tree-item {
    -fx-background-color: ... ;
}

how to force that at most one subtree could be expanded

Add the following ChangeListener to each of your TreeItem's expanded properties:

ChangeListener<Boolean> expandedListener = (obs, wasExpanded, isNowExpanded) -> {
    if (isNowExpanded) {
        ReadOnlyProperty<?> expandedProperty = (ReadOnlyProperty<?>) obs ;
        Object itemThatWasJustExpanded = expandedProperty.getBean();
        for (TreeItem<TabMenuElement> item : treeView.getRoot().getChildren()) {
            if (item != itemThatWasJustExpanded) {
                item.setExpanded(false);
            }
        }
    }
};

TreeItem<TabMenuElement> biblioteka = new TreeItem<>(...);
biblioteka.expandedProperty().addListener(expandedListener);
TreeItem<TabMenuElement> analiza = new TreeItem<>(...);
analiza.expandedProperty().addListener(expandedListener);
// etc, for all "top-level" items.

how to set bigger size to top level items?

.tree-cell {
    -fx-padding: 0.75em 0em 0.75em 0em ;
}
.tree-cell:sub-tree-item {
    -fx-padding: 0.25em ;
}

(Or change the font size, or similar.)

Here is a complete example:

import javafx.application.Application;
import javafx.beans.property.ReadOnlyProperty;
import javafx.beans.value.ChangeListener;
import javafx.css.PseudoClass;
import javafx.scene.Scene;
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 StyledUniqueExpandingTree extends Application {

    @Override
    public void start(Stage primaryStage) {
        TreeView<String> tree = new TreeView<>();
        tree.setShowRoot(false);
        TreeItem<String> root = new TreeItem<>("");
        tree.setRoot(root);

        ChangeListener<Boolean> expandedListener = (obs, wasExpanded, isNowExpanded) -> {
            if (isNowExpanded) {
                ReadOnlyProperty<?> expandedProperty = (ReadOnlyProperty<?>) obs ;
                Object itemThatWasJustExpanded = expandedProperty.getBean();
                for (TreeItem<String> item : tree.getRoot().getChildren()) {
                    if (item != itemThatWasJustExpanded) {
                        item.setExpanded(false);
                    }
                }
            }
        };

        for (int i=1; i<=4; i++) {
            TreeItem<String> item = new TreeItem<>("Top level "+i);
            item.expandedProperty().addListener(expandedListener);
            root.getChildren().add(item);
            for (int j=1; j<=4; j++) {
                TreeItem<String> subItem = new TreeItem<>("Sub item "+i+":"+j);
                item.getChildren().add(subItem);
            }
        }

        PseudoClass subElementPseudoClass = PseudoClass.getPseudoClass("sub-tree-item");

        tree.setCellFactory(tv -> {
            TreeCell<String> cell = new TreeCell<String>() {
                @Override
                public void updateItem(String item, boolean empty) {
                    super.updateItem(item, empty);
                    setDisclosureNode(null);

                    if (empty) {
                        setText("");
                        setGraphic(null);
                    } else {
                        setText(item); // appropriate text for item
                    }
                }

            };
            cell.treeItemProperty().addListener((obs, oldTreeItem, newTreeItem) -> {
                cell.pseudoClassStateChanged(subElementPseudoClass,
                        newTreeItem != null && newTreeItem.getParent() != cell.getTreeView().getRoot());
            });
            return cell ;
        });

        BorderPane uiRoot = new BorderPane(tree);
        Scene scene = new Scene(uiRoot, 250, 400);
        scene.getStylesheets().add("styled-unique-expanded-tree.css");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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

with styled-unique-expanded-tree.css:

.tree-view, .tree-cell {
    -fx-background-color: black ;
    -fx-text-fill: white ;
}
.tree-cell:expanded, .tree-cell:sub-tree-item {
    -fx-background-color: #404040 ;
}
.tree-cell:selected {
    -fx-font-weight: bold ;
}
.tree-cell {
    -fx-padding: 0.75em 0em 0.75em 0em ;
}
.tree-cell:sub-tree-item {
    -fx-padding: 0.25em ;
}
Sign up to request clarification or add additional context in comments.

2 Comments

This work very good. Thanks a lot! Yet I have two more questions: 1. I would like the subtree to be expanded on single click (for now it works only on double click) 2. I have icons only for top level elements but I want labels on all levels to be aligned to single line. (for now sub levels are shifted right). Please advise.
I tested your example and it is causing strange problem Note for test I just copied 1:1 your *.java and *.css files. In my case problem is visible almost every time in case of your app I have to retry it few times and it is not always visible. Here are steps to reproduce: 1. Start App 2. Expand lowest subtree 3. increase size of window vertically (up to screen height) 4. reduce size of the window vertically to initial size 5. Again increase size of window vertically (up to screen height). 6. Now try few times expand 3toplevel and again 4toplevel - result is lighter bar show below tree view
1

I figured by myself how to expand on single click treeview:

    treeView.setCellFactory(tv -> new TreeCell<TabMenuElement>() {
        @Override
        public void updateItem(TabMenuElement item, boolean empty) {
            super.updateItem(item, empty);
            setDisclosureNode(null);

            if (empty) {
                setText("");
                setGraphic(null);
            } else {
                setText(item.getName()); // appropriate text for item
                setOnMouseClicked(event -> {
                    if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 1) {
                        TreeItem<TabMenuElement> ti = treeItemProperty().get();
                        ti.expandedProperty().set(true);
                    }
                    event.consume();
                });

                if (item.getIv() != null) {
                    setGraphic(item.getIv());
                } else {

                }
            }
        }
    });

specially the part with setOnMouseClicked.

Also having the same indent for all nodes could be done in this case by setting in

.tree-cell:sub-tree-item{
     -fx-indent: 22;
}

Well this is for my case where icons for top level element have width 22. Then I have only one sub level and there I set the same ident. If you want to have just in whole tree the same indent without icsons it is enough to set indet to 0 for all .tree-cell.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.