Javafx:如何在 TreeTableView 中实现三态复选框

Javafx: How to implement 3-state checkboxes inside a TreeTableView

我正在尝试在 javafx 中实现 TreeTableView,其中第一列包含字符串值,第三列将呈现为三态复选框。 使用以下 MCVE,我可以获得一棵树 table,但是复选框中的 none 选项仍然存在 parent collapse/expand 或 table 的调整大小.
MCVE
Class A 是 parent.
Class B 扩展了 A 并且是 child.
Class C 表示第 2 列,(呈现为复选框)
Class MVCECheckBox 构建树table 并显示它。

A.java

package mcve.checkbox;

import java.util.List;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

/**
 *
 * @author returncode13
 */
public class A {
    StringProperty name=new SimpleStringProperty();
    C check=new C();
    List<A> children;

    public StringProperty getName() {
        return name;
    }

     public void setName(String name) {
        this.name.set(name);
    }

    public C getCheck() {
        return check;
    }

    public void setCheck(C check) {
        this.check = check;
    }

    public StringProperty nameProperty(){
        return name;
    }

    public List<A> getChildren() {
        return children;
    }

    public void setChildren(List<A> children) {
        this.children = children;
    }


    }

B.java

package mcve.checkbox;

import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

/**
 *
 * @author returncode13
 */
public class B extends A{
    StringProperty name=new SimpleStringProperty();
    C Check=new C();

    @Override
    public StringProperty getName() {
        return name;
    }

    @Override
    public void setName(String name) {
        this.name.set(name);
    }

    @Override
    public C getCheck() {
        return Check;
    }

    @Override
    public void setCheck(C Check) {
        this.Check = Check;
    }

    @Override
     public StringProperty nameProperty(){
        return name;
    }

}

C.java

package mcve.checkbox;

import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;

/**
 *
 * @author returncode13
 */
public class C {
    BooleanProperty checkUncheck=new SimpleBooleanProperty();
    BooleanProperty indeterminate=new SimpleBooleanProperty();

    public BooleanProperty getCheckUncheck() {
        return checkUncheck;
    }

    public void setCheckUncheck(BooleanProperty checkUncheck) {
        this.checkUncheck = checkUncheck;
    }

    public BooleanProperty getIndeterminate() {
        return indeterminate;
    }

    public void setIndeterminate(BooleanProperty indeterminate) {
        this.indeterminate = indeterminate;
    }

    public BooleanProperty checkUncheckProperty(){
        return checkUncheck;
    }

    public BooleanProperty indeterminateProperty(){
        return indeterminate;
    }
} 

MCVECheckBox.java

package mcve.checkbox;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableCell;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import javafx.scene.control.cell.TreeItemPropertyValueFactory;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.util.Callback;

/**
 *
 * @author returncode13
 */
public class MCVECheckBox extends Application {
    A selectedItem;
    private TreeTableView<A> treetable=new TreeTableView<>();
    @Override
    public void start(Stage primaryStage) {
       //setting up parents (A) and children (B)
        A a1=new A();                       
        a1.setName("A1");

        List<A> A1Children=new ArrayList();
        B b11=new B();
        b11.setName("B11");
        B b12=new B();
        b12.setName("B12");
        A1Children.add(b11);
        A1Children.add(b12);
        a1.setChildren(A1Children);


        A a2=new A();
        a2.setName("A2");

        List<A> A2Children=new ArrayList();
        B b21=new B();
        b21.setName("B21");
        B b22=new B();
        b22.setName("B22");
        A2Children.add(b21);
        A2Children.add(b22);
        a2.setChildren(A2Children);



        //tree columns . first one holds strings
        TreeTableColumn<A,String> name=new TreeTableColumn<>("Name");
        name.setCellValueFactory(new TreeItemPropertyValueFactory<>("name"));


        //2nd tree columns. rendered as checkboxes. boolean values
        TreeTableColumn<A,Boolean> checks=new TreeTableColumn<>("Checks");
        checks.setCellValueFactory(new Callback<TreeTableColumn.CellDataFeatures<A, Boolean>, ObservableValue<Boolean>>() {
            @Override
            public ObservableValue<Boolean> call(TreeTableColumn.CellDataFeatures<A, Boolean> param) {
                A a=param.getValue().getValue();
                SimpleBooleanProperty checkUncheck=new SimpleBooleanProperty();
                SimpleBooleanProperty indeterminate=new SimpleBooleanProperty();

                checkUncheck=(SimpleBooleanProperty) a.getCheck().getCheckUncheck();
                indeterminate=(SimpleBooleanProperty) a.getCheck().getIndeterminate();

                //to do: set parents status based on children status.


                if(indeterminate.get()){
                    return indeterminate;
                }else{
                    return checkUncheck;
                }

            }
        });


        checks.setCellFactory(new Callback<TreeTableColumn<A, Boolean>, TreeTableCell<A, Boolean>>() {
            @Override
            public TreeTableCell<A, Boolean> call(TreeTableColumn<A, Boolean> param) {
                return new CheckBoxCell();
            }
        });


        //building the tree;
        TreeItem<A> a1item=new TreeItem<>(a1);
        TreeItem<A> b11item=new TreeItem<>(b11);
        TreeItem<A> b12item=new TreeItem<>(b12);
        a1item.getChildren().add(b11item);
        a1item.getChildren().add(b12item);

        TreeItem<A> a2item=new TreeItem<>(a2);
        TreeItem<A> b21item=new TreeItem<>(b21);
        TreeItem<A> b22item=new TreeItem<>(b22);
        a2item.getChildren().add(b21item);
        a2item.getChildren().add(b22item);




        TreeItem<A> root=new TreeItem<>();
        root.getChildren().add(a1item);
        root.getChildren().add(a2item);

        treetable.getColumns().addAll(name,checks);

        treetable.setRoot(root);
        treetable.setShowRoot(false);
        treetable.setEditable(true);


      //  StackPane rootSp = new StackPane();
        Scene scene = new Scene(treetable, 300, 250);

        primaryStage.setTitle("Hello World!");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        launch(args);
    }


    //to render checkboxes in treetable


    private  class CheckBoxCell extends TreeTableCell<A, Boolean> {


        CheckBox checkbox;

        public CheckBoxCell() {
            checkbox=new CheckBox();
            checkbox.setAllowIndeterminate(true);

            checkbox.selectedProperty().addListener((obs,wasSelected,isNowSelected) -> {
            if(isNowSelected){
               selectedItem=getTreeTableRow().getItem();
            }
            });

        }


        @Override
        public void updateItem(Boolean b,boolean empty){
            super.updateItem(b, empty);

            if(empty){
                setGraphic(null);
            }else{
                checkbox.setSelected(b);
                setGraphic(checkbox);
            }
        }
    }

}

我之前曾尝试使用 CheckTreeTableCell 在第二列设置单元格工厂,但很快发现 CheckTreeTableCell 不支持三态(选中、取消选中、不确定)复选框。 之后我尝试实现上面的代码。尽管我能够引入三态复选框,但我无法让它们的状态持续存在。每次 parent 为 collapsed/expanded 时,对其 children 进行的检查将被取消选择。
感谢您在确定修复方面提供的任何帮助。

我现在可以通过对发布的 MCVE 进行以下修改来实现三态复选框,它现在是一个完整的工作示例。

A.java (parent class)

package com.mycompany.yetanothercheckbox;

import java.util.Iterator;
import java.util.List;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

/**
 *
 * @author returncode13
 */
public class A {
    private StringProperty name=new SimpleStringProperty();
    C check=new C();
    List<A> children;
    final boolean isLeaf=false;
    final boolean isParent=true;
    public boolean updateParent=false;
    public boolean updateChildren=false;
    public boolean isLeaf() {
        return isLeaf;
    }

    public boolean isParent() {
        return isParent;
    }
    public StringProperty getName() {
        return name;
    }

     public void setName(String name) {
        this.name.set(name);
    }

    public C getCheck() {
        return check;
    }

    public void setCheck(C check) {
        this.check = check;
        for (Iterator<A> iterator = children.iterator(); iterator.hasNext();) {
            A next = iterator.next();
            next.setCheck(check);

        }
    }

    public StringProperty nameProperty(){
        return name;
    }

    public List<A> getChildren() {
        return children;
    }

    public void setChildren(List<A> children) {
        this.children = children;
    }

    public A getParent() {
        return this;
    }
}

B.java (child class)

package com.mycompany.yetanothercheckbox;

import java.util.List;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

/**
 *
 * @author returncode13
 */
public class B extends A{
    private StringProperty name=new SimpleStringProperty();
    C Check=new C();
    final boolean isLeaf=true;
    final boolean isParent=false;
     public boolean updateParent=false;
    public boolean updateChildren=false;
    A parent;

    public A getParent() {
        return parent;
    }

    public void setParent(A parent) {
        this.parent = parent;
    }


    @Override
    public boolean isLeaf() {
        return isLeaf;
    }

    @Override
    public boolean isParent() {
        return isParent;
    }



    @Override
    public StringProperty getName() {
        return name;
    }

    @Override
    public void setName(String name) {
        this.name.set(name);
    }

    @Override
    public C getCheck() {
        return Check;
    }

    @Override
    public void setCheck(C Check) {
        this.Check = Check;
    }

    @Override
     public StringProperty nameProperty(){
        return name;
    }

    @Override
       public List<A> getChildren() {
        return parent.getChildren();
    }

}

C.java(保持检查状态)

package com.mycompany.yetanothercheckbox;

import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;

/**
 *
 * @author returncode13
 */
public class C {
    BooleanProperty checkUncheck=new SimpleBooleanProperty();
    BooleanProperty indeterminate=new SimpleBooleanProperty();

    public BooleanProperty getCheckUncheck() {
        return checkUncheck;
    }

    public void setCheckUncheck(BooleanProperty checkUncheck) {
        this.checkUncheck = checkUncheck;
    }

    public BooleanProperty getIndeterminate() {
        return indeterminate;
    }

    public void setIndeterminate(BooleanProperty indeterminate) {
        this.indeterminate = indeterminate;
    }

    public BooleanProperty checkUncheckProperty(){
        return checkUncheck;
    }

    public BooleanProperty indeterminateProperty(){
        return indeterminate;
    }
}

ThreeStateCheckBoxTreeTableCell.java(树的 3state 复选框 table)

package com.mycompany.yetanothercheckbox;

import java.util.List;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler;
import javafx.scene.control.CheckBox;
import javafx.scene.control.TreeTableCell;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.input.MouseEvent;

/**
 *
 * @author returncode13
 */
   //to render checkboxes in treetable


    public  class ThreeStateCheckBoxTreeTableCell extends TreeTableCell<A, Boolean> {


        A selectedItem;
        CheckBox checkbox;
        TreeTableColumn<A,Boolean> param;
        /*private static boolean updateParent=false;
        private static boolean updateChildren=false;*/


        public ThreeStateCheckBoxTreeTableCell(TreeTableColumn<A,Boolean> param) {
            checkbox=new CheckBox();
            this.param=param;
            checkbox.setAllowIndeterminate(true);

            checkbox.selectedProperty().addListener((obs,wasSelected,isNowSelected) -> {
               int sel=getTreeTableRow().getIndex();
               selectedItem=this.param.getTreeTableView().getSelectionModel().getModelItem(sel).getValue();
               selectedItem.getCheck().getCheckUncheck().set(isNowSelected);
               selectedItem.getCheck().getIndeterminate().set(false); 

              //ThreeStateCheckBoxTreeTableCell.this.param.getTreeTableView().refresh();


            });


            checkbox.indeterminateProperty().addListener((obx,ol,newV)->{

               int sel=getTreeTableRow().getIndex();
               selectedItem=this.param.getTreeTableView().getSelectionModel().getModelItem(sel).getValue();
                selectedItem.getCheck().getIndeterminate().set(newV);

           //ThreeStateCheckBoxTreeTableCell.this.param.getTreeTableView().refresh();
            });



            checkbox.setOnMouseClicked(new EventHandler<MouseEvent>(){
                @Override
                public void handle(MouseEvent event) {
                    int sel=getTreeTableRow().getIndex();
                    selectedItem=ThreeStateCheckBoxTreeTableCell.this.param.getTreeTableView().getSelectionModel().getModelItem(sel).getValue();
                    if(selectedItem.isParent()){

                        selectedItem.updateChildren=true;
                        for(A child:selectedItem.getChildren()){
                            child.updateParent=false;
                        }


                         updateDownwards();
                    }
                    if(selectedItem.isLeaf()){

                        selectedItem.getParent().updateChildren=false;
                        selectedItem.updateParent=true;

                        updateUpWards();
                    }

                    ThreeStateCheckBoxTreeTableCell.this.param.getTreeTableView().refresh();
                }

            });
        }


        @Override
        public void updateItem(Boolean b,boolean empty){
            super.updateItem(b, empty);

            if(empty){
                setGraphic(null);
            }else{


                if(b==null){
                    checkbox.setIndeterminate(true);

                }
                else{
                    checkbox.setIndeterminate(false);
                    checkbox.setSelected(b);

                }
               setGraphic(checkbox);

            }

            ThreeStateCheckBoxTreeTableCell.this.param.getTreeTableView().refresh();
        }

        private void updateUpWards(){

            if(selectedItem.updateParent){


            List<A> children=selectedItem.getChildren();
            int indeterminateCount=0;
            int selectedCount=0;

                A parent=selectedItem.getParent();
                for(A child:children){
                    indeterminateCount+=child.getCheck().getIndeterminate().get()?1:0;
                    selectedCount+=child.getCheck().getCheckUncheck().get()?1:0;

                }

                if(indeterminateCount>0) {

                     parent.getCheck().getIndeterminate().set(true);
                }
                else if(indeterminateCount==0 && selectedCount==children.size()){
                    parent.getCheck().getIndeterminate().set(false);
                    parent.getCheck().getCheckUncheck().set(true);
                }else{

                    parent.getCheck().getIndeterminate().set(false);
                    parent.getCheck().getCheckUncheck().set(false);
                }

            }
            ThreeStateCheckBoxTreeTableCell.this.param.getTreeTableView().refresh();


        }


        private void updateDownwards(){
             List<A> children=selectedItem.getChildren();

             if(selectedItem.isParent() && selectedItem.updateChildren ){

                 for(A child:children){
                   child.getCheck().getCheckUncheck().set(selectedItem.getCheck().getCheckUncheck().get());

                  child.getCheck().getIndeterminate().set(selectedItem.getCheck().getIndeterminate().get());
                }


             }
             ThreeStateCheckBoxTreeTableCell.this.param.getTreeTableView().refresh();

        }
    }

MainApp.java(作为 POC 申请)

package com.mycompany.yetanothercheckbox;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableCell;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import javafx.scene.control.cell.TreeItemPropertyValueFactory;
import javafx.stage.Stage;
import javafx.util.Callback;


public class MainApp extends Application {

    private TreeTableView<A> treetable=new TreeTableView<>();
    @Override
    public void start(Stage primaryStage) {
       //setting up parents (A) and children (B)
        A a1=new A();                       
        a1.setName("A1");

        List<A> A1Children=new ArrayList();
        B b11=new B();
        b11.setName("B11");
        B b12=new B();
        b12.setName("B12");
        b11.setParent(a1);
        b12.setParent(a1);
        A1Children.add(b11);
        A1Children.add(b12);
        a1.setChildren(A1Children);


        A a2=new A();
        a2.setName("A2");

        List<A> A2Children=new ArrayList();
        B b21=new B();
        b21.setName("B21");
        B b22=new B();
        b22.setName("B22");
        b21.setParent(a2);
        b22.setParent(a2);
        A2Children.add(b21);
        A2Children.add(b22);
        a2.setChildren(A2Children);



        //tree columns . first one holds strings
        TreeTableColumn<A,String> name=new TreeTableColumn<>("Name");
        name.setCellValueFactory(new TreeItemPropertyValueFactory<>("name"));


        //2nd tree columns. rendered as checkboxes. boolean values
        TreeTableColumn<A,Boolean> checks=new TreeTableColumn<>("Checks");
        checks.setCellValueFactory(new Callback<TreeTableColumn.CellDataFeatures<A, Boolean>, ObservableValue<Boolean>>() {
            @Override
            public ObservableValue<Boolean> call(TreeTableColumn.CellDataFeatures<A, Boolean> param) {
                A a=param.getValue().getValue();
                SimpleBooleanProperty checkUncheck=new SimpleBooleanProperty();
                SimpleBooleanProperty indeterminate=new SimpleBooleanProperty();

                checkUncheck.bindBidirectional(a.getCheck().getCheckUncheck());
                indeterminate.bindBidirectional(a.getCheck().getIndeterminate());


                checkUncheck.addListener(new ChangeListener<Boolean>(){
                    @Override
                    public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {

                        a.getCheck().indeterminateProperty().set(false);
                        a.getCheck().checkUncheckProperty().set(newValue);
                    }

                });
                indeterminate.addListener(new ChangeListener<Boolean>(){
                    @Override
                    public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {

                            a.getCheck().indeterminateProperty().set(newValue);

                    }
                });
                if(indeterminate.get()){

                return null;
                }else{

                    return checkUncheck;
                }

            }
        });


        checks.setCellFactory(new Callback<TreeTableColumn<A, Boolean>, TreeTableCell<A, Boolean>>() {
            @Override
            public TreeTableCell<A, Boolean> call(TreeTableColumn<A, Boolean> param) {
                return new ThreeStateCheckBoxTreeTableCell(param);
            }
        });


        //building the tree;
        TreeItem<A> a1item=new TreeItem<>(a1);
        TreeItem<A> b11item=new TreeItem<>(b11);
        TreeItem<A> b12item=new TreeItem<>(b12);
        a1item.getChildren().add(b11item);
        a1item.getChildren().add(b12item);

        TreeItem<A> a2item=new TreeItem<>(a2);
        TreeItem<A> b21item=new TreeItem<>(b21);
        TreeItem<A> b22item=new TreeItem<>(b22);
        a2item.getChildren().add(b21item);
        a2item.getChildren().add(b22item);




        TreeItem<A> root=new TreeItem<>();
        root.getChildren().add(a1item);
        root.getChildren().add(a2item);

        treetable.getColumns().addAll(name,checks);

        treetable.setRoot(root);
        treetable.setShowRoot(false);
        treetable.setEditable(true);


      //  StackPane rootSp = new StackPane();
        Scene scene = new Scene(treetable, 300, 250);

        primaryStage.setTitle("CheckBoxTreeTable Example");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        launch(args);
    }




}

非常感谢您提供这段很棒的代码,它让您深入了解树项 table 单元格(

不存在三态复选框这一事实

对于那些将以此 post 为食的人,我在 class '' 上做了一些小的改动。现在看起来像这样:

package com.mycompany.yetanothercheckbox;

import java.util.List;

import javafx.scene.control.CheckBox;
import javafx.scene.control.TreeTableCell;
import javafx.scene.control.TreeTableColumn;

/**
 *
 * @author returncode13
 */
// to render checkboxes in treetable

public class ThreeStateCheckBoxTreeTableCell extends TreeTableCell<A, Boolean> {
    A                           selectedItem;
    CheckBox                    checkbox;
    TreeTableColumn<A, Boolean> param;
    /*
     * private static boolean updateParent=false;
     * private static boolean updateChildren=false;
     */

    public ThreeStateCheckBoxTreeTableCell(TreeTableColumn<A, Boolean> param) {
        checkbox    = new CheckBox();
        this.param  = param;
        checkbox.setAllowIndeterminate(true);
        checkbox.selectedProperty().addListener((obs,
                oldSelectedVal,
                newSelectedVal) -> {
            int sel = getTreeTableRow().getIndex();
            selectedItem = this.param.getTreeTableView().getSelectionModel().getModelItem(sel).getValue();
            selectedItem.getCheck().getCheckUncheck().set(newSelectedVal);
            selectedItem.getCheck().getIndeterminate().set(false);
        });

        checkbox.indeterminateProperty().addListener((obx,
                ol,
                newV) -> {
            int sel = getTreeTableRow().getIndex();
            selectedItem = this.param.getTreeTableView().getSelectionModel().getModelItem(sel).getValue();
            selectedItem.getCheck().getIndeterminate().set(newV);
        });

        checkbox.setOnMouseClicked(event -> {
            int sel = getTreeTableRow().getIndex();
            selectedItem = this.param.getTreeTableView().getSelectionModel().getModelItem(sel).getValue();
            if (selectedItem.isParent()) {
                selectedItem.updateChildren = true;
                for (A child : selectedItem.getChildren()) { child.updateParent = false; }
                updateDownwards();
            }
            if (selectedItem.isLeaf()) {
                selectedItem.getParent().updateChildren = false;
                selectedItem.updateParent               = true;
                updateUpWards();
            }
        });
    }

    @Override
    public void updateItem(Boolean b,
            boolean empty) {
        super.updateItem(b, empty);
        if (empty) {
            setGraphic(null);
        } else {
            if (b == null) {
                checkbox.setIndeterminate(true);
            } else {
                checkbox.setIndeterminate(false);
                checkbox.setSelected(b);
            }
            setGraphic(checkbox);
        }
    }

    private void updateUpWards() {
        if (selectedItem.updateParent) {
            List<A> children            = selectedItem.getChildren();
            int     indeterminateCount  = 0;
            int     selectedCount       = 0;
            A       parent              = selectedItem.getParent();
            for (A child : children) {
                indeterminateCount  += child.getCheck().getIndeterminate().get() ? 1 : 0;
                selectedCount       += child.getCheck().getCheckUncheck().get() ? 1 : 0;
            }
            if (indeterminateCount > 0) {
                parent.getCheck().getIndeterminate().set(true);
            } else if (indeterminateCount == 0
                    && selectedCount == children.size()) {
                        parent.getCheck().getIndeterminate().set(false);
                        parent.getCheck().getCheckUncheck().set(true);
                    } else {
                        parent.getCheck().getIndeterminate().set(false);
                        parent.getCheck().getCheckUncheck().set(false);
                    }
        }
        this.param.getTreeTableView().refresh();
    }

    private void updateDownwards() {
        List<A> children = selectedItem.getChildren();
        if (selectedItem.isParent()
                && selectedItem.updateChildren) {
            for (A child : children) {
                child.getCheck().getCheckUncheck().set(selectedItem.getCheck().getCheckUncheck().get());
                child.getCheck().getIndeterminate().set(selectedItem.getCheck().getIndeterminate().get());
            }
        }
        this.param.getTreeTableView().refresh();

    }
}