Intro to JGoodies Forms

Intro to JGoodies Forms

By Lance Finney, OCI Senior Software Engineer

March 2005


Introduction

Sun provided many Layout Managers by default with Swing. Some are simple to use, like FlowLayout, BorderLayout, CardLayout, and GridLayout. Unfortunately, these Layout Managers are limited in the layouts and interfaces they can produce. Other Layout Managers, like SpringLayout and GridBagLayout, are powerful enough to create virtually any desired interface. Unfortunately, they are not very easy to use. Creating Swing applications would be much easier if there were a Layout Manager that combined the simplicity of the first group with the power of the second group.

Fortunately, Karsten Lentzsch at JGoodies has created an open source Layout Manager with all the power of GridBagLayout without all the complexity. FormLayout is the keystone of the JGoodies Forms framework.

This article introduces the JGoodies Forms framework, particularly in comparison to GridBagLayout, which is likely the most popular Sun-provided Layout Manager. A simple GUI is created using GridBagLayout to point out some of its complications. After an explanation of the primary differences between the GridBagLayout approach and the Forms approach, the same simple GUI is rewritten using Forms' PanelBuilder to point out the advantages. The simple GUI is rebuilt one more time to point out the further advantages of a more advanced builder from JGoodies, DefaultFormBuilder. The article ends with a demonstration of the debugging utilities in Forms.

Using GridBagLayout

Let's start the comparison with a simple GUI implemented with GridBagLayout:

GridBagLayout

Here's the code:

  1. private static JComponent buildGridBagLayout() {
  2. JTextField name = new JTextField(20);
  3. JLabel nameLabel = new JLabel("Name:");
  4. nameLabel.setDisplayedMnemonic('N');
  5. nameLabel.setLabelFor(name);
  6. JTextField areaCode = new JTextField(3);
  7. JLabel phoneLabel = new JLabel("Phone:");
  8. phoneLabel.setDisplayedMnemonic('P');
  9. phoneLabel.setLabelFor(areaCode);
  10. JTextField prefix = new JTextField(3);
  11. JTextField number = new JTextField(3);
  12. JTextField email = new JTextField(20);
  13. JLabel emailLabel = new JLabel("Email:");
  14. emailLabel.setDisplayedMnemonic('E');
  15. emailLabel.setLabelFor(email);
  16. JButton okButton = new JButton("OK");
  17. okButton.setPreferredSize(new Dimension(100,
  18. (int) okButton.getPreferredSize().getHeight()));
  19. JButton cancelButton = new JButton("Cancel");
  20. cancelButton.setPreferredSize(new Dimension(100,
  21. (int) cancelButton.getPreferredSize().getHeight()));
  22.  
  23. JPanel panel = new JPanel(new GridBagLayout());
  24. panel.setBorder(BorderFactory.createEmptyBorder(4, 10, 10, 10));
  25.  
  26. GridBagConstraints gbc = new GridBagConstraints();
  27. gbc.anchor = GridBagConstraints.WEST;
  28. gbc.fill = GridBagConstraints.HORIZONTAL;
  29. gbc.insets = new Insets(6, 6, 0, 0);
  30. gbc.gridx = GridBagConstraints.RELATIVE;
  31. gbc.gridy = 0;
  32.  
  33. panel.add(nameLabel, gbc);
  34. gbc.gridwidth = GridBagConstraints.REMAINDER;
  35. panel.add(name, gbc);
  36.  
  37. gbc.gridy++;
  38. gbc.gridwidth = 1;
  39. panel.add(phoneLabel, gbc);
  40. panel.add(areaCode, gbc);
  41. panel.add(prefix, gbc);
  42. panel.add(number, gbc);
  43.  
  44. gbc.gridy++;
  45. panel.add(emailLabel, gbc);
  46. gbc.gridwidth = GridBagConstraints.REMAINDER;
  47. gbc.weightx = 1;
  48. panel.add(email, gbc);
  49.  
  50. JPanel buttonPanel = new JPanel(new GridBagLayout());
  51. gbc.gridwidth = 1;
  52. gbc.gridy = 0;
  53. buttonPanel.add(okButton, gbc);
  54. buttonPanel.add(cancelButton, gbc);
  55.  
  56. panel.add(buttonPanel,
  57. new GridBagConstraints(1, 3, 4, 1, 0, 0,
  58. GridBagConstraints.EAST, GridBagConstraints.NONE,
  59. new Insets(0, 0, 0, 0), 0, 0));
  60.  
  61. panel.add(Box.createGlue(),
  62. new GridBagConstraints(0, 4, 4, 1, 0, 1,
  63. GridBagConstraints.EAST, GridBagConstraints.NONE,
  64. new Insets(0, 0, 0, 0), 0, 0));
  65.  
  66. return panel;
  67. }

While this approach certainly works, using GridBagLayout for form-based interfaces is ungainly. Concerns include the following:

IssueGridBagLayout
Lines of Code 69
Specifying mnemonics and associating them with their components Manual, which means it is often not done.
Space specification Specified by pixel, so the appearance changes in different resolutions.
Grid specification The layout is accomplished through a grid, but the details of the grid (the width, height, growth behavior, etc. of the rows and columns) are created by a non-obvious composite of the constraints of the components in individual cells. On the flip side, all details about column and row behavior must be managed with each cell, which is a lot of overhead.
Error handling If a component is placed in an improper cell (through a mistaken cell width, for example), GridBagLayout provides no help in figuring out what caused the resulting mangled layout.
White space Managing white space between columns and rows is accomplished through applying Insets object to each cell instead of using explicit spacing columns and rows. This means that multiple Insets objects must be checked if behavior is incorrect.
Constraints Care must be taken when using GridBagConstraints objects, but no standard solution fits well. GridBagConstraints has eleven public fields which have to be managed; often several must change for each cell, but several probably do not. This causes a tension because neither reuse nor fresh generation fit well. When reusing GridBagConstraints, several public fields must be changed for the specific needs of each cell. The other option is generating a fresh GridBagConstraints object for each cell, but this leads to significant code duplication.
Button subpanels The buttons have different widths than the fields, requiring either a complicated and fragile column set or a manually generated subpanel.
Button size and layout Having the OK button to the left of the Cancel button is correct for Windows, but it is not correct for Linux, and the sizes are correct for Windows but incorrect for Mac OS X. Creating uniform buttons requires manually specifying the width of every button, which means it is often not done.
Growable white space To create explicit growable whitespace in the layout, the very non-intuitive Box.createGlue() must be used.

While some of these issues can be improved through tools like Packer, a completely different approach is necessary overall: JGoodies Forms.

FormLayout's Approach

The largest difference between FormLayout and GridBagLayout is the means of specifying the rows and columns.

As noted above, the specification of a column or row in GridBagLayout is actually the combination of the specifications of the GridBagConstraints of the components in that column or row. This leads to an unnecessarily complicated implementation because column- or row-specific information is imbedded in the wrong place, and it often must be duplicated across many different components. When there is a mistake in the layout, the developer must look at many different components to determine the problem instead of looking directly at the specification of the column or row.

In contrast, FormLayout places the column and row information directly with the column or row itself. If necessary, a component can override the row and column specification. An example of this would be making a specific component align to the left while everything else in the column aligns to the right. This greatly simplifies layout and reduces information duplication because components are responsible only for exceptions to column and row behavior.

Additionally, Forms provides many convenience methods, Builders, Factories, and debugging tools that greatly exceed GridBagLayout's API.

Defining the grid

Like GridBagLayout, FormLayout lays out components within a grid. However, FormLayout allows defining the rows and columns declaratively when instantiating the Layout. For example, here's a declaration for a very simple layout (columns are specified before rows):

  1. FormLayout layout =
  2. new FormLayout("left:pref, 2dlu, pref, 2dlu, pref:grow",
  3. "pref, 3dlu, pref");

This layout is specified as having five columns and three rows. In this layout, columns 1, 3, and 5 can hold components and columns 2 and 4 are spacers. Similarly, rows 1 and 3 can hold components and row 2 is a spacer row.

All component columns are specified to be as wide as the widest preferred size for a component in the column, and both rows are specified to be as tall as the tallest preferred size for a component in the row. Additionally, the first column has left alignment (the default is fill), and the last column will grow to encompass all extra space as the panel is resized.

The spacers columns are set to be two Dialog Units wide. Dialog Units are based on the pixel size of the dialog font so they grow and shrink with the font and resolution. If pixels are used to specify the spacer (an option Forms provides), then the overall proportions of the layout will change when viewed on screens with different resolutions or with different fonts or font sizes.

For brevity, abbreviations can be used for the specifications.

  1. FormLayout layout =
  2. new FormLayout("l:p, 2dlu, p, 2dlu, p:g", "p, 3dlu, p");

Alternately, for those who are uncomfortable with the mini language used here and would prefer compile-time safety, the following approach works, too. It is safer, but less concise.

  1. ColumnSpec[] colSpecs = new ColumnSpec[]{
  2. new ColumnSpec(ColumnSpec.LEFT, Sizes.PREFERRED, 0),
  3. new ColumnSpec(Sizes.DLUX2),
  4. new ColumnSpec(ColumnSpec.LEFT, Sizes.PREFERRED, 0),
  5. new ColumnSpec(Sizes.DLUX2),
  6. new ColumnSpec(ColumnSpec.LEFT, Sizes.PREFERRED, 1),
  7. };
  8. RowSpec[] rowSpecs = new RowSpec[]{
  9. new RowSpec(Sizes.PREFERRED),
  10. new RowSpec(Sizes.DLUY3),
  11. new RowSpec(Sizes.PREFERRED)
  12. };
  13. new FormLayout(colSpecs, rowSpecs);

There are many other options for size, alignment, and resize behavior that allow for much flexibility in layout.

Using PanelBuilder

With FormLayout's design philosophy in mind, let's see the same simple GUI implemented with the basic FormLayout builder:

PanelBilder

Here's the code for the identical panel:

  1. private static JComponent buildPanelBuilderLayout() {
  2. JTextField name = new JTextField(20);
  3. JTextField areaCode = new JTextField(3);
  4. JTextField prefix = new JTextField(3);
  5. JTextField number = new JTextField(4);
  6. JTextField email = new JTextField(20);
  7. JButton okButton = new JButton("OK");
  8. JButton cancelButton = new JButton("Cancel");
  9.  
  10. FormLayout layout = new FormLayout(
  11. "left:pref, 2dlu, pref, 2dlu, pref, 2dlu, pref:grow",
  12. "pref, 3dlu, pref, 3dlu, pref, 3dlu, pref");
  13. PanelBuilder builder = new PanelBuilder(layout);
  14. builder.setDefaultDialogBorder();
  15. CellConstraints cc = new CellConstraints();
  16.  
  17. JLabel nameLabel = builder.addLabel("&Name:", cc.xy(1, 1));
  18. nameLabel.setLabelFor(name);
  19. builder.add(name, cc.xyw(3, 1, 5));
  20. JLabel phoneLabel = builder.addLabel("&Phone:", cc.xy(1, 3));
  21. phoneLabel.setLabelFor(areaCode);
  22. builder.add(areaCode, cc.xy(3, 3));
  23. builder.add(prefix, cc.xy(5, 3));
  24. builder.add(number, cc.xy(7, 3,
  25. CellConstraints.LEFT, CellConstraints.DEFAULT));
  26. JLabel emailLabel = builder.addLabel("&Email:", cc.xy(1, 5));
  27. emailLabel.setLabelFor(email);
  28. builder.add(email, cc.xyw(3, 5, 5));
  29. builder.add(ButtonBarFactory.buildOKCancelBar(okButton, cancelButton),
  30. cc.xyw(1, 7, builder.getColumnCount()));
  31.  
  32. JPanel panel = builder.getPanel();
  33. return panel;
  34. }

This example highlights a another difference in building a layout between FormLayout and other layouts; instead of adding components to the panel, components are added to a Builder, which takes responsibility for constructing the layout. The constructed panel is derived from the builder using the getPanel() method.

Specifying the location of the components on the panel is accomplished through the CellConstraints methods .xy() and .xyw(). For example, builder.add(areaCode, cc.xy(3, 3)) adds the areaCode component to the cell in the third row and third column (rows and columns are 1-based). For row spanning, use methods like builder.add(name, cc.xyw(3, 1, 5)), which adds the name component to the cell in the first row and third column, and spans five rows. To allow a cell to override the layout specifications for a row or column, use overriding versions like cc.xy(7, 3, CellConstraints.LEFT, CellConstraints.DEFAULT)), which lets the cell override the column's default fill behavior with left alignment.

The advantage of adding components to the PanelBuilder instead of adding it directly to the panel is that Forms provides many convenience methods. One example from the code above is builder.addLabel("&Name:", cc.xy(1, 1)), which creates a JLabel and adds it to the builder. This method also sets up the mnemonic.

Another interesting aspect of the example is the ButtonBarFactory. The ButtonBarFactory creates a panel to hold the buttons, automatically sizing them and laying out the buttons according to GUI standards for the operating system.

Comparing PanelBuilder to GridBagLayout

Let's look at each of the issues identified with GridBagLayout above:

IssueGridBagLayoutPanelBuilder
Lines of Code 69 36
Specifying mnemonics and associating them with their components Manual, which means it is often not done. Specifying mnemonics is easy using '&'. Associating mnemonics with their components is still manual.
Space specification Specified by pixel, so the appearance changes in different resolutions. Specified by Dialog Units, so the layout retains proportions when the font size or resolution changes.
Grid specification Grid details are created by a non-obvious composite of the constraints of the components in individual cells. The details of the columns and rows are specified declaratively.
Error handling GridBagLayout provides no help in figuring out what causes mangled layouts. If a component is placed in an improper cell, a very informative error message is displayed: java.lang.IndexOutOfBoundsException: The row index 8 must be less than or equal to 7.
White space Managing white space between columns and rows is accomplished through applying Insets object to each cell. Managing white space is accomplished through special spacer rows and columns.
Constraints GridBagConstraints has eleven public fields which have to be managed; often several must change for each cell, but several probably do not. This causes a tension because neither reuse nor fresh generation fit well. Much of the hassle of the constraints is managed either in the declaration of the rows and columns or by defining spacers. What is left is managed through simple methods.
Button subpanels The buttons have different widths than the fields, requiring either a complicated and fragile column set or a manually generated subpanel. The ButtonBarFactory creates a panel implicitly, taking care of the difficulty of creating a new panel.
Button size and layout Button size and placement do not follow GUI without significant effort. The ButtonBarFactory automatically sizes and lays out the buttons according to GUI standards for the operating system.
Growable white space To create explicit growable whitespace in the layout, the very non-intuitive Box.createGlue() must be used. The space at the bottom of the panel is created automatically. If it were to be in a different place, the "grow" specification on the rows could handle it more easily than it is handled by GridBagLayout.

PanelBuilder as used here has one disadvantage compared to GridBagLayout: the placement of each component in the grid must be explicitly specified. This makes adding components to the top a hassle; many placement values must be changed manually. This hassle is avoided for GridBagLayout only when GridBagConstraints.RELATIVE is used for gridx.

This hassle can be avoided with PanelBuilder using methods like builder.getRowCount() and builder.getColumnCount(). However, the following section shows another approach.

Using DefaultFormBuilder

PanelBuilder resolved most of the issues with GridBagLayout, but it didn't resolve all of them, and it added a problem with explicit placement of components in the grid. Fortunately, Forms has a more advanced builder, DefaultFormBuilder, which extends PanelBuilder to add more options:

DefaultFormBuilder

Here's the code for the identical panel:

  1. private static JComponent buildDefaultFormBuilderLayout() {
  2. JTextField name = new JTextField(20);
  3. JTextField areaCode = new JTextField(3);
  4. JTextField prefix = new JTextField(3);
  5. JTextField number = new JTextField(4);
  6. JTextField email = new JTextField(20);
  7. JButton okButton = new JButton("OK");
  8. JButton cancelButton = new JButton("Cancel");
  9.  
  10. FormLayout layout =
  11. new FormLayout("l:p, 2dlu, p, 2dlu, p, 2dlu, p, 2dlu, p:g", "");
  12. DefaultFormBuilder builder = new DefaultFormBuilder(layout);
  13. builder.setDefaultDialogBorder();
  14.  
  15. builder.append("&Name:", name, 7);
  16. builder.append("&Phone:", areaCode);
  17. builder.append(prefix);
  18. builder.append(number);
  19. builder.nextLine();
  20. builder.append("&Email:", email, 7);
  21. builder.append(ButtonBarFactory.buildOKCancelBar(okButton, cancelButton),
  22. builder.getColumnCount());
  23.  
  24. return builder.getPanel();
  25. }

Again, the builder automatically creates the JLabels for us. However, this builder associates the label with its component through methods like builder.append("&Phone:", areaCode). With the additional int parameter, builder.append("&Name:", name, 7) specifies a span of seven columns for the component. There are many other versions of append on DefaultFormBuilder.

In this implementation, the columns are specified declaratively, but the rows aren't. Instead, the rows are added to the builder automatically through the append() methods. When a component is added, the builder determines if another row is necessary, adding a new spacer and preferred size row automatically.

However, after the phone number, there is an open cell left. To move to the next row, call builder.nextLine().

Comparing DefaultFormBuilder to GridBagLayout

Let's look at each of the issues identified with GridBagLayout above:

IssueGridBagLayoutPanelBuilderDefaultFormBuilder
Lines of Code 69 36 27
Specifying mnemonics and associating them with their components Manual, which means it is often not done. Specifying mnemonics is easy using '&'. Associating mnemonics with their components is still manual. Specifying mnemonics is easy using '&'. Associating mnemonics with their components is automatic.
Space specification Specified by pixel, so the appearance changes in different resolutions. Specified by Dialog Units, so the layout retains proportions when the font size or resolution changes.
Grid specification Grid details are created by a non-obvious composite of the constraints of the components in individual cells. The details of the columns and rows are specified declaratively. The details of the columns are specified declaratively. Rows are added automatically (although declarative specification is still an option).
Error handling GridBagLayout provides no help in figuring out what causes mangled layouts. If a component is placed in an improper cell, a very informative error message is displayed: java.lang.IndexOutOfBoundsException: The row index 8 must be less than or equal to 7.
White space Managing white space between columns and rows is accomplished through applying Insets object to each cell. Managing white space is accomplished through special spacer rows and columns. Managing white space is accomplished through special spacer rows and columns, some of which are generated automatically.
Constraints GridBagConstraints has eleven public fields which have to be managed; often several must change for each cell, but several probably do not. This causes a tension because neither reuse nor fresh generation fit well. Much of the hassle of the constraints is managed either in the declaration of the rows and columns or by defining spacers. What is left is managed through simple methods.
Button subpanels The buttons have different widths than the fields, requiring either a complicated and fragile column set or a manually generated subpanel. The ButtonBarFactory creates a panel implicitly, taking care of the difficulty of creating a new panel.
Button size and layout Button size and placement do not follow GUI without significant effort. The ButtonBarFactory automatically sizes and lays out the buttons according to GUI standards for the operating system.
Growable white space To create explicit growable whitespace in the layout, the very non-intuitive Box.createGlue() must be used. The space at the bottom of the panel is created automatically. If it were to be in a different place, the "grow" specification on the rows could handle it more easily than it is handled by GridBagLayout.

Debugging

One great feature of Forms is its debugging support.

FormDebugUtils

FormDebugUtils has nine methods that print information about the layout to the console. The most comprehensive version is FormDebugUtils.dumpAll(panel). Below is the output for the PanelBuilder example above. This output shows all the necessary information to understand how each column, row, and cell is defined.

COLUMN SPECS:l:p:n, f:2dluX:n, f:p:n, f:2dluX:n, f:p:n, f:2dluX:n, f:p:g
ROW SPECS:   c:p:n, c:3dluY:n, c:p:n, c:3dluY:n, c:p:n, c:3dluY:n, c:p:n
 
COLUMN GROUPS:  {}
ROW GROUPS:     {}
 
COMPONENT CONSTRAINTS
( 1,  1,  1,  1, "d=l, d=c"); javax.swing.JLabel      "Name:"
( 3,  1,  5,  1, "d=f, d=c"); javax.swing.JTextField
( 1,  3,  1,  1, "d=l, d=c"); javax.swing.JLabel      "Phone:"
( 3,  3,  1,  1, "d=f, d=c"); javax.swing.JTextField
( 5,  3,  1,  1, "d=f, d=c"); javax.swing.JTextField
( 7,  3,  1,  1, "l, d=c"); javax.swing.JTextField
( 1,  5,  1,  1, "d=l, d=c"); javax.swing.JLabel      "Email:"
( 3,  5,  5,  1, "d=f, d=c"); javax.swing.JTextField
( 1,  7,  7,  1, "d=f, d=c"); javax.swing.JPanel
 
GRID BOUNDS
COLUMN ORIGINS: 14 53 57 94 98 135 139 187 
ROW ORIGINS:    11 31 36 56 61 81 86 112 

The column and row specs are the same as those declaratively specified, with default values visible and all specifications abbreviated.

The Column Groups and Row Groups are a feature of Forms not used in the simple example created for this article. A column group is a set of columns that are defined to have the same width, and a row group is a set of row that are defined to have the same height. If they had been used in the example, the groups would appear here.

The Component Constraints section shows the placement, column and row spanning, alignment, and type of each component in the layout. The Grid Bounds show the exact pixel placements of the columns and rows.

FormDebugPanel

Additionally, FormDebugPanel can be used to see the borders of the rows and columns, as seen in the example below for the DefaultFormBuilder implementation.

FormDebugPanel

The FormDebugPanel is specified in the constructor for the PanelBuilder or DefaultFormBuilder like this:

DefaultFormBuilder builder = 
    new DefaultFormBuilder(layout, new FormDebugPanel());

Sun's Layout Managers have nothing as useful as these utilities.

Summary

Compared to the Layout Managers Sun provides, FormLayout and its accompanying API are more powerful, more concise, and easier to use.

JGoodies Forms "..aims to make simple things easy and the hard stuff possible, the good design easy and the bad difficult." Through declarative grid creation, Builders, Factories, and debugging support, it succeeds.

References

Software Engineering Tech Trends (SETT) is a regular publication featuring emerging trends in software engineering.