ORIGINAL DRAFT

Java provides a powerful strategy for arranging components dynamically using layout managers. The layout managers that ship with Java range from the simple BorderLayout to the sophisticated GridBagLayout. GridBagLayout is the most powerful, but rarely used, primarily due of its complexity and the unexpected results programmers are often faced with. I’ve developed a number of custom layout managers over the years but recurring themes suggest that a more universal solution is possible.

This month, we’ll implement a PageLayout manager that enables you to apply more of a publishing metaphor. The approach we’ll use is fairly common in desktop publishing applications. DP software typically uses guidelines to place elements, such as text views, picture frames and illustrations. The elements may span a single row and column, or several. This approach can help us keep the layout manager as simple as possible.

Figure 1: JPageLayout organized with a set of
components. The visible numbers show constraints used get this layout, using Point and Rectangle objects.

Figure 1: JPageLayout organized with a set of components. The visible numbers show constraints used get this layout, using Point and Rectangle objects.

This article introduces a new layout manager, called PageLayout, and a default container called JPageLayout. Our objective is to provide a simple way to setup guidelines, against which we can easily place components. We want to specify rows and columns, with various attributes to control the way the layout manager will scale as the container is resized. We want to be able to place components by specifying the row and column of a given guide line, along with the number of rows and columns it will span.

Figure 2: JPageLayout can show the guides, which are
clearly visible without child components in the layout. Horizontal and vertical gaps are set to 4 in this 
example.

Figure 2: JPageLayout can show the guides, which are clearly visible without child components in the layout. Horizontal and vertical gaps are set to 4 in this example.

It turns out to be fairly easy to do all this from usability point of view. Specifying a given guide row and column can be done with a Point object. If you want to span more than one row and/or column, you can use a Rectangle, specifying the width and height in row and column units instead of pixels. In effect, a Point is really a Rectangle with a row and column of 1 in this context. In short, you can use a Point or Rectangle object to specify the constraint when adding a component to a parent using the standard add method.

We’ll want to apply any lessons learned from the publishing industry, the first of which is the notion of keeping visual breaks to a minimum. This is applicable to user interface design as well. If you draw vertical and horizontal lines along each component edge, extending to the container’s edge, you can count up the lines to apply an index of complexity. The higher this number, the more visually confusing the layout may be. The objective, then, is to keep this count to a minimum.

Another lesson we want to keep in mind comes from the web domain, which has roots in the publishing industry. The HTML table component has become the layout tool of choice for most web developers, primarily because it lets you control rows and columns, span them where you like and apply simple rules for scaling the layout. Some rows and columns have fixed sizes, others are relative to the overall view. We want to be able to do this with our layout manager as well.

Java layout managers typically use relative sizes or the preferred size of a component, or sometimes a set of dominant component sizes, such as the BorderLayout manager. In cases like a GridLayout, the cell sizes are all adjusted on a unit relationship with each other row and column. Another lesson to learn is that, in Java, the minimum, maximum or preferred size is potentially important and should be one way in which row and/or column height/width can be specified.

Given these ideas, we want to specify row and column sizes using various mechanisms. We can then specify our component positions relative to these row and column guidelines. One of our primary objectives is to define ways in which the guidelines will scale, so we’ll use definitions that reflect the ways in which this might happen. A row and/or column specification is either directly specified, with a numerical value, or is always relative to one or more child components. In other words, some specifications will required a numerical value, but others will not.

If we use child-relative values, as does BorderLayout for example, we won’t need to specify a height or width value. We only need to say whether we expect to use the minimum, maximum, or preferred size for the child component. For a given row or column, we’ll use the largest value of the specified type. This is easy enough in principle, but we’ll have to deal with situations in which components span multiple rows or columns.

For the values in which a value is specified, we’ll use HTML table-like notions, like fixed values (in pixels) and percentage values. We’ll also support relative values, where all rows or columns with relative values are added up and normalized and the remaining space is divvied out between them relative to those values. This is a great way to make a row or column twice as big as another, for example. Relative values will be applied last, after minimum, maximum, preferred, fixed and percent values are applied.

To summarize, the PageLayout manager supports the row and column definitions in the following table:

Guide Type Description
FIXED The size is specified in pixels and never changes.
PERCENT The size is defined as a percentage of the whole layout.
RELATIVE The size is determined relative to space remaining and divided over a normalized total of all relative entries such that all relative entries taken together add up to the available space. The size is relative to this total, so several division strategies are possible.
MINIMUM The size is determined by finding the largest minimum size for all components in a given row or column. If components span rows and/or columns, the calculation spreads their size evenly across rows and columns before counting them.
MAXIMUM Same as MINIMUM only the component's maximum size is used.
PREFERRED Same as MINIMUM only the component's preferred size is used.

With these combinations, it is possible to define row and column guides that replicate the behavior of most of the standard layout managers. Take for example the BorderLayout, which could be defined using three rows and three columns. The outer rows and columns would use the PREFERRED type and the middle row and column would use the RELATIVE type. The NORTH and SOUTH components span all three columns. The WEST and EAST components use the middle row in the left and right columns, respectively. The CENTER component goes in the second row and second column position.

The GridLayout is even easier to emulate using the PageLayout, with rows and columns simply defined with the same RELATIVE size. You can get an effect similar to FlowLayout, without the line wrap, by using rows and columns of MINIMUM or PREFERRED type. The BoxLayout can be emulated in the same way by controlling alignment with nested panels. Each existing layout manager has it’s own purpose and the purpose of PageLayout is not to emulate other layout managers, but it’s clearly powerful enough to deal with many situations.

Figure 3: The PageLayout manager extends AbstractLayout
and uses two PageAxix instances, which, in turn, use one or more PageGuide instances. The PageConstants interface 
holds a few common/constant values.

Figure 3: The PageLayout manager extends AbstractLayout and uses two PageAxix instances, which, in turn, use one or more PageGuide instances. The PageConstants interface holds a few common/constant values.

The central classes in our project are the PageLayout, PageAxis and PageGuide classes, so we’ll take a closer look at those. As always, you can find the source for all of the classes at www.javapro.com. Readers who’ve followed this column for while will recognize AbstractLayout as a base class I’ve used before. It manages constructors and accessors for horizontal and vertical gap values, which are common in layout managers, and provides common default behaviors for a number of methods required by the LayoutManager2 interface.

Listing 1 shows the code for PageGuide, which is primarily a container for guide values of various types. As you’ll recall from our earlier table, there are six guide types, three of which require an additional value. PageGuide implements two constructors. The first expects only the type value, taken from the PageConstants interface declarations. I’ve written a support method called validateType that checks to be sure we are using a suitable type. If the type is invalid, we throw an IllegalArgumentException. The second constructor allows for an additional value, which is stored as a double. The rest of the code takes the form of accessor methods, which let us set or get size values, as well as a number of predicate methods that return true when the type is of a specified kind.

The PageAxis class (Listing 2) takes responsibility for specifying the orientation and for storing an ordered list of PageGuide objects for row or column specifications. The constructor checks to be sure the orientation value is valid, throwing an IllegalArgumentException if the value is invalid. I’ve provided a pair of convenience static methods to create instances of either axis, named createXAxis and createYAxis. In addition, there are methods for adding each of the PageGuide types without having to create guide objects directly. Three of these expect a single value argument. In each case, the net effect is the addition of a suitable PageGuide object to the list.

There are only two other methods. The first is a convenience lookup method which casts the generic object retrieved from the Vector class, to a PageGuide. The second does quite a bit of work and deserves a little more attention. Since rows and columns behave the same way, other than orientation, many of the calculations can applied from the PageAxis class, thus reducing the need for extra code in PageLayout.

The necessary arguments are provided based on the axis we are dealing with. The startPosition is the topmost or leftmost position for the row or column we are dealing with, in pixels. The currentSize is the current width or height of the container component, normally JPageLayout, but you can use the layout manager with any container. The gap is either the horizontal or vertical gap between rows or columns. The minimum, maximum, and preferred values are arrays of widths or heights calculated by methods in the PageLayout class. There is an entry for each row or column edge, one more than the number of rows or columns, respectively.

The first loop in the calculatePositions method, precalculates totals for relative and non-relative widths or heights. This is important because relative values are calculated based on remaining space and divided into the total. The minimum, maximum and preferred sizes are taken from the array arguments for the specified index position. The fixed size is specified in pixels and the percent size is calculated relative to the total width or heights of the parent container. Once we have these values, we can proceed to the next loop, which generates an array of guide positions.

The guide positions are always on the right or bottom side of the gap or at the starting position if we are dealing with the first position. The start position accounts for the insets, which may come in to play if borders are used in the parent container. The minimum, maximum, preferred and fixed sizes are already in pixels. With the totals from the previous look, it’s now easy to calculate the percent and relative values. You’ll notice that the initial step is to set the array value to the current offset, and that the last step is to increment the offset by the gap, with suitable increments based on the type of row or column, handled in between. Before returning with the array, we add a final rightmost or bottommost value to the array.

Listing 3 shows the code for PageLayout, which is much easier to read thanks to the work we delegated to PageAxis. As you an see, we provide two constructors, one that uses the default zero values for the horizontal and vertical gaps. Both expect two instances of a PageAxis class, the first for the X axis, the second for the Y axis. I’ve also provided accessors to set these values separately, should you need to change them later on. Remember to call the doLayout method on the container to refresh the view if you do this.

Our implementation of the addLayoutComponent method does nothing if your provide the wrong constraint object. A Rectangle value is stored directly in a HashMap, associated with the component it relates to. This allows us to find the position for each object as we process it. If you specify a Point constraint, we turn it into a Rectangle to avoid branching later in the code. It’s easier to handle a unified constraint than to add a bunch of IF statements to account for the differences. The removeLayoutComponent method reverses this process by removing the HashMap entry for the specified component.

The getComponentSize method is a utility call used by some of the subsequent methods to retrieve the minimum, maximum or preferred size for a given child component. It gets used by both the calculateColSizes and calculateRowSizes, which are responsible for populating the colMins, colMaxs and colPref arrays in the first case, and the rowMins, rowMaxs and rowPref arrays in the second. These methods are called by the minimumLayoutSize and preferredLayoutSize methods, which are part of the LayoutManager interface. They are automatically called whenever the container changes size.

The calculateColSizes and calculateRowSizes methods collect up the respective minimum, maximum and preferred row and column sizes. If child components span only one row or column, the largest value is taken for any given row or column. The design choice I made for handling components that span multiple rows or columns is to use a relative fraction of their minimum, maximum or preferred size. If two rows are spanned, for example, we assume that the largest value required for that row is half the component’s minimum, maximum or preferred width. There are cases where this might prove undesirable but there is no predictably correct solution to this problem in all cases, so this is as good a design choice as any.

The minimumLayoutSize and preferredLayoutSize first make sure that all the size arrays are up to date and then use the sum utility method to add up the totals. You’ll notice that we take both the insets and the gap values into account. The gaps apply only between rows and columns, so we multiply the gap value by one less that the total number of rows and columns. By the time the layoutContainer method is called, both minimumLayoutSize and/or preferredLayoutSize will have been called. So we can use the initialized arrays.

The layoutContainer method sets up some internal variables and calls the X and Y axis calculatePositions methods to determine each of the guide positions. These are stored in the xPositions and yPositions arrays. We can then walk through each of the child components and retrieve their constraint Rectangle from the HashMap we used when adding children. We can find the relative position of each component by looking up it’s X and Y value in the position arrays. We then find the right and bottom positions and reduce the values by the gap, remove the initial X and Y position to find the correct width and height in pixels. Then we can set the bounds for each component.

The last method in Listing 3 is the paintGrid method which is there primarily for verification purposes. There is a related method, with the same name, that requires a boolean value instead, in the JPageLayout component. It lets you turn the grid on and off. If you run the JPageLayout test class and use any command line argument, the example code will show the grid (see Figure 2).

The PageLayout manager is incredibly powerful and easy to use. While layout managers like GridBagLayout are just as powerful, most are seldom used because they are so complex and often unpredictable. JPageLayout provides you with a tool that takes much of its design from the page layout paradigm in desktop publishing applications, which in turn take their wisdom from the centuries old publishing industry. I hope you’ll agree that powerful solutions don’t have to be complex when the design is elegant and the ideas behind it already familiar.

Listing 1

public class PageGuide
  implements PageConstants
{
  protected int type;
  protected double size;
  
  public PageGuide(int type)
  {
    validateType(type, false);
    this.type = type;
    this.size = 0;
  }
  
  public PageGuide(int type, double size)
  {
    validateType(type, true);
    this.type = type;
    this.size = size;
  }

  protected void validateType(int type, boolean any)
  {
    if (type == MINIMUM) return;
    if (type == MAXIMUM) return;
    if (type == PREFERRED) return;
    if (any)
    {
      if (type == FIXED) return;
      if (type == PERCENT) return;
      if (type == RELATIVE) return;
    }
    throw new IllegalArgumentException("Invalid type");
  }
  
  public int getType()
  {
    return type;
  }

  public double getSize()
  {
    return size;
  }

  public boolean isFixed()
  {
    return type == FIXED;
  }
  
  public boolean isPercent()
  {
    return type == PERCENT;
  }
  
  public boolean isRelative()
  {
    return type == RELATIVE;
  }

  public boolean isMinimum()
  {
    return type == MINIMUM;
  }

  public boolean isMaximum()
  {
    return type == MAXIMUM;
  }

  public boolean isPreferred()
  {
    return type == PREFERRED;
  }
}

Listing 2

import java.awt.*;
import java.util.*;
import javax.swing.*;

public class PageAxis extends Vector
  implements PageConstants
{
  protected int orientation;
  
  public PageAxis(int orientation)
  {
    if (orientation != XAXIS &&
        orientation != YAXIS)
    {
      throw new IllegalArgumentException(
        "Invalid orientation");
    }
    this.orientation = orientation;
  }
  
  public static PageAxis createXAxis()
  {
    return new PageAxis(XAXIS);
  }

  public static PageAxis createYAxis()
  {
    return new PageAxis(YAXIS);
  }

  public void addFixedGuide(double size)
  {
    add(new PageGuide(FIXED, size));
  }

  public void addPercentGuide(double size)
  {
    add(new PageGuide(PERCENT, size));
  }

  public void addRelativeGuide(double size)
  {
    add(new PageGuide(RELATIVE, size));
  }
  
  public void addMinimumGuide()
  {
    add(new PageGuide(MINIMUM));
  }
  
  public void addMaximumGuide()
  {
    add(new PageGuide(MAXIMUM));
  }
  
  public void addPreferredGuide()
  {
    add(new PageGuide(PREFERRED));
  }
  
  public PageGuide getGuide(int index)
  {
    return (PageGuide)get(index);
  }
  
  public int[] calculatePositions(
    int startPosition, int currentSize, int gap,
    int[] minimum, int[] maximum, int[] preferred)
  {
    currentSize -= (size() - 1) * gap;
    
    // Calculate totals
    double relativeTotal = 0;
    double nonRelativeTotal = 0;
    for (int i = 0; i < size(); i++)
    {
      PageGuide guide = getGuide(i);
      if (guide.isFixed())
      {
        nonRelativeTotal += guide.getSize();
      }
      if (guide.isPercent())
      {
        double fraction = guide.getSize() / 100.0;
        nonRelativeTotal += fraction * currentSize;
      }
      if (guide.isRelative())
      {
        relativeTotal += guide.getSize();
      }
      if (guide.isMinimum())
      {
        nonRelativeTotal += minimum[i];
      }
      if (guide.isMaximum())
      {
        nonRelativeTotal += maximum[i];
      }
      if (guide.isPreferred())
      {
        nonRelativeTotal += preferred[i];
      }
    }

    // Calculate positions
    double offset = startPosition;
    int[] positions = new int[size() + 1];
    int relativePixels = currentSize - (int)nonRelativeTotal;
    for (int i = 0; i < size(); i++)
    {
      positions[i] = (int)offset;
      PageGuide guide = getGuide(i);
      if (guide.getType() == FIXED)
      {
        offset += guide.getSize();
      }
      if (guide.getType() == PERCENT)
      {
        double fraction = guide.getSize() / 100.0;
        offset += fraction * currentSize;
      }
      if (guide.getType() == RELATIVE)
      {
        double fraction = guide.getSize() / relativeTotal;
        offset += fraction * relativePixels;
      }
      if (guide.isMinimum())
      {
        offset += minimum[i];
      }
      if (guide.isMaximum())
      {
        offset += maximum[i];
      }
      if (guide.isPreferred())
      {
        offset += preferred[i];
      }
      offset += gap;
    }
    positions[size()] = (int)offset;
    return positions;
  }
}

Listing 3

import java.awt.*;
import java.util.*;
import javax.swing.*;

public class PageLayout
  extends AbstractLayout
  implements PageConstants
{
  protected Hashtable map = new Hashtable();
  
  protected PageAxis xAxis;
  protected PageAxis yAxis;
  
  protected int[] colMins, rowMins;
  protected int[] colMaxs, rowMaxs;
  protected int[] colPref, rowPref;
  
  protected int defaultMinimum = 100;
  
  public PageLayout(
    PageAxis xAxis, PageAxis yAxis)
  {
    this.xAxis = xAxis;
    this.yAxis = yAxis;
  }
  
  public PageLayout(
    PageAxis xAxis, PageAxis yAxis,
    int hgap, int vgap)
  {
    super(hgap, vgap);
    this.xAxis = xAxis;
    this.yAxis = yAxis;
  }
  
  public void setXAxis(PageAxis xAxis)
  {
    this.xAxis = xAxis;
  }
  
  public void setYAxis(PageAxis horz)
  {
    this.yAxis = yAxis;
  }
  
  public void addLayoutComponent(Component child, Object constraints)
  {
    if (constraints instanceof Rectangle)
    {
      map.put(child, constraints);
    }
    if (constraints instanceof Point)
    {
      Rectangle rectangle = new Rectangle();
      rectangle.setLocation((Point)constraints);
      rectangle.setSize(1, 1);
      map.put(child, rectangle);
    }
  }

  public void removeLayoutComponent(Component child)
  {
    map.remove(child);
  }
  
  protected Dimension getComponentSize(int sizeType, Component child)
  {
    if (sizeType == MINIMUM)
    {
      return child.getMinimumSize();
    }
    if (sizeType == MAXIMUM)
    {
      return child.getMaximumSize();
    }
    return child.getPreferredSize();
  }
  
  protected void calculateColSizes(Container parent)
  {
    int cols = xAxis.size();
    colMins = new int[cols];
    colMaxs = new int[cols];
    colPref = new int[cols];
    for (int i = 0; i < cols; i++)
    {
      colMins[i] = defaultMinimum;
      colMaxs[i] = defaultMinimum;
      colPref[i] = defaultMinimum;
    }
    int count = parent.getComponentCount();
    for (int i = 0; i < count; i++)
    {
      Component child = parent.getComponent(i);
      Rectangle rect = (Rectangle)map.get(child);
      
      Dimension minSize = child.getMinimumSize();
      int min = minSize.width / rect.width;
      if (min > colMins[rect.x])
      {
        colMins[rect.x] = min;
      }
      
      Dimension maxSize = child.getMaximumSize();
      int max = maxSize.width / rect.width;
      if (max > colMaxs[rect.x])
      {
        colMaxs[rect.x] = max;
      }
      
      Dimension prefSize = child.getPreferredSize();
      int pref = prefSize.width / rect.width;
      if (pref > colPref[rect.x])
      {
        colPref[rect.x] = pref;
      }
    }
  }
  
  protected void calculateRowSizes(Container parent)
  {
    int rows = yAxis.size();
    rowMins = new int[rows];
    rowMaxs = new int[rows];
    rowPref = new int[rows];
    for (int i = 0; i < rows; i++)
    {
      rowMins[i] = defaultMinimum;
      rowMaxs[i] = defaultMinimum;
      rowPref[i] = defaultMinimum;
    }
    int count = parent.getComponentCount();
    for (int i = 0; i < count; i++)
    {
      Component child = parent.getComponent(i);
      Rectangle rect = (Rectangle)map.get(child);
      
      Dimension minSize = child.getMinimumSize();
      int min = minSize.height / rect.height;
      if (min > rowMins[rect.x])
      {
        rowMins[rect.x] = min;
      }
      
      Dimension maxSize = child.getMaximumSize();
      int max = maxSize.height / rect.height;
      if (max > rowMaxs[rect.x])
      {
        rowMaxs[rect.x] = max;
      }
      
      Dimension prefSize = child.getPreferredSize();
      int pref = prefSize.height / rect.height;
      if (pref > rowPref[rect.x])
      {
        rowPref[rect.x] = pref;
      }
    }
  }
  
  protected int sum(int[] array)
  {
    int sum = 0;
    for (int i = 0; i < array.length; i++)
    {
      sum += array[i];
    }
    return sum;
  }
  
  public Dimension minimumLayoutSize(Container parent)
  {
    calculateColSizes(parent);
    calculateRowSizes(parent);
    Insets insets = parent.getInsets();
    int w = insets.left + insets.right;
    int h = insets.top + insets.bottom;
    int width = w + sum(colMins) + (xAxis.size() - 1) * hgap;
    int height = h + sum(rowMins) + (yAxis.size() - 1) * vgap;
    return new Dimension(width, height);
  }

  public Dimension preferredLayoutSize(Container parent)
  {
    calculateColSizes(parent);
    calculateRowSizes(parent);
    Insets insets = parent.getInsets();
    int w = insets.left + insets.right;
    int h = insets.top + insets.bottom;
    int width = w + sum(colPref) + (xAxis.size() - 1) * hgap;
    int height = h + sum(rowPref) + (yAxis.size() - 1) * vgap;
    return new Dimension(width, height);
  }

  public void layoutContainer(Container parent)
  {
    Insets insets = parent.getInsets();
    Dimension parentSize = parent.getSize();
    int width = parentSize.width -
      (insets.left + insets.right);
    int height = parentSize.height -
      (insets.top + insets.bottom);
    int[] xPositions = xAxis.calculatePositions(
      insets.left, width, hgap, colMins, colMaxs, colPref);
    int[] yPositions = yAxis.calculatePositions(
      insets.top, height, vgap, rowMins, rowMaxs, rowPref);
    
    int count = parent.getComponentCount();
    for (int i = 0; i < count; i++)
    {
      Component child = parent.getComponent(i);
      Rectangle rect = (Rectangle)map.get(child);
      int x = xPositions[rect.x];
      int y = yPositions[rect.y];
      int w = xPositions[rect.x + rect.width] - x - hgap;
      int h = yPositions[rect.y + rect.height] - y - vgap;
      child.setBounds(x, y, w, h);
    }
  }
  
  public void paintGrid(Container parent, Graphics g)
  {
    Insets insets = parent.getInsets();
    Dimension parentSize = parent.getSize();
    int width = parentSize.width -
      (insets.left + insets.right);
    int height = parentSize.height -
      (insets.top + insets.bottom);
    int[] xPositions = xAxis.calculatePositions(
      insets.left, width, hgap, colMins, colMaxs, colPref);
    int[] yPositions = yAxis.calculatePositions(
      insets.top, height, vgap, rowMins, rowMaxs, rowPref);
    
    g.setColor(Color.blue);
    int bottom = height + insets.top - 1;
    for (int i = 0; i < xAxis.size() + 1; i++)
    {
      int x = xPositions[i];
      g.drawLine(x, insets.top, x, bottom);
      x -= (hgap + 1);
      g.drawLine(x, insets.top, x, bottom);
    }
    int right = width + insets.left - 1;
    for (int i = 0; i < yAxis.size() + 1; i++)
    {
      int y = yPositions[i];
      g.drawLine(insets.left, y, right, y);
      y -= (vgap + 1);
      g.drawLine(insets.left, y, right, y);
    }
  }
}