แสดงบทความที่มีป้ายกำกับ พื้นฐาน แสดงบทความทั้งหมด
แสดงบทความที่มีป้ายกำกับ พื้นฐาน แสดงบทความทั้งหมด

วันอาทิตย์ที่ 13 มีนาคม พ.ศ. 2565

Gtk4 ตอนที่ 5 OpenSignal

G_APPLICATION_HANDLES_OPEN flag

ในบทความที่ผ่านมาได้กล่าวถึง GtkTextView GtkTextBuffer และ GtkScrolledWindow เพราะว่ามุ่งหวังจะสร้างโปรแกรมอ่านไฟล์ ซึ่งจะมีวิธีการหลากหลาย แต่สำหรับมือใหม่ก็จะกล่าวในเนื้อหาที่ง่ายๆ แต่ก่อนอื่นก็ต้องทำความเข้าใจเรื่อง OpenSignal เสียก่อน

ต่อไปจะทำโปรแกรมให้รับพารามิเตอร์เพื่อเปิดอ่านไฟล์ไปเก็บไว้ใน GtkTextBuffer ตัวอย่างเช่น

$ ./a.out filename

วิธีการนี้เราต้องรู้ก่อนว่า GtkApplication หรือ GApplication เข้าใจอาร์กิวเมนต์หรือรับพารามิเตอร์อย่างไร 

เมื่อ GtkApplication ถูกสร้างขึ้น จะมี flag ถูกกำหนดขึ้นในฐานะอาร์กิวเมนท์ คือ

GtkApplication *
gtk_application_new (const gchar *application_id, GApplicationFlags flags);

ในบทความนี้จะอธิบาย flags สองอัน คือ G_APPLICATION_FLAGS_NONE และ G_APPLICATION_HANDLES_OPEN 

และหากสังเกต อาร์กิวเมนต์ นี้จะถูกส่งไปพร้อมกับการสร้าง application จาก GtkApplicaton และทุกครั้งเราส่ง G_APPLICATION_FLAGS_NONE เป็นค่าดีฟอลท์ นั่นคือ โปรแกรมจะไม่รับพารามิเตอร์ใดๆ ดังนั้นถ้าเราส่งอาร์กิวเมนต์ไปพร้อมกับโปรแกรม ระบบก็จะแสดงข้อความ error

แต่ถ้าเราต้องการส่งอาร์กิวเมนต์ไปขณะที่รันโปรแกรมจากคอมมานด์ไลน์นั้น เราต้องใช้ G_APPLICATION_HANDLES_COMMAND_LINE มาช่วย

โดย flags ที่จะใช้นั้น คือ

GApplicationFlags' Members

G_APPLICATION_FLAGS_NONE  Default. (ไม่มีการส่งผ่านอาร์กิวเมนต์)
  ... ... ...
G_APPLICATION_HANDLES_OPEN  ส่งผ่านอาร์กิวเมนต์ตอนเรียกใช้โปรแกรม
  ... ... ...

ตัวอย่างการใช้งาน เช่น

app = gtk_application_new ("com.github.ToshioCP.tfv3", G_APPLICATION_HANDLES_OPEN); 

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


open signal

วกกลับมาเรื่องโปรแกรม เมื่อ Application ถูกรัน จะมี signal ที่เราสามารถส่งไป 2 อย่าง คือ

  • activate signal --- เป็น signal ที่ถูกเรียกใช้เมื่อไม่มีอาร์กิวเมนต์
  • open signal --- เป็น signal ที่เรียกใช้เมื่อมีอย่างน้อย 1 อาร์กิวเมนต์

แฮนเดลอร์ที่ชื่อ open (handler คือ ตัวที่รองรับหลัง signal ถูกเรียก) จะมีรูปแบบดังนี้

void user_function (GApplication *application,
                   gpointer      files,
                   gint          n_files,
                   gchar        *hint,
                   gpointer      user_data)

โดยพารามิเตอร์ที่รับมา คือ 

  • application --- อินสแตนซ์ของ application ปกติเป็น GtkApplication
  • files --- อะเรย์ของ GFiles ประกอบด้วย [array length=n_files] [element-type GFile]
  • n_files --- เป็นจำนวนของอิลิเมนต์ของ files 
  • hint --- เป็น hint ที่จะเรียกโดย อินสแตนซ์ (ไม่ใส่ก็ได้)
  • user_data --- ข้อมูลที่ผู้ใช้ส่งไปให้กับ handler 


Making a file viewer

What is a file viewer?

ตอนนี้เราจะสร้างโปรแกรมเปิดอ่านไฟล์ โดยจะแสดงข้อความในไฟล์ที่ระบุชื่อไฟล์ตอนเริ่มต้นโปรแกรม โดยมีหลักการดังนี้

  • When arguments are given, it treats the first argument as a filename and opens it.
  • If opening the file succeeds, it reads the contents of the file and inserts it to GtkTextBuffer and then shows the window.
  • If it fails to open the file, it will show an error message and quit.
  • If there's no argument, it will shows an error message and quit.
  • If there are two or more arguments, the second one and any others are ignored.

The program which does this is shown below.

1 #include <gtk/gtk.h>
 2 
 3 static void
 4 app_activate (GApplication *app, gpointer user_data) {
 5   g_print ("You need a filename argument.\n");
 6 }
 7 
 8 static void
 9 app_open (GApplication *app, GFile ** files, gint n_files, gchar *hint, gpointer user_data) {
10   GtkWidget *win;
11   GtkWidget *scr;
12   GtkWidget *tv;
13   GtkTextBuffer *tb;
14   char *contents;
15   gsize length;
16   char *filename;
17 
18   win = gtk_application_window_new (GTK_APPLICATION (app));
19   gtk_window_set_default_size (GTK_WINDOW (win), 400, 300);
20 
21   scr = gtk_scrolled_window_new ();
22   gtk_window_set_child (GTK_WINDOW (win), scr);
23 
24   tv = gtk_text_view_new ();
25   tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
26   gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (tv), GTK_WRAP_WORD_CHAR);
27   gtk_text_view_set_editable (GTK_TEXT_VIEW (tv), FALSE);
28   gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scr), tv);
29 
30   if (g_file_load_contents (files[0], NULL, &contents, &length, NULL, NULL)) {
31     gtk_text_buffer_set_text (tb, contents, length);
32     g_free (contents);
33     if ((filename = g_file_get_basename (files[0])) != NULL) {
34       gtk_window_set_title (GTK_WINDOW (win), filename);
35       g_free (filename);
36     }
37     gtk_widget_show (win);
38   } else {
39     if ((filename = g_file_get_path (files[0])) != NULL) {
40       g_print ("No such file: %s.\n", filename);
41       g_free (filename);
42     }
43     gtk_window_destroy (GTK_WINDOW (win));
44   }
45 }
46 
47 int
48 main (int argc, char **argv) {
49   GtkApplication *app;
50   int stat;
51 
52   app = gtk_application_new ("com.github.ToshioCP.tfv3", G_APPLICATION_HANDLES_OPEN);
53   g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
54   g_signal_connect (app, "open", G_CALLBACK (app_open), NULL);
55   stat = g_application_run (G_APPLICATION (app), argc, argv);
56   g_object_unref (app);
57   return stat;
58 }

เมื่อสั่งรันโปรแกรมจากหน้าจอ Terminal  พร้อมกับกำหนดชื่อไฟล์ที่จะเปิดก็จะได้หน้าจอดังนี้


โปรแกรมจะมีรายละเอียดดังนี้

ก่อนอื่นเราต้องแก้ไขอาร์กิวเมนท์ในฟังค์ชัน main ก่อนโดยเปลี่ยนดังนี้

  • G_APPLICATION_FLAGS_NONE เปลี่ยนเป็น G_APPLICATION_HANDLES_OPEN; และ
  • เพิ่ม g_signal_connect (app, "open", G_CALLBACK (on_open), NULL)

จากนั้นลบโค้ดออกจาก app_activate แล้วให้มีเฉพาะ พิมพ์ข้อความเตือน กรณีไม่ระบุชื่อไฟล์ที่จะเปิดอ่านในโปรแกรม และโปรแกรมก็จะยกเลิกโดยอัตโนมัติเพราะใน app_activate ไม่มีการสร้างและแสดงวินโดว์ ซึ่งไม่เกิดการวนลูปในระบบโปรแกรม

หลักๆ แล้วการทำงานจะอยู่ที่ app_open ภายในฟังค์ชันนี้จะมีหน้าที่ดังนี้

  • สร้าง GtkApplicationWindow, GtkScrolledWindow, GtkTextView และ GtkTextBuffer พร้อมกับเชื่อม Widget ทั้งหมดเข้าด้วยกัน 
  • กำหนดการตัดบรรทัดเป็น GTK_WRAP_WORD_CHAR ใน GtktextView;
  • กำหนด GtkTextView ให้อ่านได้เพียงอย่างเดียว เพราะว่าโปรแกรมนี้สำหรับอ่าน ไม่ใช่โปรแกรมแก้ไขไฟล์
  • อ่านไฟล์แล้วเพิ่มข้อความไว้ใน GtkTextBuffer และ
  • ถ้าไม่มีไฟล์ที่จะเปิดหรือมีปัญหาเกี่ยวกับการเปิดไฟล์ให้ออกจากโปรแกรม พร้อมกับลบอินสแตนซ์ของ window ออกไปด้วย


ในโปรแกรมนี้ส่วนสำคัญในการเปิดอ่านไฟล์อยู่ตรงนี้

if (g_file_load_contents (files[0], NULL, &contents, &length, NULL, NULL)) {
  gtk_text_buffer_set_text (tb, contents, length);
  g_free (contents);
  if ((filename = g_file_get_basename (files[0])) != NULL) {
    gtk_window_set_title (GTK_WINDOW (win), filename);
    g_free (filename);
  }
  gtk_widget_show (win);
} else {
  if ((filename = g_file_get_path (files[0])) != NULL) {
    g_print ("No such file: %s.\n", filename);
    g_free (filename);
  }
  gtk_window_destroy (GTK_WINDOW (win));
}

ฟังค์ชัน g_file_load_contents จะโหลดไฟล์ไปไว้ใน buffer โดยจะจองหน่วยความจำ (allocated) contents ขึ้นมาโดยจะอ้างอิงเป็น pointer (ต้องมีเครื่องหมาย &) และความยาวของ buffer จะกำหนดไว้ใน length แล้วก็จะส่งค่า TRUE กลับ กรณีที่เปิดไฟล์สำเร็จ 

ดังนั้นหลังจากที่อ่านข้อมูลไปเก็บใน buffer เสร็จแล้ว ต้องคืนหน่วยความจำตำแหน่งของ contents ด้วย มิฉะนั้นจะทำให้เกิด memory leak (ตรงนี้ดูจะขัดใจเล็กน้อยตรงที่เราต้องศึกษาให้ละเอียดว่าฟังค์ชันไหนสร้างแล้วจองหน่วยความจำบ้าง ต้องคืนให้หมด หากปล่อยไว้จะกลายเป็นบัก ทำให้โปรแกรมหยุดทำงานหรือช้าลงในภายหลัง)

อีกฟังค์ชันหนึ่งที่ต้องคืนหน่วยความจำ คือ g_file_get_basename และ g_file_get_path เพราะระบบจะจองหน่วยความสำหรับเก็บข้อมูลที่ส่งกลับจากฟังค์ชัน ในตัวอย่างนี้ คือ filename 


GtkNotebook

ในระบบจะมี Widget หนึ่งชื่อว่า GtkNotebook หรือที่รู้จักกันอีกชื่อหนึ่งว่า Tabs โดยในหนึ่งหน้าจอจะมีหลายๆ tab ซึ่งเป็น Multiple child เหมือนตัวอย่างในรูปด้านล่างนี้

GtkNotebook

ดูจากภาพด้านบน ภาพแรก tab ถูกเลือกจะแสดงในกรอบด้านล่างเป็นไฟล์ pr1.c  และเมื่อคลิก tab ชื่อ tfv1.c ก็จะได้เหมือนภาพขวา โดยข้อมูลจะเปลี่ยนเป็นรายละเอียดของไฟล์ tfv1.c

ในตัวอย่างต่อไปเราจะใช้ GtkNotebook มาแทรกเข้าเป็น child ของ GtkApplicationWindow และในแต่ละ tab ของ GtkNotebook ก็จะเพิ่ม GtkScrolledWindow เข้าไปเป็น child เพื่อใช้แสดงผลในไฟล์ที่เปิด

ตัวอย่างโปรแกรม

 1 #include <gtk/gtk.h>
 2 
 3 static void
 4 app_activate (GApplication *app, gpointer user_data) {
 5   g_print ("You need a filename argument.\n");
 6 }
 7 
 8 static void
 9 app_open (GApplication *app, GFile ** files, gint n_files, gchar *hint, gpointer user_data) {
10   GtkWidget *win;
11   GtkWidget *nb;
12   GtkWidget *lab;
13   GtkNotebookPage *nbp;
14   GtkWidget *scr;
15   GtkWidget *tv;
16   GtkTextBuffer *tb;
17   char *contents;
18   gsize length;
19   char *filename;
20   int i;
21 
22   win = gtk_application_window_new (GTK_APPLICATION (app));
23   gtk_window_set_title (GTK_WINDOW (win), "file viewer");
24   gtk_window_set_default_size (GTK_WINDOW (win), 400, 300);
25   gtk_window_maximize (GTK_WINDOW (win));
26 
27   nb = gtk_notebook_new ();
28   gtk_window_set_child (GTK_WINDOW (win), nb);
29 
30   for (i = 0; i < n_files; i++) {
31     if (g_file_load_contents (files[i], NULL, &contents, &length, NULL, NULL)) {
32       scr = gtk_scrolled_window_new ();
33       tv = gtk_text_view_new ();
34       tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
35       gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (tv), GTK_WRAP_WORD_CHAR);
36       gtk_text_view_set_editable (GTK_TEXT_VIEW (tv), FALSE);
37       gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scr), tv);
38 
39       gtk_text_buffer_set_text (tb, contents, length);
40       g_free (contents);
41       if ((filename = g_file_get_basename (files[i])) != NULL) {
42         lab = gtk_label_new (filename);
43         g_free (filename);
44       } else
45         lab = gtk_label_new ("");
46       gtk_notebook_append_page (GTK_NOTEBOOK (nb), scr, lab);
47       nbp = gtk_notebook_get_page (GTK_NOTEBOOK (nb), scr);
48       g_object_set (nbp, "tab-expand", TRUE, NULL);
49     } else if ((filename = g_file_get_path (files[i])) != NULL) {
50         g_print ("No such file: %s.\n", filename);
51         g_free (filename);
52     } else
53         g_print ("No valid file is given\n");
54   }
55   if (gtk_notebook_get_n_pages (GTK_NOTEBOOK (nb)) > 0)
56     gtk_widget_show (win);
57   else
58     gtk_window_destroy (GTK_WINDOW (win));
59 }
60 
61 int
62 main (int argc, char **argv) {
63   GtkApplication *app;
64   int stat;
65 
66   app = gtk_application_new ("com.github.ToshioCP.tfv4", G_APPLICATION_HANDLES_OPEN);
67   g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
68   g_signal_connect (app, "open", G_CALLBACK (app_open), NULL);
69   stat = g_application_run (G_APPLICATION (app), argc, argv);
70   g_object_unref (app);
71   return stat;
72 }


ตัวอย่างหน้าจอที่รันโปรแกรม



ทีนี้มาดูสิ่งที่เปลี่ยนแปลงและเพิ่มเติมใน app_open โดยจะอธิบายแต่ละบรรทัดดังนี้ 

  • 11-13: ตัวแปร nblab และ nbp ถูกกำหนดขึ้นเพื่อใช้ใน GtkNotebook, GtkLabel และ GtkNotebookPage
  • 23: ชื่อวินโดว์เปลี่ยนเป็น "file viewer".
  • 25: กำหนดให้ขนาดวินโดว์ขยายใหญ่เต็มหน้าจอ
  • 27-28 GtkNotebook ถูกสร้างขึ้นและเพิ่มเข้าไปเป็น chile ใน  GtkApplicationWindow
  • 30-59 For-loop. โดยในลูปนี้จะอ่านชื่อไฟล์ที่ถูกส่งมาตอนรันโปรแกรม (arguments) และ files[i] เป็น GFile object บรรจุอาร์กิวเมนต์ลำดับที่ i
  • 32-37 GtkScrollledWindow, GtkTextView ถูกสร้างขึ้น และ GtkTextBuffer ถูกใช้ใน GtkTextView โดย GtkTextView จะถูกเพิ่มเข้าไปเป็น child ของ GtkScrolledWindow ในแต่ละไฟล์ก็จะถูกสร้างแบบนี้ซ้ำๆ จนกว่าจะหมดทุกไฟล์
  • 39-40 inserts the contents of the file into GtkTextBuffer and frees the memory pointed by contents.
  • 41-43: Gets the filename and creates GtkLabel with the filename and then frees filename.
  • 44-45: If filename is NULL, creates GtkLabel with the empty string.
  • 46: Appends GtkScrolledWindow as a page, with the tab GtkLabel, to GtkNotebook. At this time a GtkNoteBookPage widget is created automatically. The GtkScrolledWindow widget is connected to the GtkNotebookPage. Therefore, the structure is like this:
    GtkNotebook -- GtkNotebookPage -- GtkScrolledWindow
  • 47: Gets GtkNotebookPage widget and sets nbp to point to this GtkNotebookPage.
  • 48: GtkNotebookPage has a property set called "tab-expand". If it is set to TRUE then the tab expands horizontally as long as possible. If it is FALSE, then the width of the tab is determined by the size of the label. g_object_set is a general function to set properties in objects. See GObject API Reference, g_object_set.
  • 49-51: If the file cannot be read, "No such file" message is displayed and the filename buffer is freed.
  • 52-53: If filename is NULL, the "No valid file is given" message is outputted.
  • 55-58: If at least one file was read, then the number of GtkNotebookPage is greater than zero. If it's true, it shows the window. If it's false, it destroys the window, which causes the program to quit.



วันพุธที่ 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 ในบทความที่ผ่านมาเราสร้างโปรแกรมอ่านไฟล์ชนิดข้อความ และในบทความนี้ก็จะมาปรับแต่งโปรแกรมกันสักหน...