Java Swing:需要一个带有复选框的高质量开发的 JTree
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/21847411/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Java Swing: Need a good quality developed JTree with checkboxes
提问by SomethingSomething
I was looking for a a JTree implementation, that contains checkboxes and which:
我正在寻找一个 JTree 实现,它包含复选框,并且:
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
当您选择一个节点时,它在树中的所有后继节点都会被自动选择
当您取消选择一个节点时,它在树中的所有后继节点都将自动取消选择
当一个父节点已经被选中,并且该选择被从它的一个后继节点中删除时,节点颜色将被改变,以使其直观,虽然这个父节点被选中,但并非所有的后继节点都被选中(就像你选择安装在通用安装程序中的组件)
单击节点会导致(无需按住“Ctrl”键!):
- 如果该节点已被选中,它将变为未选中状态,及其所有后继节点
- 如果该节点没有被选中,它就会被选中,并带有它的所有后继
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?
有谁知道这种树的良好实现?
采纳答案by SomethingSomething
Answering myself:
回答我自己:
I decided to share my code with everyone.
我决定与大家分享我的代码。
Here's a screenshot of the result:
这是结果的屏幕截图:
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
创建了一个扩展 JTree 的新类
用我创建的一个新类替换了“TreeCellRenderer”,它显示了一个复选框和一个标签。复选框选择被更改而不是标签背景和边框。
完全终止了选拔机制。用具有空实现的“DefaultTreeSelectionModel”覆盖的内联替换了“选择模型”
创建了用于检查复选框的新事件类型
创建了特殊的数据结构,有助于快速指示每个节点的状态
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);
}
}
}
回答by user4325360
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.
所以getPathComponent()返回的xDIClicked就是JTree Model的数据项。并且每个 DataItem 都包含已检查的属性。
回答by Don
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.
我还在 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)";
}
}
}
My altered/lazy version of JCheckBoxTree is as follows:
我的 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());
}
}