Skip to main content

Using the Wizard API

February 28, 2006

{cs.r.title}







This article discusses complex user-interface components called wizards, which you probably use almost every day. But did you know that you can use them in your own Java applications? Wizards help solve common tasks--for example, setting up a network connection or copying pictures from your digital camera onto your computer--by guiding you through such processes step by step.

In the first section we will take a closer look at what wizards are, how they work, and, of course, why you should consider using them in your programs. I will then introduce you to a project hosted here at java.net which provides a very easy-to-use implementation of a wizard component. In the remaining sections you will learn how to obtain, install, and use this library.

What Are Wizards?

To get an understanding of how wizards work, let us examine one of my introductory examples in greater detail. The process of copying pictures from your digital camera onto your computer involves several steps. These are:

  1. Connecting the device
  2. Specifying the source
  3. Choosing which images should be transferred and deciding whether they should be copied or moved
  4. specifying the destination
  5. Transferring the pictures
  6. Optionally removing the pictures from the camera

The scanner and camera wizard of Windows XP, which you can see in Figure 1, implements these steps.

Microsoft Windows scanner and camera wizard
Figure 1. Microsoft Windows scanner and camera wizard

You can cycle through these steps using the Next and Previous buttons or cancel the whole process. The camera wizard will start transferring pictures as soon as the user has finished step 4, which means that all information has been gathered. Please keep that in mind; I will turn back to this implementation detail later. Let's now have a look at another wizard, shown in Figure 2.

The NetBeans IDE New Project wizard
Figure 2. The NetBeans IDE New Project wizard

The NetBeans IDE makes use of wizards on several occasions. For example, it guides you through the process of creating a new project using a wizard, which you can see in Figure 2. The steps involved here are:

  1. Choosing the type of project
  2. Specifying its name and location

Setting aside some minor differences, the overall visual appearance of both wizards seems similar. Still, there is one fundamental difference. The NetBeans wizard will not create projects, files, or directories until you explicitly click the Finish button. But Microsoft's camera wizard starts copying or moving pictures automatically; the odd consequence is that after it has finished its job, you can still click Back, even though there are no more pictures if you decided to delete them from the camera. The underlying question is whether actually carrying out the task should part of the wizard. This, as we have seen, is implementation-dependent.

So, what is a wizard? From a user's point of view, wizards help with complex tasks by breaking them into logical pieces. These pieces are represented by panels or dialogs that display information or require input. The user can navigate through these steps and even alter previously entered data until she has completed all required steps. From the programmer's perspective, a wizard collects information: it comprises one transaction. This means that no changes should be made until the user has completed all the steps and finally clicked on a Finish button.

Wizards have gained widespread acceptance in recent years because they divide complex tasks into small, manageable pieces. They are understood easily, even by inexperienced users. Additionally, previously made decisions can be altered by simply revisiting an earlier panel.

The Wizard Project at java.net

The Wizard project is a subproject of SwingLabs, which allows experimentation with extensions to existing Swing components as well as completely new ones. These new or enhanced components might be included in future versions of the JDK. Wizard is a class library that aims to provide a simple API for writing wizards. Its goal is to offer an easy-to-use solution that enables any Swing application to provide wizards with minimal code and effort.

To use the Wizard API, you can check out its sources from the CVS repository and build it on your own, or you can download a ready-to-use JAR file and documentation. Visit the Resources section for details. To use the JAR, remember to expand the class path of your application or to copy the Wizard class library to the extension directory of your Java runtime (my article The Java Extension Mechanism contains an in-depth-discussion of this Java feature).

A Simple Example

After quite a lot of theory, you probably want to see a wizard do its magic. Please have a look at WizardPanelProviderDemo.java.

package wizarddemo;

import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JPanel;
import org.netbeans.api.wizard.WizardDisplayer;
import org.netbeans.spi.wizard.Wizard;
import org.netbeans.spi.wizard.WizardController;
import org.netbeans.spi.wizard.WizardException;
import org.netbeans.spi.wizard.WizardPage;
import org.netbeans.spi.wizard.WizardPanelProvider;

public class WizardPanelProviderDemo
              extends WizardPanelProvider {
   
   private static final String ID = "page1-id";
   
   public WizardPanelProviderDemo() {
      super("A Demo Wizard",
                new String [] {ID},
                        new String [] {"Page #1"});
   }

   protected JComponent createPanel(
                     final WizardController wizardController,
                                         String str, final Map map) {
      if (str.equals(ID) == false) {
         System.err.println("I should
                            never need to print that!!");
         return null;
      } else {
         JPanel p = new JPanel(
                             new FlowLayout(FlowLayout.LEADING));
         final JCheckBox cb =
                  new JCheckBox("You must select me to finish", false);
         ActionListener al = new ActionListener() {
            public void actionPerformed(ActionEvent e) {
               boolean sel = cb.isSelected();
               wizardController.setProblem(
                                 sel ? null : "please select the checkbox");
               map.put("boxSelected", 
                                      sel ? Boolean.TRUE : Boolean.FALSE);
            }
         };
         cb.addActionListener(al);
         al.actionPerformed(new ActionEvent(cb,
                         0, cb.getActionCommand()));
         p.add(cb);
         return p;
      }
   }
   
   protected Object finish(Map settings) throws WizardException {
      Set keys = settings.keySet();
      Iterator it = keys.iterator();
      while (it.hasNext()) {
         Object key = it.next();
         System.out.println(key + "=" + settings.get(key));
      }
      return settings;
   }
   
   public static void main(String [] args) {
      WizardPanelProvider provider = new WizardPanelProviderDemo();
      Wizard wizard = provider.createWizard();
      Object result = WizardDisplayer.showWizard(wizard);
   }
}

This creates the Wizard GUI shown in Figure 3.

WizardPanelProviderDemo
Figure 3. WizardPanelProviderDemo

Creating and displaying the wizard shown in Figure 3 consists of three steps:

  1. Preparing the wizard by extending org.netbeans.spi.wizard.WizardPanelProvider
  2. Creating an instance of org.netbeans.spi.wizard.Wizard
  3. Displaying the wizard using org.netbeans.api.wizard.WizardDisplayer
WizardPanelProvider is the ideal base class for creating a wizard based on a fixed set of panels. These panels consist of a human-readable, localized description and a unique identification string. Descriptions and IDs are passed to the constructor of WizardPanelProvider. You simply need to extend this class and implement the method createPanel() which should create and return an instance of a panel based on the supplied ID. WizardPanelProviderDemo contains just one page, so I don't need to check this parameter; more complex wizards must of course do so. Please note that there is no need to explicitly call createPanel(). This is done automatically for each ID passed to the constructor.

The wizard is created in the second step. You just have to invoke createWizard() of your WizardPanelProvider subclass. This wizard can then be displayed by calling WizardDisplayer.showWizard().

Now that you have a basic understanding of how to create a wizard I would like to discuss some important concepts of the Wizard API.

Underlying Concepts

The main idea of a wizard is to collect user input and have it available when its Finish button is pressed. The Wizard API maintains a java.util.Map to store this data. The map is passed to a method called finish() when the wizard completes successfully. finish() is an instance method of WizardPanelProvider, which you need to extend anyway. The method should return whatever object the wizard creates from its gathered data. This may be the map that was passed to finish(). The default implementation returns null.

You may be wondering who populates the settings map. Obviously, it should be updated whenever the user enters data or modifies previously typed text. To get informed of these changes, you can add appropriate listeners. For example, I have added an ActionListener to my JCheckBox. Inside actionPerformed(), the put() method is invoked on the map that was originally passed to createPanel(). As you will see shortly, the Wizard API can help you automate this task.

But I would like to introduce an important method to control navigation first. If you need to prevent the user from moving to the next page you can call setProblem() on the WizardController instance which was passed to createPanel(). Passing null means that there is no problem, and therefore navigation is enabled and Next and Finish can be pressed. In any other case, a message is displayed at the bottom of the wizard. You should, however, consider carefully whether you really need to block navigation: it makes sense when (and only when) the user has yet to enter data that is vital to finish the wizard.

Using WizardPage

My first example extends WizardPanelProvider to define the wizard. Another approach is to use org.netbeans.spi.wizard.WizardPage. This class is similarly easy to work with. Its main advantage is that it can populate the settings map automatically. To see how that works, please have a look at the following WizardPageDemo.java.

package wizarddemo;

import java.awt.FlowLayout;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import javax.swing.JCheckBox;
import org.netbeans.api.wizard.WizardDisplayer;
import org.netbeans.spi.wizard.Wizard;
import org.netbeans.spi.wizard.WizardException;
import org.netbeans.spi.wizard.WizardPage;
import org.netbeans.spi.wizard.WizardPage.WizardResultProducer;

public class WizardPageDemo implements WizardResultProducer {
   
public WizardPageDemo() {
   WizardPage page1 = new WizardPage("page1-id",
                                     "Page #1");
   JCheckBox cb = new JCheckBox(
                  "You can select me", false);
   cb.setName("checkbox-1");
   page1.setLayout(new FlowLayout(
                   FlowLayout.LEADING));
   page1.add(cb);
   Wizard wizard = WizardPage.createWizard(
                   new WizardPage [] {page1}, this);
   Object result = WizardDisplayer.showWizard
                    (wizard);
   }

   public static void main(String [] args) {
      new WizardPageDemo();
   }

   /*
    * WizardResultProducer interface
    */

   public Object finish(Map settings)
                  throws WizardException {
      Set keys = settings.keySet();
      Iterator it = keys.iterator();
      while (it.hasNext()) {
         Object key = it.next();
         System.out.println(key + "=" +
                             settings.get(key));
      }
      return settings;
   }
}

Creating this wizard consists of these steps:

  1. Instantiate WizardPage and add components
  2. Invoke setName() on any component which should appear in the settings map
  3. Create the wizard using WizardPage.createWizard()
WizardPage.createWizard() takes an array of WizardPage objects. To get informed when the wizard has finished successfully you can implement the WizardResultProducer interface and pass an instance of your class as an additional argument.

WizardPage and WizardPanelProvider work very well for wizards with a fixed number of panels whose order is specified prior to execution. But what if the successor to a panel depends on user input? Think of a wizard to set up a connection to the internet. The required steps greatly depend on the desired connection method. The Wizard API can handle such situations, too.

Branches

The WizardBranchController class is used to dynamically change the order of panels. The class is abstract so you need to subclass it. Its constructor takes one argument, which provides the initial panels of a wizard. When the user reaches the last one, the WizardBranchController will be asked for a wizard to provide subsequent panels.

To decide what the next step is, your implementation of WizardBranchController should check the settings map. The base wizard must have put this information there, of course. To see how this works, I have implemented a small demo called WizardBranchControllerDemo.java. The complete source is in the sample code file wizarddemo.zip (see the Resources section for details), but for now, here is the method that does the check.

protected WizardPanelProvider 
   getPanelProviderForStep(String step,
   Map settings) {
   String str;
   str = (String)settings.get("combobox");
   if (str.equals("red")) {
      return wppRed;
   } else if (str.equals("yellow")) {
      return wppYellow;
   } else {
      return wppGreen;
   }
}

Your subclass of WizardBranchController must override one of the methods getWizardForStep() or getPanelProviderForStep(). Which one you choose depends on how you plan to create your subsequent panels. I have implemented getPanelProviderForStep() because this way I was able to reuse the class that implements the first panel. The default implementation of getWizardForStep() calls getPanelProviderForStep() and returns the result of this call.

Conclusion

The Wizard API is a very easy-to-use extension to the Swing core classes. As wizards have become mainstream, I think it would be very desirable to add Wizard-like behavior to a future release of the Java platform.

Although the Wizard API is still in development, it is very stable. According the project home page, things to do include the addition of post-finish summary panels and a possible change of the package name to net.java.

Resources

width="1" height="1" border="0" alt=" " />
Thomas Kunneth works as a software architect at the German authorities, specializing in Java-based rich clients.
Related Topics >> GUI   |   Swing   |