Layout in Java
Two factors conspired to land the client-side Java development community
with a very difficult problem. The first was Java's 'write once, run
anywhere' premise, the second was the commitment that the Java
platform would use the underlying platform's widgets instead of
defining its own. The advantage of the second commitment lay making it possible for Java programs to look like native
applications. Its disadvantage was that it prevented Java from allowing
layout tools to define layouts using fixed, pixel-based, distances as earlier
toolkits had done.
The approach that Java's designers took to provide the abstractions that are
necessary to avoid talking pixels was based on Donald
Knuth's TeX's document formatting system. The basic idea is to allow
each component to specify the size it would like to be and a cost for every possible deviation from the ideal.
Preferred Size
In Java, a layout manager finds out how much space a component requires
by calling the component's
getPreferredSize() method. The methods returns a Dimension object that defines a width and height.
This brings us briskly to the first of the fundamental problems with Java's approach.
The problem with setting preferred sizes
When using, for example, a JTextField
in a form, a designer typically
needs to specify the width of the space that should be made available
for user input, but does not want to specify its height - because it
will invariably change when the GUI is run on a different platform.
In this case, the preferred size of a component does not provide the
independent
control of height and width that the designer needs. As soon as cases
like the one above are encountered, efforts to the control the layout
with preferred sizes must be abandoned and supplanted with a more
general mechanism
that provides independent control along each axis. It is simpler to
continue our
analysis of the fundamentals of layout by assuming that this more
general
mechanism exists and that we will always use it instead of setting preferred
sizes.
Minimum and Maximum Sizes
As well as preferred size, TeX put forward a notion of
stretchiness in the 'boxes and glue' elements that made up the
document. In Java, components specify their willingness to shrink and
stretch by specifying minimum and maximum sizes respectively. Although
the maximum size was introduced half-way through the evolutionary
flourish of layout manager development (with the introduction of the LayoutManager2
interface), two layout managers treat maximum size in a way that is
both consistent with TeX and easy to define. Those layout managers are
BoxLayout and SpringLayout and they both use the difference between a
component's preferred size and its minimum size to define how willing
it is to shrink and the difference between a component's maximum size and
its preferred size to define its willingness to stretch.
With this information then, we imagine layout managers can share out
extra space, in a simply defined way that follows TeX. The details run
roughly as follows:
If we have a two components in a row and they specify minimum,
preferred and maximum widths of say: [0, 100, 200], [0, 100, 300] we
would decide that an overall width of 350 pixels is most fairly
distributed by allocating a width of 150 to the first component and a
width of 200 to the second. We do this because these sizes put both
components exactly
half-way between their preferred and maximum values.
Algorithmically, we distribute space evenly by summing the minimum values,
the preferred values and the maximum values to give three numbers that
define the minimum, preferred and maximum values of the aggregate. If
the desired size is lower than the preferred size of the aggregate we
will shrink each of the components, otherwise we will stretch them. It
is a simple mathematical fact that, if we set each of the components a
fixed percentage (50% in the example above) between their preferred and
maximum values, the size of the aggregate will be the same fixed
percentage between the preferred and maximum values of the aggregate.
This is true for all percentages, even negative ones, and so provides
an elegant way to share out a given 'delta' between an arbitrary set of
components.
Problems with setting minimum and maximum sizes
General problems
- If
a component's preferred size is computed, we cannot set minimum and
maximum values without fixing them to values that may not be
appropriate to the different values a component's preferred size may take on different platforms and locales.
- Size characteristics
belong to components and so cannot be used to control the space between
components. Layout managers address this issue in different ways.
SpringLayout and
GroupLayout split a component's size requirements into a pair of springs that can then be used to control space as well as component sizes. BoxLayout uses the
component model for space by creating a transparent Filler widget to characterize it.
- Lack of independent control over width and height - as discussed in the section on preferred sizes.
Problems with setting minimum sizes
In practice we never want a component's size to shrink below zero. So,
if we expect the proportional scheme above to be in force, a component
that wants to define a cut-off point below which it should not be
shrunk needs to have its minimum size equal to its
preferred size. Most of the simple awt/swing components, like JButton
and JLabel, do this. With the minimum size essentially removed as a
means of control, the maximum size of a component is left to control
stretchiness.
Problems with setting maximum sizes
- Swing components tend to adopt different conventions for maximum
sizes. JLabels and JButtons, for example, define a maximum size that is
equal to the preferred size. JTextField's use the value
Integer.MAX_VALUE where JSlider's use the value Short.MAX_VALUE.
- Very large values such as Integer.MAX_VALUE cause the
proportional scheme to degenerate into one that shares space out
equally rather than proportionally and require care to avoid overflow
in layout calculations.
- Maximum values were not part of the original LayoutManager
interface and are ignored by older layout managers, like GridBagLayout.
All in all, the problems listed above prevent tools from controlling layout
management effectively using minimum and maximum sizes.
Summary: what to do with minimum, preferred and maximum sizes
A component's preferred size should be treated as a read-only property. Minimum and maximum sizes should be ignored.
Existing Layout Managers
Java 6.0 offers the following public layout managers for general use:
- null (absolute positioning)
- BorderLayout
- CardLayout
- FlowLayout
- GridLayout
- GridBagLayout
- BoxLayout
- SpringLayout
- GroupLayout
Of these, only those from GridBagLayout down are viable options to
support the assembly of cross-platform forms of reasonable complexity.
Here is a, far from exhaustive, list of popular layout managers written
by third parties:
- TableLayout
- FormLayout
- MiGLayout
We will examine each of these in turn making use of the following example layouts.
Example: Form1
The first is a very simple form, with two JLabels and two JTextFields in the conventional layout used in forms.
Of this form we expect the following:
- Text in rows shall be aligned on baseline for all fonts on all platforms.
- The first column shall be right-aligned and take the width of the longer of the two labels.
Example: Form2
The next form set out here is a set of three JLabel components each with bounds around its text.
| one small |
step for man |
|
|
stumble a bit |
|
|
then take one |
giant leap for mankind |
- Rows 1 and 2 shall be right aligned
- Rows 2 and 3 shall be left aligned
- The layout manager shall observe 1 and 2 and also handle the
possibility of row 2 expanding, on internationalization, to be wider
than either of the others.
In all cases the layout manager must compute the correct container
bounds and component locations so that components fit the bounds of the
container and are aligned as above.
None of the layout managers listed above can support this operation!
What is worse is that the majority of the layout mangers allow it to be
defined and fail unpredictably when it is.
GridBagLayout
GridBagLayout has a number of open bugs that have been in
the bug database for over 10 years. That said, base-line support was added for SDK 6.0.
As of SDK 6.0, GridBagLayout can easily support Form1. Form2 tickles
several bugs in the algorithm GridBagLayout uses to deduce the size of
its columns. Its algorithm is dependent on the order that the
components are placed in the container and no order produces the
correct results.
The biggest downsides to GridBagLayout are the steep
learning curve required to configure the layout manager to achieve a
given result - and the lack of a good tool free-form tool to hide this
complexity from
designers.
GridBagConstraints: weight
GridBagLayout's policy seems to be to take the max of all the weights
in each row/columns. For mechanical configuration it is therefore
simpler to specify the weight once, in the appropriate row/column
rather than once for each component. FormLayout improves the
GridBagLayout API by dropping the weight property from its constraints
objects.
GridBagConstraints: insets
Normally, components are aligned by their edges so insets, unless they
are equal in the appropriate dimension will prevent correct alignment
by the layout manager. It is therefore normally preferable to
characterize space by creating empty rows/columns rather than using
insets. Unfortunately, specifying the width of empty rows/columns in
GridBagLayout requires assigning values to internal arrays it exposes
public fields it exposes - which is makes for unattractive setup
code. FormLayout again improves the API by providing a more
natural API for setting up rows/columns that are used for spacing.
BoxLayout
BoxLayout cannot, on its own, support Form1 as it only supports a
single row or column of components. By nesting components it is
possible to use three BoxLayouts to support a vertical arrangement of
horizontal rows. BoxLayout does not have baseline support however, so
different fonts will
not be aligned correctly if the fonts differ. More importantly, it is
not possible to left-align the two labels in the first column as the
relevant information was buried when the components were made into
rows.
SpringLayout
SpringLayout also had baseline support added to SDK 6.0. It is possible
to configure springs so as to support Form1 but,
like GridBagLayout, creating the configuration is too fiddly to do
by hand and few tools allow the designer to work without understanding
the underlying complexity of the layout manager (JFormDesigner and
GUIDE are possible exceptions).
SpringLayout also has a serious fundamental flaw that its author (who also
wrote this article) realized only many years after adding the layout
manager to the SDK. SpringLayout uses two operators sum and maxto
group springs together in series and parallel respectively.
SpringLayout cannot correctly layout Form2 because SpringLayout
only works on a set of relationships that form a series-parallel
graph. Form2 is not a series-parallel graph because there is
no way to break the three components into a smaller set that is in
parallel or series.
By hand it would be possible to use Spring's minus
operator to ensure that the container would be of a size that is the sum
of the widths of the first and last rows, minus the width of the second
- but this would not correctly handle the case when the middle row
became wider than the other rows. In this case SpringLayout would allow
components to escape the borders of the container and so could not be considered a correct solution.
GroupLayout
GroupLayout was introduced to the SDK after SpringLayout and is
'informally derived' from it (the code for javax.swing.Spring was
cut-and-pasted into the GroupLayout class as the private superclass
of GroupLayout's Group class).
Because of this GroupLayout has the same limitations as SpringLayout:
relationships between components in a GroupLayout must form a series-parallel
graph.
All configurations that have less than three components are
series-parallel but Example 2 is just one of a set of configurations of
three components that are not series-parallel. The chance of a random
configuration of components forming a series-parallel graph goes down
very quickly as the number of components in the configuration
increases. In practice, during the construction of a non-trvial form in
a free-form tool it is very uncommon indeed for all the intermediate
steps to have the series-parallel property.
It also appears to very difficult to effect graceful degradation
when
problematic graphs are encountered during free-form layout. Matisse is
prone to scattering components around the container randomly when this
happens and this presents a significant adoption barrier.
Although some of these problems are rooted in bugs in the
implementations of Matisse and GroupLayout that may one day be fixed,
the anomalies associated with the series-parallel restriction are
fundamental to the approach and provably impossible to overcome.
Mitigation is possible either by discarding the parts of the topology
that cause the problems or by nesting
components into smaller groups in which the problem is less likely to
occur, but both tactics come with their own set of drawbacks.
FormLayout
In
both its API and its operation, FormLayoutis similar
to GridBagLayout and appears to
offer the important features of tabular layout in a well crafted and
less buggy implementation than GridBagLayout. See the above section on
GridBagLayout above for some comments on the difference between
FormLayout and GridBagLayout. One important caveat is
that it predates the introduction of
baseline support in the SDK and does not support baseline
alignment in rows. Hopefully this will be added in a future release.
FormLayout aligns the components correctly in Example Form 2 but
mis-calculates the size of the container and positions components
outside its bounds.
General Comments about the future of layout on the Java platform
Users of Apple's Interface Builder have sworn by free-form layout since
the late eighties. In the end it appears that it was the production of
the Matisse GUI builder that did far more than any layout manager to
sell the idea of free-form layout to the Java community. While both
SpringLayout
and GroupLayout were laudably motivated on the premise that the
designer
should be free to move components around unconstrained by the artifacts
of a layout manager, this truism need not, with hindsight, have
precluded
a grid-based approach being used in the layout manager itself. The deep
topological restrictions associated with SpringLayout
and GroupLayout mean some new tactic is needed as their inability
to scale to real user interfaces appears to preclude their adoption at
the kind of levels enjoyed by tools such as Interface Builder and
Visual Studio.
It now seems plausible that the ideal solution for Java might well come
from its original tabular camp, with new tools that bridge the gap
between what a layout manager needs to store and what a designer needs
to see.