- The Tree Control
- Tree Appearance
- The TreeNode Interface
- The MutableTreeNode Interface
- The DefaultMutableTreeNode Class
- The TreePath Class
- What is a Leaf?
- Tree Expansion and Traversal
- Expanding and Collapsing Nodes under Program Control
- Tree Expansion Events
- Making Nodes Visible
- Controlling Node Expansion and Collapse
- Tree Model Events
- Implementation Plan for the File System Control
- File System Tree Control Implementation
- Using the File System Tree Control
- Custom Tree Rendering and Editing
- Customizing the Default Tree Cell Renderer
- ToolTips and Renderers
- Custom Cell Editors
- Controlling Which Nodes Can Be Edited
- Controlling Editability by Subclassing JTree
- Programmatic Control of Editors
- Editing Trees with Custom User Objects
- The valueForPathChanged Method
- The Tree Implementation
- The Cell Editor
- The Cell Renderer
- Summary
Controlling Node Expansion and Collapse
As you have seen, you can receive notification that a tree node has been expanded or collapsed by registering a TreeExpansionListener. Sometimes, it is useful to be notified that a node is going to be expanded or collapsed before the change is actually made. JTree supports this by first sending the TreeExpansionEvent for the expansion or collapse to any registered TreeWillExpandListeners. TreeWillExpandListener is an interface with two methods:
public interface TreeWillExpandListener extends EventListener { public void treeWillCollapse(TreeExpansionEvent evt) throws ExpandVetoException; public void treeWillExpand(TreeExpansionEvent evt) throws ExpandVetoException; }
This interface is almost the same as TreeExpansionListener, apart from the fact that the listener methods can throw an ExpandVetoException. Before any node is expanded or collapsed, the tree creates the appropriate TreeExpansionEvent and delivers it to each registered TreeWillEx-pandListener. If every listener returns without throwing an exception, the node changes state and the TreeExpansionEvent is then delivered to all TreeExpansionListeners. However, if any listener throws an ExpandVe-toException, the node will not be expanded or collapsed and no TreeEx-pansionListeners will receive the TreeExpansionEvent.
You can use a TreeWillExpandListener to decide whether to allow part of a tree to be expanded or collapsed based on any criterion you chose. Suppose, for example, we want to change our ongoing tree example so that, once the Apollo 11 node has been made visible, the tree cannot be collapsed in such a way as to hide that node. All we need to do to enforce this is to add a TreeWillExpandListener and perform the appropriate checks in the treeWillCollapse method. You can run a version of the example that has this feature using the command
java JFCBook.Chapter10.TreeExample5
When this example starts, if you wait for 15 seconds, the tree will open to show the node for Neil Armstrong and you'll see the following events delivered:
Tree will expand event, path [null] Expansion event, path [null] Tree will expand event, path [null, Apollo] Expansion event, path [null, Apollo] Tree will expand event, path [null, Apollo, 11] Expansion event, path [null, Apollo, 11]
As you can see, even though the code only requests that one node be expanded, because its parent nodes also need to be opened in order to make the target node visible, the TreeWillExpandListener treeWillExpand method is invoked before each node is expanded. Since no ExpandVetoEx-ceptions were thrown, the operation completes normally.
Now click on the 11 node. Under normal circumstances, this would cause the node to close, but on this occassion, it does not and the following output appears in the command window:
Tree will collapse event, path [null, Apollo, 11] Veto collapse of path
In this case, the collapse has been vetoed, so the node stays in its expanded state. The same thing happens if you click on either the Apollo node or the root node, because these are all the nodes that could be closed to hide the node for Neil Armstrong. On the other hand, you can still open and close the other Apollo nodes and all of the Skylab nodes.
Here is the code that has been added to enforce this policy:
t.addTreeWillExpandListener(new TreeWillExpandListener() { public void treeWillExpand(TreeExpansionEvent evt) throws ExpandVetoException { System.out.println("Tree will expand event, path " + evt.getPath()); } public void treeWillCollapse(TreeExpansionEvent evt) throws ExpandVetoException { System.out.println("Tree will collapse event, path " + evt.getPath()); DefaultMutableTreeNode rootNode = (DefaultMutableTreeNode)t.getModel().getRoot(); DefaultMutableTreeNode apolloNode = (DefaultMutableTreeNode)rootNode.getFirstChild(); DefaultMutableTreeNode apollo11Node = (DefaultMutableTreeNode)apolloNode.getFirstChild(); // Build the path to the Apollo 11 node TreeNode[] pathToRoot = apollo11Node.getPath(); TreePath apollo11Path = new TreePath(pathToRoot); // Don't allow Apollo 11 node or any ancestor // of it to collapse. if (evt.getPath().isDescendant(apollo11Path)) { System.out.println("Veto collapse of path"); throw new ExpandVetoException(evt); } } }) ;
The treeWillCollapse method contains the code of interest. It first constructs a TreePath for the 11 node in the normal way. Having done this, it uses the TreePath isDescendent method to determine whether the 11 node is a descendent of the node whose TreePath is delivered in the Tree-ExpansionEvent. If it is, then the operation should not be permitted, so an ExpandVetoException is thrown. Notice that the ExpandVetoException constructor requires the TreeExpansionEvent that is being vetoed as an argument.
Selecting Items in a Tree
If you want a tree to be useful for user feedback, you need to be allow the user to select items and to take action when those selections are made. JTree, like JList, has a separate selection model that offers several different selection modes. This section looks at how to control and respond to a selection within a tree.
Selection and the TreeSelectionModel
JTree delegates management of the selection to a selection model. By default, an instance of the class DefaultTreeSelectionModel is associated with the tree when it is created, but you can, if you wish, define your own selection model, as long as it implements the TreeSelectionModel interface. You can switch in your custom selection model using the setSe-lectionModel method. In this section, it is assumed that the default selection model is in use.
The selection model can be configured to operate in one of three different modes:
SINGLE_TREE_SELECTION: only one node from the tree can be selected at any given time.
CONTIGUOUS_TREE_SELECTION: any number of nodes can be selected, as long as they occupy adjacent rows.
DISCONTIGUOUS_TREE_SELECTION: any number of nodes can be selected, with no restriction on location relative to other nodes.
The default mode is DISCONTIGUOUS_TREE_SELECTION.
Changes in the selection can be made either under program control or by the user. No matter how the selection is changed, the selection model ensures that any changes fit in with the current selection mode. Changes to the selection also generates an event, as you'll see later.
The user can create or change the selection by using the mouse or the keyboard. Clicking on a node or pressing SPACE when the node has the focus will make the selection contain just that single node. Clicking or pressing SPACE on another node removes the first node from the selection and selects only the item just clicked on. Selection of a single item in this way is always possible, unless there is no selection model, in which case no selection is allowed. You can create this situation with the following lines of code:
JTree t = new JTree() t.setSelectionModel(null); // No selection allowed
Core Warning
If you disable all selection the user will be unable to navigate the tree and expand or collapse nodes using the keyboard. For this reason, it is usually better not to completely disallow selection, but instead to change the mode of the TreeSelectionModel to SINGLE_TREE_SELECTION. If you don't want the selected node to look selected, you can implement a custom renderer that will draw the selected node differently, perhaps by drawing a border around it but not filling the background with the selection color. Custom renderers are discussed later in this chapter.
Holding down the CTRL key while clicking on a node (or pressing the SPACE key) adds that node to the selection, leaving the existing content of the selection intact. If the node clicked on is already in the selection, it is removed. However, there are some restrictions on this, depending on the selection mode:
If the selection mode is SINGLE_TREE_SELECTION, the CTRL key is ignoredthat is, if there is already a node in the selection it is de-selected and the new node replaces it.
If the selection mode is CONTIGUOUS_TREE_SELECTION, the new node must be adjacent to the row or rows that make up the current selection. If this is not the case, the selection is cleared and the new node becomes the only member of the selection.
If the selection mode is CONTIGUOUS_TREE_SELECTION and the first or last row in the selection is clicked with CTRLpressed, that node is removed from the selection leaving the rest of the nodes selected.
If the selection mode is CONTIGUOUS_TREE_SELECTIONand the node clicked is one of the selected nodes but is not at the top or bottom of the selected range, the selection is completely cleared. The node clicked is not added to the selection.
Holding down the SHIFT key while clicking or navigating the tree with the arrow keys creates a contiguous selection between two selected nodes. This is not allowed if the selection model is in SINGLE_TREE_SELECTION mode. In that case, the clicked node would become the selected node.
When a selection is made, the path most recently selected is often the one of most interest to the application. This path is called the lead selection path and its TreePath or row number can be specifically requested by the program using the getLeadSelectionPath or getLeadSelectionRow methods (see Table 10-3).
Core Note
There is one case in which the lead row is not the last row selectedsee the discussion of tree selection events below for details.
There are, in fact, several distinguished rows and paths that the tree and/ or the selection model treat as special:
Minimum Selection Row |
The lowest numbered row that is part of the selection. This is simply the row number of the selected node nearest the top of the tree. |
Maximum Selection Row |
The highest numbered row that is part of the sele-tion. Not surprisingly, this is the row number of the selected node nearest the bottom of the tree. |
Lead Selection Row or Path |
Usually the last node to be added to the selection. |
Anchor Selection Path |
When only one node is selected, this is the same as the lead selection. When a row is added to an existing selection using the CTRL key, that row becomes both the lead and anchor selection. When a selection already exists and it is extended by using the SHIFT key, the anchor selection is the row that was already selected and which forms the other end of the selection from the most recently selected row (which is the lead selection). The concept of the anchor selection is supported only by JTree and was introduced in Java 2 version 1.3 |
There is a collection of methods, listed in Table 10-3, that can be used to query or change the selection. Unless otherwise indicated, all of these methods are provided by both DefaultTreeSelectionModel and, for convenience, by JTree. There are also several methods that are only available from JTree.
The selection can span many rows of a tree; an extreme case would be opening the whole tree, clicking on the root node, then scrolling to the bottom of the tree and holding down SHIFT while clicking on the last node which, unless the tree is in SINGLE_TREE_SELECTION mode, would select every node in the tree. Keep in mind, however, that only nodes that are visible can be selected using the mouse: Selecting a node that has child nodes does not select any of the child nodes (and vice versa). Also, if you select a node or nodes in a subtree and then collapse that subtree so that those nodes are no longer visible, they become deselected and the selection passes to the root of the closed subtree, whether or not it was selected when the node was collapsed.
It is possible to programmatically select nodes that are not visible. What happens when you do this depends on the version of Swing that you are using. In Swing 1.1.1 and Java 2 version 1.2.2, when you select an invisible node, its
Table 103 Methods Used to Manage the Selection
Method |
Description |
public int getSelectionMode() |
Returns the current tree selection mode. |
public void setSelectionMode(int mode) |
Sets the tree selection mode. |
public Object getLastSelectedPathComponent() |
If there is an item selected, returns its last path component, or null if there is no selected item. The result of calling this method when there is more than one item selected is not very useful because it does not always use the most recently selected item. This method is provided only by JTree;it is not supported by DefaultTreeSelection-Model. |
public TreePath getAnchorSelectionPath() (Java 2 version 1.3 only) |
Returns the path for the anchor selection. This method is provided only by JTree;it is not supported by DefaultTreeSelection-Model. |
public voidsetAnchorSelectionPath(TreePathpath) |
|
(Java 2 version 1.3 only) |
Sets the anchor selection path. This method is provided only by JTree; it is not supported by DefaultTreeSelectionModel |
public TreePath |
Returns the path for the lead selec- |
getLeadSelectionPath() |
tion. |
public void setLeadSelectionPath() |
Sets the path for the lead selection. |
(Java 2 version 1.3 only) |
This method is provided only by JTree; it is not supported by DefaultTreeSelection-Model. |
public int getMaxSelectionRow() |
Returns the highest selected row number. |
public int getMinSelectionRow() |
Returns the lowest selected row number. |
public int getSelectionCount() |
Gets the number of selected items. |
public TreePath getSelectionPath() |
Returns the TreePathobject for the first item in the selection. |
public TreePath[] getSelectionPaths() |
Returns the TreePathobjects for all of the selected items. |
public int[] getSelectionRows() |
Returns the row numbers of all of the items in the selection. |
public boolean isPathSelected (TreePath path) |
Returns trueif the given path is selected, falseif not. |
public boolean isRowSelected(int row) |
Returns trueif the given row is selected, falseif not. |
public boolean isSelectionEmpty() |
Returns trueif there are no selected items. |
public void clearSelection() |
Removes all items from the selection. |
public void removeSelectionInterval (int row0, int row1) |
Removes the rows in the given range from the selection. This method is provided only by JTree- it is not supported by DefaultTreeSelectionModel. |
public void removeSelectionPath(TreePath path) |
Removes one path from the selection. |
public void removeSelectionPaths(TreePath[] paths) |
Removes the listed paths from the selection. |
public void removeSelectionRow (int row) |
Removes the given row from the selection. This method is provided only by JTree;it is not supported by DefaultTreeSelection-Model. |
public void removeSelectionRows(int[] rows) |
Removes the listed rows from the selection. This method is provided only by JTree;it is not supported by DefaultTreeSelection-Model. |
public void addSelectionInterval(int row0, int row1) |
Adds the rows in the given range to the current selection. |
public void addSelectionPath(TreePath path) |
Adds the given path to the selection. |
public void addSelectionPaths(TreePath[] path) |
Adds the given set of paths to the selection. |
public void addSelectionRow(int row) |
Adds the object on the given row to the selection. |
public void addSelectionRows(int[] row) |
Adds the objects in the given rows to the selection. |
public void setSelectionInterval(int row0, int row1) |
Makes the selection equal to all rows in the given range. This method is provided only by JTree; it is not supported by DefaultTreeSelectionModel. |
public void setSelectionPath(TreePath path) |
Makes the selection be the given path. |
public void setSelectionPaths (TreePath[] path) |
Sets the selection to the given set of paths. |
public void setSelectionRow(int row) |
Sets the selection to the object on the given row. This method is provided only by JTree; it is not supported by DefaultTreeSelection-Model. |
public void setSelectionRows(int[] row) Sets the selection to the set of objects in the given rows. This method is provided only by JTree; it is not supported by Default |
TreeSelectionModel. |
parent and any other collapsed descendents are expanded so that the newly selected node is visible. In Java 2 version 1.3, this is also the default behavior, but you can call the following method with argument false to disable it:
public void setExpandsSelectedPaths(boolean cond);
Tree Selection Events
A change in the selection is reported using a TreeSelectionEvent. To receive these events, you must register a TreeSelectionListener using the addTreeSelectionListener method of your tree's TreeSelection-Model, or the convenience method of the same name provided by JTree. The TreeSelectionListener interface has only one method:
public void valueChanged(TreeSelectionEvent evt)
You need to inspect the accompanying event to determine what changes were made to the selection.
TreeSelectionEvents tell you exactly which paths are involved in the last change of selection and whether they were added or removed from the selection. You can also get the leading path and the old leading path from the event. The important methods supplied by TreeSelectionEvent are shown in Table 10-4.
Table 104 Methods Used with Tree Selection Events
Method |
Description |
public TreePath[] getPaths() |
Returns the paths for all of the nodes that were affected by the last selection change. |
public boolean isAddedPath (TreePath path) |
Assuming that path represents a node affected by this event, this method returns true if the node was added to the selection and false if it was removed from the selection. |
public boolean isAddedPath (int index) |
Returns true if the path in entry index of the paths delivered by this event was added to the selection, false if it was removed from the selection. This method was added in Java 2 version 1.3 |
public TreePath getPath() |
Returns the first TreePath in the set that would be returned by getPaths. This is a convenience method for the case where you know there is only one path of interest. |
public boolean isAddedPath() |
Returns trueif the path returned by getPathwas added to the selection and falseif it was removed. |
public TreePath getOldLeadSelection() |
Returns that path for the node that was the lead selection before this event. |
public TreePath getNewLeadSelection() |
Gets the path for the node that is now the lead selection. |
The simplest way to understand how TreeSelectionEvents work is to see an example. The program JFCBook.Chapter10.TreeSelection-Events uses the tree that has been used throughout this chapter, together with some code to receive and print the contents of selection event:
t.addTreeSelectionListener(new TreeSelectionListener() { public void valueChanged(TreeSelectionEvent evt) { System.out.println("=======\nFirst path: " + evt.getPath() + "; added? " + evt.isAddedPath()); System.out.println("Lead path: " + evt.getNewLeadSelectionPath()); System.out.println("Old Lead path: " + evt.getOldLeadSelectionPath()); TreePath[] paths = evt.getPaths(); for (int i = 0 ; i < paths.length; i++) { System.out.println("Path: < " + paths[i] + "; added? " + evt.isAddedPath(paths[i])); } System.out.println("Tree's lead selection path is " + t.getLeadSelectionPath()); } });
The listener is added using the addTreeSelectionListener method of JTree. Another way to do this is to register with the selection model itself:
TreeSelectionModel m = t.getSelectionModel(); m.addTreeSelectionModel(new TreeSelectionListener() {
In the event handler, the various paths are extracted and displayed, along with the flag indicating whether they were added or removed from the selection. To see some typical events, run the program and open the tree by expanding the Apollo node, the 11 node, and the 12 node; you should now have 16 rows on the screen.
First, select Neil Armstrong. Not surprisingly, this generates an event in which all the entries refer to this node:
First path: [null, Apollo, 11, Neil Armstrong]; added? true Lead path: [null, Apollo, 11, Neil Armstrong] Old Lead path: null Path: < [null, Apollo, 11, Neil Armstrong]; added? True
Next, click Buzz Aldrin, causing this entry to be selected and the previous one deselected. This time, the event contains entries for both paths:
First path: [null, Apollo, 11, Buzz Aldrin]; added? True Lead path: [null, Apollo, 11, Buzz Aldrin] Old Lead path: [null, Apollo, 11, Neil Armstrong] Path: < [null, Apollo, 11, Buzz Aldrin]; added? True Path: < [null, Apollo, 11, Neil Armstrong]; added? false
Here, the first path and the lead path are the one that has just been added, the old lead path is the lead path from the previous event, and the complete set of paths affected contains both the selected and the deselected path, together with booleans that indicate which of the two has just been selected. As long as you can be sure that only one item is being selected at a time (for example, if you use single-selection mode), each event will be self-contained and will tell you all you need to know about the state of the selection, as this one does. Things aren't always so simple, however. Now that you've got one path selected, hold down the CTRL key and click on Pete Conrad. This gives a selection of two individual items, but the event that it generates is not so obviously informative:
First path: [null, Apollo, 12, Pete Conrad]; added? True Lead path: [null, Apollo, 12, Pete Conrad} Old Lead path: [null, Apollo, 11, Buzz Aldrin} Path: < [null, Apollo, 12, Pete Conrad]; added? True
You can see that Pete Conrad has just been added to the selection and that Buzz Aldrin must also be in the selection because that entry used to be the leading path, but note that the complete set of paths reported doesn't include Buzz Aldrin. This is because a TreeSelectionEvent only reports changes to the selectionanything that doesn't change is not mentioned. To emphasize this further, hold down CTRL and click Alan Bean. This gives three selected items and the following event:
First path: [null, Apollo, 12, Alan Bean]; added? True Lead path: [null, Apollo, 12, Alan Bean] Old Lead path: [null, Apollo, 12, Pete Conrad] Path: < [null, Apollo, 12, Alan Bean]; added? true
Now, there's no mention of Buzz Aldrin at all and the set of paths still contains only the one entry. This is, of course, because only one path changed state.
Using the mouse with no modifiers or with the CTRL key pressed always generates events with only either one or two entries in the paths list. You can get events with more entries if you use the SHIFT key to select a range of items. To see an example, hold down the SHIFT key and click on Apollo. The SHIFT key selects all items between itself and the anchor of the existing selection. In this case, the selection will contain the items Apollo, 11, Neil Armstrong, Buzz Aldrin, Michael Collins, 12, Pete Conrad and Alan Bean, the last of which was the anchor selection before the selection changed. Recall that the anchor selection is the same as the lead selection provided that the SHIFT key has not been used.
Here is the event that is created for this selection:
First path: [null, Apollo]; added? true Lead path: [null, Apollo] Old Lead path: [null, Apollo, 12, Alan Bean] Path: < [null, Apollo]; added? true Path: < [null, Apollo, 11]; added? true Path: < [null, Apollo, 11, Neil Armstrong]; added? true Path: < [null, Apollo, 11, Michael Collins]; added? true Path: < [null, Apollo, 12]; added? true Tree's lead selection path is [null, Apollo]
As expected, the node that was clicked on, Apollo, is now the lead path and is listed as having been added, along with the nodes for Neil Arm-strong and Michael Collins.
So far, the lead path has always been the last path that was selected and this is usually the case, but there is an exception. Hold down the SHIFT key again and click on 13, to create another block selection. You might expect that 13 would now be the lead path, but the event says otherwise:
First path: [null, Apollo, 12, Richard Gordon]; added? true Lead path: [null, Apollo, 12, Alan Bean] Old Lead path: [null, Apollo] Path: < [null, Apollo, 12, Richard Gordon]; added? true Path: < [null, Apollo, 13]; added? true Path: < [null, Apollo]; added? false Path: < [null, Apollo, 11]; added? false Path: < [null, Apollo, 11, Neil Armstrong]; added? false Path: < [null, Apollo, 11, Buzz Aldrin]; added? false Path: < [null, Apollo, 11, Michael Collins]; added? false Path: < [null, Apollo, 12]; added? false Path: < [null, Apollo, 12, Pete Conrad]; added? false Tree's lead selection path is [null, Apollo, 12, Alan Bean]
The lead path is, in fact, Alan Bean, the path at the top of the block selection. For block selections, this appears to be the rule because, as you saw in the previous example, if you click above the selection with SHIFT held down, the path that was clicked on, which will be at the top of the new block, is the lead path.
Finally, note that a TreeSelectionEvent is delivered only if the selection changes. To see this, click on Richard Gordon. This clears the old block selection and selects just one entry. Now click on Richard Gordon again. This time, you don't get an eventthere is nothing to report because nothing has changed. To see why this is important, double-click on Apollo. This delivers just one event. Double-click again and you get no event at all. Many programs allow the user to pick an object from a list by double-clicking: This isn't going to be possible if you rely on TreeSelectionEvents to tell you that this has happened. We'll see below how to handle double-selection as a means of selecting an entry in a tree.
If you don't need (or want) to keep track of exactly which paths have been affected by an event, you can just treat the event as notification that something has changed and query the tree model for the selected paths, as long as the model is derived from DefaultTreeModel. The methods that you can use, which are also implemented by JTree for convenience, were shown earlier in Table 10-4.
Converting Between Locations and Tree Paths or Rows
To make it possible for the user to choose an item from the tree by double-clicking, you have to register a listener for mouse events, detect a double click, and convert the mouse location to a tree path. Fortunately, JTree has a convenience method that converts coordinates to a path in the tree, from which, as you know, you can get the node itself if you need it. Here's an example of the code that you would use to implement this:
// Here, 't' is a JTree. It must be declared as final // so that we can use it in the event handler // final JTree t; t.addMouseListener(new MouseAdapter() { public void mouseClicked(MouseEvent evt) { if (evt.getClickCount() == 2) { // A double click get the path TreePath path = t.getPathForLocation(evt.getX(), evt.getY()); if (path != null) { Object comp = path.getLastPathComponent(); if (t.getModel().isLeaf(comp)) { System.out.println("Selected path is " + path); } } } } });
Once the TreePath has been obtained, you can take any action that you need to in order to respond to the user's gesture. In this case, the code checks that the node being clicked on is a leaf node, so as not to confuse the use of double-clicking on a non-leaf node to open or close it with this use as a selection mechanism. Note that, before testing whether the chosen object is a leaf, a check is made that the getPathForLocation method returns a non-null TreePath. This is necessary, because the user could click inside the tree but outside of any area occupied by a tree node.
There are several methods that can be used to map between coordinates and parts of the tree and vice versa. These methods are summarized in Table 105.
Table 105 Mapping between coordinates and JTree
Method |
Definition |
public TreePath getClosestPathForLocation(int x, int y) |
Gets the TreePathobject for the node closest to the given position in the tree. |
public int getClosestRow- |
Gets the row number of the node closest |
ForLocation(int x, int y) |
to the given position in the tree. |
public Rectangle getPath-Bounds(TreePath path) |
Gets a Rectangledescribing the area bounding the node corresponding to the given TreePath. The coordinates are relative to the top-left hand corner of the tree. |
public TreePath getPath-ForLocation(int x, int y) |
Gets the TreePathfor the node that occupies the given coordinates relative to the top left of the tree. Returns nullif there is no node at the given location. |
public TreePath getPath-ForRow(int row) |
Converts a row number to the TreePath for the node at that row. |
public Rectangle getRow-Bounds(int row) |
Gets a Rectangledescribing the area bounding the node at the given row. The coordinates are relative to the top-left hand corner of the tree. |
public int getRowForLocation(int x, int y) |
Gets the row number for the node that occupies the given coordinates relative to the top left of the tree. |
Traversing a Tree
Sometimes it is necessary to be able to traverse some or all of a tree, or to be able to search a subtree. The TreeNode interface allows you to find the parent of a given node and to get an enumeration of all of a node's children, which is all you need to implement your own searching mechanism. If all you can be sure of is that your tree is populated with TreeNodes (which is always a safe assumption), then you will have to be satisfied with the rather primitive get-Parent and children methods. On the other hand, if your tree is composed of DefaultMutableTreeNodes, you can make use of the following more powerful methods to traverse the tree or a subtree in various different orders:
public Enumeration pathFromAncestorEnumeration( TreeNode ancestor) public Enumeration preorderEnumeration() public Enumeration postorderEnumeration() public Enumeration breadthFirstEnumeration() public Enumeration depthFirstEnumeration()
The pathFromAncestorEnumeration method is a little different from the other four, so we deal with it separately. This method involves two nodesthe one against which it is invoked (the target node) and the one passed as an argument, which must be an ancestor of the first node (if it is not, an IllegalArgumentException is thrown). Assuming that the ancestor is valid, this method walks the tree between it and the target node, adding to the enumeration each node that it encounters on the way. The first entry in the enumeration is the ancestor given as an argument and the last item is the target node itself. The other items in the enumeration are returned in the order in which they were encountered in the treein other words, each node is the parent of the one that follows it in the enumeration.
Of the other four enumerations, depthFirstEnumeration is the same as postOrderEnumeration. To see how the others order the nodes in the subtree that they cover, Listing 10-2 shows a short program that creates a small tree and applies preorderEnumeration, postorderEnumeration, and breadthFirstEnumeration to its root node. You can run this program for yourself by typing:
java JFCBook.Chapter10.TreeEnumerations
Listing 102 Various tree traversal enumerations
package JFCBook.Chapter10; import javax.swing.*; import javax.swing.tree.*; import java.util.*; public class TreeEnumerations { public static void main(String[] args) { JFrame f = new JFrame("Tree Enumerations"); DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode("Root"); for (int i = 0; i < 2; i++) { DefaultMutableTreeNode a = new DefaultMutableTreeNode("" + i); for (int j = 0; j < 2; j++) { DefaultMutableTreeNode b = new DefaultMutableTreeNode("" + i + "_" + j); for (int k = 0; k < 2; k++) { b.add(new DefaultMutableTreeNode( ("" + i + "_" + j + "_" + k)); } a.add(b); } rootNode.add(a); } JTree t = new JTree(rootNode); f.getContentPane().add(new JScrollPane(t)); f.setSize(300, 300); f.setVisible(true); // Now show various enumerations printEnum("Preorder", rootNode.preorderEnumeration()); printEnum("Postorder", rootNode.postorderEnumeration()); printEnum("Breadth First", rootNode.breadthFirstEnumeration()); } public static void printEnum(String title, Enumeration e) { System.out.println("===============\n" + title); while (e.hasMoreElements()) { System.out.print(e.nextElement() + " "); } System.out.println("\n"); } }
The tree that this program generates is shown in Figure 109; the output follows. Here is what a preorder enumeration produces:
=============== Preorder Root 0 0_0 0_0_0 0_0_1 0_1 0_1_0 0_1_1 1 1_0 1_0_0 1_0_1 1_1 1_1_0 1_1_1
As you can see, the preorder enumeration starts at the root and walks down the tree as far as it can go, adding the nodes that it traverses (0, 0_0, 0_0_0) to the enumeration. Having reached the bottom of the tree, it then walks across the children of the last node it traversed and adds them (0_0_1). Next, it moves back up to 0_0 and across to its sibling 0_1, which it adds to the enumeration and then descends and traverses its children (0_1_0, 0_1_1). This completes the traversal of the 0 subtree. Next, the subtree rooted at 1 is scanned in the same order.
Figure 109 A tree to demonstrate tree traversal enumerations
The postorder enumeration is very similar, except that it adds the parent nodes that it traverses as it crosses them on the way up, not on the way down. In other words, whereas the preorder traverse announced a subtree and then added its contents, postorder enumeration adds the contents and then adds the root of the subtree. If you think in terms of a file system, this is like processing all of the files in a directory and then applying the operation to the directory itself, which is the required order for removing a directory and its contents (since the directory must be empty to be removed).
=============== Postorder 0_0_0 0_0_1 0_0 0_1_0 0_1_1 0_1 0 1_0_0 1_0_1 1_0 1_1_0 1_1_1 1_1 1 Root
Breadth-first enumeration is much simpler to visualizeit just walks across all the nodes at each level and adds them to the enumeration, then goes down a level and lists all of the nodes at that level, and so on. In terms of the tree's display, if you open all of the subtrees so that every node in the tree is visible, you'll see that this enumeration walks vertically down the screen and, when it reaches the bottom, goes back up and across to the right one level, then back down the screen again, and so on. The effect is that you see everything at depth 0, then everything at depth 1, followed by depth 2 and so on.
=============== Breadth First Root 0 1 0_0 0_1 1_0 1_1 0_0_0 0_0_1 0_1_0 0_1_1 1_0_0 1_0_1 1_1_0 1_1_1
Changing Tree Content
Some trees will be static once you've created them, but many will not. If you need to change the content of the tree's data model, there are two ways you can do itat the model level and at the node level. You need to handle these two possibilities slightly differently, so they are described separately in this section.
Making Changes via DefaultTreeModel
The generic TreeModel interface doesn't offer any way to change the content of the modelit assumes that any changes will be made at the node level. The problem with just changing the nodes is that the screen display of the tree won't be updated. It isn't sufficient just to make changes to the datathe tree's user interface class must also be told about these changes.
If you want to handle changes at the node level, you'll see what you need to do in the next section. If you don't want to go to that much trouble, however, there are two convenience methods in the DefaultTreeModel that do everything for you:
public void insertNodeInto(MutableTreeNode child, MutableTreeNode parent, int index) public void removeNodeFromParent(MutableTreeNode parent)
These methods make the changes that their names imply by manipulating the node data in the model, then they arrange for the tree's on-screen representation to be updated by invoking the nodesWereInserted and nodesWereRemoved methods of DefaultTreeModel that will be covered shortly. If you are making a small number of changes to the model, insert-NodeInto and removeNodeFromParent are the simplest way to do it. However, because these methods generate one event for each node you add or remove, if you are adding and changing more than a few nodes, it can be more efficient to side-step them and manipulate the nodes directly because, by doing so, you can generate fewer events.
Making Changes at the Node Level
DefaultMutableTreeNode has six methods that allow you to make changes to the data model by directly manipulating the nodes themselves:
public void add(MutableTreeNode child) public void insert(MutableTreeNode child, int index) public void remove(int index) public void remove(MutableTreeNode child) public void removeAllChildren() public void removeFromParent()
With the exception of the last, all these are invoked against the parent to be affected by the change; removeFromParent is invoked on the child node to be removed from its parent. The effect of these methods is obvious from their names. All that they do, however, is to update the tree's data model and keep it in a consistent state. They do not inform the tree itself that it may need to redisplay its contents. After making any changes to the node structure, you need to invoke one of the following DefaultTreeModel methods to ensure that the tree's appearance matches its internal data:
public void reload() public void reload(TreeNode node) public void nodesWereInserted(TreeNode node, int[] childIndices) public void nodesWereRemoved(TreeNode node, int[] childIndices, Object[] removedChildren) public void nodeStructureChanged(TreeNode node)
The first of these methods is the most radical solution: It tells the JTree to discard everything that it has cached about the structure of the tree and to rebuild its view from scratch. This could be a very slow process and is not recommended except when you really have replaced the entire data model. The second form of the reload method gives a node as a limiting point. When this method is used, the tree assumes that there may have been radical changes but that they were confined to the subtree rooted at the node given as the argument. In response, the tree rebuilds its view from that point in the hierarchy downward. This method is ideal if you make major changes under a node, such as populating it from scratch or removing all of its children. You might, for example, use this after calling removeAllChildren and pass the node whose children were removed as its argument:
// Delete everything below "node" and tell the //tree about it node.removeAllChildren(); model.reload(node); // "model" is the DefaultTreeModel
The method nodeStructureChanged is identical to reload.
The other two methods are used for finer control over the mechanism. You can use these to minimize the impact of any changes by informing the tree exactly which nodes were added or removed. Both of these methods require you to accumulate a list, in ascending numerical order, of the indices of all of the nodes that were added or removed and, in the latter case, a list of all of the removed nodes themselves. This is obviously more complex than just using nodeStructureChanged, but it can be more efficient. These methods have the limitation that they can only inform the tree about changes in one parent node at a time. If you make changes to more than one node, for example, to several levels in the hierarchy, you need to invoke these methods once for each parent node affected, which can increase the complexity of the task, but this may be justified by the performance gains.
Changing the Attributes of a Node
There is a third type of change to the tree model that hasn't been covered so farchanges to the contents of the nodes themselves. Suppose that you make a change to a node that doesn't affect its relationship to its neighbors but should result in its appearance changing. This might happen if you change the value of the node's user object, as might be the case if your tree represents a file system and you allow the user to rename a file or directory. When this happens, you can use one of the following DefaultTreeModel methods:
public void nodeChanged(TreeNode changedNode); public void nodesChanged(TreeNode parentNode, int[] childIndices)
Notice the important difference between these two methods: When you change one node, you pass a reference to that node, but if you change several, then you must pass a reference to their common parent and a set of indices that identify the affected children. If you make changes that affect nodes in several parts of the tree, then you must invoke nodesChanged once for each node that has children that have been changed.