JTree rendering with JCheckBox nodes
java, jcheckbox, jtree, rendering, swing
Solution
I was able to solve the problem.
I created a model class (TreeNodeModel) to hold the relevant node data: key, value, hasCheckBox, isSelected:
public class TreeNodeModel {
int key;
String value;
boolean isSelected=false;
boolean hasCheckBox=false;
public TreeNodeModel() {
}
public TreeNodeModel(int key, String value) {
this.key=key;
this.value = value;
}
public TreeNodeModel(int key, String value, boolean hasCheckBox) {
this.key=key;
this.value = value;
this.hasCheckBox = hasCheckBox;
}
public TreeNodeModel(int key, String value, boolean hasCheckBox, boolean isSelected) {
this.key=key;
this.value = value;
this.hasCheckBox=hasCheckBox;
this.isSelected = isSelected;
}
public boolean isSelected() {
return this.isSelected;
}
public void setSelected(boolean newValue) {
this.isSelected = newValue;
}
public boolean hasCheckBox() {
return this.hasCheckBox;
}
public void setCheckBox(boolean newValue) {
this.hasCheckBox=newValue;
}
public int getKey() {
return this.key;
}
public void setKey(int newValue) {
this.key = newValue;
}
public String getValue() {
return this.value;
}
public void setValue(String newValue) {
this.value = newValue;
}
@Override
public String toString() {
return getClass().getName() + "[" + this.value + "/" + this.isSelected + "]";
// return this.text;
}
}
I created an implementation of the TreeCellEditor interface:
public class TreeNodeEditor extends AbstractCellEditor implements TreeCellEditor {
private JTree tree;
private TreeNodeModel editedModel = null;
TreeNodeRenderer renderer = new TreeNodeRenderer();
public TreeNodeEditor(JTree tree) {
this.tree=tree;
}
@Override
public boolean isCellEditable(EventObject event) {
boolean editable=false;
if (event instanceof MouseEvent) {
MouseEvent mouseEvent = (MouseEvent) event;
TreePath path = tree.getPathForLocation(mouseEvent.getX(),mouseEvent.getY());
if (path != null) {
Object node = path.getLastPathComponent();
if ((node != null) && (node instanceof DefaultMutableTreeNode)) {
DefaultMutableTreeNode editedNode = (DefaultMutableTreeNode) node;
TreeNodeModel model = (TreeNodeModel) editedNode.getUserObject();
editable = model.hasCheckBox;
} //if (node)
} //if (path)
} //if (MouseEvent)
return editable;
}
public Object getCellEditorValue() {
JCheckBox checkbox = renderer.getCheckBoxRenderer();
TreeNodeModel model = new TreeNodeModel(editedModel.getKey(), checkbox.getText(), true, checkbox.isSelected());
return model;
}
public Component getTreeCellEditorComponent(JTree tree, Object value, boolean isSelected, boolean expanded, boolean leaf, int row) {
if (((DefaultMutableTreeNode) value).getUserObject() instanceof TreeNodeModel) {
editedModel = (TreeNodeModel) ((DefaultMutableTreeNode) value).getUserObject();
}
Component editor = renderer.getTreeCellRendererComponent(tree, value, true, expanded, leaf, row, true);
// editor always selected / focused
ItemListener itemListener = new ItemListener() {
public void itemStateChanged(ItemEvent itemEvent) {
if (stopCellEditing())
fireEditingStopped();
}
};
if (editor instanceof JCheckBox) {
((JCheckBox) editor).addItemListener(itemListener);
}
return editor;
}
}
The key was capturing the model in the getTreeCellEditorComponent() method and using its Key in the getCellEditorValue() method.
Building the tree was easy:
DefaultMutableTreeNode server = new DefaultMutableTreeNode(new TreeNodeModel(0,"Server 01", false));
DefaultMutableTreeNode userFolder = new DefaultMutableTreeNode(new TreeNodeModel(1, "User Folders", false));
server.add(userFolder);
DefaultMutableTreeNode user01 = new DefaultMutableTreeNode(new TreeNodeModel(2, "User 01", true, true));
userFolder.add(user01);
DefaultMutableTreeNode clientA = new DefaultMutableTreeNode(new TreeNodeModel(3, "Client A", true, true));
user01.add(clientA);
DefaultMutableTreeNode clientB = new DefaultMutableTreeNode(new TreeNodeModel(4, "Client B", true, true));
user01.add(clientB);
DefaultMutableTreeNode publicFolder = new DefaultMutableTreeNode(new TreeNodeModel(5, "Public Folders", false));
server.add(publicFolder);
DefaultMutableTreeNode clientC = new DefaultMutableTreeNode(new TreeNodeModel(6, "Client C", true));
publicFolder.add(clientC);
Tree_Nodes.setCellRenderer(new TreeNodeRenderer());
Tree_Nodes.setCellEditor(new TreeNodeEditor(Tree_Nodes));
Tree_Nodes.setModel(new DefaultTreeModel(server);
Finally, determining which nodes where checked was a matter of examining the model's node collection (via a button):
private Map<Integer, String> checked = new HashMap<Integer, String>();
private void Button_CheckedActionPerformed(java.awt.event.ActionEvent evt) {
DefaultTableModel tableModel = ((DefaultTableModel) Table_Nodes.getModel());
tableModel.getDataVector().removeAllElements();
tableModel.fireTableDataChanged();
checked.clear();
DefaultTreeModel treeModel = (DefaultTreeModel) Tree_Nodes.getModel();
DefaultMutableTreeNode root = (DefaultMutableTreeNode) treeModel.getRoot();
getChildNodes(root);
for (Iterator it=checked.entrySet().iterator(); it.hasNext(); ) {
Map.Entry entry = (Map.Entry)it.next();
tableModel.addRow(new Object[] {entry.getKey(), entry.getValue()});
}
Button_Checked.requestFocus();
}
private void getChildNodes(DefaultMutableTreeNode parentNode) {
for (Enumeration e=parentNode.children(); e.hasMoreElements();) {
DefaultMutableTreeNode childNode = (DefaultMutableTreeNode) e.nextElement();
TreeNodeModel model = (TreeNodeModel) childNode.getUserObject();
if (model.hasCheckBox && model.isSelected()) {
checked.put(model.getKey(), model.getValue());
}
//recurse
if (childNode.getChildCount()>0) getChildNodes(childNode);
}
}
Problem
I am attempting to modify the standard Swing JTree to intermingle nodes with and without checkboxes. This is an example: When I attempt to check/uncheck one of the checkboxes (the 'User 01' node in this example), the tree loses nodes: I my code is an adaptation of this example: http://forums.sun.com/thread.jspa?threadID=5321084&start=13. Instead of embedding a JCheckBox in a DefaultMutableTreeNode like this: ``` new DefaultMutableTreeNode(new CheckBoxNode("Accessibility", true)); ``` I thought it made more sense to create a model node that derived from the DefaultMutableTreeNode, which I call JTreeNode. This class automatically sets the DefaultMutableTreeNode's UserObject to a JCheckBox. The class' ShowCheckBox property is used by the TreeCellRenderer to determine if the JCheckBox or DefaultTreeCellRenderer is used. The JTreeNode is used like this: ``` JTreeNode user01 = new JTreeNode("User 01"); user01.setShowCheckBox(true); user01.setSelected(true); ``` I believe the problem is with the class that implements the TreeCellEditor, specifically in the getCellEditorValue() or getTreeCellEditorComponent() methods. I suspect the issue has something to with the getCellEditorValue() returning a derivative of the DefaultMutableTreeNode, rather than a simpler model instance. ``` public Object getCellEditorValue() { JCheckBox checkBox = renderer.getCheckBoxRenderer(); JTreeNode node = new JTreeNode(checkBox.getText()); node.setShowCheckBox(true); node.setSelected(checkBox.isSelected()); return node; } public Component getTreeCellEditorComponent(JTree tree, Object value, boolean isSelected, boolean expanded, boolean leaf, int row) { Component editor = renderer.getTreeCellRendererComponent(tree, value, true, expanded, leaf, row, true); // editor always selected / focused ItemListener itemListener = new ItemListener() { public void itemStateChanged(ItemEvent itemEvent) { if (stopCellEditing()) { fireEditingStopped(); } } }; if (editor instanceof JCheckBox) { ((JCheckBox) editor).addItemListener(itemListener); } return editor; } ``` Here is the TreeCellRender's getTreeCellRendererComponent() method: ``` public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { Component component; //if object being passed for rendering is a JTreeNode that should show a JCheckBox, attempt to render it so if (((JTreeNode) value).getShowCheckBox()) { String stringValue = tree.convertValueToText(value, selected, expanded, leaf, row, false); //set default JCheckBox rendering checkBoxRenderer.setText(stringValue); checkBoxRenderer.setSelected(false); //not checked checkBoxRenderer.setEnabled(tree.isEnabled()); if (selected) { //removed colorization //checkBoxRenderer.setForeground(selectionForeground); //checkBoxRenderer.setBackground(selectionBackground); } else { checkBoxRenderer.setForeground(textForeground); checkBoxRenderer.setBackground(textBackground); } //DefaultMutableTreeNode if ((value != null) && (value instanceof JTreeNode)) { //userObject should be a JTreeNode instance //DefaultMutableTreeNode //Object userObject = ((JTreeNode) value).getUserObject(); //if it is //if (userObject instanceof JTreeNode) { //cast as a JTreeNode //JTreeNode node = (JTreeNode) userObject; JTreeNode node = (JTreeNode) value; //set JCheckBox settings to match JTreeNode's settings checkBoxRenderer.setText(node.getText()); checkBoxRenderer.setSelected(node.isSelected()); //} } component = checkBoxRenderer; } //if not, render the default else { component = defaultRenderer.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus); } return component; } ``` Any thoughts are greatly appreciated.