GridBagLayout
GridBagLayout
is a very flexible layout manager that allows you to position components relative to one another using constraints. With GridBagLayout
(and a fair amount of effort) you can create almost any imaginable layout. Components are arranged at "logical" coordinates on a abstract grid. We'll call them "logical" coordinates because they really designate positions in the space of rows and columns formed by the set of components. Rows and columns of the grid stretch to different sizes, based on the sizes and constraints of the components they hold.
A row or column in a GridBagLayout
expands to accommodate the dimensions and constraints of the largest component in its ranks. Individual components may span more than one row or column. Components that aren't as large as their grid cell can be "anchored" within their "cell." They can also be set to fill or expand their area in either dimension. Extra area in the grid rows and columns can be parceled out according to the weight constraints of the components. Therefore, you can control how various components will grow and stretch when a window is resized.
GridBagLayout
is much easier to use in a graphical, WYSIWYG GUI builder environment. That's because working with GridBag is kind of like messing with the "rabbit ears" antennae on your television. It's not particularly difficult to get the results that you want through trial and error, but writing out hard and fast rules for how to go about it is difficult. In short, GridBagLayout
is complex and has some quirks. It is also simply a bit ugly both in model and implementation. Remember that you can do a lot with nested panels and by composing simpler layout managers within one another. If you look back through this chapter, you'll see many examples of composite layouts; it's up to you to determine how far you should go before making the break from simpler layout managers to a more complex "do it all in one" layout manager like GridBagLayout
.
GridBagConstraints
Having said that GridBagLayout
is complex and a bit ugly, I'm going to contradict myself and say that it's surprisingly simple. There is only one constructor with no arguments (GridBagLayout()
), and there aren't a lot of fancy methods to control how the display works.
The appearance of a grid bag layout is controlled by sets of GridBagConstraints
, and that's where things get hairy. Each component managed by a GridBagLayout
is associated with a GridBagConstraints
object. GridBagConstraints
holds the following variables, which we'll describe in detail shortly:
- int gridx, gridy
- Controls the position of the component on the layout's grid.
- int weightx, weighty
- Controls how additional space in the row or column is allotted to the component.
- int fill
- Controls whether the component expands to fill the space allocated to it.
- int gridheight, gridwidth
- Controls the number of rows or columns the component occupies.
- int anchor
- Controls the position of the component if there is extra room within the space allocated for it.
- int ipadx, ipady
- Controls padding between the component and the borders of it's area.
- Insets insets
- Controls padding between the component and neighboring components.
To make a set of constraints for a component or components, you simply create a new instance of GridBagConstraints
and set these public variables to the appropriate values. There are no pretty constructors, and not much else to the class at all.
The easiest way to associate a set of constraints with a component is to use the version of add()
that takes a layout object as an argument, in addition to the component itself. This puts the component in the container and associates your GridBagConstraints
object with it.
Component component = new Label("constrain me, please..."); GridBagConstraints constraints = new GridBagConstraints; constraints.gridx = x; constraints.gridy = y; ... add( component, constraints );
You can also add a component to a GridBagLayout
using the single argument add()
method, and then later calling the layout's setConstraints()
method directly, to pass it the GridBagConstraints
object for that component.
add( component ); ... myGridBagLayout.setConstraints( component, constraints );
In either case, the set of constraints is copied when it is applied to the component. Therefore, you're free to create a single set of GridBagConstraints
, modify it as needed, and apply it as needed to different objects. You might find it helpful to create a helper method that sets the constraints appropriately, then adds the method with its constraints to the layout. That's the approach we'll take in our examples; our helper method is called addGB()
, and it takes a component plus a pair of coordinates as arguments. These coordinates become the gridx
and gridy
values for the constraints. We could expand upon this later and overload addGB()
to take more parameters for other constraints that we often change from component to component.
Grid Coordinates
One of the biggest surprises in the GridBagLayout
is that there's no way to specify the size of the grid. There doesn't have to be. The grid size is determined implicitly by the constraints of all the objects; the layout manager picks dimensions large enough so that everything fits. Thus, if you put one component in a layout and set its gridx
and gridy
constraints to 25, the layout manager creates a 25 x 25 grid, with rows and columns both numbered from 0 to 24. If you add a second component with a gridx
of 30 and a gridy
of 13, the grid's dimensions change to 30 x 25. You don't have to worry about setting up an appropriate number of rows and columns. The layout manager does it automatically, as you add components.
With this knowledge, we're ready to create some simple displays. We'll start by arranging a group of components in a cross shape. We maintain explicit x
and y
local variables, setting them as we add the components to our grid. This is partly for clarity, but it can be a handy technique when you want to add a number of components in a row or column. You can simply increment gridx
or gridy
before adding each component. This is a simple and problem-free way to achieve relative placement. (Later, we'll describe GridBagConstraints
's RELATIVE constant, which does relative placement automatically.) Here's our first layout:
Figure 12.6: "A Simple Layout"
import java.awt.*; public class GridBag1 extends java.applet.Applet { GridBagConstraints constraints = new GridBagConstraints(); void addGB( Component component, int x, int y ) { constraints.gridx = x; constraints.gridy = y; add ( component, constraints ); } public void init() { setLayout( new GridBagLayout() ); int x, y; // for clarity addGB( new Button("North"), x=1,y=0 ); addGB( new Button("West"), x=0,y=1 ); addGB( new Button("Center"), x=1,y=1 ); addGB( new Button("East"), x=2,y=1 ); addGB( new Button("North"), x=1,y=2 ); } }
You probably noticed that the buttons in this example are "clumped" together in the center of their display area. Each button is displayed at its preferred size, without stretching the button to fill the available space. This is how the layout manager behaves when the "weight" constraints are left unset. We'll talk more about weights in the next two sections.
Fill
Now let's make the buttons expand to fill the entire applet. To do so, we must take two steps: we must set the fill
constraint for each button to the value BOTH
, and we must set the weightx
and weighty
values to the same nonzero value. Here's the resulting layout, followed by the applet:
Figure 12.7: Expanded Button Layout
public void init() { setLayout( new GridBagLayout() ); constraints.weightx = 1.0; constraints.weighty = 1.0; constraints.fill = GridBagConstraints.BOTH; int x, y; // for clarity addGB( new Button("North"), x=1,y=0 ); addGB( new Button("West"), x=0,y=1 ); addGB( new Button("Center"), x=1,y=1 ); addGB( new Button("East"), x=2,y=1 ); addGB( new Button("North"), x=1,y=2 ); }
BOTH
is one of the constants of the GridBagConstraints
class; it tells the component to fill the available space in both directions. The following table lists the constants that you can use to set the fill
field:
HORIZONTAL
- Fill the available horizontal space.
VERTICAL
- Fill the available vertical space.
BOTH
- Fill the available space in both directions.
NONE
- Don't fill the available space; display the component at its preferred size.
We set the weight constraints to 1.0; it doesn't matter what they are, provided that each component has the same non-zero weight. fill
doesn't work if the component weights in the direction you're filling are 0, which is the default value.
Weighting
The weightx
and weighty
variables of a GridBagConstraints
object determine how space is distributed among the columns or rows in a layout. As long as you keep things simple, the effect these variables have is fairly intuitive: the larger the weight, the greater the amount of space allocated to the component. The display below shows what happens if we vary the weightx
constraint from 0.1 to 1.0 as we place three buttons in a row.
Figure 12.8: Varied weightx
public void init() { setLayout( new GridBagLayout() ); constraints.fill = GridBagConstraints.BOTH; constraints.weighty = 1.0; int x, y; // for clarity constraints.weightx = 0.1; addGB( new Button("one"), x=0, y=0 ); constraints.weightx = 0.5; addGB( new Button("two"), ++x, y ); constraints.weightx = 1.0; addGB( new Button("three"), ++x, y ); }
Although the weight values have no real meaning, you might find it convenient to use values that add up to 1. When you're working with weights, it is best not to get complicated. The effect of combining weights with different padding values can be very strange, as can be the effect of using different weightx
values for components in the same column, or different weighty
values for components in the same row. While it's possible to examine the code for the GridBagLayout
and figure out exactly what it will do in any given situation, it really isn't worth the effort.
Spanning rows and columns
Perhaps the best feature of the GridBaglayout
is that it lets you create arrangements in which components span two or more rows or columns. To do so, you set the gridwidth
and gridheight
variables of the GridBagConstraints
. Here's an applet that creates such a display; button one spans two columns vertically, and button four spans two horizontally.
Figure 12.9: Layout Using GridBagConstraints
public void init() { setLayout( new GridBagLayout() ); constraints.weightx = 1.0; constraints.weighty = 1.0; constraints.fill = GridBagConstraints.BOTH; int x, y; // for clarity constraints.gridheight = 2; // Span two rows addGB( new Button("one"), x=0, y=0 ); constraints.gridheight = 1; // set it back addGB( new Button("two"), x=1, y=0 ); addGB( new Button("three"), x=2, y=0 ); constraints.gridwidth = 2; // Span two columns addGB( new Button("four"), x=1, y=1 ); constraints.gridwidth = 1; // set it back }
The size of each element is controlled by the gridwidth
and gridheight
values of its constraints. For button one, we set gridheight
to 2. Therefore, it is two cells high; its gridx
and gridy
positions are both zero, so it occupies cell (0,0) and the cell directly below it, (0,1). Likewise, button four has a gridwidth
of 2 and a gridheight
of 1, so it occupies two cells horizontally. We place this button in cell (1,1), so it occupies that cell and its neighbor, (2,1).
In this example, we set the fill
to BOTH
, and weightx
and weighty
to 1, for all components. By doing so, we told each button to occupy all the space available. Strictly speaking, this isn't necessary. However, it makes it easier to see exactly how much space each button occupies.
Anchoring
If a component is smaller than the space available for it, it is centered by default. But centering isn't the only possibility. The anchor
constraint tells a grid bag layout how to position a component within its space. Possible values are: GridBagConstraints.CENTER
, NORTH
, NORTHEAST
, EAST
, SOUTHEAST
, SOUTH
, SOUTHWEST
, WEST
, and NORTHWEST
. For example, an anchor of GridBagConstraints.NORTH
centers a component at the top of its display area; SOUTHEAST
places a component at the bottom left of its area.
Padding and Insets
Another way to control the behavior of a component in a grid bag layout is to use padding and insets. Padding is determined by the ipadx
and ipady
fields of GridBagConstraints
. They specify additional horizontal and vertical space that is added to the component when it is placed in its cell. In the example below, the West button is larger because we have set the ipadx
and ipady
values of its constraints to 25. Therefore, the layout manager gets the button's preferred size and adds 25 pixels in each direction to determine the button's actual size. The sizes of the other buttons are unchanged because their padding is set to 0 (the default), but their spacing is different. The West button is unnaturally tall, which means that the middle row of the layout must be taller than the others.
Figure 12.10: Layout Using Padding and Insets
public void init() { setLayout( new GridBagLayout() ); int x, y; // for clarity addGB( new Button("North"), x=1,y=0 ); constraints.ipadx = 25; // set padding constraints.ipady = 25; addGB( new Button("West"), x=0,y=1 ); constraints.ipadx = 0; // set padding back constraints.ipady = 0; addGB( new Button("Center"), x=1,y=1 ); addGB( new Button("East"), x=2,y=1 ); addGB( new Button("North"), x=1,y=2 ); }
Notice that the horizontal padding, ipadx
, is added on both the left and right sides of the button. Therefore, the button grows horizontally by twice the value of ipadx
. Likewise, the vertical padding, ipady
, is added on both the top and the bottom.
Insets add space between the edges of the component and its cell. They are stored in the insets
field of GridBagConstraints
, which is an Insets
object. An Insets
object has four fields, to specify the margins on the top
, bottom
, left
, and right
of the component. The relationship between insets and padding can be confusing. As shown in the following diagram, padding is added to the component itself, increasing its size. Insets are external to the component and represent the margin between the component and its cell.
Figure 12.11: Another Layout Using Padding and Insets
Padding and weighting have an odd interaction with each other. If you use padding, it is best to use the default weightx
and weighty
values for each component.
Relative Positioning
In all of our grid bag layouts so far, we have specified the gridx
and gridy
coordinates of each component explicitly using its constraints. There's another alternative: relative positioning.
Conceptually, relative positioning is simple: we simply say "put this component to the left of (or below) the previous component." To do so, set gridx
or gridy
to the constant GridBagConstraints.RELATIVE
. Unfortunately, it's not as simple as this. Here are a couple of warnings:
- To place a component to the right of the previous one, set
gridx
toRELATIVE
, AND use the same value forgridy
as you used for the previous component. - Similarly, to place a component below the previous one, set
gridy
toRELATIVE
, AND leavegridx
unchanged. - Setting both
gridx
andgridy
toRELATIVE
places all the components in one row, not in a diagonal line, as you would expect. (This is the default.)
In other words, if gridx
or gridy
is RELATIVE
, you had better leave the other value unchanged. In short, RELATIVE
makes it easy to arrange a lot of components in a row or a column. That's what it was intended for; if you try to do something else, you're fighting against the layout manager, not working with it.
GridBagLayout
allows another kind of relative positioning, in which you specify where, in a row or a column, the component should be placed. To do so, you use the gridwidth
and gridheight
fields of GridBagConstraints
. Setting either of these to the constant REMAINDER
says that the component should be the last item in its row or column, and therefore should occupy all the remaining space. Setting either gridwidth
or gridheight
to RELATIVE
says that it should be the second to the last item in its row or column. Obviously, you can use these constants to create constraints that can't possibly be met; for example, you can say that two components must be the last component in a row. In these cases, the layout manager tries to do something reasonable--but it will almost certainly do something you don't want. Again, relative placement works well as long as you don't try to twist it into doing something it wasn't designed for.
Composite layouts
Sometimes things don't fall neatly into little boxes. This is true of layouts as well as life. For example, if you want to use some of GridBagLayout
's weighting features for part of your GUI, you could create separate layouts for different parts of the GUI, and combine them with yet another layout. That's how we'll build the pocket calculator interface below. We will use three grid bag layouts: one for the first row of buttons (C, %, +), one for the last (0, ., =), and one for the applet itself. The master layout (the applet's) manages the text field we use for the display, the panels containing the first and last rows of buttons, and the twelve buttons in the middle.[2]
[2] If you're curious, this calculator is based on the ELORG-801, which I found in an online "calculator museum": see http://www.geocities.com/CapeCanaveral/6747/elorg801.jpg.
Figure 12.12: The Calculator Applet
Here's the code for the Calculator
applet. It only implements the user interface (i.e., the keyboard); it collects everything you type in the display field, until you press C (clear). Figuring out how to connect the GUI to some other code that would perform the operations is up to you. One strategy would be to send an event to the object that does the computation whenever the user presses the equals sign. That object could read the contents of the text field, parse it, do the computation, and display the results.
import java.awt.*; import java.awt.event.*; public class Calculator extends java.applet.Applet implements ContainerListener, ActionListener { GridBagConstraints gbc = new GridBagConstraints(); { gbc.weightx = 1.0; gbc.weighty = 1.0; gbc.fill = GridBagConstraints.BOTH; } TextField theDisplay = new TextField(); public void init() { setFont( new Font("Monospaced", Font.BOLD, 24) ); addContainerListener( this ); gbc.gridwidth=4; addGB( this, theDisplay, 0, 0 ); // make the top row Panel topRow = new Panel(); topRow.addContainerListener( this ); gbc.gridwidth = 1; gbc.weightx = 1.0; addGB( topRow, new Button("C"), 0, 0 ); gbc.weightx = 0.33; addGB( topRow, new Button("%"), 1, 0 ); gbc.weightx = 1.0; addGB( topRow, new Button("+"), 2, 0 ); gbc.gridwidth = 4; addGB( this, topRow, 0, 1 ); gbc.weightx = 1.0; gbc.gridwidth = 1; // make the digits for(int j=0; j<3; j++) for(int i=0; i<3; i++) addGB( this, new Button( "" + ((2-j)*3+i+1) ), i, j+2 ); // -, x, and divide addGB( this, new Button("-"), 3, 2 ); addGB( this, new Button("x"), 3, 3 ); addGB( this, new Button("\u00F7"), 3, 4 ); // make the bottom row Panel bottomRow = new Panel(); bottomRow.addContainerListener( this ); gbc.weightx = 1.0; addGB( bottomRow, new Button("0"), 0, 0 ); gbc.weightx = 0.33; addGB( bottomRow, new Button("."), 1, 0 ); gbc.weightx = 1.0; addGB( bottomRow, new Button("="), 2, 0 ); gbc.gridwidth = 4; addGB( this, bottomRow, 0, 5 ); } private void addGB( Container cont, Component comp, int x, int y ) { if ( ! (cont.getLayout() instanceof GridBagLayout) ) cont.setLayout( new GridBagLayout() ); gbc.gridx = x; gbc.gridy = y; cont.add( comp, gbc ); } public void componentAdded( ContainerEvent e ) { Component comp = e.getChild(); if ( comp instanceof Button ) ((Button)comp).addActionListener( this ); } public void componentRemoved( ContainerEvent e ) { } public void actionPerformed( ActionEvent e ) { if ( e.getActionCommand().equals("C") ) theDisplay.setText( "" ); else theDisplay.setText( theDisplay.getText() + e.getActionCommand() ); } }
Once again, we use an addGB()
helper method to add components with their constraints to the layout. Before discussing how to build the display, let's look at addGB()
. We said earlier that there are three layout managers in our user interface: one for the applet itself, one for the panel containing the first row of buttons (topRow
), and one for the panel containing the bottom row of buttons (bottomRow
). We use addGB()
for all three layouts; its first argument specifies the container to add the component to. Thus, when the first argument is this
, we're adding an object to the applet itself. When the first argument is topRow
, we're adding a button to the first row of buttons. addGB()
first checks the container's layout manager, and sets it to GridBagLayout
if it isn't already set properly. It then sets the object's position by modifying a set of constraints, gbc
, and then uses these constraints to add the object to the container.
We use a single set of constraints throughout the applet, modifying fields as we see fit. The constraints are created and initialized at the beginning of the applet, using a nonstatic initializer block. Before calling addGB()
, we set any fields of gbc
for which the defaults are inappropriate. Thus, for the display itself, we set the grid width to 4, and add the display directly to the applet (this
). The add()
method, which is called by addGB()
, makes a copy of the constraints, so we're free to reuse gbc
throughout the applet.
The first row of buttons (and the last) are the motivation for the composite layout. Using a single GridBagLayout
, it's very difficult (or impossible) to create buttons that aren't aligned with the grid; that is, you can't say "I want the C button to have a width of 1.5." Therefore, topRow
has its own layout manager, with three horizontal cells, allowing each button in the row to have a width of 1. To control the size of the buttons, we set the weightx
variables so that the "clear" and "plus" buttons take up more space than the "percent" button. We then add the topRow
as a whole to the applet, with a width of 4. The bottom row is built similarly.
To build the buttons for the digits 1-9, we use a doubly nested loop. There's nothing particularly interesting about this loop, except that it's probably a bit too clever for good taste. The minus, multiply, and divide buttons are also simple: we create a button with the appropriate label, and use addGB()
to place it in the applet. It's worth noting that we used a Unicode constant to request a real division sign, rather than wimping out and using a slash.
That's it for the user interface; the only topic left is event handling. Each button generates action events; we need to register listeners for these events. We'll make the applet the listener for all the buttons. To register the applet as a listener, we'll be clever. Whenever a component is added to a container, the container generates a ContainerEvent
. Therefore, we can write componentAdded()
and componentRemoved()
methods, declare that the applet is a ContainerListener
, and use componentAdded()
to register listeners for our buttons. This means that the applet must register as a ContainerListener
for itself, and for the two panels, topRow
and bottomRow
. Our componentAdded()
method is very simple. It calls getChild()
to find out what component caused the event (i.e., what component was added). If that component is a button, it registers the applet as an ActionListener
for that button.
actionPerformed()
is called whenever the user presses any button. It clears the display if the user pressed the "C" button; otherwise, it appends the button's action command (in this case, its label) to the display.
Combining layout managers is an extremely useful trick. Granted, this applet verges on overkill. You won't often need to create a composite layout using multiple grid bags. Composite layouts are most common with the BorderLayout
; you'll frequently use different layout managers for each of a border layout's regions. For example, the Center region might be a ScrollPane
, which has its own special-purpose layout manager; the East and South regions might be panels managed by grid layouts or flow layouts, as appropriate.