2月 04, 2009

【翻譯】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。shootAngleshootForce 變數會被設成當前的加農砲角度以及力量。最後,我們啟動計時器。

 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 連結到 CannonFieldshoot() slot。


Running the Application

  加農砲能射擊了,不過沒有任何東西會被射中。


Exercises

  將砲彈變成一個被填滿的圓。[提示:QPainter::drawEllipse() 可能會有幫助。]

  當砲彈在空中時,改變加農砲的顏色。


來源:Qt Tutorial 11 - Giving It a Shot
版本:4.4.3

2 回覆:

匿名 提到...

最近一直在看板主翻譯的這一系列 Qt 教學
已經看完 10 章了
學起來很爽呢

多謝版大!

Saitor & Loki 提到...

barrelRect 是什麼?

張貼留言