2月 04, 2009

【翻譯】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);
 }

  lcdslider 的結構與先前的章節相同。接下來,我們建立一個 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。接下來,我們取得從午夜直到現在經過的秒數,並使用它作為一個亂數子。請參見文件 QDateQTime、與 QDateTime 以取得更多資訊。

  最後我們計算出攻擊目標的中心點。我們保持它在我們讓元件底邊的 y 位置為 0,並使 y 向上遞增、x 為一般的以左邊界為 0,x 向右遞增的座標系統中的一個矩形裡(x = 200、y = 35、寬度 = 190、高度 = 255。換句話說,可能的 xy 值分別介於 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

0 回覆:

張貼留言