|
Comments
Did you read today's front page stories & breaking news?
SYS-CON.TV
|
General Java Climbing a JTree for the First Time
Climbing a JTree for the First Time
By: Mark Steenbarger
Apr. 1, 1999 12:00 AM
Since the introduction of the Java Foundation Classes (JFC), Java applications have been able to be implemented using a rich set of window components. These components - called Swing - along with customizable "look and feel," allow applications to be implemented without relying on a native windowing system. With the release of Java 2 (a.k.a. JDK 1.2), the JFC has found a permanent home as part of the JDK rather than being distributed separately. Swing includes two very powerful but complex components called JTable and JTree. This article focuses solely on the JTree and explores various aspects of the JTree by using two examples that show how business objects can be visually represented within the JTree component. Using the Model-View-Controller (MVC) pattern, originally from Smalltalk, the majority of the Swing classes are implemented by using a variation of the MVC that collapses the view and controller into a single class called the delegate. (For more details on this topic, you can visit an online Swing tutorial, "What is Swing? - 2"). A working understanding of the model/delegate relationship will help you understand the classes and interfaces that accompany each of the Swing components. As stated by Sun, "With the JTree class, you can display hierarchical data. JTree doesn't actually contain your data; it's simply a view of the data." The challenge comes from associating business objects with a corresponding Swing object. So how do you represent your current business objects in a JTree without altering your class definition of the business objects? This article covers two alternatives. In both examples the objectives are to minimize (1) the coupling between the business objects and the JTree, and (2) the amount of effort and code to accomplish the tasks. In both examples I use a class called Vehicle that represents any business object and that can be displayed in hierarchical fashion. Since the JTree is designed for displaying data with hierarchical properties, the only requirement is that there be methods within the business object's class definition to implement navigation within a hierarchical structure. In the examples, the Vehicle class has subtypes that provide a hierarchical structure. For example, one instance of the Vehicle class might be called "Motor Vehicle." The class could contain subtypes of Car, Truck and Van. These vehicles, while separate, share the commonality of being a motor vehicle. Throughout this article I use terminology common to the JTree API and its associated classes and interfaces. The terminology is defined for you in the JTree Terminology table. One aspect of the JTree that makes it more complex than other Swing classes is that the associated model in the model-delegate pattern for the JTree isn't where data is maintained. Take the JTextField class, for example. In JTextFields, the view (JTextArea) offers a setText(String) method, and its associated model (called a document) offers an insertString(int, String, AttributeSet). Both methods allow manipulation of the underlying data. In the case of the JTree, neither the JTree class nor its associated model interface - the javax.swing.tree.TreeModel - offers a means of manipulating the underlying data. Another aspect of the JTree is the add(Component) method, which comes from being a subclass of the Container class. This method, however, is used for performing additions in the sense of containment (i.e., JPanels are commonly used to contain several other components by adding them to the JPanel), not for adding data to the JTree. In the first example - AddData_ExampleA.java (see Listing 1) - associating a business object's data to the JTree is done using the default helper classes: javax.swing.tree.DefaultTreeModel and javax.swing.tree.DefaultMutableTreeNode. The default model class consists of the methods insertNodeInto(MutableTree-Node, MutableTreeNode, int) and removeNodeFromParent(MutableTree-Node). These methods allow the addition and removal of nodes from the JTree. Since my business object, the Vehicle class, doesn't implement the MutableTreeNode interface, it can't be directly added to the JTree. Therefore, to make it a valid MutableTreeNode without altering the class definition, I "wrap" the Vehicle class using the DefaultMutableTreeNode.
Node: Any position within the JTree where data associated with the business object is being represented. Path: A collection of a contiguous set of nodes. A path can contain one or many nodes. A null path indicates a zero node path or an empty path. The collection of nodes will consist of a strict ancestry line. (If you think of a traditional organizational chart as a tree, then an example of a path would be the line drawn from you to the president or CEO.) Leaf: A special kind of node. As its name implies, this is the node at the end of a path. There are no more nodes connected to the leaf node. (Using the organizational chart example again, the leaf is the person that has no personnel reporting to him or her.) Root: A special kind of node. In comparison to a leaf, a root's parent information is never examined. It's the highest point within the hierarchy. A root's parent relationship either does not exist or doesn't need to be displayed. Parent: Represents a node's relationship with another node. In a parent/child relationship, the parent is analogous to a super class within the realms of object-oriented concepts. Child: Represents a node's relationship with another node. In a parent/child relationship, the child is analogous to a subclass of its parent. It inherits all the properties associated with its parent. (Note: As of JDK 1.2/Swing 1.1, a node can have only one parent.)
User Object: Refers to the business object associated with a node. While not required, all user objects will usually be of the same class type. (In the examples provided, the Vehicle class is used to represent the business object.)
Here are the steps performed within the code in AddData_ExampleA.java:
2. Create an instance of TreeNode. An instance of the DefaultMutableTreeNode class that implements the MutableTree-Node interface (a subinterface of Tree-Node) is created using the instance of the user object created in step 1. (The second argument indicates whether the node will allow children to be added to it. In this example, I want to allow children so I pass in the value true.)
3. Create an instance of TreeModel. The DefaultTreeModel class implements the TreeModel interface and can be created using the TreeNode object that was created in step 2 as its constructor's argument:
4. Create an instance of the JTree. The JTree is created using the TreeModel object that was created in step 3:
5. Create a child TreeNode. When the user clicks the add button, another instance of the DefaultMutableTreeNode is created using another instance of the Vehicle class with the name of "Car": 6. Add child node to the JTree. The method insertNodeInto(MutableTreeNode, MutableTreeNode, int) from the DefaultTreeModel class is invoked on the TreeModel that was created in step 3. There are three arguments. The first argument consists of using the instance of the DefaultMutableTreeNode that was created in step 5. The second argument calls for the parent of the object being inserted, which in this case is the root. To obtain the root, the TreeModel interface provides a getRoot() method. (Note: the return type of getRoot() is Object, which requires casting the returned object to the MutableTreeNode class.) The third argument requires an int to indicate where within the children (assuming more than one child) the new node should be graphically positioned. Since there are no other children, the value used is 0: i_model.insertNodeInto( i_carNode, (MutableTreeNode)i_model.getRoot(), 0 ); To see this example run, compile and execute the AddData_ExampleA.java (see Listing 1) source file. Upon executing the application, the JTree is displayed showing a single node - the root (see Figure 1). To see this happen, click on the button labeled Remove Node: 'Car' button in the AddData_ExampleA.java program. By repeating steps 5 and 6, the program will construct the entire contents of a JTree. While this add process is simple and easy to program, there are some shortcomings with this approach. First, it demands that the application constructing the JTree take full responsibility for constructing and maintaining all the hierarchical relationships between each node. The code to handle this can easily become too large and difficult to debug or maintain. A second shortcoming is that the responsibility of keeping concurrent data accurate falls back on the application containing the JTree. Running the AddData_ExampleA class explains this. After you create and add the child node "Car," if the button labeled "Change Name to 'Van'" is clicked, the node that previously displayed "Car" will now display "Van." However, for the refresh to occur immediately, the following code is required: i_model.valueForPathChanged( pathToRoot, i_car );
Another method, called valueForPathChanged A third shortcoming is with the use of the default classes that are provided by Swing. While convenient to use, it should be noted that certain limitations and costs exist. In my example, the DefaultMutableTreeNode is not a thread-safe class. Other issues relating to performance may need to be addressed when using the "default" classes in Swing. It should also be noted that since the methods insertNodeInto() and removeNode() aren't part of the TreeModel interface, calls made to the getModel() method will require casting prior to invoking these methods. This defeats the advantage of using interfaces because if these methods were part of the TreeModel interface, then the cast to DefaultTreeModel after getModel() wouldn't be necessary. The second example, displaying a business object's data in a JTree, is done by creating a tailored MutableTreeNode class. Writing my own MutableTreeNode class gives me a "bridge" between the user object class being displayed and the Swing MutableTreeNode interface. I use the term bridge to imply that there will be a translation between API calls invoked by one class and the appropriate methods invoked in a corresponding target class (see Figure 4). This allows the target class (the user object class) to be free from knowing the functionality of the calling class, and vice versa. Therefore, the Vehicle class definition (see Listing 2) isn't influenced by how Swing is implemented. Note: It's good practice to implement the toString() method in your objects to provide a meaningful String representation of the class. The JTree uses the toString() method to determine what text to display in the TreeNodes.
Accomplishing this requires completion of the two steps listed below: When changes are made to the underlying object the JTree won't update its view to reflect the new value until it's prompted to do so. As discussed earlier, the method valueForPathChanged(TreePath, Object) on the TreeModel will update the JTree view. However, to invoke the method requires a reference to the TreeModel that can't be obtained from a TreeNode. Therefore, I chose to implement an event mechanism as a logical means of communicating updates made by the MutableTree-Nodes to their associated user objects. This required creating two interfaces (UpdateEventSource, UpdateEventListener) (see Listings 4 and 5) and one class (UpdateEvent) (see Listing 6) for the event.
Now to bring it all together! Following is the sequence of steps that occurs when executing the AddData_ExampleB (see Listing 7) application class:
2. Create an instance of the VehicleTree-Node class. The argument used in its constructor is the Vehicle class that was created in step 1. The second argument is a Boolean that indicates if the node being created will allow children. In this example, the nodes will have children so the value of true is used:
3. Create an instance of a TreeModel class. By using the DefaultTreeModel class, the constructor is passed in the root node created in step 1 and true is passed in to indicate that children are allowed:
4. Create an instance of the JTree. The JTree is constructed using an instance of the DefaultTreeModel class, which is constructed using the VehicleTreeNode object that was created in step 3: At this point the work is finished and the magic of the JTree begins (see Figure 5). Here's how this works: subsequent calls are made by the JTree to the TreeNode (in my example, it's the VehicleTreeNode), asking if it allows children (allowsChildren). If so, it obtains a child count (getChildCount), iterates through the list of children and sets the current node as the parent (setParent) on the child node. This will repeat for each node until the leaf node is reached (getChildCount returns 0). Actually, the default behavior is to perform these steps in a lazy fashion. Rather than take a performance hit by obtaining the entire structure of the JTree right away, nodes are displayed in a collapsed state and wait until they're expanded before completing construction of the JTree. Adding nodes to the JTree is accomplished by invoking methods similar to those in the first example. By invoking the DefaultTreeModel's method, insertNodeInto(MutableTreeNode, MutableTreeNode, int) with a VehicleTreeNode as the required MutableTreeNode, any subtypes associated with the Vehicle will also immediately appear on the JTree. This differs from the first example in that subsequent calls to the insertNodeInto() method would be required to add the subtypes of the Vehicle to the JTree. This can be seen by running the AddData_ExampleB.java application. The Truck/Vehicle is added to the root Vehicle prior to being added to the JTree. So when the root Vehicle is added to the JTree, the child node Truck is also added to the JTree without requiring the additional call to insertNodeInto(). It's worth noting that while the MutableTreeNode interface offers methods like insert(), remove() and removeFromParent(), invoking these methods directly to alter the parent /child relationships circumvents the TreeModel. Since the TreeModel maintains the view, changes made directly to the MutableTreeNodes won't be reflected until a forced repaint occurs (resizing the window, etc.).
Summary I hope this article assists developers who are new to Java, or to the JFC and Swing, to quickly become acclimated to the power of using a JTree to graphically represent and administer their data objects.
Resources Reader Feedback: Page 1 of 1
Latest Cloud Developer Stories
Subscribe to the World's Most Powerful Newsletters
Subscribe to Our Rss Feeds & Get Your SYS-CON News Live!
|
SYS-CON Featured Whitepapers
Most Read This Week
|
||||||||||||||||||||||||||||||||||||||||||||||||