JTF > Rationale > The acm.gui Package

Chapter 7
The acm.gui Package


Of all the problems reported by the computing education community that arise from using Java at the introductory level, the most difficult one for the Task Force to solve was the lack of appropriate components for creating simple graphical user interfaces, more commonly known as GUIs. In the taxonomy presented in Chapter 3, this problem was expressed as follows:

   A3.GUI components inappropriate for beginners

From our initial search for strategies to address this problem, we concluded that the chance of finding a solution that would appeal to any large segment of the education community was remote and that the tools already provided by Java were likely to prove more successful than any alternative we could provide. In fact, the original decision of the Task Force was to abandon the search for a solution to this problem after several failed attempts to come up with a satisfactory design. In the first draft of this Rationale document, we summarized our failure to identify a workable solution as follows:

Unfortunately, the Task Force has not been able to come up with a satisfactory design. What’s more, some members of the Task Force have become convinced that it is not possible to create a design that a significant fraction of the potential audience would accept as a standard. That conclusion may be incorrect. There may be a design out there around which a consensus might emerge. As of this release, however, the Task Force has not been able to find it.[JTF05]

In the feedback that we received in response to the draft proposal, however, we were strongly encouraged to look harder for a solution to this problem, which was clearly an important one for a significant fraction of our prospective audience. In response to that demand, we went back to the drawing board to see if we could fashion a synthesis of the sort we had found for the acm.graphics package described in Chapter 5. After experimenting with several designs, we were able to engineer an acm.gui package that simplifies the creation and layout of GUI components in a way that is simple for novices and, at the same time, allows for a straightforward transition to the standard Java layout managers. This chapter describes the final design of that package, along with the general principles that led the Task Force to adopt it.

Although the design of the acm.gui package is largely independent of the acm.program package described in Chapter 6, we expect that most adopters will use the GUI tools in the context of a Program object. The examples in this chapter therefore are designed to use acm.program as their application framework. Adapting those examples so that they use a JFrame object instead of a Program is straightforward, but is not spelled out in detail.

7.1 What’s missing from Java’s standard GUI libraries

In many ways, developing a simple GUI package for Java is difficult not because Java’s existing libraries are lacking essential functionality but rather because they contain so much that is good. A large fraction of the classes in the javax.swing package are specifically designed to support the development of graphical user interfaces and work well at that task. The collection of GUI widgets available in Swing is extraordinarily rich and, moreover, seems on the whole to be well designed. Students have access to buttons, scrollbars, sliders, text areas, selectable lists, popup menus, and a host of other interactor classes that enable them to write graphical user interfaces containing most if not all of the interactor patterns they are likely to see in professional software. And while it may require some sophistication to use the more advanced features of these interactors, most of the Swing classes specify reasonable defaults that make it easy to use these interactors in conventional ways. Novices, for example, have little trouble using JButtons in their code once they master the standard listener paradigm Java uses for all event handling. From the outset, the Task Force recognized that trying to offer a replacement for JButton would be a losing proposition. The standard Swing class is entirely serviceable, and it would make no sense to ask students to learn some parallel structure that they would quickly have to abandon.

At the same time, educators had certainly complained about the inappropriateness of Java’s GUI components for novices. Clearly, something was missing. The important question for the Task Force was to figure out what those missing elements might be.

Input from the community

To answer the question of what was missing from Java’s standard packages in terms of GUI support, the Task Force began by looking at the proposals we had received to see what aspects of the GUI-construction process those packages had sought to change. The three proposals that most directly addressed this question were the following:

As was the case in our design of the acm.graphics package, the Java Task Force identified strengths in each of these proposals, but felt that none were suitable as they stood. The common problem in each of these packages was that they differed much more substantially from the Swing standard than we were prepared to accept. We also found the strongest features of each package occurred in complementary aspects of their designs, making it possible to create an improved package by adopting the best features of each.

The important realization that we derived from studying these proposals in detail is that the critical problem does not lie with the Swing interactor classes themselves, but rather with the facilities that the java.awt package uses for placing those interactors on the screen, which is largely unchanged in the Swing world. Richard Rasala offers the following succinct expression of this principle in the introduction to one of his proposals to the Task Force:

A central problem in GUI construction is the composition of the individual GUI widgets into some organized arrangement within a panel or frame. To avoid manual pixel positioning, Java uses layout managers to layout components. Unfortunately, the layout managers provided by Java are very problematic. The simple ones do not do enough and the advanced ones are very hard to use and often do not produce the expected results after all the work. [Rasala04c]

The essential problem can be expressed more precisely as follows: Given the standard facilities available in Java’s AWT and Swing packages, there is no good way to create a two-dimensional layout in which the preferred sizes of the components determine the sizes of the cells in the grid. The problem is not that the necessary functionality is unavailable but rather that the standard classes are difficult for novices to use. Java’s GridBagLayout class provides the necessary capability, but is not suitable for novices for the reasons described in the following section.

The shortcomings of GridBagLayout

In accordance with our general principle of minimizing the number of new classes outside the standard Java package framework, the Task Force would have been happier not to create any new layout manager classes. Unfortunately, after looking closely at the problems associated with GridBagLayout, the Task Force—along with the various people who submitted proposals in this area—became convinced that the difficulty of using GridBagLayout and the bad habits it encourages outweigh the advantage of maintaining compatibility.

The most significant problems associated with GridBagLayout are as follows:

Given these problems, the general desiderata for a new class to replace GridBagLayout are for the most part clear. To be successful, the new class should

  1. Be much simpler to use than GridBagLayout
  2. Avoid the design flaws in GridBagLayout that interfere with the development of good object-oriented style
  3. Maintain compatibility with GridBagLayout to ease the eventual transition to the standard Java classes
  4. Provide as much of the functionality of GridBagLayout as possible

The first three design criteria follow immediately from the principles that the Task Force articulated in Chapter 2. The last design goal, by contrast, is not necessarily as obvious. The external submissions we received typically eliminated some functionality from GridBagLayout as a way of simplifying it. Our concern, however, was that such an approach would give some potential adopters a reason for staying with GridBagLayout, despite its shortcomings. If the Task Force packages were incapable of duplicating a layout constraint that someone was already using, that person would be less likely to adopt the alternative design. If, on the other hand, one can do everything with the new class that had previously been possible with GridBagLayout, much of the resistance to change could be eliminated.

Our solution was to implement a TableLayout manager class that offers the functionality of GridBagLayout in a way that is much easier for novices to understand. Its motivations are therefore similar to those that gave rise to the BreezySwing package [Lambert04a] and the table capabilities of Java Power Tools [Rasala04c], both of which served as models. The principal differences in our implementation of TableLayout are that we wanted to ensure that clients had access to the full set of capabilities from GridBagLayout and that we felt it was important for the package to support a smooth transition to the standard GridBagLayout approach. The details of the TableLayout class are outlined in section 7.3.

Other extensions

Although the single most important problem in the existing Java packages is the lack of a simple tabular layout manager, the Task Force also concluded that the standard Java classes have two additional shortcomings that were important to correct:
  1. The existing Java classes make it difficult to read numeric data from the user. For the most part, the problems are the same as those involved in reading numeric data from a console or dialog box, as discussed in Chapter 4. If students are required to perform their own numeric conversion, they must first master such difficult conceptual issues as the use of wrapper classes for numeric types and the details of exception handling. Hiding that complexity simplifies such operations considerably. To do so, the Task Force decided to add two new classes—IntField and DoubleField—to simplify the development of applications that require numeric input. These classes are discussed in detail in section 7.2.
  2. In many cases, students do not want to design the user interface for an entire application but rather provide some interactors that make it easy to control the activity of some other type of program. For example, it might be useful to create a GraphicsProgram controlled by a few buttons located at the periphery of the display. This capability is provided by the ProgramLayout class and has already been described in section 6.5.

7.2 Numeric interactors

As noted in the preceding section, the Task Force decided that it was necessary to provide IntField and DoubleField classes to simplify the development of applications that require numeric input. Each of these classes extends JTextField but provides additional methods to hide the complexity involved in numeric conversion and exception handling. The additional methods available for DoubleField appear in Figure 7-1; the methods for IntField are the same except for the expected changes in the argument and result types.

Figure 7-1. Methods defined in the DoubleField class
Constructors
 
DoubleField()
  Creates a DoubleField object with no initial value.
DoubleField(double value)
  Creates a DoubleField object with the specified initial value.
DoubleField(double low, double high)
  Creates a DoubleField object whose value is constrained to the specified limits.
DoubleField(double value, double low, double high)
  Creates a DoubleField object with the specified initial value and limits.
 
Methods to set and retrieve the value of the field
 
void setValue(double value)
  Sets the value of the field and updates the display.
double getValue()
  Returns the value in the field. If the value is out of range, errors or retries occur here.
 
Methods to control formatting
 
void setFormat(String format)
  Sets the format string for the field as specified in the DecimalFormat class in java.text.
String getFormat()
  Returns the current format string.
 
Additional methods
(unlikely to be used by novices)
 
void setExceptionOnError(boolean flag)
  Sets the error-handling mode: false means retry on error, true means raise an exception.
boolean getExceptionOnError()
  Returns the error-handling mode, as defined in setExceptionOnError.
   

The IntField and DoubleField classes are closely related to a similar pair of classes proposed by Lambert and Osborne [Lambert04a]. The versions supplied by the acm.gui package have been redesigned to make the behavior of those classes more consistent with the readInt and readDouble methods defined for the IOConsole class in acm.io. In particular, the IntField and DoubleField classes adopt the paradigm of their acm.io counterparts in that their default behavior gives the user a chance to reenter values in the case of illegal input but allows more sophisticated applications to catch the exception that occurs in such cases.

The only significant design decision for these classes was how to control format. It turns out that the DoubleField class is useful in practice only if the client has some way of specifying the format of the displayed result. In the absence of format control, the display typically contains so many digits as to become unreadable. The setFormat and getFormat methods shown in Figure 7-1 eliminate this problem by allowing the client to specify the output format. The format itself is specified using a string as defined in the DecimalFormat class in java.text.

7.3 The TableLayout class

The easiest way to understand how the TableLayout class works is to look at a simple example. The usual strategy for building applications that use TableLayout to create the user interface is to implement a class that extends Program with an individually designed constructor that assembles the interactors into the desired arrangement. The first line of the constructor is typically a call to setLayout, which establishes the number of rows and columns in the tabular grid. The rest of the constructor then creates the necessary interactors and adds them to the table, filling each row from left to right and then each row from top to bottom. This strategy is illustrated in Figure 7-2, which implements a simple temperature converter.

Figure 7-2. Temperature conversion program
/*
 * File: TemperatureConverter.java
 * -------------------------------
 * This program allows users to convert temperatures
 * back and forth from Fahrenheit to Celsius.
 */
 
import acm.gui.*;
import acm.program.*;
import java.awt.event.*;
import javax.swing.*;
 
/** This class implements a temperature converter. */
public class TemperatureConverter extends Program {
 
/** Initialize the graphical user interface. */
   public void init() {
      setLayout(new TableLayout(2, 3));
      fahrenheitField = new IntField(32);
      fahrenheitField.setActionCommand("F -> C");
      fahrenheitField.addActionListener(this);
      celsiusField = new IntField(0);
      celsiusField.setActionCommand("C -> F");
      celsiusField.addActionListener(this);
      add(new JLabel("Degrees Fahrenheit"));
      add(fahrenheitField);
      add(new JButton("F -> C"));
      add(new JLabel("Degrees Celsius"));
      add(celsiusField);
      add(new JButton("C -> F"));
      addActionListeners();
   }
 
/** Listen for a button action. */
   public void actionPerformed(ActionEvent e) {
      String cmd = e.getActionCommand();
      if (cmd.equals("F -> C")) {
         int f = fahrenheitField.getValue();
         int c = (int) Math.round((5.0 / 9.0) * (f - 32));
         celsiusField.setValue(c);
      } else if (cmd.equals("C -> F")) {
         int c = celsiusField.getValue();
         int f = (int) Math.round((9.0 / 5.0) * c + 32);
         fahrenheitField.setValue(f);
      }
   }
 
/* Private instance variables. */
   private IntField fahrenheitField;
   private IntField celsiusField;
}
 

The user interface for the TemperatureConverter program looks like this:

The user can type values into either of the IntField interactors and then perform the conversion to the other scale either by hitting the appropriate button or by hitting the Enter key in the interactor itself. Each of these actions generates an ActionEvent whose action command is either the string "F -> C" or "C -> F" depending on which button or interactor generated the event. These events are fielded by the actionPerformed method in the class, which performs the necessary conversion and then updates the value of the corresponding field.

In terms of understanding what the TableLayout framework provides, all of the important code appears in the constructor. The line

 

setLayout(new TableLayout(2, 3));

defines the layout for the program as a whole to be a TableLayout object with two rows and three columns. The rest of the constructor adds interactors to the table layout in order, filling each horizontal row from left to right, and then proceeding through each row from top to bottom. In this example, each row contains a JLabel indicating the temperature scale for that row, an IntField that allows the user to enter a value, and a JButton to trigger the conversion. The code that sets the action command for the IntField and adds the program as a listener is required only to enable the Enter key in those interactors. The buttons would be active even in the absence of those calls.

The feature that sets TableLayout apart from the simpler GridLayout mechanism is that the sizes of each component in the table are adjusted according to their preferred sizes and the constraints imposed by the grid. The JLabel objects are of different sizes, but the implementation of TableLayout makes sure that there is enough space in the first column to hold the longer of the two labels. By default, each component added to a TableLayout container is expanded to fills its grid cell, although this behavior can be changed by specifying constraints as described in the following section.

What the TemperatureConverter program illustrates is that the student can rely on the TableLayout to arrange the elements appropriately on the screen without having to specify the detailed constraint information that would be required using GridBagLayout. By defining the appropriate defaults, all the student usually has to do is count the number of rows and columns in the user interface and add the interactors in the appropriate order.

Specifying constraints

Unlike the packages that were proposed to the Task Force, the TableLayout class allows the programmer to specify constraints that provide fine-grained control over the layout process. For many applications, such constraints are unnecessary. In other cases, however, it is important to be able to specify specific field sizes, to control the alignment of a cell, or to merge cells horizontally or vertically so that they span several element positions. The GridBagLayout paradigm uses a separate class called GridBagConstraints to represent these constraints. The primary advantage of TableLayout is that it hides the GridBagConstraints object from the programmer.

Instead of allocating a constraints object explicitly, the TableLayout design allows clients to specify the constraints in string form. For every field in Java’s GridBagConstraints, the TableLayout manager accepts a constraint string in the form

 

field=value

where field is the name of the GridBagConstraints field and value is a value appropriate for that field. For example, to duplicate the effect of setting the gridwidth field of a constraints object to 2 (thereby specifying a two-column field), adopters of the acm.gui package can simply specify the constraint string

 

"gridwidth=2"

instead of the more confusing process of allocating a GridBagConstraints object and setting the appropriate field, like this:

 

GridBagConstraints gbc = new GridBagConstraints();
gbc.gridwidth = 2;

The strings used as constraint objects can set several fields at once by including multiple field/value pairs separated by spaces. Moreover, for those fields whose values are defined by named constants in the GridBagConstraints class, TableLayout allows that name to be used as the value field of the constraint string. For example, the following string indicates that a field should span two columns but that the component should fill space only in the y direction:

 

"gridwidth=2 fill=VERTICAL"

Constraint strings are checked at run time to make sure that the fields and values are defined and are consistent. The case of letters, however, is ignored, which makes it possible to name the fields in a way that is consistent with Java’s conventions. Thus, if an instructor wants to emphasize the case convention that has each word within a multiword identifier begin with an uppercase letter, it is equally effective to write

 

"gridWidth=2 fill=VERTICAL"

The complete list of constraints supported by the TableLayout class is shown in Figure 7-3. The first block shows the constraints that adopters are likely to use; the second block consists of constraints that are included only to maintain symmetry with the GridBagConstraints class.

Figure 7-3. Constraints supported by the TableLayout class
Constraints that clients will often find useful
 
gridwidth=columns  or  gridheight=rows
  Indicates that this table cell should span the indicated number of columns or rows.
width=pixels  or  height=pixels
  The width specification indicates that the width of this column should be the specified number of pixels. If different widths are specified for cells in the same column, the column width is defined to be the maximum. In the absence of any width specification, the column width is the largest of the preferred widths. The height specification is interpreted symmetrically for row heights.
weightx=weight  or  weighty=weight
  If the total size of the table is less than the size of its enclosure, TableLayout will ordinarily center the table in the available space. If any of the cells, however, are given nonzero weightx or weighty values, the extra space is distributed along that axis in proportion to the weights specified. As in the GridBagLayout model, the weights are floating-point values and may therefore contain a decimal point.
fill=fill
  Indicates how the component in this cell should be resized if its preferred size is smaller than the cell size. The legal values are NONE, HORIZONTAL, VERTICAL, and BOTH, indicating the axes along which stretching should occur. The default is BOTH.
anchor=anchor
  If a component is not being filled along a particular axis, the anchor specification indicates where the component should be placed in its cell. The default value is CENTER, but any of the standard compass directions (NORTH, SOUTH, EAST, WEST, NORTHEAST, NORTHWEST, SOUTHEAST, or SOUTHWEST) may also be used.
 
Less useful constraints included for compatibility with GridBagLayout
 
top=pixels  or  bottom=pixels  or  left=pixels  or  right=pixels
  Supplies an inset value for the specified edge of the component in the cell.
ipadx=pixels  or  ipady=pixels
  Specifies additional padding internal to the component that increases its preferred size along the indicated axis. The extra pixels are added on both sides of the component, so the size increases by twice the value supplied.
gridx=column  or  gridy=row
  These constraints specify the location of the component within the grid and will not ordinarily be used in the TableLayout model, unless you are trying to teach how GridBagLayout works.
   

To emphasize the advantage of using the TableLayout model, Figure 7-4 and 7-5 offer two implementations of a program that lays out Buttons objects (the code comes from the GridBagLayout documentation, which still uses the JDK 1.1 style) in the following grid:

As the code for the figures makes clear, the TableLayout version is less than half the size of the GridBagLayout example and is significantly easier to read. At the same time, the underlying discipline is compatible, which should allow users to migrate from one model to the other.

Figure 7-4. Example from the GridBagLayout documentation
/*
 * File: GridBagExample.java
 * -------------------------
 * This application uses GridBagLayout to construct a
 * two-dimensional table of buttons.  The example is
 * adapted from the GridBagLayout documentation.
 */
 
import java.awt.*;
import javax.swing.*;
 
public class GridBagExample extends JPanel {
 
   public GridBagExample() {
      GridBagLayout gridbag = new GridBagLayout();
      GridBagConstraints c = new GridBagConstraints();
      setLayout(gridbag);
      c.fill = GridBagConstraints.BOTH;
      addButton("Button0", gridbag, c);
      addButton("Button1", gridbag, c);
      addButton("Button2", gridbag, c);
      c.gridwidth = GridBagConstraints.REMAINDER;
      addButton("Button3", gridbag, c);
      addButton("Button4", gridbag, c);
      c.gridwidth = 3;
      addButton("Button5", gridbag, c);
      c.gridwidth = GridBagConstraints.REMAINDER;
      addButton("Button6", gridbag, c);
      c.gridwidth = 1;
      c.gridheight = 2;
      addButton("Button7", gridbag, c);
      c.gridwidth = GridBagConstraints.REMAINDER;
      c.gridheight = 1;
      addButton("Button8", gridbag, c);
      addButton("Button9", gridbag, c);
   }
 
   private void addButton(String name,
                          GridBagLayout gridbag,
                          GridBagConstraints c) {
      Button button = new Button(name);
      gridbag.setConstraints(button, c);
      add(button);
   }
 
   public static void main(String[] args) {
      GridBagExample example = new GridBagExample();
      JFrame frame = new JFrame("GridBagExample");
      Container contentPane = frame.getContentPane();
      contentPane.setLayout(new BorderLayout());
      contentPane.add(BorderLayout.CENTER, example);
      frame.setSize(350, 200);
      frame.setVisible(true);
   }
}
 

Figure 7-5. Similar application using TableLayout
/*
 * File: TableExample.java
 * -----------------------
 * This application uses the ACM TablePanel class to
 * construct a two-dimensional table of buttons that
 * matches the one from GridBagExample.
 */
 
import acm.gui.*;
import java.awt.*;
 
public class TableExample extends Program {
 
   public void init() {
      setLayout(new TableLayout(5, 4));
      add(new Button("Button0"));
      add(new Button("Button1"));
      add(new Button("Button2"));
      add(new Button("Button3"));
      add(new Button("Button4"), "gridwidth=REMAINDER");
      add(new Button("Button5"), "gridwidth=3");
      add(new Button("Button6"));
      add(new Button("Button7"), "gridheight=2");
      add(new Button("Button8"), "gridwidth=REMAINDER");
      add(new Button("Button9"), "gridwidth=REMAINDER");
   }
 
}
 

Controlling spacing

Several of the layout managers in the standard Java packages—including, for example, FlowLayout, BorderLayout, and GridLayout—make it possible to control the spacing between components in the display by setting hgap and vgap parameters to specify the spacing in the horizontal and vertical dimensions, respectively. The standard GridBagLayout mechanism does not do so, even though it would be extremely useful. The TableLayout manager does support these parameters, which can be set either by using the four-argument version of the constructor

 

public TableLayout(int rows, int columns, int hgap, int vgap)

or by calling the setHgap and setVgap methods on an existing layout. Setting these parameters ensures that each row or column in the table is separated from the next by the specified number of pixels, making it easy to add spacing around the interactors.

Although the hgap and vgap parameters are typically positive, it turns out that the value –1 can be extremely useful in this setting. If you specify –1 as a gap value, each component will be positioned so that it overlaps the preceding one by one pixel. If the components have a one-pixel border, this strategy ensures that the border running between the components will be only one pixel wide. If no gap applied, each component would have a one-pixel border, which would result in a two-pixel divider on the screen.

7.4 The TablePanel Classes

The examples presented so far in this chapter use TableLayout as the layout manager for the central region of a program, which is likely to be its most common application in the introductory curriculum. The TableLayout manager, however, can be used with any container and is extremely useful in assembling patterns of interactors.

To make it easier to assemble nested containers hierarchically, the acm.gui package includes three convenience classes that extend JPanel but install an appropriate TableLayout manager. These classes and their constructor patterns appear in Figure 7-6.

Figure 7-6. Convenience classes based on TableLayout
TablePanel constructors
 
public TablePanel(int rows, int columns)
  Creates a JPanel with the indicated number of rows and columns.
public TablePanel(int rows, int columns, int hgap, int vgap)
  Creates a JPanel with the specified dimensions and gaps.
 
HPanel constructors
 
public HPanel()
  Creates a JPanel consisting of a single horizontal row.
public HPanel(int hgap, int vgap)
  Creates an HPanel with the specified gaps (vgap applies above and below the row).
 
VPanel constructors
 
public VPanel()
  Creates a JPanel consisting of a single vertical column.
public VPanel(int hgap, int vgap)
  Creates an VPanel with the specified gaps (hgap applies to the left and right of the column).
   

The HPanel and VPanel classes make it easy to create complex assemblages of interactors by decomposing them hierarchically into rows and columns. In this respect, they have at least a common purpose with the BoxLayout manager introduced in the javax.swing package. The panel HPanel and VPanel classes, however, offer far more flexibility because they have the full power of the TableLayout class. The BoxLayout manager, by contrast, makes it difficult to do anything except to string together components in a linear form with no control over spacing or format.



Final Report—July 31, 2006