jakemiles.com
Hands Off My Grid Bag
An abused GridBagLayout will render havoc on the screen - components
don't show up, they don't scale correctly, or they morph and hop around at will.
The chaos disappears if you treat it like a FlowLayout with a few extra controls -
add your components, keep your hands off the dashboard, and GridBagLayout will do everything for you.
Once you understand how little you have to do, it's possible to refactor almost all of your grid bag
layout needs into a few convenience classes that yield clean, maintainable code.
Just like FlowLayout
Creating a grid bag is straightforward - you add components one after the other to
the panel. Each component gets its own cell in a grid that stretches and
rearranges itself to fit each component in with the rest. You have additional
control over each component's behavior via its GridBagConstraints object, but
there's no need to go crazy.
For example, if we just add three components to
a GridBagLayout one after the other, using the default constraint values,
we create a grid three columns wide and one row high, centered in the panel:
|   |
// create a panel using a GridBagLayout
JPanel panel = new JPanel (new GridBagLayout());
// three labels on the first row
panel.add (new JLabel ("One"));
panel.add (new JLabel ("Two"));
panel.add (new JLabel ("Threeeee"));
|
|
A little scrunched together, but the result is exactly that of FlowLayout - each label appears one after the
other left to right. We don't have to assign any GridBagConstraints objects
because GridBagLayout assigns the default for us, and for now the default
values are all we need.
Leave gridx and gridy alone.
The most important default value is GridBagConstraints.RELATIVE, the default value of gridx and gridy.
It tells GridBagLayout to put the component in the next available cell in the grid.
If we always add our components in order from left to right as we do with FlowLayout, we never need
to touch gridx and gridy.
GridBagConstraints.REMAINDER finishes the current row.
The default constraints are so useful that the only value we must change at some point is gridwidth.
Setting GridBagConstraints.gridwidth to GridBagConstraints.REMAINDER signals the end of the row to the GridBagLayout.
To do this we assign the "Threeeee" label a constraints object:
|   |
// three labels on the first row
panel.add (new JLabel ("One"));
panel.add (new JLabel ("Two"));
// ends first row
GridBagConstraints threeLabelGBC = new GridBagConstraints();
threeLabelGBC.gridwidth = GridBagConstraints.REMAINDER;
panel.add (new JLabel ("Threeeee"), threeLabelGBC);
// starts second row
panel.add (new JLabel ("Start of next row"));
|
|
The gridwidth of REMAINDER ends the first row, putting the next
component added, with its RELATIVE gridx and gridy (the default), at the start of the next row.
Rows and columns stretch to fit their largest components.
The "Start of next row" label upset the grid - it's wider than the "One" label
above it and forced the whole column to stretch with it.
Each column
takes on the greatest preferred width and each row takes on the greatest preferred
height of all its components.
We didn't have to set any preferred widths or heights ourselves because JLabels calculate
their preferred sizes automatically.
Gridwidth and gridheight control the alignment of cells.
If we now add a text field, specifying a character-width of 5, it will determine
the width of the second column, because the text field's preferred width is greater
than that of the "Two" label above it:
|   |
// add 5-character-wide text field to layout
JTextField textField = new JTextField (5);
panel.add (textField);
|
|
If instead we want to leave the "Two" label alone, allowing it to
define its column's width, we can specify that the text field's cell span the rest of the
row instead of just the one column:
|   |
// 5-character-wide text field finishes row
JTextField textField = new JTextField (5);
GridBagConstraints textFieldConstraints = new GridBagConstraints();
textFieldConstraints.gridwidth = GridBagConstraints.REMAINDER;
panel.add (textField, textFieldConstraints);
|
|
The second column no longer has to stretch to fit the text field because
the text field's gridwidth - the width of its cell - is now the remainder of the row,
in this case 2 columns. The "Two" and "Threeeee" labels are once again the widest components in their
individual columns, thereby defining their columns' widths.
The cell and the component are not the same thing.
The text field's cell is now wider than the text field itself, and by default the
text field floats in the center of it. We can instead specify where in the cell the text field
anchors itself, using GridBagConstraints.anchor:
|   |
// 5-character-wide text field finishes row, anchored on left
JTextField textField = new JTextField (5);
GridBagConstraints textFieldConstraints = new GridBagConstraints();
textFieldConstraints.gridwidth = GridBagConstraints.REMAINDER;
textFieldConstraints.anchor = GridBagConstraints.WEST;
panel.add (textField, textFieldConstraints);
|
|
With an anchor value of WEST, the text field will now remain stuck to the left edge of its cell.
The text field has also not changed size, even though we changed its cell's
gridwidth to REMAINDER, because the cell
and the component are not the same thing. GridBagConstraints.fill controls the
relationship between cell size and component size - how the text field fills
its cell:
|   |
// 5-character-wide text field finishes row, fills cell horizontally
JTextField textField = new JTextField (5);
GridBagConstraints textFieldConstraints = new GridBagConstraints();
textFieldConstraints.gridwidth = GridBagConstraints.REMAINDER;
textFieldConstraints.fill = GridBagConstraints.HORIZONTAL;
panel.add (textField, textFieldConstraints);
|
|
The text field now fills its cell horizontally, overriding its preferred size. There is no more
need for the anchor value in this case, because a fill of HORIZONTAL guarantees that the cell
will never contain extra horizontal space.
Weights
Once the grid is sized to fit all its components, the extra space in the panel
is distributed equally around the grid, squeezing the grid into the center
of the panel. You can distribute this extra space among the cells in the grid using
weights:
|   |
// text field finishes row, fills its cell, has weight
GridBagConstraints textFieldConstraints = new GridBagConstraints();
textFieldConstraints.gridwidth = GridBagConstraints.REMAINDER;
textFieldConstraints.fill = GridBagConstraints.HORIZONTAL;
textFieldConstraints.weightx = 1.0;
panel.add (textField, textFieldConstraints);
|
|
Because we've only assigned a weight to the text field, and to no other component, the text field's
cell gets all extra horizontal space in the panel.
The text field's weight has also increased the width of the last column in the grid - the column
containing the "Threeeee" label. When a multi-column component requires more space than the components above and
below it, the component's rightmost column takes on the extra width. In this case, the text field is
wider than the "Two" and "Threeeee" labels put together, so the "Threeeee" label's column stretches
to accomodate it.
Perhaps the text field doesn't need all that room. Instead of assigning all the weight to the
text field, we can distribute the extra space proportionally among multiple cells in the grid:
|   |
// start of next row
GridBagConstraints startOfNextRowGBC = new GridBagConstraints();
startOfNextRowGBC.weightx = 0.5;
panel.add (new JLabel ("Start of next row"), startOfNextRowGBC);
// text field finishes row, fills its cell, has weight
GridBagConstraints textFieldConstraints = new GridBagConstraints();
textFieldConstraints.gridwidth = GridBagConstraints.REMAINDER;
textFieldConstraints.fill = GridBagConstraints.HORIZONTAL;
textFieldConstraints.weightx = 0.5;
panel.add (textField, textFieldConstraints);
|
|
Now the "Start of next row" label and the text field share the extra horizontal space. Weights
can add up to whatever total you like, and they will distribute the space proportionally, but it's
easiest to visualize the result if you make sure they always add up to a number like 1.0 or 100, each
denoting a percentage of the extra space.
Note that the extra space is applied to the cells, not the components.
The text field only reflects the change because its fill value is HORIZONTAL.
As for weightY, sometimes your layout contains a two-dimensional component like
a JTextArea or a JTable that you can place on the last row and give all the weight. This
pushes the rest of the grid to the top:
|   |
// Comments label
panel.add (new JLabel ("Comments:"));
// comments text area gets all extra vertical space
GridBagConstraints textAreaGBC = new GridBagConstraints();
textAreaGBC.gridwidth = GridBagConstraints.REMAINDER;
textAreaGBC.fill = GridBagConstraints.BOTH;
textAreaGBC.weighty = 1.0;
panel.add (new JTextArea(), textAreaGBC);
|
|
If you don't have a two-dimensional component to take all the weight, but you
want to anchor the whole grid to the top of the panel, you can replace
the text area in the example above with an empty label:
|   |
GridBagConstraints dummyLabelGBC = new GridBagConstraints();
dummyLabelGBC.gridwidth = GridBagConstraints.REMAINDER;
dummyLabelGBC.weighty = 1.0;
panel.add (new JLabel(""), dummyLabelGBC);
|
|
The empty label's cell now gets all the extra space, pushing everything else into the
ceiling. Whether you want to do this is a matter of preference.
Insets
Insets define the inner border of a cell - the padding between the component and the edge of
its cell. Cells' edges are always immediately adjacent with
no padding between them, so use insets to keep components a comfortable distance from each other:
|   |
// three labels
GridBagConstraints oneLabelGBC = new GridBagConstraints();
oneLabelGBC.insets = new Insets (2, 2, 2, 2);
panel.add (new JLabel ("One"), oneLabelGBC);
GridBagConstraints twoLabelGBC = new GridBagConstraints();
twoLabelGBC.insets = new Insets (2, 2, 2, 2);
panel.add (new JLabel ("Two"), twoLabelGBC);
GridBagConstraints threeLabelGBC = new GridBagConstraints();
threeLabelGBC.gridwidth = GridBagConstraints.REMAINDER;
threeLabelGBC.insets = new Insets (2, 2, 2, 2);
panel.add (new JLabel ("Threeeee"), threeLabelGBC);
|
|
In an input form that pairs labels to fields, generally you can give all the labels insets of (5, 5, 5, 2) -
padding five pixels on all sides except the right, which pads two pixels, and you can give all the components
insets of (5, 2, 5, 5) - padding five pixels on all sides except the left, which pads two pixels. This
naturally separates the form into label-field pairs.
Do not use insets as a brute force fix.
The instinct to mash a component into place using insets is tempting when you're new
to grid bag and things aren't going as expected. Fight this urge and solve the real problem.
Do not use insets to push a component to one edge of its cell - use an anchor value.
Do not use insets to push the grid to one side or to the top of the panel - use a weighted
empty label as in the example above. Using insets in the place of other constraints fields
essentially assigns a static position to the component, which will break when the panel is
resized or modified later by unsuspecting developers.
Ipad values
ipadx and ipady exist to confuse you. Leave them alone.
The mysterious collapsing text field
Sometimes you'll lay out a grid bag, doing everything right, to discover that your
text fields have all deflated:
|   |
JPanel panel = new JPanel (new GridBagLayout ());
JTextField textField = new JTextField (25);
panel.add (textField);
|
|
The problem is that the frame isn't wide enough to fit a 25-character text field.
When the parent container is too small to fit a component according to its
preferred size, the layout uses its
minimum size to determine its space requirements. This results in an abrupt
collapsing of text fields as you resize the frame with the mouse - as soon as
you resize it smaller than the preferred size point, it flips to minimum size mode and everything collapses.
Setting the text field's minimum size to something reasonable accounts for this case:
|   |
JPanel panel = new JPanel (new GridBagLayout ());
JTextField textField = new JTextField (25);
textField.setMinimumSize (new Dimension (50, textField.getMinimumSize().height));
panel.add (textField);
|
|
The text field will now scale with the frame to sizes smaller than its preferred size.
The simplest solution to this problem:
set a minimum size on the frame so it never gets too small to fit everything according to
its preferred size. If you want to allow resizing of the frame to sizes smaller that this,
you can also use a subclass of JTextField that sets its default minimum
size to 50 pixels or so. The most involved solution explicitly sets appropriate minimum sizes on all
the text fields, but the less code the better.
The less code the better
A lot of grid bag examples create one GridBagConstraints object and then
reuse it, changing this or that value for each component along the way.
The resulting code is brittle - it's difficult
to see what the constraints actually are for a particular component because
they are the result of many changes further up in the method. A change at the top
can also break something further down.
It's better style to create a separate GridBagConstraints object for each component. This seems
like more work, but luckily if you've seen one combo box you've seen them all -
the constraints values used for each kind of component don't vary much from instance
to instance. We can refactor these constraints combinations into subclasses of
GridBagConstraints, eliminating most of your grid bag code:
- GBConstraints
-
Extends GridBagConstraints, and contains a cascade of constructors, each
taking more of the constraints values as parameters. Uses the default values
of gridx and gridy (RELATIVE), and sets insets to (2, 2, 2, 2).
- LabelConstraints
-
Extends GBConstraints, anchor = NORTHEAST. This
allows a natural pairing of labels and fields - labels anchor themselves
to the right of their cells while fields anchor themselves to the left.
- FieldConstraints
-
Extends GBConstraints, anchor = NORTHWEST.
fill defaults to HORIZONTAL (the usual value for a text field), but if you
specify a gridheight in the constructor, fill is set to BOTH.
- RemainderConstraints
-
Extends InputFieldConstraints, sets gridwidth = REMAINDER.
- RemainderConstraintsNoFill
-
Same as RemainderConstraints, but sets fill to NONE - useful for placing
a JComboBox or JCheckbox at the end of a row, because these components
don't need to fill their cells.
- MiddleFieldConstraints
-
Extends InputFieldConstraints. Useful for placing an input component somewhere other than the end of
the row. fill defaults to HORIZONTAL (for text fields), but if
a gridheight is specified in the constructor, fill is set to BOTH.
- MiddleFieldConstraintsNoFill
-
Extends InputFieldConstraints. Useful for placing an input component that doesn't
need to fill its cell, like a JComboBox or JCheckBox, somewhere other than the end of the row.
For example:
JPanel panel = new JPanel (new GridBagLayout ());
panel.add (new JLabel ("First name:"), new LabelConstraints());
panel.add (new JTextField (15), new MiddleFieldConstraints());
panel.add (new JLabel ("Last name:"), new LabelConstraints());
panel.add (new JTextField (15), new RemainderConstraints());
panel.add (new JLabel ("Address:"), new LabelConstraints());
panel.add (new JTextField (30), new RemainderConstraints());
panel.add (new JLabel ("City:"), new LabelConstraints());
panel.add (new JTextField (15), new MiddleFieldConstraints());
panel.add (new JLabel ("State:"), new LabelConstraints());
panel.add (new JComboBox (), new RemainderConstraintsNoFill());
panel.add (new JLabel ("Zip code:"), new LabelConstraints());
panel.add (new JTextField (10), new MiddleFieldConstraints());
|
Getting it to show up on screen
One problem with any layout is getting it to show up on the screen in the first place
and getting it to scale with the JFrame or JInternalFrame containing it. The
following two lines will ensure that your panel forever fills its frame no matter how you
stretch it:
frame.getContentPane().setLayout (new BorderLayout ());
frame.getContentPane().add (panel, BorderLayout.CENTER);
|
Be sure to call these methods on the content pane, not on the frame itself.
Swing's frames use a content pane to hold all your components, keeping them
separate from the more frame-related components like the title bar.