gtk教程 linux界面编程

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 的官方網站.

GTKhttp://www.gtk.org/
GNOMEhttp://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 <gtk/gtk.h>
2
3 int main(int argc, char**argv)
4 {
5 GtkWidget *window;
6 gtk_init(&argc, &argv);
7 window=gtk_window_new(GTK_WINDOW_TOPLEVEL);
8 gtk_widget_show(window);
9 gtk_main();
10 return 0;
11 }

第 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 <gtk/gtk.h>
2
3 int main(int argc, char**argv)
4 {
5 GtkWidget *window;
6 GtkWidget *label;
7 gtk_init(&argc, &argv);
8 window=gtk_window_new(GTK_WINDOW_TOPLEVEL);
9 g_signal_connect(G_OBJECT(window), "delete_event",
10 gtk_main_quit, NULL);
11 label=gtk_label_new("Hello world!");
12 gtk_container_add(GTK_CONTAINER(window), label);
13 gtk_widget_show_all(window);

14 gtk_main();
15 return 0;
16 }

第 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 程式設計上的一些特性:
  1. 在 main() 的一開始需要呼叫 gtk_init() 啟動.
  2. 各 widget (GTK 中像 window, label, button 等可以顯示的物件叫 widget) 均以名字加上 new 的 function 建立, 如 gtk_window_new(), gtk_label_new().
  3. 跟某 widget 有直接關聯的 function 名字開頭都會是那 widget 的名稱, 如 gtk_window_new().
  4. GTK 各個 widget 建立後都是以 GtkWidget * 型態傳回, 唯有到使用時才需要做型別轉換. (eg. GTK_CONTAINER(window) 將 window 從 GtkWidget * 轉成 GtkContainer *)
  5. 許多 widget 本身是個 container 可以用 gtk_container_add() 置放另一個 widget.
  6. widget 剛建立時內定都是不顯示, 要用 gtk_widget_show() 或 gtk_widget_show_all().
  7. 當 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,
const gchar *name,
GCallback func,
gpointer func_data);
傳回值代表這個 signal connection 的 id.
object要處理事件的物件. 我們用的 widget 可以透過 G_OBJECT() 轉換, 如 G_OBJECT(window).
name事件的名稱, 請參考 GTK 文件上的列表.
func當事件發生後會呼叫這個 callback function.
func_data提供給 func 的額外資料. 一般不需提供而使用 NULL.

Callback function 的參數和傳回值格式最正確的格式可以在 GTK 的參考手冊中各個 widget 的 Signal Prototypes 列表裡查到. 例如 GtkButton 下的幾個:

"clicked"   void user_function(GtkButton *button,
gpointer user_data);
"enter" void user_function(GtkButton *button,
gpointer user_data);
"leave" void user_function(GtkButton *button,
gpointer user_data);

要在 button 被按下時執行某個 function 我們可以 connect 那個 function 到 button 的 "clicked" 事件. 用:

void on_clicked()
{
g_print("Hello world!/n");
}

....

g_signal_connect(G_OBJECT(button), "clicked",
G_CALLBACK(on_click), NULL);

其他也有較為複雜的參數, 像是 GtkWidget 中的畫面重繪 expose_event:

"expose-event"
gboolean user_function (GtkWidget *widget,
GdkEventExpose *event,
gpointer user_data);

當我們的 callback function 被呼叫時, 這些參數也會被提供. 有時候我們並不需要這些額外的資料就能在 callback function 中完成工作, 便可以省略後面幾個參數. 例如重繪一個視窗時, GdkEventExpose 中提供了需要更新的方塊, 讓程式設計時可以只更新需要更新的部份不用全部重繪. 若只是簡單的程式想直接重繪整個視窗區, 則不需要理會這個方塊. 可能只會使用第一個參數接收 GtkWidget 再拿它來重新畫過內容.

gboolean on_exposed(GtkWidget *widget)
{
// do drawings here
....
return TRUE;
}

在自己定義的 callback function 中傳回 TRUE 表示這個事件已經被處理完畢. 傳回 FALSE 則 GTK 會繼續找是否還有其他合適的 event handler.



Button widget

在看完了事件處理之後我們馬上先做可以讓 user click 的 button 一個小程式試試.

button1.c
1 #include <gtk/gtk.h>
2
3 void on_clicked(GtkWidget *widget, gpointer data)
4 {
5 g_print("User has clicked button %s./n", (gchar *)data);
6 }
7
8 int main(int argc, char **argv)
9 {
10 GtkWidget *window;
11 GtkWidget *button;
12
13 gtk_init(&argc, &argv);
14 window=gtk_window_new(GTK_WINDOW_TOPLEVEL);
15 g_signal_connect(G_OBJECT(window), "delete_event",
16 G_CALLBACK(gtk_main_quit), NULL);
17 button=gtk_button_new_with_label("Click Me");
18 g_signal_connect(G_OBJECT(button), "clicked",
19 G_CALLBACK(on_clicked), "[Click Me]");
20 gtk_container_add(GTK_CONTAINER(window), button);
21 gtk_widget_show_all(window);
22 gtk_main();
23 return 0;
24 }

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;
GtkWidget *box2;

box1=gtk_hbox_new(FALSE, 4); /* 不等寬, 間隔 4px */
box2=gtk_vbox_new(TRUE, 0); /* 等高, 無間隔 (0px) */

將 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,
GtkWidget *child,
gboolean expand,
gboolean fill,
guint padding);

void gtk_box_pack_end (GtkBox *box,
GtkWidget *child,
gboolean expand,
gboolean fill,
guint padding);
box做為容器的 box.
child要裝入 box 的 widget.
expandTRUE 則分配剩餘的空白區域給這個 widget, 若有兩個以上會平均分配.
fillTRUE 則放大 widget 來填滿在 expand 時被分配的剩餘空白區域.
padding實際分配的 widget 離所在格子邊緣的距離, 單位 pixel.

底下是 Glade 中使用 hbox 的幾個圖例. 為了方便說明, 圖中網狀的部份表示屬於 button1 格子的空白空間. 在實際的程式中並不會出現網狀, 而是以類似左圖的樣子兩側有著空格.

expand
fill
FALSE
FALSE
TRUE
FALSE
TRUE
TRUE
格子寬度 =
button 寬度
格子寬度 =
button 寬度 + 剩餘
button 寬度 =
格子寬度

以下片段 source 將三個按鈕放置在 hbox 中, 並依剩餘的寬度拉寬各 button.

GtkWidget *box1;
GtkWidget *button1;
GtkWidget *button2;
GtkWidget *button3;

... 建立 button 跟事件處理 ...

box1=gtk_hbox_new(TRUE, 2); /* 等寬, 間隔 2px */
gtk_box_pack_start(GTK_BOX(box1), button1, TRUE, TRUE, 0);
gtk_box_pack_start(GTK_BOX(box1), button2, TRUE, TRUE, 0);
gtk_box_pack_start(GTK_BOX(box1), button3, TRUE, TRUE, 0);
gtk_widget_show_all(box1);

由上面的 source 做出來的佈置, 會受 button 本身大小影響而無法做到同樣寬度大小. 原因在於只有多出來的部份會被平均分配, button 原來的寬度是不會被平均分配的. 在一般使用 box 的佈置中, expand + fill 是個十分實用的選擇.


各 button 寬度相似

各 button 寬度相差許多


應用程式設計


GTK 踩地雷
利用上面所介紹的 box 跟 button 以及 label 已經足夠做出踩地雷這個在 windows 上常用來消遣時間的遊戲. 在開始程式設計之前, 我們應該先詳細分析, 再決定該如何使用已知的工具來達到想要的效果.

我們先來分析這個遊戲, 它需要哪些功能? 在踩地雷的過程中, 玩家可以掀開或標記某個區塊. 假如掀開的區塊藏有地雷, 遊戲結束. 否則顯示周圍有多少地雷. 遊戲中也應該提供一個數字, 避免讓玩家需要計算剩下多少地雷. 當所有不含地雷的區塊都被掀開後, 恭喜玩家並結束遊戲. 此外也需要計算遊戲時間, 以反映玩家對這遊戲的熟練程度. 至於其它功能如提供玩家改變地雷數目, 區塊數目等等, 目前不考慮包含.

我們先考慮地雷區中每個格子所需要的資料. 每個格子需要一個 button, 標識自己是否藏有地雷, 還需要記錄玩家是否曾以滑鼠右鍵 (right-click) 標記過. 為了方便起見, 我們將格子周圍的地雷數目在遊戲初始的安置地雷時也一併計算, 並自行標記格子是否已被掀開. 綜合以上資料, 我們可以定義一個 struct 給格子使用.

struct block
{
gint count; /* 周圍有多少地雷 */
gboolean mine; /* 是否藏有地雷 */
gboolean marked; /* 是否被標記過 */
gboolean opened; /* 是否已被掀開 */
GtkWidget *button;
};

有了格子的資料格式後, 我們可以定義一個存放 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; /* 地雷區資料 */
static gint width=10; /* 地雷區寬度 */
static gint height=10; /* 地雷區高度 */
static gint mines=20; /* 地雷數量 */

/* 分配記憶體給 map 並初始化 */
map=(struct block *)g_malloc0(sizeof(struct block)*
width*height);

佈置地雷的部份可以使用 g_random_int_range() 的亂數 function. c = g_random_int_range(a, b); 會從 a 到 b-1 (包含 a 及 b-1) 之中選一個數字出來存入 c, 也就是 a <= c <= b-1. 範圍內每個數字所出現的機率在理論上都相同.

/* 以亂數安置地雷 */
gint size=width*height;
gint i=0;
while(i<mines){
gint index;
gint row, col;
index=g_random_int_range(0, size);
if(map[index].mine==TRUE) /* 已有地雷 */
continue;
map[index].mine=TRUE;
row=index/width;
col=index%width;

/* 四周格子的 count 加一 */
if(row>0){
/* 左上 */ if(col>0) map[index-width-1].count++;
/* 正上 */ map[index-width].count++;
/* 右上 */ if(col<width-1) map[index-width+1].count++;
}
/* 左 */ if(col>0) map[index-1].count++;
/* 右 */ if(col<width-1) map[index+1].count++;
if(row<height-1){
/* 左下 */ if(col>0) map[index+width-1].count++;
/* 正下 */ map[index+width].count++;
/* 右下 */ if(col<width-1) map[index+width+1].count++;
}

i++;
}

再來看看介面的設計. 我們可以用 label 來存放要顯示的文字, 並安排在最上面. 接下來需要 width x height 個的 button. 利用 label, button 和 box 佈置好後可以達到類似下圖的畫面.


踩地雷的外觀 (10x10)

在這 window 裡一開始就使用 vbox, 並於第一排插入一個 hbox. 第一排的 hbox 則專門用來放置 label, 也就是剩下的地雷數和遊戲時間. 從第二排開始使用 for loop 來做出 width x height 個的 button 做為地雷區. 這部份的全域變數和程式碼如下.

static GtkWidget *mine_label; /* 顯示剩餘地雷數 */
static GtkWidget *time_label; /* 顯示遊戲時間 */
static gint button_size=16; /* button 大小 */

1 GtkWidget *vbox;
2 GtkWidget *hbox;
3 GtkWidget *label;
4 gint i, j index;
5
6 vbox=gtk_vbox_new(FALSE, 0);
7
8 /* 存放 label 的第一個 hbox */
9 hbox=gtk_hbox_new(FALSE, 0);
10 label=gtk_label_new("Mines:");
11 gtk_box_pack_start(GTK_BOX(hbox), label,
12 FALSE, FALSE, 4);
13 mine_label=gtk_label_new("0");
14 gtk_box_pack_start(GTK_BOX(hbox), mine_label,
15 FALSE, FALSE, 2);
16 label=gtk_label_new("Time:");
17 gtk_box_pack_start(GTK_BOX(hbox), label,
18 FALSE, FALSE, 4);
19 time_label=gtk_label_new("0");
20 gtk_box_pack_start(GTK_BOX(hbox), time_label,
21 FALSE, FALSE, 2);
22 gtk_widget_show_all(hbox);
23 gtk_box_pack_start(GTK_BOX(vbox), hbox,
24 FALSE, FALSE, 0);
25
26 /* width x height 個 button 的區塊 */
27 for(i=0, index=0; i<height; i++){
28 hbox=gtk_hbox_new(FALSE, 0);
29 for(j=0; j<width; j++){
30 GtkWidget *button;
31 button=gtk_button_new();
32 gtk_widget_set_usize(button,
33 button_size, button_size);
34 g_object_set(G_OBJECT(button),
35 "can-focus", FALSE, NULL);
36 gtk_box_pack_start(GTK_BOX(hbox),
37 button, FALSE, FALSE, 0);
38 gtk_widget_show(button);
39 g_signal_connect(G_OBJECT(button),
40 "button-press-event",
41 G_CALLBACK(on_mouse_click),
42 (gpointer)index);
43 map[index].button=button;
44 index++;
45 }
46 gtk_box_pack_start(GTK_BOX(vbox), hbox,
47 FALSE, FALSE, 0);
48 gtk_widget_show(hbox);
49 }

第 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
+----GtkObject
+----GtkWidget
+----GtkContainer
+----GtkBin
+----GtkButton
+----GtkToggleButton

GtkWidget 下 button-press-event 的 callback function 原型
"button-press-event"
gboolean user_function (GtkWidget *widget,
GdkEventButton *event,
gpointer user_data);

處理事件的 on_mouse_click() 就依上面的原型定義. 我們用 user_data 來分辨被按下的 button (GtkToggleButton) 是屬於哪一個格子, 之後以 GdkEventButton 中的 button (滑鼠的左鍵, 中鍵 或是右鍵) 決定要採取什麼動作. 有了掀格子與做記號的動作後, 我們還需要考慮目前已有幾個標了記號的格子, 現在已經掀開了多少個沒有地雷的格子以及遊戲是否已結束. 因此在全域變數中需要再加入幾個統計用的變數.

static gint opened_count;  /* 已經掀開多少格子 */
static gint marked_count; /* 已經標記多少格子 */
static gboolean game_over; /* 遊戲是否已結束 */

1 gboolean on_mouse_click(GtkWidget *widget,
2 GdkEventButton *event,
3 gpointer data)
4 {
5 gint index;
6 gint row, col;
7 gchar buf[4];
8
9 if(game_over==TRUE) return TRUE; /* 遊戲已結束 */
10
11 index=(gint)data;
12
13 switch(event->button){
14 case 1: /* 滑鼠左鍵 */
15 /* 從 index 算出發生事件格子的行列 */
16 row=index/width;
17 col=index%width;
18 /* 掀開格子 */
19 open_block(col, row);
20 break;
21 case 2: /* 滑鼠中鍵 */
22 break;
23 case 3: /* 滑鼠右鍵 */
24 /* 已掀開的格子不做記號 */
25 if(map[index].opened==TRUE)
26 break;
27 /* 原來有記號則消掉, 沒有則畫上記號 */
28 if(map[index].marked!=TRUE){
29 map[index].marked=TRUE;
30 gtk_button_set_label(
31 GTK_BUTTON(widget), "@");
32 marked_count++;
33 }else{
34 map[index].marked=FALSE;
35 gtk_button_set_label(
36 GTK_BUTTON(widget), "");
37 marked_count--;
38 }
39 /* 顯示新的地雷數 */
40 g_snprintf(buf, 4, "%d",
41 MAX(0, mines-marked_count));
42 gtk_label_set_text(GTK_LABEL(mine_label), buf);
43 }
44
45 return TRUE;
46 }

第 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)
2 {
3 gint index;
4 GtkWidget *button;
5
6 index=x+y*width;
7
8 if(game_over==TRUE || map[index].marked==TRUE)
9 return; /* 遊戲已結束或防止玩家誤翻有記號的格子 */
10
11 button=map[index].button;
12 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button),
13 TRUE); /* 改變 button 狀態為按下 */
14
15 if(map[index].opened==TRUE) /* 掀開的格子保持按下狀態即可 */
16 return;
17
18 map[index].opened=TRUE; /* 格子狀態為掀開 */
19
20 if(map[index].mine==TRUE){ /* 若藏有地雷 */
21 gtk_button_set_label(GTK_BUTTON(button), "*");
22 gameover(FALSE); /* 踩到地雷遊戲結束 */
23 return;
24 }
25
26 if(map[index].count>0){ /* 若周圍有地雷 */
27 gchar buf[2];
28 g_snprintf(buf, 2, "%d", map[index].count);
29 gtk_button_set_label(GTK_BUTTON(button), buf);
30 }
31
32 opened_count++; /* 已掀開的格子又多了一個 */
33
34 if(opened_count+mines==width*height){
35 gameover(TRUE); /* 所有空地都被翻完時遊戲結束 */
36 return;
37 }
38
39 if(map[index].count==0){ /* 若周圍沒有地雷 */
40 /* 掀開周圍格子 */
41 if(y>0){
42 if(x>0) open_block(x-1, y-1);
43 open_block(x, y-1);
44 if(x<width-1) open_block(x+1, y-1);
45 }
46 if(x>0) open_block(x-1, y);
47 if(x<width-1) open_block(x+1, y);
48 if(y<height-1){
49 if(x>0) open_block(x-1, y+1);
50 open_block(x, y+1);
51 if(x<width-1) open_block(x+1, y+1);
52 }
53 }
54 }

第 12,13 行使用了 GtkToggleButton 的 gtk_toggle_button_set_active() 將 button 設定成按下的狀態. TRUE 是按下, FALSE 則是未按下的狀態.
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值