The Wizard Magically Reappears

by Jo Asplin

After disappearing in a puff of smoke with the introduction of Qt 4, QWizard is now once again part of Qt. QWizard and its new apprentice, QWizardPage, spent their brief time in Qt 4 Solutions well, and are now loaded with features that will make the wizard programmer's job easier than ever.

QWizard provides a framework for writing wizards (also called assistants). The purpose of a wizard is to guide the user through a process step by step. Compared with the QWizard class found in Qt 3 and the Q3Wizard compatibility class of Qt 4, the new wizard provides the following features:

Wizard-Styles

In this article, we will focus on the control and data flow aspects of wizards: What happens when the user navigates between pages, when is the user allowed to navigate between pages, and how do we access data on which to base these decisions?

A Ferry Booking Example

We will use a ferry trip booking wizard to illustrate a number of the concepts of the new wizard framework. The five pages involved are intentionally very simple: A page for selecting a sailing date, a page for entering the name of a single passenger, a page for choosing a cabin type, a page for specifying a car registration number in case you want to bring your car, and finally a page for entering a credit card number.

The following diagram depicts the possible navigation paths through the wizard:

Ferryexample

The initial implementation looks like this:

    class BookingWizard : public QWizard
    {
    public:
        BookingWizard();
    
        QString sailingDate() const
            { return sailing->selectedDate().toString(); }
        QString passengerName() const
            { return passenger->text(); }
        ...
    
    private:
        QCalendarWidget *sailing;
        QLineEdit *passenger;
        ...
    };
    
    class SailingPage : public QWizardPage
    {
    public:
        SailingPage(QCalendarWidget *sailing) {
            setTitle(tr("Sailing"));
            setLayout(new QVBoxLayout);
            sailing->setMinimumDate(
                    QDate::currentDate().addDays(1));
            layout()->addWidget(sailing);
        }
    };
    
    ...
    
    BookingWizard::BookingWizard()
    {
        setWindowTitle(tr("Ferry Trip Booking Wizard"));
    
        sailing = new QCalendarWidget;
        addPage(new SailingPage(sailing));
    
        passenger = new QLineEdit;
        addPage(new PassengerPage(passenger));
    
        ...
    }

The following code snippet shows how to open the wizard and collect results from it:

    BookingWizard wizard;
    if (wizard.exec()) {
        qDebug() << "booking accepted";
        qDebug() << "sailing date:" << wizard.sailingDate();
        qDebug() << "passenger:" << wizard.passengerName();
        ...
    }

Notice how the BookingWizard class has to keep pointers to the input widgets of all the pages in order to implement its public interface. In addition, if a page needs to access an input widget of another page, the pointer to the input widget would have to be passed to both pages.

Registering and Using Fields

To solve the problems identified above, we can use QWizard's field mechanism. A field consists of a widget instance, one of the widget's properties (representing the value of the widget), the signal to inform us about changes to the property (typically there is only one such signal), and finally a name which must be unique within the wizard.

By referring to a field name and knowing how to convert its value from a QVariant, we can conveniently access fields in a uniform way across the wizard. The field mechanism also encourages us to keep the details of creating input widgets local to the respective wizard pages.

Here is how the booking wizard is converted to represent its input widgets as fields:

    class BookingWizard : public QWizard
    {
    public:
        BookingWizard();
    
        QString sailingDate() const
            { return field("sailing").toString(); }
        QString passengerName() const
            { return field("passenger").toString(); }
        ...
    };
    
    class SailingPage : public QWizardPage
    {
    public:
        SailingPage()
        {
            QCalendarWidget *sailing = new QCalendarWidget;
            registerField("sailing", sailing, "selectedDate",
                          SIGNAL(selectionChanged()));
            ...
        }
    };
    
    class PassengerPage : public QWizardPage
    {
    public:
        PassengerPage()
        {
            QLineEdit *passenger = new QLineEdit;
            registerField("passenger", passenger);
            ...
        }
    };
    
    ...

In order for the Sailing page to have its QCalendarWidget correctly recognized as a field, we call registerField() like this in the SailingPage constructor:

    registerField("sailing", sailing, "selectedDate",
                  SIGNAL(selectionChanged()));

For the Passenger page, we simply call

    registerField("passenger", passenger);

What about the property and the change signal? These could have been passed as the third and fourth arguments to registerField(), but by omitting them (effectively passing null pointers instead), we tell QWizardPage that we would like to use default values here. QWizardPage knows about the most common types of input widgets. Since QLineEdit is among the lucky ones (a subclass would also do), the text property and the textChanged() signal is automatically used.

Alternatively, we could have added QCalendarWidget to QWizard's list of recognized field types once and for all:

    setDefaultProperty("QCalendarWidget", "selectedDate",
                       SIGNAL(selectionChanged()));

Validate Before It's Too Late

If some information in the wizard is invalid or inconsistent (e.g., the passenger name is empty), it is currently not detected until after the wizard is closed. The wizard would then have to be reopened, and all the information, including the field that was incorrect in the first place, would have to be entered again. This is a very tedious, error-prone, and repetitive process that defeats the purpose of using a wizard.

When hitting Next or Finish to accept the current state of a wizard, the user would intuitively expect the result to be acceptable. We would like errors to be caught and dealt with as early as possible.

Let's see how we can improve our booking wizard. How can we ensure that the passenger name is not empty before proceeding to the Cabin page? It turns out that we get the validation we're after almost for free when we represent the passenger name as a field. All it takes is to register the field as a mandatory field by appending an asterisk to the name:

    registerField("passenger*", passenger);

When we query its value, the field is still referred to using its regular name (without the asterisk), but being a mandatory field, it is required to be filled (i.e., have a value different from the one it had at the time of registration) before the Next button is enabled. The default value of a QLineEdit is an empty string, so we are not allowed to proceed to the Cabin page until we enter a non-empty passenger name.

But how does the wizard know when to check a mandatory field for updates? Simple: QWizard automatically connects to the change notification signal associated with the field; e.g., textChanged() for QLineEdit. This ensures that the Next or Finish button is in the correct "enabled" state at all times.

What if a simple update check is not sufficient to validate a page? Assume for example that there are no departures on Sundays. How can we ensure that it is impossible to proceed to the Passenger page as long as a Sunday is selected as sailing date? A mandatory field would not work in this case, because there are many invalid values (i.e., all Sundays), and there is no way we could have all of these represent the invalid initial value to compare the field against.

We essentially need a way to program the validation rule ourselves. This is achieved by reimplementing the virtual function, QWizardPage::isComplete():

    bool SailingPage::isComplete() const
    {
        return field("sailing").toDate().dayOfWeek()
               != Qt::Sunday;
    }

We also need to emit the QWizardPage::completeChanged() signal every time isComplete() may potentially return a different value, so that the wizard knows that it must refresh the Next button. This requires us to add the following connect() call to the SailingPage constructor:

    connect(sailing, SIGNAL(selectionChanged()),
            this, SIGNAL(completeChanged()));

Initializing a Page

We would now like to ensure that the contents of a page is filled with sensible values every time the page is entered. Let's assume that an additional, slightly more expensive cabin type is available on Saturdays. By reimplementing the virtual function QWizardPage::initializePage(), we can populate the QComboBox representing the cabin types whenever we enter the Cabin page (assuming the cabin combobox is a private member of SailingPage):

    void CabinPage::initializePage()
    {
        cabin->clear();
        cabin->addItem(tr("Cabin without window"));
        cabin->addItem(tr("Cabin with window"));
        if (field("sailing").toDate().dayOfWeek()
                == Qt::Saturday)
            cabin->addItem(tr("Luxury cabin with window and "
                              "champagne"));
    }

Notice how the CabinPage accesses a field registered by a previous page ("sailing") to determine the day of the week.

Skipping a Page in the Middle

We are now going to support another peculiarity of the ferry company: Cars are not allowed on Saturdays. If the user has selected Saturday as the sailing date, the Car page is skipped altogether.

Skipcar

By default, the wizard pages are presented in a strictly linear order: For any given page, there is only one possible page that can be arrived from, and only one possible page to proceed to. For instance, the Payment page of our ferry example can only be arrived at from the Car page, and the Car page is the only page we can proceed to from the Cabin page.

We will now relax this restriction so that pushing the Next button on the Cabin page takes us to either the Car page or straight to the Payment page depending on the selected sailing date. As a consequence, the Payment page may be reached directly from either the Cabin page or the Car page.

The non-linear behavior is achieved by reimplementing the virtual function, QWizardPage::nextId(). This function is evaluated by the wizard when the Next button is pressed to determine which page to enter. In order for this to work, we need to associate a unique ID with each page. The way a unique ID is assigned to a page depends on how the page is registered in the wizard: addPage() assigns an ID that is greater than any other ID so far, while setPage() accepts the ID as an argument.

The base implementation of nextId() simply returns the next ID in the increasing sequence of registered IDs. Reimplementing nextId() works best in combination with setPage() as illustrated by the following example:

    class BookingWizard : public QWizard
    {
    public:
        enum { Sailing, Passenger, Cabin, Car, Payment };
        ...
    };
    int CabinPage::nextId() const
    {
        if (field("sailing").toDate().dayOfWeek()
                == Qt::Saturday) {
            return BookingWizard::Payment;
        } else {
            return BookingWizard::Car;
        }
    }
    BookingWizard::BookingWizard()
    {
        ...
        setPage(Car, new CarPage);
        setPage(Payment, new PaymentPage);
        ...
    }

Use nextId() with care to avoid cycles and non-existent page IDs. Fortunately, QWizard will warn about these cases at run-time.

Skipping the Last Page

To illustrate a slightly different use of nextId(), let's drop the rule about Saturdays from the previous section and assume instead that passengers whose name contains "wizard" don't have to pay for the trip at all — for them, the Car page should be the final page.

Skippayment

We achieve this by letting nextId() return {-}1 if the Payment page should be skipped. As it happens, nextId() is not only evaluated when the Next button is pressed to determine which page is the next page, but also upon entering a new page to find out whether there is a next page at all. If there is no next page, the Finish button should replace the Next button. Here is the code:

    int CarPage::nextId() const
    {
        if (field("passenger").toString()
               .contains(tr("wizard"), Qt::CaseInsensitive)) {
            return -1;
        } else {
            return BookingWizard::Payment;
        }
    }

This of course assumes that the passenger field doesn't change as long as we're on the Car page.

Summary

The bare bones example we have shown demonstrates how easily wizard navigation can be programmed using the new QWizard and QWizardPage classes. Most importantly, we have seen how reimplementing virtual functions lets us answer some common questions about wizard navigation: When is the user allowed to move to the next page, which page is to be considered the next one, and on which page can the user accept the current input and close the wizard? In addition to providing easy access to data across the wizard, the field mechanism we have used offers a basic but extremely convenient type of page validation through mandatory fields.


This document is licensed under the Creative Commons Attribution-Share Alike 2.5 license.

Copyright © 2007 Trolltech Trademarks