NOTICE

 任何跟文章無關的閒聊,請愛用 留言板(Guestbook)。

 想要快速瀏覽主題,請點選單 目錄 標籤。

 停止更新ing,請見諒。 <(_ _)>


顯示具有 翻譯 標籤的文章。 顯示所有文章
顯示具有 翻譯 標籤的文章。 顯示所有文章

2月 25, 2009

【翻譯】Address Book Tutorial

@
  這份指導教學利用 Qt 的跨平台(cross-platform)框架(framework),為 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()。

     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。

     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 父元件,即使對話視窗將會做為一個單獨的視窗開啟。

 #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 列舉:

  • 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 檔案:

     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。

 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() 函式可以被分成三個部份:

  1. 我們設法從 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;
         }
    

  2. 然後,我們開始確認聯絡人是否已經存在。假如不存在,我們加入聯絡人到 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 為基礎。因此,我們要確保鍵是唯一的。

  3. 一旦我們處理完上述兩種情況,我們用接下來的程式碼來將按鈕恢復成它們的標準狀態:

         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:

  • 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月 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 來建立、顯示一個採用預設大小的視窗:

     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 也有了一道障礙牆。

 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

     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 現在有一個文字標籤了。

 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 現在有射擊能力了。

     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

【翻譯】Qt Tutorial 10 - Smooth as Silk

@
.tutorials/tutorial/t10/cannonfield.cpp
.tutorials/tutorial/t10/cannonfield.h
.tutorials/tutorial/t10/lcdrange.cpp
.tutorials/tutorial/t10/lcdrange.h
.tutorials/tutorial/t10/main.cpp
.tutorials/tutorial/t10/t10.pro

  在這個範例中,我們加入了力量(force)控制。




Line by Line Walkthrough

t10/cannonfield.h

  CannonField 現在除了角度外,又多了一個力量值。

     int angle() const { return currentAngle; }
     int force() const { return currentForce; }

 public slots:
     void setAngle(int angle);
     void setForce(int force);

 signals:
     void angleChanged(int newAngle);
     void forceChanged(int newForce);

  力量的介面遵循著與角度同樣的寫法。

 private:
     QRect cannonRect() const;

  我們將加農砲的封裝矩形定義放到一個單獨的函式中。

     int currentAngle;
     int currentForce;
 };

  力量被存在一個 currentForce 變數中。

t10/cannonfield.cpp

 CannonField::CannonField(QWidget *parent)
     : QWidget(parent)
 {
     currentAngle = 45;
     currentForce = 0;
     setPalette(QPalette(QColor(250, 250, 200)));
     setAutoFillBackground(true);
 }

  力量被初始為零。

 void CannonField::setAngle(int angle)
 {
     if (angle < 5)
         angle = 5;
     if (angle > 70)
         angle = 70;
     if (currentAngle == angle)
         return;
     currentAngle = angle;
     update(cannonRect());
     emit angleChanged(currentAngle);
 }

  我們在 setAngle() 函式中做一點細微的改變。它只會重繪元件中包含加農砲的那一部分。

 void CannonField::setForce(int force)
 {
     if (force < 0)
         force = 0;
     if (currentForce == force)
         return;
     currentForce = force;
     emit forceChanged(currentForce);
 }

  setForce() 的實現與 setAngle() 相當類似。它們唯一的不同是因為我們沒有顯示出力量值,所以我們不需要重繪元件。

 void CannonField::paintEvent(QPaintEvent * /* event */)
 {
     QPainter painter(this);

     painter.setPen(Qt::NoPen);
     painter.setBrush(Qt::blue);

     painter.translate(0, height());
     painter.drawPie(QRect(-35, -35, 70, 70), 0, 90 * 16);
     painter.rotate(-currentAngle);
     painter.drawRect(QRect(30, -5, 20, 10));
 }

  我們像第九章一樣畫出來。

 QRect CannonField::cannonRect() const
 {
     QRect result(0, 0, 50, 50);
     result.moveBottomLeft(rect().bottomLeft());
     return result;
 }

  cannonRect() 函式返回在元件座標中封裝加農砲的矩形。首先我們建立一個 50 x 50 的矩形,然後移動它。所以它的左下角與元件的左下角是相同的。

  QWidget::rect() 函式返回元件自身座標的封裝矩形。矩形的左上角永遠是 (0, 0)。

t10/main.cpp

 MyWidget::MyWidget(QWidget *parent)
     : QWidget(parent)
 {

  建構子大致上相同,不過加入了一些新東西。

     LCDRange *force = new LCDRange;
     force->setRange(10, 50);

  我們加入了第二個 LCDRange,它將會被用來設定力量值。

     connect(force, SIGNAL(valueChanged(int)),
             cannonField, SLOT(setForce(int)));
     connect(cannonField, SIGNAL(forceChanged(int)),
             force, SLOT(setValue(int)));

  我們將連結 force 元件與 cannonField 元件,就像我們為 angle 元件所做的。

     QVBoxLayout *leftLayout = new QVBoxLayout;
     leftLayout->addWidget(angle);
     leftLayout->addWidget(force);

     QGridLayout *gridLayout = new QGridLayout;
     gridLayout->addWidget(quit, 0, 0);
     gridLayout->addLayout(leftLayout, 1, 0);
     gridLayout->addWidget(cannonField, 1, 1, 2, 1);
     gridLayout->setColumnStretch(1, 10);

  在第九章,我們把 angle 放在版面配置的左下那格。現在我們想要讓兩個元件放在同一格裡,所以我們做了一個直的盒子(vertical box),把直的盒子放在網格中,並把 angle 與 range 元件放在直的盒子裡。

     force->setValue(25);

  我們將力量值初始為 25。


Running the Application

  我們現在有力量控制了。


Exercises

  使加農砲的砲管大小依據力量值變化。

  把加農砲放在右下角。

  試著加入更好的鍵盤介面。例如,讓 + 和 - 來增加和減少力量值,並使用 enter 發射。假如你對左方向鍵和右方向鍵的運作方式感到苦惱,也把它改掉吧。[提示:重新實現 QWidget::keyPressEvent()。]


來源:Qt Tutorial 10 - Smooth as Silk
版本:4.4.3

【翻譯】Qt Tutorial 9 - With Cannon You Can

@
.tutorials/tutorial/t9/cannonfield.cpp
.tutorials/tutorial/t9/cannonfield.h
.tutorials/tutorial/t9/lcdrange.cpp
.tutorials/tutorial/t9/lcdrange.h
.tutorials/tutorial/t9/main.cpp
.tutorials/tutorial/t9/t9.pro

  在這個範例我們來畫一個可愛的藍色小加農砲。只有 cannonfield.cpp 不同於先前的章節。




Line by Line Walkthrough

t9/cannonfield.cpp

 void CannonField::paintEvent(QPaintEvent * /* event */)
 {
     QPainter painter(this);

  我們要開始認真地使用 QPainter 了。我們建立了一個操作這個元件的繪圖工具(painter)。

     painter.setPen(Qt::NoPen);

  QPainter 所繪的邊緣(edge)是使用畫筆(pen)畫出來的。這裡我們將之設成 Qt::NoPen,代表我們在畫出某些東西時,將不會有一個明確的邊。

     painter.setBrush(Qt::blue);

  當 QPainter 填滿一個矩形、圓形、或是其他任何東西,它會使用它的筆刷(brush)來填滿圖形。這裡我們將它設成使用純藍色筆刷。(我們也可以使用調色盤。)藍色筆刷將會填滿所有我們畫出來的東西的邊緣內部。

     painter.translate(0, rect().height());

  QPainter::translate() 函式轉化(translate)了 QPainter 的座標系統(換言之,就是藉由偏移量[offset]移動)。這裡我們將元件的左下角設為(0, 0),x 與 y 的方向仍然沒有改變。也就是說,現在所有在這個元件之內的 y 坐標都是負的。(關於 Qt 的座標系統的更多資訊請參見 The Coordinate System。)

     painter.drawPie(QRect(-35, -35, 70, 70), 0, 90 * 16);

  QPainter::drawPie() 函式使用一個起始角度以及弧長,在特定的矩形中畫一個圓餅的形狀。角度的單位是 16 分之一度。零度在三點鐘方向。畫的方向為逆時鐘。這裡我們在元件的左下角畫了一個四分之一的圓。這個圓餅被藍色填滿,而且沒有邊框。

     painter.rotate(-currentAngle);

  QPainter::rotate() 函式繞著 QPainter 的原點旋轉(rotate)座標系統。旋轉引數是一個由度數(不是上面所說的 16 分之一度)給定的浮點數(float),並且是順時鐘的。這裡我們順時鐘旋轉了 currentAngle 度。

     painter.drawRect(QRect(30, -5, 20, 10));

  QPainter::drawRect() 函式畫出一個特定的矩形。這裡我們畫出加農砲的砲管。

  當座標系統像上面這樣被轉換(轉化、旋轉、縮放[scale]、或是修剪[shear]),常常很難去想像畫出來的結果。

  在這個例子中,坐標系統先被轉化再被旋轉。若是矩形 QRect(30, -5, 20, 10) 被畫在這個轉換後的座標系統,它看起來會像這樣:



  注意矩形被 CannonField 元件的邊界修剪了一部份。當我們旋轉座標系統,舉 60 度為例,矩形將會繞著 (0, 0)--也就是左下角,因為我們轉化了座標系統--被旋轉。結果看起來像這樣:




Running the Application

  當滾動軸被操作時,畫出來的加農砲角度也會跟著改變。

  Quit 按紐的 'Q' 現在有條底線,而且 Alt+Q 會按下這個按鈕。


Exercises

  設定一支不同的畫筆代替 Qt::NoPen。設定一支調色盤的筆刷。

  試著將按鈕上的文字改成 "Q&uit" 或是 "Qu&it" 來代替 "&Quit"。發生了什麼事?


來源:Qt Tutorial 9 - With Cannon You Can
版本:4.4.3

【翻譯】Qt Tutorial 8 - Preparing for Battle

@
.tutorials/tutorial/t8/cannonfield.cpp
.tutorials/tutorial/t8/cannonfield.h
.tutorials/tutorial/t8/lcdrange.cpp
.tutorials/tutorial/t8/lcdrange.h
.tutorials/tutorial/t8/main.cpp
.tutorials/tutorial/t8/t8.pro

  在這個範例中,我們介紹了第一個可以描繪自身的元件。我們也會加入一個有用的鍵盤介面(使用兩行程式碼)。




Line by Line Walkthrough

t8/lcdrange.h

  這個檔案與第七章的 lcdrange.h 非常相似。我們加入了一個 slot:setRange()。

     void setRange(int minValue, int maxValue);

  我們現在增加了設定 LCDRange 範圍的可能性。到現在為止,它已經被固定在 0 到 99 之間。

t8/lcdrange.cpp

  這裡的建構子做了一點改變(我們稍後將會討論它)。

 void LCDRange::setRange(int minValue, int maxValue)
 {
     if (minValue < 0 || maxValue > 99 || minValue > maxValue) {
         qWarning("LCDRange::setRange(%d, %d)\n"
                  "\tRange must be 0..99\n"
                  "\tand minValue must not be greater than maxValue",
                  minValue, maxValue);
         return;
     }
     slider->setRange(minValue, maxValue);
 }

  setRange() slot 設定了 LCDRange 中滾動軸的範圍。因為我們已經把 QLCDNumber 設定為總是顯示兩個數字了,所以我們想限制 minVal 與 maxVal 的可能範圍,以避免 QLCDNumber 溢出。(我們可以允許最小值到 -9,不過我們選擇不這麼做。)假如引數是非法的,我們使用 Qt 的 qWarning() 函式對使用者發出一個警告並直接返回。qWarning() 是一個預設輸出到 stderr、類似於 printf 函式。只要你願意,你可以使用 qInstallMsgHandler() 設置你自己的處理函式。

t8/cannonfield.h

  CannonField 是一個知道如何顯示自身的新自訂元件。

 class CannonField : public QWidget
 {
     Q_OBJECT

 public:
     CannonField(QWidget *parent = 0);

  CannonField 繼承自 QWidget。我們使用了與 LCDRange 相同的寫法。

     int angle() const { return currentAngle; }

 public slots:
     void setAngle(int angle);

 signals:
     void angleChanged(int newAngle);

  現在,CannonField 只包含一個角度(angle)值,我們採用了與 LCDRange 值相同的寫法提供它的介面。

 protected:
     void paintEvent(QPaintEvent *event);

  這是我們第二次遇到 QWidget 眾多事件處理器(handler)中的一個。無論一個元件在何時需要更新(update),這個虛擬函式就會被 Qt 呼叫。

t8/cannonfield.cpp

 CannonField::CannonField(QWidget *parent)
     : QWidget(parent)
 {

  再一次地,我們使用了與前一章 LCDRange 相同的寫法。

     currentAngle = 45;
     setPalette(QPalette(QColor(250, 250, 200)));
     setAutoFillBackground(true);
 }

  這個建構子將角度值初始化為 45 度,並為這個元件設定了一個自訂調色盤(palette)。

  這個調色盤使用了象徵性的顏色作為背景,並挑選其他適當的顏色。(對這個範例而言,只有背景跟文字的顏色會被真正用到。)我們呼叫 setAutoFillBackground(true) 以告知 Qt 自動填滿背景。

  QColor 是指定一組 RGB (red-green-blue,紅-綠-藍)值,其中每一個值都介於 0 (暗)到 255 (亮)之間。我們也可以使用像是 Qt::yellow 這種預先定義好的顏色,來代替指定 RGB 值。

 void CannonField::setAngle(int angle)
 {
     if (angle < 5)
         angle = 5;
     if (angle > 70)
         angle = 70;
     if (currentAngle == angle)
         return;
     currentAngle = angle;
     update();
     emit angleChanged(currentAngle);
 }

  這個函式設定了角度值。我們選擇 5 到 70 為合法範圍,並以此調整給定的角度數值。若新的角度值超出範圍,我們選擇不發出警告。

  假如新的角度值與舊的角度值相等,我們立即返回。只有在角度真正發生改變時,才發出 angleChanged() signal 是很重要的。

  這時我們設定了新的角度值,並重繪我們的元件。QWidget::update() 函式會清空這個元件(通常以背景色填滿),並送出一個繪圖(paint)事件給元件。這樣的結果就是呼叫了這個元件的繪圖事件函式。

  最後,我們發出 angleChanged() signal 告知外界角度值被改變了。這個 emit 關鍵字是 Qt 特有的,不是標準的 C++ 語法。事實上,它是一個巨集。

 void CannonField::paintEvent(QPaintEvent * /* event */)
 {
     QPainter painter(this);
     painter.drawText(200, 200,
                      tr("Angle = ") + QString::number(currentAngle));
 }

  這是我們第一次嘗試寫繪圖事件處理器。事件引數包含了有關繪圖事件的資訊,例如元件必須被更新的區域資訊。現在,讓我們懶惰點,直接畫出所有東西。

  我們的程式在元件的一個固定位置顯示角度值。為了實現它,我們建立了一個在 CannonField 元件中運作的 QPainter,並使用它繪出一個表示 currentAngle 值的字串。我們稍後會回到 QPainter,它可以做非常多的事。

t8/main.cpp

 #include "cannonfield.h"

  我們引入了我們新類別的定義。

 class MyWidget : public QWidget
 {
 public:
     MyWidget(QWidget *parent = 0);
 };

  MyWidget 類別將單獨引入一個 LCDRange 與一個 CannonField。

     LCDRange *angle = new LCDRange;

  在建構子裡,我們建立並設定了 LCDRange 元件。

     angle->setRange(5, 70);

  我們使 LCDRange 接受 5 到 70 度的角度。

     CannonField *cannonField = new CannonField;

  我們建立了我們的 CannonField 元件。

     connect(angle, SIGNAL(valueChanged(int)),
             cannonField, SLOT(setAngle(int)));
     connect(cannonField, SIGNAL(angleChanged(int)),
             angle, SLOT(setValue(int)));

  這裡我們將 LCDRange 的 valueChanged() signal 連接到 CannonField 的 setAngle() slot。這會使得無論使用者何時操作 LCDRange,都會更新 CannonField 的角度值。我們也做了反向(reverse)連結,因此更改 CannonField 的角度也會更新 LCDRange 的值。在我們的範例中,我們不曾直接改變 CannonField 的角度值。不過藉由最後的 connect(),我們可以肯定:沒有任何改變會打斷這兩個值的同步關係。

  這說明了組件程式設計與適當封裝的威力。

  注意到只有在角度真正發生改變時,才發出 angleChanged() signal 有多麼重要。假如 LCDRange 與 CannonField 都忽略了這個檢查,在其中一個值首次被改變之後,程式將會進入到一個無窮迴圈(infinite loop)裡。

     QGridLayout *gridLayout = new QGridLayout;

  到目前為止,我們都是使用 QVBoxLayout 來做幾何管理。然而,現在我們想要對版面配置做多一點控制,所以我們使用了更加強大的 QGridLayout 類別。QGridLayout 不是一個元件,它是一個可以管理任何子元件的不同類別。

  我們不需要在 QGridLayout 的建構子裡指定任何尺寸。QGridLayout 將會根據我們擴充的網格決定列數及行數。



  上圖展示了我們試著去實現的版面配置。左圖展示了一個配置概要的畫面;而右圖則是程式的實際擷圖。

     gridLayout->addWidget(quit, 0, 0);

  我們加入 Quit 按紐在網格的左上角。換言之,就是座標 (0, 0)。

     gridLayout->addWidget(angle, 1, 0);

  我們將代表角度的 LCDRange 放置在 (1, 0)。

     gridLayout->addWidget(cannonField, 1, 1, 2, 1);

  我們使 CannonField 物件佔據 (1, 1) 與 (2, 1)。

     gridLayout->setColumnStretch(1, 10);

  我們告知 QGridLayout 右邊那行(行 1)的伸展係數(stretch factor)為 10,是可伸展的(stretchable)。因為左邊那行不可伸展(根據預設值,它的伸展係數為 0),QGridLayout 將會試著使左邊的元件大小不變,並在 MyWidget 改變大小時,只改變 CannonField 的大小。

  在這個特殊的例子中,行 1 使用任何大於 0 的伸展係數都將有相同的效果。在更加複雜的版面配置中,你可以藉由分配適當的伸展係數,告知一個特定的行或列,是另一行或另一列伸展的兩倍快。

     angle->setValue(60);

  我們設定了一個初始角度值。注意,這將會觸發由 LCDRange 到 CannonField 的連結。

     angle->setFocus();

  我們的最後一個動作是使 angle 獲得鍵盤焦點(focus),使鍵盤輸入在預設情況下將會傳遞到 LCDRange 元件中。

  LCDRange 並沒有包含任何 keyPressEvent(),所以這看起來並不是非常有用。然而,只需要在它的建構子中加入新的一行:

     setFocusProxy(slider);

  LCDRange 將 slider 設為它的焦點代理者(proxy)。這表示當某個人(程式或是使用者)想要讓 LCDRange 取得鍵盤焦點,slider 就會處理它。而 QSlider 有一個還不錯的鍵盤介面,所以加入了我們給了 LCDRange 的這一行。


Running the Application

  鍵盤現在會做某些事:方向鍵、Home、End、PageUp、和 PageDown 都會做一些合理的事。

  當滾動軸被操作時,CannonField 會顯示新的角度值。若是改變大小,CannonField 會得到盡可能多的空間。


Exercises

  試著改變視窗大小。若是你使它變得非常窄或非常矮,發生了什麼事?

  假如你給了左邊那行一個非 0 的伸展係數。在你改變視窗大小的時候,發生了什麼事?

  不呼叫 QWidget::setFocus()。你更喜歡做什麼行為?

  試著把 "Quit" 改成 "&Quit"。這個按鈕看起來有什麼改變?(是否改變視平台而定。)假如你在程式運作時按下 Alt+Q,發生了什麼事?

  把 CannonField 的文字置中。


來源:Qt Tutorial 8 - Preparing for Battle
版本:4.4.3