When building a reasonably complex Swing application, more often than not you might end up needing a GUI component that is not made available in the Java distribution. In the following tutorial I will present the required steps to make a working treeview with checkboxes that allows the user to browse his/her filesystem.
Important TreeView-related classes
Basically, when you instantiate a JTreeView class, the constructor will create other helper classes, the most important being: a TreeCellRenderer implementation, a TreeModel implementation and a TreeCellEditor implementation. The Renderer is used to render each of the tree's items, the model is used to store the data in a view-independed way and the editor is used to handle the editing (checking/unchecking, in our case).
The filesystem model
We will implement a custom model that, when asked for the children of a tree node, will fetch the list of subfolders and feed the result to the TreeView. The advantage of this method is that data is fetched on demand and cached, thus avoiding scanning the whole hdd on application startup.
public class FilesystemFolderModel
implements TreeModel {
public static final FolderBean ROOT =
new FolderBean
("Computer",
File.
listRoots(),
false);
private static final List<TreeModelListener> listeners = new LinkedList<TreeModelListener>();
private final List<FolderBean> checkedFolders = new LinkedList<FolderBean>();
private final List<FolderBean> uncheckedFolders = new LinkedList<FolderBean>();
@Override
FolderBean parentFB = (FolderBean) parent;
return parentFB.getChild(index);
}
@Override
public int getChildCount
(Object parent
) { FolderBean folder = (FolderBean) parent;
return folder.getChildCount();
}
@Override
return ROOT;
}
@Override
public boolean isLeaf
(Object node
) { FolderBean folder = (FolderBean) node;
return folder.getChildCount() == 0;
}
/**
* Called after the call to TreeCellEditor.getCellEditorValue(), passing the result of that method in the newValue parameter.
*/
@Override
FolderBean folder = (FolderBean) path.getLastPathComponent();
folder.
setChecked((Boolean) newValue
);
if (folder.isChecked()) {
checkedFolders.add(folder);
uncheckedFolders.remove(folder);
} else {
checkedFolders.remove(folder);
uncheckedFolders.add(folder);
}
fireModelChangedEvent(path);
}
private void fireModelChangedEvent
(TreePath path
) { }
}
/* more code */
}
public class FolderBean {
private boolean isChecked = false;
private List<FolderBean> children;
private void fillCache() {
String[] childs =
new File(fullPath
).
list(FoldersOnlyFilter.
getInstance());
if(childs != null) {
// all childs are directories
children = new ArrayList<FolderBean>(childs.length);
children.add(new FolderBean(fullPath + "\\" + child, child, isChecked));
}
} else {
children = new ArrayList<FolderBean>(0);
}
}
public FolderBean getChild(int index) {
if(children == null)
fillCache();
return children.get(index);
}
public int getChildCount() {
if(children == null)
fillCache();
return children.size();
}
/* more code */
}
At this point, our tree should look like this, showing our custom data (the folders), but still with the default rendering.
The renderer
The common way to implement a renderer is to use a single instance of the component you wish to use for rendering the nodes of the treeview and to reconfigure it's appearence on each getTreeCellRendererComponent call. Swing uses renderers as some sort of rubber stamp, so it's not necessary to have a checkbox instance for each tree node. To correctly draw the background/foreground colors, we use the UIManager.getColor() method that (I suspect) looks up these values in the current Look&Feel.
private Color selectionForeground;
private Color selectionBackground;
private Color textForeground;
private Color textBackground;
public CheckboxCellRenderer() {
selectionForeground =
UIManager.
getColor("Tree.selectionForeground");
selectionBackground =
UIManager.
getColor("Tree.selectionBackground");
textForeground =
UIManager.
getColor("Tree.textForeground");
textBackground =
UIManager.
getColor("Tree.textBackground");
}
boolean expanded, boolean leaf, int row, boolean hasFocus) {
FolderBean folder = (FolderBean) value;
checkbox.setText(folder.toString());
checkbox.setSelected(folder.isChecked());
if (folder == FilesystemFolderModel.ROOT) {
checkbox.
setIcon(UIManager.
getIcon("FileView.hardDriveIcon"));
} else {
checkbox.setIcon(null);
}
if (sel) {
checkbox.setForeground(selectionForeground);
checkbox.setBackground(selectionBackground);
} else {
checkbox.setForeground(textForeground);
checkbox.setBackground(textBackground);
}
checkbox.setEnabled(tree.isEnabled());
return checkbox;
}
}
This is how our treeview looks after setting both the model and renderer. Still, clicking on the checkboxes doesn't have any effect, for that we'll have to implement an editor.
The editor
One interesting feature that Swing provides is the ability to use other components for editing than those used for rendering. This means you can show (render) values as a checkbox, but when the user wants to edit the value, present him/her with a drop-down list. In our case we will use the same component (checkbox) for both rendering and editing. Since we already wrote the code that correctly displays the checkbox with it's associated state, we will reuse that code by having a reference to a new instance of our CheckboxCellRenderer in our CheckboxNodeEditor. We need to add is a listener that should be called when the state of the checkbox changes; our renderer reference will return the same checkbox instance each time, so we have to make sure to add the listener only once. When the checkbox notifies the listener that it's state has changed, we call fireEditingStopped() to notify the model that the data has been changed.
private boolean firstCall = true;
@Override
boolean expanded, boolean leaf, int row) {
editor =
(JCheckBox) renderer.
getTreeCellRendererComponent( tree, value, isSelected, expanded, leaf, row, true);
// avoid adding new listeners on every call
if (firstCall) {
public void itemStateChanged
(ItemEvent itemEvent
) { fireEditingStopped();
}
};
editor.addItemListener(itemListener);
firstCall = false;
}
return editor;
}
@Override
public Object getCellEditorValue
() { return editor.isSelected();
}
}
Sticking it all together
I have made an archive of the project both with a Main class that puts it all together, but basically what you need is:
final FilesystemFolderModel filesystemFolderModel = new FilesystemFolderModel();
tree.setModel(filesystemFolderModel);
tree.setCellRenderer(new CheckboxCellRenderer());
tree.setCellEditor(new CheckboxNodeEditor());
The final result should look something like this:
