我有类似的需求,但这里发布的解决方案对我来说不够好(高效)。因为我的完整树模型可能非常大(具有许多千个节点),所以我需要懒惰地构建复选框树的模型。此外,保持所有已选路径的集合是一种成本,对于我的数据集和用法来说似乎过度和不必要: 我只需要用户实际选择和取消选择的节点映射,因为这是可以推断出下级节点的最小信息。
因此,我“增强”了上面的解决方案,使其在构建树模型时变得懒惰(仅在用户实际展开节点时将其添加到模型中),并且在显示树模型时也变得懒惰(仅在用户实际检查/取消检查父节点时设置子节点的状态)。
我还添加了JCheckBoxTree的接口,以提供来自外部的treeModel和expand-listener。要使用此功能,您需要根据自己的领域基础提供代码下面的“Your Domain specific stuff goes here”之后的部分的替代实现。 TreeExpansionListener使用领域对象的扩展方法进行延迟插入节点。
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);
cbt.addCheckChangeEventListener(new JCheckBoxTree.CheckChangeEventListener() {
public void checkStateChanged(JCheckBoxTree.CheckChangeEvent event) {
updatePaths(cbt, event);
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);
}
protected void updatePaths(JCheckBoxTree cbt,
JCheckBoxTree.CheckChangeEvent event){
boolean parentAlreadyIncluded = false;
boolean parentAlreadyExcluded = false;
TreePath clickedPath = (TreePath) event.getSource();
HashSet<TreePath> toBeRemoved = new HashSet<>();
for( TreePath exp : excludedPaths){
if( clickedPath.isDescendant(exp) )
toBeRemoved.add(exp);
if( isParent(exp, clickedPath))
parentAlreadyExcluded = true;
}
excludedPaths.removeAll(toBeRemoved);
toBeRemoved.clear();
for( TreePath inp : includedPaths) {
if(clickedPath.isDescendant(inp))
toBeRemoved.add(inp);
if( isParent(inp, clickedPath))
parentAlreadyIncluded = true;
}
includedPaths.removeAll(toBeRemoved);
toBeRemoved.clear();
if( cbt.getCheckMode(clickedPath) ){
if(!parentAlreadyIncluded)
includedPaths.add(clickedPath);
excludedPaths.remove(clickedPath);
}else {
if( !parentAlreadyExcluded )
excludedPaths.add(clickedPath);
includedPaths.remove(clickedPath);
}
}
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);
}
class NodeExpansionListener implements TreeExpansionListener
{
public void treeExpanded(TreeExpansionEvent event) {
final DefaultMutableTreeNode node = JCheckBoxTree.getTreeNode(event.getPath());
Object obj = node.getUserObject();
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) {}
}
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);
}
class MyDomainObject {
protected Object data;
protected boolean hasChildren;
public MyDomainObject(Object obj) {
data = obj;
hasChildren = new Random().nextBoolean();
}
public boolean expand(DefaultMutableTreeNode parent) {
DefaultMutableTreeNode flagNode = (DefaultMutableTreeNode) parent.getFirstChild();
if (flagNode == null)
return false;
Object obj = flagNode.getUserObject();
if (!(obj instanceof Boolean))
return false;
parent.removeAllChildren();
Object[] children = getChildren();
if (children == null)
return true;
ArrayList sortedChildDomainObjects = new ArrayList();
for (Object child : children) {
MyDomainObject newNode = new MyDomainObject(child);
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);
}
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());
}
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;
protected 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;
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);
}
}
}
public void setModel(TreeModel newModel) {
super.setModel(newModel);
resetCheckingState();
}
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);
}
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);
}
}
}
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;
}
}
public JCheckBoxTree(){
this(JTree.getDefaultTreeModel(), null, null);
}
public JCheckBoxTree(TreeModel treeModel){
this(treeModel, null, null);
}
public JCheckBoxTree(TreeModel treeModel,
TreeWillExpandListener tweListener,
TreeCellRenderer treeCellRenderer) {
super(treeModel);
this.setToggleClickCount(0);
if( treeCellRenderer == null )
treeCellRenderer = new CheckBoxCellRenderer();
this.setCellRenderer(treeCellRenderer);
DefaultTreeSelectionModel dtsm = new DefaultTreeSelectionModel() {
public void setSelectionPath(TreePath path) {
}
public void addSelectionPath(TreePath path) {
}
public void removeSelectionPath(TreePath path) {
}
public void setSelectionPaths(TreePath[] pPaths) {
}
};
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);
updatePredecessorsWithCheckMode(tp);
fireCheckChangeEvent(new CheckChangeEvent(tp));
selfPointer.repaint();
}
public void mouseEntered(MouseEvent arg0) {
}
public void mouseExited(MouseEvent arg0) {
}
public void mousePressed(MouseEvent arg0) {
}
public void mouseReleased(MouseEvent arg0) {
}
});
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);
}
public boolean getCheckMode( TreePath nodePath ){
CheckedNode checkedNode = getCheckedNode(nodePath);
return checkedNode.isSelected;
}
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;
}
protected boolean getAncestorCheckMode(TreePath nodePath){
TreePath parentPath = nodePath.getParentPath();
if( parentPath == null ) {
return false;
}
else {
CheckedNode checkedNode = nodesCheckingState.get(parentPath);
if( checkedNode == null )
return getAncestorCheckMode(parentPath);
else
return checkedNode.isSelected;
}
}
protected void updatePredecessorsWithCheckMode(TreePath tp) {
TreePath parentPath = tp.getParentPath();
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);
if (! childCheckedNode.allChildrenSelected) {
parentCheckedNode.allChildrenSelected = false;
}
if (childCheckedNode.isSelected) {
parentCheckedNode.isSelected = true;
}
}
updatePredecessorsWithCheckMode(parentPath);
}
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());
}
}