Indepth

使用 Qt 來開發 Embedded Linux

Qt/Embedded 為嵌入式應用程式的開發中帶來便利性與開發速度

by
Natalie Watson

繁體中文翻譯:黃敬群 <jimchyun@ccns.ncku.edu.tw>

[譯注:可能因為編排因素,原文的程式碼列表處有相當程度的錯誤,我在進行翻譯時已經參考 Qtopia 內的程式碼作對應的修正,並且將原本內文與程式碼列表共三個 HTML 檔案整合成這份翻譯。]

在 2001 年一月,我進入 Trolltech 在澳洲 Brisbane 的嵌入式研發部門實習。當時我主要有兩個目標:學習 C++ 與學習如何開發嵌入式應用程式,而在 Trolltech 五週的工作,讓我達成這目標 (當然,要精通 C++ 真的須要相當多的時間)。值得注意的是,還未在 Trolltech 工作前,我之前的程式設計經驗只有在大學中學習一年的 Java,而在此之前我對 C++ 或 Qt 沒有涉獵。在這五週結束後,我已經發展兩個完整的 2-D 遊戲 [譯注:分別是傘兵遊戲 Parashoot 與貪食蛇遊戲 snake],這兩者現在都已經整合到 Qt Palmtop Environment (QPE [譯注:QPE 是舊稱,現在 Trolltech 官方說法一律改稱作 Qtopia]) 中

稍微提一下,Qt/Embedded 是 Trolltech 推出的嵌入式版的 Qt,一個跨越 Windows、UNIX、Mac,及嵌入式 Linux 等平台的 C++ GUI application framework。Qt 與 Qt/Embedded 對開發者來說,都能以適宜的雙重授權方式使用。對開發自由軟體的程式設計者來說,Qt 可以免費取得與使用 (Qt 是以 GPL 與自家的 QPL 授權釋出,而 Qt/Embedded 僅依據 GPL 授權),但若用以開發商業軟體來說,Qt framework 就依商業授權方式使用。不管是商業授權抑或開放原始碼版本的 Qt,都可直接與 Trolltech 聯繫:http://www.trolltech.com/.

開始第一步

我相信任何具有基本程式設計技能者,都可很快對 Qt 上手,並且快速開發出好用而看起來頗專業的應用程式出來。I found the API quite intuitive, which greatly shortened the learning time. 儘管 C++ 在語言層面的複雜度不在話下,但是 Qt 採用其中適當的子集合,所以不必是 C++ 高手也能很快的使用 Qt。

本文說明了採用 Qt 發展簡單小遊戲的技巧。在此之前,你應該要熟悉以下四項:Qt tutorial、基本程式設計技能、物件導向成設計,以及 Qt reference documentation。

在 Qt tutorial 前面幾個章節將會引領你開發一個小型的應用程式,如著名的 "hello world'' 程式。這涵蓋幾項技巧,諸如原始檔與標頭檔的建立,以及如何編譯與執行這樣一個 Qt 程式。

Qt 是個 C++ 撰寫的 Application Framework,但我同時學會 framework 主體與基本的程式設計。如果你對 C++ 不熟稔,我建議你先閱讀一本好的教科書,特別是把指標 (pointer) 的部分弄懂。

以我的經驗來說,之前在大學時我已經研讀過 Java 了,而讓我發現物件導向程式設計的概念,對於學習 Qt 來說是很重要的基礎。Qt 是設計來讓物件導向程式設計更為簡單,所以學好 classes、objects,以及元件 (component) 等程式設計觀念相當受用。

為了能夠使用 Qt 來開發專案,你必須確認你已經安裝必要的軟體 [譯注:以 RPMs 為基礎的 Linux 系統來說,就必須檢查系統是否有 libqt2-devel、一份 Qt/Embedded 的套件,還有 Qtopia 套件] 以及對 Qt reference documentation 能夠上手。當然,一個 C++ 編譯器是不可少的,而一個除錯器 (debugger) 也用得上,幾乎所有的 Linux 套件都包含以上這兩項進去了。像是 viEmacs 這樣的文書編輯程式對撰寫原始程式碼來說,也是必要的。最重要的是,你一定預先安裝好 X11 版與 embedded 版的 Qt。

用以建構 Qt/Embedded 應用程式的工具,在絕大多數的 Linux 套件都已經內附了,包含 GCC 和 GDB 等,而我也會使用由 Trolltech 發展、用來建構與維護 Makefile 的 TMake 工具 [譯注:在 Qt 3.x 系列則建議使用 Qmake,這是 Tmake 的加強版本]。最好也能準備好文件,如 Qt documentation (可在 http://doc.trolltech.com/ 取得) 與一本 Qt 教科書:由 Kalle Dalheimer 所寫的《Programming with Qt[譯注:據 O'Reilly 的說法,這本暢銷書會在 2002 年三月推出第二版]。在 Qt documentation 解釋著 Qt API 中相當多 classes 的功能,並說明這些設計如何讓一般的程式設計工作更為簡單而快速克服。

針對開發 2-D 遊戲來說,你最早應該注意三個項目:

  • QMainWindow:提供典型的應用程式視窗,並伴隨有完整的 toolbar、menu,與 status bars
  • QCanvas:一塊可以擺放 QCanvasItems (圖形物件) 的平面繪圖區域
  • QCanvasView:提供 a view of the canvas [譯注:為了不影響對照 Qt Document 的措辭正確性,這邊的術語皆不翻譯]

閱讀 Qt documentation 是個很好的出發點,因為這將會讓你知曉 Qt 提供了哪些豐富的功能,並且,你可以得知哪些功能已經內建了。

在本文接下來的地方,我將會用我撰寫過的貪食蛇遊戲 (請見下方圖,以下簡稱 Snake) 作為範例,說明使用 Qt 是如何輕鬆的開發這遊戲出來。當然,這個技巧也可以適用在其他 2-D 遊戲的開發上。

figure

MainWindow

首先你必須要知道主要的程式框架 main class,應該在 main.cpp 的檔案撰寫中 [譯注:這不是必要的,但是對於採用 tmake 卻是個好習慣],這裡頭將只擺放一個用以建立程式 instance [譯注:在某些中文物件導向的資料上,instance 翻譯作「案例」] 的main function,以 Snake 來說,就像是這樣:

int main( int argc, char **argv )
{
    QPEApplication app( argc,argv );
    /*↑譯注:請留意到 QPEApplication,這是開發 Qtopia 應用程式與一般
         Qt 應用程式的不同處 */
    SnakeGame* m = new SnakeGame;
    // 建立一份 Snake 的 instance
    m->resize( m->sizeHint() );
    qApp->setMainWidget( m );
    m->show();
    return app.exec();
}

如果你已經寫過 Qt 程式的話,你會發現上面這個程式片段相當熟悉,這是標準開始的程序。

Canvas 與 CanvasView

你需要一個用以處理啟始動作的 class。這個 class 的 constructor 將會建立一份用在剛剛上面所列程式碼所要的 instance,並且,要繼承 (inherit) 自QMainWindow,以建立 menus 或 toolbars,也可能是個 status bar。此外,也需要建立個 QCanvas 與 QCanvasView。以上的動作請見 [程式列表一]。

程式列表一:建立 QCanvas 與 QCanvasView

這個 class 也會包含你程式中所需要的 method。在 Snake 中,這個 class 就包含用來顯示一個小螢幕、開啟新遊戲、更新得分、改變遊戲等級、清除螢幕、關閉遊戲,以及處理鍵盤事件 (key events) 等動作的 method。當然,這與你的程式流程有關 [譯注:即非通用性的],在此我不附上程式碼。

角色 (Sprites)

現在,你已經有個 QCanvas,於是你可以開始在上面放置 QCanvasItems 物件。有很多型態的 QCanvasItems,而對圖形物件來說,我們會使用QCanvasSprite,詳情請見 Qt documentation 中描述 QCanvasItems 各種型態的資訊。在 Snake 遊戲中,作為貪食蛇補食的老鼠 [譯注:原文以及程式碼都是說 "the target mouse",與一般我們的習慣上的「蛋」不太一致,可能就是文化上的差異吧] 是一個附帶圖形的 QCanvasSprite 物件。這個老鼠是宣告在自己的 class 中,並且也繼承自 QCanvasSprite。以下的程式碼就是關於處理老鼠的 constructor 與其所在的 class,Target:

Target::Target( QCanvas* canvas )
    : QCanvasSprite( 0, canvas )
    //inherits from QCanvasSprite
{
    mouse = new QCanvasPixmapArray();
    setSequence( mouse );
    newTarget();
}

在以上程式碼片段中,一開始作的事情就是載入圖形到一個稱為 "mouse" 的 pixmap array 中。如果你有若干圖形,並且想要讓你的角色能處理動畫,你可以透過 readPixmaps() 方法來把這些圖形都載入到 array 中。而,透過在 QCanvasSprite 中的 setSequence() 方法,可以告知你的 sprite 去使用剛剛載入圖形的 array。constructor 也會呼叫 newTarget() 的函式,這是用來產生新的目標所在 x 與 y 座標的亂數值。接著,這個函式會去呼叫 QCanvasSprite 中的 move (int x, int y) 方法,以便設定位置,而 show() 方法可讓這角色顯示出來。牆壁的處理也是用相同的方式。

鍵盤事件 (Keyboard Events)

現在,你可以讓你的程式與玩者互動起來。Qt 中的 keyEvent class 匯集關於鍵盤事件的資訊。事件處理器 (event handler) keyPressEventkeyReleaseEvent 會接收以上資訊。為了能夠使用這些,你必須實作 (implement,[譯注:更好的說法是去 override 原本的功能])。一個典型的keyPressEvent 實作是透過 switch 敘述來針對使用者按擊不同按鍵去呼叫對應的功能。

在這個遊戲中,蛇會在方向鍵被按擊時改變原先行進的方向。這是在 switch 敘述中用方向來當參數的方式呼叫 move() 函式。以下的程式片段就是 keyPressEvent() 函式的實作。看起來相當簡單,就只去呼叫 Snake class 中的函式以記憶之前被按下的按鍵,並且呼叫另一個函式 moveSnake(),以判別蛇應該移動的方向。這些是在 interface.cpp 檔中定義的。

void SnakeGame::keyPressEvent(QKeyEvent* event)
{
       int newkey = event->key();
       snake->go( newkey )
}

而在 snake.cpp 中:

void Snake::moveSnake()
{
    switch ( last ) {
        case Key_Up: move( up ); break;
        case Key_Left: move( left );  break;
        case Key_Right: move( right ); break;
        case Key_Down: move( down );  break;
    }
    detectCrash();
}

如你所見,如果玩者按下上鍵,程式會去呼叫 move(up)。處理移動的函式執行時,蛇會移往按鍵的方向,以剛剛的狀況來說,就是往上移。對不同類型的遊戲來說,move() 函式也會跟著有不同的面貌。

實際上,處理蛇的移動是頗為複雜的,因為舌的身體是一連串小圖形所構成。為了達到移動的效果,末端的圖形必須安置到前端,因此頭尾的小圖形就必須移動了。在其他的情況來說,如按下一個鍵來發射子彈的處理,Qt 中的 setVelocity() 函式就可用來設定子彈移動所必須保持的常數速率。

碰撞處理 (Collisions)

在這個階段,你已經完成具有 a QMainWindow containing a QCanvasView with a canvas that contains items, some of which may be moving [譯注:保留原文,這段敘述應該會比中文的說法流暢多了]。下一步就是藉著碰撞偵測 (detecting collisions) 的技巧來讓你的程式更加有趣。你可以藉由使用collidesWith()collisions() 得知 QCanvasItems 物件間碰撞的資訊。 collidesWith() 是用來測試一個單元是否會碰撞到另一個單元,而collisions() 會回傳一份關於所有單元與一個特定單元是否碰撞的列表。 在Snake 遊戲中,collisions() 會派上用場,因為蛇有可能碰撞到自己本身、周圍的牆壁,或是目標。[程式列表二] 將會顯示遊戲中是如何處理碰撞的。

程式列表二:處理碰撞

關於蛇頭部與 QCanvasItems 碰撞的列表會被指定到命名為 "l" [譯注;是小寫英文字母 L] 的QCanvasItemList 中。collisions() 中的 false 參數指定在非精準模式下測試碰撞,這會回傳頭部附近的單位,並且比在精準模式下運作更快些。If the item is a useful candidate, it is tested with collidesWith().

在 "for" 迴圈所做的就是尋訪 list l、指定每個項目 (item) 為 "item" 變數。每個在 canvs 上的 QCanvasItem 物件可有一個特別的 rtti() 值,這個 rtti() 值是指一個讓你辨識若干項目的整數值 [譯注:所以,由此可見這與 C++ 進階議題 RTTI : RunTime Type Identification 沒有任何關連]。目標的 rtti() 值是 1,500,並且這項資訊是用在在 list l 中的項目是否涵蓋蛇所要吃到的目標。如果有包含目標,函式會傳回,不然就會移往下一個敘述,以測試項目是否就是牆壁。值得注意的是,上面的程式碼片段祇是僅供說明,實際上,Snake 遊戲則在 for 迴圈有另外的 if 判斷句,以便處理其他狀況,而原本的 "do something'' 也會替換掉。

Signals 與 Slots 機制

另外一項在開發程式時重要的考量就是其設計了。Qt 提供跟元件程式設計 (或是說物件導向程式設計) 關連性的機制:signals 與 slots 機制。這是個相當重要的特徵,可以在與其他不同類型的物件溝通間,節省相當多時間與成本。這允許物件能夠 emit [譯注:這是 Qt 的保留字] 一個 signal(),以觸發在另一個物件中的一個 slot()。Slots 可以是一般的 member functions,而參數也可以被傳遞進去。

我發現 Signals 與 Slots 機制在發展 Snake 相當好用。一個這樣的例子就是處理遊戲終結的部分,當 snake 物件發現與牆壁碰撞時,它會 emits 一個dead() 的 signal,接著,蛇的身體或是螢幕的邊界會跟著連接 (connect) 到 interface class 中的 gameOver() 的 slot,這樣就會結束這場遊戲並另開新的項目。以下就是這部分的程式碼:

//check if snake hit itself
for ( uint i = 3; i < snakelist.count(); i++ ) {
    if ( head->collidesWith( snakelist.at( i ) ) ) {
        emit dead();      // signal to end game
        autoMoveTimer->stop();
        return;
    }
}

而在 interface.cpp 的 the newGame() 函式就像這樣:

// connect the signal to the slot
connect( snake, SIGNAL( dead() ), this, SLOT( gameOver() ) );

結論

如果說要對一個 Application Framework 作測試的項目是能否快速建構專業、具有威力,以及沒有 Bug 的程式碼,Qt/Embedded 顯然可以通過這個測試。雖然只具備少數的程式設計經驗,我還是能夠建構出一個 2-D 電腦遊戲,這隻遊戲還優秀到可以納入 Qt Palmtop Environment 套件中,並且,我實際的開發時間少於五週。Qt/Embedded 的目標是 "Small Enough for the Job",我認為這是很好的感覺,Qt 相當具有威力,並且有豐富的功能,這些讓開發程式變得更為輕鬆,而整個程式佔 Flash 的記憶體另也相當低。如果還有日後我的工作能接觸到這樣的工具,甚至只有這樣一半好,我將會樂意當個程式開發者。

Listing 1. Creating a QCanvas and a QCanvasView

[譯注:詳細 (或是說最終版) 的程式可以參考 qpe/snake/interface.cpp

//This is the constructor for Snake.
SnakeGame::SnakeGame( QWidget* parent, const char* name, 
                      WFlags f) )
: QMainWindow( parent, name, f ), canvas( 232, 258 ) 
{
    // create a canvas
    view cv = new QCanvasView( &canvas, this );
    // create a toolbar and put a new game button on it
    QToolBar* toolbar = new QToolBar( this );
    QToolButton* newgame = new QToolButton( 
        newicon, SLOT( newGame() ), toolbar, "New Game" );
    // set the canvasview to appear in the center of the window
    setCentralWidget( cv );
}
          

Listing 2. Handling Collisions

[譯注:詳細 (或是說最終版) 的程式可以參考 qpe/snake/snake.cpp

QCanvasItem* item;
QCanvasItemList l = head->collisions( FALSE );
for (QCanvasItemList::Iterator it=l.begin(); item = *it;
   // check if snake hit target
   if ( ( item->rtti() == 1500 ) &&
        ( item->collidesWith( head ) ) ) {
          // do something
          return;
   }
   // check if snake hit wall
   if ( ( item->rtti() == 1600 ) &&
        ( item->collidesWith( head ) ) ) {
          // do something
          return;
   }
}

Natalie Watson (nwatson04@optusnet.com.au) 目前是 Griffith University 的學生,正為取得資訊科技 (Information Technology) 的學位努力中。她的程式設計經驗涵蓋 Java 與 C++。

E-mail 給作者