HomeDigital EditionSys-Con RadioSearch Java Cd
Advanced Java AWT Book Reviews/Excerpts Client Server Corba Editorials Embedded Java Enterprise Java IDE's Industry Watch Integration Interviews Java Applet Java & Databases Java & Web Services Java Fundamentals Java Native Interface Java Servlets Java Beans J2ME Libraries .NET Object Orientation Observations/IMHO Product Reviews Scalability & Performance Security Server Side Source Code Straight Talking Swing Threads Using Java with others Wireless XML
 

This month we put together a visual component, designed to render a tree structure in a scrollable window. JComponentTree can display a set of arbitrary components in a tree hierarchy. It can orient the tree in any of the cardinal directions (north, south, east or west); display the connections between nodes using straight or angled lines; and align each of the tree nodes to be left-, center- or right-justified.

The JComponentTree stores its data in a DefaultTreeModel compatible with JTree and uses TreeNode objects that extend the DefaultMutableTreeNode class.

Figure 1 shows an example JComponent hierarchy, fairly typical of the kind of structures you might want to display using the JComponentTree widget.

Figure 1
Figure 1:

Using the JFC TreeModel
The JComponentTree widget is designed to take advantage of concepts developed by Sun Microsystems in the JFC JTree component. We work with the DefaultTreeModel, for example, so that migrating between views is possible. An application could implement a structure based on the DefaultTreeModel and easily switch between display components, permitting the JTree or the JComponentTree - or both - components to display the model.

The TreeModel provides methods that manage (TreeModelListener) listeners, get the root node and permit access to, or statistics about, the nodes at each level in the hierarchy. However, the TreeModel interface doesn't provide methods that actually change values in the model, so we use the DefaultTreeModel as the basis for our own JComponentTree model, accessing available nodes through the TreeModel interface.

My first attempt to keep the compatibility between JTree and JComponentTree optimized led to a few deep forays into the JTree source code. Starting with a pure implementation of the TreeNode interface wasn't enough since it doesn't allow you to change any values. For that you need the MutableTreeNode, which provides a method for setting a user object - in our case a Component to be displayed. It doesn't provide a way to get the object back, however. This is a bit of a mystery to me, and clearly a shortcoming of the interface design. Our attempt to make the tree model as generic and compatible to JTree as possible leads us finally to the DefaultMutableTreeNode class.

Since we have to use the DefaultMutableTreeNode as the basis for our own tree structure, we'll add another layer, the ComponentTreeNode, to provide type safety, thus ensuring that the user object is always a component. By extending DefaultMutableTreeNode, we can also make sure that a JTree control could display the same basic structure with minimal effort.

Listing 1 shows the source code for the ComponentTreeNode class. We extend DefaultMutableTreeNode and pass the component to be stored as the user object in the constructor, saving it by calling the superclass constructor. The getComponent method casts the user object into a Component when we ask for it.

The ComponentTreeLayout Manager
To keep the coupling to a minimum, we use a layout manager. Unfortunately, this implementation is slightly more coupled than you might normally expect since we need to draw the lines connecting nodes by explicitly using the paintComponent method in the parent container.

The ComponentTreeLayout class performs a lot of work. Here's a quick list of its major responsibilities.

  • Set/get values for alignment, linetype, tree direction, etc.
  • Calculate minimumLayoutSize and preferredLayoutSize.
  • Calculate component positions and lay out the tree.
  • Draw the lines that connect each of the components.
The more notable member variables are alignment, linetype and direction. The alignment value determines whether child nodes are aligned to the LEFT, CENTER or RIGHT of the parent. The linetype value determines whether lines are drawn directly between nodes in a STRAIGHT line or as SQUARE lines, forming right angles to the nodes. The direction value determines whether the tree is drawn with its leaves to the NORTH, SOUTH, EAST or WEST.

Each of these values has an associated set and get method that follows the JavaBean standard (setValue/getValue, where Value is the actual name of the variable), and constant values are declared in the ComponentTreeConstants interface, which you can see in Listing 2. In addition to these, we have access to the TreeModel model value, which is exposed through the getModel and setModel methods, and the root node, through the setRoot and getRoot methods. Several constructor variants are available. Each requires a TreeModelListener so that we can notify the parent container of any changes to the model.

Size Calculations
The ComponentTreeLayout subclasses the AbstractLayout class, which you might remember from an earlier article I wrote, "Practical Layout Managers" (JDJ, Vol. 3, Issue 8). It provides default behavior for most layout manager calls and a good foundation to start from when you're working with them. As with any layout manager, the ComponentTreeLayout must calculate the minimum and preferred size for the container in which it's used.

Listing 3 shows code for the preferredLayoutSize method. We take the inset values into account and consider the horizontal gap between each node. Figure 2 shows how we can determine the width of a given node and its immediate children. We simply add the child node widths and the hgap values together, and test to make sure the parent node is not wider. If it is, we always take the wider value.

Figure 2
Figure 2:

The main preferredLayoutSize method calls getPreferredSize with the root node, retrieved directly from the model, and getPreferredSize calls itself for each of the child nodes it finds along the way. This accumulates the height and width until we've traversed the entire tree and then returns the correct value at each level. The minimumLayoutSize calculation is almost identical, though we ask each node for its minimumSize instead.

Positioning Components
When the container calls doLayout, the layout manager calls the layoutContainer method. This method determines which orientation we're dealing with and provides a starting x and y value along with the root node to the layout method, which recursively repositions and resizes each component using the setBounds method.

The layout method needs to determine the size of each node and its immediate children, but it can't call the getPreferredSize method without running into proportion problems. Instead, we have a near duplicate of getPreferredSize called getLayoutSize. Listing 4 shows the layoutContainer and layout methods, but leaves out getLayoutSize because it's almost identical to getPreferredSize in Listing 3.

The layout method takes into account the orientation and node alignment before deciding where each node should be placed. We walk through the child nodes, calculate the x and y positions and recursively call the layout method to traverse the tree structure.

To paint the lines between components, we have to call the drawLines method explicitly. Listing 5 shows the drawLines method, which recursively draws lines by calling getBounds on each component to determine where they're positioned. Painting always happens after the layout call, so this is perfectly safe.

The JComponentTree Component
The JComponentTree code, shown in Listing 6, provides an interface to a number of ComponentTreeLayout methods, allowing the model, orientation, linetype and alignment to be set and retrieved. Whenever one of these attributes is reset, the doLayout method is called, along with repaint, to refresh the JComponentTree view. Listing 6 shows only one of the available constructor variations, and skips some of the accessor methods and most of the methods required by the TreeModelListener interface. Each of the constructors calls the ComponentTreeLayout equivalent. The one in Listing 6 is the most extensive.

Figure 3
Figure 3:  An application component hierarchy

The JComponentTree control also implements the TreeModelListener interface to monitor changes in the tree structure. If one of these events is fired, the layout is recalculated and redrawn. When field values are changed, we also fire the layout method and repaint the tree. The exception is setDirection, which also calls setSize to make sure the scrollable panel size is correct.

The addNode method creates a ComponentTreeNode object and adds the component to the container. To make it possible to create children that refer to an added node, we return the ComponentTreeNode object and handle null parents as a request to set the root node. You'll want to pay attention to this since accidental null parent nodes won't throw an exception and you'll end up with unexpected results.

When you download the code from our Web site, you'll find a test class called JComponentTreeTest. This class generates a random tree with variations in depth, maximum width and randomly selected components. In addition, it provides a set of buttons that lets you dynamically change the direction, linetype and node alignment so you can get a feel for what can be done.

Summary
You now have yet another control to add to your programming toolbox. JComponentTree offers an occasional alternative to JTree and provides some overlapping functionality, but it's typically used in situations that require displaying a tree structure in different ways. This widget lets you organize arbitrary components rather than using a renderer to display data, so it has a different overall purpose. Still, there's just enough commonality with JTree to allow for easy migration.

About the Author
Claude Duguay has been programming since 1980. In 1988 he founded LogiCraft Corporation, and he currently leads the development team at Atrieva Corp. You can contact him at [email protected]

	

Listing 1
 
public class ComponentTreeNode extends DefaultMutableTreeNode 
{ 
  public ComponentTreeNode(Component obj) 
  { 
    super(obj); 
  } 

  public ComponentTreeNode(Component obj, boolean allowsChildren) 
  { 
    super(obj, allowsChildren); 
  } 

  public Component getComponent() 
  { 
    return (Component)getUserObject(); 
  } 
} 

Listing 2
 
public interface ComponentTreeConstants 
{ 
  // Orientation constants 
  public static final int NORTH = 1; 
  public static final int SOUTH = 2; 
  public static final int EAST = 3; 
  public static final int WEST = 4; 

  // Justification constants 
  public static final int LEFT = 1; 
  public static final int CENTER = 2; 
  public static final int RIGHT = 3; 
  public static final int TOP = 1; 
  public static final int MIDDLE = 2; 
  public static final int BOTTOM = 3; 

  // Line type constants 
  public static final int SQUARE = 1; 
  public static final int STRAIGHT = 2; 
} 

Listing 3 

public Dimension preferredLayoutSize(Container container) 
{ 
  Dimension dim = getPreferredSize(getRoot()); 
  Insets insets = container.getInsets(); 
  int vertInsets = insets.top + insets.bottom; 
  int horzInsets = insets.left + insets.right; 
  dim.width += horzInsets; 
  dim.height += vertInsets; 
  return dim; 
} 

private Dimension getPreferredSize(ComponentTreeNode node) 
{ 
  if (!node.getComponent().isVisible()) 
    return new Dimension(0, 0); 

  Dimension dim = node.getComponent().getPreferredSize(); 
  Dimension preferredSize = 
    new Dimension(dim.width, dim.height); 

  int children = model.getChildCount(node); 
  if (direction == EAST || direction == WEST) 
  { 
    int width = 0; 
    int height = 0; 
    if (children > 0) 
    { 
      Dimension size; 
      ComponentTreeNode child; 
      for (int i = 0; i < children; i++) 
      { 
        child = (ComponentTreeNode)model.getChild(node, i); 
        if (child.getComponent().isVisible()) 
        { 
          size = getPreferredSize(child); 
          height += size.height + vgap; 
          if (size.width > width) 
          width = size.width; 
        } 
      } 
    } 
    preferredSize = new Dimension( 
      preferredSize.width + width + hgap, 
      Math.max(preferredSize.height, height - vgap)); 
  } 
  if (direction == NORTH || direction == SOUTH) 
  { 
    int width = 0; 
    int height = 0; 
    if (children > 0) 
    { 
      Dimension size; 
      ComponentTreeNode child; 
      for (int i = 0; i < children; i++) 
      { 
        child = (ComponentTreeNode)model.getChild(node, i); 
        if (child.getComponent().isVisible()) 
        { 
          size = getPreferredSize(child); 
          width += size.width + hgap; 
          if (size.height > height) 
     height = size.height; 
        } 
      } 
    } 
    preferredSize = new Dimension( 
      Math.max(preferredSize.width, width - hgap), 
      preferredSize.height + height + vgap); 
  } 
  return preferredSize; 
} 

Listing 4 

public void layoutContainer(Container container) 
{ 
  Insets insets = container.getInsets(); 
  Dimension dim = container.getSize(); 
  Dimension prefered = getPreferredSize(getRoot()); 
  int vertInsets = insets.top + insets.bottom; 
  int horzInsets = insets.left + insets.right; 

  if (direction == WEST) 
    layout(getRoot(), dim.width - insets.right, insets.top); 
  if (direction == NORTH) 
    layout(getRoot(), insets.left, dim.height - insets.bottom); 
  if (direction == EAST || direction == SOUTH) 
    layout(getRoot(), insets.left, insets.top); 
} 

public void layout(ComponentTreeNode node, int x, int y) 
{ 
  if (!node.getComponent().isVisible()) return; 

  int children = model.getChildCount(node); 
  if (direction == EAST || direction == WEST) 
  { 
    Dimension size = node.getComponent().getPreferredSize(); 

    Dimension down = getLayoutSize(node); 

    int pos = y; 
    if (alignment == MIDDLE) 
    { 
      pos = y + (down.height - size.height) / 2; 
    } 
    if (alignment == BOTTOM) 
    { 
      pos = y + (down.height - size.height); 
    } 
    if (direction == EAST) 
    { 
      node.getComponent(). 
        setBounds(x, pos, size.width, size.height); 
      x += Math.max(size.width, down.width) + hgap; 
    } 
    else 
    { 
      node.getComponent(). 
        setBounds(x - size.width, pos, size.width, size.height); 
      x -= Math.max(size.width, down.width) + hgap; 
    } 

    ComponentTreeNode child; 
    for (int i = 0; i < children; i++) 
    { 
      child = (ComponentTreeNode)model.getChild(node, i); 
      if (child.getComponent().isVisible()) 
      { 
        layout(child, x, y); 
        y += getLayoutSize(child).height + vgap; 
      } 
    } 
  } 
  if (direction == NORTH || direction == SOUTH) 
  { 
    Dimension size = 
      node.getComponent().getPreferredSize(); 
    Dimension right = getLayoutSize(node); 

    int pos = x; 
    if (alignment == CENTER) 
    { 
      pos = x + (right.width - size.width) / 2; 
    } 
    if (alignment == RIGHT) 
    { 
      pos = x + (right.width - size.width); 
    } 

    if (direction == SOUTH) 
    { 
      node.getComponent(). 
        setBounds(pos, y, size.width, size.height); 
      y += Math.max(size.height, right.height) + vgap; 
    } 
    else 
    { 
      node.getComponent(). 
        setBounds(pos, y - size.height, size.width, size.height); 
      y -= Math.max(size.height, right.height) + vgap; 
    } 

    ComponentTreeNode child; 
    for (int i = 0; i < children; i++) 
    { 
      child = (ComponentTreeNode)model.getChild(node, i); 
      if (child.getComponent().isVisible()) 
      { 
        layout(child, x, y); 
        x += getLayoutSize(child).width + hgap; 
      } 
    } 
  } 
} 

Listing 5. 

public void drawLines(Container cont, Graphics g) 
{ 
  Color dark = cont.getBackground().darker(); 
  Color lite = cont.getBackground().brighter(); 
  drawLines(getRoot(), g, dark, lite); 
} 

private void drawLines(ComponentTreeNode node, 
  Graphics g, Color dark, Color lite) 
{ 
  if (!node.getComponent().isVisible()) return; 
  int children = model.getChildCount(node); 
  if (direction == EAST || direction == WEST) 
  { 
    Rectangle dim = node.getComponent().getBounds(); 
    int x0, y0, x1, y1, x2, y2; 

    if (direction == EAST) 
    { 
      x0 = dim.x + dim.width; 
      x1 = x0 + hgap / 2; 
      x2 = x0 + hgap - 1; 
    } 
    else 
    { 
      x0 = dim.x; 
      x1 = x0 - hgap / 2; 
      x2 = x0 - hgap + 1; 
    } 
    y0 = dim.y; 
    y1 = dim.y + dim.height / 2; 

    ComponentTreeNode child; 
    for (int i = 0; i < children; i++) 
    { 
      child = (ComponentTreeNode)model.getChild(node, i); 
      if (child.getComponent().isVisible()) 
      { 
        Rectangle bounds = child.getComponent().getBounds(); 
        y2 = bounds.y + bounds.height / 2; 

        if (linetype == SQUARE) 
        { 
          drawLine(g, dark, lite, x0, y1, x1, y1); 
          drawLine(g, dark, lite, x1, y2, x2, y2); 
          if (y1 != y2) 
            drawLine(g, dark, lite, x1, y1, x1, y2); 
          if (i == 0) 
          { 
            if(alignment == LEFT) 
              y0 = Math.min(y2, y1); 
            if (alignment == RIGHT) 
              y0 = Math.max(y2, y1); 
          } 
          if (children >= 1  && alignment != CENTER) 
            drawLine(g, dark, lite, x1, y0, x1, y2); 
        } 
        else 
        { 
          drawLine(g, dark, lite, x0, y1, x2, y2); 
        } 

        drawLines(child, g, dark, lite); 
      } 
    } 
  } 
  if (direction == NORTH || direction == SOUTH) 
  { 
    Rectangle dim = node.getComponent().getBounds(); 
    int x0, y0, x1, y1, x2, y2; 

    if (direction == SOUTH) 
    { 
      y0 = dim.y + dim.height; 
      y1 = y0 + vgap / 2; 
      y2 = y0 + vgap - 1; 
    } 
    else 
    { 
      y0 = dim.y; 
      y1 = y0 - vgap / 2; 
      y2 = y0 - vgap + 1; 
    } 
    x0 = dim.x; 
    x1 = dim.x + dim.width / 2; 

    ComponentTreeNode child; 
    for (int i = 0; i < children; i++) 
    { 
      child = (ComponentTreeNode)model.getChild(node, i); 
      if (child.getComponent().isVisible() 
      { 
        Rectangle bounds = child.getComponent().getBounds(); 
        x2 = bounds.x + bounds.width / 2; 

        if (linetype == SQUARE) 
        { 
          drawLine(g, dark, lite, x1, y0, x1, y1); 
          drawLine(g, dark, lite, x2, y1, x2, y2); 
          if (x1 != x2) 
            drawLine(g, dark, lite, x1, y1, x2, y1); 
          if (i == 0) 
          { 
            if (alignment == LEFT) 
              x0 = Math.min(x2, x1); 
            if (alignment == RIGHT) 
              x0 = Math.max(x2, x1); 
          } 
          if (children >= 1 && alignment != CENTER) 
          drawLine(g, dark, lite, x0, y1, x2, y1); 
        } 
        else 
        { 
          drawLine(g, dark, lite, x1, y0, x2, y2); 
        } 
        drawLines(child, g, dark, lite); 
      } 
    } 
  } 
} 

private void drawLine(Graphics g, 
  Color dark, Color lite, int x1, int y1, int x2, int y2) 
{ 
  g.setColor(lite); 
  g.drawLine(x1, y1, x2, y2); 
  g.setColor(dark); 
  g.drawLine(x1 + 1, y1 + 1, x2 + 1, y2 + 1); 
} 

Listing 6. 

public class JComponentTree extends JPanel 
  implements ComponentTreeConstants, TreeModelListener 
{ 
  protected ComponentTreeLayout treeLayout; 
  protected CellRendererPane pane; 

  public JComponentTree( 
    int direction, int alignment, int linetype, 
    int hgap, int vgap) 
  { 
    treeLayout = new ComponentTreeLayout(this, 
      direction, alignment, linetype, hgap, vgap); 
    setLayout(treeLayout); 
  } 

  // Other constructor variations not listed 

  public ComponentTreeNode addNode( 
    ComponentTreeNode parent, Component child) 
  { 
    ComponentTreeNode node = 
      new ComponentTreeNode(child); 
    if (parent == null) setRoot(node); 
    else treeLayout.addNode(parent, node); 
    add(child); 
    return node; 
  } 

  public void setDirection(int direction) 
  { 
    treeLayout.setDirection(direction); 
    setSize(getPreferredSize()); 
    doLayout(); 
    repaint(); 
  } 

  public int getDirection() 
  { 
    return treeLayout.getDirection(); 
  } 

  // Other field accessor methods not listed 

  public void paintComponent(Graphics g) 
  { 
    super.paintComponent(g); 
    treeLayout.drawLines(this, g); 
  } 

  public Insets getInsets() 
  { 
    return new Insets(10, 10, 10, 10); 
  } 

  public void treeNodesChanged(TreeModelEvent event) 
  { 
    doLayout(); 
    repaint(); 
  } 

  // Other TreeModelListener methods not listed 
} 


Download Assoicated Source Files (Zip format - 33.4 KB)
 

All Rights Reserved
Copyright ©  2004 SYS-CON Media, Inc.
  E-mail: [email protected]

Java and Java-based marks are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries. SYS-CON Publications, Inc. is independent of Sun Microsystems, Inc.