Layout


The user interface for the Sis application is so poorly laid out that it's confusing to the end user. The problem is that by default, Swing lays out components to flow from left to right, in the order you add them to a container. When insufficient room remains to place a component on a "line," Swing wraps it, just like a word in a word processor. The component appears to the left side of the panel below the current line.

In Figure 4, the first line consists of four widgets: the "Courses" label, the list of courses, the Add button, and the "Department" label. The second line consists of the department field, the "Number" label, and the course number field.

Resize the window and make it as wide as your screen. Swing will redraw the widgets. If your screen is wide enough, all of the components should flow from left to right on a single line.

Swing provides several alternate layout mechanisms, each in a separate class, to help you produce aesthetically pleasing user interfaces. Swing refers to these classes as layout managers. You can associate a different layout manager with each container. The default layout manager, java.awt.FlowLayout, is not very useful if you want to create a professional-looking user interface.

Getting a view to look "just right" is an incremental, taxing exercise. You will find that mixing and matching layouts is an easier strategy than trying to stick to a single layout for a complex view.

An alternative to hand-coding layouts is to use a tool. Many IDEs provide GUI (view) composition tools that allow you to visually edit a layout. Using a tool can reduce the tedium of trying to get a user interface to be perfect.

You will learn to use a few of the more-significant layout mechanisms in an attempt to make CoursesPanel look good. Little of this work requires you to test first. Instead, you should test last. Make a small change, compile, run your tests, execute CoursesPanel as a stand-alone application, view the results, react!

GridLayout

You'll start with a simply understood but usually inappropriate layout, GridLayout. A GridLayout divides the container into equal-sized rectangles based on the number of rows and columns you specify. As you add components to the container, the GridLayout puts each in a cell (rectangle), moving from left to right, top to bottom (by default). The layout manager resizes each component to fit its cell.

Make the following changes in CoursesPanel:

 private void createLayout() {    JLabel coursesLabel =       createLabel(COURSES_LABEL_NAME, COURSES_LABEL_TEXT);    JList coursesList = createList(COURSES_LIST_NAME, coursesModel);    addButton =       createButton(ADD_BUTTON_NAME, ADD_BUTTON_TEXT);    int columns = 20;    JLabel departmentLabel =       createLabel(DEPARTMENT_LABEL_NAME, DEPARTMENT_LABEL_TEXT);    JTextField departmentField =       createField(DEPARTMENT_FIELD_NAME, columns);    JLabel numberLabel =       createLabel(NUMBER_LABEL_NAME, NUMBER_LABEL_TEXT);    JTextField numberField =       createField(NUMBER_FIELD_NAME, columns);    int rows = 4;    int cols = 2;    setLayout(new GridLayout(rows, cols));    add(coursesLabel);    add(coursesList);    add(addButton);    add(new JPanel());    add(departmentLabel);    add(departmentField);    add(numberLabel);    add(numberField); } 

You assign a layout manager to the panel by sending it the message setLayout. In createLayout, you send the message setLayout to the CoursesPanel object, passing it a GridLayout instance.

The result of executing CoursesPanel using the GridLayout is shown in Figure 5.

Figure 5.


The coursesLabel ends up in the upper left rectangle. The coursesList, the second component to be added, is in the upper right rectangle. The Add button drops down to the second row of rectangles, and is followed by an empty JPanel to fill the next rectangle. Each of the final two rows displays a label and its corresponding field.

Since each rectangle must be the same size, GridLayout doesn't have a lot of applicability in organizing "typical" interfaces with lots of buttons, fields, lists, and labels. It does have applicability if, for example, you are presenting a suite of icons to the end user. GridLayout does contain additional methods to improve upon its look, but you will usually want to use a more--sophisticated layout manager.

BorderLayout

BorderLayout is a simple but effective layout manager. BorderLayout allows you to place up to five components within the container: one each at the compass points north, east, south, and west and one to fill the remainder, or center (see Figure 6).

Figure 6. A Border Layout Configuration


For CoursesPanel, you will replace the GridLayout with a BorderLayout. Your BorderLayout will use three of the available areas: north, to contain the "Courses" Label; center, to contain the list of courses; and south, to contain the remainder of the widgets. You will organize the southern, or "bottom," widgets in a subpanel that uses a separate layout manager.

The createLayout method already is overly long, at close to 30 lines. CoursesPanel is a simple interface so far. Imagine a sophisticated panel with several dozen widgets. Unfortunately, it is common to encounter Swing code that does all of the requisite initialization and layout in a single method. Developers create panels within panels, they create and initialize components and place them in panels, they create layouts, and so on.

A more effective code composition is to extract the creation of each panel into a separate method. This not only makes the code far more readable but also provides more flexibility in rearranging the layout. I've refactored the code in CoursesPanel to reflect this cleaner organization.

 private void createLayout() {    JLabel coursesLabel =       createLabel(COURSES_LABEL_NAME, COURSES_LABEL_TEXT);    JList coursesList = createList(COURSES_LIST_NAME, coursesModel);    setLayout(new BorderLayout());    add(coursesLabel, BorderLayout.NORTH);    add(coursesList, BorderLayout.CENTER);    add(createBottomPanel(), BorderLayout.SOUTH); } JPanel createBottomPanel() {    addButton = createButton(ADD_BUTTON_NAME, ADD_BUTTON_TEXT);    JPanel panel = new JPanel();    panel.setLayout(new BorderLayout());    panel.add(addButton, BorderLayout.NORTH);    panel.add(createFieldsPanel(), BorderLayout.SOUTH);    return panel; } JPanel createFieldsPanel() {    int columns = 20;    JLabel departmentLabel =       createLabel(DEPARTMENT_LABEL_NAME, DEPARTMENT_LABEL_TEXT);    JTextField departmentField =       createField(DEPARTMENT_FIELD_NAME, columns);    JLabel numberLabel =       createLabel(NUMBER_LABEL_NAME, NUMBER_LABEL_TEXT);    JTextField numberField =       createField(NUMBER_FIELD_NAME, columns);    int rows = 2;    int cols = 2;    JPanel panel = new JPanel();    panel.setLayout(new GridLayout(rows, cols));    panel.add(departmentLabel);    panel.add(departmentField);    panel.add(numberLabel);    panel.add(numberField);    return panel; } 

The code in the CoursesPanel constructor sets its layout to a new instance of BorderLayout. It puts the label on the north (top) side of the panel and the list in the center of the panel. It puts the result of the method createBottomPanel, another JPanel, on the south (bottom) side of the panel (see Figure 7) The benefit of putting the list in the center is that it will expand as the frame window expands. The other widgets retain their original size.

Figure 7.


The code in createBottomPanel creates a JPanel that also uses a BorderLayout to organize its components. It places the Add button north and the resulting JPanel from createFieldsPanel south. The createFieldsPanel uses a GridLayout to organize the department and course number labels and fields. The result (Figure 7) is a considerable improvement but is still not good enough. Again, make sure you experiment with resizing the frame window to see how the layout reacts.

A Test Problem

If you rerun your tests, you now get three NullPointerException errors. How can that be, since you neither changed logic nor added/removed any components?

When you investigate the stack trace for the NullPointerException, you should discover that some of the get methods to extract a component from a container are failing. The problem is that the Util method getComponent only looks at components directly embedded within a container. Your layout code now embeds containers within containers (JPanels within JPanels). The code in getComponent ignores Components that have been added to subpanels.

The Util class doesn't have any tests associated with it. At this point, to help fix the problem and enhance your test coverage, you need to add appropriate tests. UtilTest contains three tests that should cover most expected circumstances:

 package sis.ui; import junit.framework.*; import javax.swing.*; import java.awt.*; public class UtilTest extends TestCase {    private JPanel panel;    protected void setUp() {       panel = new JPanel();    }    public void testNotFound() {       assertNull(Util.getComponent(panel, "abc"));    }    public void testDirectlyEmbeddedComponent() {       final String name = "a";       Component component = new JLabel("x");       component.setName(name);       panel.add(component);       assertEquals(component, Util.getComponent(panel, name));    }    public void testSubcomponent() {       final String name = "a";       Component component = new JLabel("x");       component.setName(name);       JPanel subpanel = new JPanel();       subpanel.add(component);       panel.add(subpanel);       assertEquals(component, Util.getComponent(panel, name));    } } 

The third test, testSubcomponent, should fail for the same reason your other tests are failing. To fix the problem, you will need to modify getComponent. For each component in a container, you will need to determine whether or not that component is a container (using instanceof). If so, you will need to traverse all of that subcontainer's components, repeating the same process for each. The most effective way to accomplish this is to make recursive calls to getComponent.

 static Component getComponent(Container container, String name) {    for (Component component: container.getComponents()) {       if (name.equals(component.getName()))          return component;       if (component instanceof Container) {          Container subcontainer = (Container)component;          Component subcomponent = getComponent(subcontainer, name);          if (subcomponent != null)             return subcomponent;       }    }    return null; } 

Your tests should all pass with this change.

BoxLayout

The BoxLayout class allows you to lay your components out on either a horizontal or vertical axis. Components are not wrapped when you resize the container; also, components do not grow to fill any area. The bottom panel, which must position an Add button and the fields subpanel vertically, one atop the other, is an ideal candidate for BoxLayout.

 JPanel createBottomPanel() {    addButton = createButton(ADD_BUTTON_NAME, ADD_BUTTON_TEXT);    JPanel panel = new JPanel();    panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));    panel.add(Box.createRigidArea(new Dimension(0, 6)));    addButton.setAlignmentX(Component.CENTER_ALIGNMENT);    panel.add(addButton);    panel.add(Box.createRigidArea(new Dimension(0, 6)));    panel.add(createFieldsPanel());    panel.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));    return panel; } 

You must pass an instance of the panel to the constructor of BoxPanel and a constant indicating the direction in which to lay out components. The constant PAGE_AXIS by default represents a top-to-bottom, or vertical, orientation. The other option is LINE_AXIS, which represents horizontal orientation by default.[4]

[4] You can change the orientation by sending applyComponentOrientation to the container. Older versions of BoxLayout supported only explicit X_AXIS and Y_AXIS constants. The newer constants allow for dynamic reorganization, perhaps to support internationalization needs.

You can create invisible "rigid areas" to separate components with whitespace. These rigid areas retain a fixed size even when the container is resized. The class method createRigidArea takes a Dimension object as a parameter. A Dimension is a width (0 in this example) by a height (6).

You may want to align each component with respect to the axis. In the example, you center the Add button around the vertical axis by sending it the message setAlignmentX with the parameter Component.CENTER_ALIGNMENT.

A final tweak is to supply an invisible border around the entire panel. The BorderFactory class can provide several types of borders that you can pass to the panel's setBorder method. Creating an empty border requires four parameters, each representing the width of the spacing from the outside edge of the panel. Other borders you can create include beveled borders, line borders, compound borders, etched borders, matte borders, raised beveled borders, and titled borders. Take a look at the Java API documentation and experiment with the effects that using different borders produces.

The code now produces an effective, but not quite perfect, layout. See Figure 8.

Figure 8. Using BoxLayout




Agile Java. Crafting Code with Test-Driven Development
Agile Javaв„ў: Crafting Code with Test-Driven Development
ISBN: 0131482394
EAN: 2147483647
Year: 2003
Pages: 391
Authors: Jeff Langr

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net