前往
大廳
主題

【程式】GUI元件——按鈕類 (Gtk)

Shark | 2023-08-02 19:17:14 | 巴幣 2 | 人氣 300

之前介紹頂層視窗,這篇開始介紹各種GUI元件的建立方法和事件處理。目標是Windows篇和Gtk篇只看一篇就能看懂,有些內容跟Windows篇重覆。

各種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`
因為有用printf()顯示一些訊息,要在命令列打./buttonwidget執行才看得到。
執行的樣子




各種按鈕分成很多個class,以下點選連結會開啟官方文件。
步驟大致都是用「class名稱+new」的函式產生物件、呼叫method設定屬性、用g_signal_connect()設定callback處理事件、用gtk_fixed_put()放進視窗。
在建視窗那篇說過,widget建構子傳回來的都是GtkWidget*型態,呼叫method時要用macro轉型。

為了分辨是哪個按鈕被按下,設定callback時在userData放一個整數當作識別碼。
  1. button,圖中的(1)。class GtkButton
    最一般的按鈕,按滑鼠按鈕時外觀變成被按下,放開滑鼠按鈕就跳起。
    至於偵測按鈕按下的方法,查官方文件找到clicked signal

    buttonClicked()的參數和傳回值要照這個宣告。

    本篇用的建構子是gtk_check_button_new_with_label()。Gtk的按鈕裡不只能顯示文字,也可以顯示圖甚至其他元件,「_with_label()」的建構子是自動在按鈕裡放文字,另一個版本gtk_button_new()是按鈕建立後是空的,顯示的東西要自己處理。
  2. check button(核取方塊),圖中的(2)~(4)。class GtkCheckButton
    多個是或否的選擇,每個check button各自獨立。有的GUI函式庫稱為check box。
    偵測按下的方法是toggled信號,但目前狀態不會傳給callback,要用gtk_check_button_get_active()取得。
    本程式的GtkCheckButton有9個之多,把程式碼共通的部分放在一個函式newCheckButton()。
  3. 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系統都有的。
  4. 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信號。
  5. 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)。
  6. toggle button (13~15)。class GtkToggleButtton
    按一下會保持在按下的狀態,再按一下才浮起。基本上是外觀不一樣的check button,一樣有_get_active()取得狀態,也有_set_group()將它變成單選按鈕。不過因為它沒有繼承GtkCheckButton所以不能用check button的method和信號,但有繼承GtkButton。
官方文件還有一些元件被分類成按鈕,drop down、menu button、scale button等等的是按鈕後會跳出其他元件,跳出的東西要另外設定;lock button的使用需要GPermission物件,這些可能以後再介紹。

至於把元件放在視窗裡的方法,其實頂層視窗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,其他函式庫也是這樣做。關閉頂層視窗以後其下的元件也會一併刪除,不用手動刪除。

創作回應

更多創作