Java Swing:需要具有复选框的高质量开发 JTree

Posted

技术标签:

【中文标题】Java Swing:需要具有复选框的高质量开发 JTree【英文标题】:Java Swing: Need a good quality developed JTree with checkboxes 【发布时间】:2014-03-17 19:56:44 【问题描述】:

我正在寻找一个 JTree 实现,它包含复选框并且:

当你选择一个节点时,它在树中的所有后继节点都会被自动选择

当您取消选择一个节点时,树中的所有后继节点都会自动取消选择

当一个父节点已经被选中,并且选择从它的一个后继节点中被移除时,节点颜色将会改变,以直观地表明虽然这个父节点被选中,但并不是它的所有后继节点都被选中已选中(就像您在常用安装程序中选择要安装的组件时一样)

点击一个节点会导致(无需按住'Ctrl'键!):

如果节点已被选中,则它会变为未选中状态,包括其所有后继节点 如果节点没有被选中,它会被选中,它的所有后继节点都被选中

我在网上搜索了一些简单的东西,但找不到我想要的简单的东西。

有谁知道这种树的良好实现?

【问题讨论】:

你不需要 selfPointer。请改用 JCheckBoxTree.this.repaint()。拥有 SSCCE 会很好(使用 main() 方法运行并查看它是如何工作的)。还有为什么它有serialVersionUID?你需要树序列化吗? +1,也许可以在我的帖子中查看(一些帖子)JTree + JCheckBox 很高兴知道我可以通过这种方式访问​​树。 selfPointer 是我常用的一种解决方法。 serialVersionUID 只是这样 Eclipse 不会向我显示有关它的警告 非常有用,谢谢!赞成。 你有什么问题?共享代码不是 SO 问题的目的。如果需要,您可以提出虚假问题并提供答案。 【参考方案1】:

回答我自己:

我决定与大家分享我的代码。

这是结果的截图:

实现细节:

创建了一个扩展 JTree 的新类

用我创建的一个新类替换了“TreeCellRenderer”,它显示了一个复选框和一个标签。复选框选择被更改,而不是标签背景和边框。

完全终止选择机制。将“选择模型”替换为“DefaultTreeSelectionModel”覆盖的内联,它具有空实现

创建了用于检查复选框的新事件类型

创建了有助于快速指示每个节点状态的特殊数据结构

享受!!

这是一个用法示例:

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);
    

这是类本身的源代码:

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);
        
    


【讨论】:

总是很高兴看到人们回答自己的问题并与他人分享:) 如果你想知道,默认条目是从哪里来的:它们是用 JTree 的 Default-Constructor 创建的 这段代码在我的 Mac 上以某种方式呈现不正确。节点文本根本不显示Image 谢谢@SomethingSomething。我正在尝试按照您提供的链接按照您所说的进行操作,但没有成功。我创建了一个 DefaultMutableTreeNode 并尝试将其添加到 JCheckBoxTree 中,如下所示:DefaultMutableTreeNode root = new DefaultMutableTreeNode("root");,但是当将其添加到 JCheckBoxTree 时,final JCheckBoxTree cbt = new JCheckBoxTree(root);cbt.add(root); 都不适用于我。我还尝试了JCheckBoxMenuItem 而不是DefaultMutableTreeNode,但结果相同。这里有什么不正确的?我仍然看到带有颜色、运动和食物的样本树。非常感谢! 您是对的,该教程不适用于这种情况。你应该这样做:DefaultMutableTreeNode root = new DefaultMutableTreeNode("root"); DefaultTreeModel model = new DefaultTreeModel(root); cbt.setModel(model);【参考方案2】:

我也有类似的需求,但这里发布的解决方案对我来说并不完全正确(有效)。我需要懒惰地构建复选框树的模型,因为我的完整树模型可能很大(有数千个节点)。此外,保留所有已检查路径的集合对于我的数据集和使用而言似乎是过多且不必要的成本:我所需要的只是用户实际选择和取消选择的节点图,因为这是从中获取的极简信息可以推断出从属节点。

所以我“增强”了上面的解决方案,以便在构建树模型时懒惰(仅在用户实际扩展节点时添加到模型中)和显示它(仅在用户实际时设置从属节点的状态检查/取消检查父节点)。

我还在 JCheckBoxTree 的接口中添加了从外部提供 treeModel 和 expand-listener 的功能。要使用它,您需要根据您自己的域,在注释“您的域特定的东西在此处”下方提供代码部分的替代实现。 TreeExpansionListener 使用域对象的 expand 方法进行节点的延迟插入。

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)";
        
    



我的 JCheckBoxTree 修改/惰性版本如下:

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());
    

【讨论】:

【参考方案3】:

可以是没有地图结构的解决方案。

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);

所以 getPathComponent() 返回的 xDIClicked 是 JTree Model 的数据项。并且每个 DataItem 都包含选中的属性。

【讨论】:

以上是关于Java Swing:需要具有复选框的高质量开发 JTree的主要内容,如果未能解决你的问题,请参考以下文章

java的swing组件的使用

JAVA-基础(十) Swing

java GUI编程(swing)之三swing单选框复选框组件

Java二级-Swing实现复选按钮

Java图形化界面学习

Java Swing,尝试用图像图标复选框替换 JTable 中的布尔复选框