Speed up your Swing GUI construction with better building blocks
Use two helper classes to reduce dialog development time
This article introduces two helper classes for speeding up the construction of Swing user interfaces. The idea is that these classes encapsulate the layout of form and data entry dialogs in a standard way. Specific dialogs can be quickly constructed by simply adding the necessary dialog fields to the two classes, without worrying about the layout of the fields.
The two classes are:
LabelledItemPanel
: This class lays out items and their labels neatly on a panelStandardDialog
: This class provides the general look of dialogs and default button processing
These two classes leverage the fact that most dialogs used for entering information usually have the same basic layouts. Look at three dialogs for entering customer, product, and employee data:
Each dialog has the same layout consisting of a list of labelled fields. Each field’s layout is the same. The dialogs all have the normal Ok and Cancel buttons present on most data entry dialogs.
On our project, we achieved significant time savings as a result of using these classes instead of an IDE graphical user interface (GUI) builder. We had to build more than 20 dialogs for performing create, modify, and view operations on various data types. At the start of the project, a first version of the LabelledItemPanel
and StandardDialog
were built. Both classes had a few alignment problems. However, all the required panels and dialogs were created without programmers worrying about layout aspects. Further into the project, the alignment problems were solved in LabelledItemPanel
and StandardDialog
. All the panels and dialogs that used these two classes were rectified. This approach allowed developers to integrate the GUI with the application code much faster than if they were required to fully complete individual dialogs.
In this article, we compare our approach to the popular approach of using an IDE GUI builder. We explain how we came up with the idea for the classes and how they save development time. We describe our two classes and their usage in detail.
Note: Download this article’s source code from Resources.
Using an IDE GUI builder
Most IDEs today have some sort of capability to construct user interfaces without writing much code. This is an attractive option to many developers because writing user interfaces from scratch can be complex. However, using an IDE GUI builder to build numerous windows or dialogs has its disadvantages. The next section explains these disadvantages.
The myth versus reality for large user interfaces
An IDE is a good tool when you need to build a simple GUI with only a small number of windows or dialogs. If you need to build a large number of windows or dialogs, then using an IDE GUI builder has the following disadvantages:
-
Unless you use simple layout managers or absolute positioning you still need to understand the complexities of various layout managers, especially
GridBagLayout
. -
Each item on the dialog needs to be individually laid out, which involves setting a number of properties for each item. This makes it slow and tedious to lay out large numbers of dialogs or dialogs with many fields.
-
You cannot use the same layout over similar screens. Each new dialog needs to be laid out from scratch.
-
The generated code is generally not portable between IDEs and is sometimes not even compatible between different IDE versions. This is an important fact that must be considered in regards to code maintenance.
-
Dialogs are not easy to modify because the layout constraints are assigned to each item. If an item is moved, its constraints and all the other items’ constraints affected by the moved item will need adjusting as well.
- If there is a bug, looking through the generated code can be frightening, as generated code (in most cases) is not well laid out or readable. For example, the following is code generated by a popular IDE’s GUI builder for a customer data entry panel:
public class JBuilderPanel extends JPanel
{
...
GridBagLayout gridBagLayout1 = new GridBagLayout();
private JLabel myCustomerCodeLabel = new JLabel();
private JTextField myCustomerCodeField = new JTextField();
private JLabel myNameLabel = new JLabel();
private JTextField myNameField = new JTextField();
private JLabel myAddressLabel = new JLabel();
private JScrollPane myAddressScrollpane = new JScrollPane();
private JTextArea myAddressField = new JTextArea();
...
public JBuilderPanel()
{
try
{
jbInit();
}
catch(Exception e)
{
e.printStackTrace();
}
}
private void jbInit() throws Exception
{
this.setLayout(gridBagLayout1);
this.setBorder(BorderFactory.createEtchedBorder());
myCustomerCodeLabel.setText("Customer Code");
myNameLabel.setText("Name");
myAddressLabel.setText("Address");
...
this.add(myCustomerCodeLabel,
new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0,
GridBagConstraints.NORTHEAST, GridBagConstraints.NONE,
new Insets(10, 10, 0, 0), 0, 0));
this.add(myCustomerCodeField,
new GridBagConstraints(1, 0, 1, 1, 1.0, 0.0,
GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL,
new Insets(10, 10, 0, 10), 0, 0));
this.add(myNameLabel,
new GridBagConstraints(0, 1, 1, 1, 0.0, 0.0,
GridBagConstraints.NORTHEAST, GridBagConstraints.NONE,
new Insets(10, 0, 0, 0), 0, 0));
this.add(myNameField,
new GridBagConstraints(1, 1, 1, 1, 1.0, 0.0,
GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL,
new Insets(10, 10, 0, 10), 0, 0));
this.add(myAddressLabel,
new GridBagConstraints(0, 2, 1, 1, 0.0, 0.0,
GridBagConstraints.NORTHEAST, GridBagConstraints.NONE,
new Insets(10, 10, 0, 0), 0, 0));
this.add(myAddressScrollpane,
new GridBagConstraints(1, 2, 1, 1, 1.0, 0.0,
GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL,
new Insets(10, 10, 0, 10), 0, 0));
...
}
...
}
An alternative approach
At the start of our project we looked for alternatives to using an IDE to build the dialogs because we had many dialogs to build and were aware of the disadvantages described above. The only alternative was to hand code the dialogs and their associated panels. However, we were not going to be any better off simply using Swing’s layout managers and components. We would end up producing similar code to the IDE with the same drawbacks. To speed up development we needed better building blocks (classes) that would take care of the dialogs’ common aspects and be easily customized to handle the differences between screens.
What is common? What is different?
We determined the following three elements were common to most of our dialogs:
- The dialogs usually consisted of a list of items and labels neatly aligned down the page
- Each dialog normally had Ok and Cancel buttons
- The actions performed when the Ok and Cancel buttons were pressed were usually the same
The differences between screens were the actual fields being edited and their labels.
Better building blocks
The most tedious part of developing the dialogs was specifying the layout constraints for each field and its label. The fastest way to develop is for the classes to allow us to specify the fields and their labels without specifying the layout constraints. It seemed to make sense to use two different classes: one for handling layout of the labelled fields and one for the common dialog buttons and associated processing. We called the class for layout handling LabelledItemPanel
, while we called the class for the common dialog features StandardDialog
.
A common panel: LabelledItemPanel
The purpose of LabelledItemPanel
is to lay out items and their labels neatly on a panel. The class uses a GridBagLayout
to control the actual layout, but this is all hidden from class users. To use the class, the item and its label are added to the panel with the addItem()
method, passing the text for the field label and the field component. The user does not have to specify any layout constraints because the class works out all of the layout constraints needed.
The LabelledItemPanel
can be used in two ways:
- Create an instance of the
LabelledItemPanel
and add data entry fields to the panel - Subclass
LabelledItemPanel
and add the data entry fields to the panel during construction
An example of using LabelledItemPanel
directly is shown in the following code fragment:
JTextField customerCodeField = new JTextField();
JTextField nameField = new JTextField();
JTextArea addressField = new JTextArea(3, 20);
...
LabelledItemPanel panel = new LabelledItemPanel()
panel.setBorder(BorderFactory.createEtchedBorder());
panel.addItem("Customer Code", customerCodeField);
panel.addItem("Name", nameField);
panel.addItem("Address", new JScrollPane(addressField,
JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
JScrollPane.HORIZONTAL_SCROLLBAR_NEVER));
...
// Display the panel to the User in a dialog
String customerCode = customerCodeField.getText();
String name = nameField.getText();
String address = addressField.getText();
...
// Process the data
If you compare the simple addItem()
method calls with the IDE-generated code shown below, the time-saving advantages of using this class are quite obvious:
this.add(myCustomerCodeLabel,
new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0,
GridBagConstraints.NORTHEAST, GridBagConstraints.NONE,
new Insets(10, 10, 0, 0), 0, 0));
this.add(myCustomerCodeField,
new GridBagConstraints(1, 0, 1, 1, 1.0, 0.0,
GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL,
new Insets(10, 10, 0, 10), 0, 0));
this.add(myNameLabel,
new GridBagConstraints(0, 1, 1, 1, 0.0, 0.0,
GridBagConstraints.NORTHEAST, GridBagConstraints.NONE,
new Insets(10, 0, 0, 0), 0, 0));
An example of using the LabelledItemPanel
by subclassing is shown in the following code fragment:
public class CustomerPanel extends LabelledItemPanel
{
private JTextField myCustomerCodeField = new JTextField();
private JTextField myNameField = new JTextField();
private JTextArea myAddressField = new JTextArea(3, 20);
...
public CustomerPanel()
{
init();
}
private void init()
{
setBorder(BorderFactory.createEtchedBorder());
addItem("Customer Code", myCustomerCodeField);
addItem("Name", myNameField);
addItem("Address", new JScrollPane(myAddressField,
JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
JScrollPane.HORIZONTAL_SCROLLBAR_NEVER));
...
}
public CustomerData getCustomerData()
{
CustomerData customerData = new CustomerData();
customerData.myCustomerCode = myCustomerCodeField.getText();
customerData.myName = myNameField.getText();
customerData.myAddress = myAddressField.getText();
...
return customerData;
}
...
}
// A method in some class using the CustomerPanel
{
CustomerPanel panel = new CustomerPanel();
// Display the panel to the User in a dialog
CustomerData customerData = panel.getCustomerData();
// Process the data
}
LabelledItemPanel under the hood
This section explains how LabelledItemPanel
works. The code for the class is shown below:
public class LabelledItemPanel extends JPanel
{
/** The row to add the next labelled item to */
private int myNextItemRow = 0;
public LabelledItemPanel()
{
init();
}
private void init()
{
setLayout(new GridBagLayout());
// Create a blank label to use as a vertical fill so that the
// label/item pairs are aligned to the top of the panel and are not
// grouped in the centre if the parent component is taller than
// the preferred size of the panel.
GridBagConstraints constraints = new GridBagConstraints();
constraints.gridx = 0;
constraints.gridy = 99;
constraints.insets = new Insets(10, 0, 0, 0);
constraints.weighty = 1.0;
constraints.fill = GridBagConstraints.VERTICAL;
JLabel verticalFillLabel = new JLabel();
add(verticalFillLabel, constraints);
}
public void addItem(String labelText, JComponent item)
{
// Create the label and its constraints
JLabel label = new JLabel(labelText);
GridBagConstraints labelConstraints = new GridBagConstraints();
labelConstraints.gridx = 0;
labelConstraints.gridy = myNextItemRow;
labelConstraints.insets = new Insets(10, 10, 0, 0);
labelConstraints.anchor = GridBagConstraints.NORTHEAST;
labelConstraints.fill = GridBagConstraints.NONE;
add(label, labelConstraints);
// Add the component with its constraints
GridBagConstraints itemConstraints = new GridBagConstraints();
itemConstraints.gridx = 1;
itemConstraints.gridy = myNextItemRow;
itemConstraints.insets = new Insets(10, 10, 0, 10);
itemConstraints.weightx = 1.0;
itemConstraints.anchor = GridBagConstraints.WEST;
itemConstraints.fill = GridBagConstraints.HORIZONTAL;
add(item, itemConstraints);
myNextItemRow++;
}
}
The class extends Swing’s JPanel
and has two additional methods init()
and addItem()
. The init()
is called from the constructor and sets up GridBagLayout
used by the panel. We have already shown the second method, addItem()
, being used. This method creates a JLabel
for the label text and then adds JLabel
and passed-in item to the panel with the correct GridBagConstraints
. The constraints are set up so that the label text and the item are properly aligned, and that the item extends to the dialog’s right-hand edge.
A common dialog
The purpose of the class StandardDialog
is to hold a data entry panel (such as a LabelledItemPanel
), provide the standard Ok and Cancel buttons, and handle basic processing when the Ok or Cancel buttons are pressed.
The StandardDialog
is modal, so a call to the show()
method is a blocking call. After calling the show()
method, the hasUserCancelled()
method should be called to see if the user has cancelled the dialog by either pressing the Cancel button or the dialog close button (on the dialog frame).
StandardDialog
can be used in two ways:
- Create an instance of
StandardDialog
and add a data entry panel to the dialog - Subclass
StandardDialog
and add a data entry panel to the dialog during construction
An example of using StandardDialog
directly is shown in the following code fragment:
// Create a data entry panel
StandardDialog dialog = new StandardDialog(
(Frame)null, "Customer Details");
dialog.setContentPane(panel);
dialog.pack();
dialog.show();
if(!dialog.hasUserCancelled())
{
// Process the data in the panel
}
An example of using StandardDialog
by subclassing is shown in the following code fragment:
public class CustomerDialog extends StandardDialog
{
private JTextField myCustomerCodeField = new JTextField();
private JTextField myNameField = new JTextField();
private JTextArea myAddressField = new JTextArea(3, 20);
...
private LabelledItemPanel myContentPane = new LabelledItemPanel();
public CustomerDialog()
{
init();
}
private void init()
{
setTitle("Customer Dialog");
myContentPane.setBorder(BorderFactory.createEtchedBorder());
myContentPane.addItem("Customer Code", myCustomerCodeField);
myContentPane.addItem("Name", myNameField);
myContentPane.addItem("Address", new JScrollPane(myAddressField,
JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
JScrollPane.HORIZONTAL_SCROLLBAR_NEVER));
...
setContentPane(myContentPane);
}
public CustomerData getCustomerData()
{
CustomerData customerData = new CustomerData();
customerData.myCustomerCode = myCustomerCodeField.getText();
customerData.myName = myNameField.getText();
customerData.myAddress = myAddressField.getText();
...
return customerData;
}
}
// A method in some class using the CustomerDialog
{
CustomerDialog dialog = new CustomerDialog();
dialog.pack();
dialog.show();
if(!dialog.hasUserCancelled())
{
CustomerData customerData = dialog.getCustomerData();
// Process the data
}
}
If data validation needs to be performed before the dialog closes, then the isValidData()
method should be overridden. An example of this is given below:
protected boolean isValidData()
{
if(myCustomerCodeField.getText().equals(""))
{
JOptionPane.showMessageDialog(this,
"Please enter a Customer Code",
"Blank Customer Code",
JOptionPane.WARNING_MESSAGE);
myCustomerCodeField.requestFocus();
return false;
}
if(myNameField.getText().equals(""))
{
JOptionPane.showMessageDialog(this,
"Please enter a Name",
"Blank Name",
JOptionPane.WARNING_MESSAGE);
myNameField.requestFocus();
return false;
}
return true;
}
StandardDialog behind the scenes
This section explains how StandardDialog
works. The appearance and behavior of StandardDialog
is set up in the init()
method shown below:
private void init()
{
setModal(true);
setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
// Setup the internal content pane to hold the user content pane
// and the standard button panel
JPanel internalContentPane = new JPanel();
internalContentPane.setLayout(
new BorderLayout(COMPONENT_SPACING, COMPONENT_SPACING));
internalContentPane.setBorder(
BorderFactory.createEmptyBorder(COMPONENT_SPACING,
COMPONENT_SPACING, COMPONENT_SPACING, COMPONENT_SPACING));
// Create the standard button panel with "Ok" and "Cancel"
Action okAction = new AbstractAction("Ok")
{
public void actionPerformed(ActionEvent actionEvent)
{
if(isValidData())
{
myIsDialogCancelled = false;
dispose();
}
}
};
Action cancelAction = new AbstractAction("Cancel")
{
public void actionPerformed(ActionEvent actionEvent)
{
myIsDialogCancelled = true;
dispose();
}
};
JPanel buttonPanel = new JPanel();
buttonPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
buttonPanel.add(new JButton(okAction));
buttonPanel.add(new JButton(cancelAction));
internalContentPane.add(buttonPanel, BorderLayout.SOUTH);
// Initialise the user content pane with a JPanel
setContentPane(new JPanel(new BorderLayout()));
super.setContentPane(internalContentPane);
// Finally, add a listener for the window close button.
// Process this event the same as the "Cancel" button.
WindowAdapter windowAdapter = new WindowAdapter()
{
public void windowClosing(WindowEvent windowEvent)
{
myIsDialogCancelled = true;
dispose();
}
};
addWindowListener(windowAdapter);
}
The init()
is called from the constructor. The method does the following:
-
Sets up actions for the Ok and Cancel buttons. These actions set the field
myIsDialogCancelled
to indicate which button has been pressed and then calldispose
, which causes theshow()
method to unblock. The methodhasUserCancelled()
checks the value ofmyIsDialogCancelled
as shown in the usage example. -
Builds the two buttons and ties them to the actions. The two buttons are added to a panel, and the panel is then added to the dialog.
- Sets up an event handler for the close window event. This event handler performs the same processing as the action for the Cancel button.
Note: Download the full source code for LabelledItemPanel
and StandardDialog
from Resources.
Advantages
We have saved substantial development time with this approach. The average time spent building a panel using an IDE GUI builder was between one and three hours depending on the number of fields on the panel. Using our LabelledItemPanel
class, the panels were constructed in less than 20 minutes. We have used LabelledItemPanel
35 times.
The other advantages of this approach include:
-
Absolutely no layout constraints need to be understood or specified. This allows developers with little Swing knowledge to quickly construct user interfaces.
-
Consistency of data entry panels and dialogs, both for users and developers.
-
Reordering the fields is simple. You only need to reorder.
addItem()
calls. You do not have to adjust constraints on all of the reordered fields and labels. -
The layout and properties for all entry panels can be changed centrally.
- No unnecessary references are held for labels.
Further enhancements
Our simple classes could be further enhanced to include the following:
LabelledItemPanel
could support a read-only flag for viewing, but not editing, dataLabelledItemPanel
could support I18N by passing in aResourceBundle
key for a label instead of passing in text already internationalized
The need for speed
We have described an approach that speeds up the construction of Swing user interfaces by using better building blocks than ones available in the Swing libraries. It relies on two classes: LabelledItemPanel
and StandardDialog
. The essence of our approach was to identify repetitive elements of GUIs that could be encapsulated in easily reusable code.