Java Swing: Need a good quality developed JTree with checkboxes
Asked Answered
U

3

25

I was looking for a a JTree implementation, that contains checkboxes and which:

  • When you select one node, all its successors in the tree are automatically selected

  • When you unselect one node, all its successors in the tree are automatically unselected

  • When a parent node is already selected, and the selection was removed from one of its successors, the node color will be changed, to make it intuitive that although this parent node is selected, not all of its successors are selected (Like when you select components to install in common installers)

  • A click on a node leads to (No need to hold 'Ctrl' key!):

    • If the node is already selected, it becomes unselected, with all its successors
    • If the node is not selected, it becomes selected, with all its successors

I searched the net for something simple, but could not find something as simple as I wanted.

Does anyone know a good implementation of such tree?

Uniliteral answered 18/2, 2014 at 7:56 Comment(6)
You don't need selfPointer. Use JCheckBoxTree.this.repaint() instead. Would be good to have SSCCE (with main() method to run and see how it works). Also why it has serialVersionUID? Do you need the tree serializing?Queri
+1, maybe to check with (a few posts) JTree + JCheckBox in my posts hereEurypterid
Nice to know that I can access the tree this way. The selfPointer is a workaround I commonly use. The serialVersionUID is just so Eclipse won't show me a warning about itUniliteral
What's your question? Sharing code is not what SO questions are for. You can ask a fake question and provide an answer if you want.Tusker
Ok, I am splitting my question into a question and an answerUniliteral
Any class that implements the Serializable interface (like JTree) should define a serialVersionID.Coo
U
45

Answering myself:

I decided to share my code with everyone.

Here's a screenshot of the result:

Screenshot

The implementation details:

  • Created a new class that extends JTree

  • Replaced the 'TreeCellRenderer' by a new class I created, that shows a checkbox and a label. The checkbox selection is changed instead of the label background and border.

  • Totally terminated the selection mechanism. Replaced the 'Selection Model' by a 'DefaultTreeSelectionModel' overridden inline, that has empty implementation

    • Created new event type for checking of the checkboxes

    • Created special data structures that help to indicate fast the state of each node

Enjoy!!

Here's a usage example:

public class Main extends JFrame {

    private static final long serialVersionUID = 4648172894076113183L;

    public Main() {
        super();
        setSize(500, 500);
        this.getContentPane().setLayout(new BorderLayout());
        final JCheckBoxTree cbt = new JCheckBoxTree();
        this.getContentPane().add(cbt);
        cbt.addCheckChangeEventListener(new JCheckBoxTree.CheckChangeEventListener() {
            public void checkStateChanged(JCheckBoxTree.CheckChangeEvent event) {
                System.out.println("event");
                TreePath[] paths = cbt.getCheckedPaths();
                for (TreePath tp : paths) {
                    for (Object pathPart : tp.getPath()) {
                        System.out.print(pathPart + ",");
                    }                   
                    System.out.println();
                }
            }           
        });         
        this.setDefaultCloseOperation(EXIT_ON_CLOSE);
    }

    public static void main(String args[]) {
        Main m = new Main();
        m.setVisible(true);
    }
}

Here is the source code of the class itself:

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.EventListener;
import java.util.EventObject;
import java.util.HashMap;
import java.util.HashSet;

import javax.swing.JCheckBox;
import javax.swing.JPanel;
import javax.swing.JTree;
import javax.swing.event.EventListenerList;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeSelectionModel;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;

public class JCheckBoxTree extends JTree {

    private static final long serialVersionUID = -4194122328392241790L;

    JCheckBoxTree selfPointer = this;



    // Defining data structure that will enable to fast check-indicate the state of each node
    // It totally replaces the "selection" mechanism of the JTree
    private class CheckedNode {
        boolean isSelected;
        boolean hasChildren;
        boolean allChildrenSelected;

        public CheckedNode(boolean isSelected_, boolean hasChildren_, boolean allChildrenSelected_) {
            isSelected = isSelected_;
            hasChildren = hasChildren_;
            allChildrenSelected = allChildrenSelected_;
        }
    }
    HashMap<TreePath, CheckedNode> nodesCheckingState;
    HashSet<TreePath> checkedPaths = new HashSet<TreePath>();

    // Defining a new event type for the checking mechanism and preparing event-handling mechanism
    protected EventListenerList listenerList = new EventListenerList();

    public class CheckChangeEvent extends EventObject {     
        private static final long serialVersionUID = -8100230309044193368L;

        public CheckChangeEvent(Object source) {
            super(source);          
        }       
    }   

    public interface CheckChangeEventListener extends EventListener {
        public void checkStateChanged(CheckChangeEvent event);
    }

    public void addCheckChangeEventListener(CheckChangeEventListener listener) {
        listenerList.add(CheckChangeEventListener.class, listener);
    }
    public void removeCheckChangeEventListener(CheckChangeEventListener listener) {
        listenerList.remove(CheckChangeEventListener.class, listener);
    }

    void fireCheckChangeEvent(CheckChangeEvent evt) {
        Object[] listeners = listenerList.getListenerList();
        for (int i = 0; i < listeners.length; i++) {
            if (listeners[i] == CheckChangeEventListener.class) {
                ((CheckChangeEventListener) listeners[i + 1]).checkStateChanged(evt);
            }
        }
    }

    // Override
    public void setModel(TreeModel newModel) {
        super.setModel(newModel);
        resetCheckingState();
    }

    // New method that returns only the checked paths (totally ignores original "selection" mechanism)
    public TreePath[] getCheckedPaths() {
        return checkedPaths.toArray(new TreePath[checkedPaths.size()]);
    }

    // Returns true in case that the node is selected, has children but not all of them are selected
    public boolean isSelectedPartially(TreePath path) {
        CheckedNode cn = nodesCheckingState.get(path);
        return cn.isSelected && cn.hasChildren && !cn.allChildrenSelected;
    }

    private void resetCheckingState() { 
        nodesCheckingState = new HashMap<TreePath, CheckedNode>();
        checkedPaths = new HashSet<TreePath>();
        DefaultMutableTreeNode node = (DefaultMutableTreeNode)getModel().getRoot();
        if (node == null) {
            return;
        }
        addSubtreeToCheckingStateTracking(node);
    }

    // Creating data structure of the current model for the checking mechanism
    private void addSubtreeToCheckingStateTracking(DefaultMutableTreeNode node) {
        TreeNode[] path = node.getPath();   
        TreePath tp = new TreePath(path);
        CheckedNode cn = new CheckedNode(false, node.getChildCount() > 0, false);
        nodesCheckingState.put(tp, cn);
        for (int i = 0 ; i < node.getChildCount() ; i++) {              
            addSubtreeToCheckingStateTracking((DefaultMutableTreeNode) tp.pathByAddingChild(node.getChildAt(i)).getLastPathComponent());
        }
    }

    // Overriding cell renderer by a class that ignores the original "selection" mechanism
    // It decides how to show the nodes due to the checking-mechanism
    private class CheckBoxCellRenderer extends JPanel implements TreeCellRenderer {     
        private static final long serialVersionUID = -7341833835878991719L;     
        JCheckBox checkBox;     
        public CheckBoxCellRenderer() {
            super();
            this.setLayout(new BorderLayout());
            checkBox = new JCheckBox();
            add(checkBox, BorderLayout.CENTER);
            setOpaque(false);
        }

        @Override
        public Component getTreeCellRendererComponent(JTree tree, Object value,
                boolean selected, boolean expanded, boolean leaf, int row,
                boolean hasFocus) {
            DefaultMutableTreeNode node = (DefaultMutableTreeNode)value;
            Object obj = node.getUserObject();          
            TreePath tp = new TreePath(node.getPath());
            CheckedNode cn = nodesCheckingState.get(tp);
            if (cn == null) {
                return this;
            }
            checkBox.setSelected(cn.isSelected);
            checkBox.setText(obj.toString());
            checkBox.setOpaque(cn.isSelected && cn.hasChildren && ! cn.allChildrenSelected);
            return this;
        }       
    }

    public JCheckBoxTree() {
        super();
        // Disabling toggling by double-click
        this.setToggleClickCount(0);
        // Overriding cell renderer by new one defined above
        CheckBoxCellRenderer cellRenderer = new CheckBoxCellRenderer();
        this.setCellRenderer(cellRenderer);

        // Overriding selection model by an empty one
        DefaultTreeSelectionModel dtsm = new DefaultTreeSelectionModel() {      
            private static final long serialVersionUID = -8190634240451667286L;
            // Totally disabling the selection mechanism
            public void setSelectionPath(TreePath path) {
            }           
            public void addSelectionPath(TreePath path) {                       
            }           
            public void removeSelectionPath(TreePath path) {
            }
            public void setSelectionPaths(TreePath[] pPaths) {
            }
        };
        // Calling checking mechanism on mouse click
        this.addMouseListener(new MouseListener() {
            public void mouseClicked(MouseEvent arg0) {
                TreePath tp = selfPointer.getPathForLocation(arg0.getX(), arg0.getY());
                if (tp == null) {
                    return;
                }
                boolean checkMode = ! nodesCheckingState.get(tp).isSelected;
                checkSubTree(tp, checkMode);
                updatePredecessorsWithCheckMode(tp, checkMode);
                // Firing the check change event
                fireCheckChangeEvent(new CheckChangeEvent(new Object()));
                // Repainting tree after the data structures were updated
                selfPointer.repaint();                          
            }           
            public void mouseEntered(MouseEvent arg0) {         
            }           
            public void mouseExited(MouseEvent arg0) {              
            }
            public void mousePressed(MouseEvent arg0) {             
            }
            public void mouseReleased(MouseEvent arg0) {
            }           
        });
        this.setSelectionModel(dtsm);
    }

    // When a node is checked/unchecked, updating the states of the predecessors
    protected void updatePredecessorsWithCheckMode(TreePath tp, boolean check) {
        TreePath parentPath = tp.getParentPath();
        // If it is the root, stop the recursive calls and return
        if (parentPath == null) {
            return;
        }       
        CheckedNode parentCheckedNode = nodesCheckingState.get(parentPath);
        DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode) parentPath.getLastPathComponent();     
        parentCheckedNode.allChildrenSelected = true;
        parentCheckedNode.isSelected = false;
        for (int i = 0 ; i < parentNode.getChildCount() ; i++) {                
            TreePath childPath = parentPath.pathByAddingChild(parentNode.getChildAt(i));
            CheckedNode childCheckedNode = nodesCheckingState.get(childPath);           
            // It is enough that even one subtree is not fully selected
            // to determine that the parent is not fully selected
            if (! childCheckedNode.allChildrenSelected) {
                parentCheckedNode.allChildrenSelected = false;      
            }
            // If at least one child is selected, selecting also the parent
            if (childCheckedNode.isSelected) {
                parentCheckedNode.isSelected = true;
            }
        }
        if (parentCheckedNode.isSelected) {
            checkedPaths.add(parentPath);
        } else {
            checkedPaths.remove(parentPath);
        }
        // Go to upper predecessor
        updatePredecessorsWithCheckMode(parentPath, check);
    }

    // Recursively checks/unchecks a subtree
    protected void checkSubTree(TreePath tp, boolean check) {
        CheckedNode cn = nodesCheckingState.get(tp);
        cn.isSelected = check;
        DefaultMutableTreeNode node = (DefaultMutableTreeNode) tp.getLastPathComponent();
        for (int i = 0 ; i < node.getChildCount() ; i++) {              
            checkSubTree(tp.pathByAddingChild(node.getChildAt(i)), check);
        }
        cn.allChildrenSelected = check;
        if (check) {
            checkedPaths.add(tp);
        } else {
            checkedPaths.remove(tp);
        }
    }

}
Uniliteral answered 18/2, 2014 at 10:39 Comment(28)
always good to see people answering their own questions and sharing with others :)Apologize
If you ever wonder, where do the default entries come from: They are created with the Default-Constructor of JTreeHaustellum
This code somehow renders incorrectly on my Mac. The node text is not displayed at all ImageElboa
@Clashsoft, do you use some special Look and Feel?Uniliteral
It worked fine to me on Unix, with the cross-platform built-in Look and Feels (Metal, Nimbus, Motif). It did work not so good with GTK+ native look and feel in Unix. It might also happen on Mac native Look and Feel and same for WindowsUniliteral
Damn good, extendable, clear-cut and elegant answer that works just fine! :) After searching for hours and trying all sorts of approaches with complex custom renders this is spot-on. One improvement could be weak-referencing the listeners, but not much else (at least that I can see).Capacious
how and where can I add my data, set text for nodes for this tree?Inculpate
@HuyHómHỉnh - same API as regular JTreeUniliteral
Very good post here @SomethingSomething! I am trying to use this example but I am not able to add my own data. You said that is the same API as a regular tree, but I am trying to do this: JCheckBox checkbox1 = new JCheckBox();, checkbox1.setName("Checkbox1"); and cbt.add(checkbox1); but nothing happens on the tree, it is showing the same tree as the example you posted. Some advice to add elements to the tree? Thanks.Boland
@daniellozano you should not add any checkbox - the tree will take care for them. Just add regular nodes, like in this tutorial: codejava.net/java-se/swing/… . You will need to create a root node and set it as the tree's rootUniliteral
Thanks @SomethingSomething. I am trying to follow what you said, following the link you provided, but no success. I created a DefaultMutableTreeNode and tried to add it to the JCheckBoxTree like this: DefaultMutableTreeNode root = new DefaultMutableTreeNode("root"); , but when adding it to the JCheckBoxTree, neither final JCheckBoxTree cbt = new JCheckBoxTree(root);, nor cbt.add(root); works to me. I also tried JCheckBoxMenuItem instead of DefaultMutableTreeNode but same result. What is not correct here? I am still seeing the sample tree with colors, sports and food.. Thanks a lot!Boland
You're right, that tutorial was not correct for this case. You should do it this way: DefaultMutableTreeNode root = new DefaultMutableTreeNode("root"); DefaultTreeModel model = new DefaultTreeModel(root); cbt.setModel(model);Uniliteral
That's correct @SomethingSomething! Now I can add all the nodes needed by using the DefaultTreeModel as you said. Now one simple question: by doing this, I can add nodes as checkboxes but, what if I want that my leafs nodes are not checkboxes, but just labels?Boland
@daniellozano then you'll need to modify the code (probably the renderer)Uniliteral
If I modify the renderer, I think I cannot access to the leafs of the model, because the renderer of the JCheckBoxTree runs before the model is setted, am I right? So I think that modifying the renderer to put the leafs as labels, I am going to modify also the nodes that are not leafs, because when the renderer runs, I don't know the nodes of the model yet.. it is correct? Sorry if I am saying stupid things, I am still learning =)Boland
Also when I create DefaultMutableTreeNodes with a name, for example, "New Node", I only see "Ne.." instead of "New Node", Is this normal? Thanks a lot!Boland
@daniellozano, in getTreeCellRendererComponent, you can set checkBox.setVisible(! leaf)Uniliteral
The problem of "New Node" --> "Ne" should not appear in general. Maybe you limited the width of your component?Uniliteral
About the "New Node" --> "Ne.." it also appears using the example code you provided, with no changes, ¿this does not happen to you using this example? About the getTreeCellRendererComponent I'm going to try it now!Boland
@daniellozano did you find any sollution for "New Node" --> "Ne.." ?Spiroid
@Uniliteral any sollution please?Spiroid
@Spiroid I'd really like to assist, but it may take me some time to get back into it, as this is an answer from more than 5 years ago. My guess is that it is related to width limitation of the container. Maybe the user 'daniellozano' can assist, as he struggled with the same problemUniliteral
problem solved, so in class JCheckBoxTree method getTreeCellRendererComponent new code must be { checkBox.setText(value.toString()); DefaultMutableTreeNode node = (DefaultMutableTreeNode)value; TreePath tp = new TreePath(node.getPath()); CheckedNode cn = nodesCheckingState.get(tp); if (cn == null) { return this; } checkBox.setSelected(cn.isSelected); checkBox.setOpaque(cn.isSelected && cn.hasChildren && ! cn.allChildrenSelected); return this; } Spiroid
@daniellozano Did you ever find a solution for treating leaves differently? I find myself in the situation you described: Querying for if(leaf)seems to have no effect, i.e. also the parent nodes are modified.Aparri
Great answer. Just a small picky comment: within this.addMouseListener(...) extend the MouseAdapter instead of implementing the MouseListener. You won't need to implement the unused methods. The adapter was created for that purpose.Samanthasamanthia
how to pass set of nodes to be selected in this classGrindery
About the problem "New Node" --> "Ne...", I have the same issue but using completely different code. I use a tree with checkboxes that I found on the web back in 2009. In addition to what this one does, it allows an icon to be included with each tree node (in addition to the checkbox icon). In some situations, on some screens, the text is cut off and ends with "...". I know you can't diagnose code you can't see, but this seems to be a general difficult issue with JTreeUI.Avertin
A bit more on this... It seems that the problem of text being cut-off happens even with a normal JTree. It is possible, but not easy, to fix this issue by creating a subclass of BasicTreeUI. explodingpixels.wordpress.com/2008/06/02/…Avertin
M
3

I had a similar need but the solution posted here was not quite right (efficient) for me. I needed the check-box-tree's model to be built lazily as my full tree-model could be huge (with many thousands of nodes). Also, keeping the set of all the checkedPaths was a cost that seemed excessive and unnecessary for my data-set and usage : all I needed was the map of nodes which were actually selected and deselected by the user as that is the minimalist info from which the subordinate nodes could be inferred.

So I "enhanced" the solution above to be lazy in building the tree-model (only add to the model when a node is actually expanded by user) and in displaying it (only set state of subordinate nodes when a user actually checks/un-checks a parent node).

I also added to JCheckBoxTree's interface to provide treeModel and expand-listener from outside. To use this you need to provide alternate implementation of the portion of the code below the comment "Your Domain specific stuff goes here", based on your own domain. The TreeExpansionListener does the lazy insertion of nodes, using the domain-object's expand method.

package contrib.backup.checkboxtree;

import javax.swing.*;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeExpansionListener;
import javax.swing.tree.*;
import java.awt.BorderLayout;
import java.awt.Component;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Random;

public class MyDomainCheckBoxTree extends JFrame {

    HashSet<TreePath> includedPaths = new HashSet<>();
    HashSet<TreePath> excludedPaths = new HashSet<>();
    TreeModel treeModel;

    public MyDomainCheckBoxTree(boolean testDefault) {
        super();
        setSize(500, 500);
        this.getContentPane().setLayout(new BorderLayout());

        final JCheckBoxTree cbt;
        if( testDefault ) {
            treeModel = null;
            cbt = new JCheckBoxTree();
        }
        else {
            treeModel = buildModel();
            LazyCheckBoxCellRenderer treeCellRenderer = new LazyCheckBoxCellRenderer();
            cbt = new JCheckBoxTree(treeModel, null, treeCellRenderer);
            treeCellRenderer.setCheckBoxTree(cbt);
            cbt.addTreeExpansionListener(new NodeExpansionListener());
        }

        JScrollPane s = new JScrollPane();
        s.getViewport().add(cbt);
        getContentPane().add(s, BorderLayout.CENTER);

        //this.getContentPane().add(cbt);

        cbt.addCheckChangeEventListener(new JCheckBoxTree.CheckChangeEventListener() {
            public void checkStateChanged(JCheckBoxTree.CheckChangeEvent event) {
                updatePaths(cbt, event);
                // For Debugging (correctness and laziness)
                System.out.println("\n========== Current State ========");
                System.out.println("+ + + Included Paths: ");
                printPaths(includedPaths);
                System.out.println("- - - Excluded Paths: ");
                printPaths(excludedPaths);
                System.out.println("Size of node-checkState cache = " + cbt.nodesCheckingState.size());
                //
            }
        });
        this.setDefaultCloseOperation(EXIT_ON_CLOSE);
    }

    // (As prelude)Purges any of clickedPath's children from the 2 path-sets.
    // Then adds/removes clickedPath from the 2 path-sets if appropriate.
    protected void updatePaths(JCheckBoxTree cbt,
                               JCheckBoxTree.CheckChangeEvent event){
        boolean  parentAlreadyIncluded = false;
        boolean  parentAlreadyExcluded = false;
        TreePath clickedPath = (TreePath) event.getSource();
        HashSet<TreePath>  toBeRemoved = new HashSet<>();

        //When a node is included/excluded, its children are implied as included/excluded.
        // Note: The direct-parent check is needed to avoid problem if immediate-parent is excluded
        // but grand-father/higher-ancestor is included
        for( TreePath exp : excludedPaths){
            if( clickedPath.isDescendant(exp) ) // exp is descended from clickedPath
                toBeRemoved.add(exp);
            if( isParent(exp, clickedPath)) // clickedPath is child of exp
                parentAlreadyExcluded = true;
        }
        excludedPaths.removeAll(toBeRemoved);
        toBeRemoved.clear();

        for( TreePath inp : includedPaths) {
            if(clickedPath.isDescendant(inp)) // inp is descended from clickedPath
                toBeRemoved.add(inp);
            if( isParent(inp, clickedPath)) // clickedPath is child of inp
                parentAlreadyIncluded = true;
        }
        includedPaths.removeAll(toBeRemoved);
        toBeRemoved.clear();

        // Now add/remove clickedPath from the path-sets as appropriate
        if( cbt.getCheckMode(clickedPath) ){ //selected => to be included
            if(!parentAlreadyIncluded)
                includedPaths.add(clickedPath);
            excludedPaths.remove(clickedPath);
        }else {    //deselected => to be excluded
            if( !parentAlreadyExcluded )
                excludedPaths.add(clickedPath);
            includedPaths.remove(clickedPath);
        }
    }

    // returns true if aPath is immediate parent of bPath; both must be non-null
    protected boolean isParent(TreePath aPath, TreePath bPath){
        return aPath.equals(bPath.getParentPath());
    }

    protected void printPaths(HashSet<TreePath> pathSet){
        TreePath[] paths = pathSet.toArray(new TreePath[pathSet.size()]);
        for (TreePath tp : paths) {
            for (Object pathPart : tp.getPath()) {
                System.out.print(pathPart + ",");
            }
            System.out.println();
        }
    }

    private class LazyCheckBoxCellRenderer extends JPanel implements TreeCellRenderer {
        JCheckBoxTree cbt;
        JCheckBox checkBox;

        public LazyCheckBoxCellRenderer() {
            super();
            this.setLayout(new BorderLayout());
            checkBox = new JCheckBox();
            add(checkBox, BorderLayout.CENTER);
            setOpaque(false);
        }

        public void setCheckBoxTree(JCheckBoxTree someCbt) { cbt = someCbt;}

        @Override
        public Component getTreeCellRendererComponent(JTree tree, Object value,
                                                      boolean selected, boolean expanded, boolean leaf, int row,
                                                      boolean hasFocus) {
            DefaultMutableTreeNode node = (DefaultMutableTreeNode)value;
            Object obj = node.getUserObject();

            checkBox.setText(obj.toString());

            if (obj instanceof Boolean)
                checkBox.setText("Retrieving data...");
            else
            {
                TreePath tp = new TreePath(node.getPath());
                JCheckBoxTree.CheckedNode cn = null;
                if( cbt != null )
                    cn = cbt.getCheckedNode(tp);
                if (cn == null) {
                    return this;
                }
                checkBox.setSelected(cn.isSelected);
                checkBox.setText(obj.toString());
                checkBox.setOpaque(cn.isSelected && cn.hasChildren && ! cn.allChildrenSelected);
            }
            return this;
        }
    }

    public static void main(String args[]) {
        boolean test = false;
        if( args.length > 0 && args[0].equalsIgnoreCase("test") )
            test = true;
        MyDomainCheckBoxTree m = new MyDomainCheckBoxTree(test);
        m.setVisible(true);
    }

    // Make sure expansion is threaded and updating the tree model
    // only occurs within the event dispatching thread.
    class NodeExpansionListener implements TreeExpansionListener
    {
        public void treeExpanded(TreeExpansionEvent event) {
            final DefaultMutableTreeNode node = JCheckBoxTree.getTreeNode(event.getPath());
            Object obj = node.getUserObject();

            //Expand by adding any children nodes
            Thread runner = new Thread() {
                public void run() {
                    if (obj != null && ((MyDomainObject)obj).expand(node)) {
                        Runnable runnable = new Runnable() {
                            public void run() {
                                ((DefaultTreeModel)treeModel).reload(node);
                            }
                        };
                        SwingUtilities.invokeLater(runnable);
                    }
                }
            };
            runner.start();
        }

        public void treeCollapsed(TreeExpansionEvent event) {}
    }

    //====================== Your Domain specific stuff goes here

    protected TreeModel buildModel() {
        DefaultMutableTreeNode topNode = new DefaultMutableTreeNode("Root");
        DefaultMutableTreeNode node;
        String[] categories = {"Product","Place","Critter"};
        for (String cat : categories) {
            MyDomainObject d = new MyDomainObject(cat);
            d.hasChildren = true;
            node = new DefaultMutableTreeNode(d);

            topNode.add(node);
            node.add( new DefaultMutableTreeNode(true));
        }

        return new DefaultTreeModel(topNode);
    }

    //sample impl of a domain-object; should have expand method
    class MyDomainObject {
        protected Object data;
        protected boolean  hasChildren;

        public MyDomainObject(Object obj) {
            data = obj;
            hasChildren = new Random().nextBoolean();
        }

        // Expand the tree at parent node and add nodes.
        public boolean expand(DefaultMutableTreeNode parent) {
            DefaultMutableTreeNode flagNode = (DefaultMutableTreeNode) parent.getFirstChild();
            if (flagNode == null)    // No flag
                return false;
            Object obj = flagNode.getUserObject();
            if (!(obj instanceof Boolean))
                return false;      // Already expanded

            parent.removeAllChildren();  // Remove FlagNode

            Object[] children = getChildren();
            if (children == null)
                return true;

            // Create a sorted list of domain-objects
            ArrayList sortedChildDomainObjects = new ArrayList();

            for (Object child : children) {
                MyDomainObject newNode = new MyDomainObject(child);
                //System.out.println("Size of arraylist=" + sortedChildDomainObjects.size());
                boolean isAdded = false;
                for (int i = 0; i < sortedChildDomainObjects.size(); i++) {
                    MyDomainObject nd = (MyDomainObject) sortedChildDomainObjects.get(i);
                    if (newNode.compareTo(nd) < 0) {
                        sortedChildDomainObjects.add(i, newNode);
                        isAdded = true;
                        break;
                    }
                }
                if (!isAdded)
                    sortedChildDomainObjects.add(newNode);
            }

            // Add children nodes under parent in the tree
            for (Object aChild : sortedChildDomainObjects) {
                MyDomainObject nd = (MyDomainObject) aChild;
                DefaultMutableTreeNode node = new DefaultMutableTreeNode(nd);
                parent.add(node);

                if (nd.hasChildren)
                    node.add(new DefaultMutableTreeNode(true));
            }

            return true;
        }

        private int compareTo(MyDomainObject toCompare) {
            assert toCompare.data != null;
            return data.toString().compareToIgnoreCase(toCompare.data.toString());
        }

        //should be Domain specific; dummy impl provided
        private Object[]  getChildren(){
            if( data == null || (!hasChildren))
                return null;

            Random   rand = new Random();

            Object[] children = new Object[rand.nextInt(20)];
            for( int i=0; i < children.length; i++){
                children[i] = data.toString() + "-" + rand.nextInt(1024); ;
            }
            return children;
        }

        public String toString() {
            return data != null ? data.toString() : "(EMPTY)";
        }
    }


}

My altered/lazy version of JCheckBoxTree is as follows:

package contrib.backup.checkboxtree;

import javax.swing.JCheckBox;
import javax.swing.JPanel;
import javax.swing.JTree;
import javax.swing.event.EventListenerList;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeWillExpandListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeSelectionModel;
import javax.swing.tree.ExpandVetoException;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.EventListener;
import java.util.EventObject;
import java.util.HashMap;

public class JCheckBoxTree extends JTree {

    JCheckBoxTree selfPointer = this;

    // Data structure to quickly indicate the state of each node
    // It totally replaces the "selection" mechanism of the JTree
    protected class CheckedNode {
        boolean isSelected;
        boolean hasChildren;
        boolean allChildrenSelected;

        public CheckedNode(boolean isSelected_, boolean hasChildren_, boolean allChildrenSelected_) {
            isSelected = isSelected_;
            hasChildren = hasChildren_;
            allChildrenSelected = allChildrenSelected_;
        }
    }
    //CHANGED Data struct to hold nodes lazily (as they are expanded).REMOVED other data-struct
    HashMap<TreePath, CheckedNode> nodesCheckingState;

    // Defining a new event type for the checking mechanism and preparing event-handling mechanism
    protected EventListenerList listenerList = new EventListenerList();

    public class CheckChangeEvent extends EventObject {
        public CheckChangeEvent(Object source) {
            super(source);
        }
    }

    public interface CheckChangeEventListener extends EventListener {
        void checkStateChanged(CheckChangeEvent event);
    }

    public void addCheckChangeEventListener(CheckChangeEventListener listener) {
        listenerList.add(CheckChangeEventListener.class, listener);
    }
    public void removeCheckChangeEventListener(CheckChangeEventListener listener) {
        listenerList.remove(CheckChangeEventListener.class, listener);
    }

    void fireCheckChangeEvent(CheckChangeEvent evt) {
        Object[] listeners = listenerList.getListenerList();
        for (int i = 0; i < listeners.length; i++) {
            if (listeners[i] == CheckChangeEventListener.class) {
                ((CheckChangeEventListener) listeners[i + 1]).checkStateChanged(evt);
            }
        }
    }

    // Override
    public void setModel(TreeModel newModel) {
        super.setModel(newModel);
        resetCheckingState();
    }

    // Returns true in case that the node is selected, has children but not all of them are selected
    public boolean isSelectedPartially(TreePath path) {
        CheckedNode cn = getCheckedNode(path);
        return cn.isSelected && cn.hasChildren && !cn.allChildrenSelected;
    }

    private void resetCheckingState() {
        nodesCheckingState = new HashMap<>();
        DefaultMutableTreeNode node = (DefaultMutableTreeNode)getModel().getRoot();
        if (node == null) {
            return;
        }
        addSubtreeToCheckingStateTracking(node);
    }

    // Builds up data structure for the checking mechanism. CHANGED to be lazy (do if expanded)
    private void addSubtreeToCheckingStateTracking(DefaultMutableTreeNode node) {
        TreeNode[] path = node.getPath();
        TreePath tp = new TreePath(path);
        CheckedNode cn = new CheckedNode(false, node.getChildCount() > 0, false);
        nodesCheckingState.put(tp, cn);
        if( isExpanded(tp) ) {
            for (int i = 0; i < node.getChildCount(); i++) {
                DefaultMutableTreeNode treeNode = getTreeNode(tp.pathByAddingChild(node.getChildAt(i)));
                addSubtreeToCheckingStateTracking(treeNode);
            }
        }
    }


    // Overriding cell renderer by a class that ignores the original "selection" mechanism
    // It decides how to show the nodes due to the checking-mechanism
    private class CheckBoxCellRenderer extends JPanel implements TreeCellRenderer {
        JCheckBox checkBox;
        public CheckBoxCellRenderer() {
            super();
            this.setLayout(new BorderLayout());
            checkBox = new JCheckBox();
            add(checkBox, BorderLayout.CENTER);
            setOpaque(false);
        }

        @Override
        public Component getTreeCellRendererComponent(JTree tree, Object value,
                                                      boolean selected, boolean expanded, boolean leaf, int row,
                                                      boolean hasFocus) {
            DefaultMutableTreeNode node = (DefaultMutableTreeNode)value;
            Object obj = node.getUserObject();
            TreePath tp = new TreePath(node.getPath());
            CheckedNode cn = getCheckedNode(tp);
            if (cn == null) {
                return this;
            }
            checkBox.setSelected(cn.isSelected);
            checkBox.setText(obj.toString());
            checkBox.setOpaque(cn.isSelected && cn.hasChildren && ! cn.allChildrenSelected);
            return this;
        }
    }

    // CHANGED to simply delegate to others
    public JCheckBoxTree(){
        this(JTree.getDefaultTreeModel(), null, null);
    }

    // added NEW, arg can be passed null.
    public JCheckBoxTree(TreeModel treeModel){
        this(treeModel, null, null);
    }

    // CHANGED; with added params, any or all of which may be null
    public JCheckBoxTree(TreeModel treeModel,
                         TreeWillExpandListener tweListener,
                         TreeCellRenderer treeCellRenderer) {
        super(treeModel);

        // Disabling toggling by double-click
        this.setToggleClickCount(0);
        // Overriding cell renderer by new one defined above OR provided one
        if( treeCellRenderer == null )
            treeCellRenderer = new CheckBoxCellRenderer();
        //cellRenderer = treeCellRenderer;
        this.setCellRenderer(treeCellRenderer);

        // Overriding selection model by an empty one
        DefaultTreeSelectionModel dtsm = new DefaultTreeSelectionModel() {
            // Totally disabling the selection mechanism
            public void setSelectionPath(TreePath path) {
            }
            public void addSelectionPath(TreePath path) {
            }
            public void removeSelectionPath(TreePath path) {
            }
            public void setSelectionPaths(TreePath[] pPaths) {
            }
        };

        // Calling checking mechanism on mouse click
        this.addMouseListener(new MouseListener() {
            public void mouseClicked(MouseEvent arg0) {
                TreePath tp = selfPointer.getPathForLocation(arg0.getX(), arg0.getY());
                if (tp == null) {
                    return;
                }
                boolean checkMode = ! getCheckMode(tp);
                checkSubTree(tp, checkMode, false); // func CHANGED for laziness

                updatePredecessorsWithCheckMode(tp);
                // Firing the check change event
                //fireCheckChangeEvent(new CheckChangeEvent(new Object())); //REPLACED by next-line
                fireCheckChangeEvent(new CheckChangeEvent(tp));
                // Repainting tree after the data structures were updated
                selfPointer.repaint();
            }
            public void mouseEntered(MouseEvent arg0) {
            }
            public void mouseExited(MouseEvent arg0) {
            }
            public void mousePressed(MouseEvent arg0) {
            }
            public void mouseReleased(MouseEvent arg0) {
            }
        });

        // added NEW for lazy action
        // Do the checkbox update just before the tree expands
        if( tweListener == null )
            tweListener =
                    new TreeWillExpandListener() {
                        @Override
                        public void treeWillExpand(TreeExpansionEvent event) throws ExpandVetoException {
                            TreePath expandingNodePath = event.getPath();
                            boolean checkMode = getCheckMode(expandingNodePath);
                            checkSubTree(expandingNodePath, checkMode, true);
                        }

                        @Override
                        public void treeWillCollapse(TreeExpansionEvent event) throws ExpandVetoException {
                        }
                    };

        this.addTreeWillExpandListener(tweListener);
        this.setSelectionModel(dtsm);
    }


    // added NEW
    public boolean getCheckMode( TreePath nodePath ){
        CheckedNode checkedNode = getCheckedNode(nodePath);
        return checkedNode.isSelected;
    }

    // added NEW.
    // Fetches checked-node if available or lazily add it with
    // checkMode inherited from 'nearest' ancestor.
    CheckedNode getCheckedNode(TreePath nodePath){
        CheckedNode checkedNode = nodesCheckingState.get(nodePath);
        if( checkedNode == null ){
            DefaultMutableTreeNode node = getTreeNode(nodePath);
            boolean ancestorCheckedMode = getAncestorCheckMode(nodePath);
            checkedNode = new CheckedNode(ancestorCheckedMode, node.getChildCount() > 0, ancestorCheckedMode);
            nodesCheckingState.put(nodePath, checkedNode);
        }
        return checkedNode;
    }

    // added NEW
    // Returns the checkedMode of the nearest ancestor that can be found, else false
    protected boolean getAncestorCheckMode(TreePath  nodePath){
        TreePath parentPath = nodePath.getParentPath();
        if( parentPath == null ) {// nodePath is root so has null parent
            return false;
        }
        else {
            CheckedNode checkedNode = nodesCheckingState.get(parentPath);
            if( checkedNode == null )
                return getAncestorCheckMode(parentPath);
            else
                return checkedNode.isSelected;
        }
    }

    // When a node is checked/unchecked, updating the states of the predecessors
    protected void updatePredecessorsWithCheckMode(TreePath tp) {
        TreePath parentPath = tp.getParentPath();
        // If it is the root, stop the recursive calls and return
        if (parentPath == null) {
            return;
        }
        CheckedNode parentCheckedNode = getCheckedNode(parentPath);
        DefaultMutableTreeNode parentNode = getTreeNode(parentPath);
        parentCheckedNode.allChildrenSelected = true;
        parentCheckedNode.isSelected = false;
        for (int i = 0 ; i < parentNode.getChildCount() ; i++) {
            TreePath childPath = parentPath.pathByAddingChild(parentNode.getChildAt(i));
            CheckedNode childCheckedNode = getCheckedNode(childPath);
            // It is enough that even one subtree is not fully selected
            // to determine that the parent is not fully selected
            if (! childCheckedNode.allChildrenSelected) {
                parentCheckedNode.allChildrenSelected = false;
            }
            // If at least one child is selected, selecting also the parent
            if (childCheckedNode.isSelected) {
                parentCheckedNode.isSelected = true;
            }
        }
        // Go to upper predecessor
        updatePredecessorsWithCheckMode(parentPath);
    }

    // Recursively checks/unchecks a subtree. NEW: modified to perform lazily.
    // NEW arg goOneLevelDown will be false when checkbox is clicked, true when expanding a node.
    protected void checkSubTree(TreePath tp, boolean check, boolean goOneLevelDown) {
        CheckedNode cn =  getCheckedNode(tp);
        cn.isSelected = check;
        DefaultMutableTreeNode node = getTreeNode(tp);
        if( isExpanded(tp) || goOneLevelDown ){
            for (int i = 0 ; i < node.getChildCount() ; i++) {
                checkSubTree(tp.pathByAddingChild(node.getChildAt(i)), check, false);
            }
        }
        cn.allChildrenSelected = check;
    }

    public static DefaultMutableTreeNode getTreeNode(TreePath path)
    {
        return (DefaultMutableTreeNode)(path.getLastPathComponent());
    }
}
Mccullum answered 6/2, 2016 at 22:8 Comment(0)
G
0

Can be solution without map structure.

MouseAdapter ml = new MouseAdapter() {
        public void mousePressed(MouseEvent e) {
            if ( e.getClickCount() == 1 )
            {
                TreePath selPath = m_xTree.getPathForLocation(e.getX(), e.getY());
                if(selPath==null) return;
                else if (selPath!=null && selPath.getPathCount()>=1)
                {
                    DataItem xDIClicked = (DataItem)( selPath.getPathComponent(selPath.getPathCount()-1) );
                    xDIClicked.setIsSelectedByCheckbox( !xDIClicked.getIsSelectedByCheckbox() );
                    m_xTree.repaint();
                }
            }
        }
    };
    m_xTree.addMouseListener(ml);

So xDIClicked returned by getPathComponent() is JTree Model's data item. And each DataItem contains checked property.

Guzel answered 4/12, 2014 at 16:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.