之前介紹頂層視窗,這篇開始介紹各種GUI元件的建立方法和事件處理。目標是Windows篇和Gtk篇只看一篇就能看懂,有些內容跟Windows篇重覆。
各種GUI系統有很多共通部分,每個GUI系統在製作時都會參考現有的系統,做成大家已經習慣的方式比較容易讓人接受。像是check box是多個是非題,radio button是多選一,在每個系統上都是如此。
這是一些GUI系統的元件一覽,即使不是用這些系統寫程式也可以參考。
Gtk 4 Java Swing HTML <input> tag
前篇:如何建一個視窗—GTK篇
按鈕類Windows API篇
Shark的程式教學目錄
buttonwidget.c
初始化使用「如何建一個視窗—GTK篇」裡gtk_init()的方法。
先不解釋程式碼,先編譯出來,一邊操作一邊看解說比較好懂。
因為有用printf()顯示一些訊息,要在命令列打./buttonwidget執行才看得到。
執行的樣子
各種按鈕分成很多個class,以下點選連結會開啟官方文件。
步驟大致都是用「class名稱+new」的函式產生物件、呼叫method設定屬性、用g_signal_connect()設定callback處理事件、用gtk_fixed_put()放進視窗。
在建視窗那篇說過,widget建構子傳回來的都是GtkWidget*型態,呼叫method時要用macro轉型。
為了分辨是哪個按鈕被按下,設定callback時在userData放一個整數當作識別碼。
至於把元件放在視窗裡的方法,其實頂層視窗GtkWindow裡面只能放一個元件,想放多個元件就要先用gtk_window_set_child()放一個「容器」在視窗裡,再把元件放進容器。這篇說明有列出一部分容器:Getting Started #Packing
本篇用的是概念簡單但最不自動化的容器:GtkFixed,自己指定坐標,程式開頭定義一些常數方便計算坐標。按鈕大小會根據顯示的東西自動調整,不用自己設。
class GtkFixed說明
GtkFixed不能在視窗改變大小,或顯示不同語言時自動調整元件的位置大小,因此官方文件不建議用,只是因應特殊需求而保留。本篇只是示範有這個東西存在,以後的篇幅應該不會再用。
各個按鈕實際上內含一個用來顯示文字的label元件,可以用gtk_button_get_child()或gtk_check_button_get_child()取出,只有switch沒顯示文字。所以本程式的元件樹狀圖是這樣:
GUI元件是樹狀結構,不止Gtk,其他函式庫也是這樣做。關閉頂層視窗以後其下的元件也會一併刪除,不用手動刪除。
各種GUI系統有很多共通部分,每個GUI系統在製作時都會參考現有的系統,做成大家已經習慣的方式比較容易讓人接受。像是check box是多個是非題,radio button是多選一,在每個系統上都是如此。
這是一些GUI系統的元件一覽,即使不是用這些系統寫程式也可以參考。
Gtk 4 Java Swing HTML <input> tag
前篇:如何建一個視窗—GTK篇
按鈕類Windows API篇
Shark的程式教學目錄
buttonwidget.c
#include<gtk/gtk.h> #include<stdio.h> #include<stdint.h> const double BUTTON_OFSX=140; const double BUTTON_OFSY=42; //---- //callback static void buttonClicked(GtkButton* self, intptr_t buttonID){ printf("button %u clicked\n", buttonID); } static void checkButtonToggled(GtkCheckButton* self, intptr_t buttonID){ gboolean state=gtk_check_button_get_active(self); printf("checkButton %u toggled. state=%u\n", buttonID, state); } static void toggleButtonToggled(GtkToggleButton* self, intptr_t buttonID){ gboolean state=gtk_toggle_button_get_active(self); printf("toggleButton %u toggled. state=%u\n", buttonID, state); } static gboolean linkClicked(GtkLinkButton* self, intptr_t unused){ return FALSE; } static gboolean switchStateSet(GtkSwitch* self, gboolean newState, intptr_t buttonID){ gboolean active=gtk_switch_get_active(self); gboolean nowState=gtk_switch_get_state(self); printf("switch %u clicked active:%u, nowState:%u newState:%u\n", buttonID, active, nowState, newState); return FALSE; } static void windowClose(GtkWidget* self, int* isEndPtr){ *isEndPtr=1; } //---- //shortcut函式 static GtkWidget* newCheckButton(const char* text, intptr_t id, int x, int y, GtkWidget* container){ GtkWidget* checkButton=gtk_check_button_new_with_label(text); g_signal_connect(checkButton, "toggled", G_CALLBACK(checkButtonToggled), (void*)id); gtk_fixed_put(GTK_FIXED(container), checkButton, x,y); return checkButton; } static GtkWidget* newToggleButton(const char* text, intptr_t id, int x, int y, GtkWidget* container){ GtkWidget* toggleButton=gtk_toggle_button_new_with_label(text); g_signal_connect(toggleButton, "toggled", G_CALLBACK(toggleButtonToggled), (void*)id); gtk_fixed_put(GTK_FIXED(container), toggleButton, x,y); return toggleButton; } //---- //main int main(){ gtk_init(); GtkWidget* window= gtk_window_new(); gtk_window_set_title(GTK_WINDOW(window), "button widget"); gtk_window_set_default_size(GTK_WINDOW(window), BUTTON_OFSX*3+10, BUTTON_OFSY*7+10); gtk_window_set_resizable(GTK_WINDOW(window), 0); //讓視窗不可變更大小 int isEnd=0; g_signal_connect(window, "destroy", G_CALLBACK(windowClose), &isEnd); GtkWidget* fixedContainer= gtk_fixed_new(); gtk_window_set_child(GTK_WINDOW(window), fixedContainer); //建立各種按鈕 GtkWidget* pushButton= gtk_button_new_with_label("(1) button"); g_signal_connect(pushButton, "clicked", G_CALLBACK(buttonClicked), (void*)1); gtk_fixed_put(GTK_FIXED(fixedContainer), pushButton, 10,10); GtkWidget* checkButton1= newCheckButton("(2)check button",2, 10,10+BUTTON_OFSY, fixedContainer); GtkWidget* checkButton2= newCheckButton("(3)check button",3, 10,10+BUTTON_OFSY*2, fixedContainer); GtkWidget* checkButton3= newCheckButton("(4)check button",4, 10,10+BUTTON_OFSY*3, fixedContainer); gtk_check_button_set_inconsistent(GTK_CHECK_BUTTON(checkButton3), 1); GtkWidget* radio1= newCheckButton("(5)radio button",5, 10+BUTTON_OFSX,10+BUTTON_OFSY, fixedContainer); GtkWidget* radio2= newCheckButton("(6)radio button",6, 10+BUTTON_OFSX,10+BUTTON_OFSY*2, fixedContainer); GtkWidget* radio3= newCheckButton("(7)radio button",7, 10+BUTTON_OFSX,10+BUTTON_OFSY*3, fixedContainer); gtk_check_button_set_group(GTK_CHECK_BUTTON(radio2), GTK_CHECK_BUTTON(radio1)); gtk_check_button_set_group(GTK_CHECK_BUTTON(radio3), GTK_CHECK_BUTTON(radio1)); gtk_check_button_set_inconsistent(GTK_CHECK_BUTTON(radio3), 1); GtkWidget* radio4= newCheckButton("(8)radio button",8, 10+BUTTON_OFSX*2,10+BUTTON_OFSY, fixedContainer); GtkWidget* radio5= newCheckButton("(9)radio button",9, 10+BUTTON_OFSX*2,10+BUTTON_OFSY*2, fixedContainer); GtkWidget* radio6= newCheckButton("(10)radio button",10, 10+BUTTON_OFSX*2,10+BUTTON_OFSY*3, fixedContainer); gtk_check_button_set_group(GTK_CHECK_BUTTON(radio5), GTK_CHECK_BUTTON(radio4)); gtk_check_button_set_group(GTK_CHECK_BUTTON(radio6), GTK_CHECK_BUTTON(radio4)); GtkWidget* linkButton= gtk_link_button_new_with_label("http://www.gamer.com.tw/", "(11)link button"); gtk_fixed_put(GTK_FIXED(fixedContainer), linkButton, 10,10+BUTTON_OFSY*4); g_signal_connect(linkButton, "clicked", G_CALLBACK(buttonClicked), (void*)11); g_signal_connect(linkButton, "activate-link", G_CALLBACK(linkClicked), 0); GtkWidget* switchButton= gtk_switch_new(); gtk_fixed_put(GTK_FIXED(fixedContainer), switchButton, 10+BUTTON_OFSX, 10+BUTTON_OFSY*4); g_signal_connect(switchButton, "state-set", G_CALLBACK(switchStateSet), (void*)12); GtkWidget* toggleButton1= newToggleButton("(13)toggle button", 13, 10,10+BUTTON_OFSY*5,fixedContainer); GtkWidget* toggleButton2= newToggleButton("(14)toggle button", 14, 10+BUTTON_OFSX,10+BUTTON_OFSY*5, fixedContainer); GtkWidget* toggleButton3= newToggleButton("(15)toggle button", 15, 10+BUTTON_OFSX*2,10+BUTTON_OFSY*5, fixedContainer); gtk_toggle_button_set_group(GTK_TOGGLE_BUTTON(toggleButton3), GTK_TOGGLE_BUTTON(toggleButton2)); gtk_window_present(GTK_WINDOW(window)); while(!isEnd){ g_main_context_iteration(NULL,TRUE); } return 0; } |
初始化使用「如何建一個視窗—GTK篇」裡gtk_init()的方法。
先不解釋程式碼,先編譯出來,一邊操作一邊看解說比較好懂。
gcc buttonwidget.c -o buttonwidget -Os -s `pkg-config --cflags --libs gtk4` |
執行的樣子
各種按鈕分成很多個class,以下點選連結會開啟官方文件。
步驟大致都是用「class名稱+new」的函式產生物件、呼叫method設定屬性、用g_signal_connect()設定callback處理事件、用gtk_fixed_put()放進視窗。
在建視窗那篇說過,widget建構子傳回來的都是GtkWidget*型態,呼叫method時要用macro轉型。
為了分辨是哪個按鈕被按下,設定callback時在userData放一個整數當作識別碼。
- button,圖中的(1)。class GtkButton
最一般的按鈕,按滑鼠按鈕時外觀變成被按下,放開滑鼠按鈕就跳起。
至於偵測按鈕按下的方法,查官方文件找到clicked signal:
buttonClicked()的參數和傳回值要照這個宣告。
本篇用的建構子是gtk_check_button_new_with_label()。Gtk的按鈕裡不只能顯示文字,也可以顯示圖甚至其他元件,「_with_label()」的建構子是自動在按鈕裡放文字,另一個版本gtk_button_new()是按鈕建立後是空的,顯示的東西要自己處理。 - check button(核取方塊),圖中的(2)~(4)。class GtkCheckButton
多個是或否的選擇,每個check button各自獨立。有的GUI函式庫稱為check box。
偵測按下的方法是toggled信號,但目前狀態不會傳給callback,要用gtk_check_button_get_active()取得。
本程式的GtkCheckButton有9個之多,把程式碼共通的部分放在一個函式newCheckButton()。 - radio button(單選按鈕) (5)~(10)。class GtkCheckButton
用在多個選項裡選一個。點選一個之後,群組裡其他radio button自動取消選取。
跟check button是同一個class,用gtk_check_button_set_group()設定群組之後,外觀和行為就自動變成radio button。
有的GUI系統有3-state check button,它有空白、打勾、未決定三種狀態。Gtk沒有自動改變狀態的3-state check button,要自己寫程式控制。呼叫gtk_check_button_set_inconsistent()會讓按鈕有未決定、打勾兩個狀態,這種按鈕要怎麼用就自己看情況。
以上三種是基本,以下是比較不常用,或不是每個GUI系統都有的。 - link button (11)。class GtkLinkButton
外觀像網頁裡的超連結。gtk_link_button_new_with_label()第一參數填一個URI,按這個按鈕就會用網頁瀏覽器開檔案。本篇填的是巴哈姆特首頁,如果用「file://」開頭可以開啟本機檔案。
查activate-link signal的callback會發現它有傳回值,說明有這兩句:
The default handler is called after the handlers added via g_signal_connect().
Return TRUE if the signal has been handled.
意思是如果callback傳回FALSE,Gtk會再呼叫系統內建的callback(開啟連結),如果傳回TRUE則處理到此為止。可以試試看把linkClicked()的傳回值改成TRUE,這樣按link button也不會開啟連結。
另外因為它有繼承GtkButton,也可以使用clicked信號。 - switch (12)。class GtkSwitch
沒顯示文字。這個按鈕複雜一些,它有active、state兩個屬性,可組合出以下四種狀態:
按下按鈕後內部處理是「修改active → 執行state-set callback → 讓state=active」,而它的state-set callback有傳回值:
如果傳回TRUE,則處理到此為止不會修改state。
如果state-set callback一律傳回FALSE,那它變成單純on、off兩個狀態。但這個按鈕特別的地方是可以延遲改變狀態,像這樣做:
a)一開始按鈕是上圖(A)的狀態。
b)使用者按下後,state-set callback執行一個需要時間處理的工作並傳回TRUE,按鈕變成(B)。
Gtk的callback必須迅速return否則程式會卡住,所以可能要開新的thread執行工作。
c)等工作完成後用gtk_switch_set_state()修改state,讓按鈕變成(C)。 - toggle button (13~15)。class GtkToggleButtton
按一下會保持在按下的狀態,再按一下才浮起。基本上是外觀不一樣的check button,一樣有_get_active()取得狀態,也有_set_group()將它變成單選按鈕。不過因為它沒有繼承GtkCheckButton所以不能用check button的method和信號,但有繼承GtkButton。
至於把元件放在視窗裡的方法,其實頂層視窗GtkWindow裡面只能放一個元件,想放多個元件就要先用gtk_window_set_child()放一個「容器」在視窗裡,再把元件放進容器。這篇說明有列出一部分容器:Getting Started #Packing
本篇用的是概念簡單但最不自動化的容器:GtkFixed,自己指定坐標,程式開頭定義一些常數方便計算坐標。按鈕大小會根據顯示的東西自動調整,不用自己設。
class GtkFixed說明
GtkFixed不能在視窗改變大小,或顯示不同語言時自動調整元件的位置大小,因此官方文件不建議用,只是因應特殊需求而保留。本篇只是示範有這個東西存在,以後的篇幅應該不會再用。
各個按鈕實際上內含一個用來顯示文字的label元件,可以用gtk_button_get_child()或gtk_check_button_get_child()取出,只有switch沒顯示文字。所以本程式的元件樹狀圖是這樣:
GUI元件是樹狀結構,不止Gtk,其他函式庫也是這樣做。關閉頂層視窗以後其下的元件也會一併刪除,不用手動刪除。