Chapter 17. Trees
Subpages: 1. JTree
2. Basic JTree example
3. Directories tree: part I - Dynamic node retrieval
4. Directories tree: part II - Popup menus and programmatic navigation
5. Directories tree: part III - Tooltips
6. JTree and XML documents
7. Custom editors and renderers
In this chapter:
- Basic JTree example
- Directories tree: part I - Dynamic node retrieval
- Directories tree: part II - Popup menus and programmatic navigation
- Directories tree: part III - Tooltips
- JTree and XML documents
- Custom editors and renderers
JTree is a perfect tool for the display, navigation, and editing of hierarchical data. Because of its complex nature, JTree has a whole package devoted to it: javax.swing.tree. This package consists of a set of classes and interfaces which we will briefly review before moving on to several examples. But first, what is a tree?
17.1.1 Tree concepts and terminology
The tree is a very important and heavily used data structure throughout computer science (e.g. compiler design, graphics, artificial intelligence, etc.). This data structure consists of a logically arranged set of nodes, which are containers for data. Each tree contains one root node, which serves as that tree's top-most node. Any node can have an arbitrarty number of child (descendant) nodes. In this way, each descendant node is the root of a subtree.
Each node is connected by an edge. An edge signifies the relationship between two nodes. A node's direct predecessor is called its parent, and all predecesors (above and including the parent) are called its ancestors. A node that has no descendants is called a leaf node. All direct child nodes of a given node are siblings.
A path from one node to another is a sequence of nodes with edges from one node to the next. The level of a node is the number of nodes visited in the path between the root and that node. The height of a tree is its largest level--the length of its longest path.
17.1.2 Tree traversal
It is essential that we be able to systematically visit each and every node of a tree. (The term 'visit' here refers to performing some task before moving on.) There are three common traversal orders used for performing such an operation: preoder, inorder, and postorder. Each is recursive and can be summarized as follows:
Recursively do the following:
If the tree is not empty, visit the root and then traverse all subtrees in ascending order.
Inorder (often referred to as breadth first):
Start the traversal by visiting the main tree root. Then, in ascending order, visit the root of each subtree. Continue visiting the roots of all subtrees in this mannar, in effect visiting the nodes at each level of the tree in ascending order.
Postorder (often referred to as depth first):
Recursively do the following:
If the tree is not empty, traverse all subtrees in ascending order, and then visit the root.
So how does Swing's JTree component deal with all this structure? Implementations of the TreeModel interface encapsulate all tree nodes, which are implementations of the TreeNode interface. The DefaultMutabeTreeNode class (an implementation of TreeNode) provides us with the ability to perform preorder, inorder, and postorder tree traversals.
JTree graphically displays each node similar to how JList displays its elements: in a vertical column of cells. Similarly, each cell can be rendered with a custom renderer (an implementation of TreeCellRenderer) and can be edited with a custom TreeCellEditor. Each tree cell shows a non-leaf node as being expanded or collapsed, and can represent node relationships (i.e. edges) in various ways. Expanded nodes show their subtree nodes, and collapsed nodes hide this information.
The selection of tree cells is similar to JList's selection mechanism, and is controlled by a TreeSelectionModel. Selection also involves keeping track of paths between nodes as instances of TreeNode. Two kinds of events are used specifically with trees and tree selections: TreeModelEvent and TreeExpansionEvent. Other AWT and Swing events also apply to JTree. For instance, we can use MouseListeners to intercept mouse presses and clicks. Also note that JTree implements the Scrollable interface (see chapter 7) and is intended to be placed in a JScrollPane.
A JTree can be constructed using either the default constructor, by providing a TreeNode to use for the root node, providing a TreeModel containing all constituent nodes, or by providing a one-dimensional array, Vector, or Hashtable of objects. In the latter case, if any element in the given structure is a multi-element structure itself, it is recursively used to build a subtree (this functionality is handled by an inner class called DynamicUtilTreeNode).
We will see how to construct and work with all aspects of a JTree soon enough. But first we need to develop a more solid understanding of its underlying constituents and how they interact.
17.1.4 The TreeModel interface
abstract interface javax.swing.tree.TreeModel
This model handles the data to be used in a JTree, assuming that each node maintains an array of child nodes. Nodes are represented as Objects, and a separate root node accessor is defined. A set of methods is intended to retrieve a node based on a given parent node and index, return the number of children of a given node, return the index of a given node based on a given parent, check if a given node is a leaf node (has no children), and a method to notify JTree that a node which is the destination of a given TreePath has been modified. It also provides method declarations for adding and removing TreeModelListeners which should be notified when any nodes are added, removed, or changed. A JTree's TreeModel can be retrieved and assigned with its getModel() and setModel() methods respectively.
DefaultTreeModel is the default concrete implementation of the TreeModel interface. It defines the root and each node of the tree as TreeNode instances. It maintains an EventListenerList of TreeModelListeners and provides several methods for firing TreeModelEvents when anything in the tree changes. It defines the asksAllowedChildren flag which is used to confirm whether or not a node allows children to be added before actually attempting to add them. DefaultTreeModel also defines methods for returning an array of nodes from a given node to the root node, inserting and removing nodes, and a method to reload/refresh a tree from a specified node. We normally build off of this class when implementing a JTree component.
17.1.6 The TreeNode interface
abstract interface javax.swing.tree.TreeNode
TreeNode describes the base interface which all tree nodes must conform to in a DefaultTreeModel. Implementations of this class represent the basic building block of JTree's model. It declares properties for specifying whether a node is a leaf, a parent, allows addition of child nodes, determining the number of children, obtaining a TreeNode child at a given index or the parent node, and obtaining an Enumeration of all child nodes.
17.1.7 The MutableTreeNode interface
abstract interface javax.swing.tree.MutableTreeNode
Thic interface extends TreeNode to describe a more sophisticated tree node which can carry a user object. This is the object that represents the data of a given tree node. The setUserObject() method declares how the user object should be assigned (it is assumed that implementations of this interface will provide the equivalent of a getUserObject() method, even though none is included here). This interface also provides method declarations for inserting and removing nodes from a given node, and changing its parent node.
DefaultMutableTreeNode is a concrete implementation of the MutableTreeNode interface. Method getUserObject() returns a the data object encapsulated by this node. It stores all child nodes in a Vector called children, accessible with the children() method which returns an Enumeration of all child nodes. We can also use the getChildAt() method to retreive the node corresponding to the given index. There are many methods for, among other things, retrieving and assigning tree nodes, and they are all self-explanitory (or can be understood through simple reference of the API docs). The only methods that deserve special mention here are the overriden toString() method, which returns the String given by the user object's toString() method, and the tree traversal methods which return an Enumeration of nodes in the order they were visited. As discussed above, there are three types of traversal supported: preorder, inorder, and postorder. The corresponding methods are preorderEnumeration(), breadthFirstEnumeration(), depthFirstEnumeration() and postorderEnumeration() (note that the last two methods do the same thing).
A TreePath represents the path to a node as a set of nodes starting from the root. (Recall that nodes are Objects, not necessarily TreeNodes.) TreePaths are read-only objects and provide functionality for comparison between other TreePaths. The getLastPathComponent() gives us the final node in the path, equals() compares two paths, getPathCount() gives the number of nodes in a path, isDescendant() checks whether a given path is a descendant of (i.e. is completely contained in) a given path, and pathByAddingChild() returns a new TreePath instance resulting from adding the given node to the path. See the example of section 17.2 for more about working with TreePaths.
17.1.10 The TreeCellRenderer interface
abstract interface javax.swing.tree.TreeCellRenderer
This interface describes the component used to render a cell of the tree. The getTreeCellRendererComponent() method is called to return the component to render corresponding to a given cell and that cell's selection, focus, and tree state (i.e. whether it is a leaf or a parent, and whether it is expanded or collapsed). This works similar to custom cell rendering in JList and JComboBox (see chapters 9 and 10). To assign a renderer to JTree we use its setCellRenderer() method. Recall that renderer components are not at all interactive and simply act as "rubber stamps" for display purposes only.
DefaultTreeCellRenderer is the default concrete implementation of the TreeCellRenderer interface. It extends JLabel and maintains several properties used to render a tree cell based on its current state, as described above. These properties include Icons used to represent the node in any of its possible states (leaf, parent collapsed, parent expanded) and background and foreground colors to use based on whether the node is selected or unselected. Each of these properties is self-explanitory and typical get/set accessors are provided.
In chapter 2 we discussed the painting and validation process in detail, but we purposely avoided the discussion of how renderers actually work behind the scenes because they are only used be a few specific components. The component returned by a renderer's getXXRendererComponent() method is placed in an instance of CellRenderPane. The CellRenderPane is used to act as the component's parent so that any validation and repaint requests that occur do not propogate up the ancestry tree of the container it resides in. It does this by overriding the paint() and invalidate() with empty implementations.
Several paintComponent() methods are provided to render a given component onto a given graphical context. These are used by the JList, JTree, and JTable UI delegates to actually paint each cell, which results in the "rubber stamp" behavior we have referred to.
17.1.13 The CellEditor interface
Unlike renderers, cell editors for JTree and JTable are defined from a generic interface. This interface is CellEditor and it declares methods for controlling when editing will start and stop, retrieving a new value resulting from an edit, and whether or not an edit request changes the component's current selection.
Object getCellEditorValue(): used by JTree and JTable after an accepted edit to retrieve the new value.
boolean isCellEditable(EventObject anEvent): used to test whether the given event should trigger a cell edit. For instance, to accept a single mouse click as an edit invocation we would override this method to test for an instance of MouseEvent and check its click count. If the click count is 1 return true, otherwise return false.
boolean shouldSelectCell(EventObject anEvent): used to specify whether the given event causes a cell that is about to be edited to also be selected. This will cancel all previous selection, and for components that want to allow editing during an ongoing selection we would return false here. It is most common to return true, as we normally think of the cell being edited as the currently selected cell.
boolean stopCellEditing(): used to stop a current cell edit. This method can be overriden to perform input validation. If a value is found to be unacceptable we can return false indicating to the component that editing should not be stopped.
void cancelCellEditing(): used to stop a current cell edit and ignore any new input.
This interface also declares methods for adding and removing CellEditorListeners which should recieve ChangeEvents whenever an edit is stopped or canceled. So stopCellEditing() and cancelCellEditing() are responsible for firing ChangeEvents to any registered listeners.
Normally cell editing starts with the user clicking on a cell a specified number of times which can be defined in the isCellEditable() method. The component containing the cell then replaces the current renderer pane with its editor component (JTree's editor component is that returned by TreeCellEditor's getTreeCellEditorComponent() method). If shouldSelectCell() returns true then the component's selection state changes to only contain the cell being edited. A new value is entered using the editor and an appropriate action takes place which invokes either stopCellEditing() or cancelCellEditing(). Finally, if the edit was stopped and not canceled, the component retrieves the new value from the editor, using getCellEditorValue(), and overwrites the old value. The editor is then replaced by the renderer pane which is updated to reflect the new data value.
17.1.14 The TreeCellEditor interface
abstract interface javax.swing.tree.TreeCellEditor
This interface extends CellEditor and describes the behavior of a component to be used in editing the cells of a tree. Method getTreeCellEditorComponent() is called prior to the editing of a new cell to set the initial data for the component it returns as the editor, based on a given cell and that cell's selection, focus, and its expanded/collapsed states. We can use any component we like as an editor. To assign a TreeCellEditor to JTree we use its setCellEditor() method.
This is a concrete implementation of the TreeCellEditor interface as well as the TableCellEditor interface (see 18.1.11). This editor allows the use of JTextField, JComboBox, or JCheckBox components to edit data. It defines a protected inner class called EditorDelegate which is responsible for returning the current value of the editor component in use when the getCellEditorValue() method is invoked. DefaultCellEditor is limited to three constructors for creating a JTextField, JComboBox, or a JCheckBox editor.
DefaultCellEditor maintains an int property called clickCountToStart which specifies how many mouse click events should trigger an edit. By default this is 2 for JTextFields and 1 for JComboBox and JCheckBox editors. As expected ChangeEvents are fired when stopCellEditing() and cancelCellEditing() are invoked.
DefaultTreeCellEditor extends DefaultCellEditor, and is the default concrete implementation of the TreeCellEditor interface. It uses a JTextField for editing a node's data (an instance of DefaultTreeCellEditor.DefaultTextField). stopCellEditing() is called when ENTER is pressed in this text field.
An instance of DefaultTreeCellRenderer is needed to construct this editor, allowing renderer icons to remain visible while editing (accomplished by embedding the editor in an instance of DefaultTreeCellEditor.EditorContainer), and fires ChangeEvents when editing begins and ends. As expected, we can add CellEditorListeners to intercept and process these events.
By default, editing starts (if it is enabled) when a cell is triple-clicked or a pause of 1200ms occurs between two single mouse clicks (the latter is accomplished using an internal Timer). We can set the click count requirement using the setClickCountToStart() method, or check for it directly by overriding isCellEditable().
17.1.17 The RowMapper interface
abstract interface javax.swing.text.RowMapper
RowMapper describes a single method, getRowsForPaths(), which maps an array of tree paths to array of tree rows. A tree row corrsponds to a tree cell, and as we discussed, these are organized similar to JList cells. JTree selections are based on rows and tree paths, and we can choose which to deal with depending on the needs of our application. We aren't expected to implement this interface unless we decide to build our own JTree UI delegate.
17.1.18 The TreeSelectionModel interface
abstract interface javax.swing.tree.TreeSelectionModel
The TreeSelectionModel interface describes a base interface for a tree's selection model. Three modes of selection are supported, similar to JList (see chapter 10), and implementations allow setting this mode through the setSelectionMode() method: SINGLE_TREE_SELECTION, DISCONTIGUOUS_TREE_SELECTION, and CONTIGUOUS_TREE_SELECTION. Implementations are expected to maintain a RowMapper instance. The getSelectionPath() and getSelectionPaths() methods are intended to return a TreePath and an array of TreePaths respectively, allowing access to the currently selected paths. The getSelectionRows() method should return an int array representing the indices of all rows currently selected. The lead selection refers to the most recently added path to the current selection. Whenever the selection changes, implementations of this interface should fire TreeSelectionEvents. Appropriately, add/remove TreeSelectionListener methods are also declared. All other methods are, for the most part, self explanitory (see API docs). The tree selection model can be retrieved using JTree's getSelectionModel() method.
DefaultTreeSelectionModel is the default concrete implementation of the TreeSelectionModel interface. This model supports TreeSelectionListener notification when changes are made to a tree's path selection. Several methods are defined for, among other things, modifying a selection, testing if it can be modified, and firing TreeSelectionEvents when a modification occurs.
17.1.20 The TreeModelListener interface
abstract interface javax.swing.event.TreeModelListener
The TreeModelListener interface describes a listener which receives notifications about changes in a tree's model. TreeModelEvents are normally fired from a TreeModel when nodes are modified, added, or removed. We can register/unregsiter a TreeModelListener with a JTree's model using TreeModel's addTreeModelListener() and removeTreeModelListener() methods respectively.
17.1.21 The TreeSelectionListener interface
abstract interface javax.swing.event.TreeSelectionListener
The TreeSelectionListener interface describes a listener which receives notifications about changes in a tree's selection. It declares only one method, valueChanged(), accepting a TreeSelectionEvent. These events are normally fired whenever a tree's selection changes. We can register/unregister a TreeSelectionListener with a tree's selection model using JTree's addTreeSelectionListener() and removeTreeSelectionListener() methods respectively.
17.1.22 The TreeExpansionListener interface
abstract interface javax.swing.event.TreeExpansionListener
The TreeExpansionListener interface describes a listener which receives notifications about tree expansions and collapses. Implementations must define treeExpanded() and treeCollapsed() events, which take a TreeExpansionEvent as parameter. We can register/unregister a TreeExpansionListener with a tree using JTree's addTreeExpansionListener() and removeTreeExpansionListener() methods respectively.
17.1.23 The TreeWillExpandListener interface
abstract interface javax.swing.event.TreeWillExpandListener
The TreeWillExpandListener interface describes a listener which receives notifications when a tree is about to expand or collapse. Unlike TreeExpansionListener this listener will be notified before the actual change occurs. Implementations are expected to throw an ExpandVetoException if it is determined that a pending expansion or collapse should not be carried out. Its two methods, treeWillExpand() and treeWillCollapse(), take a TreeExpansionEvent as parameter. We can register/unregister a TreeWillExpandListener with a tree using JTree's addTreeWillExpandListener() and removeTreeWillExpandListener() methods respectively.
TreeModelEvent is used to notify TreeModelListeners that all or part of a JTree's data has changed. This event encapsulates a reference to the source component, and a single TreePath or an array of path Objects leading to the top-most affected node. We can extract the source as usual, using getSource(), and we can extract the path(s) using either of the getPath() or getTreePath() methods (the former returns an array of Objects, the latter returns a TreePath). Optionally, this event can also carry an int array of node indices and an array of child nodes. These can be extracted using the getChildIndices() and getChildren() methods respectively.
TreeSelectionEvent is used to notify TreeSelectionListeners that the selection of a JTree has changed. One variant of this event encapsulates a reference to the source component, the selected TreePath, a flag specifying whether the tree path is a new addition to the selection (true if so), and the new and old lead selection paths (remember that the lead selection path is the newest path added to a selection). The second variant of this event encapsulates a reference to the source component, an array of selected TreePaths, an array of flags specifying whether each path is a new addition or not, and the new and old lead selection paths. Typical getXX() accessor methods allow extraction of this data.
TreeExpansionEvent is used to encapsulate a TreePath corresponding to a recently, or possibly pending, expanded or collapsed tree path. This path can be extracted with the getPath() method.
ExpandVetoException may be thrown by TreeWillExpandListener methods to indicate that a tree path expansion or collapse is prohibited, and should be vetoed.
17.1.28 JTree client properties and UI defaults
When using the Metal L&F, JTree uses a specific line style to represent the edges between nodes. The default is no edges, but we can set JTree's lineStyle client property so that each parent node appears connected to each of its child nodes by an angled line:
We can also set this property such that each tree cell is separated by a horizontal line:
To disable the line style:
As with any Swing component, we can also change the UI resource defaults used for all instances of the JTree class. For instance, to change the color of the lines used for rendering the edges between nodes as described above, we can modify the entry in the UI defaults table for this resource as follows:
To modify the open node icons used by all trees when a node's children are shown:
UIManager.put("Tree.openIcon", new IconUIResource(
We can do a similar thing for the closed, leaf, expanded, and collapsed icons using Tree.closedIcon, Tree.leafIcon, Tree.expandedIcon, and Tree.collapsedIcon respectively. (See the BasicLookAndFeel source code for a complete list of UI resource defaults.)
17.1.29 Controlling JTree appearance
Though we haven't concentrated heavily on UI delegate customization for each component throughout this book, Swing certainly provides us with a high degree of flexibility in this area. It is particularly useful with JTree because no methods are provided in the component itself to control the indentation spacing of tree cells (note that the row height can be specified with JTree's setRowHeight() method). The JTree UI delegate also provides methods for setting expanded and collapsed icons, allowing us to assign these on a per-component basis rather than a global basis (which is done using UIManager -- see 17.1.28). The following BasicTreeUI methods provide this control, and figure 17.1 illustrates:
void setCollapsedIcon(Icon newG): icon used to specify that a child icon is in the collapsed state.
void setExpandedicon(Icon newG): icon used to specify that a child icon is in the expanded state.
void setLeftChildIndent(int newAmount): used to assign a distance between the left side of a parent node and the center of an expand/collapse box of a child node.
void setRightChildIndent(int newAmount): used to assign a distance between the center of the expand/collapse box of a child node to the left side of that child node's cell renderer.
Figure 17.1 JTree UI delegate icon and indentation properties.
To actually use these methods we first have to obtain the target tree's UI delegate. For example, to assign a left indent of 8 and a right indent of 10:
BasicTreeUI basicTreeUI = (BasicTreeUI) myJTree.getUI();