12.6 Sophisticated Layout Management

So far we’ve been using only the border layout, flow layout, and grid layout for the user interface of our sample applications. For more complex tasks, this is not going to be enough. In this section, we will discuss advanced layout management in detail.

Windows programmers may well wonder why Java makes so much fuss about layout managers. After all, in Windows, layout management is not a big deal; you just use a dialog editor to drag and drop your components onto the surface of a dialog, and then use editor tools to line up components, to space them equally, to center them, and so on. If you are working on a big project, you probably don’t have to worry about component layout at all—a skilled user interface designer does all this for you.

The problem with this approach is that the resulting layout must be manually updated if the sizes of the components change. Why would the component sizes change? This can happen when the strings in an application are translated to a foreign language. For example, the German word for “Cancel” is “Abbrechen.” If a button has been designed with just enough room for the string “Cancel”, the German version will look broken, with a clipped string.

Why don’t the buttons simply grow to accommodate the labels? When you drop buttons in a dialog editor, there is no indication in which direction they should grow. After the dragging and dropping and arranging, the dialog editor merely remembers the pixel position and size of each component. It does not remember why the components were arranged in this fashion.

The Java layout managers are a much better approach to component layout. With a layout manager, the layout comes with instructions about the relationships among the components. This was particularly important in the original AWT, which used native user interface elements. The size of a button or a list box in Motif, Windows, and the Macintosh could vary widely, and an application or applet would not know a priori on which platform it would display its user interface. To some extent, that degree of variability has gone away with Swing. If your application forces a particular look-and-feel, such as Metal, it looks identical on all platforms. However, if you let users of your application choose their favorite look-and-feel, then you again need to rely on the flexibility of layout managers to arrange the components.

Since Java 1.0, the AWT includes the grid bag layout that lays out components in rows and columns. The row and column sizes are flexible, and components can span multiple rows and columns. This layout manager is very flexible, but also very complex. The mere mention of the words “grid bag layout” has been known to strike fear in the hearts of Java programmers.

In an unsuccessful attempt to design a layout manager that would free programmers from the tyranny of the grid bag layout, the Swing designers came up with the box layout. According to the JDK documentation of the BoxLayout class: “Nesting multiple panels with different combinations of horizontal and vertical [sic] gives an effect similar to GridBagLayout, without the complexity.” However, as each box is laid out independently, you cannot use box layouts to arrange neighboring components both horizontally and vertically.

Java SE 1.4 saw yet another attempt to design a replacement for the grid bag layout—the spring layout. You use imaginary springs to connect the components in a container. As the container is resized, the springs stretch or shrink, thereby adjusting the positions of the components. This sounds tedious and confusing, and it is. The spring layout quickly sank into obscurity.

In 2005, the NetBeans team invented the Matisse technology, which combines a layout tool and a layout manager. A user interface designer uses the tool to drop components into a container and to indicate which components should line up. The tool translates the designer’s intentions into instructions for the group layout manager. This is much more convenient than writing the layout management code by hand. The group layout manager became a part of Java SE 6. Even if you don’t use NetBeans as your IDE, we think you should consider using its GUI builder tool. You can design your GUI in NetBeans and paste the resulting code into your IDE of choice.

In the coming sections, we will cover the grid bag layout because it is commonly used and is still the easiest mechanism for producing layout code for older Java versions. We will show you a strategy that makes grid bag layouts relatively painless in common situations.

Next, we will cover the Matisse tool and the group layout manager. You will want to know how the group layout manager works so that you can check whether Matisse recorded the correct instructions when you visually positioned your components.

Finally, we will show you how you can bypass layout management altogether and place your components manually, and how you can write your own layout manager.

12.6.1 The Grid Bag Layout

The grid bag layout is the mother of all layout managers. You can think of a grid bag layout as a grid layout without the limitations. In a grid bag layout, the rows and columns can have variable sizes. You can join adjacent cells to make room for larger components. (Many word processors, as well as HTML, provide similar capabilities for tables: You can start out with a grid and then merge adjacent cells as necessary.) The components need not fill the entire cell area, and you can specify their alignment within cells.

Consider the font selector of Figure 12.29. It consists of the following components:

• Two combo boxes to specify the font face and size

• Labels for these two combo boxes

• Two checkboxes to select bold and italic

• A text area for the sample string

Image

Figure 12.29 A font selector

Now, chop up the container into a grid of cells, as shown in Figure 12.30. (The rows and columns need not have equal size.) Each checkbox spans two columns, and the text area spans four rows.

Image

Figure 12.30 Dialog box grid used in design

To describe the layout to the grid bag manager, use the following procedure:

1. Create an object of type GridBagLayout. You don’t need to tell it how many rows and columns the underlying grid has. Instead, the layout manager will try to guess it from the information you give it later.

2. Set this GridBagLayout object to be the layout manager for the component.

3. For each component, create an object of type GridBagConstraints. Set field values of the GridBagConstraints object to specify how the components are laid out within the grid bag.

4. Finally, add each component with its constraints by using the call add(component, constraints);

Here’s an example of the code needed. (We’ll go over the various constraints in more detail in the sections that follow—so don’t worry if you don’t know what some of the constraints do.)

GridBagLayout layout = new GridBagLayout();
panel.setLayout(layout);
GridBagConstraints constraints = new GridBagConstraints();
constraints.weightx = 100;
constraints.weighty = 100;
constraints.gridx = 0;
constraints.gridy = 2;
constraints.gridwidth = 2;
constraints.gridheight = 1;
panel.add(component, constraints);

The trick is knowing how to set the state of the GridBagConstraints object. We’ll discuss this object in the sections that follow.

12.6.1.1 The gridx, gridy, gridwidth, and gridheight Parameters

The gridx, gridy, gridwidth, and gridheight constraints define where the component is located in the grid. The gridx and gridy values specify the column and row positions of the upper left corner of the component to be added. The gridwidth and gridheight values determine how many columns and rows the component occupies.

The grid coordinates start with 0. In particular, gridx = 0 and gridy = 0 denotes the top left corner. The text area in our example has gridx = 2, gridy = 0 because it starts in column 2 (that is, the third column) of row 0. It has gridwidth = 1 and gridheight = 4 because it spans one column and four rows.

12.6.1.2 Weight Fields

You always need to set the weight fields (weightx and weighty) for each area in a grid bag layout. If you set the weight to 0, the area never grows or shrinks beyond its initial size in that direction. In the grid bag layout for Figure 12.29, we set the weightx field of the labels to be 0. This allows the labels to keep constant width when you resize the window. On the other hand, if you set the weights for all areas to 0, the container will huddle in the center of its allotted area instead of stretching to fill it.

Conceptually, the problem with the weight parameters is that weights are properties of rows and columns, not individual cells. But you need to specify them for cells because the grid bag layout does not expose the rows and columns. The row and column weights are computed as the maxima of the cell weights in each row or column. Thus, if you want a row or column to stay at a fixed size, you need to set the weights of all components in it to zero.

Note that the weights don’t actually give the relative sizes of the columns. They tell what proportion of the “slack” space should be allocated to each area if the container exceeds its preferred size. This isn’t particularly intuitive. We recommend that you set all weights at 100. Then, run the program and see how the layout looks. Resize the dialog to see how the rows and columns adjust. If you find that a particular row or column should not grow, set the weights of all components in it to zero. You can tinker with other weight values, but it is usually not worth the effort.

12.6.1.3 The fill and anchor Parameters

If you don’t want a component to stretch out and fill the entire area, set the fill constraint. You have four possibilities for this parameter: the valid values are GridBagConstraints.NONE, GridBagConstraints.HORIZONTAL, GridBagConstraints.VERTICAL, and GridBagConstraints.BOTH.

If the component does not fill the entire area, you can specify where in the area you want it by setting the anchor field. The valid values are GridBagConstraints.CENTER (the default), GridBagConstraints.NORTH, GridBagConstraints.NORTHEAST, GridBagConstraints.EAST, and so on.

12.6.1.4 Padding

You can surround a component with additional blank space by setting the insets field of GridBagConstraints. Set the left, top, right, and bottom values of the Insets object to the amount of space that you want to have around the component. This is called the external padding.

The ipadx and ipady values set the internal padding. These values are added to the minimum width and height of the component. This ensures that the component does not shrink down to its minimum size.

12.6.1.5 Alternative Method to Specify the gridx, gridy, gridwidth, and gridheight Parameters

The AWT documentation recommends that instead of setting the gridx and gridy values to absolute positions, you set them to the constant GridBagConstraints.RELATIVE. Then, add the components to the grid bag layout in a standardized order, going from left to right in the first row, then moving along the next row, and so on.

You would still specify the number of rows and columns spanned, by giving the appropriate gridheight and gridwidth fields. However, if the component extends to the last row or column, you don’t need to specify the actual number, but the constant GridBagConstraints.REMAINDER. This tells the layout manager that the component is the last one in its row.

This scheme does seem to work. But it sounds really goofy to hide the actual placement information from the layout manager and hope that it will rediscover it.

All this sounds like a lot of trouble and complexity. But in practice, the following recipe makes grid bag layouts relatively trouble free:

1. Sketch out the component layout on a piece of paper.

2. Find a grid such that the small components are each contained in a cell and the larger components span multiple cells.

3. Label the rows and columns of your grid with 0, 1, 2, 3, . . . You can now read off the gridx, gridy, gridwidth, and gridheight values.

4. For each component, ask yourself whether it needs to fill its cell horizontally or vertically. If not, how do you want it aligned? This tells you the fill and anchor parameters.

5. Set all weights to 100. However, if you want a particular row or column to always stay at its default size, set the weightx or weighty to 0 in all components that belong to that row or column.

6. Write the code. Carefully double-check your settings for the GridBagConstraints. One wrong constraint can ruin your whole layout.

7. Compile, run, and enjoy.

The GUI builder in NetBeans has tools for specifying the constraints visually—see Figure 12.31.

Image

Figure 12.31 Specifying grid bag constraints in NetBeans

12.6.1.6 A Helper Class to Tame the Grid Bag Constraints

The most tedious aspect of the grid bag layout is writing the code that sets the constraints. Most programmers write helper functions or a small helper class for this purpose. We present such a class after the complete code for the font dialog example. This class has the following features:

• Its name is short: GBC instead of GridBagConstraints.

• It extends GridBagConstraints, so you can use shorter names such as GBC.EAST for the constants.

• Use a GBC object when adding a component, such as

add(component, new GBC(1, 2));

• There are two constructors to set the most common parameters: gridx and gridy, or gridx, gridy, gridwidth, and gridheight.

add(component, new GBC(1, 2, 1, 4));

• There are convenient setters for the fields that come in x/y pairs:

add(component, new GBC(1, 2).setWeight(100, 100));

• The setter methods return this, so you can chain them:

add(component, new GBC(1, 2).setAnchor(GBC.EAST).setWeight(100, 100));

• The setInsets methods construct the Insets object for you. To get one-pixel insets, simply call

add(component, new GBC(1, 2).setAnchor(GBC.EAST).setInsets(1));

Listing 12.10 shows the frame class for the font dialog example. The GBC helper class is in Listing 12.12. Here is the code that adds the components to the grid bag:

add(faceLabel, new GBC(0, 0).setAnchor(GBC.EAST));
add(face, new GBC(1, 0).setFill(GBC.HORIZONTAL).setWeight(100, 0).setInsets(1));
add(sizeLabel, new GBC(0, 1).setAnchor(GBC.EAST));
add(size, new GBC(1, 1).setFill(GBC.HORIZONTAL).setWeight(100, 0).setInsets(1));
add(bold, new GBC(0, 2, 2, 1).setAnchor(GBC.CENTER).setWeight(100, 100));
add(italic, new GBC(0, 3, 2, 1).setAnchor(GBC.CENTER).setWeight(100, 100));
add(sample, new GBC(2, 0, 1, 4).setFill(GBC.BOTH).setWeight(100, 100));

Once you understand the grid bag constraints, this kind of code is fairly easy to read and debug.


Image Note

The tutorial at http://docs.oracle.com/javase/tutorial/uiswing/layout/gridbag.html suggests that you reuse the same GridBagConstraints object for all components. We find the resulting code hard to read and error-prone. For example, look at the demo at http://docs.oracle.com/javase/tutorial/uiswing/events/containerlistener.html. Was it really intended that the buttons are stretched horizontally, or did the programmer just forget to turn off the fill constraint?


Listing 12.10 gridbag/FontFrame.java


 1   package gridbag;
 2
 3   import java.awt.Font;
 4   import java.awt.GridBagLayout;
 5   import java.awt.event.ActionListener;
 6
 7   import javax.swing.BorderFactory;
 8   import javax.swing.JCheckBox;
 9   import javax.swing.JComboBox;
10   import javax.swing.JFrame;
11   import javax.swing.JLabel;
12   import javax.swing.JTextArea;
13
14   /**
15    * A frame that uses a grid bag layout to arrange font selection components.
16    */
17   public class FontFrame extends JFrame
18   {
19      public static final int TEXT_ROWS = 10;
20      public static final int TEXT_COLUMNS = 20;
21
22      private JComboBox<String> face;
23      private JComboBox<Integer> size;
24      private JCheckBox bold;
25      private JCheckBox italic;
26      private JTextArea sample;
27
28      public FontFrame()
29      {
30         GridBagLayout layout = new GridBagLayout();
31         setLayout(layout);
32
33         ActionListener listener = event -> updateSample();
34
35         // construct components
36
37         JLabel faceLabel = new JLabel("Face: ");
38
39         face = new JComboBox<>(new String[] { "Serif", "SansSerif", "Monospaced",
40               "Dialog", "DialogInput" });
41
42         face.addActionListener(listener);
43
44         JLabel sizeLabel = new JLabel("Size: ");
45
46         size = new JComboBox<>(new Integer[] { 8, 10, 12, 15, 18, 24, 36, 48 });
47
48         size.addActionListener(listener);
49
50         bold = new JCheckBox("Bold");
51         bold.addActionListener(listener);
52
53         italic = new JCheckBox("Italic");
54         italic.addActionListener(listener);
55
56         sample = new JTextArea(TEXT_ROWS, TEXT_COLUMNS);
57         sample.setText("The quick brown fox jumps over the lazy dog");
58         sample.setEditable(false);
59         sample.setLineWrap(true);
60         sample.setBorder(BorderFactory.createEtchedBorder());
61
62         // add components to grid, using GBC convenience class
63
64         add(faceLabel, new GBC(0, 0).setAnchor(GBC.EAST));
65         add(face, new GBC(1, 0).setFill(GBC.HORIZONTAL).setWeight(100, 0)
66               .setInsets(1));
67         add(sizeLabel, new GBC(0, 1).setAnchor(GBC.EAST));
68         add(size, new GBC(1, 1).setFill(GBC.HORIZONTAL).setWeight(100, 0)
69               .setInsets(1));
70         add(bold, new GBC(0, 2, 2, 1).setAnchor(GBC.CENTER).setWeight(100, 100));
71         add(italic, new GBC(0, 3, 2, 1).setAnchor(GBC.CENTER).setWeight(100, 100));
72         add(sample, new GBC(2, 0, 1, 4).setFill(GBC.BOTH).setWeight(100, 100));
73         pack();
74         updateSample();
75      }
76
77      public void updateSample()
78      {
79         String fontFace = (String) face.getSelectedItem();
80         int fontStyle = (bold.isSelected() ? Font.BOLD : 0)
81               + (italic.isSelected() ? Font.ITALIC : 0);
82         int fontSize = size.getItemAt(size.getSelectedIndex());
83         Font font = new Font(fontFace, fontStyle, fontSize);
84         sample.setFont(font);
85         sample.repaint();
86      }
87   }


Listing 12.11 gridbag/GBC.java


 1   package gridbag;
 2
 3   import java.awt.*;
 4
 5   /**
 6    * This class simplifies the use of the GridBagConstraints class.
 7    * @version 1.01 2004-05-06
 8    * @author Cay Horstmann
 9    */
10   public class GBC extends GridBagConstraints
11   {
12      /**
13       * Constructs a GBC with a given gridx and gridy position and all other grid
14       * bag constraint values set to the default.
15       * @param gridx the gridx position
16       * @param gridy the gridy position
17       */
18      public GBC(int gridx, int gridy)
19      {
20         this.gridx = gridx;
21         this.gridy = gridy;
22      }
23
24      /**
25       * Constructs a GBC with given gridx, gridy, gridwidth, gridheight and all
26       * other grid bag constraint values set to the default.
27       * @param gridx the gridx position
28       * @param gridy the gridy position
29       * @param gridwidth the cell span in x-direction
30       * @param gridheight the cell span in y-direction
31       */
32      public GBC(int gridx, int gridy, int gridwidth, int gridheight)
33      {
34         this.gridx = gridx;
35         this.gridy = gridy;
36         this.gridwidth = gridwidth;
37         this.gridheight = gridheight;
38      }
39
40      /**
41       * Sets the anchor.
42       * @param anchor the anchor value
43       * @return this object for further modification
44       */
45      public GBC setAnchor(int anchor)
46      {
47         this.anchor = anchor;
48         return this;
49      }
50
51      /**
52       * Sets the fill direction.
53       * @param fill the fill direction
54       * @return this object for further modification
55       */
56      public GBC setFill(int fill)
57      {
58         this.fill = fill;
59         return this;
60      }
61
62      /**
63       * Sets the cell weights.
64       * @param weightx the cell weight in x-direction
65       * @param weighty the cell weight in y-direction
66       * @return this object for further modification
67       */
68      public GBC setWeight(double weightx, double weighty)
69      {
70         this.weightx = weightx;
71         this.weighty = weighty;
72         return this;
73      }
74
75      /**
76       * Sets the insets of this cell.
77       * @param distance the spacing to use in all directions
78       * @return this object for further modification
79       */
80      public GBC setInsets(int distance)
81      {
82         this.insets = new Insets(distance, distance, distance, distance);
83         return this;
84      }
85
86      /**
87       * Sets the insets of this cell.
88       * @param top the spacing to use on top
89       * @param left the spacing to use to the left
90       * @param bottom the spacing to use on the bottom
91       * @param right the spacing to use to the right
92       * @return this object for further modification
93       */
94      public GBC setInsets(int top, int left, int bottom, int right)
95      {
96         this.insets = new Insets(top, left, bottom, right);
97         return this;
98      }
99
100     /**
101      * Sets the internal padding
102      * @param ipadx the internal padding in x-direction
103      * @param ipady the internal padding in y-direction
104      * @return this object for further modification
105      */
106     public GBC setIpad(int ipadx, int ipady)
107     {
108        this.ipadx = ipadx;
109        this.ipady = ipady;
110        return this;
111     }
112  }


12.6.2 Group Layout

Before discussing the API of the GroupLayout class, let us have a quick look at the Matisse GUI builder in NetBeans. We won’t give you a full Matisse tutorial—see http://netbeans.org/kb/docs/java/quickstart-gui.html for more information.

Here is the workflow for laying out the top of the dialog in Figure 12.13. Start a new project and add a new JFrame form. Drag a label until two guidelines appear that separate it from the container borders:

Image

Place another label below the first row:

Image

Drag a text field so that its baseline lines up with the baseline of the first label. Again, note the guidelines:

Image

Finally, line up a password field with the label to the left and the text field above.

Image

Matisse translates these actions into the following Java code:

layout.setHorizontalGroup(
    layout.createParallelGroup(GroupLayout.Alignment.LEADING)
    .addGroup(layout.createSequentialGroup()
        .addContainerGap()
    .addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addComponent(jLabel1)
                .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(jTextField1))
            .addGroup(layout.createSequentialGroup()
                .addComponent(jLabel2)
                .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(jPasswordField1)))
        .addContainerGap(222, Short.MAX_VALUE)));
layout.setVerticalGroup(
    layout.createParallelGroup(GroupLayout.Alignment.LEADING)
    .addGroup(layout.createSequentialGroup()
        .addContainerGap()
        .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
            .addComponent(jLabel1)
            .addComponent(jTextField1))
        .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
        .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
            .addComponent(jLabel2)
            .addComponent(jPasswordField1))
        .addContainerGap(244, Short.MAX_VALUE)));

That looks a bit scary, but fortunately you don’t have to write this code. However, it is helpful to have a basic understanding of the layout actions so that you can spot errors. We will analyze the basic structure of the code. The API notes at the end of this section explain each of the classes and methods in detail.

Components are organized by placing them into objects of type GroupLayout.SequentialGroup or GroupLayout.ParallelGroup. These classes are subclasses of GroupLayout.Group. Groups can contain components, gaps, and nested groups. The various add methods of the group classes return the group object, so that method calls can be chained like this:

group.addComponent(...).addPreferredGap(...).addComponent(...);

As you can see from the sample code, the group layout separates the horizontal and vertical layout computations.

To visualize the horizontal computations, imagine that the components are flattened so they have zero height, like this:

Image

There are two parallel sequences of components, corresponding to the (slightly simplified) code:

.addContainerGap()
 .addGroup(layout.createParallelGroup()
    .addGroup(layout.createSequentialGroup()
        .addComponent(jLabel1)
        .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
        .addComponent(jTextField1))
    .addGroup(layout.createSequentialGroup()
        .addComponent(jLabel2)
        .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
        .addComponent(jPasswordField1)))

But wait, that can’t be right. If the labels have different lengths, the text field and the password field won’t line up.

We have to tell Matisse that we want the fields to line up. Select both fields, right-click, and select Align → Left to Column from the menu. Also line up the labels (see Figure 12.32).

Image

Figure 12.32 Aligning the labels and text fields in Matisse

This dramatically changes the layout code:

.addGroup(layout.createSequentialGroup()
    .addContainerGap()
    .addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
        .addComponent(jLabel1, GroupLayout.Alignment.TRAILING)
        .addComponent(jLabel2, GroupLayout.Alignment.TRAILING))
    .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
    .addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
        .addComponent(jTextField1)
        .addComponent(jPasswordField1))

Now the labels and fields are each placed in a parallel group. The first group has an alignment of TRAILING (which means alignment to the right when the text direction is left-to-right):

Image

It seems like magic that Matisse can translate the designer’s instructions into nested groups—but, as Arthur C. Clarke said, any sufficiently advanced technology is indistinguishable from magic.

For completeness, let’s look at the vertical computation. Now you should think of the components as having no width. We have a sequential group that contains two parallel groups, separated by gaps:

Image

The corresponding code is

layout.createSequentialGroup()
    .addContainerGap()
    .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
        .addComponent(jLabel1)
        .addComponent(jTextField1))
    .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
    .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
        .addComponent(jLabel2)
        .addComponent(jPasswordField1))

As you can see from the code, the components are aligned by their baselines. (The baseline is the line on which the component text is aligned.)

You can force a set of components to have equal size. For example, we may want to make sure that the widths of the text field and the password field match exactly. In Matisse, select both, right-click, and select Same Size → Same Width from the menu (see Figure 12.33).

Image

Figure 12.33 Forcing two components to have the same width

Matisse adds the following statement to the layout code:

layout.linkSize(SwingConstants.HORIZONTAL, new Component[] {jPasswordField1, jTextField1});

The code in Listing 12.12 shows how to lay out the font selector of the preceding section using the GroupLayout instead of the GridBagLayout. The code may not look any simpler than that of Listing 12.10, but we didn’t have to write it. We used Matisse to do the layout and then cleaned up the code a bit.

Listing 12.12 groupLayout/FontFrame.java


 1   package groupLayout;
 2
 3   import java.awt.Font;
 4   import java.awt.event.ActionListener;
 5
 6   import javax.swing.BorderFactory;
 7   import javax.swing.GroupLayout;
 8   import javax.swing.JCheckBox;
 9   import javax.swing.JComboBox;
10   import javax.swing.JFrame;
11   import javax.swing.JLabel;
12   import javax.swing.JScrollPane;
13   import javax.swing.JTextArea;
14   import javax.swing.LayoutStyle;
15   import javax.swing.SwingConstants;
16
17   /**
18    * A frame that uses a group layout to arrange font selection components.
19    */
20   public class FontFrame extends JFrame
21   {
22      public static final int TEXT_ROWS = 10;
23      public static final int TEXT_COLUMNS = 20;
24
25      private JComboBox<String> face;
26      private JComboBox<Integer> size;
27      private JCheckBox bold;
28      private JCheckBox italic;
29      private JScrollPane pane;
30      private JTextArea sample;
31
32      public FontFrame()
33      {
34         ActionListener listener = event -> updateSample();
35
36         // construct components
37
38         JLabel faceLabel = new JLabel("Face: ");
39
40         face = new JComboBox<>(new String[] { "Serif", "SansSerif", "Monospaced", "Dialog",
41               "DialogInput" });
42
43         face.addActionListener(listener);
44
45         JLabel sizeLabel = new JLabel("Size: ");
46
47         size = new JComboBox<>(new Integer[] { 8, 10, 12, 15, 18, 24, 36, 48 });
48
49         size.addActionListener(listener);
50
51         bold = new JCheckBox("Bold");
52         bold.addActionListener(listener);
53
54         italic = new JCheckBox("Italic");
55         italic.addActionListener(listener);
56
57         sample = new JTextArea(TEXT_ROWS, TEXT_COLUMNS);
58         sample.setText("The quick brown fox jumps over the lazy dog");
59         sample.setEditable(false);
60         sample.setLineWrap(true);
61         sample.setBorder(BorderFactory.createEtchedBorder());
62
63         pane = new JScrollPane(sample);
64
65         GroupLayout layout = new GroupLayout(getContentPane());
66         setLayout(layout);
67         layout.setHorizontalGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
68               .addGroup(
69                     layout.createSequentialGroup().addContainerGap().addGroup(
70                           layout.createParallelGroup(GroupLayout.Alignment.LEADING).addGroup(
71                                 GroupLayout.Alignment.TRAILING,
72                                 layout.createSequentialGroup().addGroup(
73                                       layout.createParallelGroup(GroupLayout.Alignment.TRAILING)
74                                             .addComponent(faceLabel).addComponent(sizeLabel))
75                                       .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
76                                       .addGroup(
77                                             layout.createParallelGroup(
78                                                   GroupLayout.Alignment.LEADING, false)
79                                                   .addComponent(size).addComponent(face)))
80                                 .addComponent(italic).addComponent(bold)).addPreferredGap(
81                           LayoutStyle.ComponentPlacement.RELATED).addComponent(pane)
82                           .addContainerGap()));
83
84         layout.linkSize(SwingConstants.HORIZONTAL, new java.awt.Component[] { face, size });
85
86         layout.setVerticalGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
87               .addGroup(
88                     layout.createSequentialGroup().addContainerGap().addGroup(
89                           layout.createParallelGroup(GroupLayout.Alignment.LEADING).addComponent(
90                                 pane, GroupLayout.Alignment.TRAILING).addGroup(
91                                 layout.createSequentialGroup().addGroup(
92                                       layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
93                                             .addComponent(face).addComponent(faceLabel))
94                                       .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
95                                       .addGroup(
96                                             layout.createParallelGroup(
97                                                   GroupLayout.Alignment.BASELINE).addComponent(size)
98                                                   .addComponent(sizeLabel)).addPreferredGap(
99                                             LayoutStyle.ComponentPlacement.RELATED).addComponent(
100                                            italic, GroupLayout.DEFAULT_SIZE,
101                                            GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
102                                      .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
103                                      .addComponent(bold, GroupLayout.DEFAULT_SIZE,
104                                            GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)))
105                          .addContainerGap()));
106        pack();
107     }
108
109     public void updateSample()
110     {
111        String fontFace = (String) face.getSelectedItem();
112        int fontStyle = (bold.isSelected() ? Font.BOLD : 0)
113              + (italic.isSelected() ? Font.ITALIC : 0);
114        int fontSize = size.getItemAt(size.getSelectedIndex());
115        Font font = new Font(fontFace, fontStyle, fontSize);
116        sample.setFont(font);
117        sample.repaint();
118     }
119  }


12.6.3 Using No Layout Manager

There will be times when you don’t want to bother with layout managers but just want to drop a component at a fixed location (sometimes called absolute positioning). This is not a great idea for platform-independent applications, but there is nothing wrong with using it for a quick prototype.

Here is what you do to place a component at a fixed location:

1. Set the layout manager to null.

2. Add the component you want to the container.

3. Specify the position and size that you want:

frame.setLayout(null);
JButton ok = new JButton("OK");
frame.add(ok);
ok.setBounds(10, 10, 30, 15);

12.6.4 Custom Layout Managers

You can design your own LayoutManager class that manages components in a special way. As a fun example, let’s arrange all components in a container to form a circle (see Figure 12.34).

Image

Figure 12.34 Circle layout

Your own layout manager must implement the LayoutManager interface. You need to override the following five methods:

void addLayoutComponent(String s, Component c);
void removeLayoutComponent(Component c);
Dimension preferredLayoutSize(Container parent);
Dimension minimumLayoutSize(Container parent);
void layoutContainer(Container parent);

The first two methods are called when a component is added or removed. If you don’t keep any additional information about the components, you can make them do nothing. The next two methods compute the space required for the minimum and the preferred layout of the components. These are usually the same quantity. The fifth method does the actual work and invokes setBounds on all components.


Image Note

The AWT has a second interface, called LayoutManager2, with ten methods to implement rather than five. The main point of the LayoutManager2 interface is to allow you to use the add method with constraints. For example, the BorderLayout and GridBagLayout implement the LayoutManager2 interface.


Listing 12.13 shows the code for the CircleLayout manager which, uselessly enough, lays out the components along a circle inside the parent. The frame class of the sample program is in Listing 12.14.

Listing 12.13 circleLayout/CircleLayout.java


 1   package circleLayout;
 2
 3   import java.awt.*;
 4
 5   /**
 6    * A layout manager that lays out components along a circle.
 7    */
 8   public class CircleLayout implements LayoutManager
 9   {
10      private int minWidth = 0;
11      private int minHeight = 0;
12      private int preferredWidth = 0;
13      private int preferredHeight = 0;
14      private boolean sizesSet = false;
15      private int maxComponentWidth = 0;
16      private int maxComponentHeight = 0;
17
18      public void addLayoutComponent(String name, Component comp)
19      {
20      }
21
22      public void removeLayoutComponent(Component comp)
23      {
24      }
25
26      public void setSizes(Container parent)
27      {
28         if (sizesSet) return;
29         int n = parent.getComponentCount();
30
31         preferredWidth = 0;
32         preferredHeight = 0;
33         minWidth = 0;
34         minHeight = 0;
35         maxComponentWidth = 0;
36         maxComponentHeight = 0;
37
38         // compute the maximum component widths and heights
39         // and set the preferred size to the sum of the component sizes.
40         for (int i = 0; i < n; i++)
41         {
42            Component c = parent.getComponent(i);
43            if (c.isVisible())
44            {
45               Dimension d = c.getPreferredSize();
46               maxComponentWidth = Math.max(maxComponentWidth, d.width);
47               maxComponentHeight = Math.max(maxComponentHeight, d.height);
48               preferredWidth += d.width;
49               preferredHeight += d.height;
50            }
51         }
52         minWidth = preferredWidth / 2;
53         minHeight = preferredHeight / 2;
54         sizesSet = true;
55      }
56
57      public Dimension preferredLayoutSize(Container parent)
58      {
59         setSizes(parent);
60         Insets insets = parent.getInsets();
61         int width = preferredWidth + insets.left + insets.right;
62         int height = preferredHeight + insets.top + insets.bottom;
63         return new Dimension(width, height);
64      }
65
66      public Dimension minimumLayoutSize(Container parent)
67      {
68         setSizes(parent);
69         Insets insets = parent.getInsets();
70         int width = minWidth + insets.left + insets.right;
71         int height = minHeight + insets.top + insets.bottom;
72         return new Dimension(width, height);
73      }
74
75      public void layoutContainer(Container parent)
76      {
77         setSizes(parent);
78
79         // compute center of the circle
80
81         Insets insets = parent.getInsets();
82         int containerWidth = parent.getSize().width - insets.left - insets.right;
83         int containerHeight = parent.getSize().height - insets.top - insets.bottom;
84
85         int xcenter = insets.left + containerWidth / 2;
86         int ycenter = insets.top + containerHeight / 2;
87
88         // compute radius of the circle
89
90         int xradius = (containerWidth - maxComponentWidth) / 2;
91         int yradius = (containerHeight - maxComponentHeight) / 2;
92         int radius = Math.min(xradius, yradius);
93
94         // lay out components along the circle
95
96         int n = parent.getComponentCount();
97         for (int i = 0; i < n; i++)
98         {
99            Component c = parent.getComponent(i);
100           if (c.isVisible())
101           {
102              double angle = 2 * Math.PI * i / n;
103
104              // center point of component
105              int x = xcenter + (int) (Math.cos(angle) * radius);
106              int y = ycenter + (int) (Math.sin(angle) * radius);
107
108              // move component so that its center is (x, y)
109              // and its size is its preferred size
110              Dimension d = c.getPreferredSize();
111              c.setBounds(x - d.width / 2, y - d.height / 2, d.width, d.height);
112           }
113         }
114      }
115   }


Listing 12.14 circleLayout/CircleLayoutFrame.java


 1   package circleLayout;
 2
 3   import javax.swing.*;
 4
 5   /**
 6    * A frame that shows buttons arranged along a circle.
 7    */
 8   public class CircleLayoutFrame extends JFrame
 9   {
10      public CircleLayoutFrame()
11      {
12         setLayout(new CircleLayout());
13         add(new JButton("Yellow"));
14         add(new JButton("Blue"));
15         add(new JButton("Red"));
16         add(new JButton("Green"));
17         add(new JButton("Orange"));
18         add(new JButton("Fuchsia"));
19         add(new JButton("Indigo"));
20         pack();
21      }
22   }


12.6.5 Traversal Order

When you add many components into a window, you need to give some thought to the traversal order. When a window is first displayed, the first component in the traversal order has the keyboard focus. Each time the user presses the Tab key, the next component gains focus. (Recall that a component that has the keyboard focus can be manipulated with the keyboard. For example, a button can be “clicked” with the space bar when it has focus.) You may not personally care about using the Tab key to navigate through a set of controls, but plenty of users do. Among them are the mouse haters and those who cannot use a mouse, perhaps because of a handicap or because they are navigating the user interface by voice. For that reason, you need to know how Swing handles traversal order.

The traversal order is straightforward: first, left to right, and then, top to bottom. For example, in the font dialog example, the components are traversed in the following order (see Figure 12.35):

1. Face combo box

2. Sample text area (press Ctrl+Tab to move to the next field; the Tab character is considered text input)

3. Size combo box

4. Bold checkbox

5. Italic checkbox

Image

Figure 12.35 Geometric traversal order

The situation is more complex if your container contains other containers. When the focus is given to another container, it automatically ends up within the top left component in that container and then traverses all other components in that container. Finally, the focus is given to the component following the container.

You can use this to your advantage by grouping related elements in another container such as a panel.


Image Note: Call

component.setFocusable(false);

to remove a component from the focus traversal. This is useful for painted components that don’t take keyboard input.


12.7 Dialog Boxes

So far, all our user interface components have appeared inside a frame window that was created in the application. This is the most common situation if you write applets that run inside a web browser. But if you write applications, you usually want separate dialog boxes to pop up to give information to, or get information from, the user.

Just as with most windowing systems, AWT distinguishes between modal and modeless dialog boxes. A modal dialog box won’t let users interact with the remaining windows of the application until he or she deals with it. Use a modal dialog box when you need information from the user before you can proceed with execution. For example, when the user wants to read a file, a modal file dialog box is the one to pop up. The user must specify a file name before the program can begin the read operation. Only when the user closes the modal dialog box can the application proceed.

A modeless dialog box lets the user enter information in both the dialog box and the remainder of the application. One example of a modeless dialog is a toolbar. The toolbar can stay in place as long as needed, and the user can interact with both the application window and the toolbar as needed.

We will start this section with the simplest dialogs—modal dialogs with just a single message. Swing has a convenient JOptionPane class that lets you put up a simple dialog without writing any special dialog box code. Next, you will see how to write more complex dialogs by implementing your own dialog windows. Finally, you will see how to transfer data from your application into a dialog and back.

We’ll conclude this section by looking at two standard dialogs: file dialogs and color dialogs. File dialogs are complex, and you definitely want to be familiar with the Swing JFileChooser for this purpose—it would be a real challenge to write your own. The JColorChooser dialog is useful when you want users to pick colors.

12.7.1 Option Dialogs

Swing has a set of ready-made simple dialogs that suffice to ask the user for a single piece of information. The JOptionPane has four static methods to show these simple dialogs:

Image

Figure 12.36 shows a typical dialog. As you can see, the dialog has the following components:

• An icon

• A message

• One or more option buttons

Image

Figure 12.36 An option dialog

The input dialog has an additional component for user input. This can be a text field into which the user can type an arbitrary string, or a combo box from which the user can select one item.

The exact layout of these dialogs and the choice of icons for standard message types depend on the pluggable look-and-feel.

The icon on the left side depends on one of five message types:

ERROR_MESSAGE
INFORMATION_MESSAGE
WARNING_MESSAGE
QUESTION_MESSAGE
PLAIN_MESSAGE

The PLAIN_MESSAGE type has no icon. Each dialog type also has a method that lets you supply your own icon instead.

For each dialog type, you can specify a message. This message can be a string, an icon, a user interface component, or any other object. Here is how the message object is displayed:

Image

You can see these options by running the program in Listing 12.15.

Of course, supplying a message string is by far the most common case. Supplying a Component gives you ultimate flexibility because you can make the paintComponent method draw anything you want.

The buttons at the bottom depend on the dialog type and the option type. When calling showMessageDialog and showInputDialog, you get only a standard set of buttons (OK and OK/Cancel, respectively). When calling showConfirmDialog, you can choose among four option types:

DEFAULT_OPTION
YES_NO_OPTION
YES_NO_CANCEL_OPTION
OK_CANCEL_OPTION

With the showOptionDialog you can specify an arbitrary set of options. You supply an array of objects for the options. Each array element is rendered as follows:

Image

The return values of these functions are as follows:

Image

The showConfirmDialog and showOptionDialog return integers to indicate which button the user chose. For the option dialog, this is simply the index of the chosen option or the value CLOSED_OPTION if the user closed the dialog instead of choosing an option. For the confirmation dialog, the return value can be one of the following:

OK_OPTION
CANCEL_OPTION
YES_OPTION
NO_OPTION
CLOSED_OPTION

This all sounds like a bewildering set of choices, but in practice it is simple. Follow these steps:

1. Choose the dialog type (message, confirmation, option, or input).

2. Choose the icon (error, information, warning, question, none, or custom).

3. Choose the message (string, icon, custom component, or a stack of them).

4. For a confirmation dialog, choose the option type (default, Yes/No, Yes/No/Cancel, or OK/Cancel).

5. For an option dialog, choose the options (strings, icons, or custom components) and the default option.

6. For an input dialog, choose between a text field and a combo box.

7. Locate the appropriate method to call in the JOptionPane API.

For example, suppose you want to show the dialog in Figure 12.36. The dialog shows a message and asks the user to confirm or cancel. Thus, it is a confirmation dialog. The icon is a question icon. The message is a string. The option type is OK_CANCEL_OPTION. Here is the call you would make:

int selection = JOptionPane.showConfirmDialog(parent,
   "Message", "Title",
   JOptionPane.OK_CANCEL_OPTION,
   JOptionPane.QUESTION_MESSAGE);
if (selection == JOptionPane.OK_OPTION) . . .


Image Tip

The message string can contain newline ('\n') characters. Such a string is displayed in multiple lines.


The program whose frame class is shown in Listing 12.15 displays six button panels (see Figure 12.37). Listing 12.16 shows the class for the panels. When you click the Show button, the selected dialog is displayed.

Listing 12.15 optionDialog/OptionDialogFrame.java


 1   package optionDialog;
 2
 3   import java.awt.*;
 4   import java.awt.event.*;
 5   import java.awt.geom.*;
 6   import java.util.*;
 7   import javax.swing.*;
 8
 9   /**
10    * A frame that contains settings for selecting various option dialogs.
11    */
12   public class OptionDialogFrame extends JFrame
13   {
14      private ButtonPanel typePanel;
15      private ButtonPanel messagePanel;
16      private ButtonPanel messageTypePanel;
17      private ButtonPanel optionTypePanel;
18      private ButtonPanel optionsPanel;
19      private ButtonPanel inputPanel;
20      private String messageString = "Message";
21      private Icon messageIcon = new ImageIcon("blue-ball.gif");
22      private Object messageObject = new Date();
23      private Component messageComponent = new SampleComponent();
24
25      public OptionDialogFrame()
26      {
27         JPanel gridPanel = new JPanel();
28         gridPanel.setLayout(new GridLayout(2, 3));
29
30         typePanel = new ButtonPanel("Type", "Message", "Confirm", "Option", "Input");
31         messageTypePanel = new ButtonPanel("Message Type", "ERROR_MESSAGE", "INFORMATION_MESSAGE",
32               "WARNING_MESSAGE", "QUESTION_MESSAGE", "PLAIN_MESSAGE");
33         messagePanel = new ButtonPanel("Message", "String", "Icon", "Component", "Other",
34               "Object[]");
35         optionTypePanel = new ButtonPanel("Confirm", "DEFAULT_OPTION", "YES_NO_OPTION",
36               "YES_NO_CANCEL_OPTION", "OK_CANCEL_OPTION");
37         optionsPanel = new ButtonPanel("Option", "String[]", "Icon[]", "Object[]");
38         inputPanel = new ButtonPanel("Input", "Text field", "Combo box");
39
40         gridPanel.add(typePanel);
41         gridPanel.add(messageTypePanel);
42         gridPanel.add(messagePanel);
43         gridPanel.add(optionTypePanel);
44         gridPanel.add(optionsPanel);
45         gridPanel.add(inputPanel);
46
47         // add a panel with a Show button
48
49         JPanel showPanel = new JPanel();
50         JButton showButton = new JButton("Show");
51         showButton.addActionListener(new ShowAction());
52         showPanel.add(showButton);
53
54         add(gridPanel, BorderLayout.CENTER);
55         add(showPanel, BorderLayout.SOUTH);
56         pack();
57      }
58
59      /**
60       * Gets the currently selected message.
61       * @return a string, icon, component, or object array, depending on the Message panel selection
62       */
63      public Object getMessage()
64      {
65         String s = messagePanel.getSelection();
66         if (s.equals("String")) return messageString;
67         else if (s.equals("Icon")) return messageIcon;
68         else if (s.equals("Component")) return messageComponent;
69         else if (s.equals("Object[]")) return new Object[] { messageString, messageIcon,
70               messageComponent, messageObject };
71         else if (s.equals("Other")) return messageObject;
72         else return null;
73      }
74
75      /**
76       * Gets the currently selected options.
77       * @return an array of strings, icons, or objects, depending on the Option panel selection
78       */
79      public Object[] getOptions()
80      {
81         String s = optionsPanel.getSelection();
82         if (s.equals("String[]")) return new String[] { "Yellow", "Blue", "Red" };
83         else if (s.equals("Icon[]")) return new Icon[] { new ImageIcon("yellow-ball.gif"),
84               new ImageIcon("blue-ball.gif"), new ImageIcon("red-ball.gif") };
85         else if (s.equals("Object[]")) return new Object[] { messageString, messageIcon,
86               messageComponent, messageObject };
87         else return null;
88      }
89
90      /**
91       * Gets the selected message or option type
92       * @param panel the Message Type or Confirm panel
93       * @return the selected XXX_MESSAGE or XXX_OPTION constant from the JOptionPane class
94       */
95      public int getType(ButtonPanel panel)
96      {
97         String s = panel.getSelection();
98         try
99         {
100            return JOptionPane.class.getField(s).getInt(null);
101        }
102        catch (Exception e)
103        {
104           return -1;
105        }
106     }
107
108     /**
109      * The action listener for the Show button shows a Confirm, Input, Message, or Option dialog
110      * depending on the Type panel selection.
111      */
112     private class ShowAction implements ActionListener
113     {
114        public void actionPerformed(ActionEvent event)
115        {
116           if (typePanel.getSelection().equals("Confirm")) JOptionPane.showConfirmDialog(
117                 OptionDialogFrame.this, getMessage(), "Title", getType(optionTypePanel),
118                 getType(messageTypePanel));
119           else if (typePanel.getSelection().equals("Input"))
120           {
121              if (inputPanel.getSelection().equals("Text field")) JOptionPane.showInputDialog(
122                    OptionDialogFrame.this, getMessage(), "Title", getType(messageTypePanel));
123              else JOptionPane.showInputDialog(OptionDialogFrame.this, getMessage(), "Title",
124                    getType(messageTypePanel), null, new String[] { "Yellow", "Blue", "Red" },
125                    "Blue");
126           }
127           else if (typePanel.getSelection().equals("Message")) JOptionPane.showMessageDialog(
128                 OptionDialogFrame.this, getMessage(), "Title", getType(messageTypePanel));
129           else if (typePanel.getSelection().equals("Option")) JOptionPane.showOptionDialog(
130                 OptionDialogFrame.this, getMessage(), "Title", getType(optionTypePanel),
131                 getType(messageTypePanel), null, getOptions(), getOptions()[0]);
132        }
133     }
134  }
135
136  /**
137   * A component with a painted surface
138   */
139
140  class SampleComponent extends JComponent
141  {
142     public void paintComponent(Graphics g)
143     {
144        Graphics2D g2 = (Graphics2D) g;
145        Rectangle2D rect = new Rectangle2D.Double(0, 0, getWidth() - 1, getHeight() - 1);
146        g2.setPaint(Color.YELLOW);
147        g2.fill(rect);
148        g2.setPaint(Color.BLUE);
149        g2.draw(rect);
150     }
151
152     public Dimension getPreferredSize()
153     {
154        return new Dimension(10, 10);
155     }
156  }


Listing 12.16 optionDialog/ButtonPanel.java


 1   package optionDialog;
 2
 3   import javax.swing.*;
 4
 5   /**
 6    * A panel with radio buttons inside a titled border.
 7    */
 8   public class ButtonPanel extends JPanel
 9   {
10      private ButtonGroup group;
11
12      /**
13       * Constructs a button panel.
14       * @param title the title shown in the border
15       * @param options an array of radio button labels
16       */
17      public ButtonPanel(String title, String... options)
18      {
19         setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), title));
20         setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
21         group = new ButtonGroup();
22
23         // make one radio button for each option
24         for (String option : options)
25         {
26            JRadioButton b = new JRadioButton(option);
27            b.setActionCommand(option);
28            add(b);
29            group.add(b);
30            b.setSelected(option == options[0]);
31         }
32      }
33
34      /**
35       * Gets the currently selected option.
36       * @return the label of the currently selected radio button.
37       */
38      public String getSelection()
39      {
40         return group.getSelection().getActionCommand();
41      }
42   }


Image

Figure 12.37 The OptionDialogTest program

12.7.2 Creating Dialogs

In the last section, you saw how to use the JOptionPane class to show a simple dialog. In this section, you will see how to create such a dialog by hand.

Figure 12.38 shows a typical modal dialog box—a program information box that is displayed when the user clicks the About button.

Image

Figure 12.38 An About dialog box

To implement a dialog box, you extend the JDialog class. This is essentially the same process as extending JFrame for the main window for an application. More precisely:

1. In the constructor of your dialog box, call the constructor of the superclass JDialog.

2. Add the user interface components of the dialog box.

3. Add the event handlers.

4. Set the size for the dialog box.

When you call the superclass constructor, you will need to supply the owner frame, the title of the dialog, and the modality.

The owner frame controls where the dialog is displayed. You can supply null as the owner; then, the dialog is owned by a hidden frame.

The modality specifies which other windows of your application are blocked while the dialog is displayed. A modeless dialog does not block other windows. A modal dialog blocks all other windows of the application (except for the children of the dialog). You would use a modeless dialog for a toolbox that the user can always access. On the other hand, you would use a modal dialog if you want to force the user to supply required information before continuing.


Image Note

As of Java SE 6, there are two additional modality types. A document-modal dialog blocks all windows belonging to the same “document,” or more precisely, all windows with the same parentless root window as the dialog. This solves a problem with help systems. In older versions, users were unable to interact with the help windows when a modal dialog was popped up. A toolkit-modal dialog blocks all windows from the same “toolkit.” A toolkit is a Java program that launches multiple applications, such as the applet engine in a browser. For more information on these advanced issues, see www.oracle.com/technetwork/articles/javase/modality-137604.html.


Here’s the code for a dialog box:

public AboutDialog extends JDialog
{
   public AboutDialog(JFrame owner)
   {
      super(owner, "About DialogTest", true);
      add(new JLabel(
         "<html><h1><i>Core Java</i></h1><hr>By Cay Horstmann</html>"),
         BorderLayout.CENTER);

      JPanel panel = new JPanel();
      JButton ok = new JButton("OK");

      ok.addActionListener(event -> setVisible(false));
      panel.add(ok);
      add(panel, BorderLayout.SOUTH);
      setSize(250, 150);
   }
}

As you can see, the constructor adds user interface elements—in this case, labels and a button. It adds a handler to the button and sets the size of the dialog.

To display the dialog box, create a new dialog object and make it visible:

JDialog dialog = new AboutDialog(this);
dialog.setVisible(true);

Actually, in the sample code below, we create the dialog box only once, and we can reuse it whenever the user clicks the About button.

if (dialog == null) // first time
   dialog = new AboutDialog(this);
dialog.setVisible(true);

When the user clicks the OK button, the dialog box should close. This is handled in the event handler of the OK button:

ok.addActionListener(event -> setVisible(false));

When the user closes the dialog by clicking the Close button, the dialog is also hidden. Just as with a JFrame, you can override this behavior with the setDefaultCloseOperation method.

Listing 12.17 is the code for the frame class of the test program. Listing 12.18 shows the dialog class.

Listing 12.17 dialog/DialogFrame.java


 1   package dialog;
 2
 3   import javax.swing.JFrame;
 4   import javax.swing.JMenu;
 5   import javax.swing.JMenuBar;
 6   import javax.swing.JMenuItem;
 7
 8   /**
 9    * A frame with a menu whose File->About action shows a dialog.
10    */
11   public class DialogFrame extends JFrame
12   {
13      private static final int DEFAULT_WIDTH = 300;
14      private static final int DEFAULT_HEIGHT = 200;
15      private AboutDialog dialog;
16
17      public DialogFrame()
18      {
19         setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
20
21         // Construct a File menu.
22
23         JMenuBar menuBar = new JMenuBar();
24         setJMenuBar(menuBar);
25         JMenu fileMenu = new JMenu("File");
26         menuBar.add(fileMenu);
27
28         // Add About and Exit menu items.
29
30         // The About item shows the About dialog.
31
32         JMenuItem aboutItem = new JMenuItem("About");
33         aboutItem.addActionListener(event -> {
34            if (dialog == null) // first time
35               dialog = new AboutDialog(DialogFrame.this);
36            dialog.setVisible(true); // pop up dialog
37         });
38         fileMenu.add(aboutItem);
39
40         // The Exit item exits the program.
41
42         JMenuItem exitItem = new JMenuItem("Exit");
43         exitItem.addActionListener(event -> System.exit(0));
44         fileMenu.add(exitItem);
45      }
46   }


Listing 12.18 dialog/AboutDialog.java


 1   package dialog;
 2
 3   import java.awt.BorderLayout;
 4
 5   import javax.swing.JButton;
 6   import javax.swing.JDialog;
 7   import javax.swing.JFrame;
 8   import javax.swing.JLabel;
 9   import javax.swing.JPanel;
10
11   /**
12    * A sample modal dialog that displays a message and waits for the user to click the OK button.
13    */
14   public class AboutDialog extends JDialog
15   {
16      public AboutDialog(JFrame owner)
17      {
18         super(owner, "About DialogTest", true);
19
20         // add HTML label to center
21
22         add(
23               new JLabel(
24                     "<html><h1><i>Core Java</i></h1><hr>By Cay Horstmann</html>"),
25               BorderLayout.CENTER);
26
27         // OK button closes the dialog
28
29         JButton ok = new JButton("OK");
30         ok.addActionListener(event -> setVisible(false));
31
32         // add OK button to southern border
33
34         JPanel panel = new JPanel();
35         panel.add(ok);
36         add(panel, BorderLayout.SOUTH);
37
38         pack();
39      }
40   }


12.7.3 Data Exchange

The most common reason to put up a dialog box is to get information from the user. You have already seen how easy it is to make a dialog box object: Give it initial data and call setVisible(true) to display the dialog box on the screen. Now let’s see how to transfer data in and out of a dialog box.

Consider the dialog box in Figure 12.39 that could be used to obtain a user name and a password to connect to some online service.

Image

Figure 12.39 Password dialog box

Your dialog box should provide methods to set default data. For example, the PasswordChooser class of the example program has a method, setUser, to place default values into the next fields:

public void setUser(User u)
{
   username.setText(u.getName());
}

Once you set the defaults (if desired), show the dialog by calling setVisible(true). The dialog is now displayed.

The user then fills in the information and clicks the OK or Cancel button. The event handlers for both buttons call setVisible(false), which terminates the call to setVisible(true). Alternatively, the user may close the dialog. If you did not install a window listener for the dialog, the default window closing operation applies: The dialog becomes invisible, which also terminates the call to setVisible(true).

The important issue is that the call to setVisible(true) blocks until the user has dismissed the dialog. This makes it easy to implement modal dialogs.

You want to know whether the user has accepted or canceled the dialog. Our sample code sets the ok flag to false before showing the dialog. Only the event handler for the OK button sets the ok flag to true; that’s how you retrieve the user input from the dialog.


Image Note

Transferring data out of a modeless dialog is not as simple. When a modeless dialog is displayed, the call to setVisible(true) does not block and the program continues running while the dialog is displayed. If the user selects items on a modeless dialog and then clicks OK, the dialog needs to send an event to some listener in the program.


The example program contains another useful improvement. When you construct a JDialog object, you need to specify the owner frame. However, quite often you want to show the same dialog with different owner frames. It is better to pick the owner frame when you are ready to show the dialog, not when you construct the PasswordChooser object.

The trick is to have the PasswordChooser extend JPanel instead of JDialog. Build a JDialog object on the fly in the showDialog method:

public boolean showDialog(Frame owner, String title)
{
   ok = false;

   if (dialog == null || dialog.getOwner() != owner)
   {
      dialog = new JDialog(owner, true);
      dialog.add(this);
      dialog.pack();
   }

   dialog.setTitle(title);
   dialog.setVisible(true);
   return ok;
}

Note that it is safe to have owner equal to null.

You can do even better. Sometimes, the owner frame isn’t readily available. It is easy enough to compute it from any parent component, like this:

Frame owner;
if (parent instanceof Frame)
   owner = (Frame) parent;
else
   owner = (Frame) SwingUtilities.getAncestorOfClass(Frame.class, parent);

We use this enhancement in our sample program. The JOptionPane class also uses this mechanism.

Many dialogs have a default button, which is automatically selected if the user presses a trigger key (Enter in most look-and-feel implementations). The default button is specially marked, often with a thick outline.

Set the default button in the root pane of the dialog:

dialog.getRootPane().setDefaultButton(okButton);

If you follow our suggestion of laying out the dialog in a panel, then you must be careful to set the default button only after you wrapped the panel into a dialog. The panel dialog itself has no root pane.

Listing 12.19 is for the frame class of the program that illustrates the data flow into and out of a dialog box. Listing 12.20 shows the dialog class.

Listing 12.19 dataExchange/DataExchangeFrame.java


 1   package dataExchange;
 2
 3   import java.awt.*;
 4   import java.awt.event.*;
 5   import javax.swing.*;
 6
 7   /**
 8    * A frame with a menu whose File->Connect action shows a password dialog.
 9    */
10   public class DataExchangeFrame extends JFrame
11   {
12      public static final int TEXT_ROWS = 20;
13      public static final int TEXT_COLUMNS = 40;
14      private PasswordChooser dialog = null;
15      private JTextArea textArea;
16
17      public DataExchangeFrame()
18      {
19         // construct a File menu
20
21         JMenuBar mbar = new JMenuBar();
22         setJMenuBar(mbar);
23         JMenu fileMenu = new JMenu("File");
24         mbar.add(fileMenu);
25
26         // add Connect and Exit menu items
27
28         JMenuItem connectItem = new JMenuItem("Connect");
29         connectItem.addActionListener(new ConnectAction());
30         fileMenu.add(connectItem);
31
32         // The Exit item exits the program
33
34         JMenuItem exitItem = new JMenuItem("Exit");
35         exitItem.addActionListener(event -> System.exit(0));
36         fileMenu.add(exitItem);
37
38         textArea = new JTextArea(TEXT_ROWS, TEXT_COLUMNS);
39         add(new JScrollPane(textArea), BorderLayout.CENTER);
40         pack();
41      }
42
43      /**
44       * The Connect action pops up the password dialog.
45       */
46      private class ConnectAction implements ActionListener
47      {
48         public void actionPerformed(ActionEvent event)
49         {
50            // if first time, construct dialog
51
52            if (dialog == null) dialog = new PasswordChooser();
53
54            // set default values
55            dialog.setUser(new User("yourname", null));
56
57            // pop up dialog
58            if (dialog.showDialog(DataExchangeFrame.this, "Connect"))
59            {
60               // if accepted, retrieve user input
61               User u = dialog.getUser();
62               textArea.append("user name = " + u.getName() + ", password = "
63                     + (new String(u.getPassword())) + "\n");
64            }
65         }
66      }
67   }


Listing 12.20 dataExchange/PasswordChooser.java


 1   package dataExchange;
 2
 3   import java.awt.BorderLayout;
 4   import java.awt.Component;
 5   import java.awt.Frame;
 6   import java.awt.GridLayout;
 7
 8   import javax.swing.JButton;
 9   import javax.swing.JDialog;
10   import javax.swing.JLabel;
11   import javax.swing.JPanel;
12   import javax.swing.JPasswordField;
13   import javax.swing.JTextField;
14   import javax.swing.SwingUtilities;
15
16   /**
17    * A password chooser that is shown inside a dialog
18    */
19   public class PasswordChooser extends JPanel
20   {
21      private JTextField username;
22      private JPasswordField password;
23      private JButton okButton;
24      private boolean ok;
25      private JDialog dialog;
26
27      public PasswordChooser()
28      {
29         setLayout(new BorderLayout());
30
31         // construct a panel with user name and password fields
32
33         JPanel panel = new JPanel();
34         panel.setLayout(new GridLayout(2, 2));
35         panel.add(new JLabel("User name:"));
36         panel.add(username = new JTextField(""));
37         panel.add(new JLabel("Password:"));
38         panel.add(password = new JPasswordField(""));
39         add(panel, BorderLayout.CENTER);
40
41         // create Ok and Cancel buttons that terminate the dialog
42
43         okButton = new JButton("Ok");
44         okButton.addActionListener(event -> {
45            ok = true;
46            dialog.setVisible(false);
47         });
48
49         JButton cancelButton = new JButton("Cancel");
50         cancelButton.addActionListener(event -> dialog.setVisible(false));
51
52         // add buttons to southern border
53
54         JPanel buttonPanel = new JPanel();
55         buttonPanel.add(okButton);
56         buttonPanel.add(cancelButton);
57         add(buttonPanel, BorderLayout.SOUTH);
58     }
59
60     /**
61      * Sets the dialog defaults.
62      * @param u the default user information
63      */
64     public void setUser(User u)
65     {
66        username.setText(u.getName());
67     }
68
69     /**
70      * Gets the dialog entries.
71      * @return a User object whose state represents the dialog entries
72      */
73     public User getUser()
74     {
75        return new User(username.getText(), password.getPassword());
76     }
77
78     /**
79      * Show the chooser panel in a dialog
80      * @param parent a component in the owner frame or null
81      * @param title the dialog window title
82      */
83     public boolean showDialog(Component parent, String title)
84     {
85        ok = false;
86
87        // locate the owner frame
88
89        Frame owner = null;
90        if (parent instanceof Frame)
91           owner = (Frame) parent;
92        else
93           owner = (Frame) SwingUtilities.getAncestorOfClass(Frame.class, parent);
94
95        // if first time, or if owner has changed, make new dialog
96
97        if (dialog == null || dialog.getOwner() != owner)
98        {
99           dialog = new JDialog(owner, true);
100          dialog.add(this);
101          dialog.getRootPane().setDefaultButton(okButton);
102          dialog.pack();
103       }
104
105       // set title and show dialog
106
107       dialog.setTitle(title);
108       dialog.setVisible(true);
109       return ok;
110     }
111  }


12.7.4 File Dialogs

In an application, you often want to be able to open and save files. A good file dialog box that shows files and directories and lets the user navigate the file system is hard to write, and you definitely don’t want to reinvent that wheel. Fortunately, Swing provides a JFileChooser class that allows you to display a file dialog box similar to the one that most native applications use. JFileChooser dialogs are always modal. Note that the JFileChooser class is not a subclass of JDialog. Instead of calling setVisible(true), call showOpenDialog to display a dialog for opening a file, or call showSaveDialog to display a dialog for saving a file. The button for accepting a file is then automatically labeled Open or Save. You can also supply your own button label with the showDialog method. Figure 12.40 shows an example of the file chooser dialog box.

Image

Figure 12.40 File chooser dialog box

Here are the steps to put up a file dialog box and recover what the user chooses from the box:

1. Make a JFileChooser object. Unlike the constructor for the JDialog class, you do not supply the parent component. This allows you to reuse a file chooser dialog with multiple frames.

For example:

JFileChooser chooser = new JFileChooser();


Image Tip

Reusing a file chooser object is a good idea because the JFileChooser constructor can be quite slow, especially on Windows when the user has many mapped network drives.


2. Set the directory by calling the setCurrentDirectory method.

For example, to use the current working directory

chooser.setCurrentDirectory(new File("."));

you need to supply a File object. File objects are explained in detail in Chapter 2 of Volume II. All you need to know for now is that the constructor File(String filename) turns a file or directory name into a File object.

3. If you have a default file name that you expect the user to choose, supply it with the setSelectedFile method:

chooser.setSelectedFile(new File(filename));

4. To enable the user to select multiple files in the dialog, call the setMultiSelectionEnabled method. This is, of course, entirely optional and not all that common.

chooser.setMultiSelectionEnabled(true);

5. If you want to restrict the display of files in the dialog to those of a particular type (for example, all files with extension .gif), you need to set a file filter. We discuss file filters later in this section.

6. By default, a user can select only files with a file chooser. If you want the user to select directories, use the setFileSelectionMode method. Call it with JFileChooser.FILES_ONLY (the default), JFileChooser.DIRECTORIES_ONLY, or JFileChooser.FILES_AND_DIRECTORIES.

7. Show the dialog box by calling the showOpenDialog or showSaveDialog method. You must supply the parent component in these calls:

int result = chooser.showOpenDialog(parent);

or

int result = chooser.showSaveDialog(parent);

The only difference between these calls is the label of the “approve button”—the button that the user clicks to finish the file selection. You can also call the showDialog method and pass an explicit text for the approve button:

int result = chooser.showDialog(parent, "Select");

These calls return only when the user has approved, canceled, or dismissed the file dialog. The return value is JFileChooser.APPROVE_OPTION, JFileChooser.CANCEL_OPTION, or JFileChooser.ERROR_OPTION.

8. Get the selected file or files with the getSelectedFile() or getSelectedFiles() method. These methods return either a single File object or an array of File objects. If you just need the name of the file object, call its getPath method. For example:

String filename = chooser.getSelectedFile().getPath();

For the most part, these steps are simple. The major difficulty with using a file dialog is to specify a subset of files from which the user should choose. For example, suppose the user should choose a GIF image file. Then, the file chooser should only display files with the extension .gif. It should also give the user some kind of feedback that the displayed files are of a particular category, such as “GIF Images.” But the situation can be more complex. If the user should choose a JPEG image file, the extension can be either .jpg or .jpeg. Instead of a way to codify these complexities, the designers of the file chooser povided a more elegant mechanism: to restrict the displayed files, supply an object that extends the abstract class javax.swing.filechooser.FileFilter. The file chooser passes each file to the file filter and displays only those files that the filter accepts.

At the time of this writing, two such subclasses are supplied: the default filter that accepts all files, and a filter that accepts all files with a given extension. However, it is easy to write ad-hoc file filters. You simply implement the two abstract methods of the FileFilter superclass:

public boolean accept(File f);
public String getDescription();

The first method tests whether a file should be accepted. The second method returns a description of the file type that can be displayed in the file chooser dialog.


Image Note

An unrelated FileFilter interface in the java.io package has a single method, boolean accept(File f). It is used in the listFiles method of the File class to list files in a directory. We do not know why the designers of Swing didn’t extend this interface—perhaps the Java class library has now become so complex that even the programmers at Sun were no longer aware of all the standard classes and interfaces.

You will need to resolve the name conflict between these two identically named types if you import both the java.io and the javax.swing.filechooser packages. The simplest remedy is to import javax.swing.filechooser.FileFilter, not javax.swing.filechooser.*.


Once you have a file filter object, use the setFileFilter method of the JFileChooser class to install it into the file chooser object:

chooser.setFileFilter(new FileNameExtensionFilter("Image files", "gif", "jpg"));

You can install multiple filters to the file chooser by calling

chooser.addChoosableFileFilter(filter1);
chooser.addChoosableFileFilter(filter2);
. . .

The user selects a filter from the combo box at the bottom of the file dialog. By default, the “All files” filter is always present in the combo box. This is a good idea—just in case a user of your program needs to select a file with a nonstandard extension. However, if you want to suppress the “All files” filter, call

chooser.setAcceptAllFileFilterUsed(false)


Image Caution

If you reuse a single file chooser for loading and saving different file types, call

chooser.resetChoosableFilters()

to clear any old file filters before adding new ones.


Finally, you can customize the file chooser by providing special icons and file descriptions for each file that the file chooser displays. Do this by supplying an object of a class extending the FileView class in the javax.swing.filechooser package. This is definitely an advanced technique. Normally, you don’t need to supply a file view—the pluggable look-and-feel supplies one for you. But if you want to show different icons for special file types, you can install your own file view. You need to extend the FileView class and implement five methods:

Icon getIcon(File f);
String getName(File f);
String getDescription(File f);
String getTypeDescription(File f);
Boolean isTraversable(File f);

Then, use the setFileView method to install your file view into the file chooser.

The file chooser calls your methods for each file or directory that it wants to display. If your method returns null for the icon, name, or description, the file chooser then consults the default file view of the look-and-feel. That is good, because it means you need to deal only with the file types for which you want to do something different.

The file chooser calls the isTraversable method to decide whether to open a directory when a user clicks on it. Note that this method returns a Boolean object, not a boolean value! This seems weird, but it is actually convenient—if you aren’t interested in deviating from the default file view, just return null. The file chooser will then consult the default file view. In other words, the method returns a Boolean to let you choose among three options: true (Boolean.TRUE), false (Boolean.FALSE), or don’t care (null).

The example program contains a simple file view class. That class shows a particular icon whenever a file matches a file filter. We use it to display a palette icon for all image files.

class FileIconView extends FileView
{
   private FileFilter filter;
   private Icon icon;

   public FileIconView(FileFilter aFilter, Icon anIcon)
   {
      filter = aFilter;
      icon = anIcon;
   }

   public Icon getIcon(File f)
   {
      if (!f.isDirectory() && filter.accept(f))
         return icon;
      else return null;
   }
}

Install this file view into your file chooser with the setFileView method:

chooser.setFileView(new FileIconView(filter,
   new ImageIcon("palette.gif")));

The file chooser will then show the palette icon next to all files that pass the filter and use the default file view to show all other files. Naturally, we use the same filter that we set in the file chooser.


Image Tip

You can find a more useful ExampleFileView class in the demo/jfc/FileChooserDemo directory of the JDK. That class lets you associate icons and descriptions with arbitrary extensions.


Finally, you can customize a file dialog by adding an accessory component. For example, Figure 12.41 shows a preview accessory next to the file list. This accessory displays a thumbnail view of the currently selected file.

Image

Figure 12.41 A file dialog with a preview accessory

An accessory can be any Swing component. In our case, we extend the JLabel class and set its icon to a scaled copy of the graphics image:

class ImagePreviewer extends JLabel
{
   public ImagePreviewer(JFileChooser chooser)
   {
      setPreferredSize(new Dimension(100, 100));
      setBorder(BorderFactory.createEtchedBorder());
   }

   public void loadImage(File f)
   {
      ImageIcon icon = new ImageIcon(f.getPath());
      if(icon.getIconWidth() > getWidth())
         icon = new ImageIcon(icon.getImage().getScaledInstance(
            getWidth(), -1, Image.SCALE_DEFAULT));
      setIcon(icon);
      repaint();
   }
}

There is just one challenge. We want to update the preview image whenever the user selects a different file. The file chooser uses the “JavaBeans” mechanism of notifying interested listeners whenever one of its properties changes. The selected file is a property that you can monitor by installing a PropertyChangeListener. We discuss this mechanism in greater detail in Chapter 11 of Volume II. Here is the code that you need to trap the notifications:

chooser.addPropertyChangeListener(event -> {
   if (event.getPropertyName() == JFileChooser.SELECTED_FILE_CHANGED_PROPERTY)
   {
      File newFile = (File) event.getNewValue();
      // update the accessory
      ...
   }
});

In our example program, we add this code to the ImagePreviewer constructor.

Listings 12.21 through 12.23 contain a modification of the ImageViewer program from Chapter 2, in which the file chooser has been enhanced by a custom file view and a preview accessory.

Listing 12.21 fileChooser/ImageViewerFrame.java


 1   package fileChooser;
 2
 3   import java.io.*;
 4
 5   import javax.swing.*;
 6   import javax.swing.filechooser.*;
 7   import javax.swing.filechooser.FileFilter;
 8
 9   /**
10    * A frame that has a menu for loading an image and a display area for the
11    * loaded image.
12    */
13   public class ImageViewerFrame extends JFrame
14   {
15      private static final int DEFAULT_WIDTH = 300;
16      private static final int DEFAULT_HEIGHT = 400;
17      private JLabel label;
18      private JFileChooser chooser;
19
20      public ImageViewerFrame()
21      {
22         setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
23
24         // set up menu bar
25         JMenuBar menuBar = new JMenuBar();
26         setJMenuBar(menuBar);
27
28         JMenu menu = new JMenu("File");
29         menuBar.add(menu);
30
31         JMenuItem openItem = new JMenuItem("Open");
32         menu.add(openItem);
33         openItem.addActionListener(event -> {
34            chooser.setCurrentDirectory(new File("."));
35
36            // show file chooser dialog
37               int result = chooser.showOpenDialog(ImageViewerFrame.this);
38
39               // if image file accepted, set it as icon of the label
40               if (result == JFileChooser.APPROVE_OPTION)
41               {
42                  String name = chooser.getSelectedFile().getPath();
43                  label.setIcon(new ImageIcon(name));
44                  pack();
45               }
46            });
47
48         JMenuItem exitItem = new JMenuItem("Exit");
49         menu.add(exitItem);
50         exitItem.addActionListener(event -> System.exit(0));
51
52         // use a label to display the images
53         label = new JLabel();
54         add(label);
55
56         // set up file chooser
57         chooser = new JFileChooser();
58
59         // accept all image files ending with .jpg, .jpeg, .gif
60         FileFilter filter = new FileNameExtensionFilter(
61               "Image files", "jpg", "jpeg", "gif");
62         chooser.setFileFilter(filter);
63
64         chooser.setAccessory(new ImagePreviewer(chooser));
65
66         chooser.setFileView(new FileIconView(filter, new ImageIcon("palette.gif")));
67      }
68   }


Listing 12.22 fileChooser/ImagePreviewer.java


 1    package fileChooser;
 2
 3    import java.awt.*;
 4    import java.io.*;
 5
 6    import javax.swing.*;
 7
 8    /**
 9     * A file chooser accessory that previews images.
10     */
11    public class ImagePreviewer extends JLabel
12    {
13       /**
14        * Constructs an ImagePreviewer.
15        * @param chooser the file chooser whose property changes trigger an image
16        *        change in this previewer
17        */
18       public ImagePreviewer(JFileChooser chooser)
19       {
20          setPreferredSize(new Dimension(100, 100));
21          setBorder(BorderFactory.createEtchedBorder());
22
23          chooser.addPropertyChangeListener(event -> {
24             if (event.getPropertyName() == JFileChooser.SELECTED_FILE_CHANGED_PROPERTY)
25             {
26                // the user has selected a new file
27                File f = (File) event.getNewValue();
28                if (f == null)
29                {
30                   setIcon(null);
31                   return;
32                }
33
34                // read the image into an icon
35                ImageIcon icon = new ImageIcon(f.getPath());
36
37                // if the icon is too large to fit, scale it
38                if (icon.getIconWidth() > getWidth())
39                   icon = new ImageIcon(icon.getImage().getScaledInstance(
40                         getWidth(), -1, Image.SCALE_DEFAULT));
41
42                setIcon(icon);
43             }
44         });
45      }
46   }


Listing 12.23 fileChooser/FileIconView.java


 1   package fileChooser;
 2
 3   import java.io.*;
 4   import javax.swing.*;
 5   import javax.swing.filechooser.*;
 6   import javax.swing.filechooser.FileFilter;
 7
 8   /**
 9    * A file view that displays an icon for all files that match a file filter.
10    */
11   public class FileIconView extends FileView
12   {
13      private FileFilter filter;
14      private Icon icon;
15
16      /**
17       * Constructs a FileIconView.
18       * @param aFilter a file filter--all files that this filter accepts will be shown
19       * with the icon.
20       * @param anIcon--the icon shown with all accepted files.
21       */
22      public FileIconView(FileFilter aFilter, Icon anIcon)
23      {
24         filter = aFilter;
25         icon = anIcon;
26      }
27
28      public Icon getIcon(File f)
29      {
30         if (!f.isDirectory() && filter.accept(f)) return icon;
31         else return null;
32      }
33   }


12.7.5 Color Choosers

As you saw in the preceding section, a high-quality file chooser is an intricate user interface component that you definitely do not want to implement yourself. Many user interface toolkits provide other common dialogs: to choose a date/time, currency value, font, color, and so on. The benefit is twofold: Programmers can simply use a high-quality implementation instead of rolling out their own, and users get a consistent experience with these components.

At this point, Swing provides only one additional chooser, the JColorChooser (see Figures 12.42 through 12.44). Use it to let users pick a color value. Like the JFileChooser class, the color chooser is a component, not a dialog, but it has convenience methods to create dialogs that contain a color chooser component.

Image

Figure 12.42 The Swatches pane of a color chooser

Image

Figure 12.43 The HSB pane of a color chooser

Image

Figure 12.44 The RGB pane of a color chooser

Here is how you show a modal dialog with a color chooser:

Color selectedColor = JColorChooser.showDialog(parent,title, initialColor);

Alternatively, you can display a modeless color chooser dialog. Supply the following:

• A parent component

• The title of the dialog

• A flag to select either a modal or a modeless dialog

• A color chooser

• Listeners for the OK and Cancel buttons (or null if you don’t want a listener)

Here is how you make a modeless dialog that sets the background color when the user clicks the OK button:

chooser = new JColorChooser();
dialog = JColorChooser.createDialog(
   parent,
   "Background Color",
false /* not modal */,
chooser,
event -> setBackground(chooser.getColor()),
null /* no Cancel button listener */);

You can do even better than that and give the user immediate feedback of the color selection. To monitor the color selections, you need to obtain the selection model of the chooser and add a change listener:

chooser.getSelectionModel().addChangeListener(event -> {
   do something with chooser.getColor();
});

In this case, there is no benefit to the OK and Cancel buttons that the color chooser dialog provides. You can just add the color chooser component directly into a modeless dialog:

dialog = new JDialog(parent, false /* not modal */);
dialog.add(chooser);
dialog.pack();

The program in Listing 12.24 shows the three types of dialogs. If you click on the Modal button, you must select a color before you can do anything else. If you click on the Modeless button, you get a modeless dialog, but the color change only happens when you click the OK button on the dialog. If you click the Immediate button, you get a modeless dialog without buttons. As soon as you pick a different color in the dialog, the background color of the panel is updated.

Listing 12.24 colorChooser/ColorChooserPanel.java


 1   package colorChooser;
 2
 3   import java.awt.Color;
 4   import java.awt.Frame;
 5   import java.awt.event.ActionEvent;
 6   import java.awt.event.ActionListener;
 7
 8   import javax.swing.JButton;
 9   import javax.swing.JColorChooser;
10   import javax.swing.JDialog;
11   import javax.swing.JPanel;
12
13   /**
14    * A panel with buttons to pop up three types of color choosers
15    */
16   public class ColorChooserPanel extends JPanel
17   {
18      public ColorChooserPanel()
19      {
20         JButton modalButton = new JButton("Modal");
21         modalButton.addActionListener(new ModalListener());
22         add(modalButton);
23
24         JButton modelessButton = new JButton("Modeless");
25         modelessButton.addActionListener(new ModelessListener());
26         add(modelessButton);
27
28         JButton immediateButton = new JButton("Immediate");
29         immediateButton.addActionListener(new ImmediateListener());
30         add(immediateButton);
31      }
32
33      /**
34       * This listener pops up a modal color chooser
35       */
36      private class ModalListener implements ActionListener
37      {
38         public void actionPerformed(ActionEvent event)
39         {
40            Color defaultColor = getBackground();
41            Color selected = JColorChooser.showDialog(ColorChooserPanel.this, "Set background",
42                  defaultColor);
43            if (selected != null) setBackground(selected);
44         }
45      }
46
47      /**
48       * This listener pops up a modeless color chooser. The panel color is changed when the user
49       * clicks the OK button.
50       */
51      private class ModelessListener implements ActionListener
52      {
53         private JDialog dialog;
54         private JColorChooser chooser;
55
56         public ModelessListener()
57         {
58            chooser = new JColorChooser();
59            dialog = JColorChooser.createDialog(ColorChooserPanel.this, "Background Color",
60                  false /* not modal */, chooser,
61                  event -> setBackground(chooser.getColor()),
62                  null /* no Cancel button listener */);
63         }
64
65         public void actionPerformed(ActionEvent event)
66         {
67            chooser.setColor(getBackground());
68            dialog.setVisible(true);
69         }
70      }
71
72      /**
73       * This listener pops up a modeless color chooser. The panel color is changed immediately when
74       * the user picks a new color.
75       */
76      private class ImmediateListener implements ActionListener
77      {
78         private JDialog dialog;
79         private JColorChooser chooser;
80
81         public ImmediateListener()
82         {
83            chooser = new JColorChooser();
84            chooser.getSelectionModel().addChangeListener(
85                  event -> setBackground(chooser.getColor()));
86
87            dialog = new JDialog((Frame) null, false /* not modal */);
88            dialog.add(chooser);
89            dialog.pack();
90         }
91
92         public void actionPerformed(ActionEvent event)
93         {
94            chooser.setColor(getBackground());
95            dialog.setVisible(true);
96         }
97      }
98   }


12.8 Troubleshooting GUI Programs

In the next section, we will give a few debugging tips for GUI programming. Then, we will show you how to use the AWT robot to automate GUI testing.

12.8.1 Debugging Tips

If you ever looked at a Swing window and wondered how its designer managed to get all the components to line up so nicely, you can spy on the contents. Press Ctrl+Shift+F1 to get a printout of all components in the hierarchy:

FontDialog[frame0,0,0,300x200,layout=java.awt.BorderLayout,...
  javax.swing.JRootPane[,4,23,292x173,layout=javax.swing.JRootPane$RootLayout,...
   javax.swing.JPanel[null.glassPane,0,0,292x173,hidden,layout=java.awt.FlowLayout,...
   javax.swing.JLayeredPane[null.layeredPane,0,0,292x173,...
     javax.swing.JPanel[null.contentPane,0,0,292x173,layout=java.awt.GridBagLayout,...
       javax.swing.JList[,0,0,73x152,alignmentX=null,alignmentY=null,...
         javax.swing.CellRendererPane[,0,0,0x0,hidden]
           javax.swing.DefaultListCellRenderer$UIResource[,-73,-19,0x0,...
       javax.swing.JCheckBox[,157,13,50x25,layout=javax.swing.OverlayLayout,...
       javax.swing.JCheckBox[,156,65,52x25,layout=javax.swing.OverlayLayout,...
       javax.swing.JLabel[,114,119,30x17,alignmentX=0.0,alignmentY=null,...
       javax.swing.JTextField[,186,117,105x21,alignmentX=null,alignmentY=null,...
       javax.swing.JTextField[,0,152,291x21,alignmentX=null,alignmentY=null,...

If you design your own custom Swing component and it doesn’t seem to be displayed correctly, you’ll really love the Swing graphics debugger. Even if you don’t write your own component classes, it is instructive and fun to see exactly how the contents of a component are drawn. To turn on debugging for a Swing component, use the setDebugGraphicsOptions method of the JComponent class. The following options are available:

Image

We have found that for the flash option to work, you must disable “double buffering”—the strategy used by Swing to reduce flicker when updating a window. The magic incantation for turning on the flash option is

RepaintManager.currentManager(getRootPane()).setDoubleBufferingEnabled(false);
((JComponent) getContentPane()).setDebugGraphicsOptions(DebugGraphics.FLASH_OPTION);

Simply place these lines at the end of your frame constructor. When the program runs, you will see the content pane filled in slow motion. Or, for more localized debugging, just call setDebugGraphicsOptions for a single component. Control freaks can set the duration, count, and color of the flashes—see the online documentation of the DebugGraphics class for details.

If you want to get a record of every AWT event generated in your GUI application, you can install a listener in every component that emits events. This is easily automated, due to the power of reflection. Listing 12.25 shows the EventTracer class.

To spy on messages, add the component whose events you want to trace to an event tracer:

EventTracer tracer = new EventTracer();
tracer.add(frame);

You will then get a textual description of all events, as shown in Figure 12.45.

Listing 12.25 eventTracer/EventTracer.java


 1   package eventTracer;
 2
 3   import java.awt.*;
 4   import java.beans.*;
 5   import java.lang.reflect.*;
 6
 7   /**
 8    * @version 1.31 2004-05-10
 9    * @author Cay Horstmann
10    */
11   public class EventTracer
12   {
13      private InvocationHandler handler;
14
15      public EventTracer()
16      {
17         // the handler for all event proxies
18         handler = new InvocationHandler()
19            {
20               public Object invoke(Object proxy, Method method, Object[] args)
21               {
22                  System.out.println(method + ":" + args[0]);
23                  return null;
24               }
25            };
26      }
27
28      /**
29       * Adds event tracers for all events to which this component and its children can listen
30       * @param c a component
31       */
32      public void add(Component c)
33      {
34         try
35         {
36            // get all events to which this component can listen
37            BeanInfo info = Introspector.getBeanInfo(c.getClass());
38
39            EventSetDescriptor[] eventSets = info.getEventSetDescriptors();
40            for (EventSetDescriptor eventSet : eventSets)
41               addListener(c, eventSet);
42         }
43         catch (IntrospectionException e)
44         {
45         }
46         // ok not to add listeners if exception is thrown
47
48         if (c instanceof Container)
49         {
50            // get all children and call add recursively
51            for (Component comp : ((Container) c).getComponents())
52               add(comp);
53         }
54      }
55
56      /**
57       * Add a listener to the given event set
58       * @param c a component
59       * @param eventSet a descriptor of a listener interface
60       */
61      public void addListener(Component c, EventSetDescriptor eventSet)
62      {
63         // make proxy object for this listener type and route all calls to the handler
64         Object proxy = Proxy.newProxyInstance(null, new Class[] { eventSet.getListenerType() },
65               handler);
66
67         // add the proxy as a listener to the component
68         Method addListenerMethod = eventSet.getAddListenerMethod();
69         try
70         {
71            addListenerMethod.invoke(c, proxy);
72         }
73         catch (ReflectiveOperationException e)
74         {
75         }
76         // ok not to add listener if exception is thrown
77      }
78   }


Image

Figure 12.45 The EventTracer class at work

12.8.2 Letting the AWT Robot Do the Work

The Robot class can send keystrokes and mouse clicks to any AWT program. This class is intended for automatic testing of user interfaces.

To get a robot, you need to first get a GraphicsDevice object. You can get the default screen device via this sequence of calls:

GraphicsEnvironment environment = GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice screen = environment.getDefaultScreenDevice();

Then you construct a robot:

Robot robot = new Robot(screen);

To send a keystroke, tell the robot to simulate a key press and a key release:

robot.keyPress(KeyEvent.VK_TAB);
robot.keyRelease(KeyEvent.VK_TAB);

For a mouse click, you first need to move the mouse and then press and release a button:

robot.mouseMove(x, y); // x and y are absolute screen pixel coordinates.
robot.mousePress(InputEvent.BUTTON1_MASK);
robot.mouseRelease(InputEvent.BUTTON1_MASK);

The idea is that you simulate key and mouse input and then take a screenshot to see whether the application did what it was supposed to. To capture the screen, use the createScreenCapture method:

Rectangle rect = new Rectangle(x, y, width, height);
BufferedImage image = robot.createScreenCapture(rect);

The rectangle coordinates also refer to absolute screen pixels.

Finally, you will usually want to add a small delay between robot instructions so that the application can catch up. Use the delay method and give it the number of milliseconds to delay. For example:

robot.delay(1000); // delay by 1000 milliseconds

The program in Listing 12.26 shows how you can use a robot. This robot tests the button test program that you saw in Chapter 11. First, pressing the space bar activates the leftmost button. Then the robot waits for two seconds so that you can see what it has done. After the delay, the robot simulates the Tab key and another space bar press to click on the next button. Finally, it simulates a mouse click on the third button. (You may need to adjust the x and y coordinates of the program to actually press the buttons.) The program ends by taking a screen capture and displaying it in another frame (see Figure 12.46).

Image

Figure 12.46 Capturing the screen with the AWT robot


Image Note

You need to run the robot in a separate thread, as shown in the example code. See Chapter 14 for more information about threads.


As you can see from this example, the Robot class is not by itself suitable for convenient user interface testing. Instead, it is a basic building block that can be a foundational part of a testing tool. A professional testing tool can capture, store, and replay user interaction scenarios and find out the screen locations of the components so that mouse clicks aren’t guesswork.

Listing 12.26 robot/RobotTest.java


 1   package robot;
 2
 3   import java.awt.*;
 4   import java.awt.event.*;
 5   import java.awt.image.*;
 6   import javax.swing.*;
 7
 8   /**
 9    * @version 1.05 2015-08-20
10    * @author Cay Horstmann
11    */
12   public class RobotTest
13   {
14      public static void main(String[] args)
15      {
16         EventQueue.invokeLater(() ->
17               {
18                  // make frame with a button panel
19
20                  ButtonFrame frame = new ButtonFrame();
21                  frame.setTitle("ButtonTest");
22                  frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
23                  frame.setVisible(true);
24               });
25
26         // attach a robot to the screen device
27
28         GraphicsEnvironment environment = GraphicsEnvironment.getLocalGraphicsEnvironment();
29         GraphicsDevice screen = environment.getDefaultScreenDevice();
30
31         try
32         {
33            final Robot robot = new Robot(screen);
34            robot.waitForIdle();
35            new Thread()
36            {
37               public void run()
38               {
39                  runTest(robot);
40               };
41            }.start();
42         }
43         catch (AWTException e)
44         {
45            e.printStackTrace();
46         }
47      }
48
49      /**
50       * Runs a sample test procedure
51       * @param robot the robot attached to the screen device
52       */
53      public static void runTest(Robot robot)
54      {
55         // simulate a space bar press
56         robot.keyPress(' ');
57         robot.keyRelease(' ');
58
59         // simulate a tab key followed by a space
60         robot.delay(2000);
61         robot.keyPress(KeyEvent.VK_TAB);
62         robot.keyRelease(KeyEvent.VK_TAB);
63         robot.keyPress(' ');
64         robot.keyRelease(' ');
65
66         // simulate a mouse click over the rightmost button
67         robot.delay(2000);
68         robot.mouseMove(220, 40);
69         robot.mousePress(InputEvent.BUTTON1_MASK);
70         robot.mouseRelease(InputEvent.BUTTON1_MASK);
71
72         // capture the screen and show the resulting image
73         robot.delay(2000);
74         BufferedImage image = robot.createScreenCapture(new Rectangle(0, 0, 400, 300));
75
76         ImageFrame frame = new ImageFrame(image);
77         frame.setVisible(true);
78      }
79   }
80
81   /**
82    * A frame to display a captured image
83    */
84   class ImageFrame extends JFrame
85   {
86      private static final int DEFAULT_WIDTH = 450;
87      private static final int DEFAULT_HEIGHT = 350;
88
89      /**
90       * @param image the image to display
91       */
92      public ImageFrame(Image image)
93      {
94         setTitle("Capture");
95         setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
96
97         JLabel label = new JLabel(new ImageIcon(image));
98         add(label);
99      }
100  }


This ends our discussion of user interface components. The material in Chapters 10 through 12 showed you how to implement simple GUIs in Swing. Turn to Volume II for more advanced Swing components and sophisticated graphics techniques.