Qt 相關一覽
介紹
‧What is Qt
‧What is QtCreator - coming soon...
筆記
‧Installing Qt with MinGW
‧Small Test of Qt
‧Creating a Event Label
‧Using Qt Designer
翻譯
‧Qt Tutorial
├ 1. Hello World!
├ 2. Calling it Quits
├ 3. Family Values
├ 4. Let There Be Widgets
├ 5. Building Blocks
├ 6. Building Blocks Galore!
├ 7. One Thing Leads to Another
├ 8. Preparing for Battle
├ 9. With Cannon You Can
├ 10. Smooth as Silk
├ 11. Giving It a Shot
├ 12. Hanging in the Air the Way Bricks Don't
├ 13. Game Over
└ 14. Facing the Wall
‧Widgets Tutorial
‧Address Book Tutorial
├ 1. Designing the User Interface
├ 2. Adding Addresses
├ 3. Navigating between Entries
├ 4. Editing and Removing Addresses
├ 5. Adding a Find Function
├ 6. Loading and Saving
└ 7. Additional Features
NOTICE
2月 25, 2009
【翻譯】Address Book Tutorial
這份指導教學利用 Qt 的跨平台(cross-platform)框架(framework),為 GUI 程式作了一個介紹。

在過程中,我們將要學習一些 Qt 提供的基礎技術,像是:
假如 Qt 對你而言完全是陌生的,且未讀過 How to Learn Qt 的話,請先讀過一次。
這份指導教學的原始碼都放在 Qt 的 examples/tutorials/addressbook 資料夾中。
指導章節:
1. Designing the User Interface
2. Adding Addresses
3. Navigating between Entries
4. Editing and Removing Addresses
5. Adding a Find Function
6. Loading and Saving
7. Additional Features
雖然這支小程式看起來並沒有這麼像一個十分時髦的 GUI 應用程式,但是它使用了相當多使用在複雜應用程式中的基礎技術。在你開始使用它之後,我們建議你試試這個 Application 範例,它提供了一個擁有選單、工具列、一個狀態列等等的小型 GUI 應用程式。

在過程中,我們將要學習一些 Qt 提供的基礎技術,像是:
- 元件(widgets)與佈局管理者(layout manager)
- 容器類別(class)
- signal 與 slot
- 輸出與輸入裝置
假如 Qt 對你而言完全是陌生的,且未讀過 How to Learn Qt 的話,請先讀過一次。
這份指導教學的原始碼都放在 Qt 的 examples/tutorials/addressbook 資料夾中。
指導章節:
1. Designing the User Interface
2. Adding Addresses
3. Navigating between Entries
4. Editing and Removing Addresses
5. Adding a Find Function
6. Loading and Saving
7. Additional Features
雖然這支小程式看起來並沒有這麼像一個十分時髦的 GUI 應用程式,但是它使用了相當多使用在複雜應用程式中的基礎技術。在你開始使用它之後,我們建議你試試這個 Application 範例,它提供了一個擁有選單、工具列、一個狀態列等等的小型 GUI 應用程式。
來源:Address Book Tutorial
版本:4.4.3
【翻譯】Address Book 7 - Additional Features
.tutorials/addressbook/part7/addressbook.cpp
.tutorials/addressbook/part7/addressbook.h
.tutorials/addressbook/part7/finddialog.cpp
.tutorials/addressbook/part7/finddialog.h
.tutorials/addressbook/part7/main.cpp
.tutorials/addressbook/part7/part7.pro
這一章涵蓋了一些使得 Address Book 程式更加方便於每天使用的附加功能。

雖然我們的 Address Book 程式已經相當有用了,但是假如我們可以與其他程式交流聯絡人將會更好。vCard 格式是一個可以被用在這種用途上的普遍檔案格式。在這一章,我們擴展我們的 Address Book 客戶端(client),以允許聯絡人被轉換成 vCard .vcf 檔。
Defining the AddressBook Class
我們在 addressbook.h 檔案中的 AddressBook 類別加入一個 QPushButton 物件:exportButton,以及一個對應的公開 slot:exportAsVCard()。
Implementing the AddressBook Class
在 AddressBook 建構子中,我們將 exportButton 的 clicked() signal 連接到 exportAsVCard()。我們也將這個按鈕加入到我們的 buttonLayout1 中,其負責管理右邊按鈕控制板的佈局。
在我們的 exportAsVCard() 函式,我們藉由取得聯絡人姓名到 name 中開始。我們宣告了 firstName、lastName 與 nameList。接下來,我們尋找姓名第一個為空格的索引值。假如有一個空格,我們將聯絡人的姓名分割成 firstName 與 lastName。然後,我們以一個底線("_")來取代空白。或者,假如沒有空格,我們則假定聯絡人僅有名字(first name)。
就像 saveToFile() 函式,我們開啟一個檔案對話視窗,以讓使用者選擇一個檔案位置。我們使用選擇的檔案名稱,建立一個 QFile 的實體用以寫入。
我們試圖以 WriteOnly 模式去開啟檔案。假如這個程序失敗了,我們顯示一個 QMessageBox 以告知使用者這個問題並返回。否則,我們將檔案作為引數傳入ㄧ個 QTextStream 物件:out。如同 QDataStream,QTextStream 類別提供了讀寫純文字檔案的功能。因此,產生出來的 .vcf 可以在文字編輯器裡開啟編輯。
然後我們將 BEGIN:VCARD 標籤,以及 VERSION:2.1 標簽寫入到 vCard 檔案裡。聯絡人的姓名與 N: 一同寫入。 對於填入 vCard "File as" 屬性的 FN: 標籤,我們必須去確認聯絡人是否有姓氏(last name)。假如有,我們使用 nameList 中的詳細資料填入。否則,我們只寫入 firstName。
我們繼續寫入聯絡人住址。住址中的分號(semicolon)利用 "\" 跳脫(escape)、換行被取代成分號、且逗號(comma)被取代成空格。最後,我們寫入 ADR;HOME:; 標籤以及住址,然後是 END:VCARD 標籤。
結束時,一個 QMessageBox 被顯示以告知使用者:vCard 已經成功被輸出。
vCard 是一個 Internet Mail Consortium 的商標
.tutorials/addressbook/part7/addressbook.h
.tutorials/addressbook/part7/finddialog.cpp
.tutorials/addressbook/part7/finddialog.h
.tutorials/addressbook/part7/main.cpp
.tutorials/addressbook/part7/part7.pro
這一章涵蓋了一些使得 Address Book 程式更加方便於每天使用的附加功能。

雖然我們的 Address Book 程式已經相當有用了,但是假如我們可以與其他程式交流聯絡人將會更好。vCard 格式是一個可以被用在這種用途上的普遍檔案格式。在這一章,我們擴展我們的 Address Book 客戶端(client),以允許聯絡人被轉換成 vCard .vcf 檔。
Defining the AddressBook Class
我們在 addressbook.h 檔案中的 AddressBook 類別加入一個 QPushButton 物件:exportButton,以及一個對應的公開 slot:exportAsVCard()。
void exportAsVCard();
...
QPushButton *exportButton;
Implementing the AddressBook Class
在 AddressBook 建構子中,我們將 exportButton 的 clicked() signal 連接到 exportAsVCard()。我們也將這個按鈕加入到我們的 buttonLayout1 中,其負責管理右邊按鈕控制板的佈局。
在我們的 exportAsVCard() 函式,我們藉由取得聯絡人姓名到 name 中開始。我們宣告了 firstName、lastName 與 nameList。接下來,我們尋找姓名第一個為空格的索引值。假如有一個空格,我們將聯絡人的姓名分割成 firstName 與 lastName。然後,我們以一個底線("_")來取代空白。或者,假如沒有空格,我們則假定聯絡人僅有名字(first name)。
void AddressBook::exportAsVCard()
{
QString name = nameLine->text();
QString address = addressText->toPlainText();
QString firstName;
QString lastName;
QStringList nameList;
int index = name.indexOf(" ");
if (index != -1) {
nameList = name.split(QRegExp("\\s+"), QString::SkipEmptyParts);
firstName = nameList.first();
lastName = nameList.last();
} else {
firstName = name;
lastName = "";
}
QString fileName = QFileDialog::getSaveFileName(this,
tr("Export Contact"), "",
tr("vCard Files (*.vcf);;All Files (*)"));
if (fileName.isEmpty())
return;
QFile file(fileName);
就像 saveToFile() 函式,我們開啟一個檔案對話視窗,以讓使用者選擇一個檔案位置。我們使用選擇的檔案名稱,建立一個 QFile 的實體用以寫入。
我們試圖以 WriteOnly 模式去開啟檔案。假如這個程序失敗了,我們顯示一個 QMessageBox 以告知使用者這個問題並返回。否則,我們將檔案作為引數傳入ㄧ個 QTextStream 物件:out。如同 QDataStream,QTextStream 類別提供了讀寫純文字檔案的功能。因此,產生出來的 .vcf 可以在文字編輯器裡開啟編輯。
if (!file.open(QIODevice::WriteOnly)) {
QMessageBox::information(this, tr("Unable to open file"),
file.errorString());
return;
}
QTextStream out(&file);
然後我們將 BEGIN:VCARD 標籤,以及 VERSION:2.1 標簽寫入到 vCard 檔案裡。聯絡人的姓名與 N: 一同寫入。 對於填入 vCard "File as" 屬性的 FN: 標籤,我們必須去確認聯絡人是否有姓氏(last name)。假如有,我們使用 nameList 中的詳細資料填入。否則,我們只寫入 firstName。
out << "BEGIN:VCARD" << "\n";
out << "VERSION:2.1" << "\n";
out << "N:" << lastName << ";" << firstName << "\n";
if (!nameList.isEmpty())
out << "FN:" << nameList.join(" ") << "\n";
else
out << "FN:" << firstName << "\n";
我們繼續寫入聯絡人住址。住址中的分號(semicolon)利用 "\" 跳脫(escape)、換行被取代成分號、且逗號(comma)被取代成空格。最後,我們寫入 ADR;HOME:; 標籤以及住址,然後是 END:VCARD 標籤。
address.replace(";", "\\;", Qt::CaseInsensitive);
address.replace("\n", ";", Qt::CaseInsensitive);
address.replace(",", " ", Qt::CaseInsensitive);
out << "ADR;HOME:;" << address << "\n";
out << "END:VCARD" << "\n";
QMessageBox::information(this, tr("Export Successful"),
tr("\"%1\" has been exported as a vCard.").arg(name));
}
結束時,一個 QMessageBox 被顯示以告知使用者:vCard 已經成功被輸出。
vCard 是一個 Internet Mail Consortium 的商標
來源:Address Book 7 - Additional Features
版本:4.4.3
2月 24, 2009
【翻譯】Address Book 6 - Loading and Saving
.tutorials/addressbook/part6/addressbook.cpp
.tutorials/addressbook/part6/addressbook.h
.tutorials/addressbook/part6/finddialog.cpp
.tutorials/addressbook/part6/finddialog.h
.tutorials/addressbook/part6/main.cpp
.tutorials/addressbook/part6/part6.pro
這一章涵蓋了 Qt 的檔案處理功能,我們可以使用它來撰寫 Address Book 程式載入與儲存的例行程序。

雖然導覽與搜尋聯絡人都是有用的功能,但是我們的 Address Book 在我們能儲存現存的聯絡人,並能在之後再度載入它們之前,都還不完全算是真的可以使用。Qt 為輸入(input)與輸出(output)提供了一些類別,不過我們已經選擇使用其中兩種簡易使用的組合:QFile 與 QDataStream。
一個 QFile 物件代表一個硬碟上可被讀寫的檔案。QFile 是代表著許多不同種類裝置(device)的 QIODevice 類別的一個子類別。
一個 QDataStream 物件被使用來序列化(serialize)二元(binary)資料,所以它可以被儲存在一個 QIODevice 並在之後再度被讀取。從一個 QIODevice 中進行讀寫,與開啟--將各自的裝置作為參數--與讀取或寫入到串流(stream)是相似的。
Defining the AddressBook Class
我們宣告了兩個公開 slot:saveToFile() 與 loadFromFile(),以及兩個 QPushButton 物件:loadButton 與 saveButton。
Implementing the AddressBook Class
在我們的建構子,我們實體化 loadButton 與 saveButton。理論上,將按鈕的標籤設為「從一個檔案載入聯絡人(Load contacts from a file)」與「儲存聯絡人到一個檔案(Save contacts to a file)」將會更加友善於使用者的。然而,由於我們其他按鈕的大小,我們將標籤設為 Load... 與 Save...。幸運地,Qt 提供一個簡單的方式來利用 setToolTip() 設定工具提示(tooltip),我們為我們的按鈕用接下來的方式來使用它:
雖然並不會顯示在這裡,不過如同其餘我們實現的功能,我們將按鈕加入到右邊的佈局控制板--button1Layout--並連接按鈕的 clicked() signal 到它們各自的 slot。
對於儲存功能,我們首先使用 QFileDialog::getSaveFileName() 取得 fileName。這是一個由 QFileDialog 提供的方便函式。QFileDialog 彈出一個典型的檔案對話視窗,並允許使用者輸入一個檔案名稱或是選擇任何存在的 .abk 檔。.abk 檔是我們的 Address Book 在我們儲存聯絡人時所建立的擴展(extension)。
彈出的檔案對話視窗顯示於下方截圖:

假如 fileName 不為空,我們利用 fileName 建立一個 QFile 物件:file。QFile 與 QDataStream 的運作,如同 QFile 是一個 QIODevice。
接下來,我們試圖在 WriteOnly 模式下開啟檔案。假如開啟不成功,我們顯示一個 QMessageBox 以告知使用者。
否則,我們實體化一個 QDataStream 物件:out。QDataStream 需要與用來讀寫的串流版本相同。在序列化資料到檔案前,我們藉由設定使用的版本為採用的 Qt 4.3 版本,以確保這種情況。
對於讀取功能,我們也使用 QFileDialog::getOpenFileName() 來 取得 fileName。這個與 QFileDialog::getSaveFileName() 極為相像的函式,也彈出一個典型的對話視窗,並允許使用者輸入一個檔案名稱,或是選擇任何存在的 .abk 檔以讀取到 Address Book 中。
舉例來說,在 Windows 上,這個函式彈出一個原生檔案對話視窗,如同接下來顯示的截圖:

再一次,假如 fileName 不為空,我們使用一個 QFile 物件--file--並試圖以。 ReadOnly 模式下開啟檔案。與我們在 saveToFile() 中實現相同的方式,假如開啟不成功,我們顯示一個 QMessageBox 以告知使用者。
否則,我們實體化一個 QDataStream 物件:in,將它的版本如前述設定,並讀取序列化的資料到 contacts 資料結構中。 注意,我們在讀入資料到 contacts 以簡化檔案讀取程序之前,先清空 contacts。一個更進階的方法為讀取聯絡人到暫時的 QMap 物件中,並且只有在聯絡人並非已存在於 contacts 時複製它。
為了顯示已經從檔案讀取到的聯絡人,首先我們必須驗證(validate)獲取的資料,以確保我們讀取的檔案真的包含 Address Book 聯絡人。假如是,我們顯示第一個聯絡人;否則,我們顯示一個 QMessageBox 以告知使用者這個問題。之後,我們會更新介面來以此生效或失效按鈕。
.tutorials/addressbook/part6/addressbook.h
.tutorials/addressbook/part6/finddialog.cpp
.tutorials/addressbook/part6/finddialog.h
.tutorials/addressbook/part6/main.cpp
.tutorials/addressbook/part6/part6.pro
這一章涵蓋了 Qt 的檔案處理功能,我們可以使用它來撰寫 Address Book 程式載入與儲存的例行程序。

雖然導覽與搜尋聯絡人都是有用的功能,但是我們的 Address Book 在我們能儲存現存的聯絡人,並能在之後再度載入它們之前,都還不完全算是真的可以使用。Qt 為輸入(input)與輸出(output)提供了一些類別,不過我們已經選擇使用其中兩種簡易使用的組合:QFile 與 QDataStream。
一個 QFile 物件代表一個硬碟上可被讀寫的檔案。QFile 是代表著許多不同種類裝置(device)的 QIODevice 類別的一個子類別。
一個 QDataStream 物件被使用來序列化(serialize)二元(binary)資料,所以它可以被儲存在一個 QIODevice 並在之後再度被讀取。從一個 QIODevice 中進行讀寫,與開啟--將各自的裝置作為參數--與讀取或寫入到串流(stream)是相似的。
Defining the AddressBook Class
我們宣告了兩個公開 slot:saveToFile() 與 loadFromFile(),以及兩個 QPushButton 物件:loadButton 與 saveButton。
void saveToFile();
void loadFromFile();
...
QPushButton *loadButton;
QPushButton *saveButton;
Implementing the AddressBook Class
在我們的建構子,我們實體化 loadButton 與 saveButton。理論上,將按鈕的標籤設為「從一個檔案載入聯絡人(Load contacts from a file)」與「儲存聯絡人到一個檔案(Save contacts to a file)」將會更加友善於使用者的。然而,由於我們其他按鈕的大小,我們將標籤設為 Load... 與 Save...。幸運地,Qt 提供一個簡單的方式來利用 setToolTip() 設定工具提示(tooltip),我們為我們的按鈕用接下來的方式來使用它:
loadButton->setToolTip(tr("Load contacts from a file"));
...
saveButton->setToolTip(tr("Save contacts to a file"));
雖然並不會顯示在這裡,不過如同其餘我們實現的功能,我們將按鈕加入到右邊的佈局控制板--button1Layout--並連接按鈕的 clicked() signal 到它們各自的 slot。
對於儲存功能,我們首先使用 QFileDialog::getSaveFileName() 取得 fileName。這是一個由 QFileDialog 提供的方便函式。QFileDialog 彈出一個典型的檔案對話視窗,並允許使用者輸入一個檔案名稱或是選擇任何存在的 .abk 檔。.abk 檔是我們的 Address Book 在我們儲存聯絡人時所建立的擴展(extension)。
void AddressBook::saveToFile()
{
QString fileName = QFileDialog::getSaveFileName(this,
tr("Save Address Book"), "",
tr("Address Book (*.abk);;All Files (*)"));
彈出的檔案對話視窗顯示於下方截圖:

假如 fileName 不為空,我們利用 fileName 建立一個 QFile 物件:file。QFile 與 QDataStream 的運作,如同 QFile 是一個 QIODevice。
接下來,我們試圖在 WriteOnly 模式下開啟檔案。假如開啟不成功,我們顯示一個 QMessageBox 以告知使用者。
if (fileName.isEmpty())
return;
else {
QFile file(fileName);
if (!file.open(QIODevice::WriteOnly)) {
QMessageBox::information(this, tr("Unable to open file"),
file.errorString());
return;
}
否則,我們實體化一個 QDataStream 物件:out。QDataStream 需要與用來讀寫的串流版本相同。在序列化資料到檔案前,我們藉由設定使用的版本為採用的 Qt 4.3 版本,以確保這種情況。
QDataStream out(&file);
out.setVersion(QDataStream::Qt_4_3);
out << contacts;
}
}
對於讀取功能,我們也使用 QFileDialog::getOpenFileName() 來 取得 fileName。這個與 QFileDialog::getSaveFileName() 極為相像的函式,也彈出一個典型的對話視窗,並允許使用者輸入一個檔案名稱,或是選擇任何存在的 .abk 檔以讀取到 Address Book 中。
void AddressBook::loadFromFile()
{
QString fileName = QFileDialog::getOpenFileName(this,
tr("Open Address Book"), "",
tr("Address Book (*.abk);;All Files (*)"));
舉例來說,在 Windows 上,這個函式彈出一個原生檔案對話視窗,如同接下來顯示的截圖:

再一次,假如 fileName 不為空,我們使用一個 QFile 物件--file--並試圖以。 ReadOnly 模式下開啟檔案。與我們在 saveToFile() 中實現相同的方式,假如開啟不成功,我們顯示一個 QMessageBox 以告知使用者。
if (fileName.isEmpty())
return;
else {
QFile file(fileName);
if (!file.open(QIODevice::ReadOnly)) {
QMessageBox::information(this, tr("Unable to open file"),
file.errorString());
return;
}
QDataStream in(&file);
in.setVersion(QDataStream::Qt_4_3);
contacts.empty(); // empty existing contacts
in >> contacts;
否則,我們實體化一個 QDataStream 物件:in,將它的版本如前述設定,並讀取序列化的資料到 contacts 資料結構中。 注意,我們在讀入資料到 contacts 以簡化檔案讀取程序之前,先清空 contacts。一個更進階的方法為讀取聯絡人到暫時的 QMap 物件中,並且只有在聯絡人並非已存在於 contacts 時複製它。
if (contacts.isEmpty()) {
QMessageBox::information(this, tr("No contacts in file"),
tr("The file you are attempting to open contains no contacts."));
} else {
QMap<QString, QString>::iterator i = contacts.begin();
nameLine->setText(i.key());
addressText->setText(i.value());
}
}
updateInterface(NavigationMode);
}
為了顯示已經從檔案讀取到的聯絡人,首先我們必須驗證(validate)獲取的資料,以確保我們讀取的檔案真的包含 Address Book 聯絡人。假如是,我們顯示第一個聯絡人;否則,我們顯示一個 QMessageBox 以告知使用者這個問題。之後,我們會更新介面來以此生效或失效按鈕。
來源:Address Book 6 - Loading and Saving
版本:4.4.3
2月 23, 2009
【翻譯】Address Book 5 - Adding a Find Function
.tutorials/addressbook/part5/addressbook.cpp
.tutorials/addressbook/part5/addressbook.h
.tutorials/addressbook/part5/finddialog.cpp
.tutorials/addressbook/part5/finddialog.h
.tutorials/addressbook/part5/main.cpp
.tutorials/addressbook/part5/part5.pro
在這一章,我們著眼於 Address Book 程式中找出聯絡人與住址的方法。

當我們持續加入聯絡人到我們的 Address Book 程式中,利用 Next 與 Previous 按紐來進行導覽將變得令人厭煩。在這個情況下,一個 Find 函式將會更加有效率的找到聯絡人。上方的截圖顯示了 Find 按紐,以及它在按鈕面板(panel)上的位置。
當使用者點擊 Find 按鈕時,顯示一個對話視窗(dialog)是相當有用的,它可以提示使用者輸入聯絡人的姓名。Qt 提供了 QDialog--我們在這個章節子類別化它--來實現一個 FindDialog 類別。
Defining the FindDialog Class

為了要子類別化 QDialog,我們首先在 finddialog.h 檔案中引入 QDialog 的標頭。同樣的,由於我們將要在我們的對話視窗類別中使用 QLineEdit 與 QPushButton,所以我們使用前向宣告來宣告它們。
正如我們的 AddressBook 類別,FindDialog 類別包含了 Q_OBJECT 巨集,且它的建構子被宣告成允許一個 QWidget 父元件,即使對話視窗將會做為一個單獨的視窗開啟。
我們定義了一個公開函式--getFindText()--供 FindDialog 實體化的類別使用,這允許它們獲得由使用者輸入的文字。一個公開的 slot--findClicked()--被定義成在使用者點擊 Find 按鈕時處理搜尋字串。
最後,我們定義私有變數:findButton、lineEdit 與 findText,分別對應 Find 按鈕、使用者輸入搜尋字串的單行文字框(line edit)、與一個用以儲存稍後使用的搜尋字串的內部(internal)字串。
Implementing the FindDialog Class
在 FindDialog 的建構子中,我們設置了私有變數:findButton、lineEdit 與 findText。我們使用一個 QHBoxLayout 以將元件放在適當位置。
我們設定了佈局與視窗標題,也將 signal 連接到它們各自的 slot。注意,findButton 的 clicked() signal 被連接到了 findClicked() 與 accept()。QDialog 提供的 accept() slot 隱藏對話視窗,並將結果代碼(result code)設為 Accepted。我們使用這個函式,來幫助 AddressBook 的 findContact() 得知:FindDialog 物件被關閉了。這將會在討論 findContact() 函式時進一步的解釋。

在 findClicked() 中,我們確保使用者不會在沒有輸入一個聯絡人姓名時點擊 Find 按鈕。然後,我們將 findText 設為從 lineEdit 中取得的搜尋字串。之後,我們清除 lineEdit 的聯絡人,並隱藏對話視窗。
findText 變數有一個與之相關的公開獲取函數(getter function):getFindText()。之前我們僅在建構子與 findClicked() 函式中直接設置 findText,我們並沒有隨著 getFindText() 建立一個設置函式(setter function)。因為 getFindText() 是公開的,類別實體化與使用 FindDialog 總是能存取到使用者輸入並接受(accept)的搜尋字串。
Defining the AddressBook Class
為了確保我們能夠在我們的 AddressBook 類別中使用 FindDialog,我們在 addressbook.h 檔案中引入 finddialog.h。
到目前為止,所有我們的 Address Book 功能都有一個 QPushButton 和一個對應的 slot。同樣的,對於 Find 功能,我們有 findButton 與 findContact()。
findButton 被宣告成一個私有變數,且 findContact() 函式被宣告成一個公開的 slot。
最後,我們宣告我們將會用以參照一個 FindDialog 實體的私有變數:dialog。
一旦我們實體化一個對話視窗,我們將會不只一次地使用它;使用一個私有變數允許我們在類別中不只一個地方參照它。
Implementing the AddressBook Class
在 AddressBook 類別的建構子中,我們實體化我們的私有物件,findButton 與 findDialog:
接下來,我們將 findButton 的 clicked() signal 連接到 findContact()。
現在所有剩下的就是我們 findContact() 函式的程式碼:
我們藉由顯示 FindDialog 實體 dialog 開始。這是當使用者輸入一個聯絡人姓名供查閱的時候。一旦使用者點擊對話視窗的 findButton,對話視窗會被隱藏,且將結果代碼設為 QDialog::Accepted。這使得我們的 if 敘訴為真(true)。
然後我們開始使用 FindDialog 的 getFindText() 函式取得搜尋字串--這個例子中的 contactName。假如聯絡人存在於我們的 Address Book,我們立刻顯示它。否則,我們顯示如下顯示的 QMessageBox 以告知它們的搜尋失敗。

.tutorials/addressbook/part5/addressbook.h
.tutorials/addressbook/part5/finddialog.cpp
.tutorials/addressbook/part5/finddialog.h
.tutorials/addressbook/part5/main.cpp
.tutorials/addressbook/part5/part5.pro
在這一章,我們著眼於 Address Book 程式中找出聯絡人與住址的方法。

當我們持續加入聯絡人到我們的 Address Book 程式中,利用 Next 與 Previous 按紐來進行導覽將變得令人厭煩。在這個情況下,一個 Find 函式將會更加有效率的找到聯絡人。上方的截圖顯示了 Find 按紐,以及它在按鈕面板(panel)上的位置。
當使用者點擊 Find 按鈕時,顯示一個對話視窗(dialog)是相當有用的,它可以提示使用者輸入聯絡人的姓名。Qt 提供了 QDialog--我們在這個章節子類別化它--來實現一個 FindDialog 類別。
Defining the FindDialog Class

為了要子類別化 QDialog,我們首先在 finddialog.h 檔案中引入 QDialog 的標頭。同樣的,由於我們將要在我們的對話視窗類別中使用 QLineEdit 與 QPushButton,所以我們使用前向宣告來宣告它們。
正如我們的 AddressBook 類別,FindDialog 類別包含了 Q_OBJECT 巨集,且它的建構子被宣告成允許一個 QWidget 父元件,即使對話視窗將會做為一個單獨的視窗開啟。
#include <QDialog>
class QLineEdit;
class QPushButton;
class FindDialog : public QDialog
{
Q_OBJECT
public:
FindDialog(QWidget *parent = 0);
QString getFindText();
public slots:
void findClicked();
private:
QPushButton *findButton;
QLineEdit *lineEdit;
QString findText;
};
我們定義了一個公開函式--getFindText()--供 FindDialog 實體化的類別使用,這允許它們獲得由使用者輸入的文字。一個公開的 slot--findClicked()--被定義成在使用者點擊 Find 按鈕時處理搜尋字串。
最後,我們定義私有變數:findButton、lineEdit 與 findText,分別對應 Find 按鈕、使用者輸入搜尋字串的單行文字框(line edit)、與一個用以儲存稍後使用的搜尋字串的內部(internal)字串。
Implementing the FindDialog Class
在 FindDialog 的建構子中,我們設置了私有變數:findButton、lineEdit 與 findText。我們使用一個 QHBoxLayout 以將元件放在適當位置。
FindDialog::FindDialog(QWidget *parent)
: QDialog(parent)
{
QLabel *findLabel = new QLabel(tr("Enter the name of a contact:"));
lineEdit = new QLineEdit;
findButton = new QPushButton(tr("&Find"));
findText = "";
QHBoxLayout *layout = new QHBoxLayout;
layout->addWidget(findLabel);
layout->addWidget(lineEdit);
layout->addWidget(findButton);
setLayout(layout);
setWindowTitle(tr("Find a Contact"));
connect(findButton, SIGNAL(clicked()), this, SLOT(findClicked()));
connect(findButton, SIGNAL(clicked()), this, SLOT(accept()));
}
我們設定了佈局與視窗標題,也將 signal 連接到它們各自的 slot。注意,findButton 的 clicked() signal 被連接到了 findClicked() 與 accept()。QDialog 提供的 accept() slot 隱藏對話視窗,並將結果代碼(result code)設為 Accepted。我們使用這個函式,來幫助 AddressBook 的 findContact() 得知:FindDialog 物件被關閉了。這將會在討論 findContact() 函式時進一步的解釋。

在 findClicked() 中,我們確保使用者不會在沒有輸入一個聯絡人姓名時點擊 Find 按鈕。然後,我們將 findText 設為從 lineEdit 中取得的搜尋字串。之後,我們清除 lineEdit 的聯絡人,並隱藏對話視窗。
void FindDialog::findClicked()
{
QString text = lineEdit->text();
if (text.isEmpty()) {
QMessageBox::information(this, tr("Empty Field"),
tr("Please enter a name."));
return;
} else {
findText = text;
lineEdit->clear();
hide();
}
}
findText 變數有一個與之相關的公開獲取函數(getter function):getFindText()。之前我們僅在建構子與 findClicked() 函式中直接設置 findText,我們並沒有隨著 getFindText() 建立一個設置函式(setter function)。因為 getFindText() 是公開的,類別實體化與使用 FindDialog 總是能存取到使用者輸入並接受(accept)的搜尋字串。
QString FindDialog::getFindText()
{
return findText;
}
Defining the AddressBook Class
為了確保我們能夠在我們的 AddressBook 類別中使用 FindDialog,我們在 addressbook.h 檔案中引入 finddialog.h。
#include "finddialog.h"
到目前為止,所有我們的 Address Book 功能都有一個 QPushButton 和一個對應的 slot。同樣的,對於 Find 功能,我們有 findButton 與 findContact()。
findButton 被宣告成一個私有變數,且 findContact() 函式被宣告成一個公開的 slot。
void findContact();
...
QPushButton *findButton;
最後,我們宣告我們將會用以參照一個 FindDialog 實體的私有變數:dialog。
FindDialog *dialog;
一旦我們實體化一個對話視窗,我們將會不只一次地使用它;使用一個私有變數允許我們在類別中不只一個地方參照它。
Implementing the AddressBook Class
在 AddressBook 類別的建構子中,我們實體化我們的私有物件,findButton 與 findDialog:
findButton = new QPushButton(tr("&Find"));
findButton->setEnabled(false);
...
dialog = new FindDialog;
接下來,我們將 findButton 的 clicked() signal 連接到 findContact()。
connect(findButton, SIGNAL(clicked()), this, SLOT(findContact()));
現在所有剩下的就是我們 findContact() 函式的程式碼:
void AddressBook::findContact()
{
dialog->show();
if (dialog->exec() == QDialog::Accepted) {
QString contactName = dialog->getFindText();
if (contacts.contains(contactName)) {
nameLine->setText(contactName);
addressText->setText(contacts.value(contactName));
} else {
QMessageBox::information(this, tr("Contact Not Found"),
tr("Sorry, \"%1\" is not in your address book.").arg(contactName));
return;
}
}
updateInterface(NavigationMode);
}
我們藉由顯示 FindDialog 實體 dialog 開始。這是當使用者輸入一個聯絡人姓名供查閱的時候。一旦使用者點擊對話視窗的 findButton,對話視窗會被隱藏,且將結果代碼設為 QDialog::Accepted。這使得我們的 if 敘訴為真(true)。
然後我們開始使用 FindDialog 的 getFindText() 函式取得搜尋字串--這個例子中的 contactName。假如聯絡人存在於我們的 Address Book,我們立刻顯示它。否則,我們顯示如下顯示的 QMessageBox 以告知它們的搜尋失敗。

來源:Address Book 5 - Adding a Find Function
版本:4.4.3
2月 18, 2009
【翻譯】Address Book 4 - Editing and Removing Addresses
.tutorials/addressbook/part4/addressbook.cpp
.tutorials/addressbook/part4/addressbook.h
.tutorials/addressbook/part4/main.cpp
.tutorials/addressbook/part4/part4.pro
在這一章,我們著眼於修改儲存在 Address Book 程式中聯絡人內容的方法。

我們現在擁有一個不只能以有系統的方式存取資料,還能夠允許導覽的 Address Book。而包含編輯和移除函式--使得聯絡人的詳細資料可以在需要時被改變--將會是方便的。然而,這需要在列舉(enum)的形式下做一點改良。在先前的章節中,我們有兩種模式(mode):AddingMode 與 NavigationMode--但是它們並不是作為列舉而被定義的。相反的,我們手動讓對應的按鈕生效或失效,結果就是多行重複的程式碼。
在這一章,我們定義了包含三種值的 Mode 列舉:
Defining the AddressBook Class
addressbook.h 被更新成包含了 Mode 列舉:
我們也加入了兩個新的 slot--editContact() 與 removeContact()--到我們當前的公開 slot 列表。
為了在模式間做轉換,我們引入了 updateInterface() 函式來管理所有 QPushButton 物件的生效與失效。為了先前提到的編輯和刪除函式,我們也加入了兩個新按鈕:editButton 與 removeButton。
最後,我們宣告了 currentMode 以追蹤當前的列舉模式。
Implementing the AddressBook Class
我們現在必須要實現 Address Book 程式的模式改變功能。editButton 與 removeButton 被實體化,並在預設情況下使其失效,因為 Address Book 啟動時並無聯絡人在記憶體中。
然後,這些按鈕被連接到它們各自的 slot:editContact() 與 removeContact()。且我們將這些按鈕加入到 buttonLayout1e 中。
在模式轉換成 EditingMode 之前,editContact() 函式將聯絡人的舊資料儲存在 oldName 與 oldAddress 中。在這個模式,submitButton 與 cancelButton 都是有效的。因此,使用者可以改變聯絡人的詳細資料並點擊任一按鈕。
submitContact() 函式被一個 if-else 敘訴(statement)分割成兩個部分。我們檢查 currentMode 以確認它是否在 AddingMode 下。假如是,我們開始我們的加入動作。
否則,我們檢查 currentMode 以確認它是否在 EditingMode 下。假如是,我們比較 oldName 與 name。假如名字被改變了,我們從 contacts 移除舊的聯絡人,並插入更新過的新聯絡人。
只有在住址被改變(換言之,oldAddress 與 address 不相同)時,我們才更新聯絡人的住址。最後,我們將 currentMode 設為 NavigationMode。這是作為重新啟動所有失效按鈕的一個重要步驟。
為了從 Address Book 中移除一個聯絡人,我們實現了 removeContact() 函式。這個函式確認聯絡人是否在 contacts 中。
假如存在,我們顯示一個 QMessageBox,以讓使用者確認(confirm)刪除動作。一旦使用者確認了,我們呼叫 previous() 以確保使用者介面顯示了另一個聯絡人,且利用 QMap 的 remove() 函式來移除聯絡人。基於禮貌,我們顯示了一個 QMessageBox 以告知使用者。這個函式中所使用的訊息視窗顯示如下:

Updating the User Interface
我們先前提到了 updateInterface() 函式作為使按鈕根據當前模式生效與失效的工具。函式根據傳入的 mode 引數更新當前模式,在確認它的值之前賦值(assign)給 currentMode。
這時,每個按鈕都根據當前模式生效或失效。其於 AddingMode 與 EditingMode 的程式碼顯示如下:
然而,對於 NavigationMode,我們在 QPushButton::setEnabled() 的參數中包含條件。這是為了確保在 Address Book 中至少有一個聯絡人時,能使 editButton 與 removeButton 按紐生效;只有在 Address Book 中有多於一個聯絡人時,能使 nextButton 與 previousButton 按紐生效。
藉由執行設置模式的任務以及在同一函式中更新使用者介面,我們避免了使用者介面的程式內部狀態發生「不同步(out of sync)」的可能性。
.tutorials/addressbook/part4/addressbook.h
.tutorials/addressbook/part4/main.cpp
.tutorials/addressbook/part4/part4.pro
在這一章,我們著眼於修改儲存在 Address Book 程式中聯絡人內容的方法。

我們現在擁有一個不只能以有系統的方式存取資料,還能夠允許導覽的 Address Book。而包含編輯和移除函式--使得聯絡人的詳細資料可以在需要時被改變--將會是方便的。然而,這需要在列舉(enum)的形式下做一點改良。在先前的章節中,我們有兩種模式(mode):AddingMode 與 NavigationMode--但是它們並不是作為列舉而被定義的。相反的,我們手動讓對應的按鈕生效或失效,結果就是多行重複的程式碼。
在這一章,我們定義了包含三種值的 Mode 列舉:
- NavigationMode,
- AddingMode, 與
- EditingMode.
Defining the AddressBook Class
addressbook.h 被更新成包含了 Mode 列舉:
enum Mode { NavigationMode, AddingMode, EditingMode };
我們也加入了兩個新的 slot--editContact() 與 removeContact()--到我們當前的公開 slot 列表。
void editContact();
void removeContact();
為了在模式間做轉換,我們引入了 updateInterface() 函式來管理所有 QPushButton 物件的生效與失效。為了先前提到的編輯和刪除函式,我們也加入了兩個新按鈕:editButton 與 removeButton。
void updateInterface(Mode mode);
...
QPushButton *editButton;
QPushButton *removeButton;
...
Mode currentMode;
最後,我們宣告了 currentMode 以追蹤當前的列舉模式。
Implementing the AddressBook Class
我們現在必須要實現 Address Book 程式的模式改變功能。editButton 與 removeButton 被實體化,並在預設情況下使其失效,因為 Address Book 啟動時並無聯絡人在記憶體中。
editButton = new QPushButton(tr("&Edit"));
editButton->setEnabled(false);
removeButton = new QPushButton(tr("&Remove"));
removeButton->setEnabled(false);
然後,這些按鈕被連接到它們各自的 slot:editContact() 與 removeContact()。且我們將這些按鈕加入到 buttonLayout1e 中。
connect(editButton, SIGNAL(clicked()), this, SLOT(editContact()));
connect(removeButton, SIGNAL(clicked()), this, SLOT(removeContact()));
...
buttonLayout1->addWidget(editButton);
buttonLayout1->addWidget(removeButton);
在模式轉換成 EditingMode 之前,editContact() 函式將聯絡人的舊資料儲存在 oldName 與 oldAddress 中。在這個模式,submitButton 與 cancelButton 都是有效的。因此,使用者可以改變聯絡人的詳細資料並點擊任一按鈕。
void AddressBook::editContact()
{
oldName = nameLine->text();
oldAddress = addressText->toPlainText();
updateInterface(EditingMode);
}
submitContact() 函式被一個 if-else 敘訴(statement)分割成兩個部分。我們檢查 currentMode 以確認它是否在 AddingMode 下。假如是,我們開始我們的加入動作。
void AddressBook::submitContact()
{
...
if (currentMode == AddingMode) {
if (!contacts.contains(name)) {
contacts.insert(name, address);
QMessageBox::information(this, tr("Add Successful"),
tr("\"%1\" has been added to your address book.").arg(name));
} else {
QMessageBox::information(this, tr("Add Unsuccessful"),
tr("Sorry, \"%1\" is already in your address book.").arg(name));
return;
}
否則,我們檢查 currentMode 以確認它是否在 EditingMode 下。假如是,我們比較 oldName 與 name。假如名字被改變了,我們從 contacts 移除舊的聯絡人,並插入更新過的新聯絡人。
} else if (currentMode == EditingMode) {
if (oldName != name) {
if (!contacts.contains(name)) {
QMessageBox::information(this, tr("Edit Successful"),
tr("\"%1\" has been edited in your address book.").arg(oldName));
contacts.remove(oldName);
contacts.insert(name, address);
} else {
QMessageBox::information(this, tr("Edit Unsuccessful"),
tr("Sorry, \"%1\" is already in your address book.").arg(name));
return;
}
} else if (oldAddress != address) {
QMessageBox::information(this, tr("Edit Successful"),
tr("\"%1\" has been edited in your address book.").arg(name));
contacts[name] = address;
}
}
updateInterface(NavigationMode);
}
只有在住址被改變(換言之,oldAddress 與 address 不相同)時,我們才更新聯絡人的住址。最後,我們將 currentMode 設為 NavigationMode。這是作為重新啟動所有失效按鈕的一個重要步驟。
為了從 Address Book 中移除一個聯絡人,我們實現了 removeContact() 函式。這個函式確認聯絡人是否在 contacts 中。
void AddressBook::removeContact()
{
QString name = nameLine->text();
QString address = addressText->toPlainText();
if (contacts.contains(name)) {
int button = QMessageBox::question(this,
tr("Confirm Remove"),
tr("Are you sure you want to remove \"%1\"?").arg(name),
QMessageBox::Yes | QMessageBox::No);
if (button == QMessageBox::Yes) {
previous();
contacts.remove(name);
QMessageBox::information(this, tr("Remove Successful"),
tr("\"%1\" has been removed from your address book.").arg(name));
}
}
updateInterface(NavigationMode);
}
假如存在,我們顯示一個 QMessageBox,以讓使用者確認(confirm)刪除動作。一旦使用者確認了,我們呼叫 previous() 以確保使用者介面顯示了另一個聯絡人,且利用 QMap 的 remove() 函式來移除聯絡人。基於禮貌,我們顯示了一個 QMessageBox 以告知使用者。這個函式中所使用的訊息視窗顯示如下:

Updating the User Interface
我們先前提到了 updateInterface() 函式作為使按鈕根據當前模式生效與失效的工具。函式根據傳入的 mode 引數更新當前模式,在確認它的值之前賦值(assign)給 currentMode。
這時,每個按鈕都根據當前模式生效或失效。其於 AddingMode 與 EditingMode 的程式碼顯示如下:
void AddressBook::updateInterface(Mode mode)
{
currentMode = mode;
switch (currentMode) {
case AddingMode:
case EditingMode:
nameLine->setReadOnly(false);
nameLine->setFocus(Qt::OtherFocusReason);
addressText->setReadOnly(false);
addButton->setEnabled(false);
editButton->setEnabled(false);
removeButton->setEnabled(false);
nextButton->setEnabled(false);
previousButton->setEnabled(false);
submitButton->show();
cancelButton->show();
break;
然而,對於 NavigationMode,我們在 QPushButton::setEnabled() 的參數中包含條件。這是為了確保在 Address Book 中至少有一個聯絡人時,能使 editButton 與 removeButton 按紐生效;只有在 Address Book 中有多於一個聯絡人時,能使 nextButton 與 previousButton 按紐生效。
case NavigationMode:
if (contacts.isEmpty()) {
nameLine->clear();
addressText->clear();
}
nameLine->setReadOnly(true);
addressText->setReadOnly(true);
addButton->setEnabled(true);
int number = contacts.size();
editButton->setEnabled(number >= 1);
removeButton->setEnabled(number >= 1);
nextButton->setEnabled(number > 1);
previousButton->setEnabled(number >1 );
submitButton->hide();
cancelButton->hide();
break;
}
}
藉由執行設置模式的任務以及在同一函式中更新使用者介面,我們避免了使用者介面的程式內部狀態發生「不同步(out of sync)」的可能性。
來源:Address Book 4 - Editing and Removing Addresses
版本:4.4.3
2月 17, 2009
【翻譯】Address Book 3 - Navigating between Entries
.tutorials/addressbook/part3/addressbook.cpp
.tutorials/addressbook/part3/addressbook.h
.tutorials/addressbook/part3/main.cpp
.tutorials/addressbook/part3/part3.pro
Address Book 程式現在完成一半了。我們需要加入一些函式以在聯絡人之間做導覽(navigation)。不過首先,我們必須要決定:我們要使用哪種資料結構來存取這些聯絡人。
在章節 2,我們使用一個以聯絡人的姓名為鍵、以聯絡人住址為值的一對 key-value QMap。對於我們的情況,這是有效的。然而,為了要導覽並顯示每一個項目,是需要做點改良的。
我們藉由將 QMap 複製成一個類似於循環連結串列(circularly-linked list)的資料結構,其中所有的元素(element)都是被連接著的,包含了第一個元素與最後一個元素。下圖說明了這個資料結構。

Defining the AddressBook Class
為了要加入導覽函式到 Address Book 程式中,我們還需要加入兩個 slot 到我們的 AddressBook 類別中:next() 與 previous()。它們被加入到我們的 addressbook.h 檔案:
我們也需要另外兩個 QPushButton 物件,所以我們在私有變數中宣告了 nextButton 與 previousButton:
Implementing the AddressBook Class
在 addressbook.cpp 中的 AddressBook 建構子,我們實體化 nextButton 與 previousButton 並在預設情況下使他們失效。這是因為只有在在 Address Book 中的聯絡人多於一個時才能夠進行導覽。
然後,我們將這些按鈕連接到它們各自的 slot。
下圖是我們預期的圖形使用者介面。注意,它正在接近我們預期的最終成果。

對於 next() 與 previous() 函式,我們遵循基本的慣例,將 nextButton 放在右邊、將 previousButton 放在左邊。為了達到這個直觀的佈局,我們使用了 QHBoxLayout 來一步一步擺放元件:
接著,QHBoxLayout 物件:buttonLayout2 被加入到 mainLayout 中。
下圖顯示了元件在 mainLayout 中的座標。

在我們的 addContact() 函式中,我們必須讓這些按鈕失效,所以使用者不會在加入一個聯絡人時試圖進行導覽。
在我們的 submitContact() 函式,我們再讓導覽按鈕--nextButton 與 previousButton--根據聯絡人的數量生效。如同先前提到的,導覽只有在 Address Book 中的聯絡人多於一個時才是有效的。接下來的幾行程式展示了該如何做到這件事:
我們也在 cancel() 函式中包含了這幾行程式。
回想到我們打算利用我們的 QMap 物件來模擬一個循環連結串列:contacts。所以,在 next() 函式中,我們為 contacts 取得一個迭代器(iterator),然後:
一旦我們已經迭代到了 contacts 中正確的物件,我們在 nameLine 與 addressText 顯示它的內容。
同樣的,對於 previous() 函式,我們為 contacts 取得一個迭代器(iterator),然後:
再一次,我們顯示了當前 contacts 中的物件內容。
.tutorials/addressbook/part3/addressbook.h
.tutorials/addressbook/part3/main.cpp
.tutorials/addressbook/part3/part3.pro
Address Book 程式現在完成一半了。我們需要加入一些函式以在聯絡人之間做導覽(navigation)。不過首先,我們必須要決定:我們要使用哪種資料結構來存取這些聯絡人。
在章節 2,我們使用一個以聯絡人的姓名為鍵、以聯絡人住址為值的一對 key-value QMap。對於我們的情況,這是有效的。然而,為了要導覽並顯示每一個項目,是需要做點改良的。
我們藉由將 QMap 複製成一個類似於循環連結串列(circularly-linked list)的資料結構,其中所有的元素(element)都是被連接著的,包含了第一個元素與最後一個元素。下圖說明了這個資料結構。

Defining the AddressBook Class
為了要加入導覽函式到 Address Book 程式中,我們還需要加入兩個 slot 到我們的 AddressBook 類別中:next() 與 previous()。它們被加入到我們的 addressbook.h 檔案:
void next();
void previous();
我們也需要另外兩個 QPushButton 物件,所以我們在私有變數中宣告了 nextButton 與 previousButton:
QPushButton *nextButton;
QPushButton *previousButton;
Implementing the AddressBook Class
在 addressbook.cpp 中的 AddressBook 建構子,我們實體化 nextButton 與 previousButton 並在預設情況下使他們失效。這是因為只有在在 Address Book 中的聯絡人多於一個時才能夠進行導覽。
nextButton = new QPushButton(tr("&Next"));
nextButton->setEnabled(false);
previousButton = new QPushButton(tr("&Previous"));
previousButton->setEnabled(false);
然後,我們將這些按鈕連接到它們各自的 slot。
connect(nextButton, SIGNAL(clicked()), this, SLOT(next()));
connect(previousButton, SIGNAL(clicked()), this, SLOT(previous()));
下圖是我們預期的圖形使用者介面。注意,它正在接近我們預期的最終成果。

對於 next() 與 previous() 函式,我們遵循基本的慣例,將 nextButton 放在右邊、將 previousButton 放在左邊。為了達到這個直觀的佈局,我們使用了 QHBoxLayout 來一步一步擺放元件:
QHBoxLayout *buttonLayout2 = new QHBoxLayout;
buttonLayout2->addWidget(previousButton);
buttonLayout2->addWidget(nextButton);
接著,QHBoxLayout 物件:buttonLayout2 被加入到 mainLayout 中。
mainLayout->addLayout(buttonLayout2, 3, 1);
下圖顯示了元件在 mainLayout 中的座標。

在我們的 addContact() 函式中,我們必須讓這些按鈕失效,所以使用者不會在加入一個聯絡人時試圖進行導覽。
nextButton->setEnabled(false);
previousButton->setEnabled(false);
在我們的 submitContact() 函式,我們再讓導覽按鈕--nextButton 與 previousButton--根據聯絡人的數量生效。如同先前提到的,導覽只有在 Address Book 中的聯絡人多於一個時才是有效的。接下來的幾行程式展示了該如何做到這件事:
int number = contacts.size();
nextButton->setEnabled(number > 1);
previousButton->setEnabled(number > 1);
我們也在 cancel() 函式中包含了這幾行程式。
回想到我們打算利用我們的 QMap 物件來模擬一個循環連結串列:contacts。所以,在 next() 函式中,我們為 contacts 取得一個迭代器(iterator),然後:
- 若是迭代器不在 contacts 的末端,我們將它加一
- 若是迭代器在 contacts 的末端,我們移動它到 contacts 的開頭。這給了我們 QMap 像是一個循環連結串列在運作的錯覺。
void AddressBook::next()
{
QString name = nameLine->text();
QMap<QString, QString>::iterator i = contacts.find(name);
if (i != contacts.end())
i++;
if (i == contacts.end())
i = contacts.begin();
nameLine->setText(i.key());
addressText->setText(i.value());
}
一旦我們已經迭代到了 contacts 中正確的物件,我們在 nameLine 與 addressText 顯示它的內容。
同樣的,對於 previous() 函式,我們為 contacts 取得一個迭代器(iterator),然後:
- 若是迭代器在 contacts 的末端,我們清除顯示並返回。
- 若是迭代器在 contacts 的開頭,我們將它移動到末端。
- 然後,我們將迭代器減一。
void AddressBook::previous()
{
QString name = nameLine->text();
QMap<QString, QString>::iterator i = contacts.find(name);
if (i == contacts.end()){
nameLine->clear();
addressText->clear();
return;
}
if (i == contacts.begin())
i = contacts.end();
i--;
nameLine->setText(i.key());
addressText->setText(i.value());
}
再一次,我們顯示了當前 contacts 中的物件內容。
來源:Address Book 3 - Navigating between Entries
版本:4.4.3
2月 15, 2009
【翻譯】Address Book 2 - Adding Addresses
.tutorials/addressbook/part2/addressbook.cpp
.tutorials/addressbook/part2/addressbook.h
.tutorials/addressbook/part2/main.cpp
.tutorials/addressbook/part2/part2.pro
建立我們 Address Book 的下一步是,允許一點點使用者的互動。

我們將要提供一個可以讓使用者點擊以新增一個聯絡人的按鈕(push button),以及允許加入新聯絡人的按鈕。另外,還需要某種資料結構(data structure)以有組織的方式儲存這些聯絡人。
Defining the AddressBook Class
現在,我們已經設置好了標籤和輸入區塊,我們加入按鈕以完成新增一個聯絡人的處理。這代表我們的 addressbook.h 檔案現在擁有三個 QPushButton 物件宣告,與三個對應的公開(public) slot。
一個 slot 是一個回應一個特定 signal 的函式。我們將會在實現 AddressBook 類別時更詳細的討論這個概念。然而,你可以參照 Signals and Slots 文件,以獲得一份 Qt signal 與 slot 概念的簡介。
三個 QPushButton 物件:addButton、submitButton 與 cancelButton,現在與前一章的 nameLine 與 addressText 一同被包含在我們的私有變數宣告中。
我們需要一個容器以儲存我們的 Address Book 聯絡人,讓我們能夠遍歷(traverse)並顯示它們。一個 QMap 物件:contacts 用以掌握一對 key-value 來達成這個目的:聯絡人的姓名為鍵(key),而聯絡人住址為值(value)。
我們也宣告了兩個私有的 QString 物件:oldName 與 oldAddress。在使用者點擊 "Add" 之前,需要這些物件保留最後顯示的聯絡人的姓名與地址。所以,當使用者點擊 "Cancel",我們可以回復顯示最後的聯絡人詳細資料。
Implementing the AddressBook Class
在 AddressBook 的建構子中,我們將 nameLine 與 addressText 設為唯讀(read-only),所以我們只能顯示它,而不能編輯已存在的聯絡人詳細資料。
現在,我們實體化我們的按鈕:addButton、submitButton、與 cancelButton。
當 submitButton 與 cancelButton 藉由呼叫 hide() 被隱藏時,addButton 藉由呼叫 show() 函式而被顯示。這兩個按鈕只有在使用者按下 "Add" 時才會被顯示,且這是由下方敘述的 addContact() 函式處理的。
我們連結(connect)按紐的 clicked() signal 到它們各自的 slot。下圖說明了這件事。

接下來,我們使用 QVBoxLayout 垂直對齊我們的按鈕,恰好排列到我們 Address Book 元件的右邊。
addStretch() 函式用以確保按鈕不會均分空間,而是緊密的排列在元件上方。下圖顯示了使用了 addStretch() 與不使用它的不同。

我們現在使用 addLayout() 將 buttonLayout1 加入到 mainLayout 中。這使得我們嵌套(nested)佈局,buttonLayout1 現在就像是 mainLayout 的一個子佈局。
我們的佈局座標現在看起來像這樣:

在 addContact() 函式中,我們儲存最後的聯絡人詳細資料在 oldName 與 oldAddress 中。現在我們清除這些輸入區塊,並關閉唯讀模式。我們將焦點(focus)設在 nameLine 上,並顯示了 submitButton 與 cancelButton。
submitContact() 函式可以被分成三個部份:
下方的螢幕截圖顯示了我們用以顯示給使用者的訊息的 QMessageBox 物件。

cancel() 函式恢復了最後顯示的聯絡人詳細資料、啟用 addButton,並隱藏了 submitButton 與 cancelButton。
加入一個聯絡人的普遍構想,給了使用者在任何時間點擊 "Submit" 或是 "Cancel" 的彈性。下方的流程圖進一步的說明了這個概念:

.tutorials/addressbook/part2/addressbook.h
.tutorials/addressbook/part2/main.cpp
.tutorials/addressbook/part2/part2.pro
建立我們 Address Book 的下一步是,允許一點點使用者的互動。

我們將要提供一個可以讓使用者點擊以新增一個聯絡人的按鈕(push button),以及允許加入新聯絡人的按鈕。另外,還需要某種資料結構(data structure)以有組織的方式儲存這些聯絡人。
Defining the AddressBook Class
現在,我們已經設置好了標籤和輸入區塊,我們加入按鈕以完成新增一個聯絡人的處理。這代表我們的 addressbook.h 檔案現在擁有三個 QPushButton 物件宣告,與三個對應的公開(public) slot。
public slots:
void addContact();
void submitContact();
void cancel();
一個 slot 是一個回應一個特定 signal 的函式。我們將會在實現 AddressBook 類別時更詳細的討論這個概念。然而,你可以參照 Signals and Slots 文件,以獲得一份 Qt signal 與 slot 概念的簡介。
三個 QPushButton 物件:addButton、submitButton 與 cancelButton,現在與前一章的 nameLine 與 addressText 一同被包含在我們的私有變數宣告中。
private:
QPushButton *addButton;
QPushButton *submitButton;
QPushButton *cancelButton;
QLineEdit *nameLine;
QTextEdit *addressText;
我們需要一個容器以儲存我們的 Address Book 聯絡人,讓我們能夠遍歷(traverse)並顯示它們。一個 QMap 物件:contacts 用以掌握一對 key-value 來達成這個目的:聯絡人的姓名為鍵(key),而聯絡人住址為值(value)。
QMap<QString, QString> contacts;
QString oldName;
QString oldAddress;
};
我們也宣告了兩個私有的 QString 物件:oldName 與 oldAddress。在使用者點擊 "Add" 之前,需要這些物件保留最後顯示的聯絡人的姓名與地址。所以,當使用者點擊 "Cancel",我們可以回復顯示最後的聯絡人詳細資料。
Implementing the AddressBook Class
在 AddressBook 的建構子中,我們將 nameLine 與 addressText 設為唯讀(read-only),所以我們只能顯示它,而不能編輯已存在的聯絡人詳細資料。
...
nameLine->setReadOnly(true);
...
addressText->setReadOnly(true);
現在,我們實體化我們的按鈕:addButton、submitButton、與 cancelButton。
addButton = new QPushButton(tr("&Add"));
addButton->show();
submitButton = new QPushButton(tr("&Submit"));
submitButton->hide();
cancelButton = new QPushButton(tr("&Cancel"));
cancelButton->hide();
當 submitButton 與 cancelButton 藉由呼叫 hide() 被隱藏時,addButton 藉由呼叫 show() 函式而被顯示。這兩個按鈕只有在使用者按下 "Add" 時才會被顯示,且這是由下方敘述的 addContact() 函式處理的。
connect(addButton, SIGNAL(clicked()), this, SLOT(addContact()));
connect(submitButton, SIGNAL(clicked()), this, SLOT(submitContact()));
connect(cancelButton, SIGNAL(clicked()), this, SLOT(cancel()));
我們連結(connect)按紐的 clicked() signal 到它們各自的 slot。下圖說明了這件事。

接下來,我們使用 QVBoxLayout 垂直對齊我們的按鈕,恰好排列到我們 Address Book 元件的右邊。
QVBoxLayout *buttonLayout1 = new QVBoxLayout;
buttonLayout1->addWidget(addButton, Qt::AlignTop);
buttonLayout1->addWidget(submitButton);
buttonLayout1->addWidget(cancelButton);
buttonLayout1->addStretch();
addStretch() 函式用以確保按鈕不會均分空間,而是緊密的排列在元件上方。下圖顯示了使用了 addStretch() 與不使用它的不同。

我們現在使用 addLayout() 將 buttonLayout1 加入到 mainLayout 中。這使得我們嵌套(nested)佈局,buttonLayout1 現在就像是 mainLayout 的一個子佈局。
QGridLayout *mainLayout = new QGridLayout;
mainLayout->addWidget(nameLabel, 0, 0);
mainLayout->addWidget(nameLine, 0, 1);
mainLayout->addWidget(addressLabel, 1, 0, Qt::AlignTop);
mainLayout->addWidget(addressText, 1, 1);
mainLayout->addLayout(buttonLayout1, 1, 2);
我們的佈局座標現在看起來像這樣:

在 addContact() 函式中,我們儲存最後的聯絡人詳細資料在 oldName 與 oldAddress 中。現在我們清除這些輸入區塊,並關閉唯讀模式。我們將焦點(focus)設在 nameLine 上,並顯示了 submitButton 與 cancelButton。
void AddressBook::addContact()
{
oldName = nameLine->text();
oldAddress = addressText->toPlainText();
nameLine->clear();
addressText->clear();
nameLine->setReadOnly(false);
nameLine->setFocus(Qt::OtherFocusReason);
addressText->setReadOnly(false);
addButton->setEnabled(false);
submitButton->show();
cancelButton->show();
}
submitContact() 函式可以被分成三個部份:
- 我們設法從 nameLine 與 addressText 得到聯絡人的詳細資料,並儲存他們在 QString 物件中。我們也要確保使用者在輸入區塊空白時無法點擊 "Submit";此外,一個 QMessageBox 會被顯示,以提醒使用者輸入姓名與住址。
void AddressBook::submitContact() { QString name = nameLine->text(); QString address = addressText->toPlainText(); if (name == "" || address == "") { QMessageBox::information(this, tr("Empty Field"), tr("Please enter a name and address.")); return; } - 然後,我們開始確認聯絡人是否已經存在。假如不存在,我們加入聯絡人到 contacts 中,並顯示一個 QMessageBox 告知使用者:聯絡人已被加入。
if (!contacts.contains(name)) { contacts.insert(name, address); QMessageBox::information(this, tr("Add Successful"), tr("\"%1\" has been added to your address book.").arg(name)); } else { QMessageBox::information(this, tr("Add Unsuccessful"), tr("Sorry, \"%1\" is already in your address book.").arg(name)); return; }
假如聯絡人已存在,我們同樣顯示一個 QMessageBox 以告知使用者,預防使用者加入了重複的聯絡人。我們的 contacts 物件以一對姓名與住址的 key-value 為基礎。因此,我們要確保鍵是唯一的。
- 一旦我們處理完上述兩種情況,我們用接下來的程式碼來將按鈕恢復成它們的標準狀態:
if (contacts.isEmpty()) { nameLine->clear(); addressText->clear(); } nameLine->setReadOnly(true); addressText->setReadOnly(true); addButton->setEnabled(true); submitButton->hide(); cancelButton->hide(); }
下方的螢幕截圖顯示了我們用以顯示給使用者的訊息的 QMessageBox 物件。

cancel() 函式恢復了最後顯示的聯絡人詳細資料、啟用 addButton,並隱藏了 submitButton 與 cancelButton。
void AddressBook::cancel()
{
nameLine->setText(oldName);
nameLine->setReadOnly(true);
addressText->setText(oldAddress);
addressText->setReadOnly(true);
addButton->setEnabled(true);
submitButton->hide();
cancelButton->hide();
}
加入一個聯絡人的普遍構想,給了使用者在任何時間點擊 "Submit" 或是 "Cancel" 的彈性。下方的流程圖進一步的說明了這個概念:

來源:Address Book 2 - Adding Addresses
版本:4.4.3
2月 14, 2009
【翻譯】Address Book 1 - Designing the User Interface
.tutorials/addressbook/part1/addressbook.cpp
.tutorials/addressbook/part1/addressbook.h
.tutorials/addressbook/part1/main.cpp
.tutorials/addressbook/part1/part1.pro
這份指導的第一部分,涵蓋了我們使用在 Address Book 程式上的基礎圖形使用者介面(GUI,graphical user interface)設計。
建立一個 GUI 程式的第一步,是設計使用者介面。在這一章,我們的目標是建立實現一個基礎的 Address Book 所需的標籤(label)與輸入區塊(input field)。下圖是我們期望的成果截圖。

我們需要兩個 QLabel 物件(object):nameLabel 與 addressLabel,還有兩個輸入區塊:一個 QLineEdit - nameLine 和一個 QTextEdit - addressText,讓使用者能夠輸入連絡姓名以及住址。使用到的元件與它們的位置顯示在下圖。

這裡使用了三個檔案來實現這個 Address Book:
Qt Programming - Subclassing
在寫 Qt 程式時,我們經常子類別化(subclass) Qt 物件以增加功能。這是建立自訂元件或是標準元件集合背後的其中一個基本要素。子類別化以擴展或改變一個元件的行為,有以下幾點好處:
由於 Qt 並沒有提供一個特有的 Address Book 元件,我們子類別化並加入功能到一個標準的 Qt 元件中。我們在這份指導教學中建立的 AddressBook 類別,在需要一個基礎 Address Book 元件的情況下可以被重複使用。
Defining the AddressBook Class
addressbook.h 檔案被用來定義 AddressBook 類別。
我們從將 AddressBook 定義為一個 QWidget 子類別與宣告(declare)建構子(constructor)開始。我們也使用了 Q_OBJECT 巨集(macro)以表示類別使用了國際化(internationalization)與 Qt 的 signal 與 slot 功能,即使我們在這一階段還不會使用到所有的功能。
類別包含了 nameLine 與 addressText 的定義,也就是我們先前提到的 QLineEdit 與 QTextEdit 的私有(private)實體。在接下來的章節,我們將會發現將資料儲存在 nameLine 與 addressText 需要許多 Address Book 的函式。
我們不需要包含我們將會用到的 QLabel 的定義,因為一旦它們被建立,我們就不需要再參照(reference)它們了。Qt 追蹤物件所有權(ownership)的方式會在下一節解釋。
Q_OBJECT 巨集自身實現了一些 Qt 中較為進階的功能。目前,將 Q_OBJECT 巨集想成一個允許我們使用 tr() 與 connect() 函式的捷徑(shortcut)是有用的。
我們現在已經完成了 addressbook.h 檔案,讓我們繼續實現對應的 addressbook.cpp 檔案。
Defining the AddressBook Class
AddressBook 的建構子接受一個 QWidget 參數(parameter)--父元件(parent)。照慣例,我們傳遞這個參數到基礎類別的建構子中。一個父元件可以擁有一個或多個子元件--這個所有權的概念,對於在 Qt 中組合元件是有用的。舉例來說,假如你刪除一個父元件,所有它的子元件也將會被刪除。
在這個建構子中,我們宣告並實體化兩個區域(local) QLabel 物件:nameLabel 與 addressLabel,並實體化了 nameLine 與 addressText。tr() 函式返回一個字串的翻譯版本,假如它是有效的;否則,它會返回字串本身。將這個函式想成一個 <在這裡插入翻譯> 記號以標記用以翻譯的 QString 物件。你將會注意到,在接下來的章節以及 Qt Examples 中,無論我們在何時使用一個可翻譯的字串,我們都會包含它。
寫 Qt 程式時,瞭解佈局(layout)的運作是有用的。Qt 提供了三種主要的佈局類別:QHBoxLayout、QVBoxLayout 與 QGridLayout 來管理元件的位置。

我們使用一個 QGridLayout 以結構化的方式擺放我們的標籤與輸入區塊到適當的位置。QGridLayout 將可用空間分割成一個網格(grid)並放置元件在我們指定行與列數的格子(cell)中。上方的示意圖顯示了佈局網格與我們的元件位置,我們使用接下來的程式碼來指定這個排列:
注意到 addressLabel 是利用 Qt::AlignTop 作為一個附加引數(argument)來做定位的。這用以確定它不會在格子 (1,0) 中垂直置中。參考 Layout Classes 文件,以獲得 Qt 佈局的基礎簡介。
為了要將佈局物件設置到元件上,我們必須呼叫元件的 setLayout() 函式:
最後,我們將元件的標題設為 "Simple Address Book"。
Running the Application
一個獨立的檔案 main.cpp 用於 main() 函式。在這個函式中,我們實體化一個 QApplication 物件:app。QApplication 負責像是預設字體(font)和游標(cursor)等多種應用廣泛的資源,與執行事件迴圈(event loop)。因此,總是有一個 QApplication 物件在每個使用 Qt 的 GUI 應用程式中。
我們使用 new 關鍵字,在堆積(heap)中建構了一個新的 AddressBook 元件,並呼叫它的 show() 函式來顯示它。然而,直到應用程式的事件迴圈開始前,元件還不會被顯示。我們藉由呼叫應用程式的 exec() 函式來開始事件迴圈;這個函式返回的執行結果同樣也作為 main() 函式的傳回值。
.tutorials/addressbook/part1/addressbook.h
.tutorials/addressbook/part1/main.cpp
.tutorials/addressbook/part1/part1.pro
這份指導的第一部分,涵蓋了我們使用在 Address Book 程式上的基礎圖形使用者介面(GUI,graphical user interface)設計。
建立一個 GUI 程式的第一步,是設計使用者介面。在這一章,我們的目標是建立實現一個基礎的 Address Book 所需的標籤(label)與輸入區塊(input field)。下圖是我們期望的成果截圖。

我們需要兩個 QLabel 物件(object):nameLabel 與 addressLabel,還有兩個輸入區塊:一個 QLineEdit - nameLine 和一個 QTextEdit - addressText,讓使用者能夠輸入連絡姓名以及住址。使用到的元件與它們的位置顯示在下圖。

這裡使用了三個檔案來實現這個 Address Book:
- addressbook.h - AddressBook 類別的定義檔,
- addressbook.cpp - AddressBook 類別的實現檔,以及
- main.cpp - 包含了一個 main() 函式,加上 AddressBook 實體(instance)的檔案
Qt Programming - Subclassing
在寫 Qt 程式時,我們經常子類別化(subclass) Qt 物件以增加功能。這是建立自訂元件或是標準元件集合背後的其中一個基本要素。子類別化以擴展或改變一個元件的行為,有以下幾點好處:
- 我們可以寫出虛擬(virtual)或是純虛擬(pure virtual)函式的實現以完全獲得我們的需求,在必要時則回頭採用基礎類別(base class)的實現。
- 這允許我們封裝(encapsulate)部分的使用者介面在類別中,所以應用程式的其餘部分並不需要知道使用者介面中的獨立元件。
- 子類別(subclass)可以在相同的應用程式或函式庫(library)中被用來建立多樣的自訂元件,且子類別的程式碼能夠在其他專案中被重複使用(reused)。
由於 Qt 並沒有提供一個特有的 Address Book 元件,我們子類別化並加入功能到一個標準的 Qt 元件中。我們在這份指導教學中建立的 AddressBook 類別,在需要一個基礎 Address Book 元件的情況下可以被重複使用。
Defining the AddressBook Class
addressbook.h 檔案被用來定義 AddressBook 類別。
我們從將 AddressBook 定義為一個 QWidget 子類別與宣告(declare)建構子(constructor)開始。我們也使用了 Q_OBJECT 巨集(macro)以表示類別使用了國際化(internationalization)與 Qt 的 signal 與 slot 功能,即使我們在這一階段還不會使用到所有的功能。
class AddressBook : public QWidget
{
Q_OBJECT
public:
AddressBook(QWidget *parent = 0);
private:
QLineEdit *nameLine;
QTextEdit *addressText;
};
類別包含了 nameLine 與 addressText 的定義,也就是我們先前提到的 QLineEdit 與 QTextEdit 的私有(private)實體。在接下來的章節,我們將會發現將資料儲存在 nameLine 與 addressText 需要許多 Address Book 的函式。
我們不需要包含我們將會用到的 QLabel 的定義,因為一旦它們被建立,我們就不需要再參照(reference)它們了。Qt 追蹤物件所有權(ownership)的方式會在下一節解釋。
Q_OBJECT 巨集自身實現了一些 Qt 中較為進階的功能。目前,將 Q_OBJECT 巨集想成一個允許我們使用 tr() 與 connect() 函式的捷徑(shortcut)是有用的。
我們現在已經完成了 addressbook.h 檔案,讓我們繼續實現對應的 addressbook.cpp 檔案。
Defining the AddressBook Class
AddressBook 的建構子接受一個 QWidget 參數(parameter)--父元件(parent)。照慣例,我們傳遞這個參數到基礎類別的建構子中。一個父元件可以擁有一個或多個子元件--這個所有權的概念,對於在 Qt 中組合元件是有用的。舉例來說,假如你刪除一個父元件,所有它的子元件也將會被刪除。
AddressBook::AddressBook(QWidget *parent)
: QWidget(parent)
{
QLabel *nameLabel = new QLabel(tr("Name:"));
nameLine = new QLineEdit;
QLabel *addressLabel = new QLabel(tr("Address:"));
addressText = new QTextEdit;
在這個建構子中,我們宣告並實體化兩個區域(local) QLabel 物件:nameLabel 與 addressLabel,並實體化了 nameLine 與 addressText。tr() 函式返回一個字串的翻譯版本,假如它是有效的;否則,它會返回字串本身。將這個函式想成一個 <在這裡插入翻譯> 記號以標記用以翻譯的 QString 物件。你將會注意到,在接下來的章節以及 Qt Examples 中,無論我們在何時使用一個可翻譯的字串,我們都會包含它。
寫 Qt 程式時,瞭解佈局(layout)的運作是有用的。Qt 提供了三種主要的佈局類別:QHBoxLayout、QVBoxLayout 與 QGridLayout 來管理元件的位置。

我們使用一個 QGridLayout 以結構化的方式擺放我們的標籤與輸入區塊到適當的位置。QGridLayout 將可用空間分割成一個網格(grid)並放置元件在我們指定行與列數的格子(cell)中。上方的示意圖顯示了佈局網格與我們的元件位置,我們使用接下來的程式碼來指定這個排列:
QGridLayout *mainLayout = new QGridLayout;
mainLayout->addWidget(nameLabel, 0, 0);
mainLayout->addWidget(nameLine, 0, 1);
mainLayout->addWidget(addressLabel, 1, 0, Qt::AlignTop);
mainLayout->addWidget(addressText, 1, 1);
注意到 addressLabel 是利用 Qt::AlignTop 作為一個附加引數(argument)來做定位的。這用以確定它不會在格子 (1,0) 中垂直置中。參考 Layout Classes 文件,以獲得 Qt 佈局的基礎簡介。
為了要將佈局物件設置到元件上,我們必須呼叫元件的 setLayout() 函式:
setLayout(mainLayout);
setWindowTitle(tr("Simple Address Book"));
}
最後,我們將元件的標題設為 "Simple Address Book"。
Running the Application
一個獨立的檔案 main.cpp 用於 main() 函式。在這個函式中,我們實體化一個 QApplication 物件:app。QApplication 負責像是預設字體(font)和游標(cursor)等多種應用廣泛的資源,與執行事件迴圈(event loop)。因此,總是有一個 QApplication 物件在每個使用 Qt 的 GUI 應用程式中。
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
AddressBook *addressBook = new AddressBook;
addressBook->show();
return app.exec();
}
我們使用 new 關鍵字,在堆積(heap)中建構了一個新的 AddressBook 元件,並呼叫它的 show() 函式來顯示它。然而,直到應用程式的事件迴圈開始前,元件還不會被顯示。我們藉由呼叫應用程式的 exec() 函式來開始事件迴圈;這個函式返回的執行結果同樣也作為 main() 函式的傳回值。
來源:Address Book 1 - Designing the User Interface
版本:4.4.3
2月 07, 2009
【筆記】Installing Qt with MinGW
由於安裝 Qt 感覺起來還滿麻煩的,所以在這裡,我使用 Windows XP + MinGW 的環境,紀錄一下安裝 Qt 的步驟與過程。

首先,請先到 Qt for Open Source C++ development on Windows 的下載頁中,選擇一個載點下載當前最新版的 "qt-win-opensource-4.4.3-mingw.exe"(請自己選擇當前最新版)。

下載完成,開始安裝之後,安裝程式會詢問你要不要將 .ui 副檔名跟 Qt 的 GUI 編輯器(Qt Designer)作檔案關聯。
基本上不用更改直接繼續就可以了。

接著選擇安裝 Qt 的目錄。

在這裡,假如你已經安裝過 MinGW,請直接選擇 MinGW 所在的目錄。
(要瞭解如何安裝 MinGW,請見這篇文章。)

此時,若你的 MinGW 是新版(我的是 5.1.4 版),Qt 的安裝程式會對 w32api.h 版本不合提出警告。
但是,因為此時我的 w32api.h 的版本為 3.12,照理說是比 Qt 需求的 3.2 版還要新的。既然我的版本比 Qt 需求的版本還新,安裝程式還對此提出警告,實在是有些怪異。
無論如何,在此我們先選擇「是」,忽略這個警告繼續進行安裝。

而若是電腦裡並沒有預先安裝 MinGW,則可以勾選上面的 "Download ans install minimal MinGW installition",再選擇欲安裝 MinGW 的目錄。

接著它會要你選擇下載 MinGW 的鏡像點(mirror)。
其實也沒什麼好選的,因為站點只有一個 Norway。
至於下方的 "Download MinGW source code" 則是可以順便將 MinGW 的原始碼下載下來。沒必要的話就別勾選吧。

之後就會開始下載 MinGW 的動作(由於鏡像點相當遠,下載可能會等很久)。


等一段時間,就能安裝完成了。
不過,這樣還不能夠藉由 C++ 來使用 Qt。我們還需要將 Qt 函式庫進行編譯才能夠使用。
首先,請到系統內容中,將 MinGW 目錄下的 bin 資料夾與 Qt 目錄下的 bin 資料夾加入到 PATH 環境變數中。例如 C:\MinGW\bin 跟 C:\Qt\4.4.3\bin。(不知道如何設定環境變數,可以參考這篇文章。)
然後,執行開始功能表中 Qt 資料夾(預設名稱為 "Qt by Nokia v4.4.3 (OpenSource)")裡的 "Qt 4.4.3 (Build Debug Libraries)",以建立 Debug Library。
剛開始,應該會看到以下這段文字:
這裡是在詢問你同不同意 GNU GPL 的條款。若是要詳細瞭解條款內容,就輸入 2 或 3 (GPL的版本),再按下 Enter。
若是同意接受 GNU GPL 條款,就輸入 y 並按下 Enter 繼續安裝的程序吧。
但是,假如你在之前選擇 MinGW 的地方,也碰上了 w32api 的警告,在這個過程中將會兩項錯誤(若是選擇由鏡像點下載並安裝 MinGW 則不會出現此問題)。
所幸的是,這兩個問題都已經在網路上找到解決的方法。你可以到這裡閱讀相關的討論。
第一個錯誤是 MinGW 目錄下 "\include\sspi.h" 的 "error: `UNICODE_STRING' does not name a type"。根據查到的結果,"UNICODE_STRING" 是被定義在 <subauth.h> 標頭檔中的。所以要修正這個問題,請開啟 sspi.h 找到以下程式碼:
並在這段程式碼下面加上這一行:
然後存檔關閉,這樣就可以了。
而碰到的第二個錯誤,則是 Qt 目錄下的 "\src\corelib\arch\qatomic_windows.h" 與 MinGW 目錄下的 "\include\winbase.h" 中有部分函式發生了原型衝突(prototype conflict),也就是相同的函式被重複定義了兩次。
解決的方法是,開啟 qatomic_windows.h 後,找到以下程式碼:
並將它修改成:
以巨集的方式避開重複定義,這樣就沒有問題了。
話說回來,這個建立 Debug Library 的過程其實相當久(約兩個小時)。
確認沒有問題之後,建議就別盯著發呆,開個即時通聊天也好。
完成建置工作之後,請點選「開始」中的「執行」,並輸入 "cmd" 以開啟命令列提示工具。
接著,我們要將所在目錄移到 Qt 的根目錄中。請輸入 "cd" 加上 Qt 的所在目錄,例如:
然後,再輸入以下命令:
這一個步驟也相當耗時。想睡的,甚至可以回去補個眠了(無誤)。
等到睡醒了,發現 make 的步驟也順利完成了。
最後,我們需要將一些不必要的檔案清除,請輸入以下命令:
安裝到這裡,就大功告成囉!
首先,請先到 Qt for Open Source C++ development on Windows 的下載頁中,選擇一個載點下載當前最新版的 "qt-win-opensource-4.4.3-mingw.exe"(請自己選擇當前最新版)。
下載完成,開始安裝之後,安裝程式會詢問你要不要將 .ui 副檔名跟 Qt 的 GUI 編輯器(Qt Designer)作檔案關聯。
基本上不用更改直接繼續就可以了。
接著選擇安裝 Qt 的目錄。
在這裡,假如你已經安裝過 MinGW,請直接選擇 MinGW 所在的目錄。
(要瞭解如何安裝 MinGW,請見這篇文章。)
此時,若你的 MinGW 是新版(我的是 5.1.4 版),Qt 的安裝程式會對 w32api.h 版本不合提出警告。
但是,因為此時我的 w32api.h 的版本為 3.12,照理說是比 Qt 需求的 3.2 版還要新的。既然我的版本比 Qt 需求的版本還新,安裝程式還對此提出警告,實在是有些怪異。
無論如何,在此我們先選擇「是」,忽略這個警告繼續進行安裝。
而若是電腦裡並沒有預先安裝 MinGW,則可以勾選上面的 "Download ans install minimal MinGW installition",再選擇欲安裝 MinGW 的目錄。
接著它會要你選擇下載 MinGW 的鏡像點(mirror)。
其實也沒什麼好選的,因為站點只有一個 Norway。
至於下方的 "Download MinGW source code" 則是可以順便將 MinGW 的原始碼下載下來。沒必要的話就別勾選吧。
之後就會開始下載 MinGW 的動作(由於鏡像點相當遠,下載可能會等很久)。
等一段時間,就能安裝完成了。
不過,這樣還不能夠藉由 C++ 來使用 Qt。我們還需要將 Qt 函式庫進行編譯才能夠使用。
首先,請到系統內容中,將 MinGW 目錄下的 bin 資料夾與 Qt 目錄下的 bin 資料夾加入到 PATH 環境變數中。例如 C:\MinGW\bin 跟 C:\Qt\4.4.3\bin。(不知道如何設定環境變數,可以參考這篇文章。)
然後,執行開始功能表中 Qt 資料夾(預設名稱為 "Qt by Nokia v4.4.3 (OpenSource)")裡的 "Qt 4.4.3 (Build Debug Libraries)",以建立 Debug Library。
剛開始,應該會看到以下這段文字:
This is the Qt for Windows Open Source Edition. You are licensed to use this software under the terms of the GNU General Public License (GPL) version 2 or 3. Type '2' to view the GNU General Public License version 2 (GPLv2). Type '3' to view the GNU General Public License version 3 (GPLv3). Type 'y' to accept this license offer. Type 'n' to decline this license offer. Do you accept the terms of the license?
這裡是在詢問你同不同意 GNU GPL 的條款。若是要詳細瞭解條款內容,就輸入 2 或 3 (GPL的版本),再按下 Enter。
若是同意接受 GNU GPL 條款,就輸入 y 並按下 Enter 繼續安裝的程序吧。
但是,假如你在之前選擇 MinGW 的地方,也碰上了 w32api 的警告,在這個過程中將會兩項錯誤(若是選擇由鏡像點下載並安裝 MinGW 則不會出現此問題)。
所幸的是,這兩個問題都已經在網路上找到解決的方法。你可以到這裡閱讀相關的討論。
第一個錯誤是 MinGW 目錄下 "\include\sspi.h" 的 "error: `UNICODE_STRING' does not name a type"。根據查到的結果,"UNICODE_STRING" 是被定義在 <subauth.h> 標頭檔中的。所以要修正這個問題,請開啟 sspi.h 找到以下程式碼:
#ifndef _SSPI_H
#define _SSPI_H
#if __GNUC__ >=3
#pragma GCC system_header
#endif
#ifdef __cplusplus
extern "C" {
#endif
並在這段程式碼下面加上這一行:
#include <subauth.h>
然後存檔關閉,這樣就可以了。
而碰到的第二個錯誤,則是 Qt 目錄下的 "\src\corelib\arch\qatomic_windows.h" 與 MinGW 目錄下的 "\include\winbase.h" 中有部分函式發生了原型衝突(prototype conflict),也就是相同的函式被重複定義了兩次。
解決的方法是,開啟 qatomic_windows.h 後,找到以下程式碼:
extern "C" {
__declspec(dllimport) long __stdcall InterlockedCompareExchange(long *, long, long);
__declspec(dllimport) long __stdcall InterlockedIncrement(long *);
__declspec(dllimport) long __stdcall InterlockedDecrement(long *);
__declspec(dllimport) long __stdcall InterlockedExchange(long *, long);
__declspec(dllimport) long __stdcall InterlockedExchangeAdd(long *, long);
}
並將它修改成:
#ifndef __INTERLOCKED_DECLARED
#define __INTERLOCKED_DECLARED
extern "C" {
__declspec(dllimport) long __stdcall InterlockedCompareExchange(long *, long, long);
__declspec(dllimport) long __stdcall InterlockedIncrement(long *);
__declspec(dllimport) long __stdcall InterlockedDecrement(long *);
__declspec(dllimport) long __stdcall InterlockedExchange(long *, long);
__declspec(dllimport) long __stdcall InterlockedExchangeAdd(long *, long);
}
#endif
以巨集的方式避開重複定義,這樣就沒有問題了。
話說回來,這個建立 Debug Library 的過程其實相當久(約兩個小時)。
確認沒有問題之後,建議就別盯著發呆,
完成建置工作之後,請點選「開始」中的「執行」,並輸入 "cmd" 以開啟命令列提示工具。
接著,我們要將所在目錄移到 Qt 的根目錄中。請輸入 "cd" 加上 Qt 的所在目錄,例如:
cd C:\Qt\4.4.3
然後,再輸入以下命令:
make
這一個步驟也相當耗時。想睡的,甚至可以回去補個眠了(無誤)。
等到睡醒了,發現 make 的步驟也順利完成了。
最後,我們需要將一些不必要的檔案清除,請輸入以下命令:
make clean
安裝到這裡,就大功告成囉!
2月 05, 2009
【翻譯】Widgets Tutorial
Introduction
元件(widget)是使用 Qt 製作圖形使用者介面(GUI,Graphical User Interface)應用程式的基礎。每一個 GUI 組件(component)--像是一個按鈕(button)、標籤(label)或是文字編輯器(text editor)--都是一個元件,並且可以被放置在一個已存在的使用者介面中,或是作為一個獨立的視窗(window)被顯示。每一類型的組件都由 QWidget 的特定子類別(subclass)所提供,而 QWidget 本身則為 QObject 的子類別。
QWidget 不是一個抽象類別(abstract class);它可以被用來當作其他元件的容器(container),而且可以用最少的努力子類別化(subclassed)以建立自訂的元件。建立其他元件所放置的視窗是相當常見的。
與 QObject 相同,元件可以於建立時指定父物件(parent object)以表明所有權(ownership),確保物件在不再使用時都會被刪除。對元件來說,這些父子關係還具有額外的意義:每個子元件都是被顯示在其父元件的螢幕區域中。這表示,當一個視窗被刪除,所有它所包含的元件都會自動被刪除。
Creating a Window
若是一個元件被建立時沒有父元件,當它顯示時就可以把它看做是一個視窗(window)、或是一個頂層元件(top-level widget)。由於它並沒有父物件以確保它將會在不再使用時被刪除,所以是否保留頂層元件就取決於應用程式的開發者了。
在接下來的範例,我們使用 QWidget 來建立、顯示一個採用預設大小的視窗:

我們可以藉由將 window 作為父元件傳遞到元件建構子的方式來加入一個子元件。在這個例子裡,我們將一個按鈕加入到 window 中,並將之放置到指定位置:

這個按鈕現在是 window 的子元件了,而且它將會在視窗毀滅的時候被刪除。注意,隱藏或是關閉視窗並不會自動毀滅它。
Using Layouts
通常,子元件並不是以明確指定位置和大小的方式來排列在視窗中的,而是使用版面配置(layout)。這裡我們建立了一個標籤(label)與一個單行文字框元件,我們想要一步一步地排列它們。

我們建立出來的 layout 物件管理了由 addWidget() 函式提供給它的元件的位置以及大小。而版面配置自身則是在 setLayout()的呼叫中被提供給視窗。版面配置只有經由它們所負責管理的元件(與其它版面配置)影響才是可見的。
在上面的範例中,每個元件的所有權都不是很清楚。由於我們建立了沒有父物件的元件與版面配置,我們預期會看到一個空視窗、與兩個分別包含一個標籤與一個單行文字框的視窗。然而,當我們告知版面配置以管理標籤與單行文字框,並將版面配置設置在視窗中,這些元件與版面配置本身都會「重新定義父元件(reparented)」成 window 的子元件。
就像元件可以包含其他元件,版面配置可以被用來提供元件不同層級(level)的群組(grouping)。在這裡,我們想要讓一個並排在單行文字框的標籤顯示在一個視窗的頂端、以及一個顯示了查詢結果的表格上面。

與 QHBoxLayout 和 QVBoxLayout 相同,Qt 也提供了 QGridLayout 和 QFormLayout 類別來輔助更複雜的使用者介面。
元件(widget)是使用 Qt 製作圖形使用者介面(GUI,Graphical User Interface)應用程式的基礎。每一個 GUI 組件(component)--像是一個按鈕(button)、標籤(label)或是文字編輯器(text editor)--都是一個元件,並且可以被放置在一個已存在的使用者介面中,或是作為一個獨立的視窗(window)被顯示。每一類型的組件都由 QWidget 的特定子類別(subclass)所提供,而 QWidget 本身則為 QObject 的子類別。
QWidget 不是一個抽象類別(abstract class);它可以被用來當作其他元件的容器(container),而且可以用最少的努力子類別化(subclassed)以建立自訂的元件。建立其他元件所放置的視窗是相當常見的。
與 QObject 相同,元件可以於建立時指定父物件(parent object)以表明所有權(ownership),確保物件在不再使用時都會被刪除。對元件來說,這些父子關係還具有額外的意義:每個子元件都是被顯示在其父元件的螢幕區域中。這表示,當一個視窗被刪除,所有它所包含的元件都會自動被刪除。
Creating a Window
若是一個元件被建立時沒有父元件,當它顯示時就可以把它看做是一個視窗(window)、或是一個頂層元件(top-level widget)。由於它並沒有父物件以確保它將會在不再使用時被刪除,所以是否保留頂層元件就取決於應用程式的開發者了。
在接下來的範例,我們使用 QWidget 來建立、顯示一個採用預設大小的視窗:
QWidget *window = new QWidget();
window->resize(320, 240);
window->show();

我們可以藉由將 window 作為父元件傳遞到元件建構子的方式來加入一個子元件。在這個例子裡,我們將一個按鈕加入到 window 中,並將之放置到指定位置:
QPushButton *button = new QPushButton(tr("Press me"), window);
button->move(100, 100);
button->show();

這個按鈕現在是 window 的子元件了,而且它將會在視窗毀滅的時候被刪除。注意,隱藏或是關閉視窗並不會自動毀滅它。
Using Layouts
通常,子元件並不是以明確指定位置和大小的方式來排列在視窗中的,而是使用版面配置(layout)。這裡我們建立了一個標籤(label)與一個單行文字框元件,我們想要一步一步地排列它們。
QLabel *label = new QLabel(tr("Name:"));
QLineEdit *lineEdit = new QLineEdit();
QHBoxLayout *layout = new QHBoxLayout();
layout->addWidget(label);
layout->addWidget(lineEdit);
window->setLayout(layout);

我們建立出來的 layout 物件管理了由 addWidget() 函式提供給它的元件的位置以及大小。而版面配置自身則是在 setLayout()的呼叫中被提供給視窗。版面配置只有經由它們所負責管理的元件(與其它版面配置)影響才是可見的。
在上面的範例中,每個元件的所有權都不是很清楚。由於我們建立了沒有父物件的元件與版面配置,我們預期會看到一個空視窗、與兩個分別包含一個標籤與一個單行文字框的視窗。然而,當我們告知版面配置以管理標籤與單行文字框,並將版面配置設置在視窗中,這些元件與版面配置本身都會「重新定義父元件(reparented)」成 window 的子元件。
就像元件可以包含其他元件,版面配置可以被用來提供元件不同層級(level)的群組(grouping)。在這裡,我們想要讓一個並排在單行文字框的標籤顯示在一個視窗的頂端、以及一個顯示了查詢結果的表格上面。
QLabel *queryLabel = new QLabel(tr("Query:"));
QLineEdit *queryEdit = new QLineEdit();
QTableView *resultView = new QTableView();
QHBoxLayout *queryLayout = new QHBoxLayout();
queryLayout->addWidget(queryLabel);
queryLayout->addWidget(queryEdit);
QVBoxLayout *mainLayout = new QVBoxLayout();
mainLayout->addLayout(queryLayout);
mainLayout->addWidget(resultView);
window->setLayout(mainLayout);

與 QHBoxLayout 和 QVBoxLayout 相同,Qt 也提供了 QGridLayout 和 QFormLayout 類別來輔助更複雜的使用者介面。
來源:Widgets Tutorial
版本:4.4.3
2月 04, 2009
【翻譯】Qt Tutorial 14 - Facing the Wall
.tutorials/tutorial/t14/cannonfield.cpp
.tutorials/tutorial/t14/cannonfield.h
.tutorials/tutorial/t14/gameboard.cpp
.tutorials/tutorial/t14/gameboard.h
.tutorials/tutorial/t14/lcdrange.cpp
.tutorials/tutorial/t14/lcdrange.h
.tutorials/tutorial/t14/main.cpp
.tutorials/tutorial/t14/t14.pro
這是最後一個範例:一個完整的遊戲。

我們加入鍵盤快捷鍵(accelerator),並將滑鼠事件引進到 CannonField。我們放置了一個環繞著 CannonField 的外框,並且加了一個障礙物(牆)使這個遊戲更具挑戰性。
Line by Line Walkthrough
t14/cannonfield.h
CannonField 現在可以接收滑鼠事件,讓使用者藉由點選並拖曳(drag)砲管來瞄準。CannonField 也有了一道障礙牆。
除了熟悉的事件處理器以外,CannonField 引入了三個滑鼠事件處理器。名稱說明了一切。
這個私有函式畫出障礙牆。
這個私有函式返回障礙物的封裝矩形。
這個私有函式檢查某個點是否在加農砲的砲管內部。
若是使用者在砲管上按下滑鼠,並且沒有釋放它,這個私有變數的值為 true。
t14/cannonfield.cpp
這一行被加入到建構子裡。最初,滑鼠並沒有在砲管上被按下。
現在我們有了一個障礙物,也就是第三種不擊中目標的方式。我們也做了第三種測試。
這是一個 Qt 事件處理器。當滑鼠指標在元件上,且使用者按下滑鼠按鈕時,它就會被呼叫。
假如這個事件不是由左鍵觸發的,我們立刻返回。除此之外,我們確認滑鼠指標是否在加農砲的砲管之中。假如是,我們就將 barrelPressed 設為 true。
注意到 QMouseEvent::pos() 函式會返回一個在元件座標系統裡的點。
這是另一個 Qt 事件處理器。它會在使用者已經在這個元件內部按下滑鼠按鈕,並移動 / 拖曳滑鼠時被呼叫。(你還可以使 Qt 在沒有按鈕被按下時送出滑鼠移動事件。參見 QWidget::setMouseTracking()。)
這個處理器根據滑鼠指標的位置重新調整加農砲砲管的位置。
首先,假如砲管沒有被按下,我們返回。接下來,我們取得滑鼠指標的位置。假如滑鼠指標移到了元件左邊或是底部,我們調整這個點到元件內部。
然後我們計算元件底邊,與元件左下角和指標位置構成的虛線之間的角度。最後,我們將加農砲的角度設為被轉換成度數的新值。
記得要用 setAngle() 重繪加農砲。
無論使用者在何時釋放滑鼠按鈕,且滑鼠按鈕是在元件內部被按下時,這個 Qt 事件處理器就會被呼叫。
假如左鍵被釋放,我們可以確定砲管不再被按下。
繪圖事件有了額外的一行:
paintBarrier() 與 paintShot()、paintTarget()、和 paintCannon() 做的是一樣的事。
這個私有函式畫出以一個黑色邊緣、以黃色填滿的矩形作為障礙物。
這個私有函式返回障礙物的矩形。我們將障礙物的底邊固定在元件底邊。
如果點在砲管裡,這個函式會返回 true;否則返回 false。
這裡我們使用了 QMatrix 類別。QMatrix 定義了一個座標系統映射。它可以完成與 QPainter 一樣的轉換。
這裡我們完成了與我們在 paintCannon() 函式中描繪砲管所做的相同轉換步驟。首先我們轉化座標系統,然後我們旋轉它。
現在我們需要檢查點 pos (在元件座標中)是否位於砲管中。為了做到這件事,我們倒轉這個轉換矩陣。倒轉的矩陣執行了我們描繪砲管所使用的倒轉轉換。我們藉由倒轉的矩陣來映射點 pos。並且,若是它在最初的砲管矩形內部,則返回 true。
t14/gameboard.cpp
我們建立及設定一個 QFrame,並設定好它的框架風格(style)。這樣的結果是一個環繞 CannonField 的 3D 框架。
這裡我們建立及設定三個 QShortcut 物件。這些物件擷取(intercept)鍵盤事件到一個元件中,並在某幾個鍵被按下時呼叫 slot。注意,一個 QShortcut 物件是一個元件的子元件,且會在這個元件毀滅時被毀滅。QShortcut 自己並不是一個元件,且在它的父元件中沒有任何可看見的效果。
我們定義了三個快速鍵(shortcut key)。我們想要讓 fire() slot 在使用者按下 Enter 或 Return 時被呼叫。我們也想在 Ctrl+Q 被按下時退出應用程式。這次我們連接到 QWidget::close(),而不是連接到 QCoreApplication::quit()。自從 GameBoard 成為應用程式的主元件以後,它也有與 quit() 有相同的效果。
Qt::CTRL、Qt::Key_Enter、Qt::Key_Return、與 Qt::Key_Q 全都是宣告在 Qt 名稱空間裡的常數。
我們給了 cannonBox 它自己的 QVBoxLayout,並且把 cannonField 加入到版面配置中。這將間接使得 cannonField 成為 cannonBox 的子元件。因為沒有其它東西在這個盒子裡,所以結果就是 QVBoxLayout 會放置一個環繞著 CannonField 的框架。我們放置 cannonBox,而非 cannonField,到網格版面配置中。
Running the Application
現在加農砲會在你按下 Enter 時射擊。你也可以使用滑鼠把加農砲調到適當的角度。障礙物使得玩遊戲多了點挑戰性。我們也有一個看起來還不錯的外框環繞著 CannonField。
Exercises
寫一個太空入侵者的遊戲。
(這個練習首先被 Igor Rafienko 做出來了。你可以下載他的遊戲。)
新的練習是:寫一個打磚塊(Breakout)遊戲。
最後的忠告:繼續往前走,並創造程式設計藝術的傑作!
.tutorials/tutorial/t14/cannonfield.h
.tutorials/tutorial/t14/gameboard.cpp
.tutorials/tutorial/t14/gameboard.h
.tutorials/tutorial/t14/lcdrange.cpp
.tutorials/tutorial/t14/lcdrange.h
.tutorials/tutorial/t14/main.cpp
.tutorials/tutorial/t14/t14.pro
這是最後一個範例:一個完整的遊戲。

我們加入鍵盤快捷鍵(accelerator),並將滑鼠事件引進到 CannonField。我們放置了一個環繞著 CannonField 的外框,並且加了一個障礙物(牆)使這個遊戲更具挑戰性。
Line by Line Walkthrough
t14/cannonfield.h
CannonField 現在可以接收滑鼠事件,讓使用者藉由點選並拖曳(drag)砲管來瞄準。CannonField 也有了一道障礙牆。
protected:
void paintEvent(QPaintEvent *event);
void mousePressEvent(QMouseEvent *event);
void mouseMoveEvent(QMouseEvent *event);
void mouseReleaseEvent(QMouseEvent *event);
除了熟悉的事件處理器以外,CannonField 引入了三個滑鼠事件處理器。名稱說明了一切。
void paintBarrier(QPainter &painter);
這個私有函式畫出障礙牆。
QRect barrierRect() const;
這個私有函式返回障礙物的封裝矩形。
bool barrelHit(const QPoint &pos) const;
這個私有函式檢查某個點是否在加農砲的砲管內部。
bool barrelPressed;
若是使用者在砲管上按下滑鼠,並且沒有釋放它,這個私有變數的值為 true。
t14/cannonfield.cpp
barrelPressed = false;
這一行被加入到建構子裡。最初,滑鼠並沒有在砲管上被按下。
} else if (shotR.x() > width() || shotR.y() > height()
|| shotR.intersects(barrierRect())) {
現在我們有了一個障礙物,也就是第三種不擊中目標的方式。我們也做了第三種測試。
void CannonField::mousePressEvent(QMouseEvent *event)
{
if (event->button() != Qt::LeftButton)
return;
if (barrelHit(event->pos()))
barrelPressed = true;
}
這是一個 Qt 事件處理器。當滑鼠指標在元件上,且使用者按下滑鼠按鈕時,它就會被呼叫。
假如這個事件不是由左鍵觸發的,我們立刻返回。除此之外,我們確認滑鼠指標是否在加農砲的砲管之中。假如是,我們就將 barrelPressed 設為 true。
注意到 QMouseEvent::pos() 函式會返回一個在元件座標系統裡的點。
void CannonField::mouseMoveEvent(QMouseEvent *event)
{
if (!barrelPressed)
return;
QPoint pos = event->pos();
if (pos.x() <= 0)
pos.setX(1);
if (pos.y() >= height())
pos.setY(height() - 1);
double rad = atan(((double)rect().bottom() - pos.y()) / pos.x());
setAngle(qRound(rad * 180 / 3.14159265));
}
這是另一個 Qt 事件處理器。它會在使用者已經在這個元件內部按下滑鼠按鈕,並移動 / 拖曳滑鼠時被呼叫。(你還可以使 Qt 在沒有按鈕被按下時送出滑鼠移動事件。參見 QWidget::setMouseTracking()。)
這個處理器根據滑鼠指標的位置重新調整加農砲砲管的位置。
首先,假如砲管沒有被按下,我們返回。接下來,我們取得滑鼠指標的位置。假如滑鼠指標移到了元件左邊或是底部,我們調整這個點到元件內部。
然後我們計算元件底邊,與元件左下角和指標位置構成的虛線之間的角度。最後,我們將加農砲的角度設為被轉換成度數的新值。
記得要用 setAngle() 重繪加農砲。
void CannonField::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton)
barrelPressed = false;
}
無論使用者在何時釋放滑鼠按鈕,且滑鼠按鈕是在元件內部被按下時,這個 Qt 事件處理器就會被呼叫。
假如左鍵被釋放,我們可以確定砲管不再被按下。
繪圖事件有了額外的一行:
paintBarrier(painter);
paintBarrier() 與 paintShot()、paintTarget()、和 paintCannon() 做的是一樣的事。
void CannonField::paintBarrier(QPainter &painter)
{
painter.setPen(Qt::black);
painter.setBrush(Qt::yellow);
painter.drawRect(barrierRect());
}
這個私有函式畫出以一個黑色邊緣、以黃色填滿的矩形作為障礙物。
QRect CannonField::barrierRect() const
{
return QRect(145, height() - 100, 15, 99);
}
這個私有函式返回障礙物的矩形。我們將障礙物的底邊固定在元件底邊。
bool CannonField::barrelHit(const QPoint &pos) const
{
QMatrix matrix;
matrix.translate(0, height());
matrix.rotate(-currentAngle);
matrix = matrix.inverted();
return barrelRect.contains(matrix.map(pos));
}
如果點在砲管裡,這個函式會返回 true;否則返回 false。
這裡我們使用了 QMatrix 類別。QMatrix 定義了一個座標系統映射。它可以完成與 QPainter 一樣的轉換。
這裡我們完成了與我們在 paintCannon() 函式中描繪砲管所做的相同轉換步驟。首先我們轉化座標系統,然後我們旋轉它。
現在我們需要檢查點 pos (在元件座標中)是否位於砲管中。為了做到這件事,我們倒轉這個轉換矩陣。倒轉的矩陣執行了我們描繪砲管所使用的倒轉轉換。我們藉由倒轉的矩陣來映射點 pos。並且,若是它在最初的砲管矩形內部,則返回 true。
t14/gameboard.cpp
QFrame *cannonBox = new QFrame;
cannonBox->setFrameStyle(QFrame::WinPanel | QFrame::Sunken);
我們建立及設定一個 QFrame,並設定好它的框架風格(style)。這樣的結果是一個環繞 CannonField 的 3D 框架。
(void) new QShortcut(Qt::Key_Enter, this, SLOT(fire()));
(void) new QShortcut(Qt::Key_Return, this, SLOT(fire()));
(void) new QShortcut(Qt::CTRL + Qt::Key_Q, this, SLOT(close()));
這裡我們建立及設定三個 QShortcut 物件。這些物件擷取(intercept)鍵盤事件到一個元件中,並在某幾個鍵被按下時呼叫 slot。注意,一個 QShortcut 物件是一個元件的子元件,且會在這個元件毀滅時被毀滅。QShortcut 自己並不是一個元件,且在它的父元件中沒有任何可看見的效果。
我們定義了三個快速鍵(shortcut key)。我們想要讓 fire() slot 在使用者按下 Enter 或 Return 時被呼叫。我們也想在 Ctrl+Q 被按下時退出應用程式。這次我們連接到 QWidget::close(),而不是連接到 QCoreApplication::quit()。自從 GameBoard 成為應用程式的主元件以後,它也有與 quit() 有相同的效果。
Qt::CTRL、Qt::Key_Enter、Qt::Key_Return、與 Qt::Key_Q 全都是宣告在 Qt 名稱空間裡的常數。
QVBoxLayout *cannonLayout = new QVBoxLayout;
cannonLayout->addWidget(cannonField);
cannonBox->setLayout(cannonLayout);
QGridLayout *gridLayout = new QGridLayout;
gridLayout->addWidget(quit, 0, 0);
gridLayout->addLayout(topLayout, 0, 1);
gridLayout->addLayout(leftLayout, 1, 0);
gridLayout->addWidget(cannonBox, 1, 1, 2, 1);
gridLayout->setColumnStretch(1, 10);
setLayout(gridLayout);
我們給了 cannonBox 它自己的 QVBoxLayout,並且把 cannonField 加入到版面配置中。這將間接使得 cannonField 成為 cannonBox 的子元件。因為沒有其它東西在這個盒子裡,所以結果就是 QVBoxLayout 會放置一個環繞著 CannonField 的框架。我們放置 cannonBox,而非 cannonField,到網格版面配置中。
Running the Application
現在加農砲會在你按下 Enter 時射擊。你也可以使用滑鼠把加農砲調到適當的角度。障礙物使得玩遊戲多了點挑戰性。我們也有一個看起來還不錯的外框環繞著 CannonField。
Exercises
寫一個太空入侵者的遊戲。
(這個練習首先被 Igor Rafienko 做出來了。你可以下載他的遊戲。)
新的練習是:寫一個打磚塊(Breakout)遊戲。
最後的忠告:繼續往前走,並創造程式設計藝術的傑作!
來源:Qt Tutorial 14 - Facing the Wall
版本:4.4.3
【翻譯】Qt Tutorial 13 - Game Over
.tutorials/tutorial/t13/cannonfield.cpp
.tutorials/tutorial/t13/cannonfield.h
.tutorials/tutorial/t13/gameboard.cpp
.tutorials/tutorial/t13/gameboard.h
.tutorials/tutorial/t13/lcdrange.cpp
.tutorials/tutorial/t13/lcdrange.h
.tutorials/tutorial/t13/main.cpp
.tutorials/tutorial/t13/t13.pro
在這個範例,我們開始接近一個擁有分數、真正可以玩的遊戲。我們給了 MyWidget 一個新名字(GameBoard)並加入一些 slot。

我們把定義放在 gameboard.h 裡,並把實現放在 gameboard.cpp 中。
CannonField 現在有一個遊戲結束的狀態。
LCDRange 的版面配置問題被解決了。
Line by Line Walkthrough
t13/lcdrange.cpp
我們 QLabel 的大小策略(policy)設為 (Preferred, Fixed)。垂直分量確保標籤不會垂直地伸展或收縮;它將會保持在最理想的大小(它的 sizeHint())。這裡解決的版面配置的問題請看第 12 章。
t13/cannonfield.h
CannonField 現在有一個遊戲結束狀態,和一些新函數。
假如遊戲結束,這個函式會返回 true。若是遊戲仍在進行,則返回 false。
這裡有兩個新的 slot:setGameOver() 與 restartGame()。
這個新的 signal 表示 CannonField 在一個 shoot() slot 生效的狀態。我們將在下面使用它讓 Shoot 按鈕生效(enable)與失效(disable)。
這個私有變數包含了遊戲狀態;true 代表遊戲結束,而 false 代表遊戲還在進行。
t13/cannonfield.cpp
這一行被加入到建構子中。最初,遊戲還沒有結束(這對玩家來說是幸運的 :-)。
我們加入了一個新的 isShooting() 函式,所以 shoot() 使用它以取代直接進行測試。shoot() 也會告訴外界:CannonField 現在無法進行射擊。
這個 slot 終止遊戲。它必須從 CannonField 以外的地方被呼叫,因為這個元件並不知道要何時終止遊戲。這在組件程式設計中是一個重要的設計原則。我們使組件盡可能地有彈性,讓它在不同規則中都是可以使用的(舉例來說,一個第一個射中十次的玩家將獲得勝利的多玩家版本可以使用相同的 CannonField)。
假如這個遊戲已經被終止,我們立即返回。假如遊戲仍在繼續,我們則停止砲彈,設定遊戲結束的旗標(flag),並重繪整個元件。
這個 slot 開始一場新遊戲。假如一個砲彈在空中,我們停止射擊。然後我們重設了 gameEnded 變數並重繪整個元件。
就像 hit() 或是 miss(),moveShot() 也在同一時間發出新的 canShoot(true) signal。
在 CannonField::paintEvent() 中的修改:
繪圖事件被擴大成:假如遊戲結束,也就是 gameEnded 為 true 時,顯示 "Game Over" 這段文字。這裡我們並不費心於檢查矩形的更新,因為在遊戲結束的時候,速度不是必要的。
為了描繪出文字,我們首先設定了一個黑色的畫筆;畫筆的顏色是描繪文字時所使用的。接下來,我們選擇 Courier 字型 48 字級的粗體字。最後,我們將文字畫在元件矩形的中央。不幸的,在某些系統(尤其是使用 Unicode 字集的 X 伺服器)會花上一段時間來讀取如此大的字體。因為 Qt 暫存了這些字體,所以你只有在這個字體第一次被使用的時候才會注意到。
我們只有在射擊時會畫出砲彈,並只在遊戲時(這是指,遊戲還沒有結束的時候)畫出攻擊目標。
t13/gameboard.h
這個檔案是新的。它包含了 GameBoard 類別的定義,也就是我們上次看到的 MyWidget。
現在我們增加了四個 slot。它們是保護屬性且於內部被使用。我們也加入了兩個顯示遊戲狀態的 QLCDNumber(hits 與 shotsLeft)。
t13/gameboard.cpp
這個檔案是新的,它包含了 GameBoard 類別的實現,也就是我們上次看到的 MyWidget。
我們在 GameBoard 建構子中做了一些改變。
cannonField 現在是一個成員變數,所以我們小心地改變建構子以使用它。
當砲彈擊中或未擊中目標時,我們想做某些事。於是我們連接 CannonField 的 hit() 與 missed() signal 到這個類別的兩個同名的保護屬性 slot。
以前我們直接連接 Shoot 按鈕的 clicked() signal 到 CannonField 的 shoot() slot。這次我們想要紀錄大砲開火的次數,所以取而代之的,我們將它連接到這個類別裡的一個保護屬性 slot。
注意到,當你使用獨立的組件操作時,去改變一支程式的行為有多麼容易。
我們同樣使用 cannonField 的 canShoot() signal 使 Shoot 按鈕適當地生效與失效。
我們建立、設定、並連接 New Game 按鈕,跟我們為其他按鈕所做的相同。按下這個按鈕將會使這個元件的 newGame() slot 運作。
我們建立我們的新元件。注意,我們並不費心將 QLabel 元件的指標保留在 GameBoard 類別裡,因為我們沒有什麼想為它做的了。Qt 將會在 GameBoard 元件被毀滅時刪除它們,而版面配置類別則會適當地改變它們的大小。
QGridLayout 的右上格開始變擠了。我們將一個伸展空間(stretch)放置在 New Game 按鈕的左邊,確保這個按鈕總是出現在視窗的右邊。
我們做完了所有建構 GameBoard 的動作,所以我們使用 newGame() 來開始。雖然 newGame() 是一個 slot,但是它也可以像一個普通的函式一樣使用。
這個函式發射出一個砲彈。假如遊戲結束或是砲彈還在空中,我們立即返回。否則我們減少砲彈的數量,並告知加農砲射擊。
這個 slot 會在砲彈打中目標時被啟動。我們遞增擊中的次數。假如沒有剩下的砲彈,遊戲就結束了。否則,我們讓 CannonField 產生一個新目標。
這個 slot 會在砲彈沒打中目標時被啟動。假如沒有剩下的砲彈,遊戲就結束了。
這個 slot 會在使用者按下 New Game 按鈕時被啟動。它也會被建構子呼叫。首先它將砲彈數設為 15。注意,這是我們在程式中唯一設定砲彈數量的地方。接下來我們重設擊中次數、重新開始遊戲,並產生一個新的攻擊目標。
t13/main.cpp
這個檔案僅僅被減少了一部份。MyWidget 不見了,唯一留下的是 main() 函式,它除了名稱以外沒有其它改變。
Running the Application
加農砲現在可以射中目標了;當一個目標被擊中,一個新的目標會自動被建立。
擊中次數與剩餘砲彈會被顯示,而程式會紀錄它們。遊戲可以終止了,而且有一個按鈕可以開始新遊戲。
Exercises
加入一個隨機的風的因素,並把它顯示給使用者。
當砲彈擊中目標時,做一些飛濺(splatter)的效果。
實現多個目標。
.tutorials/tutorial/t13/cannonfield.h
.tutorials/tutorial/t13/gameboard.cpp
.tutorials/tutorial/t13/gameboard.h
.tutorials/tutorial/t13/lcdrange.cpp
.tutorials/tutorial/t13/lcdrange.h
.tutorials/tutorial/t13/main.cpp
.tutorials/tutorial/t13/t13.pro
在這個範例,我們開始接近一個擁有分數、真正可以玩的遊戲。我們給了 MyWidget 一個新名字(GameBoard)並加入一些 slot。

我們把定義放在 gameboard.h 裡,並把實現放在 gameboard.cpp 中。
CannonField 現在有一個遊戲結束的狀態。
LCDRange 的版面配置問題被解決了。
Line by Line Walkthrough
t13/lcdrange.cpp
label->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
我們 QLabel 的大小策略(policy)設為 (Preferred, Fixed)。垂直分量確保標籤不會垂直地伸展或收縮;它將會保持在最理想的大小(它的 sizeHint())。這裡解決的版面配置的問題請看第 12 章。
t13/cannonfield.h
CannonField 現在有一個遊戲結束狀態,和一些新函數。
bool gameOver() const { return gameEnded; }
假如遊戲結束,這個函式會返回 true。若是遊戲仍在進行,則返回 false。
void setGameOver();
void restartGame();
這裡有兩個新的 slot:setGameOver() 與 restartGame()。
void canShoot(bool can);
這個新的 signal 表示 CannonField 在一個 shoot() slot 生效的狀態。我們將在下面使用它讓 Shoot 按鈕生效(enable)與失效(disable)。
bool gameEnded;
這個私有變數包含了遊戲狀態;true 代表遊戲結束,而 false 代表遊戲還在進行。
t13/cannonfield.cpp
gameEnded = false;
這一行被加入到建構子中。最初,遊戲還沒有結束(這對玩家來說是幸運的 :-)。
void CannonField::shoot()
{
if (isShooting())
return;
timerCount = 0;
shootAngle = currentAngle;
shootForce = currentForce;
autoShootTimer->start(5);
emit canShoot(false);
}
我們加入了一個新的 isShooting() 函式,所以 shoot() 使用它以取代直接進行測試。shoot() 也會告訴外界:CannonField 現在無法進行射擊。
void CannonField::setGameOver()
{
if (gameEnded)
return;
if (isShooting())
autoShootTimer->stop();
gameEnded = true;
update();
}
這個 slot 終止遊戲。它必須從 CannonField 以外的地方被呼叫,因為這個元件並不知道要何時終止遊戲。這在組件程式設計中是一個重要的設計原則。我們使組件盡可能地有彈性,讓它在不同規則中都是可以使用的(舉例來說,一個第一個射中十次的玩家將獲得勝利的多玩家版本可以使用相同的 CannonField)。
假如這個遊戲已經被終止,我們立即返回。假如遊戲仍在繼續,我們則停止砲彈,設定遊戲結束的旗標(flag),並重繪整個元件。
void CannonField::restartGame()
{
if (isShooting())
autoShootTimer->stop();
gameEnded = false;
update();
emit canShoot(true);
}
這個 slot 開始一場新遊戲。假如一個砲彈在空中,我們停止射擊。然後我們重設了 gameEnded 變數並重繪整個元件。
就像 hit() 或是 miss(),moveShot() 也在同一時間發出新的 canShoot(true) signal。
在 CannonField::paintEvent() 中的修改:
void CannonField::paintEvent(QPaintEvent * /* event */)
{
QPainter painter(this);
if (gameEnded) {
painter.setPen(Qt::black);
painter.setFont(QFont("Courier", 48, QFont::Bold));
painter.drawText(rect(), Qt::AlignCenter, tr("Game Over"));
}
繪圖事件被擴大成:假如遊戲結束,也就是 gameEnded 為 true 時,顯示 "Game Over" 這段文字。這裡我們並不費心於檢查矩形的更新,因為在遊戲結束的時候,速度不是必要的。
為了描繪出文字,我們首先設定了一個黑色的畫筆;畫筆的顏色是描繪文字時所使用的。接下來,我們選擇 Courier 字型 48 字級的粗體字。最後,我們將文字畫在元件矩形的中央。不幸的,在某些系統(尤其是使用 Unicode 字集的 X 伺服器)會花上一段時間來讀取如此大的字體。因為 Qt 暫存了這些字體,所以你只有在這個字體第一次被使用的時候才會注意到。
paintCannon(painter);
if (isShooting())
paintShot(painter);
if (!gameEnded)
paintTarget(painter);
}
我們只有在射擊時會畫出砲彈,並只在遊戲時(這是指,遊戲還沒有結束的時候)畫出攻擊目標。
t13/gameboard.h
這個檔案是新的。它包含了 GameBoard 類別的定義,也就是我們上次看到的 MyWidget。
class CannonField;
class GameBoard : public QWidget
{
Q_OBJECT
public:
GameBoard(QWidget *parent = 0);
protected slots:
void fire();
void hit();
void missed();
void newGame();
private:
QLCDNumber *hits;
QLCDNumber *shotsLeft;
CannonField *cannonField;
};
現在我們增加了四個 slot。它們是保護屬性且於內部被使用。我們也加入了兩個顯示遊戲狀態的 QLCDNumber(hits 與 shotsLeft)。
t13/gameboard.cpp
這個檔案是新的,它包含了 GameBoard 類別的實現,也就是我們上次看到的 MyWidget。
我們在 GameBoard 建構子中做了一些改變。
cannonField = new CannonField;
cannonField 現在是一個成員變數,所以我們小心地改變建構子以使用它。
connect(cannonField, SIGNAL(hit()),
this, SLOT(hit()));
connect(cannonField, SIGNAL(missed()),
this, SLOT(missed()));
當砲彈擊中或未擊中目標時,我們想做某些事。於是我們連接 CannonField 的 hit() 與 missed() signal 到這個類別的兩個同名的保護屬性 slot。
connect(shoot, SIGNAL(clicked()),
this, SLOT(fire()));
以前我們直接連接 Shoot 按鈕的 clicked() signal 到 CannonField 的 shoot() slot。這次我們想要紀錄大砲開火的次數,所以取而代之的,我們將它連接到這個類別裡的一個保護屬性 slot。
注意到,當你使用獨立的組件操作時,去改變一支程式的行為有多麼容易。
connect(cannonField, SIGNAL(canShoot(bool)),
shoot, SLOT(setEnabled(bool)));
我們同樣使用 cannonField 的 canShoot() signal 使 Shoot 按鈕適當地生效與失效。
QPushButton *restart = new QPushButton(tr("&New Game"));
restart->setFont(QFont("Times", 18, QFont::Bold));
connect(restart, SIGNAL(clicked()), this, SLOT(newGame()));
我們建立、設定、並連接 New Game 按鈕,跟我們為其他按鈕所做的相同。按下這個按鈕將會使這個元件的 newGame() slot 運作。
hits = new QLCDNumber(2);
hits->setSegmentStyle(QLCDNumber::Filled);
shotsLeft = new QLCDNumber(2);
shotsLeft->setSegmentStyle(QLCDNumber::Filled);
QLabel *hitsLabel = new QLabel(tr("HITS"));
QLabel *shotsLeftLabel = new QLabel(tr("SHOTS LEFT"));
我們建立我們的新元件。注意,我們並不費心將 QLabel 元件的指標保留在 GameBoard 類別裡,因為我們沒有什麼想為它做的了。Qt 將會在 GameBoard 元件被毀滅時刪除它們,而版面配置類別則會適當地改變它們的大小。
QHBoxLayout *topLayout = new QHBoxLayout;
topLayout->addWidget(shoot);
topLayout->addWidget(hits);
topLayout->addWidget(hitsLabel);
topLayout->addWidget(shotsLeft);
topLayout->addWidget(shotsLeftLabel);
topLayout->addStretch(1);
topLayout->addWidget(restart);
QGridLayout 的右上格開始變擠了。我們將一個伸展空間(stretch)放置在 New Game 按鈕的左邊,確保這個按鈕總是出現在視窗的右邊。
newGame();
我們做完了所有建構 GameBoard 的動作,所以我們使用 newGame() 來開始。雖然 newGame() 是一個 slot,但是它也可以像一個普通的函式一樣使用。
void GameBoard::fire()
{
if (cannonField->gameOver() || cannonField->isShooting())
return;
shotsLeft->display(shotsLeft->intValue() - 1);
cannonField->shoot();
}
這個函式發射出一個砲彈。假如遊戲結束或是砲彈還在空中,我們立即返回。否則我們減少砲彈的數量,並告知加農砲射擊。
void GameBoard::hit()
{
hits->display(hits->intValue() + 1);
if (shotsLeft->intValue() == 0)
cannonField->setGameOver();
else
cannonField->newTarget();
}
這個 slot 會在砲彈打中目標時被啟動。我們遞增擊中的次數。假如沒有剩下的砲彈,遊戲就結束了。否則,我們讓 CannonField 產生一個新目標。
void GameBoard::missed()
{
if (shotsLeft->intValue() == 0)
cannonField->setGameOver();
}
這個 slot 會在砲彈沒打中目標時被啟動。假如沒有剩下的砲彈,遊戲就結束了。
void GameBoard::newGame()
{
shotsLeft->display(15);
hits->display(0);
cannonField->restartGame();
cannonField->newTarget();
}
這個 slot 會在使用者按下 New Game 按鈕時被啟動。它也會被建構子呼叫。首先它將砲彈數設為 15。注意,這是我們在程式中唯一設定砲彈數量的地方。接下來我們重設擊中次數、重新開始遊戲,並產生一個新的攻擊目標。
t13/main.cpp
這個檔案僅僅被減少了一部份。MyWidget 不見了,唯一留下的是 main() 函式,它除了名稱以外沒有其它改變。
Running the Application
加農砲現在可以射中目標了;當一個目標被擊中,一個新的目標會自動被建立。
擊中次數與剩餘砲彈會被顯示,而程式會紀錄它們。遊戲可以終止了,而且有一個按鈕可以開始新遊戲。
Exercises
加入一個隨機的風的因素,並把它顯示給使用者。
當砲彈擊中目標時,做一些飛濺(splatter)的效果。
實現多個目標。
來源:Qt Tutorial 13 - Game Over
版本:4.4.3
【翻譯】Qt Tutorial 12 - Hanging in the Air the Way Bricks Don't
.tutorials/tutorial/t12/cannonfield.cpp
.tutorials/tutorial/t12/cannonfield.h
.tutorials/tutorial/t12/lcdrange.cpp
.tutorials/tutorial/t12/lcdrange.h
.tutorials/tutorial/t12/main.cpp
.tutorials/tutorial/t12/t12.pro
在這個範例中,我們擴充我們的 LCDRange 來引入一個文字標籤(label)。我們也會提供用來射擊的東西。

Line by Line Walkthrough
t12/lcdrange.h
LCDRange 現在有一個文字標籤了。
我們前向宣告了 QLabel 與 QSlider,因為我們想要在類別定義裡使用它們的指標。我們也可以使用 #include,不過這會白白拖慢編譯速度。
我們加入了一個新建構子。除了父元件之外,它還可以設定標籤的文字。
這個函式返回標籤文字。
這個 slot 設定標籤文字。
因為我們現在有兩個建構子,所以我們選擇把共同的初始化動作放到一個私有的 init() 函式中。
我們也多了一個新的私有變數:一個 QLabel。QLabel 是 Qt 標準元件的其中一個,並可以顯示有或沒有外框的一段文字或是一個 QPixmap。
t12/lcdrange.cpp
這個建構子呼叫了包含通用初始化程式碼的 init() 函式。
這個建構子先呼叫 init() 函式,然後設定標籤文字
lcd 與 slider 的結構與先前的章節相同。接下來,我們建立一個 QLabel,並告知它以水平置中與垂直向上的方式對齊內容。QObject::connect() 的呼叫也同樣取自於先前的章節。
這個函式返回標籤文字。
這個函式設定了標籤文字。
t12/cannonfield.h
CannonField 現在有兩個新的 signal:hit() 與 missed()。此外,它還多了一個攻擊目標。
這個 slot 在一個新的地方建立一個攻擊目標。
hit() signal 會在砲彈擊中目標時被送出。missed() signal 會在砲彈移動到超過元件右邊或底部邊界(換句話說,就是確信它沒有,也不會擊中目標)時被發出。
這個私有函式畫出攻擊的目標。
這個函式返回攻擊目標的封裝矩形。
這個私有變數包含了攻擊目標的中心點。
t12/cannonfield.cpp
因為我們需要 qrand() 函式,所以我們引入了 <stdlib.h> 標頭檔。
這一行被加入了建構子當中。它會為攻擊目標建立了一個「隨機」的位置。事實上,newTarget() 函式將會試著畫出攻擊目標。因為我們在一個建構子中,所以 CannonField 元件是不可見的。Qt 保證在隱藏的元件呼叫 QWidget::update() 是無害的。
這個私有函式在一個新的隨機位置建立了一個攻擊目標中心點。
我們使用 qrand() 來取得隨機的整數。qrand() 函式通常在你每次運行程式都會返回同一組數字。這會使得攻擊目標每次都出現在相同的位置。為了避免它,我們必須在這個函式第一次被呼叫的時候設定一個亂數子(random seed)。為了避免同一組數字,亂數子也必須是隨機的。解決的方法是使用從午夜到現在經過的秒數,作為一個近似隨機(pseudo-random)的值。
首先我們建立一個靜態(static)布林(bool)區域變數。一個靜態變數就像是保證在多次呼叫函式之間,保持它的值不變。
因為我們在 if 區塊把 firstTime 設成了 false,所以只有在第一次呼叫這個函式時 if 的條件才會成立。
然後,我們建立了表示時間 00:00:00 的 QTime 物件 midnight。接下來,我們取得從午夜直到現在經過的秒數,並使用它作為一個亂數子。請參見文件 QDate、QTime、與 QDateTime 以取得更多資訊。
最後我們計算出攻擊目標的中心點。我們保持它在我們讓元件底邊的 y 位置為 0,並使 y 向上遞增、x 為一般的以左邊界為 0,x 向右遞增的座標系統中的一個矩形裡(x = 200、y = 35、寬度 = 190、高度 = 255。換句話說,可能的 x 與 y 值分別介於 200 ~ 389 與 35 ~ 289 之間)。
經過實驗,我們發現這都是砲彈可觸及的。
來自先前章節的計時器事件這個部份並沒有被改變。
這個 if 敘述確認砲彈矩形是否與目標矩形相交。假如是,砲彈就擊中目標了(哎喲!)。我們停止射擊計時器並送出 hit() signal 告訴外界:目標已被毀滅,並返回。
注意,我們可以在這個點上建一個新的攻擊目標,不過因為 CannonField 是一個組件,我們把這樣的決定留給組件的使用者。
這個 if 敘述與先前的章節相同,除了它現在會送出 missed() signal 以告知外界射擊失敗。
而函式的其他部分與之前相同。
CannonField::paintEvent() 與之前相同,除了被加入了這個:
這一行確定在需要的時候,攻擊目標也會被畫出來。
這個私有函式畫出攻擊目標;一個黑色外框,以紅色填滿的矩形。
這個私有函式返回攻擊目標的封裝矩形。記得從 newTarget() 所得到的 target 點以元件底部邊界的 y 坐標為 0。我們在呼叫 QRect::moveCenter() 之前,在元件座標中計算這個點。
我們選擇這個坐標映射(mapping)的原因,是為了固定攻擊目標與元件底部的距離。記得這個元件可以在任何時間被程式或是使用者改變大小。
t12/main.cpp
MyWidget 類別裡沒有新成員,不過我們稍微改變了一下建構子,以設定這個新的 LCDRange 文字標籤。
我們設定角度的文字標籤為 "ANGLE"。
我們設定力量的文字標籤為 "FORCE"。
Running the Application
LCDRange 元件看起來有一點奇怪:當改變 MyWidget 的大小時,QVBoxLayout 內建的配置管理給了標籤太多空間,而其他的空間就不夠了;使得兩個 LCDRange 元件之間的空間改變大小。我們將在下一個章節修正它。
Exercises
做一個作弊按鈕,當它被按下,使 CannonField 顯示五秒的砲彈軌跡。
假如你做了前一章的「圓形砲彈」練習,試著把 shotRect() 改變成一個返回 QRegion 的 shotRegion()。如此你可以做到相當準確的碰撞判定。
做一個移動的攻擊目標。
確保攻擊目標被建立時,總是完整的在螢幕中。
確保元件不會被改變大小,以致於攻擊目標變成不可見的。[提示:QWidget::setMinimumSize() 是你的好伙伴。]
這不太容易;使空中一次有多個砲彈變為可能。[提示:建立一個 shot 類別。]
.tutorials/tutorial/t12/cannonfield.h
.tutorials/tutorial/t12/lcdrange.cpp
.tutorials/tutorial/t12/lcdrange.h
.tutorials/tutorial/t12/main.cpp
.tutorials/tutorial/t12/t12.pro
在這個範例中,我們擴充我們的 LCDRange 來引入一個文字標籤(label)。我們也會提供用來射擊的東西。

Line by Line Walkthrough
t12/lcdrange.h
LCDRange 現在有一個文字標籤了。
class QLabel; class QSlider;
我們前向宣告了 QLabel 與 QSlider,因為我們想要在類別定義裡使用它們的指標。我們也可以使用 #include,不過這會白白拖慢編譯速度。
class LCDRange : public QWidget
{
Q_OBJECT
public:
LCDRange(QWidget *parent = 0);
LCDRange(const QString &text, QWidget *parent = 0);
我們加入了一個新建構子。除了父元件之外,它還可以設定標籤的文字。
QString text() const;
這個函式返回標籤文字。
void setText(const QString &text);
這個 slot 設定標籤文字。
private:
void init();
因為我們現在有兩個建構子,所以我們選擇把共同的初始化動作放到一個私有的 init() 函式中。
QLabel *label;
我們也多了一個新的私有變數:一個 QLabel。QLabel 是 Qt 標準元件的其中一個,並可以顯示有或沒有外框的一段文字或是一個 QPixmap。
t12/lcdrange.cpp
LCDRange::LCDRange(QWidget *parent)
: QWidget(parent)
{
init();
}
這個建構子呼叫了包含通用初始化程式碼的 init() 函式。
LCDRange::LCDRange(const QString &text, QWidget *parent)
: QWidget(parent)
{
init();
setText(text);
}
這個建構子先呼叫 init() 函式,然後設定標籤文字
void LCDRange::init()
{
QLCDNumber *lcd = new QLCDNumber(2);
lcd->setSegmentStyle(QLCDNumber::Filled);
slider = new QSlider(Qt::Horizontal);
slider->setRange(0, 99);
slider->setValue(0);
label = new QLabel;
label->setAlignment(Qt::AlignHCenter | Qt::AlignTop);
connect(slider, SIGNAL(valueChanged(int)),
lcd, SLOT(display(int)));
connect(slider, SIGNAL(valueChanged(int)),
this, SIGNAL(valueChanged(int)));
QVBoxLayout *layout = new QVBoxLayout;
layout->addWidget(lcd);
layout->addWidget(slider);
layout->addWidget(label);
setLayout(layout);
setFocusProxy(slider);
}
lcd 與 slider 的結構與先前的章節相同。接下來,我們建立一個 QLabel,並告知它以水平置中與垂直向上的方式對齊內容。QObject::connect() 的呼叫也同樣取自於先前的章節。
QString LCDRange::text() const
{
return label->text();
}
這個函式返回標籤文字。
void LCDRange::setText(const QString &text)
{
label->setText(text);
}
這個函式設定了標籤文字。
t12/cannonfield.h
CannonField 現在有兩個新的 signal:hit() 與 missed()。此外,它還多了一個攻擊目標。
void newTarget();
這個 slot 在一個新的地方建立一個攻擊目標。
signals:
void hit();
void missed();
hit() signal 會在砲彈擊中目標時被送出。missed() signal 會在砲彈移動到超過元件右邊或底部邊界(換句話說,就是確信它沒有,也不會擊中目標)時被發出。
void paintTarget(QPainter &painter);
這個私有函式畫出攻擊的目標。
QRect targetRect() const;
這個函式返回攻擊目標的封裝矩形。
QPoint target;
這個私有變數包含了攻擊目標的中心點。
t12/cannonfield.cpp
#include <stdlib.h>
因為我們需要 qrand() 函式,所以我們引入了 <stdlib.h> 標頭檔。
newTarget();
這一行被加入了建構子當中。它會為攻擊目標建立了一個「隨機」的位置。事實上,newTarget() 函式將會試著畫出攻擊目標。因為我們在一個建構子中,所以 CannonField 元件是不可見的。Qt 保證在隱藏的元件呼叫 QWidget::update() 是無害的。
void CannonField::newTarget()
{
static bool firstTime = true;
if (firstTime) {
firstTime = false;
QTime midnight(0, 0, 0);
qsrand(midnight.secsTo(QTime::currentTime()));
}
target = QPoint(200 + qrand() % 190, 10 + qrand() % 255);
update();
}
這個私有函式在一個新的隨機位置建立了一個攻擊目標中心點。
我們使用 qrand() 來取得隨機的整數。qrand() 函式通常在你每次運行程式都會返回同一組數字。這會使得攻擊目標每次都出現在相同的位置。為了避免它,我們必須在這個函式第一次被呼叫的時候設定一個亂數子(random seed)。為了避免同一組數字,亂數子也必須是隨機的。解決的方法是使用從午夜到現在經過的秒數,作為一個近似隨機(pseudo-random)的值。
首先我們建立一個靜態(static)布林(bool)區域變數。一個靜態變數就像是保證在多次呼叫函式之間,保持它的值不變。
因為我們在 if 區塊把 firstTime 設成了 false,所以只有在第一次呼叫這個函式時 if 的條件才會成立。
然後,我們建立了表示時間 00:00:00 的 QTime 物件 midnight。接下來,我們取得從午夜直到現在經過的秒數,並使用它作為一個亂數子。請參見文件 QDate、QTime、與 QDateTime 以取得更多資訊。
最後我們計算出攻擊目標的中心點。我們保持它在我們讓元件底邊的 y 位置為 0,並使 y 向上遞增、x 為一般的以左邊界為 0,x 向右遞增的座標系統中的一個矩形裡(x = 200、y = 35、寬度 = 190、高度 = 255。換句話說,可能的 x 與 y 值分別介於 200 ~ 389 與 35 ~ 289 之間)。
經過實驗,我們發現這都是砲彈可觸及的。
void CannonField::moveShot()
{
QRegion region = shotRect();
++timerCount;
QRect shotR = shotRect();
來自先前章節的計時器事件這個部份並沒有被改變。
if (shotR.intersects(targetRect())) {
autoShootTimer->stop();
emit hit();
這個 if 敘述確認砲彈矩形是否與目標矩形相交。假如是,砲彈就擊中目標了(哎喲!)。我們停止射擊計時器並送出 hit() signal 告訴外界:目標已被毀滅,並返回。
注意,我們可以在這個點上建一個新的攻擊目標,不過因為 CannonField 是一個組件,我們把這樣的決定留給組件的使用者。
} else if (shotR.x() > width() || shotR.y() > height()) {
autoShootTimer->stop();
emit missed();
這個 if 敘述與先前的章節相同,除了它現在會送出 missed() signal 以告知外界射擊失敗。
} else {
region = region.unite(shotR);
}
update(region);
}
而函式的其他部分與之前相同。
CannonField::paintEvent() 與之前相同,除了被加入了這個:
paintTarget(painter);
這一行確定在需要的時候,攻擊目標也會被畫出來。
void CannonField::paintTarget(QPainter &painter)
{
painter.setPen(Qt::black);
painter.setBrush(Qt::red);
painter.drawRect(targetRect());
}
這個私有函式畫出攻擊目標;一個黑色外框,以紅色填滿的矩形。
QRect CannonField::targetRect() const
{
QRect result(0, 0, 20, 10);
result.moveCenter(QPoint(target.x(), height() - 1 - target.y()));
return result;
}
這個私有函式返回攻擊目標的封裝矩形。記得從 newTarget() 所得到的 target 點以元件底部邊界的 y 坐標為 0。我們在呼叫 QRect::moveCenter() 之前,在元件座標中計算這個點。
我們選擇這個坐標映射(mapping)的原因,是為了固定攻擊目標與元件底部的距離。記得這個元件可以在任何時間被程式或是使用者改變大小。
t12/main.cpp
MyWidget 類別裡沒有新成員,不過我們稍微改變了一下建構子,以設定這個新的 LCDRange 文字標籤。
LCDRange *angle = new LCDRange(tr("ANGLE"));
我們設定角度的文字標籤為 "ANGLE"。
LCDRange *force = new LCDRange(tr("FORCE"));
我們設定力量的文字標籤為 "FORCE"。
Running the Application
LCDRange 元件看起來有一點奇怪:當改變 MyWidget 的大小時,QVBoxLayout 內建的配置管理給了標籤太多空間,而其他的空間就不夠了;使得兩個 LCDRange 元件之間的空間改變大小。我們將在下一個章節修正它。
Exercises
做一個作弊按鈕,當它被按下,使 CannonField 顯示五秒的砲彈軌跡。
假如你做了前一章的「圓形砲彈」練習,試著把 shotRect() 改變成一個返回 QRegion 的 shotRegion()。如此你可以做到相當準確的碰撞判定。
做一個移動的攻擊目標。
確保攻擊目標被建立時,總是完整的在螢幕中。
確保元件不會被改變大小,以致於攻擊目標變成不可見的。[提示:QWidget::setMinimumSize() 是你的好伙伴。]
這不太容易;使空中一次有多個砲彈變為可能。[提示:建立一個 shot 類別。]
來源:Qt Tutorial 12 - Hanging in the Air the Way Bricks Don't
版本:4.4.3
【翻譯】Qt Tutorial 11 - Giving It a Shot
.tutorials/tutorial/t11/cannonfield.cpp
.tutorials/tutorial/t11/cannonfield.h
.tutorials/tutorial/t11/lcdrange.cpp
.tutorials/tutorial/t11/lcdrange.h
.tutorials/tutorial/t11/main.cpp
.tutorials/tutorial/t11/t11.pro
在這個範例裡,我們採用了計時器來實現動畫的射擊(shoot)。

Line by Line Walkthrough
t11/cannonfield.h
CannonField 現在有射擊能力了。
假如砲彈不在空中,呼叫這個 slot 將會使加農砲射擊。
當砲彈在空中時,這個私有的(private) slot 會使用 QTimer 來移動砲彈。
這個私有函式會畫出砲彈。
假如砲彈在空中,這個私有函式會返回它的封裝矩形;否則傳回的是一個未定義的矩型。
這些私有變數包含了描述砲彈的資訊。timerCount 紀錄了砲彈開火之後經過的時間。shootAngle 為加農砲的角度,shootForce 是加農砲射擊時的力量。
t11/cannonfield.cpp
因為我們需要 sin() 及 cos() 函式,所以我們引入了 <math.h>。(另一個選擇是引入比較新式的 <cmath> 標頭檔。不幸的是,一些 Unix 平台仍然沒有正確的支援它。)
我們初始化我們的新私有變數,並連接 QTimer::timeout() signal 到我們的 moveShot() slot。我們將在每次計時器超時(time out)時移動砲彈。
除非砲彈在空中,否則這個函式會射出一個砲彈。timerCount 被重置為 0。shootAngle 與 shootForce 變數會被設成當前的加農砲角度以及力量。最後,我們啟動計時器。
moveShot() 是一個移動砲彈的 slot,當 QTimer 啟動後,其每 5 毫秒會被呼叫一次。
它的任務是計算新的砲彈位置,將螢幕的砲彈更新到新的位置。而且,如果需要的話,停止計時器。
首先我們建立一個 QRegion 來保留舊的 shotRect()。一個 QRegion 可以保留任何種類的區域(region),而且我們可以在這裡使用它來簡化繪圖。shotRect() 返回砲彈現在位置的矩形。這在稍後會詳細解釋。
現在我們遞增 timerCount,它實現了砲彈在軌道中移動的每一步。
接下來我們取得新的砲彈矩形。
假如砲彈越過了元件右邊或底部的邊界,我們就停止計時器,否則我們將新的 shotRect() 加入到 QRegion。
最後,我們重繪 QRegion。這將會送出一個只有一個或兩個矩形需要更新的單一繪圖事件。
繪圖事件函式比起先前的章節被簡化了。許多邏輯操作被移到新的 paintShot() 和 paintCannon() 函式。
這個私有函式畫出一個使用黑色填滿的矩型作為砲彈。
我們把 paintCannon() 的實現丟到一旁;它與先前章節 QWidget::paintEvent() 的重新實現相同。
這個私有函式計算出砲彈的中心點,並返回砲彈的封裝矩形。除了隨著時間經過而增加的 timerCount 之外,它還使用了加農砲的最初力量與角度。
這個公式使用的是重力場下無摩擦力移動的標準牛頓公式。為了簡單起見,我們選擇忽略任何愛因斯坦理論。
我們在一個 y 坐標向上遞增的座標系統中計算中心點。在我們計算出中心點之後,我們建構了一個大小為 6 x 6 的 QRect,並移動它的中心點到計算出來的中心點之上。藉由同樣的操作,我們將這個點轉換成元件的座標系統(參閱 The Coordinate System)。
qRound() 函式是一個定義在 <QtGlobal> (被所有 Qt 標頭檔所引入)裡的行內(inline)函式。qRound() 會將一個雙精度浮點數(double)轉換為最接近的整數。
t11/main.cpp
唯一加入的東西是 Shoot 按紐。
在建構子中,我們建立並設定 Shoot 按鈕,就像我們為 Quit 按紐所做的
將 Shoot 按鈕的 clicked() signal 連結到 CannonField 的 shoot() slot。
Running the Application
加農砲能射擊了,不過沒有任何東西會被射中。
Exercises
將砲彈變成一個被填滿的圓。[提示:QPainter::drawEllipse() 可能會有幫助。]
當砲彈在空中時,改變加農砲的顏色。
.tutorials/tutorial/t11/cannonfield.h
.tutorials/tutorial/t11/lcdrange.cpp
.tutorials/tutorial/t11/lcdrange.h
.tutorials/tutorial/t11/main.cpp
.tutorials/tutorial/t11/t11.pro
在這個範例裡,我們採用了計時器來實現動畫的射擊(shoot)。

Line by Line Walkthrough
t11/cannonfield.h
CannonField 現在有射擊能力了。
void shoot();
假如砲彈不在空中,呼叫這個 slot 將會使加農砲射擊。
private slots:
void moveShot();
當砲彈在空中時,這個私有的(private) slot 會使用 QTimer 來移動砲彈。
private:
void paintShot(QPainter &painter);
這個私有函式會畫出砲彈。
QRect shotRect() const;
假如砲彈在空中,這個私有函式會返回它的封裝矩形;否則傳回的是一個未定義的矩型。
int timerCount;
QTimer *autoShootTimer;
float shootAngle;
float shootForce;
};
這些私有變數包含了描述砲彈的資訊。timerCount 紀錄了砲彈開火之後經過的時間。shootAngle 為加農砲的角度,shootForce 是加農砲射擊時的力量。
t11/cannonfield.cpp
#include <math.h>
因為我們需要 sin() 及 cos() 函式,所以我們引入了 <math.h>。(另一個選擇是引入比較新式的 <cmath> 標頭檔。不幸的是,一些 Unix 平台仍然沒有正確的支援它。)
CannonField::CannonField(QWidget *parent)
: QWidget(parent)
{
currentAngle = 45;
currentForce = 0;
timerCount = 0;
autoShootTimer = new QTimer(this);
connect(autoShootTimer, SIGNAL(timeout()), this, SLOT(moveShot()));
shootAngle = 0;
shootForce = 0;
setPalette(QPalette(QColor(250, 250, 200)));
setAutoFillBackground(true);
}
我們初始化我們的新私有變數,並連接 QTimer::timeout() signal 到我們的 moveShot() slot。我們將在每次計時器超時(time out)時移動砲彈。
void CannonField::shoot()
{
if (autoShootTimer->isActive())
return;
timerCount = 0;
shootAngle = currentAngle;
shootForce = currentForce;
autoShootTimer->start(5);
}
除非砲彈在空中,否則這個函式會射出一個砲彈。timerCount 被重置為 0。shootAngle 與 shootForce 變數會被設成當前的加農砲角度以及力量。最後,我們啟動計時器。
void CannonField::moveShot()
{
QRegion region = shotRect();
++timerCount;
QRect shotR = shotRect();
if (shotR.x() > width() || shotR.y() > height()) {
autoShootTimer->stop();
} else {
region = region.unite(shotR);
}
update(region);
}
moveShot() 是一個移動砲彈的 slot,當 QTimer 啟動後,其每 5 毫秒會被呼叫一次。
它的任務是計算新的砲彈位置,將螢幕的砲彈更新到新的位置。而且,如果需要的話,停止計時器。
首先我們建立一個 QRegion 來保留舊的 shotRect()。一個 QRegion 可以保留任何種類的區域(region),而且我們可以在這裡使用它來簡化繪圖。shotRect() 返回砲彈現在位置的矩形。這在稍後會詳細解釋。
現在我們遞增 timerCount,它實現了砲彈在軌道中移動的每一步。
接下來我們取得新的砲彈矩形。
假如砲彈越過了元件右邊或底部的邊界,我們就停止計時器,否則我們將新的 shotRect() 加入到 QRegion。
最後,我們重繪 QRegion。這將會送出一個只有一個或兩個矩形需要更新的單一繪圖事件。
void CannonField::paintEvent(QPaintEvent * /* event */)
{
QPainter painter(this);
paintCannon(painter);
if (autoShootTimer->isActive())
paintShot(painter);
}
繪圖事件函式比起先前的章節被簡化了。許多邏輯操作被移到新的 paintShot() 和 paintCannon() 函式。
void CannonField::paintShot(QPainter &painter)
{
painter.setPen(Qt::NoPen);
painter.setBrush(Qt::black);
painter.drawRect(shotRect());
}
這個私有函式畫出一個使用黑色填滿的矩型作為砲彈。
我們把 paintCannon() 的實現丟到一旁;它與先前章節 QWidget::paintEvent() 的重新實現相同。
QRect CannonField::shotRect() const
{
const double gravity = 4;
double time = timerCount / 20.0;
double velocity = shootForce;
double radians = shootAngle * 3.14159265 / 180;
double velx = velocity * cos(radians);
double vely = velocity * sin(radians);
double x0 = (barrelRect.right() + 5) * cos(radians);
double y0 = (barrelRect.right() + 5) * sin(radians);
double x = x0 + velx * time;
double y = y0 + vely * time - 0.5 * gravity * time * time;
QRect result(0, 0, 6, 6);
result.moveCenter(QPoint(qRound(x), height() - 1 - qRound(y)));
return result;
}
這個私有函式計算出砲彈的中心點,並返回砲彈的封裝矩形。除了隨著時間經過而增加的 timerCount 之外,它還使用了加農砲的最初力量與角度。
這個公式使用的是重力場下無摩擦力移動的標準牛頓公式。為了簡單起見,我們選擇忽略任何愛因斯坦理論。
我們在一個 y 坐標向上遞增的座標系統中計算中心點。在我們計算出中心點之後,我們建構了一個大小為 6 x 6 的 QRect,並移動它的中心點到計算出來的中心點之上。藉由同樣的操作,我們將這個點轉換成元件的座標系統(參閱 The Coordinate System)。
qRound() 函式是一個定義在 <QtGlobal> (被所有 Qt 標頭檔所引入)裡的行內(inline)函式。qRound() 會將一個雙精度浮點數(double)轉換為最接近的整數。
t11/main.cpp
class MyWidget : public QWidget
{
public:
MyWidget(QWidget *parent = 0);
};
唯一加入的東西是 Shoot 按紐。
QPushButton *shoot = new QPushButton(tr("&Shoot"));
shoot->setFont(QFont("Times", 18, QFont::Bold));
在建構子中,我們建立並設定 Shoot 按鈕,就像我們為 Quit 按紐所做的
connect(shoot, SIGNAL(clicked()), cannonField, SLOT(shoot()));
將 Shoot 按鈕的 clicked() signal 連結到 CannonField 的 shoot() slot。
Running the Application
加農砲能射擊了,不過沒有任何東西會被射中。
Exercises
將砲彈變成一個被填滿的圓。[提示:QPainter::drawEllipse() 可能會有幫助。]
當砲彈在空中時,改變加農砲的顏色。
來源:Qt Tutorial 11 - Giving It a Shot
版本:4.4.3
訂閱:
文章 (Atom)
Infinite Loop © 2008. Blog design by Blogcut | Converted by Fernandooo1 (Randomness)