.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
0 回覆:
張貼留言