HomeDigital EditionSys-Con RadioSearch Java Cd
Advanced Java AWT Book Reviews/Excerpts Client Server Corba Editorials Embedded Java Enterprise Java IDE's Industry Watch Integration Interviews Java Applet Java & Databases Java & Web Services Java Fundamentals Java Native Interface Java Servlets Java Beans J2ME Libraries .NET Object Orientation Observations/IMHO Product Reviews Scalability & Performance Security Server Side Source Code Straight Talking Swing Threads Using Java with others Wireless XML
 

User interface design can be a real struggle when one of the requirements is to make programs accessible to a larger market. The most suitable metaphor for a given domain may not be simple enough for inexperienced users, yet the program still has to address their needs. If you can explicitly lead users through each step 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. The WizardImage panel on the left 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 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).

Figure 1
Figure 1:

The JWizard framework design lets you drop components, usually JPanel objects, in order and then manage the sequence more explicitly. It uses a WizardSequenceManager, 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 choose.

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't 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 of these values to null for 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 collect some personal information and ask a simple question before producing a result panel. The logic for this simple application could become complicated if the framework didn't handle it well. The flow from one panel to the next is shown in Figure 2.

Figure 2
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 calls 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 checks the WizardValidator interface to activate the button(s) as appropriate. The Next button becomes active only 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 then sets the next panel with the setNext method. At the same time, it sets the subsequent panel's next value to a null ("") string, which indicates a final panel in the sequence. When JWizard sees this, it replaces the Next button with a Finish button.

At the end of this sequence, if you press the Finish button, it will print out the PropertiesDataModel so you can see what you've collected. You can find all this code on the JDJ Web site (www.sys-con.com), along with the source for the entire JWizard framework. To try this example, just run the JWizardTest class.

Stacking the Deck
The DeckLayout and DeckPanel classes are fairly simple. The DeckLayout is similar to the CardLayout manager that comes with 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 also makes them visible or invisible depending on the Boolean argument. The show method looks for and disables the active page before activating a new page.

Listing 2 shows a couple of calls from the DeckPanel class. In particular, the addPanel method automatically sets the sequence manager to the order in which panels are added. The first panel is set as the first active panel in the sequence; all others are set as subsequent to the previous panel. This simplifies 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. Page names are used to keep things simple. The concrete implementation keeps track of the first and current positions, and holds two Hashtables that 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 WizardSequence-Manager. Before setting 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 it. 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 allow you to 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 so we can 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 the 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 is 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 (see Listing 8 for WizardPanel's source code). The constructor lets you specify a title and/or description for your page. These are both optional since setting them to null ignores them. The title is an 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 the 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 easily accessing the DataCollectionModel and SequenceManager. The getDataModel and getSequenceManager calls get and effectively casts 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're free to extend it if needed. 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 JComponentTree widget that lets you create connected component trees with various alignment and orientation choices.

About the Author
Claude Duguay has been programming since 1980. In 1988 he founded LogiCraft Corporation, and he currently leads the development team at Atrieva Corp. You can contact him with questions and comments at [email protected]

	

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(); 
} 
  

Listing 7
 
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(); 
} 
} 

}  
  
      

Download Assoicated Source Files (Zip file format - 60.1 KB)
 

All Rights Reserved
Copyright ©  2004 SYS-CON Media, Inc.
  E-mail: [email protected]

Java and Java-based marks are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries. SYS-CON Publications, Inc. is independent of Sun Microsystems, Inc.