Richard G Baldwin (512) 223-4758, baldwin@austin.cc.tx.us, http://www2.austin.cc.tx.us/baldwin/

Swing, Custom Rendering of Tree Nodes

Java Programming, Lecture Notes # 191, Revised 03/10/99.


Preface

Students in Prof. Baldwin's Advanced Java Programming classes at ACC will be responsible for knowing and understanding all of the material in this lesson beginning with the spring semester of 1999.

This lesson was originally written on September 27, 1998, using the JDK 1.1.6 download package along with Swing 1.0.3. The purpose of this lesson is to illustrate the use of the JTree class and associated classes for creating and using trees.

On 3/10/99, the import directives in the sample program were modified for JDK 1.2 compatibility. The name of the modified program is Tree06A.java. The modified program was confirmed to compile and run properly under JDK 1.2.

Introduction

Swing makes it possible to create tree structures similar to Windows Explorer.  The tree can be collapsed and expanded to hide or expose inner nodes in the tree.  The look of the tree is subject to Swing's pluggable look and feel.  The sample program in this lesson uses a Windows look and feel, although it can easily be changed by modifying only one statement.

It is possible for the programmer to define a special model class to maintain the data, in a Model-View-Controller sense.  However, that isn't necessary because a default model is also available.  The sample program in this lesson uses the default data model.

It is also possible for the programmer to define how the individual nodes in the tree are to be rendered.  Rendering of the nodes is customized in the sample program in this lesson.  They are rendered using an image and a text label.

Several different types of events can be generated and handled when the user performs various actions on a Swing tree.  The sample program in this lesson uses this event capability to change the icon whenever the user selects a leaf of the tree for the first time, and also to display a message identifying the leaf that was selected.

Overview

Swing implements a modified form of the Model-View-Controller (MVC) paradigm on all its components.  As of September 1998, most of the current literature on Swing trees presents them in this paradigm.  While the MVC paradigm may lead to better program design, it also leads to additional programming complexity and requires a much higher level of skill and understanding on the part of the programmer.

It is often possible to use Swing components in your programs without getting explicitly involved in the MVC paradigm.  This lesson shows you how to create and use Swing trees in an effective manner without any explicit consideration of MVC.  Several subsequent lessons, centered around the JList class, discusses this topic in depth.  (The JList and JTree classes are very similar from a programming viewpoint.)

In addition to the use of trees and selection listener objects, this lesson also illustrates the use of object serialization and scroll panes.

Sample Program

As of JDK 1.1.6 and Swing 1.0.3, this program has a problem with some of the labels being chopped off when they are displayed in the tree.  Some news group articles indicate that this may be a bug in the JDK or the Swing library.  If so, it will hopefully be fixed in a future release of Swing.  If you can explain how to fix this problem, I would appreciate hearing about it.

Another aspect of this problem is that the JList class provides a couple of methods by which you can control the size of the cells into which you render your objects.  This makes it possible to avoid the problem with JList.  If such a method exists to be used with the JTree class, I haven't discovered it yet in JDK 1.1. (Haven't had time yet to look for it in JDK 1.2.)

This program provides a fairly simple approach to creating and maintaining a tree structure using the Swing JTree without becoming explicitly involved in the MVC paradigm.

The program creates a tree that has a Windows look and feel.  The tree can be collapsed and expanded by clicking on the + and - boxes in the typical Windows Explorer style.

The tree consists of nodes and leaves.  A leaf is a node that doesn't have any children.  Each leaf on the tree has both an icon and a label.  When a leaf is selected the first time, the icon changes to indicate that it has been selected.  When the program terminates, a history file is created and saved containing the state of each leaf.  When the program is restarted, the history file is read and used to restore the state of each leaf to its previous value.  Object serialization is used to save and restore the history data file.
 

To run this program, you will need to modify the references to the history file and two image files to match your configuration.  If you wish to run this program two or more times without loading the history file, find and delete the file named junk.dat from your disk.

When a leaf is selected, an event is generated and handled to cause the identification of the leaf that was selected to appear on the bottom of the JFrame containing the tree.

The program was tested using JDK 1.1.6 and Swing 1.0.3 under Win95.

Interesting Code Fragments

The program is written as a single class, containing several inner classes and several instance variables.  The first code fragments shows some of those instance variables which are discussed individually following the fragment.
 

public class Tree06 extends JFrame{
  Hashtable theHashtable = new Hashtable();
  JLabel display = new JLabel("Display Selection Here");
  String historyFile = "/jnk/junk.dat";
  String unSelectedImage = "blue-ball.gif";
  String selectedImage = "red-ball.gif";
  int frameWidth = 350;
  int frameHeight = 350;

Swing allows the programmer to define the manner in which the nodes of the tree are rendered whenever it is necessary to redraw the tree.  Each of the nodes in this tree is rendered using a JLabel.  The JLabel used for all of the nodes contains a text label. In addition, the JLabel used for the leaves also contains an image.  The image changes once the leaf has been selected.  The text label is different for each node.

A method is provided that is called by the system whenever it is necessary to draw a node.  To customize the rendering of the nodes, I override this method.  (I will discuss the method in detail later.)  This method returns the appropriate JLabel for rendering the node.

A hashtable is created and populated at startup that associates the hashcode of each node with an index into an array containing the JLabel objects.  When time comes to render the node, the node is used to access the hashtable to obtain the index of the JLabel required for that particular node.  That hashtable is referenced by the reference variable named theHashtable shown in the code fragment above.

The reference variable named display is simply a reference to a JLabel that is used to display information when a leaf is selected.

The reference variables named historyFile, unSelectedImage, and selectedImage refer to disk file used to store the history data, and disk files containing the images displayed on the leaves of the tree.  You will need to provide your own image files, and will probably modify these references to agree with your disk organization.

This is followed by two reference variables that specify the size of the JFrame object used as a container for this program.

The next fragment shows additional instance variables, the first being numberRows which specifies the number of rows of data displayed on the screen when the tree is in the fully expanded mode.  This is followed by a reference variable named tree, which is a reference to the actual instance of the JTree class that constitutes the tree.
 

  int numberRows = 19;
  JTree tree;
  MyData myData;
  TheData[] data;
  TreePanel treePanel;
  String plafClassName 
    = "com.sun.java.swing.plaf.windows.WindowsLookAndFeel";

The reference variable named myData is a reference to an object containing an array of objects, each of which  contains the information necessary to render and process a node in the tree.  Each object in the array contains two instance variables:  a JLabel and a String.  The JLabel contains an image and some text that is displayed when the node is rendered.  The String contains some supplementary information that is accessed when the user selects the node.

The state of this object is maintained throughout the running of the program, and the object is written to disk, using object serialization, when the user terminates the program.  It is read back when the user restarts the program.  Thus, this is a persistence mechanism for a single user.  If an attempt is made to read the object from disk and it doesn't exist, a new object is created with initial data under program control.  This object is stored in the disk file referenced by historyFile mentioned earlier.

The variable named data is simply a convenience variable that refers to the array in the above-mentioned object.

The variable named treePanel is a reference to a JPanel object that contains the tree in a scrollable pane.

Finally, the reference variable named plafClassName is used to cause the look and feel of the program to emulate Windows.  Other L&F implementations could be used by substituting the proper information in this String.

That gets us through the preliminaries and into the real code.  The next fragment shows a simple main() method that can be used to run this application in a stand-alone mode.  This method simply instantiates an object of the controlling class.
 

  public static void main(String args[]) {
    new Tree06();
  }//end main

That brings us to the constructor where the work of creating the tree is carried out.  After setting the title on the frame, the following fragment gets the history data from disk, or causes it to be created under program control if it doesn't already exist.  I will discuss the code that actually accomplishes this in detail later.
 

  public Tree06 () {//constructor
    setTitle("Copyright 1998, R.G.Baldwin");  

    myData = new MyData();//ref to object containing array
    data = myData.getData();//ref to the array

The next fragment is very important in understanding how this Swing tree works.  This fragment first creates an array of references to tree nodes of type DefaultMutableTreeNode.  Then it constructs the tree by invoking the add method on a node to add children to that node.  For example, I have highlighted three lines of code in boldface showing that nodes 1, 5, and 14 are children of node 0.  This is accomplished by adding those three nodes to node 0.

Also, I have italicized three lines of code showing that nodes 2, 3, and 4 are children of node 1.  Hopefully with these clues, you will be able to see the rest of the pattern.

Again, these are nothing but references to node objects, and the manner in which these references are related establishes the structure of the tree.  There is one reference variable for each node in the tree.
 

    // Create an array of references to tree nodes.  
    DefaultMutableTreeNode[] theTreeNodes = 
                    new DefaultMutableTreeNode[numberRows];
    for(int cnt = 0; cnt < numberRows; cnt++){
      theTreeNodes[cnt] = new DefaultMutableTreeNode("");
    }//end for loop

    //Construct the tree using the nodes created above.
    // These statements establish ParentChild relationships
    // among the nodes.
    theTreeNodes[0].add(theTreeNodes[1]);
    theTreeNodes[1].add(theTreeNodes[2]);
    theTreeNodes[1].add(theTreeNodes[3]);
    theTreeNodes[1].add(theTreeNodes[4]);
    theTreeNodes[0].add(theTreeNodes[5]);
    theTreeNodes[5].add(theTreeNodes[6]);
    theTreeNodes[6].add(theTreeNodes[7]);
    theTreeNodes[6].add(theTreeNodes[8]);
    theTreeNodes[6].add(theTreeNodes[9]);
    theTreeNodes[5].add(theTreeNodes[10]);
    theTreeNodes[10].add(theTreeNodes[11]);
    theTreeNodes[10].add(theTreeNodes[12]);
    theTreeNodes[10].add(theTreeNodes[13]);
    theTreeNodes[0].add(theTreeNodes[14]);
    theTreeNodes[14].add(theTreeNodes[15]);
    theTreeNodes[14].add(theTreeNodes[16]);
    theTreeNodes[14].add(theTreeNodes[17]);
    theTreeNodes[14].add(theTreeNodes[18]);

As mentioned earlier, when time comes later to draw the tree, it is necessary to render each node into a picture on the screen.  In order to do this, it is necessary to first identify the node being rendered, and then retrieve the information required to properly render that node.  There are numerous ways to do this.  My approach in this program is to maintain the rendering information in an array of objects.  That means that I need a way to associate the node being rendered with the element in the array containing the rendering information.  I accomplish this by creating a hashtable containing one key/value pair for each node in the tree.  The key is the hashcode representation of the node itself, and the value is the index in the array from which rendering information can be retrieved when needed.  Note that the rendering information in the array changes over time so the approach must accommodate these changes.

The next fragment shows the hashtable being populated with the proper index value for each node key.  Because an object of the Hashtable class deals only with objects of type Object, it was necessary to wrap the index values in an object of type Integer in order to store them in the hashtable.
 

    for(int cnt = 0; cnt < numberRows; cnt++){
      theHashtable.put(theTreeNodes[cnt],new Integer(cnt));
    }//end for loop

The next fragment instantiates an object of type CellRenderer which will be used later by the system to render the individual nodes when they are drawn on the screen.  This class implements the TreeCellRenderer interface which in turn requires that it defines a method named getTreeCellRendererComponent().  This method is called by the system to get rendering information for a node whenever it is necessary to redraw the node on the screen.  The design of the method in this program causes it to return a reference to a  JLabel object containing a text label in all cases and an image in the case where the node is a leaf.

Next, the CellRenderer object, along with a reference to the root node in the tree, are passed to the constructor of a class named TreePanel that instantiates the actual JTree object in a scrollable pane in a JPanel object.  I will discuss that class in detail later.
 

    CellRenderer cell = new CellRenderer();
    treePanel = new TreePanel(theTreeNodes[0], cell);

There are several listener interfaces defined for use with trees in Swing.  The next fragment instantiates a TreeSelectionListener object and registers it on the tree to listen for events caused by the user selecting a node with the mouse or the arrow keys.  An event is generated and processed each time the user selects a different node.  Nodes that were previously selected can be selected again if another node has been selected in the meantime.

Selection of a leaf in this program causes the image to change to a red ball and displays the identification of the leaf in a JPanel at the bottom of the outer frame object.  Selecting the node again doesn't cause it to change back to a blue ball, but it wouldn't be difficult to implement that kind of toggle behavior.

I will discuss the class named MyTreeListener from which the listener object is instantiated later.
 

    tree.addTreeSelectionListener(new MyTreeListener());

The remaining code in the constructor is very routine, doing such things as adding the treePanel to the outer frame, setting the size of the frame, making it visible, etc., so I won't highlight and discuss it further in these fragments.  You can view that code in the complete listing of the program that follows later.

The next fragment begins the discussion of the CellRenderer class.  This is probably the most complex code in the entire program, so I will try to explain it in detail, possibly repeating some information that I have already mentioned before.

This class implements the TreeCellRenderer interface, which requires it to define a method named getTreeCellRendererComponent().  Whenever the system needs to render a node, it calls this method and expects the method to return a reference to an object of type Component.  That object is used to actually render the visual part of the node (the object is painted on the screen at the appropriate spot).

Many different classes extend Component, so the returned object can be of many different types including JLabel.  I elected to create and return a reference to a JLabel object to be used to render each node.

The real trick, and the thing that makes this task somewhat difficult is deciding which JLabel reference to return.  Although the method receives a large number of parameters, none of them specifically identify the node that is being rendered.  The closest thing to an identification of the node being rendered is an incoming parameter named row.  However, that parameter doesn't specifically identify the node.  It simply identifies the row number of the node in the current visual representation of the tree.  A node that appears on a row with a large value when the tree is fully expanded will appear on a row with a smaller value when the tree has been partially collapsed.

Therefore, it is necessary to use the row value to work backwards to positively identify the node, and to use that identification to specify the correct JLabel object to return for rendering that node.  After invoking some methods to identify the node from the row value, I use the hashtable discussed earlier to retrieve information that allows me to access and return the correct JLabel object.

More specifically, I enter the hashtable with the node as a key and retrieve an index into an array that contains the rendering information for that node, including a reference to the proper JLabel object.
 

  private class CellRenderer implements TreeCellRenderer {
    public Component getTreeCellRendererComponent(
                JTree tree, Object value, boolean selected, 
                   boolean expanded, boolean leaf, int row, 
                                        boolean hasFocus) {

Accomplishing all of this requires several distinct steps that I will discuss in successive code fragments.

The first step is to invoke the getPathForRow() method on the JTree object which uses the row value to get the path from the root to the node on that row as an object of type TreePath.
 

      TreePath thePath = tree.getPathForRow(row);

Once I have the path, assuming that is isn't null, I can invoke the getLastPathComponent() method on the TreePath object to get the node at the end of the path.

Sometimes this method gets called by the system with a null value for the path.  I'm not certain why this is, but I don't believe that I have any control over it.  Therefore, I must protect against that possibility by testing the path for null before trying to use it.
 

      if(thePath != null){
        DefaultMutableTreeNode theNode = 
                           (DefaultMutableTreeNode)thePath.
                                    getLastPathComponent();

Once I have the node, I use it as a key to access my hashtable and obtain the index needed to retrieve the rendering information for that node from the array that contains it.  Recall that what I actually get from the hashtable is a reference to an object of type Integer.  I must invoke the intValue() method on that object to get the actual index value.

Following this, I use the index to access the array named data from which I extract an object that contains the required JLabel object and some other information as well.  I pull out the JLabel and return it.  The system then uses this JLabel object to render the node in the drawing.
 

        int theProperDataID = 
           ((Integer)theHashtable.get(theNode)).intValue();

        return data[theProperDataID].theJLabel;
      }//end if

As mentioned earlier, sometimes the system calls this method with a null value for the path.  When that happens, I simply return a dummy JLabel object as shown below.
 

      else return new JLabel("Dummy");

    }//end getTreeCellRendererComponent()
  }//end inner class CellRenderer

The next fragment is similar to, but somewhat less complicated than the previous one.  This fragment begins the discussion of the class from which TreeSelectionListener objects are instantiated.

The TreeSelectionListener interface declares a method named valueChanged() that must be defined in any method that implements the interface.  This is standard Delegation Event Model material, so there should be no surprises here.

The complexity comes from the fact that once again, it is necessary to identify the node that was selected in order to accomplish the desired objective.  The objective is to replace the blue ball image with a red ball image the first time that the leaf node is selected.  It's a little simpler this time than before.

The incoming object of type TreeSelectionEvent contains the path to the node, so all that is required is to invoke the getPath() method on the event object and invoke the getLastPathComponent() method on the path to get the node.  Then, as before, I use the node to access the index value from the hashtable.  All of this is shown in the following fragment.
 

  class MyTreeListener implements TreeSelectionListener{
    public void valueChanged(TreeSelectionEvent e) {
      DefaultMutableTreeNode theNode =
                    (DefaultMutableTreeNode)
                      (e.getPath().getLastPathComponent());

      int theProperDataID = 
           ((Integer)theHashtable.get(theNode)).intValue();

If the node is a leaf, the code in the following fragment uses the index value to access the data array to replace the blue ball image with a red ball image.  This is accomplished by invoking the setIcon() method on the JLabel.  (You will probably be using different images, but you can use the same variable names to reference them.)

Then the invalidate() method is invoked to force the entire tree to be redrawn so that the new image will become visible.
 

      if (theNode.isLeaf()) {
          data[theProperDataID].theJLabel.setIcon(
                             new ImageIcon(selectedImage));

        invalidate();

I have one thing remaining to be done in this method, and that is to perform some kind of action related to the selection of the node.  In this sample program, all that I do is to display a message on the bottom of the frame identifying the leaf that was selected.  In your program designed to accomplish some useful objective, you would probably put the code to accomplish that objective in the next fragment.
 

        display.setText(data[theProperDataID].
                                      theSupplementalInfo);
      }//end if
    }//end valueChanged() method
  }//end inner class MyTreeListener

The next fragment is the class from which the JPanel containing the tree in a scrollable pane is instantiated.  The constructor receives a reference to the root object of the tree, along with a reference to the TreeCellRenderer object.  These two references are used to instantiate a new object of type JTree and set the cell renderer for that tree to the renderer object received as a parameter.

Following this, I do some utility things as described in the comments.  You can view the code for these actions in the full listing of the program that follows later.
 

  class TreePanel extends JPanel {
    public TreePanel(TreeNode root, 
                                TreeCellRenderer renderer){

      tree = new JTree(root);
      tree.setCellRenderer(renderer);

      //Put the tree in a scrollable pane
      //Set the look and feel to emulate Windows
      //Set to BorderLayout
      //Expand the tree
      //Code omitted for brevity
    }//end constructor
  }//end inner class TreePanel

The next code fragment contains part of the code used to construct the object containing an array of objects, each of which contains rendering information for a particular node plus supplemental information for that node.

The getData() method of this class searches for a specific file on the disk containing historical data about the tree from previous runs.  If the file is located, it is read using object serialization and becomes the starting data for the new run.

If the file is not located, a new set of initialized data is created.

Note that this class implements Serializable to make it possible to serialize an object of this type and write it to the disk.

This code is pretty straightforward, so I won't have much to say about it.  If you haven't seen object serialization in action, you might want to pay particular attention to that part of the code.  As is usually the case in Java I/O, this entails wrapping objects in objects that are wrapped in objects, etc.

Once the ObjectInputStream is available, you simply invoke the readObject() method on the stream to read, de-serialize, and reconstruct the entire object.  I have noticed that there appears to be some degree of size bloat when using object serialization.  Generally speaking, the disk files produced by object serialization generally tend to be much larger than would be required to store the same amount of data in a more conventional manner.
 

  class MyData implements Serializable{
    //Instantiate an array of objects of type TheData
    TheData[] theData = new TheData[numberRows];
    
    TheData[] getData(){
      System.out.println("Attempt to find and read " 
                                            + historyFile);
      if(new File(historyFile).exists()){
        System.out.println("File exists, read it");
        try{//try to read the file as a serialized object
          ObjectInputStream in = new ObjectInputStream(
                         new FileInputStream(historyFile));
          myData = (MyData)in.readObject();
          System.out.println("Read completed");
          return myData.theData;
        }catch(Exception e){
          e.printStackTrace();
          System.out.println("Aborting program");
          System.exit(0);
        }//end catch

If the historical data file doesn't exist, a new set of data is created by the following code.  This code is very repetitious, so I omitted most of it.  You can view it in the program listing later.

This code populates an array of objects in an object of type TheData.  (You can view the definition of the class named TheData in the program listing.)  Each object in the array corresponds to one node, and contains a JLabel and a String.  The JLabel contains a text label for all nodes, and contains an image for the leaf nodes.  The String contains some supplemental information for the node that is displayed when the node is selected.

As of JDK 1.1.6 and Swing 1.0.3, some of the labels in the JLabel objects are truncated and replaced by ... when they are displayed in the tree.   As I mentioned earlier, I don't know why this happens.  Sometimes, but not always, it is possible to minimize this effect by inserting extra spaces to cause the label to be longer than required.  That is the reason for all of the extra spaces in the strings in this code.
 

      }else
      System.out.println(historyFile 
                            + " doesn't exist, create it");
      theData[0] = new TheData(
        "----------------SAMPLE TREE-----------------","");
      theData[1] = new TheData("Vegetables           ","");
      theData[2] = new TheData("Cabbage",
                                       "Selected Cabbage");
      theData[2].theJLabel.setIcon(
                           new ImageIcon(unSelectedImage));

//omit most of the code here

      theData[18] = new TheData(
                               "Orange","Selected Orange");
      theData[18].theJLabel.setIcon(
                           new ImageIcon(unSelectedImage));

      return theData;
      
    }//end method getData()
  }//end class MyData

 

Program Listing

This section contains a complete listing of the program.

/*File Tree06.java
Revised 9/27/98

This program has a problem some of the labels being
chopped off.  Some newsgroup articles indicate that this
may be a bug in the JDK or the Swing library.

This program provides a simple-minded approach to creating
and maintaining a tree structure using the Swing JTree.

The program doesn't make any explicit use of the Swing
model concept.

The program creates a tree that has a Windows look and 
feel.  The tree can be collapsed and expanded by clicking
on the + and - boxes in the typical Windows Explorer style.

Each leaf on the tree has both an icon and a label.  When
a leaf is selected, the icon changes to indicate that it
has been selected.  When the program terminates, a history
file is created and saved containing the state of each
leaf.  When the program is restarted, the history file
is read and used to restore the state of each leaf to its
previous value.

Object serialization is used to save and restore the
history data file.

When a leaf is selected, an event is generated and handled
to cause the identification of the leaf that was selected
to appear on the bottom of the JFrame containing the 
tree.

Tested using JDK 1.1.6 and Swing 1.0.3 under Win95.
**********************************************************/

import java.io.*;
import java.util.*;
import java.awt.*;
import java.awt.event.*;
import com.sun.java.swing.*;
import com.sun.java.swing.tree.*;
import com.sun.java.swing.event.*;

//=======================================================//
public class Tree06 extends JFrame{
  //The following hashtable is used to maintain a link
  // between the nodes/leaves in the tree and the JLabel
  // object that is used to render them.  
  Hashtable theHashtable = new Hashtable();
  
  //The following object is used to display the selection
  // at the bottom of the JFrame object
  JLabel display = new JLabel("Display Selection Here");

  //Paths and filenames.  Modify as you see fit.
  String historyFile = "/jnk/junk.dat";
  //Following image displayed when leaf is not selected
  String unSelectedImage = "blue-ball.gif";
  //Following image displayed when leaf is selected
  String selectedImage = "red-ball.gif";
  
  //Absolute coordinate info in pixels.
  int frameWidth = 350;
  int frameHeight = 350;
  
  //The number of rows in the tree when it is fully
  // expanded
  int numberRows = 19;

  //Reference to the JTree object
  JTree tree;
  
  //The following object of type MyData contains an array
  // of objects of type TheData.  Each of these objects 
  // represents a node in the tree.  Each object contains
  // two instance variables:  a JLabel object that contains
  // the text and icon for a node, and a String that
  // contains the supplementary information to be accessed
  // when that node is selected.  This object is written
  // to disk when the user terminates the program, and is
  // read from the disk when the user starts the program.
  // If a file containing the object doesn't exist when the
  // program is started, a new object is created with
  // initialized data.
  MyData myData;

  
  //The following is a reference to the array contained
  // in the object described above.
  TheData[] data;

  
  //The following is a reference to a panel which contains
  // the tree.
  TreePanel treePanel;
  
  //The following is a reference to a class which is used
  // to cause the look & feel of the program to emulate
  // Windows.  This is necessary to get the + and - boxes
  // that are used to expand and collapse the nodes in
  // the tree. Otherwise, the tree would default to the
  // metal look and feel with buttons instead of + and -
  // boxes.
  String plafClassName 
    = "com.sun.java.swing.plaf.windows.WindowsLookAndFeel";
  
  //-----------------------------------------------------//

  //The following main method that can be used to test 
  // this class on a stand-alone basis.
  public static void main(String args[]) {
    //Instantiate the top-level JFrame object.
    new Tree06();
  }//end main
  //-----------------------------------------------------//

  public Tree06 () {//constructor
    setTitle("Copyright 1998, R.G.Baldwin");  

    //Create new data object, or get historical data object
    // from disk. Put references to the object and the
    // data array contained in that object in variables
    // for later access.
    myData = new MyData();//ref to object containing array
    data = myData.getData();//ref to the array

    // Create an array of references to tree nodes.  
    DefaultMutableTreeNode[] theTreeNodes = 
                    new DefaultMutableTreeNode[numberRows];
    for(int cnt = 0; cnt < numberRows; cnt++){
      theTreeNodes[cnt] = new DefaultMutableTreeNode("");
    }//end for loop

    //Construct the tree using the nodes created above.
    // These statements establish ParentChild relationships
    // among the nodes.
    theTreeNodes[0].add(theTreeNodes[1]);
    theTreeNodes[1].add(theTreeNodes[2]);
    theTreeNodes[1].add(theTreeNodes[3]);
    theTreeNodes[1].add(theTreeNodes[4]);
    theTreeNodes[0].add(theTreeNodes[5]);
    theTreeNodes[5].add(theTreeNodes[6]);
    theTreeNodes[6].add(theTreeNodes[7]);
    theTreeNodes[6].add(theTreeNodes[8]);
    theTreeNodes[6].add(theTreeNodes[9]);
    theTreeNodes[5].add(theTreeNodes[10]);
    theTreeNodes[10].add(theTreeNodes[11]);
    theTreeNodes[10].add(theTreeNodes[12]);
    theTreeNodes[10].add(theTreeNodes[13]);
    theTreeNodes[0].add(theTreeNodes[14]);
    theTreeNodes[14].add(theTreeNodes[15]);
    theTreeNodes[14].add(theTreeNodes[16]);
    theTreeNodes[14].add(theTreeNodes[17]);
    theTreeNodes[14].add(theTreeNodes[18]);

    
    //Load the hash table with keys corresponding to
    // each of the nodes in the above array and values
    // which are Integer objects containing the int values
    // from 0 through numberRows-1.  Later, when it is
    // time to render a node or a leaf, the hashcode of
    // the node to be rendered will be obtained, that
    // hashcode will be used to determine the corresponding
    // int value, and the int value will be used to index
    // into an array containing the JLabel object to be
    // used to render the node.  This is one way to deal
    // with the problem of the row numbers changing when
    // the tree is collapsed.
    for(int cnt = 0; cnt < numberRows; cnt++){
      theHashtable.put(theTreeNodes[cnt],new Integer(cnt));
    }//end for loop

    //Get a cell renderer object that is used to render the
    // image and label for each node.  This CellRenderer 
    // object is designed to cause each node to consist of 
    // a JLabel which can contain both a String and an 
    // image icon.
    CellRenderer cell = new CellRenderer();
    
    //Pass the CellRenderer object along with the root node
    // to the constructor for a TreePanel object.  This 
    // will construct a JTree object in a scrollpane in
    // a JPanel container.
    treePanel = new TreePanel(theTreeNodes[0], cell);
    
    //Instantiate a TreeSelectionListener object and 
    // register it to listen for events caused by the user
    // selecting a node in the tree with the mouse or the 
    // arrow keys.  An event is processed each time the 
    // user selects a different node.  Note that nodes that
    // were previously selected can be selected again if 
    // another node is selected in between.  However, once
    // the icon on a node is modified to indicate that the
    // node has been selected, it does not change back if
    // the node is selected again. (It wouldn't be 
    // difficult, however to cause it to toggle.)
    tree.addTreeSelectionListener(new MyTreeListener());
    
    //Display the TreePanel object in the top-level frame.
    Container content = getContentPane();
    content.add(treePanel,BorderLayout.CENTER);

    //Place a JLabel at the bottom of the JFrame to display
    // the identity of the node that is selected.
    content.add(display,BorderLayout.SOUTH);
    
    //Set the size of the frame and make it visible.
    setSize(frameWidth, frameHeight);
    setVisible(true);

    //An anonymous inner class to terminate the program
    // and save the history file when the
    // user clicks the close button on the frame. 
    // The data is saved using object serialization.
    this.addWindowListener(new WindowAdapter() {
      public void windowClosing(WindowEvent e) {
        //Write historical data to disk
        System.out.println("Writing " + historyFile);
        try{
          ObjectOutputStream out =
            new ObjectOutputStream(
              new FileOutputStream(historyFile));
          out.writeObject(myData);
          out.close();
        }catch(Exception ex){
          ex.printStackTrace();
        }//end catch block

        System.exit(0);}//terminate the program
    });
    
  }//end constructor
  //=====================================================//

  //This is an inner class that instantiates a CellRenderer
  // object.  In this case, it returns a reference to a
  // JLabel object from an array containing references to
  // JLabel objects.  The ID of the node to be
  // rendered is obtained.  This ID is used to access
  // a hashtable that associates node hashcodes with int
  // values.  The int value is obtained and used to index
  // into an array containing the objects that are used to
  // render the nodes.  This approach eliminates problems
  // that occur when relying on the row value (which 
  // changes when the tree is collapsed) to index 
  // the array.
   
  private class CellRenderer implements TreeCellRenderer {
    //The following method is declared in the interface and
    // must be defined here.  
    public Component getTreeCellRendererComponent(
                JTree tree, Object value, boolean selected, 
                   boolean expanded, boolean leaf, int row, 
                                        boolean hasFocus) {

      //Use the row value to get the path to the node
      // to be rendered.
      TreePath thePath = tree.getPathForRow(row);

      //Use the path to get the node, and use the ID
      // of the node as a hashtable key to get the int 
      // value required to index the array discussed above.
      // Note that the int values are stored in the 
      // hashtable as Integer objects and therefore must
      // be extracted from those objects using intValue().
      if(thePath != null){//sometimes it is null
        DefaultMutableTreeNode theNode = 
                           (DefaultMutableTreeNode)thePath.
                                    getLastPathComponent();
        int theProperDataID = 
           ((Integer)theHashtable.get(theNode)).intValue();
        //Access the data array, get and return the JLabel
        // contained in the object referenced in that 
        // array.
        return data[theProperDataID].theJLabel;
      }//end if
      //Sometimes this method is called with a null path
      // but I don't know why.  In that case, return a 
      // dummy JLabel.
      else return new JLabel("Dummy");

    }//end getTreeCellRendererComponent()
  }//end inner class CellRenderer
  //=====================================================//

  //This inner class is used to instantiate a 
  // TreeSelectionListener object and register it on the
  // JTree object.  The method in this object is invoked
  // whenever the user selects a new node in the tree.
  // A previously-selected node can be selected again if
  // another node is selected in between.  The first time
  // a node is selected, an image of a red ball replaces
  // an image of a blue ball that was placed on the node
  // when it was originally instantiated.  Selecting the
  // node again does not change it back to a blue ball,
  // but it wouldn't be too difficult to cause it to
  // toggle between the two.

  class MyTreeListener implements TreeSelectionListener{
    public void valueChanged(TreeSelectionEvent e) {
      //Get the node that was selected.
      DefaultMutableTreeNode theNode =
                    (DefaultMutableTreeNode)
                      (e.getPath().getLastPathComponent());
    
      //Use the node as the key and go to the hashtable to 
      // get the index of the element in the data array 
      // that corresponds to the node.
      int theProperDataID = 
           ((Integer)theHashtable.get(theNode)).intValue();

      //If the node is a leaf, change the image for that 
      // node from a blue ball to a red ball in the data
      // array.
      if (theNode.isLeaf()) {
          data[theProperDataID].theJLabel.setIcon(
                             new ImageIcon(selectedImage));
        
        //Force a redraw to cause the red ball to be
        // displayed.
        invalidate();

        
        //%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%//
        //This is the place to insert any code that should
        // be executed when the user selects a node.  In
        // this sample program, the action is to simply to
        // display information about the node in a JLabel
        // at the bottom of the frame.
        display.setText(data[theProperDataID].
                                      theSupplementalInfo);
        //%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%//
      }//end if
    }//end valueChanged() method
  }//end inner class MyTreeListener
  //=====================================================//
  
  //This inner class is used to instantiate the actual
  // JTree object and place it in a scrollable pane in a
  // JPanel container.  The JPanel can then be placed in 
  // any other container.

  class TreePanel extends JPanel {
    public TreePanel(TreeNode root, 
                                TreeCellRenderer renderer){
      setLayout(new BorderLayout());

      //Create tree
      tree = new JTree(root);
      
      //Specify the cell renderer to be used whenever the
      // system needs to redraw the tree.
      tree.setCellRenderer(renderer);

      // Put the tree in a scrollable pane and add the
      // scrollable pane to the JPanel container.
      JScrollPane sp = new JScrollPane();
      sp.getViewport().add(tree);
      add(sp, BorderLayout.CENTER);
      
      //Set the look and feel to emulate Windows
      try{
        UIManager.setLookAndFeel(plafClassName);
      }catch(Exception ex){System.out.println(ex);}
      SwingUtilities.updateComponentTreeUI(this);

      //Expand the tree
      for(int cnt = 0; cnt < numberRows; cnt++){
        tree.expandRow(cnt);
      }//end for loop
    }//end constructor
  }//end inner class TreePanel
  //=====================================================//

  //Inner class to construct a data object containing the
  // text for each node, the icon for each node, and the
  // supplementary information associated with each node.
  // The getData method of this class searches for a
  // specific disk file containing historical 
  // tree-navigation data, and returns an object from
  // that file if it exists.  Object serialization 
  // is used to retrieve the object from the disk file.
  // Otherwise, it constructs a new object and returns the
  // array that it contains.  
  class MyData implements Serializable{
    //Instantiate an array of objects of type TheData
    TheData[] theData = new TheData[numberRows];
    
    TheData[] getData(){
      //Test for the disk file containing the historical
      // data here.  If it exists, read and return it.
      // Otherwise, create a new initial set of data.
      
      System.out.println("Attempt to find and read " 
                                            + historyFile);
      if(new File(historyFile).exists()){
        System.out.println("File exists, read it");
        try{//try to read the file as a serialized object
          ObjectInputStream in = new ObjectInputStream(
                         new FileInputStream(historyFile));
          myData = (MyData)in.readObject();
          System.out.println("Read completed");
          return myData.theData;
        }catch(Exception e){
          e.printStackTrace();
          System.out.println("Aborting program");
          System.exit(0);
        }//end catch
      }else
      //Historical data file doesn't exist, create it.
      // Initialize text data and initialize the icon
      // to the unselected image for each leaf node.
      // As of JDK 1.1.6 and Swing 1.0.3, some of the
      // labels on the JLable object are truncated and
      // replaced by ...  Sometimes, but not always, it is
      // possible to minimize this by inserting spaces to
      // cause the label to be longer than required.
      System.out.println(historyFile 
                            + " doesn't exist, create it");
      theData[0] = new TheData(
        "----------------SAMPLE TREE-----------------","");
      theData[1] = new TheData("Vegetables           ","");
      theData[2] = new TheData("Cabbage",
                                       "Selected Cabbage");
      theData[2].theJLabel.setIcon(
                           new ImageIcon(unSelectedImage));
      theData[3] = new TheData("Squash","Selected Squash");
      theData[3].theJLabel.setIcon(
                           new ImageIcon(unSelectedImage));
      theData[4] = new TheData("Onion","Selected Onion");
      theData[4].theJLabel.setIcon(
                           new ImageIcon(unSelectedImage));
      theData[5] = new TheData(
        "Animals                                     ","");
      theData[6] = new TheData(
        "Forrest                                     ","");
      theData[7] = new TheData("Squirrel",
                                      "Selected Squirrel");
      theData[7].theJLabel.setIcon(
                           new ImageIcon(unSelectedImage));
      theData[8] = new TheData("Rabbit","Selected Rabbit");
      theData[8].theJLabel.setIcon(
                           new ImageIcon(unSelectedImage));
      theData[9] = new TheData("Fox","Selected Fox");
      theData[9].theJLabel.setIcon(
                           new ImageIcon(unSelectedImage));
      theData[10] = new TheData(
        "Farm                                        ","");
      theData[11] = new TheData("Horse","Selected Horse");
      theData[11].theJLabel.setIcon(
                           new ImageIcon(unSelectedImage));
      theData[12] = new TheData("Pig","Selected Pig");
      theData[12].theJLabel.setIcon(
                           new ImageIcon(unSelectedImage));
      theData[13] = new TheData("Cow","Selected Cow");
      theData[13].theJLabel.setIcon(
                           new ImageIcon(unSelectedImage));
      theData[14] = new TheData(
        "Fruit                                       ","");
      theData[15] = new TheData(
                                 "Peach","Selected Peach");
      theData[15].theJLabel.setIcon(
                           new ImageIcon(unSelectedImage));
      theData[16] = new TheData("Grape","Selected Grape");
      theData[16].theJLabel.setIcon(
                           new ImageIcon(unSelectedImage));
      theData[17] = new TheData("Apple","Selected Apple");
      theData[17].theJLabel.setIcon(
                           new ImageIcon(unSelectedImage));
      theData[18] = new TheData(
                               "Orange","Selected Orange");
      theData[18].theJLabel.setIcon(
                           new ImageIcon(unSelectedImage));

      return theData;
      
    }//end method getData()
  }//end class MyData
  //===================================================//

  //The following inner class is used to create an object
  // in which to maintain the JLabel and the
  // supplementary information for each node in the tree.
  class TheData implements Serializable{
    JLabel theJLabel = new JLabel();
    String theSupplementalInfo;
    
    //constructor
    TheData(String theLabel,String theSupplementalInfo){
      this.theJLabel.setText(theLabel);
      this.theSupplementalInfo = theSupplementalInfo;
    }//end constructor
  }//end class TheData
  //=====================================================//

}//end class Tree06

-end-