チェックボックスを含み、次のJTree実装を探していました。
1つのノードを選択すると、ツリー内のすべての後続ノードが自動的に選択されます
1つのノードの選択を解除すると、ツリー内のすべての後続ノードが自動的に選択解除されます
親ノードがすでに選択されていて、その選択が後続ノードの1つから削除された場合、ノードの色が変更され、この親ノードが選択されていても、すべての後続ノードが選択されているわけではないことが直感的にわかります(選択したときのように)一般的なインストーラーにインストールするコンポーネント)
ノードをクリックすると(「Ctrl」キーを押したままにする必要はありません!):
ネットで簡単なものを探しましたが、思ったほど簡単なものが見つかりませんでした。
誰かがそのようなツリーの良い実装を知っていますか?
自分自身に答える:
私は自分のコードをみんなと共有することにしました。
結果のスクリーンショットは次のとおりです。
実装の詳細:
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);
}
}
}
私にも同様のニーズがありましたが、ここに掲載されている解決策は私にとって完全に適切(効率的)ではありませんでした。完全なツリーモデルは巨大になる可能性があるため(数千のノードがある)、チェックボックスツリーのモデルを遅延して構築する必要がありました。また、すべてのcheckedPathsのセットを維持することは、私のデータセットと使用法にとって過剰で不必要に思えるコストでした。必要なのは、ユーザーによって実際に選択および選択解除されたノードのマップだけでした。従属ノードを推測できます。
そのため、上記のソリューションを「拡張」して、ツリーモデルの構築(ノードが実際にユーザーによって拡張されたときにのみモデルに追加)と表示(ユーザーが実際にチェック/アンしたときに従属ノードの状態を設定する)を怠惰にしました-親ノードをチェックします)。
また、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());
}
}
マップ構造なしで解決できます。
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モデルのデータ項目です。また、各DataItemにはcheckedプロパティが含まれています。