A long time ago, in a land far, far away… (from New Zealand) I was writing a GUI in Java. After about 7 levels of nesting I began to think that I was missing something critical - that there had to be an easier way. Don’t get me wrong, I love Java’s concept of resizeable flowing layouts (I cannot count the number of times I have had to deal with a Windows O/S dialog on my 1920x1200 screen utilizing about 10% of my available real estate with a little teeny tiny list box in the middle showing 9 of the 2700 files I am sifting through). But there just has to be a better way!
One of those annoying little non-resizeable windows:
So it’s not that the basic idea behind layout-driven GUIs is flawed, it was just all the darned nesting - it resulted in code that had absolutely no visual resemblance to the end GUI, and resulted in building things inside out, instead of outside in. I wanted a layout that handled most or all of the requirements for a particular window or panel in one level, expressed in a way that made it easy to visualize the end result.
My first foray into writing a layout manager was largely a bust. It specified size and positioning relative to the sides of the window and/or other components. It actually worked quite well, but its big problem was calculating preferred size - it couldn’t; it was designed to position and size the components within a known panel area. It couldn’t figure things out in reverse - things just got too complicated too fast.
Then, drawing on experience from the early days of writing HTML, before CSS, I realized the potential of tables to easily define how different elements of a form should be positioned with respect to one another. Research into existing solutions revealed a few candidates, but they were either commercially licensed (I couldn’t use them for my hobbies), restricted from commercial redistribution (I couldn’t use them for work), or were part of larger frameworks the entirety of which were too bulky for my needs.
But the biggest detractor was that they used constraint objects, but I needed a string-based constraint specification (for reasons not relevant to this discussion). One of these other layout managers was already named TableLayout, so I chose to call mine MatrixLayout (but the name TableLayout was my first choice).
The layout manager itself works for either AWT or Swing layouts; but to compile without the reference to the javax.swing package a number of the preformed layout helper methods at the end need to be commented out or redefined with AWT components.
How It Works
Essentially, the layout works by subdividing the panel into a grid of some number of rows and columns. The resulting cells form the containers in which components can be placed. Spanning, coupled with the ability to specify as many rows or columns desired, allows virtually any non-overlapping form design to be created. Each cell has insets to create space around the component placed in it, avoiding the need to have empty rows and columns to separate components.
Each layout is created with a set of row and column constraints that govern all cells in that row or column. In addition each component is added with constraints which apply to that specific cell. A fundamental design decision was made to have only a single component per cell - this avoids a lot of complications. Putting multiple components in a single cell is done by nesting another panel with its own layout (perhaps a MatrixLayout), which turns the problems into a nesting/recursion consideration which is already nicely solved by Java GUI layout.
Row Constraint Attributes:
|Size||Row Height||"Preferred"||"Preferred", "Pref", n!, n.nn%|
|CellSpan||Default row spanning for cell contents||Defaults||1-number of rows, or '*' for all remaining|
|CellAlign||Default vertical alignment for cell contents||Defaults||"Top", "Center" or "Middle", "Bottom", "Fill"|
|CellInsets||Default vertical insets for cell contents||Defaults||top,bottom (each can be Default, Def, or Dft)||#1|
|CellGroup||Default vertical group for cell contents||Defaults||A label to link cells for common heights|
Column Constraint Attributes:
|Size||Column width||"Preferred"||"Preferred", "Pref", n!, n.nn%|
|CellSpan||Default column spanning for cell contents||Defaults||1-number of columns, or '*' for all remaining|
|CellAlign||Default horizontal alignment for cell contents||Defaults||"Left", "Center" or "Middle", "Right", "Fill"|
|CellInsets||Default horizontal insets for cell contents||Defaults||left,right (each can be Default, Def, or Dft)||#1|
|CellGroup||Default horizontal group for cell contents||Defaults||A label to link cells for common widths|
Cell Constraint Attributes:
|Row||Row number||"Current"||"Next", "Current", 1 - Number of rows||#2|
|Col||Column number||"Next"||"Next", "Current", 1 - Number of columns||#2|
|hSpan||Number of columns to span||"1"||1 - Number of Columns, or '*' for all remaining||#3|
|vSpan||Number of rows to span||"1"||1 - Number of Rows, or '*' for all remaining||#3|
|hAlign||Horizontal alignment of component in cell||"Left"||"Left", "Center" or "Middle", "Right", "Fill"|
|vAlign||Vertical alignment of component in cell||"Middle"||"Top", "Center" or "Middle", "Bottom", "Fill"|
|hGroup||Horizontal cell-group name||None||A label to link cells for common widths|
|vGroup||Vertical cell-group name||None||A label to link cells for common heights|
|Insets||Insets for cell||"3,3:3,3"||top,left:bottom,right (each can be "Default,Def, or Dft")||#4|
|#1 Insets for the outer edges of cells which are on the outer edges of the container are ignored.|
|#2 Both row and column may not be "Current" since only 1 component is allowed in each cell.|
|#3 The number of cells to span cannot extend beyond to the last cell in the row or column.|
|#4 Insets for the outer edges of cells which are on the outer edges of the container are ignored.|
The layout manager is constructed with row and column specifications, which may be specified as either two strings for the simple cases or, to allow greater control, two arrays of strings, one element for each row/column. Then, as each component is added to the layout, specific constraints for the cell are supplied. To make life simpler, the constructor may also be given a set of default constraints applied initially to each cell. Perhaps the best way to get a feel for it is to see some actual code, here is the code that defines the main window for TimeKeeper.
is defined by this code (the parts salient to layout, excluding component creation):
// COMPONENT STRIPS as=MatrixLayout.singleRowBar(5,2,projCode,MatrixLayout.NULL,bb1); ts=MatrixLayout.singleRowBar(5,5,curHours,makeLabel("(n.nn or HH:MM)"),makeLabel("Total :"),totalHours,MatrixLayout.NULL,reminderEnabled); sp=makeScrollPane("ActivityScrollPane",activity); // WINDOW PANEL cp=new EPanel(new MatrixLayout("Pref 100% Pref Pref","Pref 100%","hAlign=Right")); cp.setBorder(BorderFactory.createEmptyBorder(10,10,10,10)); cp.setName("TimeKeeper"); // WINDOW PANEL CONTENTS cp.add(makeLabel( "Project :"),"row=Next col=1" ); cp.add(as,"hAlign=Fill "); cp.add(makeLabel("Activity :"),"row=Next col=1 vAlign=Top"); cp.add(sp,"hAlign=Fill vAlign=Fill"); cp.add(makeLabel( "Hours :"),"row=Next col=1" ); cp.add(ts,"hAlign=Fill "); cp.add(bb2 ,"row=Next col=1 hSpan=*" );
Breaking it down…
The rows are all set to get the (largest) preferred size of all the components within them, except row 2, which is defined to consume 100% of the space remaining after fixed sizes have been allocated - row 2 contains the text entry box. The columns get their preferred size, except column 2 which expands and contracts like row 2. The default value for row is “Cur” and for col it’s “Next”, so we just need row and col specified for the first cell on each line and the other components are put into the next column on the same line. The default horizontal alignment is set to Right.
We give the entire panel a border to create space around the outside edge, between the window border and the panel contents. This is necessary because MatrixLayout ignores cell insets for its outer edges, which makes nesting panels within panels much easier (In AWT where we don’t have the convenience of borders, we would add extra rows and columns at the outside edges to create space.)
Next we populate the window’s cells with our components. Note the scroll pane which is placed in cell R2,C2 with horizontal and vertical alignments of Fill; it grows with its row/column in both dimensions - and since in turn R2,C2 grows in both dimensions with the size of the window, the text box therefore fills the available space as we resize the window. The nested activity-strip (as) and time-strip (ts) panels use a utility method to create a component bar with a little trick to push some components to the right of the window; one of the cells is specified to be the expanding cell but it is skipped over using the special NULL component. The expanding cell fills the extra space from sizing the window.
Lastly, the button bar in row 4 is put into cell 1, but it spans the entire row and is right aligned, so it shoots across all the way to the right edge of the panel. Note the use of “hSpan=*” to cause the component to span the remaining cells in that row.
How about that – resizeable windows are really very simple!!
Get The Source
The source compiles to Java 5, but it should be able to compile against Java 2 or later.