The Wizard Magically Reappears

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

(Различия между версиями)
Перейти к: навигация, поиск
(Регистрация и использование полей: перевод в движении)
(A Ferry Booking Example: перевод в движении)
Строка 18: Строка 18:
===A Ferry Booking Example===
===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:
+
Следующая диаграмма изображает возможные пути навигации с помощью мастера:
[[Image:qq22-ferryexample.png|center]]
[[Image:qq22-ferryexample.png|center]]
-
The initial implementation looks like this:
+
Исходная реализация выглядит подобно этой:
<source lang="cpp-qt">
<source lang="cpp-qt">
class BookingWizard : public QWizard
class BookingWizard : public QWizard
Строка 70: Строка 70:
}
}
</source>  
</source>  
-
The following code snippet shows how to open the wizard and collectresults from it:
+
Следующий фрагмент кода показывает как открыть мастер и собрать результаты из него:
<source lang="cpp-qt">
<source lang="cpp-qt">
BookingWizard wizard;
BookingWizard wizard;
Строка 80: Строка 80:
}
}
</source>  
</source>  
-
Notice how the <tt>BookingWizard</tt> 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.
+
Обратите внимание, как класс <tt>BookingWizard</tt> должен хранить указатели на виджеты ввода всех страниц для того, чтобы реализовать открытый интерфейс. Кроме того, если страница нуждается в доступе к виджету ввода с другой страницы, то указатель на виджет ввода должен быть передан обоим страницам.
-
 
+
===Регистрация и использование полей===
===Регистрация и использование полей===

Версия 17:59, 22 марта 2009

Image:qt-logo_new.png Image:qq-title-article.png
Qt Quarterly | Выпуск 22 | Документация


by Jo Asplin

После исчезновения в клубах дыма, с выходом Qt4, QWizard сново часть Qt. QWizard и его нвоый подмастерье, QWizardPage, отработавшие свое краткое время в Qt4 Solutions, и теперь насыщенные возможностями, которые сделают работу програмиста мастеров легче, чем когда либо.

Содержание


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:

  • Естественный внешний вид и поведение на всех платформах
    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

  • Более мощьный и удобный 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.
  • Поддержка нелинейных мастеров
    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

Мы будем использовать мастер заказов паромной переправы, чтобы проилюстрировать различные концепции нового каркаса мастеров. Пять используемых страниц, умыхленно сделаны очень простыми: Страница для выбора даты отплытия, страница для ввода имени единственного пассажира, страница для выбора типа купе, страница для указания госномера автомобиля, в случае если вы хотите перевести ваш автомобиль и, наконец, страница ввода номера кредитной карты.

Следующая диаграмма изображает возможные пути навигации с помощью мастера:

center

Исходная реализация выглядит подобно этой:

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

Следующий фрагмент кода показывает как открыть мастер и собрать результаты из него:

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

Обратите внимание, как класс BookingWizard должен хранить указатели на виджеты ввода всех страниц для того, чтобы реализовать открытый интерфейс. Кроме того, если страница нуждается в доступе к виджету ввода с другой страницы, то указатель на виджет ввода должен быть передан обоим страницам.

Регистрация и использование полей

Чтобы решить проблему идентификации, указанную выше, мы можем использовать механизм полей QWizard'а. Поле состоит из из экземпляра объекта, одного из свойств виджета (представляющее значение виджета), сигнала информирующего нас об изменении свойства (обычно имеется только один такой сигнал) и, наконец, имя, которое должно быть уникальным в пределах мастера.

Обращаясь к имени поля и зная как преобразовать его значение из QVariant, мы можем без труда получать доступ к полям в одинаковой форме через мастр. Механизм полей также позволяет нам хранить подробности создания виджетов ввода локально на соответствующих страницах мастера.

Вот как масетер заказов (booking) преобразован, чтобы представить его виджеты ввода как поля:

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

Для страницы Sailing (Отплытие), чтобы её QCalendarWidget был корректно распознан как поле, мы вызываем registerField() в конструкторе SailingPage:

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

Для страницы Passenger (Пасажир), мы просто вызываем:

registerField("passenger", passenger);

А что со свойством и сигналом изменения? Они могут быть переданы, как третий и четвертый аргументы в registerField(), но опуская их (в действительности передавая нулевой указатель), мы говорим QWizardPage, что мы хотели бы использовать здесь значение по умолчанию. QWizardPage знает о наиболее общих типах виджетов ввода. Так как QLineEdit из числа таких счастливчиков (и подкласс тоже), свойство text и сигнал textChanged() используются автоматически.

В качестве альтернативы, мы могли бы добавить QCalendarWidget в список распознаваемых типов полей QWizard'а, раз и на всегда:

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

Инициализация страницы

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.

Пропуск страницы в середине

Сейчас мы собираемся поддержать другую особенность паромной компании: по субботам автомобили на паром не допускаются. Если пользователь выбирает субботу для отплытия, страница Car (Автомобиль) пропускается полностью.

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.

Пропуск последней страницы

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.


Резюме

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.