我有类似的需求,但这里发布的解决方案对我来说不太正确(有效)。我需要懒惰地构建复选框树的模型,因为我的完整树模型可能很大(有数千个节点)。此外,保留所有checkedPath的集合对于我的数据集和使用情况来说似乎是过度和不必要的成本:我所需要的只是用户实际选择和取消选择的节点的映射,因为这是可以从中推断出从属节点的极简主义信息。
因此,我“增强”了上面的解决方案,以便在构建树模型(仅当用户实际扩展节点时才添加到模型中)和显示它(仅在用户实际检查/取消检查父节点时设置从属节点的状态)时懒惰。
我还添加到JCheckBoxTree的界面中,以提供树模型和来自外部的扩展侦听器。要使用它,您需要根据您自己的域提供注释“您的域特定内容转到此处”下面的代码部分的替代实现。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());
}
}