The Wizard Magically Reappears

Материал из Wiki.crossplatform.ru

Перейти к: навигация, поиск

__NOTOC__

by Jo Asplin

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

QWizard provides a framework for writing wizards (also calledassistants). The purpose of a wizard is to guide the user through aprocess step by step. Compared with the QWizard class foundin Qt 3 and the Q3Wizard compatibility class of Qt 4, the newwizard provides the following features:

  • A native look and feel on all platforms
    QWizard supports four different looks — Classic (Windows 95 and X11), Modern(Windows 98 to XP), Aero (Windows Vista), and Mac (Mac OS X). By default, QWizard automatically chooses the most appropriate style for theuser's platform, but this can be overridden.

center

  • A more powerful and convenient API
    The new API makes iteasy to enable or disable the Next and Finish buttonsbased on the dialog's contents, and to exchange information betweenpages.
  • Support for non-linear wizards
    Non-linear wizards allow different traversal pathsbased on the information provided by the user.

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

Содержание

A Ferry Booking Example

We will use a ferry trip booking wizard to illustrate a number ofthe concepts of the new wizard framework. The five pages involved areintentionally very simple: A page for selecting a sailing date, apage for entering the name of a single passenger, a page for choosinga cabin type, a page for specifying a car registration number in caseyou want to bring your car, and finally a page for entering a creditcard number.

The following diagram depicts the possible navigation paths throughthe wizard:

center

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 collectresults 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 theinput widgets of all the pages in order to implement its publicinterface. In addition, if a page needs to access an input widget ofanother page, the pointer to the input widget would have to be passedto both pages.

Registering and Using Fields

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

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

Here is how the booking wizard is converted to represent its inputwidgets 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 QCalendarWidgetcorrectly recognized as a field, we call registerField() likethis 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 beenpassed as the third and fourth arguments to registerField(), butby 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 alsodo), the text property and the textChanged() signal isautomatically 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 untilafter the wizard is closed. The wizard would then have to bereopened, and all the information, including the field that was incorrectin the first place, would have to be entered again. This is a verytedious, error-prone, and repetitive process that defeats thepurpose of using a wizard.

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

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

registerField("passenger*", passenger);

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

But how does the wizard know when to check a mandatory field forupdates? Simple: QWizard automatically connects to the changenotification signal associated with the field; e.g.,textChanged() for QLineEdit. This ensures that theNext 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 weensure that it is impossible to proceed to the Passenger page aslong as a Sunday is selected as sailing date? A mandatory field would notwork in this case, because there are many invalidvalues (i.e., all Sundays), and there is no way we could have all ofthese represent the invalid initial value to compare the fieldagainst.

We essentially need a way to program the validation ruleourselves. 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() signalevery 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 theSailingPage constructor:

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

Initializing a Page

We would now like to ensure that the contents of a page isfilled with sensible values every time the page is entered. Let'sassume that an additional, slightly more expensive cabin type isavailable on Saturdays. By reimplementing the virtual functionQWizardPage::initializePage(), we can populate the QComboBoxrepresenting the cabin types whenever we enter the Cabin page(assuming the cabin combobox is a private member ofSailingPage):

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 aprevious 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 Saturdayas the sailing date, the Car page is skipped altogether.

center

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

We will now relax this restriction so that pushing the Nextbutton on the Cabin page takes us to either the Car pageor straight to the Payment page depending on the selectedsailing date. As a consequence, the Payment page may be reacheddirectly from either the Cabin page or the Car page.

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

The base implementation of nextId() simply returns the next ID inthe increasing sequence of registered IDs. ReimplementingnextId() works best in combination with setPage() asillustrated 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 therule about Saturdays from the previous section and assume instead thatpassengers whose name contains "wizard" don't have to pay for the tripat all — for them, the Car page should be the final page.

center

We achieve this by letting nextId() return {-}1 if thePayment page should be skipped. As it happens, nextId()is not only evaluated when the Next button is pressed to determine whichpage is the next page, but also upon entering a new page to find outwhether there is a next page at all. If there is no next page, theFinish button should replace the Next button. Here isthe 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 longas we're on the Car page.

Summary

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