The Wizard Magically Reappears
Материал из Wiki.crossplatform.ru
Qt Quarterly | Выпуск 22 | Документация |
by Jo Asplin
После исчезновения в клубах дыма, с выходом Qt4, QWizard сново часть Qt. QWizard и его новый подмастерье - QWizardPage, отработавшие свое краткое время в Qt4 Solutions, и теперь насыщенные возможностями, которые сделают работу програмиста мастеров легче, чем когда либо.
Содержание |
QWizard предоставляет каркас для написания мастеров (wizard). Цель мастера - провести пользователя через процесс шаг за шагом. Сравнивая класс QWizard предоставляемый Qt 3 и класс совместимости Q3Wizard из Qt 4, новый мастер предоствляет следующие особенности:
- Естественный внешний вид и поведение на всех платформах
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.
- Более мощьный и удобный 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?
[править] Пример "Книга заказов парома"
Мы будем использовать мастер заказов паромной переправы, чтобы проилюстрировать различные концепции нового каркаса мастеров. Пять используемых страниц, умышленно сделаны очень простыми: Страница для выбора даты отплытия, страница для ввода имени единственного пассажира, страница для выбора типа каюты, страница для указания госномера автомобиля, в случае если вы хотите перевести ваш автомобиль и, наконец, страница ввода номера кредитной карты.
Следующая диаграмма изображает возможные пути навигации с помощью мастера:
Исходная реализация выглядит подобно этой:
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()));
[править] Проверяйте, пока это не слишком поздно
Если какая-то информация в мастере не верна или недопустима (например, неуказано имя пассажира), она, в данный момент, не обнаруживается до тех пор пока мастер небудет закрыт. Мастер затем должен быть вновь открыт, и вся информация, включая поля, которые были не верны в первый раз, должны быть введены снова. Это очень утомительный, подверженный ошибкам, и повторяющийся процесс, который делает бессмысленной цель использования мастера.
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 (Автомобиль) пропускается полностью.
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.
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.