ORIGINAL DRAFT
“Everything should be as simple as possible, but no simpler.” - Albert Einstein.
User interface design can be a real struggle when one of the requirements is to make our programs accessible to a larger market. The most suitable metaphor for a given domain may not be simple enough for an inexperienced user, yet the program still has to address their needs. If you can lead the user through each step explicitly, and get them to their objective, their net experience is usually positive. This month, we’ll build a framework you can use to develop your own sophisticated Java wizards.
Basic Design
Figure 1 shows a screen shot of the JWizard class in action. There is a WizardImage panel on the left which is responsible for displaying an image, border and the basic spacing. The buttons and beveled line above them are part of the WizardNavigator panel. The rest of the display changes, as you move forward or backward, and is simply contained by a panel that uses the DeckLayout, a layout manager that works like the familiar CardLayout, but overcomes fundamental focus management problems by disabling components when they are made invisible.
The JWizard framework is designed to let you drop in components, usually JPanel objects, in order and later manage the sequence more explicitly. It does this by using a WizardSequenceManager which which implements the SequenceManager interface. Since Wizards typically collect some kind of data, we implement a DataCollectionModel interface to store results. Our default being a PropertyDataModel, derived from Properties. Both the SequenceManager and DataCollectionModel can be replaced by other implementations if you like.
We also implement a WizardValidator interface that lets you control whether the user can move forward or backward at any given point, supporting dynamic data validation on each panel. When changes are made to the DataCollectionModel, the JWizard framework is notified and checks the current panel through this interface to decide whether it needs to update the button status. If the user can not move forward and/or backward, the button(s) are disabled.
The WizardPanel implements most of the page mechanics and was designed to be subclassed. It lets you create the larger underlined title and explanation text by default, though you can set one or both these values to null if you want more control. The WizardPanel implements the WizardValidator interface and provides a few utility methods that make development easier.
Quick Tour
It may seem complicated to have a data model, sequence manager and validator interface, but the benefits are obvious when you start building with JWizard. Here’s a quick example of how it works in practice. We collects some personal information and asks a simple question before producing a result panel. The logic for this simple application could become complicated if the framework didn’t handle it so well. The flow from one panel to the next is show in Figure 2.
The first panel collects information and registers itself as a DocumentListener for each of the JTextField objects in order to update the data model as changes are made. Since InformationPanel is a subclass of WizardPanel, we can call getDataModel() to get the model and the setValue method to update changes. The event handler call the model’s hasValue method to determine if a field has content. In this example, if all three fields have data, the canMoveForward flag is set to true. When the model changes, it notifies the framework which then check the WizardValidator interface to activate the button(s), as appropriate. The Next button becomes active if all three fields have content.
The second panel sets up six JRadioButton objects, makes them part of the same button group and registers itself as an ActionListener for each button. When a button is pressed, the FavoritesPanel calls the WizardPanel superclass method getManager to get the SequenceManager and sets the next panel with the setNext method. At the same time, it sets the subsequent panel’s next value to a null ("") string. A null string indicates a final panel in the sequence. JWizard replaces the Next button with a Finish button when it sees this.
At the end of this sequence if you press the Finish button, we print out the PropertiesDataModel so you can see what you’ve collected. You can find all this code on the JDJ web site, along with the source for the whole JWizard framework. To try this example, just run the JWizardTest class.
Stacking the Deck
The DeckLayout and DeckPanel classes are fairly uncomplicated. The DeckLayout is very similar to the CardLayout manager that comes with the JDK, updated to eliminate deprecated calls and extended to handle focus traversal properly. Listing 1 presents the show method, which lets you select an active page, and the setActive method, which is shared by all page-switching calls. The setActive method enables or disables components and their children, and makes them visible or invisible, depending on the boolean argument. The show method looks for and disables the active page before activating the new page.
Listing 2 show a couple of calls from the DeckPanel class. In particular, the addPanel method automatically sets the sequence manager to the order in which the panels are added. The first panel is set as the first active panel in the sequence, all others being set as subsequent to the previousl panel. This is meant to simplify the most common case and allows changes to be made to the sequence manager at runtime. The DeckPanel keeps references to the DeckLayout and SequenceManager which are both used in the setPanel method. Only DeckPanel accesses the DeckLayout manager directly.
The SequenceManager interface is presented in Listing 3. Pages names are used to keep things simple. The concrete implementation keeps track of the first and current positions and holds two Hashtables which associate a given key with the next and previous pages, respectively. Listing 4 shows the member variables as well as the getNext and setNext methods for the WizardSequenceManager. Before we set a sequence pair, we always remove any previous links. The JWizard class lets you retrieve and set the sequence manager with the getManager and setManager methods.<
Validated Modeling
Wizards typically collect some kind of information and then act on that information. To make it as easy as possible to collect various kinds of information, we use a replaceable model called the DataCollectionModel. Listing 5 shows the interface for this model which has methods to set, get and remove values, test for the existence of a field and lets you register and unregister a change listener. The listener is automatically registered by the JWizard class when the setModel method is called.
The PropertyDataModel is the default implementation of our DataCollectionModel interface. It extends the Java Properties class and simply stores and retrieves relevant values in a Properties object. Listing 6 shows how the addChangeListener method adds a listener to the listeners Vector. The fireChangeEvent method dispatches a ChangeEvent to registered listeners. Notice that we clone the list before iterating through the listeners to avoid concurrency problems. The setValue method puts the value into the Properties set and fires the change event. These events are fired only when the model changes, so set and remove methods are the only ones that trigger it.
Listing 7 shows the WizardValidator interface, which contains only two methods, canMoveForward and canMoveBackward. These are verified, for the active page, whenever JWizard gets notified that a change took place in the model. Based on the response, the Next button may be active or inactive and the Back button may disappear.
Paneling Wizardry
The WizardPanel class extends JPanel and implements the WizardValidator interface. Listing 8 shows the source code for WizardPanel. The constructor lets you specify a title and/or description for your page. These are both optional, since setting them to null skips over them. The title is a large font, underlined text label at the top of the page and the description is a word-wrapped text description that you might use to explain what the user is expected to do on this page. These elements are so common that it makes sense to keep this functionality in the superclass. They use the north part of a BorderLayout and leave you free to use the rest. Normally, you would add a panel to the center of the page and place your user interface elements on that panel.
The WizardPanel uses a pair of member variables to keep track of the canMoveForward and canMoveBackward flags for the WizardValidator interface. As a subclass, you can change these directly at your leisure. Also provided, are a pair of utility methods for accessing the DataCollectionModel and SequenceManager more easily. The getDataModel and getSequenceManager calls get and effectively cast the required information from the JWizard (parent) container.
In Practice
To put the JWizard class to use, you need to place it in a Window or Dialog box. Since JWizard is an extension to JPanel, you can embed wizards into any interface, not necessarily in a separate window. Listing 9 shows the source code for JWizardTest, which extends JFrame and sets the size to match a typical Microsoft Wizard. You can resize it if you like. We then create a JWizard instance with JWizard.gif as the left-hand image and add the various WizardPanel (extension) pages. Finally, we set the first panel and add the JWizard reference to the center of the Frame. The main method creates a JWizardTest object and displays it on the screen.
The JWizard widget is a powerful tool for developing wizards under Swing. It demonstrates the use of several, reusable interfaces and a flexible design. While it may not do everything you could possibly want, you are free to extend it if you need to. The foundation is strong enough to apply in a production environment. I hope it serves you well. Next month, we’ll take a look at a JGraphicTree widget that lets you create connected component trees with various alignment and orientation choices.
Listing 1
private void setActive(Component comp, boolean enabled)
{
comp.setVisible(enabled);
comp.setEnabled(enabled);
if (comp instanceof Container)
{
Container cont = (Container)comp;
int count = cont.getComponentCount();
for (int i = 0; i < count; i++)
{
setActive(cont.getComponent(i), enabled);
}
}
}
public void show(Container parent, int index)
{
synchronized (parent.getTreeLock())
{
checkLayout(parent);
if (index < 0 || index > parent.getComponentCount() - 1)
return;
int ncomponents = parent.getComponentCount();
for (int i = 0 ; i < ncomponents ; i++)
{
Component comp = parent.getComponent(i);
if (comp.isVisible())
{
setActive(comp, false);
comp = parent.getComponent(index);
setActive(comp, true);
parent.validate();
return;
}
}
}
}
Listing 2
public void addPanel(String name, Component panel)
{
if (index < 0)
{
manager.setFirst(name);
manager.setCurrent(name);
}
else
{
manager.setPrevious(name, getName(index));
}
add(name, panel);
index++;
}
public void setPanel(String name)
{
if (name == "") return;
manager.setCurrent(name);
layout.show(this, name);
}
Listing 3
public interface SequenceManager
{
public String getFirst();
public void setFirst(String name);
public String getCurrent();
public void setCurrent(String name);
public String getNext(String name);
public void setNext(String name, String next);
public String getPrevious(String name);
public void setPrevious(String name, String previous);
}
Listing 4
protected String first;
protected String current;
protected Hashtable next;
protected Hashtable prev;
public String getNext(String name)
{
if (!next.containsKey(name)) return "";
return (String)next.get(name);
}
public void setNext(String name, String link)
{
if (next.containsKey(name)) next.remove(name);
if (prev.containsKey(link)) prev.remove(link);
next.put(name, link);
prev.put(link, name);
}
Listing 5
public interface DataCollectionModel
{
public boolean hasValue(String name);
public Object getValue(String name);
public Object getValue(String name, Object def);
public void setValue(String name, Object value);
public void removeValue(String name);
public void addChangeListener(ChangeListener listener);
public void removeChangeListener(ChangeListener listener);
}
Listing 6
public void addChangeListener(ChangeListener listener)
{
listeners.addElement(listener);
}
public void fireChangeEvent()
{
Vector list = (Vector)listeners.clone();
ChangeEvent event = new ChangeEvent(this);
ChangeListener listener;
for (int i = 0; i < list.size(); i++)
{
listener = (ChangeListener)list.elementAt(i);
listener.stateChanged(event);
}
}
public void setValue(String name, Object value)
{
put(name, value);
fireChangeEvent();
}
</PRE>
<PRE><B>Listing 7</B>
public interface WizardValidator
{
public boolean canMoveForward();
public boolean canMoveBackward();
}
Listing 8
protected boolean canMoveForward = false;
protected boolean canMoveBackward = true;
public WizardPanel(String name, String description)
{
setLayout(new BorderLayout());
JPanel north = new JPanel();
north.setLayout(new BorderLayout());
if (name != null)
{
JLabel title = new JLabel(name);
title.setFont(new Font("Helvetica", Font.PLAIN, 24));
title.setBorder(new EdgeBorder(EdgeBorder.SOUTH));
north.add("North", title);
}
if (description != null)
{
JTextArea explain = new JTextArea(description);
explain.setFont(getFont());
explain.setBorder(new EmptyBorder(15, 2, 5, 15));
explain.setWrapStyleWord(true);
explain.setEditable(false);
explain.setLineWrap(true);
explain.setOpaque(false);
north.add("Center", explain);
}
add("North", north);
}
public boolean canMoveForward()
{
return canMoveForward;
}
public boolean canMoveBackward()
{
return canMoveBackward;
}
public DataCollectionModel getDataModel()
{
return ((JWizard)getParent().getParent()).getModel();
}
public SequenceManager getSequenceManager()
{
return ((JWizard)getParent().getParent()).getManager();
}
Listing 9
public class JWizardTest extends JFrame
{
public JWizardTest()
{
super("JWizard Test");
setBounds(100, 100, 479, 357);
JWizard wizard = new JWizard("JWizard.gif");
wizard.addPanel("info", new InformationPanel());
wizard.addPanel("favorites", new FavoritesPanel());
wizard.addPanel("java", new ResultPanel("Java", "100% Pure Genius!"));
wizard.addPanel("cpp", new ResultPanel("C/C++", "You'll see..."));
wizard.addPanel("pascal", new ResultPanel("Pascal", "Oh well..."));
wizard.addPanel("smalltalk", new ResultPanel("SmallTalk", "Object Oriented"));
wizard.addPanel("cobol", new ResultPanel("Cobol", "Year 2k problems?"));
wizard.addPanel("fortran", new ResultPanel("Fortran", "Try Mathematica!"));
wizard.setFirst();
getContentPane().setLayout(new BorderLayout());
getContentPane().add("Center", wizard);
}
public static void main(String[] args)
{
JWizardTest wiz = new JWizardTest();
wiz.show();
}
}