ORIGINAL DRAFT
This article tries to take the mystery out of the black art of developing layout managers. Much of the coverage in books and magazines typically centers on trying to wrench the complicated GridBagLayout into submission or demonstrates the development of a layout manager with virtually no practical use. In the real world of software engineering, the need for applicable solutions takes precedence. This article will add a trio of reusable classes to your inventory and, hopefully, a few techniques to your personal bag of tricks.
If you’ve done any Java user interface development, you’ve probably used the BorderLayout and GridLayout to some extent. In fact, you probably figured out, pretty quickly, that nesting these layout managers typically gives you the control you need to get the job done, getting the layout to look the way you intended. Since one of Java’s primary objectives is portability, you’ve managed to avoid hard-coding positions by setting the layout manager to null, right? If you haven’t, you really should stop doing that. Unfortunately, some RAD tools tend to do this by default. But the payoff is considerable if you use proper layout managers.
Practical Layout Design
We can learn a little from the layout managers that ship with the Java Development Kit and a couple of the new layout managers released with the Java Foundation Classes. Here’s a quick summary of those managers. Pay special attention to the descriptions.
Layout Manager | From | Description |
---|---|---|
BorderLayout | JDK | Lays out a container, arranging and resizing its components to fit into five regions: North, South, East, West and Center. |
CardLayout | JDK | Acts like a stack of cards, treating each component in the container as a card where only one card is visible at a time. |
FlowLayout | JDK | Arranges components in a left-to-right flow, like lines of text in a paragraph. |
GridLayout | JDK | Lays out a container's components in a rectangular grid. |
GridBagLayout | JDK | Lays out components based on a comprehensive constraint object that defines relative positional information. |
BoxLayout | JFC | Allows multiple components to be laid out either vertically or horizontally. |
OverlayLayout | JFC | Arrange components on top of each other, overlapping where their dimensions require it. |
The JFC also introduces the ScrollPaneLayout and ViewportLayout managers, but they are so tightly coupled to their respective JScrollPane and JViewport components that they are virtually useless in other contexts.
You’ll notice a few things about each of these:
- Each has a simple purpose, described in a single sentence and easy to understand.
- None of them uses exact positioning and so avoids tight coupling to a given display area.
- Each method has a natural organization behind it, often reflected in a real-life approach.
- All layout managers have the Layout suffix as part of their name.
This article cover the implementation of the following new layout managers.
Layour Manager | Description |
---|---|
CastleLayout | Arranges components to fit into nine regions: North, South, East, West, NorthWest, NorthEast, SouthWest, SouthEast and Center. |
ProportionLayout | Lays out components in a proportional rectangular grid defined by a collection of specified row heights and column widths. |
ScalingLayout | Lays out components in a scaleable regular grid where components can occupying any specified rectangular region, possibly overlapping. |
We’ll elaborate on their internal function after a quick look at the layout manager interfaces.
The LayoutManager Interface
The basic design of a LayoutManager is fairly simple. You need to declare your class as implementing the LayoutManager interface and then implement the following methods:
addLayoutComponent(String, Component)
removeLayoutComponent(Component)
layoutContainer(Container)
Dimension minimumLayoutSize(Container)
Dimension preferredLayoutSize(Container)
The addLayoutComponent method is called when you use the add method in your container. Typically, the add method has only one parameter, so the addLayoutComponent method is called with the string set to null. With the BorderLayout, the label indicating position is passed using the add method with the string and component arguments. The layout manager handles the add with the addLayoutComponent method. The remove method is seldom used in a container but it works pretty much the same way, calling the removeLayoutComponent method. The layoutContainer method is called when you or the system call the doLayout method. Two additional methods are provided to determine what the minimum and preferred layout sizes are. These are called by getMinimumSize and getPreferredSize methods.
The LayoutManager2 Interface
Most of the shortcomings in the LayoutManager design stem from having to use a String to describe positional or constraint information in the add method. The LayoutManager2 interface extends LayoutManager to deal with this more effectively. With the extended addLayoutComponent method, you can pass any object you want to handle specialized circumstances. The container add method calls this method automatically if it has the component, object argument signature.
Here’s a list of the methods in the LayoutManager2 interface. Note that these extend the LayoutManager interface and, therefore, require those methods to be implemented as well.
addLayoutComponent(Component, Object)
Dimension maximumLayoutSize(Container)
invalidateLayout(Container)
int getLayoutAlignmentX(Container)
int getLayoutAlignmentY(Container)
The getMaximumSize method in containers is supported by calling the maximumLayoutSize method in the layout manager. This functionality was missing in the earlier interface and seems like a natural extension. If the layout manager or supporting classes make changes to the container in important ways, the invalidateLayout method accommodates forcing a repaint operation. Finally, the getLayoutAlignmentX and getLayoutAlignmentY methods provide a way of determining how to align the component. The value returned, indicates a relative position along the specified axis. A value of zero (0) indicates the origin and one (1) furthest from the origin. A value of 0.5, typically used as a default, indicates a position in the middle.
The AbstractLayout Class
When I developed the three layout managers presented here, I first wrote them independently. But as often happens in a development project, I noticed several common elements and revised the code to move these commonalties into a parent class. The resulting abstract class is reusable, as you might expect, and provides a number of common and default behaviors that help make the code much thinner. Figure 1 shows the class hierarchy for the layout managers and their AbstractLayout parent class. Let’s take a quick look at the commonalties.
Each of the layout managers allow you to specify horizontal and vertical gaps between components. These have accessor methods associated with them so the values can be retrieved and set outside the layout manager’s constructor. Internally, they are represented as integer member variables named hgap (horizontal gap) and vgap (vertical gap). The accessors follow the Java Bean convention and are named with the with the get and set prefix. These methods will not be presented in this article, but you can find the entire code base on the Java Developers Journal web site.
The maximumLayoutSize method always returns a Dimension object with Integer.MAX_VALUE set for the width and height, leaving the maximum size unconstrained.. We’ll present specific code for the preferredLayoutSize method, which calculates the appropriate dimensions for the full layout manager, with each respective layout manager. The similarity with minimumLayoutSize is so pronounced however, that its not worth presenting those individually. The difference between them lies in the calls made to getPreferredSize for each component in the preferredLayoutSize method and the use of getMinimumSize in the minimumLayoutSize methods, respectively.
When implementing the LayoutManager2 interface, we need to provide the getLayoutAlignmentX and getLayoutAlignmentY methods. By convention, these always return a value of 0.5, which indicates centering as the default position for smaller containers. The layout managers we present always resize the component anyway, so these have no real effect on the layout behavior. If you have more specific needs, you can override this method in a subclass, at your discretion.
The addLayoutComponent and removeLayoutComponent handling is not order dependent except in the CastleLayout, which only has nine (9) positions and thus controls where in the component array each is stored. As such, we’ll only present the CastleLayout code. The AbstractLayout class simply assumes the container handles these values. The ScalingLayout manager extends this behavior and stores the constraints in a hash table.
Finally, the toString method is always implemented, as is the invalidateLayout method which actually does nothing, though it has to be present to implement the interface. This method is only used to force a repaint in specialized layout managers.
The CastleLayout Class
The CastleLayout manager is designed to handle a number of real life cases, such as:
- Setting up scroll bars or rulers in positions relative to the central panel.
- Creating borders with sides and corners drawn by individual components.
- Drawing legends, labels or other details, relative to a central display.
- Placing tool bars, logos and control elements around a central panel.
Figure 2 shows how the various areas are laid out, by name.
The CastleLayout manager implements the LayoutManager2 interface, so it has to implement all the methods in the LayoutManager and LayoutManager2 interfaces. The most important methods are the preferredLayoutSize and layoutContainer, so we’ll focus on those. There is some code reuse in these and a handful of private internal methods to make things easier. Take a look at Listing 1 to see how they work.
The instance variables and constants are listed first. The getCompoment method defaults to CENTER if the name is not provided. We assume a null name is handled the same way as the BorderLayout default. The isVisible method returns true if the component is neither null nor invisible. If a component is invisible it is considered unavailable during the layout call. The setBounds method avoids resizing an invisible component while properly handling visible components. The getSize method makes it possible to reuse the same code to get the preferred and minimum size of a given component.
The primary purpose of the interface method preferredLayoutSize is to figure out how big the container should be if it is contained in another layout manager, or if the pack method is used to resize a dialog box or window frame. We need to figure out what the maximum size of each of the north, south, east and west positions are, depending on the components in those rows and columns. To so this, we walk through the list of elements in a very specific order, calculating the largest size at each stop. At the end, we are left with the center area occupying the remaining space. Notice that the vertical and horizontal gaps are part of these calculations and that the insets for the layout are factored in as well.
Listing 2 shows the calculateLayoutSize method, which returns the four inset positions for the left/right widths and top/bottom heights. It calls the getSize private method with the correct type (PREFERRED or MINIMUM) which is passed to it by the calling method. The preferredLayoutSize method itself then takes the center component, vertical and horizontal gaps and the container’s insets into account before returning the preferred dimensions. The minimumLayoutSize is virtually identical except that the MINIMUM constant is used instead of PREFERRED.
The layoutContainer method is actually responsible for the real work in a layout manager. The code is presented in Listing 3. We do this in two passes, calculating the row and column positions for the north, south, east and west elements (which, naturally, includes the corners) by calling the calculateLayoutSize again. Once we have those figured out, we can resize the components with the setBounds method. Like the preferred and minimum size calculations, we account for the vertical and horizontal gaps and the container’s insets. Any remaining space is given to the center component.
The ProportionalLayout Class
The ProportionalLayout manager addresses the following situations:
- Lets the programmer control the width of individual columns in a grid layout.
- Lets the programmer control the height of individual rows in a grid layout.
- Provides an easy way to create homogeneous rows or columns.
Figure 3 shows how the rows and columns can vary. The values are shown as 1 and 2 for simplicity but they are completely arbitrary and relate only to each other. Any integer can be used, so long as the total of all rows or all columns doesn’t go over Integer.MAX_VALUE.
Listing 4 shows how the row proportions are precalculated when the value is set. This might take place in the constructor or with an explicit call to the setRows method. There are two signatures for setRows, one of which allows you to set an arbitrary number of rows to 1. This provides the same behavior as the GridLayout manager and may be applied to only one or both the vertical and horizontal dimensions. The setCols methods are not presented because they are virtually identical except for the orientation. The real trick lies in maintaining an integer array as well as a floating point array. The first stores the integer proportions used by the layout manager. The second normalizes those values into a range between 0 and 1 so that laying things out is easier.
The ProportionalLayout implements the LayoutManager interface, so it has fewer methods than the other two layout managers. Given that we are inheriting from the AbstractLayout class, this matters little, but its worth noting anyway. Like all layout managers, the preferredLayoutSize and minimumLayoutSize represent a good part of the work to be done. Listing 5 shows the preferredLayoutSize method. We loop through each row and column, accounting for the possibility that fewer than the maximum number of components might be used, and determine what the relative unit size should be. We then loop through each orientation to add up the width and height required. As you might expect, the minimumLayout size is virtually identical except for the call to getMinimumSize to get the dimensions.
Once again, the real workhorse is the layoutContainer method. Listing 6 shows how we start our calculation by determining what the unit size has to be. We use floating point numbers to avoid gross rounding errors during scaling. If we used integers directly, the boundary conditions would show up unevenly in our display, making some units noticeably larger or smaller than the average. We loop through the contained components and resize them based on the relative row and column sizes. All we have to do is call setBounds for each container and we’re done.
The ScalingLayout Class
The ScalingLayout manager addresses the following situations:
- Layout components positionally without tight coupling to display resolution.
- Allows scaling without destroying relative positioning of components.
- Allows objects to overlap if necessary - drawing windows, for example.
- Ideal for drawing line art, where scale changes but not realive positions.
Figure 4 shows how the ScalingLayout might look for 6 components placed at various positions in the scalable grid area. For clarity, the rectangle values are represented as x,y-width,height.
The ScalingLayout implements the LayoutManager2 interface. We use the java.awt.Rectangle object to describe constraints. The default Container behavior does not actually save this information. To properly handle this, we override the default addLayoutComponent and removeLayoutComponent to handle this explicitly. We use a HashTable object to store the Rectangle constraint associated with each object and remove it, as appropriate. Listing 7 shows these simple methods. Notice how we check the instance type for the constraint parameter in the addLayoutComponent method to help developers find the problem more easily if they use a different object type.
By now, the preferredLayoutSize and layoutContainer methods should be old friends. Not surprisingly, Listing 8 shows how the preferredLayoutSize is handled and the implication remains that the minimumLayoutSize method is almost identical, except for the underlying calls to getPreferredSize. Since we know the grid is made up of uniform cell sizes, we find the largest vertical and horizontal unit sizes and simply multiply by the number of rows and columns to get the preferred and minimum dimensions.
The layoutContainer method is presented in Listing 9. We first calculate the vertical and horizontal unit size for each grid cell as a floating point value, accounting for the container insets and the vertical and horizontal gaps. Then, we lay out each of the components in the order they are found in the Container. This is an important point if your objects overlap, since the last drawn components will overlap those which are drawn first. Notice that when we calculate the width and height, we work backward from the calculated x and y positions for the left and right or top and bottom positions. Rather than setting the component bounds to the x,y position and a calculated width and height, we figure out the right and bottom positions (based on the rectangle width and height) and then work out the relative bounds’ width and height. Doing this the more obvious way results in occasional single-pixel gaps that you don’t really want to see.
Summary
Figure 5 shows a digital clock example which is implemented almost entirely with layout managers, in particular the three presented in this article. The code is not presented here but can be found on the Java Developers Journal web site, along with the layout manager and related test harness code. The clock uses the JFC, so you’ll need to have it installed to try it out. The CastleLayout is used to place the sides and corners, the ProportionLayout is used for each digit and colon. The ScalingLayout is used to position the digits and the colon in the central area. The whole thing is implemented as the main method in the Crystal class, given that its the only new class required to draw the rounded green crystals. The rest of the code is static and only present to make the nested layout code more readable. There’s no actual timing code, since this was meant as an example, but it wouldn’t take much to turn it into a valid prop for Jeff Goldblum if you’re shooting the sequel to "Independence Day", given that it can easily scale to fill the laptop screen for effect.
Layout managers are rarely implemented by developers working on Java projects, yet they represent one of the simplest ways of controlling component positioning without tight coupling to the display resolution. While they have been discusses in various books and articles, few of these have presented a practical approach to developing solutions with reusable layout manager classes. There are many cases where it is simpler to develop a new layout manager than to nest the various existing managers to get the desired look and feel. My own experience with the GridBagLayout has never lead to a satisfying, predictable, result. If you’ve mastered and prefer it, you have my blessing.
This article tried to present enough information and example code to make developing custom layout managers an easy choice for you to make, when the opportunity presents itself. You should now have a good foundation that can help you make a more informed choice next time you are presented with a problem that would benefit from a custom layout manager. Finally, if you take nothing more away from this article than the three layout managers implemented here, you should have gained a trio of useful components for your programming arsenal which will hopefully serve you well in your programming endeavors.
Listing 1
public static final String EAST = "East";
public static final String WEST = "West";
public static final String NORTH = "North";
public static final String SOUTH = "South";
public static final String CENTER = "Center";
public static final String NORTHEAST = "NorthEast";
public static final String NORTHWEST = "NorthWest";
public static final String SOUTHEAST = "SouthEast";
public static final String SOUTHWEST = "SouthWest";
protected Hashtable table = new Hashtable();
private static final int PREFERRED = 1;
private static final int MINIMUM = 0;
private Component getComponent(String name)
{
if (!table.containsKey(name)) name = CENTER;
return (Component)table.get(name);
}
private boolean isVisible(String name)
{
if (!table.containsKey(name)) return false;
return getComponent(name).isVisible();
}
private void setBounds(String name,
int x, int y, int w, int h)
{
if (!isVisible(name)) return;
getComponent(name).setBounds(x, y, w, h);
}
private Dimension getSize(int type, String name)
{
if (!isVisible(name)) return new Dimension(0, 0);
if (type == PREFERRED)
return getComponent(name).getPreferredSize();
if (type == MINIMUM)
return getComponent(name).getMinimumSize();
return new Dimension(0, 0);
}
Listing 2
private Insets calculateLayoutSize(int type)
{
Dimension size = new Dimension(0, 0);
int northHeight = 0;
int southHeight = 0;
int eastWidth = 0;
int westWidth = 0;
size = getSize(type, NORTH);
northHeight = Math.max(northHeight, size.height);
size = getSize(type, SOUTH);
southHeight = Math.max(southHeight, size.height);
size = getSize(type, EAST);
eastWidth = Math.max(eastWidth, size.width);
size = getSize(type, WEST);
westWidth = Math.max(westWidth, size.width);
size = getSize(type, NORTHWEST);
northHeight = Math.max(northHeight, size.height);
westWidth = Math.max(westWidth, size.width);
size = getSize(type, SOUTHWEST);
southHeight = Math.max(southHeight, size.height);
westWidth = Math.max(westWidth,size.width);
size = getSize(type, NORTHEAST);
northHeight = Math.max(northHeight, size.height);
eastWidth = Math.max(eastWidth, size.width);
size = getSize(type, SOUTHEAST);
southHeight = Math.max(southHeight, size.height);
eastWidth = Math.max(eastWidth, size.width);
return new Insets(northHeight,
westWidth, southHeight, eastWidth);
}
public Dimension preferredLayoutSize(Container target)
{
Dimension size, dim = new Dimension(0, 0);
size = getSize(PREFERRED, CENTER);
dim.width += size.width;
dim.height += size.height;
Insets edge = calculateLayoutSize(PREFERRED);
dim.width += edge.right + hgap;
dim.width += edge.left + hgap;
dim.height += edge.top + hgap;
dim.height += edge.bottom + hgap;
Insets insets = target.getInsets();
dim.width += insets.left + insets.right;
dim.height += insets.top + insets.bottom;
return dim;
}
Listing 3
public void layoutContainer(Container target)
{
Insets insets = target.getInsets();
Insets edge = calculateLayoutSize(PREFERRED);
int top = insets.top;
int bottom = target.getSize().height - insets.bottom;
int left = insets.left;
int right = target.getSize().width - insets.right;
setBounds(NORTH,
left + edge.left + hgap, top,
right - edge.left - hgap - edge.right - hgap,
edge.top);
setBounds(SOUTH,
left + edge.left + hgap, bottom - edge.bottom,
right - edge.left - hgap - edge.right - hgap,
edge.bottom);
setBounds(EAST,
right - edge.right, top + edge.top + vgap, edge.right,
bottom - edge.top - vgap - edge.bottom - vgap);
setBounds(WEST,
left, top + edge.top + vgap, edge.left,
bottom - edge.top - vgap - edge.bottom - vgap);
setBounds(NORTHWEST,
left, top, edge.left, edge.top);
setBounds(SOUTHWEST,
left, bottom - edge.bottom,
edge.left, edge.bottom);
setBounds(NORTHEAST,
right - edge.right, top,
edge.right, edge.top);
setBounds(SOUTHEAST,
right - edge.right, bottom - edge.bottom,
edge.right, edge.bottom);
top += edge.top + vgap;
bottom -= edge.bottom + vgap;
left += edge.left + hgap;
right -= edge.right + hgap;
setBounds(CENTER,
left, top, right - left, bottom - top);
}
Listing 4
public void setRows(int rows)
{
if (rows == 0)
{
throw new IllegalArgumentException(
"rows cannot set to zero");
}
iRows = new int[rows];
for (int i = 0; i < rows; i++)
{
iRows[i] = 1;
}
setRows(iRows);
}
public void setRows(int[] rows)
{
if ((rows == null) || (rows.length == 0))
{
throw new IllegalArgumentException(
"rows cannot be null or zero length");
}
float total = 0;
for (int i = 0; i < rows.length; i++)
{
total += rows[i];
}
iRows = rows;
fRows = new float[rows.length];
for (int i = 0; i < rows.length; i++)
{
fRows[i] = (float)iRows[i] / total;
}
}
Listing 5
public Dimension preferredLayoutSize(Container parent)
{
Insets insets = parent.getInsets();
int ncomponents = parent.getComponentCount();
int nrows = iRows.length;
int ncols = iCols.length;
Dimension dim;
Component comp;
int count = 0;
float xUnit = 0;
float yUnit = 0;
float unit;
for (int row = 0; row < nrows; row++)
{
for (int col = 0; col < ncols; col++)
{
if (count > ncomponents) break;
else
{
comp = parent.getComponent(count);
dim = comp.getPreferredSize();
unit = (float)dim.width / (float)iCols[col];
if (unit > xUnit) xUnit = unit;
unit = (float)dim.height / (float)iRows[row];
if (unit > yUnit) yUnit = unit;
count++;
}
}
}
int height = 0;
for (int row = 0; row < nrows; row++)
{
height += yUnit * iRows[row];
}
int width = 0;
for (int col = 0; col < ncols; col++)
{
width += xUnit * iCols[col];
}
return new Dimension(
insets.left + insets.right + width,
insets.top + insets.bottom + height);
}
Listing 6
public void layoutContainer(Container parent)
{
int ncomponents = parent.getComponentCount();
if (ncomponents == 0) return;
Insets insets = parent.getInsets();
int nrows = iRows.length;
int ncols = iCols.length;
int w = parent.getSize().width -
(insets.left + insets.right);
int h = parent.getSize().height -
(insets.top + insets.bottom);
int x = insets.left;
for (int c = 0; c < ncols; c++)
{
int ww = (int)((float)w * fCols[c]) - hgap;
int y = insets.top;
for (int r = 0; r < nrows; r++)
{
int i = r * ncols + c;
int hh = (int)((float)h * fRows[r]) - vgap;
if (i < ncomponents)
{
parent.getComponent(i).setBounds(x, y, ww, hh);
}
y += hh + vgap;
}
x += ww + hgap;
}
}
Listing 7
public void addLayoutComponent(Component comp, Object rect)
{
if (!(rect instanceof Rectangle))
{
throw new IllegalArgumentException(
"constraint object must be a Rectangle");
}
table.put(comp, rect);
}
public void removeLayoutComponent(Component comp)
{
table.remove(comp);
}
Listing 8
public Dimension preferredLayoutSize(Container parent)
{
Insets insets = parent.getInsets();
int ncomponents = parent.getComponentCount();
int nrows = rows;
int ncols = cols;
Dimension dim;
Component comp;
int count = 0;
float xUnit = 0;
float yUnit = 0;
float unit;
int w = 0;
int h = 0;
for (int i = 0; i < ncomponents; i++)
{
comp = parent.getComponent(count);
dim = comp.getPreferredSize();
Rectangle rect = (Rectangle)table.get(comp);
unit = (float)dim.width / (float)rect.width;
if (unit > xUnit) xUnit = unit;
unit = (float)dim.height / (float)rect.height;
if (unit > yUnit) yUnit = unit;
count++;
}
int height = (int)(yUnit * nrows);
int width = (int)(xUnit * ncols);
return new Dimension(
insets.left + insets.right + width,
insets.top + insets.bottom + height);
}
Listing 9
public void layoutContainer(Container parent)
{
Insets insets = parent.getInsets();
int ncomponents = parent.getComponentCount();
int nrows = rows;
int ncols = cols;
if (ncomponents == 0) return;
int w = parent.getSize().width -
(insets.left + insets.right);
int h = parent.getSize().height -
(insets.top + insets.bottom);
float ww = (float)(w - (ncols - 1) * hgap) / (float)ncols;
float hh = (float)(h - (nrows - 1) * vgap) / (float)nrows;
int left = insets.left;
int top = insets.top;
Component comp;
Rectangle rect;
int x, y, width, height;
for (int i = 0; i < ncomponents; i++)
{
comp = parent.getComponent(i);
rect = (Rectangle)table.get(comp);
x = (int)(ww * (float)rect.x) + left;
y = (int)(hh * (float)rect.y) + top;
// Calculate by substraction to avoid rounding errors
width = (int)(ww * (float)(rect.x + rect.width)) - x;
height = (int)(hh * (float)(rect.y + rect.height)) - y;
comp.setBounds(new Rectangle(x, y, width, height));
}
}