ORIGINAL DRAFT

This month we put together a visual component, designed to render a tree structure in a scrollable window. JComponentTree is able to 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 extended the DefaultMutableTreeNode class.

Figure 1: JFC Components displayed in JComponentTree.

Figure 1: JFC Components displayed in JComponentTree.

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

Using the JFC TreeModel

The JComponentTree widget is designed to take advantage of concepts carefully developed by Sun 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 either the JTree or the JComponentTree, or both, components to display the model.

The TreeModel provides methods that manage (TreeModelListener) listeners, gets the root node, and permits access to, or statistics about, the nodes at each level in the hierarchy. The TreeModel interface does not provide methods that actually change values in the model, however, 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 lead 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 does not, however, provide a way to get the object back. 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 will add another layer called 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 from a layout manager, 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 the major responsibilities handled by the layout manager.

  • Set/get values for alignment, line type, tree direction, etc.
  • Calculate minimumLayoutSize and preferredLayoutSize
  • Calculate component positions and layout 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 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 which follows the Java Bean 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 also 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. There are several constructor variants available. Each of them 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 about Practical Layout Managers. It provides default behavior for most layout manager calls and a good foundation to start from when working with layout managers. As with any layout manager, the ComponentTreeLayout manager must calculate the minimum and preferred size for the container in which its used.

Figure 2: Recursive Width Calculation.

Figure 2: Recursive Width Calculation.

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.

The main preferrredLayoutSize 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 minimumLayoutsSize 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 are 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.

Figure 3: An Application Component Hierarchy.

Figure 3: An Application Component Hierarchy.

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 getPrefferedSize 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 are positioned. Painting always happens after the layout call, so this is perfectly safe.

The JComponentTree Component

The JComponentTree code is shown in Listing 6. It 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, 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.

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 creating children that refer to an added node possible, 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 will not throw and 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 let you dynamically change the direction, line type 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 is just enough commonality with JTree to allow for easy migration.

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
}