這篇算是簡單紀錄一下,怎麼用 Qt(官網)來寫出「一步一步進行下去」的精靈模式(Wizard mode)的程式。
Qt 的精靈模式,是從 Qt 4.3 開始引進的,基本上主要是兩個類別:QWizard(官方文件)和 QWizardPage(官方文件)來實作。
其中,QWizard 是提供精靈模式的一個大的框架(framework)、來處理整個精靈的流程;在一個精靈中,會有許多個 QWizardPage 的物件、個別代表精靈模式中的每一個頁面。
基本範例
下面就是官方給的一個極簡單的範例:
QWizard wizard; wizard.addPage(createIntroPage()); wizard.addPage(createRegistrationPage()); wizard.addPage(createConclusionPage()); wizard.setWindowTitle("Trivial Wizard"); wizard.show();
在這個範例裡面,QWizard 的物件 wizard 只要透過 addPage() 這個函式,來新增需要的頁面;而在最簡單的情況下,這樣透過 addPage() 加入的頁面,之後會依新增的順序顯示出來。
而在上面範例中的 createIntroPage()、createRegistrationPage() 和 createConclusionPage() 這三個函式,則是會各自回傳一個 QWizardPage 的物件指標,各自代表一個頁面。
下面就是官方的 createIntroPage() 內容:
QWizardPage *createIntroPage() { QWizardPage *page = new QWizardPage; page->setTitle("Introduction"); QLabel *label = new QLabel("This wizard will help you register your copy " "of Super Product Two."); label->setWordWrap(true); QVBoxLayout *layout = new QVBoxLayout; layout->addWidget(label); page->setLayout(layout); return page; }
這邊所回傳的 QWizardPage 的建立方法,基本上和一般的 QWidget 沒有太大的差別,可以很簡單地把需要的圖形介面元件放進去;而如果想要比較複雜的設計,也一樣可以透過 Designer 之類的工具來拉。
QWizardPage
實際上,QWizardPage 還有許多特別的函式,可以透過重新實作、來做一些額外的工作;這邊此主要包括了下面五個:
-
initializePage()
當頁面顯示時,會執行這個函式來進行頁面的初始化;如果希望在頁面出來的時候,可以根據當下狀況做改變,就需要重新實作這個函式。 -
cleanupPage()
當使用者按「上一步」(back)的時候,會執行這個函式。 -
validatePage()
當使用者和「下一步」(next)或「結束」(finish)的時候,會執行到這個函式;透過重新實作這個函式,可以在使用者按下按鈕的時候,針對其操作、輸入來做檢查,如果發現資料有問題的話,可以回傳 false、阻擋程式跳到下一頁。 -
nextId()
如果希望可以不要一頁一頁按順序跑的話,可以在這邊指定這一頁之後要跳到哪一頁。如果回傳 -1 的話,則就代表這是最後一頁。 -
isComplete()
這個函式是用來讓程式判斷這一頁是否已經完成,是否要讓「下一步」(next)或「結束」(finish)的按鈕可以使用。
當然,其他還有很多特殊的函式,可以用來快速地調整頁面的內容。這部分可以參考官方文件的「Elements of a Wizard Page」(連結)。
流程的控制
其中,透過 QWizardPage 的 nextId() 這個函式來做流程的控制,基本上是在較複雜的精靈中,應該會需要的功能;透過這項功能,可以讓精靈根據使用者的選擇、判斷接下來要跳到哪個頁面,如此一來就可以避掉很多實際上不需要讓使用者看的東西了~
在 Qt 官方文件中,也有一節「Creating Non-Linear Wizards」(連結),在講這件事。
如果是希望透過各個 QWizardPage 的 nextId() 函式,來作流程控管的話,除了可以靠 addPage() 這個函式回傳的值,來知道每個 QWizardPage 的編號外,也可以透過自行定義列舉型別(enum)、再改用 setPage() 來做 QWizardPage 的新增,這樣應該會更好撰寫。
下面就是官方給的示意程式:
class LicenseWizard : public QWizard { ... enum { Page_Intro, Page_Evaluate, Page_Register, Page_Details, Page_Conclusion }; ... }; LicenseWizard::LicenseWizard(QWidget *parent) : QWizard(parent) { setPage(Page_Intro, new IntroPage); setPage(Page_Evaluate, new EvaluatePage); setPage(Page_Register, new RegisterPage); setPage(Page_Details, new DetailsPage); setPage(Page_Conclusion, new ConclusionPage); ... }
而各個 QWizardPage 的 nextId(),則可以寫成下面的樣子:
int RegisterPage::nextId() const { if (upgradeKeyLineEdit->text().isEmpty()) { return LicenseWizard::Page_Details; } else { return LicenseWizard::Page_Conclusion; } }
如果覺得把流程控管拆到一堆類別裡面個別控制不方便,或是基於其他理由不能這樣做的話,Qt 的精靈模式也允許將流程控管的程式,整個寫在 QWizard 中。
如果想要這樣做的話,只要撰寫 QWizard 的 nextId() 就可以了。下面就是官方的範例:
int LicenseWizard::nextId() const { switch (currentId()) { case Page_Intro: if (field("intro.evaluate").toBool()) { return Page_Evaluate; } else { return Page_Register; } case Page_Evaluate: return Page_Conclusion; case Page_Register: if (field("register.upgradeKey").toString().isEmpty()) { return Page_Details; } else { return Page_Conclusion; } case Page_Details: return Page_Conclusion; case Page_Conclusion: default: return -1; } }
個人應該算是比較喜歡後面,把流程管理全部寫在一起的寫法吧。
頁面之間的變數溝通
由於每個 QWizardPage 都是個別獨立的類別,那整個精靈模式裡面,該如何去存取各個頁面的欄位資料呢?
在這部分,Qt 是採用了「field」的機制,來實作整個精靈共通的欄位讀取機制,這部分可以參考官方文件《Registering and Using Fields》(頁面)。
在使用上,基本上就是在每個 QWizardPage 中,透過 registerField() 這個函式,來把圖形介面中的特定元件登記在精靈環境中。
ClassInfoPage::ClassInfoPage(QWidget *parent) : QWizardPage(parent) { classNameLabel = new QLabel(tr("&Class name:")); classNameLineEdit = new QLineEdit; classNameLabel->setBuddy(classNameLineEdit); baseClassLabel = new QLabel(tr("B&ase class:")); ba seClassLineEdit = new QLineEdit; ba seClassLabel->setBuddy(ba seClassLineEdit); qobjectMacroCheckBox = new QCheckBox(tr("Generate Q_OBJECT ¯o")); registerField("className*", classNameLineEdit); registerField("ba seClass", ba seClassLineEdit); registerField("qobjectMacro", qobjectMacroCheckBox); }
在上面的例子中,ClassInfoPage 這個 QWizardPage 就透過 registerField() 這個函式,將 classNameLineEdit、ba
而在使用 registerField() 時,第一個參數是一個字串,代表 field 的名稱;在指定名稱時,比較需要注意的,就是由於他算是 field 的索引值,而且會是整個精靈共用的,所以就算跨頁面、也不能用同樣的名稱。
再來,就是如果在字串後面加上「*」的話,則代表這個欄位是必要的,使用者如果沒有輸入的話,就會無法按下下一步的按鈕。
之後如果要在別的頁面讀取的時候,就只需要透過 field() 這個函式,就可以取得特定元件的值了。
例如當執行
QString className = field("className").toString();
的時候,他就會去讀取 ClassInfoPage 中的 classNameLineEdit 的值;不過由於 field() 回傳的型別是 QVariant,所以還需要自己透過 toString() 將他轉形成字串。
而如果有必要的話,也可以透過 setField() 來去修改圖形介面元件的值。
另外,在使用 field 的時候,可能還會碰到的問題,那就是某些元件並沒有直接被支援;像是 QDoubleSpinBox 就不能直接透過 registerField() 來登記。(field 有直接支援的原件,也可以參考官方文件(連結))
當遇到這種狀況的時候,就需要在 QWizard 中,先行呼叫 setDefaultProperty() 這個函式,告訴 Qt 針對某個類別的元件、要存取哪個屬性、他的值變化時會觸發哪個 signal。
以 QDoubleSpinBox 來說,其寫法會如下:
setDefaultProperty("QDoubleSpinBox", "value", "valueChanged");
透過先執行這行程式,之後就可以透過 registerField() 來登記 QDoubleSpinBox 的元件了。
針對 QWizard 的紀錄,大概就先寫到這邊了。當然,他還有很多功能可以用,比如說風格的切換、或是自訂按鈕等等,不過那部份就等有真的玩到再說吧。