ZoneLayout Manual
Contents
Introduction
Arranging items in a window has traditionally been a tedious and cumbersome task. Layout APIs have given programmers precise control over layouts but leave them with fragile and confusing code. Visual design tools give the programmer an approximate view of the end result but tie the programmer to the tool and suffer from code mangling, binary formats, forced context switching, and static results.
ZoneLayout provides the programmer with the best of both worlds: a simple yet powerful API and a pseudo-visual representation of the final result all without leaving the source code.
This guide is an introduction to the ZoneLayout system. Currently the only implementation is for Java Swing, but implementations for SWT, Windows Forms, wxWidgets, and HTML/CSS may be forthcoming based on demand. If you're a Swing programmer, read on to learn how ZoneLayout can greatly simplify the layout process for you. If you work with another GUI toolkit, you can still learn about the general principles of the ZoneLayout system.
The Basics
ZoneLayout is, at its core, still a grid-based layout like GridBagLayout, JGoodies FormLayout, etc. The display area is divided into a virtual grid and individual cells are expanded and contracted based on the space requirements of the individual components and parameters in the layout definition. The layout engine in ZoneLayout is a custom implementation, but if you've worked with any of the other grid-based systems before, the behavior will be very familiar.
What makes ZoneLayout unique is the API and the manner in which a programmer defines a layout, specifically a 2-dimensional regular-expression-like language that utilizes the virtual grid that is inherent to fixed-width fonts used by most editors. A programmer uses this language to define zones, properties of zones, and then binds components into the zones. All this jargon makes it sound complicated, but it's actually incredibly simple and intuitive. Here's a simple example.
A Simple Example
This results in the following (assuming there's some JFrame bootstrapping code):
There are two important parts to the code: the layout definition and the binding. The layout definition
lies within the two layout.addRow(...) method calls and looks like:
ZoneLayout will create a zone the first time an alphabetical character
is encountered and extend the zone when it sees that character again (It scans from left to right, top to bottom). The
two characters act as the upper left and lower right corners of the zone.
So this layout definition creates two zones 'a' (colored green) and 'b' (colored yellow):
Any other characters that lie within a zone's boundary modify the zone (except the '.' character
which is just a spacer character to maintain the grid). In the above definition:
- The '
>' character tells ZoneLayout to align anything in zone 'a' to the right. - The '
-' character tells ZoneLayout to horizontally fill zone 'b' with whatever is inside of it. - The '
~' character tells ZoneLayout to expand zone 'b' horizontally to fill any extra space.
Components are bound into zones 'a' and 'b' by passing in the zone name as the constraint
in the basePanel.add(...) method calls. So in the simple example, a JLabel is bound to zone
'a' and a JTextField to zone 'b'.
When the ZoneLayout engine runs during our Simple Example, it knows that there is a JLabel in zone 'a'
that needs
to be aligned to the right, and a JTextField in zone 'b' that should horizontally fill its available
space and
expand into any more horizontal space that's available. So if we drag to make the window bigger, the JTextField
gets bigger:
Note that the available vertical space is not filled. That's because we didn't tell either zone 'a' or
'b' to expand into the available vertical space.
You should now have a general idea of how the ZoneLayout system works. You can learn a lot more about the ZoneLayout system by continuing on. If you're familiar with the other grid-based layout systems mentioned earlier, then you can probably just get away with perusing the API documentation here and jumping right in.
The API
There are essentially two parts to the ZoneLayout API: the usual java API and then the custom language used to define layouts.
Typically when using ZoneLayout, you should only have to do two things through the java API: (1) instantiate an instance
of the layout through ZoneLayoutFactory.newZoneLayout() and (2) add rows to the layout through the
addRow(...) methods, as in our simple example from earlier:
There are, however, two instances in the current version where you need to use the java API as opposed to using the layout language: if you want to custom tailoring resizing weights ("give" and "take") associated with zones or specifying the margins for a zone.
The Layout Language
Almost all code editors use a fixed-width font so all of the characters in the editor are on a virtual grid. Hence, we can model windows using rectangular groupings of characters. In the ZoneLayout system, you use some characters to define zones in your model and then other characters to modify the behavior of those zones.
Zones
You create a zone with an alphabetical character (as defined by java.lang.Character.isLetter()). So, the simplest layout can be specified as:
This creates a layout with a single zone 'w'. Note that a zone object is
created during a call to addRow(...) so you can modify the object right afterwards using
layout.getZone("w") if you want to. But be careful when doing so.
The layout code is not compiled until a component is added to the panel or you explicitly call
layout.compile(). So if you modify the same
property of a zone in the layout language and through the java API, the layout language modifier will
override the setting you made through the java API when layout.compile() is called.
You can extend a zone by using the spacer character '.' and reusing the same
character you used to create the zone initially to mark the lower right corner. Always define zones starting with
the top left corner. So, if you want to make
zone 'w' "bigger" (bigger is relative, see the section on layout optimization for
details), you could just do:
Zone 'w' now stretches across 5 characters in width and 2 characters in height. If you want to create other zones, you just add other characters, like so:
The layout now represents the beginnings of a very simple browser (back button, forward button, address bar, web page, status bar). (The zones are colored in the figure to show you where they apply.)
Modifiers
You can modify the behavior of a zone by placing a modifier character within a zone's
boundaries. You can use more than one modifier. Continuing with
the example from the last section, zone 'w' is going to contain a web page viewer which needs to take
up all of the available space. So, zone 'w' needs to fill in both directions, hence the use of the
'+' modifier.
All of the currently available modifiers are listed in the table below. Note that each
modifier provides some visual indication of its behavior so they are easy to remember ('>' means
align right, '!'
means expand vertically, etc.). The image on the right side shows how layout behavior is
changed by the modifier. The white square is the display area, the green square is the zone, and the red square is
the component in the zone. A component is centered in a zone by default.
| Modifier Character | Behavior | |
|---|---|---|
Continuing with our example:
- The address bar needs to expand horizontally into available space and fill that space.
- The web page also needs to expand into available space both horizontally and vertically in addition to filling it.
- The status bar should align to the left.
Preset Components
Layouts often involve extensive use of spacer components to pleasantly separate components on the display.
Instead of forcing you to create spacer zones, ZoneLayout has a few shortcuts for a number of useful spacer
components. These preset components are accessed through the numerical digits 0-9 and the
'~', '!', and '*' characters in a layout.
By default, ZoneLayout has the following component presets:
| Preset Character | Behavior | |
|---|---|---|
To use a preset, just use its corresponding character outside of a zone. Here's an example of a small dialog:
The preset '2' is converted into a 6 pixel invisible horizontal boundary in between zones 'a'
and 'b', and zones 'c' and 'd'. The preset '6' is converted
into a 6 pixel invisible vertical boundary between the first and third rows. The preset '7' is
converted into a 12 pixel invisible boundary between the third and last rows. As a simple
reminder to where the vertical spacer's begin in the presets, just remember "High 5". Here's what the
window looks like:
The presets are configurable. You can specify a preset by using the
setComponentPreset(...) method on the layout and passing in
a preset id from 0 - 9 and a Preset object created using the static factory methods
on the Preset class (createSpacer(...), createFlexibleSpacer, and
createExpander(...)).
The Java Look and Feel Design Guidelines generally call for 6 pixel gaps between related components (except in
toolbars where the gap should be 3 pixels between related buttons) and gaps that are multiples of 6 pixels
everywhere else. So presets 2 and 6 should be the presets you use the most.
Binding
Binding is simply telling ZoneLayout what component you want in a particular zone. This is accomplished by passing in the name of zone as the constraint when you add that component to a panel.
The necessary components for the final simple browser layout from earlier would be bound like so:
This results in the following window (assuming simple JFrame bootstrapping):
And this is what it looks like after resizing:
Templates
Often when creating layouts, programmers find themselves having to duplicate the same code for a number of rows and columns or having to place layout code within loops. This spreads the layout code out even more and makes it more difficult to understand exactly what is going on.
ZoneLayout enables the programmer to avoid this problem by providing a simple template methodology. Within a layout definition, you can define rows as being part of a template and then insert those rows dynamically. (In the current version this functionality is only available for rows, not columns.)
To create a template, you just provide a second argument to the addRow(...) method call that specifies
the name of the template. If you want to add more rows to the same template, then use the same name again.
Here is an example layout that you might use when building an editor for java.util.Map values:
The first two rows are part of the basic layout and then the last two are contained in the 'mapEntry'
template. Rows contained in a template are removed from the layout prior to the layout's
use. So the base layout actually looks like:
Before binding a component to a zone in a template, you'll need to insert the template first. Here's the code that uses this layout:
Note the call to layout.insertTemplate("mapEntry") prior to binding components to zones 'k'
and 'v'. Also note that you can just refer to the zones 'k' and 'v' without
specifying that you are referring to the zones in the template you just inserted. Every time
you insert a template, the zone names are reset to point to the latest instance of the template. This code
will result in the following layout since there are 3 entries in the Map:
And the final window will look like this:
You can have multiple templates within a layout, but be careful when putting two or more templates back-to-back. When back-to-back templates are removed from a layout, they are assigned the same insertion point by ZoneLayout and, hence, their initial ordering is not maintained.
Zones must be wholly contained within templates or span them. You cannot have a zone start outside of template and finish inside one or vice versa.
Optimization
You may have noticed in the simple browser example layout from the Layout Language Section that there was an extra row in the layout that wasn't needed. That row is bolded here:
That row could have been deleted and zone 'w' placed all on one row like so:
Is the second one better? Trivially. As was mentioned previously, ZoneLayout is a grid-based layout so you may be wondering if deleting one row from the layout makes it more efficient. It doesn't really matter. Internally, ZoneLayout optimizes the layouts you create and pans down the layout to the minimum number of rows and columns necessary to achieve the same result. So technically, those two layouts are equivalent. They both result in the same 3 column and 3 row layout internally:
So when you're creating your layouts, don't be concerned with the size of the layout. Yes, optimizing a larger layout takes additional time, but it's trivial. Your time and the clarity of the layout are much more valuable.
The Engine
The engine positions and sizes the bound components in the display space based on the definitions provided by the layout. Although the internal operations of the engine aren't particularly pertinent to using ZoneLayout, it's important to understand the sizing behavior of the engine and how it affects your layouts.
Engine Basics
All calculations within the engine start with the preferred size of the layout and then proceed from there. If there's more space available than needed to fit the preferred size requirements, then the engine allocates additional space based on the "take" of the zones. If there's less space than needed, then the engine works downwards from the preferred size to the minimum size based on the "give" of the zones.
Although ZoneLayout is a grid-based layout system, internally the engine operates on rows and columns. So after your layout is optimized, the engine then breaks it down into rows and columns and sizes the individual rows and columns based on the components contained within.
Preferred Size
All calculations start from the preferred size of the layout. The preferred size is calculated in two passes. In the first pass, only zones that are wholly contained within one row or column are considered. In the second pass, zones that span more than one row or column are considered.
The first pass is used to initially size rows and columns based on components that are wholly contained within each row and column. The second pass then enlarges rows and columns to make sure that zones that span multiple rows or columns have their requested preferred size. This ensures that all components in the layout will have their preferred size.
Note that during the second pass, when choosing which columns or rows to enlarge for a zone that needs more space, ZoneLayout takes into account the "take" of the rows or columns. If it can't find any column with a "take" greater than zero, it will then evenly distribute the needed space among all of the rows or columns that the zone spans.
Resizing: Give and Take
Instead of one monolithic resize "weight" like other layout managers, ZoneLayout allows you to set two "weights" for zones: "give" and "take". The "give" of a zone is taken into account when the space available for a layout is less than the preferred space. The "take" of a zone is taken into account when the space available is greater than the preferred space of the layout. Think of them as greediness factors. When there's more space available, how much will a zone "take"? When there's less space available, how much is a zone willing to "give"?
Both of these values are int's and they are relative to the total of all
the "weights" within the layout. Say there's a zone with a "take" of 7 and another with a "take" of 3,
giving a total "take" of 10 in the layout. That means that one zone will always get 7/10's of the available extra
pixels and another one with always get 3/10's.
By default, the "take" of a zone is zero and the "give" is equal to its size after optimization. So, the default behavior of a zone is to not expand into extra available space, but shrink to the minimum size if necessary. For example, looking at the optimized structure of the simple browser layout from earlier in the manual:
In this layout, the default "takes" of all of the zones would be 0 in both directions; zones 'b',
'f', and 'a' would have default "gives" of 1 in both directions, and zones 'w'
and 's' would have default "gives" of 1 on the vertical axis, and 3 on the horizontal
axis like so:
"Weights" of zones that span multiple rows or columns are handled specially, similarly to how they are handled when calculating the preferred size of a layout. ZoneLayout will ensure that the "take" or "give" of a zone that spans multiple rows or columns is met (i.e. total "take" >= required or total "give" <= required). If it is not, then ZoneLayout will proportionally increase or decrease the "weight" in order to meet the needs of the spanning zone.
For example, take the full simple web browser layout from the API section of the manual:
You'll notice that zone 'w' spans the entire width of the layout and has it's horizontal "take" set
to 1 by the use of the '*' modifier. ZoneLayout recognizes that zone 'a' already has
a horizontal "take" of 1 and doesn't need to do anything to meet zone 'w's requirements.
For arguments sake, what happens if the "takes" of zones 'f',
'a', and 'w' are explicity set to be 1, 4, and 10 respectively?
Notice that the "takes" of columns are proportionally increased from 0, 1, and 4 to 0, 2, and 8 in order to meet
the requirement of 10 made by the spanning zone 'w'.
It's rare that you'll have to worry about this internal behavior. Specifying "weights" using the '*',
'~', and '!' modifiers should suffice for the majority of layouts.