Qt 的精靈模式:QWizard

| | 0 Comments| 10:44
Categories:

這篇算是簡單紀錄一下,怎麼用 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」(連結)。


流程的控制

其中,透過 QWizardPagenextId() 這個函式來做流程的控制,基本上是在較複雜的精靈中,應該會需要的功能;透過這項功能,可以讓精靈根據使用者的選擇、判斷接下來要跳到哪個頁面,如此一來就可以避掉很多實際上不需要讓使用者看的東西了~

在 Qt 官方文件中,也有一節「Creating Non-Linear Wizards」(連結),在講這件事。

如果是希望透過各個 QWizardPagenextId() 函式,來作流程控管的話,除了可以靠 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);
	...
}

而各個 QWizardPagenextId(),則可以寫成下面的樣子:

int RegisterPage::nextId() const
{
	if (upgradeKeyLineEdit->text().isEmpty()) {
		return LicenseWizard::Page_Details;
	}
	else {
		return LicenseWizard::Page_Conclusion;
	}
}

如果覺得把流程控管拆到一堆類別裡面個別控制不方便,或是基於其他理由不能這樣做的話,Qt 的精靈模式也允許將流程控管的程式,整個寫在 QWizard 中。

如果想要這樣做的話,只要撰寫 QWizardnextId() 就可以了。下面就是官方的範例:

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:"));
	baseClassLineEdit = new QLineEdit;
	baseClassLabel->setBuddy(baseClassLineEdit);
	qobjectMacroCheckBox = new QCheckBox(tr("Generate Q_OBJECT ¯o"));
	registerField("className*", classNameLineEdit);
	registerField("baseClass", baseClassLineEdit);
	registerField("qobjectMacro", qobjectMacroCheckBox);
}

在上面的例子中,ClassInfoPage 這個 QWizardPage 就透過 registerField() 這個函式,將 classNameLineEditbaseClassLineEditqobjectMacroCheckBox 這三個圖形介面、登記到 QWizard 的 field 裡了。

而在使用 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 的紀錄,大概就先寫到這邊了。當然,他還有很多功能可以用,比如說風格的切換、或是自訂按鈕等等,不過那部份就等有真的玩到再說吧。

 

Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *