2月 04, 2009

【翻譯】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 設定為總是顯示兩個數字了,所以我們想限制 minValmaxVal 的可能範圍,以避免 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)));

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

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

  注意到只有在角度真正發生改變時,才發出 angleChanged() signal 有多麼重要。假如 LCDRangeCannonField 都忽略了這個檢查,在其中一個值首次被改變之後,程式將會進入到一個無窮迴圈(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);

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

     angle->setFocus();

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

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

     setFocusProxy(slider);

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


Running the Application

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

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


Exercises

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

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

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

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

  把 CannonField 的文字置中。


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

0 回覆:

張貼留言