Subpages: 1. Layouts overview
2. Comparing common layout managers
3. Using GridBagLayout
4. Choosing the right layout
5. Custom layout manager: part I -Label/field pairs
6. Custom layout manager: part II - Common interfaces
7. Dynamic layout in a JavaBeans container
4.5 Custom layout manager: part I -Label/field pairs
This example is intended to familiarize you with developing custom layouts. You may find this knowledge useful in cases where the traditional layouts are not satisfactory or are too complex. In developing large scale applications it is often more convenient to build custom layouts, such as the one we develop here, to help with specific tasks. This often provides increased consistency, and may save a significant amount of coding in the long run.
The example in the previous section highlighted a problem: what is the best way to lay out input field components (e.g. text fields, combo boxes, etc.) and their corresponding labels? We have seen that it can be done using a combination of several intermediate containers and layouts. This section shows how we can simplify the process by using a custom-built layout manager. The goal is to construct a layout manager that knows how to lay out labels and their associated input fields in two columns, allocating the minimum required space to the column containing the labels, and using the remainder for the column containing the input fields.
First we need to clearly state our design goals for this layout manager, which we will appropriately call DialogLayout. It is always a good idea to reserve plenty of time for thinking about your design. Well-defined design specifications can save you tremendous amounts of time in the long run, and can help pinpoint flaws and oversights before they arise in the code. (We strongly recommend that adopting a design specification stage become part of your development regimin.)
DialogLayout specification:
1. This layout manager will be applied to a container that has all the necessary components added to it in the following order: label1, field1, label2, field2, etc. (Note that when components are added to a container they are tracked in a list. If no index is specified when a component is added to a container it will be added to the end of the list using the next available index. As usual this indexing starts from 0. A component can be retreived by index using the getComponent(int index) method.) If the labels and fields are added correctly, all even numbered components in the container will correspond to labels, and all odd numbered components will correspond to input fields.
2. The components must be placed in pairs forming two vertical columns.
3. Components making up each pair must be placed opposite one another (i.e. label1 field1). Each pair's label and field must receive the same preferable height, which should be the preferred height of the field.
4. Each left component (labels) must receive the same width. This width should be the maximum preferable width of all left components.
5. Each right component (input fields) must also receive the same width. This width should occupy all the remaining space left over from that taken by the left components column.
The code below introduces our custom DialogLayout class which satisfies the above design specification. This class is placed in its own package named dl. The code used to construct the GUI is alsmot identical to that of the previous example. However, we now revert back to variant 1 and use an instance of DialogLayout instead of a GridLayout to manage the p1r JPanel.

Figure 4.16 Using DialogLayout - custom layout manager part I
<<file figure4-16.gif>>
The Code: FlightReservation.java
see \Chapter4\4
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;
import dl.*;
public class FlightReservation extends JFrame
{
public FlightReservation() {
super("Flight Reservation Dialog [Custom Layout]");
// Unchanged code from section 4.4
// Variant 1
JPanel p1r = new JPanel();
p1r.setBorder(new EmptyBorder(10, 10, 10, 10));
p1r.setLayout(new DialogLayout(20, 5));
p1r.add(new JLabel("Date:"));
p1r.add(new JTextField());
p1r.add(new JLabel("From:"));
JComboBox cb1 = new JComboBox();
cb1.addItem("New York");
p1r.add(cb1);
p1r.add(new JLabel("To:"));
JComboBox cb2 = new JComboBox();
cb2.addItem("London");
p1r.add(cb2);
p1.add(p1r);
getContentPane().add(p1, BorderLayout.NORTH);
// end Variant 1
// All remaining code unchanged from section 4.4
DialogLayout.java
see \Chapter4\4\dl
package dl;
import java.awt.*;
import java.util.*;
public class DialogLayout implements LayoutManager
{
protected int m_divider = -1;
protected int m_hGap = 10;
protected int m_vGap = 5;
public DialogLayout() {}
public DialogLayout(int hGap, int vGap) {
m_hGap = hGap;
m_vGap = vGap;
}
public void addLayoutComponent(String name, Component comp) {}
public void removeLayoutComponent(Component comp) {}
public Dimension preferredLayoutSize(Container parent) {
int divider = getDivider(parent);
int w = 0;
int h = 0;
for (int k=1 ; k<parent.getComponentCount(); k+=2) {
Component comp = parent.getComponent(k);
Dimension d = comp.getPreferredSize();
w = Math.max(w, d.width);
h += d.height + m_vGap;
}
h -= m_vGap;
Insets insets = parent.getInsets();
return new Dimension(divider+w+insets.left+insets.right,
h+insets.top+insets.bottom);
}
public Dimension minimumLayoutSize(Container parent) {
return preferredLayoutSize(parent);
}
public void layoutContainer(Container parent) {
int divider = getDivider(parent);
Insets insets = parent.getInsets();
int w = parent.getWidth() - insets.left
- insets.right - divider;
int x = insets.left;
int y = insets.top;
for (int k=1 ; k<parent.getComponentCount(); k+=2) {
Component comp1 = parent.getComponent(k-1);
Component comp2 = parent.getComponent(k);
Dimension d = comp2.getPreferredSize();
comp1.setBounds(x, y, divider-m_hGap, d.height);
comp2.setBounds(x+divider, y, w, d.height);
y += d.height + m_vGap;
}
}
public int getHGap() { return m_hGap; }
public int getVGap() { return m_vGap; }
public void setDivider(int divider) {
if (divider > 0)
m_divider = divider;
}
public int getDivider() { return m_divider; }
protected int getDivider(Container parent) {
if (m_divider > 0)
return m_divider;
int divider = 0;
for (int k=0 ; k<parent.getComponentCount(); k+=2) {
Component comp = parent.getComponent(k);
Dimension d = comp.getPreferredSize();
divider = Math.max(divider, d.width);
}
divider += m_hGap;
return divider;
}
public String toString() {
return getClass().getName() + "[hgap=" + m_hGap + ",vgap="
+ m_vGap + ",divider=" + m_divider + "]";
}
}
Understanding the Code
Class FlightReservation
This class now imports package dl and sets that layout for JPanel p1r (which contains the labels and input fields). Package dl contains our custom layout, DialogLayout.
Class DialogLayout
This class implements the LayoutManager interface to serve as our custom layout manager. Three instance variables are needed:
int m_divider: width of the left components. This can be calculated or set to some mandatory value.
int m_hGap: horizontal gap between components.
int m_vGap: vertical gap between components.
Two constructors are available to create a DialogLayout: a no-argument default constructor and a constructor which takes horizontal and vertical gap sizes as parameters. The rest of the code implements methods from the LayoutManager interface.
Methods addLayoutComponent() and removeLayoutComponent() are not used in this class and receive empty implementations. We do not support an internal collection of the components to be managed. Rather, we refer to these component directly from the container which is being managed.
The purpose of the preferredLayoutSize() method is to return the preferable container size required to lay out the components in the given container according to the rules used in this layout. In our implementation we first determine the divider size (the width of the first column plus the horizontal gap, m_hGap) by calling the getDivider() custom method.
int divider = getDivider(parent);
If no positive divider size has been specified using the setDivider() method (see below), the getDivider() method looks at each even indexed component in the container (this should be all the labels if the components were added to the container in the correct order) and returns the largest preferred width found plus the horizontal gap value, m_hGap (which defaults to 10 if the default constructor is used):
if (m_divider > 0)
return m_divider;
int divider = 0;
for (int k=0 ; k<parent.getComponentCount(); k+=2) {
Component comp = parent.getComponent(k);
Dimension d = comp.getPreferredSize();
divider = Math.max(divider, d.width);
}
divider += m_hGap;
return divider;
Now, back to the preferredLayoutSize() method. Once getDivider returns we then examine all components in the container with odd indices (this should be all the input fields) and determine the maximum width, w. This is found by checking the preferred width of each input field. While we are determining this maximum width, we are also continuing to accumulate the height, h, of the whole input fields column by summing each field's preferred height (not forgetting to add the vertical gap size, m_vGap, each time; notice that m_vGap is subtracted from the height at the end because there is no vertical gap for the last field). (Remember that m_vGap defaults to 5 if the the default constructor is used.)
int w = 0;
int h = 0;
for (int k=1 ; k<parent.getComponentCount(); k+=2) {
Component comp = parent.getComponent(k);
Dimension d = comp.getPreferredSize();
w = Math.max(w, d.width);
h += d.height + m_vGap;
}
h -= m_vGap;
So at this point we have determined the width of the labels column (including the space between columns), divider, and the preferred hieght, h, and width, w, of the input fields column. So divider+w gives us the preferred width of the container, and h gives us the total preferred height. Not forgetting to take into account any Insets that might have been applied to the container, we can now return the correct preferred size:
Insets insets = parent.getInsets();
return new Dimension(divider+w+insets.left+insets.right,
h+insets.top+insets.bottom);
The purpose of the minimumLayoutSize() method is to return the minimum size required to lay out the components in the given container according to the rules used in this layout. We return preferredLayoutSize() in this method, because we chose not to make a distinction between minimum and preferable sizes (to avoid over-complication).
layoutContainer() is the most important method in any layout manager. This method is responsible for actually assigning the bounds (position and size) for the components in the container being managed. First it determines the size of the divider (as discussed above), which represents the width of the labels column plus an additional m_hGap. From this it determines the width, w, of the fields column by subtracting the container's left and right insets and divider from the width of the whole container:
int divider = getDivider(parent);
Insets insets = parent.getInsets();
int w = parent.getWidth() - insets.left
- insets.right - divider;
int x = insets.left;
int y = insets.top;
Now all pairs of components are examined in turn. Each left component receives a width equal to divider-m_hGap, and all right components receive a width of w. Both left and right components receive the preferred height of the right component (which should be the input field).
Coordinates of the left components are assigned starting with the container's Insets, x and y. Notice that y is continually incremented based on the preferred height of each right component plus the vertical gap, m_vGap. The right components are assigned a y-coordinate identical to their left component counterpart, and an x-coordinate of x+divider (remember that divider includes the horizontal gap, m_hGap):
for (int k=1 ; k<parent.getComponentCount(); k+=2) {
Component comp1 = parent.getComponent(k-1);
Component comp2 = parent.getComponent(k);
Dimension d = comp2.getPreferredSize();
comp1.setBounds(x, y, divider-m_hGap, d.height);
comp2.setBounds(x+divider, y, w, d.height);
y += d.height + m_vGap;
}
Method setDivider() allows us to manually set the size of the left column. The int value passed as parameter gets stored in the m_divider instance variable. Whenever m_divider is greater than 0 the calculations of divider size are overridden in the getDivider() method and this value is returned instead.
The toString method provides typical class name and instance variable information. (It is always a good idea to implement informative toString() methods for each class. Although we don't consistently do this throughout this text, we feel that production code should always include this functionality.)
Running the Code
At this point you can compile and execute this example. Figure 4.16 shows the sample interface introduced in the previous section now using DialogLayout to manage the layout of the input fields (text field and two combo boxes) and their corresponding labels. Note that the labels occupy only their preferred space and do not resize when the frame resizes. The gap between labels and boxes can be managed easily by manually setting the divider size with the setDivider() method (discussed above). The input fields form the right column and occupy all remaining space.
Using DialogLayout, all that is required is adding the labels and input fields in the correct order. We can now use this layout manager each time we encounter label/input field pairs without worrying about intermediate containers. In the next section we build upon DialogLayout to create an even more general layout manager that can be used to create complete dialog interfaces very easily.
UI Guideline : Alignment across controls as well as within It is a common mistake in UI Design to achieve good alignment with a control or component but fail to achieve this across a whole screen, panel or dialog. Unfortunately, the architecture of Swing lends itself to this problem. For example, if you have 4 custom components which inherit from a JPanel, each has its own Layout Manager and each is functional in its own right. Then you wish to build a composite component which requires all four. So you create a new Component with a Grid Layout for example, then add each of your 4 components in turn.
The result can be very messy. The fields within each component will align e.g. 3 radio buttons, but those radio buttons will not align with say 3 TextFields in the next component. Why not? The answer is simple. With Swing, there is no way for the layout manager within each component to negotiate with the others. So alignment cannot be achieved across the components. The answer to this problem is that you must flatten out the design into a single panel, as DialogLayout achieves.



RSS feed Java FAQ News