Search code examples
javafxtreetreeview

Create a tree with a label and a checkbox, detect whether its parent is checked


I want to create a treeview whose item has a lable and a checkbox on the left. I try to write as below,but when I click the button,it prints null.If I can get the graphic not null,I can get the checkbox in it. My purpose is to konw whether the checkbox of the parent item is checked.

package com.qy.tth.fxgui;



import javafx.application.Application;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class TreeCheck extends Application{
    public static void main(String[] args) {
        launch(null);
    }

    private TreeItem<String> item11;

    @Override
    public void start(Stage stage) throws Exception {
        TreeItem<String> item1=new TreeItem<>("1");
        TreeItem<String> item2=new TreeItem<>("2");
        item11=new TreeItem<>("1-1");
        TreeItem<String> itemRoot=new TreeItem<>("root");
        item1.getChildren().add(item11);
        itemRoot.getChildren().addAll(item1,item2);
        TreeView<String> tv=new TreeView<>();
        tv.setRoot(itemRoot);
        tv.setCellFactory(tv1 -> new TreeCell<String>() {
            private HBox hb;
            {
                Label lable = new Label("icon");
                CheckBox cb=new CheckBox();
                hb=new HBox();
                hb.getChildren().addAll(lable,cb);
                setGraphic(hb);
            }
            protected void updateItem(String value, boolean empty) {
                super.updateItem(value, empty);
                if (empty || value == null) {
                    setText("");
                    hb.setVisible(false);
                }else{
                    setText(value);
                     hb.setVisible(true);
                }
            };
        });
        Button btn=new Button("show parent");
        btn.setOnAction(e->showParent());
        VBox vb=new VBox();
        vb.getChildren().addAll(btn,tv);
        Scene scene=new Scene(vb);
        stage.setScene(scene);
        stage.show();
    }

    private void showParent() {
        TreeItem<String> item1 = item11.getParent();
        Node graph = item1.getGraphic();
        System.out.println(graph);
    }
}

And I am not sure is it the best way to write like this,or you can give you own code completely. My intent is just create a tree with a label and checkbox,then detect whether its parent is checked


Solution

  • The reason your code is printing null, I believe, is because there is no graphic set on the TreeItem. You set the graphic (an HBox) on the TreeCell and then later try to retrieve that graphic from the TreeItem.

    That being said, if you are okay with the Label being to the right of the CheckBox there is no need to create your own TreeCell implementation. Instead, you can use the built in classes for this: CheckBoxTreeCell and CheckBoxTreeItem.

    To set up the TreeView you'd use the following code:

    TreeView<String> tree = new TreeView<>();
    tree.setCellFactory(CheckBoxTreeCell.forTreeView());
    

    The static method CheckBoxTreeCell.forTreeView() assumes that the root TreeItem and all descendants will be instances of CheckBoxTreeItem. Be default, the behavior of CheckBoxTreeCell is:

    • If no children are selected then the parent is not selected.
    • If some, but not all, children are selected then the parent is indeterminate.
    • If all children are selected then the parent is selected.

    If you don't want this behavior you should set the independent property to true. This property states:

    A BooleanProperty used to represent the independent state of this CheckBoxTreeItem. The independent state is used to represent whether changes to a single CheckBoxTreeItem should influence the state of its parent and children.

    By default, the independent property is false, which means that when a CheckBoxTreeItem has state changes to the selected or indeterminate properties, the state of related CheckBoxTreeItems will possibly be changed. If the independent property is set to true, the state of related CheckBoxTreeItems will never change.


    Setting your TreeView up this way makes it easy to determine if a parent is selected because CheckBoxTreeItem has a BooleanProperty named selected.

    CheckBoxTreeItem<?> parent = (CheckBoxTreeItem<?>) item.getParent();
    System.out.println(parent.isSelected());
    

    In this case, casting it okay because we know all the TreeItems will be instances of CheckBoxTreeItem.


    Since you also want a Label as a part of the TreeCell you can simply set the graphic of the CheckBoxTreeItem to a Label with the desired text. Note, however, that this will put the Label to the right of the actual check box. If, as indicated in your code, you want the Label to the left of the CheckBox this gets a little more involved.

    Internally, a CheckBoxTreeCell takes the graphic of the TreeItem and sets it on the internal CheckBox; then it sets the CheckBox as the graphic of itself. Due to the way CheckBox is designed it is not simple (probably not even possible) to put the graphic/text of the CheckBox on the other side of the actual check box (the visual box with the check). If you want to have the TreeItems graphic be on the left side of the CheckBox you have to use something like an HBox (as you were doing in your code). This can be done simpler than your attempt if you extend CheckBoxTreeCell rather than TreeCell directly.

    import javafx.beans.InvalidationListener;
    import javafx.beans.WeakInvalidationListener;
    import javafx.geometry.Pos;
    import javafx.scene.control.CheckBox;
    import javafx.scene.control.CheckBoxTreeItem;
    import javafx.scene.control.TreeCell;
    import javafx.scene.control.TreeView;
    import javafx.scene.control.cell.CheckBoxTreeCell;
    import javafx.scene.layout.HBox;
    import javafx.util.Callback;
    
    public class CustomCheckBoxTreeCell<T> extends CheckBoxTreeCell<T> {
    
        public static <T> Callback<TreeView<T>, TreeCell<T>> forTreeView() {
            return treeView -> new CustomCheckBoxTreeCell<>();
        }
    
        private final InvalidationListener graphicListener = (obs) -> updateItem(getItem(), isEmpty());
        private final WeakInvalidationListener weakGraphicListener = new WeakInvalidationListener(graphicListener);
    
        public CustomCheckBoxTreeCell() {
            // This again assumes that all TreeItems will actually be
            // instances of CheckBoxTreeItem
            super(item -> ((CheckBoxTreeItem<?>) item).selectedProperty());
            treeItemProperty().addListener((observable, oldItem, newItem) -> {
                if (oldItem != null) {
                    oldItem.graphicProperty().removeListener(weakGraphicListener);
                }
                if (newItem != null) {
                    newItem.graphicProperty().addListener(weakGraphicListener);
                }
            });
        }
    
        @Override
        public void updateItem(T item, boolean empty) {
            super.updateItem(item, empty);
            if (!empty && getTreeItem().getGraphic() != null) {
                CheckBox cBox = (CheckBox) getGraphic();
                cBox.setGraphic(null);
    
                HBox hBox = new HBox(getTreeItem().getGraphic(), cBox);
                hBox.setAlignment(Pos.CENTER_LEFT);
    
                setGraphic(hBox);
            }
        }
    
    }
    

    You would then set the cell factory like so:

    tree.setCellFactory(CustomCheckBoxTreeItem.forTreeView());
    

    A few notes:

    1. This relies on how CheckBoxTreeCell is implemented internally (specifically Java 10, but probably works with Java 8 as well). If they change the implementation in the future this could break.

    2. This requires you to set the Label as the TreeItems graphic rather than using it in the TreeCell only. If you don't want to do this, then remove the behavior that uses the TreeItems graphic and replace it with an internal Label. This would allow you to remove the use of graphicListener and weakGraphicListener. If you want to display the graphic of the TreeItem as well then you would also remove cBox.setGraphic(null).

    3. I make no attempt to cache the HBox (or the possible Label if you change things based on Note #2). Instead, I just create a new HBox every time. It should be simple to change the code so it does cache the HBox, however, if you so choose.


    If you don't want to use CheckBoxTreeItem but would rather externalize whether or not an item is selected, you should look at these methods:

    If you still want the graphic on the left of the CheckBox you'll have to extend CheckBoxTreeCell here as well. The only real difference to how CustomCheckBoxTreeCell is implemented above is you'd have to provide a way to set the Callback you would have used with the above two methods to the CustomCheckBoxTreeCell. You can do this by exposing a constructor that takes the Callback or by setting the selectedStateCallback property inside the cell factory.