วันพุธที่ 9 มีนาคม พ.ศ. 2565

Gtk4 ตอนที่ 1 เริ่มต้นเขียนโปรแกรมบน Gtk4

ตัวอย่างที่ 1

#include <gtk/gtk.h>
int main(int argc, char **argv){
  GtkApplication *app;
  int stat;
  app = gtk_application_new("com.bustecz.ds", G_APPLICATION_FLAGS_NONE);
  stat = g_application_run(G_APPLICATION(app), argc, argv);

  g_object_unref(app);

  return stat;
}


เริ่มต้นโปรแกรมจะเพิ่มส่วนหัวเพื่อเรียก gtk/gtk.h ซึ่งเป็นไลบรารีของ Gtk ลำดับต่อมากำหนดฟังค์ชัน main ซึ่งเป็นฟังค์ชันหลักที่ต้องมีในภาษา C โดยรับพารามิเตอร์ argc และ argv 

บรรทัดต่อมากกำหนดให้ app เป็น GtkApplication*  โดยจะเขียนเป็น GtkApplication* app หรือ GtkApplication *app ก็ได้ ซึ่ง app ที่ได้จะเป็น pointer จากนั้นก็ใช้ gtk_application_new เพื่อสร้าง GtkApplication instance แล้วส่งค่าของพอยเตอร์ไปเก็บไว้ที่ app (ดังนั้นเราอาจจะเรียก app ว่าเป็นตัวแปรสำหรับเก็บ Pointer ก็ได้) แต่ความเป็นจริงแล้ว GtkApplication instance นั้น คือ ข้อมูลแบบสตรัคเจอร์ของภาษา C ที่เก็บข้อมูลเกี่ยวกับ application ที่สั่งรันนั่นเอง

ฟังค์ชัน g_application_run เป็นคำสั่งรันแอปพลิเคชันที่กำหนดไว้ใน instance ในที่นี้ก็คือ app ย้ำอีกครั้งว่า app ไม่ใช่ application แต่เป็นตัวชี้ (pointer ที่ชี้ไปยัง instance ของ application)


จากนั้นก็สั่งคอมไพล์ด้วย Gtk4 ดังนี้

$ gcc `pkg-config --cflags gtk4` pr1.c `pkg-config --libs gtk4` 

คำสั่งนี้จะได้ a.out เราสามารถสั่งรันได้เลย คือ  พิมพ์ ./a.out แล้วกดปุ่ม enter ต้องใส่ ./ นำหน้าทุกครั้งเป็นการสั่งให้ระบบค้นหาในไดเรคทอรีที่เราอยู่

แต่ตัวอย่างนี้เมื่อสั่งรันแล้วจะได้ข้อความดังนี้

(git_ex01:9224): GLib-GIO-WARNING **: 11:57:48.582: Your application does not implement g_application_activate() and has no handlers connected to the 'activate' signal.  It should do one of these.

นั่นเป็นเพราะเราไม่ได้เรียกใช้ activate signal ซึ่งเป็นหลักใหญ่ของ Gtk แต่โค้ดของเราเขียนข้างบนนั้นเป็นคำสั่งแค่สร้าง instance ชื่อ app แล้วสั่งรัน application โดยชี้ไปที่ app ระบบจะไม่มีการเรียกใช้ signal ที่ชื่อ activate

Signal

ข้อความที่แจ้งทางหน้าจอแบ่งออกได้ดังนี้

  1. The application GtkApplication doesn't implement g_application_activate()
  2. It has no handlers connected to the "activate" signal, and
  3. You will need to solve at least one of these.
โดยข้อ 1 บอกว่า เราไม่ได้เรียกใช้ g_application_activate() และข้อ 2 บอกว่าไม่ได้เชื่อมต่อ activate signal โดย signal นี่แหละเป็นหลักสำคัญประการหนึ่งของ Gtk

โดย signal คือ สิ่งที่ส่งออกไปเมื่อมีเหตุการณ์ใดๆ เกิดขึ้น เช่น ขณะสร้าง window ขณะที่ลบ window ขณะที่ย่อหรือขยาย window เป็นต้น

โดย signal อันหนึ่งชื่อ "activate" เป็นสัญญานที่ส่งไปขณะที่เราสั่งรันแอปพลิเคชัน หรือ แอปพลิเคชันถูกเปิดขึ้นมา (activate)

ถ้าเราเชื่อมสัญญานหรือ signal เข้ากับฟังค์ชัน (function) ไว้แล้ว เมื่อเหตุการณ์นั้นๆ เกิดขึ้นระบบก็จะเรียกใช้ฟังค์ชันที่เรากำหนดไว้

ลำดับการสืบทอด
1. เมื่อมีเหตุการณ์บางอย่างเกิดขึ้น
2. ถ้ามีส่วนเกี่ยวข้องกับ signal ก็จะถูกส่งสัญญานออกไป
3. ถ้า signal นั้นเชื่อมต่อกับฟังค์ชันหรือส่วนที่รองรับ สิ่งนั้นก็จะถูกเรียกขึ้น

signal นั้นจะถูกกำหนดไว้ในอ็อบเจกต์ ตัวอย่างเช่น "activate" เป็น signal ของอ็อบเจ็กต์ชื่อว่า GApplication ซึ่งจะถ่ายทอดคุณสมบัติต่างๆ ไปยัง GtkApplication และทั้งหมดนั้นเป็นอ็อบเจ็กต์ลูกของ GObject อีกที โดยจะมีลำดับสืบทอดดังนี้

GObject -- GApplication -- GtkApplication

<---parent                      --->child



ตัวอย่าง

โค้ดต่อไปนี้เป็นตัวอย่างเรียก signal ชื่อ activate 

#include <gtk/gtk.h>

static void app_activate (GApplication *app, gpointer *user_data){
  g_print("GtkApplication is activated.\n");
}

int main(int argc, char **argv){
  GtkApplication *app;
  int stat;
  
  app = gtk_application_new("com.example.www", G_APPLICATION_FLAGS_NONE);
  g_signal_connect(app, "activate", G_CALLBACK(app_activate), NULL);
  stat = g_application_run(G_APPLICATION(app), argc, argv);
  g_object_ref(app);
  
  return stat;
}

เมื่อสั่งรันก็จะได้ผลลัพธ์เหมือนในรูปด้านล่างนี้






ตัวอย่างนี้มีหลักการทำงานดังนี้

ลำดับแรก เราสร้าง handler หรือฟังค์ชันที่จะรองรับการเชื่อมต่อ signal ชื่อว่า app_activate ไว้ตอนต้นโปรแกรม โดยในโปรแกรมจะสั่งพิมพ์คำว่า GtkApplication is activated แล้วขึ้นบรรทัดใหม่ 

ในฟังค์ชัน main จะเพิ่ม g_signal_connect ก่อนที่จะเรียก g_application_run โดย g_signal_connect จะมี arguments อยู่ 4 รายการ คือ

  1. instance หรืออ็อบเจ็กต์ ที่เป็นเจ้าของ signal ในที่นี้ คือ app
  2. ชื่อของ signal ในตัวอย่าง คือ activate 
  3. ชื่อฟังค์ชันที่จะเรียกเมื่อมี signal เกิดขึ้น โดยกำหนดผ่าน G_CALLBACK ในส่วนนี้จะเรียกว่า callback ก็ได้
  4. ข้อมูลที่จะส่งไปให้กับฟังค์ชัน ถ้าไม่มีให้ใส่ NULL


GtkWindow and GtkApplicationWindow

GtkWindow

ข้อความ GtkApplication is activated. เป็นข้อความความที่พิมพ์ในฟังค์ชัน app_activate เป็นการเริ่มต้นเพื่อแสดงให้เห็นถึงหลักการทำงานของ signal และ handle แต่อย่างไรก็ตาม Gtk นั้นเป็นเฟรมเวิร์คของ GUI ดังนั้นใน activate ของโปรแกรมนั้นจะต้องสร้าง window และแสดง window นั้นๆ

ดังนั้นเราจะสร้าง 

  1. หน้าจอขึ้นมาโดยสืบทอดมาจาก GtkWindow.
  2. ผูก window ที่สร้างเข้ากับ GtkApplication.
  3. แสดงหน้าจอ window ที่สร้าง
ดังนั้นเราจะแก้ไขฟังค์ชัน app_activate ใหม่ดังนี้

static void app_activate (GApplication *app, gpointer user_data){
  GtkWidget *win;
  win = gtk_window_new();
  gtk_window_set_application (GTK_WINDOW(win), GTK_APPLICATION(app));
  gtk_widget_show(win);
}

หลังจากคอมไพล์และสั่งรันจะได้หน้าจอดังนี้



Widget คือ อุปกรณ์ รูปแบบ ส่วนใดๆ ส่วนหนึ่งของโปรแกรม แต่สำหรับ GtkWidget แล้ว เป็นส่วนประกอบต่างๆ ที่เราเห็น ตัวอย่างเช่น ข้อความ ปุ่มกด ช่องกรอกข้อมูล เป็นต้น

หรือจะสรุปได้ง่ายๆ ให้เห็นภาพ GtkWidget ก็ คือ ออบเจ็กต์หนึ่งๆ นั่นเอง

parent <-----> child
GtkWidget -- GtkWindow

ตัวอย่างข้างบนนี้ GtkWindow ก็สืบทอดมาจาก GtkWidget

GtkWindow and GtkWidget

ในตัวอย่างเริ่มจากสร้าง instance ชื่อ *win เป็นพอยเตอร์ ของ GtkWidget เพื่อสร้าง GtkWindow อีกที รูปแบบการกำหนดมีดังนี้
GtkWidget *
gtk_window_new (void);

หรือ

GtkWidget *win;
win = gtk_window_new();

คำสั่งข้างบนนี้ได้กำหนดให้ชี้ไปยัง GtkWidget ไม่ใช่ GtkWindow แต่ในความเป็นจริงเราจะสร้าง instance ของ GtkWindow (ไม่ใช่ GtkWidget) 


Connect it to GtkApplication.

เมื่อกำหนด win ให้เป็น GtkWidget แล้ว เราสามารถอ้างอิงถึง window ที่สร้างผ่านมาโคร GTK_WINDOW() แต่เราก็ต้องเชื่อม window นั้นไปยัง instance ของ GtkApplication ที่สร้างไว้ (ในที่นี้ คือ app) ผ่านฟังค์ชัน gtk_window_set_application() ดังนี้

gtk_window_set_application (GTK_WINDOW (win), GTK_APPLICATION (app));

อย่าลืมว่าการทำงานนั้นต้องแปลง win ให้เป็น GtkWindow (cast GtkWidget to GtkWindow) ผ่านมาโคร GTK_WINDOW(app) และต้องแปลง app  

ดังนั้นหากเราจะอ้างอิงไปที่ GtkWindow เราต้องแปลงจาก GtkWidget ให้เป็น GtkWindow ก่อนผ่านมาโครที่คล้ายฟังค์ชัน คือ GTK_WINDOW(win) และแปลง pointer ของ app ให้เป็น GtkApplication ก็ทำผ่านมาโคร GTK_APPLICATION(app) ด้วย

GtkApplication จะถูกเรียกใช้งานจนกว่าวินโดว์ที่สร้างนั้นจะถูกทำลายหรือปิดไป แต่ถ้าเราไม่เชื่อมต่อ ระหว่าง GtkWindow และ GtkApplication ไว้ก่อน GtkApplication จะถูกทำลายทันทีที่สร้างขึ้น ที่เป็นเช่นนั้นเพราะถ้าไม่เชื่อม window กับ application แล้ว GtkApplication ก็ไม่มีที่จะให้รันหรือไม่มีที่ทำงานนั่นเอง

Show the window.

จากนั้นก็เรียกฟังค์ชัน gtk_widget_show เพื่อแสดงวินโดว์ที่สร้างขึ้น ในตัวอย่างจึงเขียนเป็น

gtk_widget_show(win);

โดยสั่งให้แสดง GtkWidget ที่มีชื่ออ้างอิงเป็น win แล้วก็รอจนกว่าวินโดว์ถูกปิดหรือ destroy


GtkApplicationWindow

GtkApplicationWindow เป็นออบเจ็กต์ลูกของ GtkWindow โดยจะมีฟังค์ชันอื่นๆ เพิ่มขึ้นอีก และรวม GtkApplication เข้าไปด้วย ดังนั้นจึงแนะนำให้ใช้แทน GtkWindow โดยการเขียนโปรแกรมก็ทำได้ดังตัวอย่างนี้

1 static void
2 app_activate (GApplication *app, gpointer user_data) {
3   GtkWidget *win;
4 
5   win = gtk_application_window_new (GTK_APPLICATION (app));
6   gtk_window_set_title (GTK_WINDOW (win), "pr4");
7   gtk_window_set_default_size (GTK_WINDOW (win), 400, 300);
8   gtk_widget_show (win);
9 }

จากตัวอย่างข้างบนนี้บรรทัดที่แก้ไข คือ 5, 6 และ 7 โดยใช้ gtk_application_window_new เพื่อสร้างอินสแตนซ์เก็บไว้ใน win และเชื่อมกับ GtkApplication ไปในฟังค์ชันนี้เลย ทำให้เราไม่ต้องรวมกันระหว่าง GtkWindow กับ GtkApplication ด้วยฟังค์ชัน gtk_window_set_application อีกต่อไป

ทั้งหมดที่กล่าวมานี้ เป็นพื้นฐานการสร้างวินโดว์และการสร้าง GtkApplication ซึ่งเป็นแนวทางใหม่สำหรับการเขียนโปรแกรมด้วย Gtk4 (จริงๆ วิธีการนี้ก็เปลี่ยนมาตั้งแต่ Gtk+3 แล้ว)

สังเกตอีกประการหนึ่งการเขียนโปรแกรมแบบนี้ไม่ต้องใช้ gtk_init() ตอนเริ่มต้นโปรแกรม เพราะเราใช้ gtk_application_new แทน ซึ่งฟังค์ชันนี้จะเรียกใช้ gtk_init() ให้เอง...

เรียบเรียงและเรียนรู้จาก 
https://github.com/ToshioCP/Gtk4-tutorial/blob/main/gfm/sec3.md

ไม่มีความคิดเห็น:

แสดงความคิดเห็น

Gtk4 ตอนที่ 6 Defining a Child object

Defining a Child object A Very Simple Editor ในบทความที่ผ่านมาเราสร้างโปรแกรมอ่านไฟล์ชนิดข้อความ และในบทความนี้ก็จะมาปรับแต่งโปรแกรมกันสักหน...