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.
jake@jakemiles.com jakemiles.com