準備工作 |
什麼是 GTK?
GTK (GIMP ToolKit) 原本只是 GIMP 開發過程上管理圖型介面的一套工具程式庫. 由於它使用 LGPL 執照, 程式開發者可以免費使用它來發展公開程式碼的軟體, 免費軟體或甚至商用軟體. 隨著使用率及使用範圍的增加, 很快的 GTK 從只為了滿足 GIMP 需求而存在的印象中跳出, 發展成今日功能廣泛的一套程式庫.
GTK 的穩定版已從 1.2 發行到現在的 2.0. 舊的 1.2 版基本上只有 GLIB 跟 GTK+ 兩個套件, 而 GTK 中另含有 GDK (GIMP Drawing Kit) 程式庫. 一般我們直接使用的是 GTK. 其中幾乎所有繪圖功能都是透過 GDK 來達成的. GDK 主要負責和 X Window 的程式庫做低階的溝通. 它也提供較為簡化的程式介面給 GTK 使用. glib 是最低階的程式庫. 它主要的功能是和系統上的 C library 做接觸和給予程式設計者一個一致的環境, 不需為了各個 UNIX 系統上的些許不同而顧慮. 2.0 除了修改 1.2 之外, 增加了 ATK (Accessibility Tool Kit) 和 Pango (pan 希臘 "全部", go 日文 "語"). 透過 ATK 使得開發幫助殘障人士的工具軟體不論在可行性及難易度上都有相當的改善. Pango 的多國文字處理能力在邁向世界化的現在更是一項不可或缺的功能. 此外專門處理圖型檔的 GDK-pixbuf 也合併到了 2.0 版的 GTK+ 套件中.
GTK 有一項特點是它完全使用 C 語言, 但無論在設計上或是應用上都故貫持著物件導向的特徵. 物件之間不但有衍生繼承的特性, 更有回呼函式 (callback function) 達成事件驅動的構造.
GTK 的世界十分廣闊. 諸如 GNet 等使用 GLIB 建立的網路公用程式庫, 雖然不是 GTK+ 小組製作但也有越來越多人使用. 有興趣的網友們可以瀏覽 GTK 及 GNOME 的官方網站.
GTK | http://www.gtk.org/ |
GNOME | http://www.gnome.org/ |
寫 GTK 程式需要哪些東西?
一般 linux 安裝時若有連 "程式發展環境" 的套件也一併安裝, 開發工具 (gcc, as, ld, make, autoconf, automake... 等等), 程式庫 (libX11, libglib, libgdk... ) 及檔頭 (/usr/include/ 中的 xxx.h) 多半都已經存在. 若 compile 下面 hello.c 的範例程式失敗, 則可能少裝了某些發展工具套件.
在安裝套件時, 可以選擇較高階的程式庫發展套件如 libgtk2.0-dev 或 libgtk+-devel-2.0. 為了配合相依性, 在過程中較低階的幾個發展套件如 libglib2.0-dev 或 libglib-devel-2.0 會一起安裝.
關於這方面的問題請於 www.linux.org.tw 的討論區中求助.
基本程式設計 |
踏出第一步 hello.c
我們先來看看最簡單的一個 GTK 程式, 只顯示一個空視窗, 比 hello.c 更簡單:
empty.c
1 #include |
第 1 行只 include 了唯一的一個檔頭. gtk.h 會自動 include 其他的檔頭.
第 6 行在程式一開始呼叫 gtk_init(). 它會負責連接到 Xserver 供程式顯示及輸入.
第 7 行用 gtk_window_new() 建立新的 GTK Window. GTK 中物件幾乎是以 gtk_objectname_new() 來建立的.
第 8 行把剛建立的 GTK Window 從預設的隱藏改成顯示.
第 9 行進入 GTK 的事件處理迴圈 (event loop). GTK 程式大部份時間都是在事件處理中.
第 10 行結束程式. 通常只有 GTK 收到了結束的事件後才會從 gtk_main() 離開進到這行.
在 shell 底下用 gcc compile 這個程式:
linux$ gcc -o empty empty.c `pkg-config --cflags --libs gtk+-2.0` linux$ ./empty |
執行後可以看到一個標題為 empty 的空白小視窗.
empty 的執行畫面 |
因為我們剛做出 GTK Window 就把他顯示出來, 自然視窗裡面沒有內容. 此外關閉視窗後程式也不會結束, 因此 shell 的回應也不會出現. 請在 shell 中以 [Ctrl]+[C] 強制中斷.
現在我們來看看正式一點的範例, hello.c. 它跟 empty.c 只有三四行的差別而已.
hello.c
1 #include |
第 9 行把 GTK Window 的結束事件用 gtk_main_quit() 處理. 關閉視窗後 gtk_main_quit() 會被呼叫, 導致第 13 行的 gtk_main() 結束而能夠終止程式. empty.c 中因為沒有設定什麼情況下要 gtk_main_quit(), 所以會永遠在 gtk_main() 裡處理事件.
第 11 行用 gtk_label_new() 做出一個 GTK Label 放 "Hello world!" 字串.
第 12 行將做出的 label 放到 window 裡面.
第 13 行用 gtk_widget_show_all() 將 window 以及裡面的 label 也一併顯示出來.
同樣的方式在 shell 底下 compile:
linux$ gcc -o hello hello.c `pkg-config --cflags --libs gtk+-2.0` linux$ ./hello |
hello 的執行畫面 |
有沒有注意到 hello 的視窗有放東西卻比起空白的 empty 小了很多? GTK 的 container 裡面因為只放一樣物件, 所以一旦有了物件就可以馬上算出實際所需要的大小. 因此 hello 的視窗被縮小為恰好足夠容納 "Hello world!" 字串的 label 的空間. 相較之下空白的 empty 只好用預設的大小顯示 (約 200x200). 除此之外按按關閉視窗的按紐, hello 馬上結束執行, shell 的提示立即出現.
從 hello.c 短短的幾行中我們不難發現到 GTK 程式設計上的一些特性:
- 在 main() 的一開始需要呼叫 gtk_init() 啟動.
- 各 widget (GTK 中像 window, label, button 等可以顯示的物件叫 widget) 均以名字加上 new 的 function 建立, 如 gtk_window_new(), gtk_label_new().
- 跟某 widget 有直接關聯的 function 名字開頭都會是那 widget 的名稱, 如 gtk_window_new().
- GTK 各個 widget 建立後都是以 GtkWidget * 型態傳回, 唯有到使用時才需要做型別轉換. (eg. GTK_CONTAINER(window) 將 window 從 GtkWidget * 轉成 GtkContainer *)
- 許多 widget 本身是個 container 可以用 gtk_container_add() 置放另一個 widget.
- widget 剛建立時內定都是不顯示, 要用 gtk_widget_show() 或 gtk_widget_show_all().
- 當 widget 都設定好後需要呼叫 gtk_main() 來啟動 GTK 的事件處理迴圈.
事件處理
所謂的事件 (event), 在這裡用來描述執行過程中所遇到的狀況. 鍵盤輸入, 滑鼠的移動, 計時通知等等, 皆是硬體的事件. 大家很容易就能想像事件發生的原因跟過程. 來自軟體本身產生的事件則不這麼明確. 例如一個 button 被按下的事件, 視窗大小被調整的事件, 畫面需要更新的重繪事件, 檔案內容被更新的事件... 許多都是依系統的設計而被定義出來的.
在 GTK 中在每個 widget 上都可能會發生好幾種事件. 事實上, 在程式設計時多半都是在寫如何處理各種事件. 這些事件處理的 function (event handler) 以 g_signal_connect() 註冊到指定的 widget 中. hello.c 中的第九,第十行便是一個使用 g_signal_connect() 將 gtk_main_quit() 註冊到 window. window 收到 delete_event (關閉視窗) 後便呼叫 gtk_main_quit().
細心的讀者可能已經注意到了: g_signal_connect() 是 g_signal 開頭, 並不是 gtk_widget 或 gtk_xxxx 開頭. 其實 GTK 事件處理的整個功能架構原本是建立在 GTK 的 Signal 訊號結構上, 自 2.0 版開始被從 GTK 中拿掉, 轉放到 GLIB 裡面.
讓指定的 object 在發生 name 事件時呼叫 func (可指定自己的額外參數 func_data):guint g_signal_connect (GObject *object,
|
Callback function 的參數和傳回值格式最正確的格式可以在 GTK 的參考手冊中各個 widget 的 Signal Prototypes 列表裡查到. 例如 GtkButton 下的幾個:
"clicked" void user_function(GtkButton *button, |
要在 button 被按下時執行某個 function 我們可以 connect 那個 function 到 button 的 "clicked" 事件. 用:
void on_clicked() |
其他也有較為複雜的參數, 像是 GtkWidget 中的畫面重繪 expose_event:
"expose-event" |
當我們的 callback function 被呼叫時, 這些參數也會被提供. 有時候我們並不需要這些額外的資料就能在 callback function 中完成工作, 便可以省略後面幾個參數. 例如重繪一個視窗時, GdkEventExpose 中提供了需要更新的方塊, 讓程式設計時可以只更新需要更新的部份不用全部重繪. 若只是簡單的程式想直接重繪整個視窗區, 則不需要理會這個方塊. 可能只會使用第一個參數接收 GtkWidget 再拿它來重新畫過內容.
gboolean on_exposed(GtkWidget *widget) |
在自己定義的 callback function 中傳回 TRUE 表示這個事件已經被處理完畢. 傳回 FALSE 則 GTK 會繼續找是否還有其他合適的 event handler.
Button widget
在看完了事件處理之後我們馬上先做可以讓 user click 的 button 一個小程式試試.
button1.c
1 #include |
compile 之後執行並按下幾次 Click Me button, on_clicked() 會被呼叫然後使用 g_print() 印出文字. 我們可以透過 func_data 來傳遞資料給 callback function. 為了方便讀者理解, source 中已經把傳遞及接收的 func_data 標為紅色. 在執行過程中 on_clicked 被呼叫時裡面的 data 會依 g_signal_connect() 的內容被設定. 在這個範例中, 唯一的一個 button 被按下時, on_clicked() 中的 widget 會被設為那個 button, data 則是在 g_signal_connect() 中最後一個參數: "[Click Me]" 字串.
linux$ gcc -o button1 button1.c `pkg-config --cflags --libs gtk+-2.0` linux$ ./button1 User has clicked button [Click Me]. User has clicked button [Click Me]. |
button1 |
這個 func_data 是很方便的, 尤其在許多 widget 要共用同一個 callback function 時, 透過它可以很輕鬆的知道是哪個 widget 產生事件, 而採取相對的行動. 當然 GtkWidget * 是一個 pointer, 也可以將所有可能產生事件的 widget 位址全都記下來, 再一個一個比對尋找. 比起這種方法, 善用 func_data 實在是方便又有效率得多了. 最好的例子就是後期會介紹的踩地雷.
使用 box 編排位置
到目前為止我們所看到的程式都是使用 gtk_container_add() 將一個 widget 放入另一個 container 之中. 但是 container 只能夠放入一個 widget. 若要放入第二個則會在執行時出現錯誤訊息. 當要放入數個 widget 並安排畫面時, box 就派上用場了.
GTK 中用 box 來排放 widget 可說是最常見也最容易寫的. 一個 box 的功能主要是容納以及計算裡面 widget 的大小, 最後決定自己要佔用的空間大小. Box 又分為 HBox 跟 VBox. HBox 把裡面的 widget 以左右橫排, vbox 則上下直排. 建議有興趣的讀者使用 Glade 嘗試兩種 box 的使用.
hbox | vbox |
在圖中 box 裡的每個格子都可以加入一個 widget. 此外 box 本身也是一個 widget, 所以可以善用各種組合來達到想要的效果. Hbox 跟 vbox 的建立跟基本的 widget 一樣, 各為 gtk_hbox_new() 和 gtk_vbox_new(), 不過需要提供兩個參數: homogeneous (每個格子等寬/等高) 及 spacing (格子之間的距離, 單位為 pixel).
GtkWidget *box1; |
將 widget 放入 box 裡可以使用 gtk_box_pack_start() 或是 gtk_box_pack_end() 兩個 function. gtk_box_pack_start() 會將 widget 依從左到右 (hbox) 或從上到下 (vbox) 的順序找位置存放, gtk_box_pack_end() 則是倒著放過來. 底下是 GTK 文件說明的部份.
使用 box 安排 widget 的配置void gtk_box_pack_start (GtkBox *box,
|
底下是 Glade 中使用 hbox 的幾個圖例. 為了方便說明, 圖中網狀的部份表示屬於 button1 格子的空白空間. 在實際的程式中並不會出現網狀, 而是以類似左圖的樣子兩側有著空格.
expand fill | FALSE FALSE | TRUE FALSE | TRUE TRUE |
---|---|---|---|
格子寬度 = button 寬度 | 格子寬度 = button 寬度 + 剩餘 | button 寬度 = 格子寬度 |
以下片段 source 將三個按鈕放置在 hbox 中, 並依剩餘的寬度拉寬各 button.
GtkWidget *box1; |
由上面的 source 做出來的佈置, 會受 button 本身大小影響而無法做到同樣寬度大小. 原因在於只有多出來的部份會被平均分配, button 原來的寬度是不會被平均分配的. 在一般使用 box 的佈置中, expand + fill 是個十分實用的選擇.
各 button 寬度相似 |
各 button 寬度相差許多 |
應用程式設計 |
GTK 踩地雷
利用上面所介紹的 box 跟 button 以及 label 已經足夠做出踩地雷這個在 windows 上常用來消遣時間的遊戲. 在開始程式設計之前, 我們應該先詳細分析, 再決定該如何使用已知的工具來達到想要的效果.
我們先來分析這個遊戲, 它需要哪些功能? 在踩地雷的過程中, 玩家可以掀開或標記某個區塊. 假如掀開的區塊藏有地雷, 遊戲結束. 否則顯示周圍有多少地雷. 遊戲中也應該提供一個數字, 避免讓玩家需要計算剩下多少地雷. 當所有不含地雷的區塊都被掀開後, 恭喜玩家並結束遊戲. 此外也需要計算遊戲時間, 以反映玩家對這遊戲的熟練程度. 至於其它功能如提供玩家改變地雷數目, 區塊數目等等, 目前不考慮包含.
我們先考慮地雷區中每個格子所需要的資料. 每個格子需要一個 button, 標識自己是否藏有地雷, 還需要記錄玩家是否曾以滑鼠右鍵 (right-click) 標記過. 為了方便起見, 我們將格子周圍的地雷數目在遊戲初始的安置地雷時也一併計算, 並自行標記格子是否已被掀開. 綜合以上資料, 我們可以定義一個 struct 給格子使用.
struct block |
有了格子的資料格式後, 我們可以定義一個存放 m x n 個格子的空間. 在這裡直覺的反應可能是採用 2D array, 如 struct block map[height][width];. 不過我們還是使用 1D array, 只用 struct block map[width*height]; 來存放. 優點是分配記憶體及初始容易, 不需要額外存放 n 個 pointer, 而且只需要一個整數作 index. 缺點是每次存取都要使用乘法, 例如原本 (x,y) 要換成 x + (y*width). 在此我們使用 GLIB 的 g_malloc0() 來配置記憶體空間. g_malloc0(sz) 會配置一個內容為 0, 共 sz bytes 的空間, 並且在配置錯誤時直接結束程式. 為了方便說明, 底下把全域變數跟程式碼分開放置.
static struct block *map; /* 地雷區資料 */ |
/* 分配記憶體給 map 並初始化 */ |
佈置地雷的部份可以使用 g_random_int_range() 的亂數 function. c = g_random_int_range(a, b); 會從 a 到 b-1 (包含 a 及 b-1) 之中選一個數字出來存入 c, 也就是 a <= c <= b-1. 範圍內每個數字所出現的機率在理論上都相同.
/* 以亂數安置地雷 */ |
再來看看介面的設計. 我們可以用 label 來存放要顯示的文字, 並安排在最上面. 接下來需要 width x height 個的 button. 利用 label, button 和 box 佈置好後可以達到類似下圖的畫面.
踩地雷的外觀 (10x10) |
在這 window 裡一開始就使用 vbox, 並於第一排插入一個 hbox. 第一排的 hbox 則專門用來放置 label, 也就是剩下的地雷數和遊戲時間. 從第二排開始使用 for loop 來做出 width x height 個的 button 做為地雷區. 這部份的全域變數和程式碼如下.
static GtkWidget *mine_label; /* 顯示剩餘地雷數 */ |
1 GtkWidget *vbox; |
第 32,33 行把 button 設成 16x16 的大小.
第 34,35 行設定 button 的屬性使它不會成為輸入的 focus. (否則有一個 button 上會有框, 很難看)
第 39~42 行為 button 提供滑鼠按鍵被按下時事件處理的 callback function. "button-press-event" 中的 button 是指一般滑鼠上面的左右鍵以及三鍵滑鼠才有的中鍵, 並非先前討論的 GtkButton. 為了避免混淆, callback function 刻意取名為 on_mouse_click(). 此外我們傳入一個 index 做為使用者資料, 使 callback function 可以輕易得算出被按下的格子位置.
透過 GTK 的 button, label 和 box, 我們可以很輕易的做出踩地雷的外觀和操作介面. 接下來的工作就是把這個介面跟遊戲內容連結在一起, 也就是開始寫 on_mouse_click() 這個 callback function. 通常在 GTK 程式設計上就是這部份使我們的程式活起來, 對使用者的動作做出各種回應.
首先列出 button-press-event 的 callback function 原型. button-press-event 是 GtkWidget 的一個事件, 所以在 GTK 的參考文件上要到 GtkWidget 底下才找得到. 通常我們在處理 event 的時候會從這個物件本身 (例如 GtkToggleButton) 找起, 若找不到理想的 event 則一步步往上尋找. (GtkButton > GtkBin > GtkContainer...) 有物件導向中的繼承觀念的讀者在這方面應該很容易理解. GtkToggleButton 的繼承關係和 button-press-event 的 callback function 原型如下.
GtkToggleButton 的 Hierarchy, 繼承關係GObject GtkWidget 下 button-press-event 的 callback function 原型 "button-press-event" |
處理事件的 on_mouse_click() 就依上面的原型定義. 我們用 user_data 來分辨被按下的 button (GtkToggleButton) 是屬於哪一個格子, 之後以 GdkEventButton 中的 button (滑鼠的左鍵, 中鍵或是右鍵) 決定要採取什麼動作. 有了掀格子與做記號的動作後, 我們還需要考慮目前已有幾個標了記號的格子, 現在已經掀開了多少個沒有地雷的格子以及遊戲是否已結束. 因此在全域變數中需要再加入幾個統計用的變數.
static gint opened_count; /* 已經掀開多少格子 */ |
1 gboolean on_mouse_click(GtkWidget *widget, |
第 9 行檢查遊戲是否已經結束. 若遊戲已結束, 玩家按下 button 也沒有反應.
第 11 行將 callback 接收的 data 換成數字的 index 來使用. 這個 index 也就是在 g_signal_connect() 中每個 button 自己的 index.
第 19 行使用了 open_block() 來掀開指定的格子. open_block() 是我們接下來要製作的 function.
第 45 行傳回 TRUE 表示這個事件已經被處理完畢, GTK 不需要再尋找其他 callback function 處理.
當玩家掀開一塊周圍完全沒有地雷的格子時 (count=0), 我們可以安全的掀開周圍的八個格子. 若這八個格子之中又有遇到相同的情況則那個格子周圍又可以繼續掀開. 因此我們準備了 open_block 這個重覆呼叫自己的 recursive function, 並由它來檢查遊戲是否結束.
1 void open_block(gint x, gint y) |
第 12,13 行使用了 GtkToggleButton 的 gtk_toggle_button_set_active() 將 button 設定成按下的狀態. TRUE 是按下, FALSE 則是未按下的狀態.
第 21 行以 gtk_button_set_label() 來改變 button 上顯示的文字.
第 28 行的 g_snprintf() 是 GLIB 版本的 snprintf(), 確保在各平台上都可以使用.
第 39~53 行是在掀開周圍沒有地雷 (count=0) 的格子時自動將四周格子也加以掀開的部份. 周圍的每個格子都要先檢查是否超出地雷區範圍.
底下準備了 gameover() 來通知玩家遊戲結束和輸贏結果. 在這裡使用 GtkMessageDialog 時會需要一開始建立出來的 GtkWindow, 所以需要把 window 從 main() 裡拿出來, 也移到全域變數中. 除此之外也要將計時的變數準備好.
static GtkWidget *window; /* 主視窗 */ |
1 void gameover(gboolean won) |
第 18,19 行用 gtk_message_dialog_new() 做出一個 GtkMessageDialog, 用來顯示訊息和一個 OK button.
第 20 行跳入 dialog 的程序中, 讓它自己控制對話窗的顯示及事件處理. (例如按下 OK button 的 clicked 事件) gtk_dialog_run() 會執行到玩家按下 OK button 後才會傳回.
第 21 行用 gtk_widget_destroy() 將剛做出來的 dialog 釋放掉.
最後要放上的是計時的部份. GLIB 提供的計時功能可以透過 g_timeout_add() 輕鬆達成. 只需要定義一個 callback function 再呼叫即可.
1 gboolean tick(gpointer data){ |
第 5 行在遊戲結束後傳回 FALSE. 若 g_timeout_add() 的 callback function 傳回 FALSE, 計時會停止, 也不會再時間到自動呼叫.
g_timeout_add() 的第一個參數是間隔時間, 以千分之一秒為單位. 我們希望每秒中更新一次時間, 所以使用 1000 做第一個參數. 第二個參數則是 callback function, 第三個是提供給 callback function 的額外參數.
/* 啟動計時 */ |
到目前為止所討論到的 GTK 踩地雷的原始碼可以在下面 link 下載.
mines.c
Compile 及執行的方式也跟前面類似.
linux$ gcc -o mines mines.c `pkg-config --cflags --libs gtk+-2.0` linux$ ./mines |
底下是遊戲結束的 GtkMessageDialog 畫面.
| ||||
|
至於其他功能, 例如標記好某些格子後按下滑鼠中鍵方便玩家掀開周圍的數個格子, 或是踩到地雷遊戲結束後顯示所有藏有地雷格子內容, 您也可以自己試著擴充看看. :)
没有评论:
发表评论