วันอังคารที่ 15 มีนาคม พ.ศ. 2565

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

Defining a Child object

A Very Simple Editor

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

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

  • เพิ่มพอยเตอร์ให้กับ GFile
  • เพิ่มฟังค์ชันสำหรับบันทึกไฟล์

แนวทางในการเก็บรายละเอียดของ GFile คือ

  • ใช้ตัวแปร global หรือ
  • สร้าง child object ที่จะสืบทอดอินสแตนซ์สำหรับ GFile object

การใช้ ตัวแปรชนิด global เป็นแนวทางที่ง่าย โดยกำหนดขนาด array แล้วชี้ไปที่ GFile ตัวอย่างเช่น

GFile *f[20];

ตัวแปร f[i] จะสอดคล้องกับการเชื่อมโยงไฟล์ในลำดับ i ของ GtkNoteBookPage อย่างไรก็ตามมักจะมีปัญหาสองประการ คือ อันดับแรกเกี่ยวกับขนาดของอะเรย์ ถ้าเรากำหนดจำนวนอะเรย์มาก (มากกว่า 20) จะไม่สามารถจัดเก็บพอยน์เตอร์เพิ่มเติมไปยังอินสแตนซ์ GFile 

ประการที่สองคือความยากที่เพิ่มขึ้นในการบำรุงรักษาโปรแกรม ถ้าเป็นโปรแกรมเล็กๆ เราก็สามารถไปแก้ไขตัวแปรได้ แต่ถ้าระบบใหญ่ขึ้นมากๆ การไล่หาตัวแปรเพื่อแก้ไขก็ยากยิ่งขึ้น และการเปลี่ยนแปลงของตัวแปรก็เกิดขึ้นได้หลายจุด บางครั้งกว่าจะดีบักเจอจุดที่มีการเปลี่ยนแปลงค่าก็ต้องเสียเวลานานๆ 

การสร้าง child object เป็นแนวทางที่ดีในการดูแลรักษาระบบ แต่สิ่งหนึ่งที่ต้องระวัง คือ ข้อแตกต่างระหว่าง child object กับ child widget ในที่นี้จะอธิบาย child object ที่สร้างและสืบทอด รวมถึงขยายคุณสมบัติจาก parent object 

Child object of GtkTextView

เราจะกำหนด TfeTextView เป็น child object ของ GtkTextView โดยมีคุณสมบัติเหมือน GtkTextView ทุกประการ ใน TfeTextView ก็ยังมี GtkTextBuffer ที่สอดคล้องกันกับ GtkTextView แต่ที่พิเศษเพิ่มเข้ามา คือ TfeTextView มีพอยเตอร์ไปยัง GFile

นี่คือหลักการทำงานของ GObjects แต่การพยายามทำความเข้าใจเกี่ยวกับ Gobject นั้นยากสำหรับมือใหม่ ในข้อเขียนนี้จะพยายามหลีกเลี่ยงเกี่ยวกับทฤษฏี แต่หากอยากศึกษาเพิ่มเติมสามารถเข้าไปดูที่ (https://github.com/ToshioCP/Gobject-tutorial).

How to Define a Child Object of GtkTextView

วิธีการกำหนดออบเจ็กต์ TfeTextView ให้เป็น child object ของ GtkTextView นั้น อันดับแรกให้ดูตัวอย่างโค้ดด้านล่างนี้

#define TFE_TYPE_TEXT_VIEW tfe_text_view_get_type ()
G_DECLARE_FINAL_TYPE (TfeTextView, tfe_text_view, TFE, TEXT_VIEW, GtkTextView)

struct _TfeTextView
{
  GtkTextView parent;
  GFile *file;
};

G_DEFINE_TYPE (TfeTextView, tfe_text_view, GTK_TYPE_TEXT_VIEW);

static void
tfe_text_view_init (TfeTextView *tv) {
}

static void
tfe_text_view_class_init (TfeTextViewClass *class) {
}

void
tfe_text_view_set_file (TfeTextView *tv, GFile *f) {
  tv -> file = f;
}

GFile *
tfe_text_view_get_file (TfeTextView *tv) {
  return tv -> file;
}

GtkWidget *
tfe_text_view_new (void) {
  return GTK_WIDGET (g_object_new (TFE_TYPE_TEXT_VIEW, NULL));
}

ถ้าอยากรู้เกี่ยวกับพื้นฐานเกี่ยวกับ GObject ที่เป็นส่วนสำคัญของ Gtk นั้นให้ดูได้ที่ GObject API Reference. และทั้งหมดที่ควรรู้นั้นมีอยู่ใน GObject tutorial 

แต่สำหรับมือใหม่ก็ยังไม่จำเป็นต้องรู้ทั้งหมดเกี่ยวกับ GObject เพียงแต่ทำความเข้าใจในสิ่งต่อไปนี้

  • TfeTextView แบ่งออกเป็นสองส่วน คือ Tfe และ TextView. Tfe เรียกว่า prefix, namespace หรือ module. TextView เรียกว่า object
  • มีรูปแบบแตกต่างกัน 3 อย่าง TfeTextView (camel case), tfe_text_view (snake case แบบนี้นิยมใช้เขียนชื่อฟังค์ชัน) and TFE_TEXT_VIEW (ใช้ cast จากพอยเตอร์ไปยัง TfeTextView type)
  • อันดับแรก กำหนดให้ TFE_TYPE_TEXT_VIEW เป็น macro สำหรับ tfe_text_view_get_type () ชื่อจะมีรูปแบบเป็น (prefix)_TYPE_(object) เสมอ และเป็นอักขระตัวใหญ่  และฟังค์ชันที่จะมาแทนมาโครต้องเป็นตัวอักษรตัวเล็ก และมีรูปแบบเป็น (prefix)_(object)_get_type ()
  • ต่อมา ใช้มาโคร G_DECALRE_FINAL_TYPE โดยอาร์กิวเมนต์เป็น ชื่อ child object (camel case), child object (snake case), prefix (upper case), object (uppercase) และ ชื่อ parent object (camel case) ตัวอย่างเช่น   G_DECLARE_FINAL_TYPE (TfeTextView, tfe_text_view, TFE, TEXT_VIEW, GtkTextView)
  • กำหนด structure ชื่อ _TfeTextView โดยจำเป็นต้องใส่เครื่องหมาย _ ด้านหน้าด้วย โดยสมารชิกแรกจะเป็น parent object ตรงนี้ไม่ใช่ pointer แต่เป็น object ด้วยตัวมันเอง อันดับสองเป็น child object, จากสตรัคเจอร์นี้ คือ สตรัคเจอร์ TfeTextView จะชี้ไปยังอินสแตนซ์ของ GFile 
  • ใช้มาโคร G_DEFINE_TYPE  โดยกำหนดอาร์กิวเมนต์ประกอบด้วย child object (camel case), child object (snake case), parent object แสดงในรูปแบบ (prefix)_TYPE_(module) ตัวอย่างเช่น G_DEFINE_TYPE (TfeTextView, tfe_text_view, GTK_TYPE_TEXT_VIEW);
  • กำหนดอินสแตนซ์ init function (tfe_text_view_init) โดยปกติเราไม่ต้องทำอะไรในนี้
  • กำหนดคลาส init function (tfe_text_view_class_init) ไม่ต้องทำอะไรภายในนี้เช่นกัน
  • เขียนฟังค์ชันสำหรับคำสั่งโปรแกรมที่ต้องการไว้ใน tfe_text_view_set_file และ tfe_text_view_get_file และ tv คือ pointer ชี้ไปยังอินสแตนซ์ TfeTextView, ใน stucture นี้มีสมาชิกชื่อ file ชี้ไปยังอินสแตนซ์ GFile, tv->file = f เป็นการกำหนดให้ f เป็นสมาชิกของ file ที่ชี้ไปยัง tv ซึ่งตัวอย่างนี้จะบอกวิธีการใช้ extend memory ใน child widget
  • เขียนฟังค์ชันเพื่อสร้างอินสแตนซ์ ให้ชื่อเป็น (prefix)_(object)_new ถ้าฟังค์ชันของ parent object ต้องการ parameter ในฟังค์ชันนี้ต้องมีด้วย ซึ่งเราอาจจะเพิ่ม parameter ในบางครั้ง, บางทีเราก็เลือกที่จะใช้ฟังค์ชัน g_object_new เพื่อสร้างอินสแตนซ์, อาร์กิวเมนต์ คือ (prefix)_TYPE_(object) และรายการเริ่มต้นของ properties จะเป็น NULL โดยในโค้ดนี้จะไม่ต้องการการเริ่มต้น (initialized) และค่าที่ส่งกลับต้อง cast ให้เป็น GtkWidget ด้วย

Close-request signal

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

GtkWindow จะส่งสัญญาน "close-request" ก่อนที่จะปิดตัวมันเอง เราจะเชื่อมสัญญานนี้ไปยัง handler ชื่อว่า before_close โดยเป็นฟังค์ชันในภาษา C และเมื่อฟังค์ชันเชื่อมต่อแล้วก็จะเรียกไปยัง handler หรือฟังค์ชันที่เราเขียนขึ้น และฟังค์ชัน before_close ก็จะถูกเรียกขึ้น 

g_signal_connect (win, "close-request", G_CALLBACK (before_close), NULL);

อาร์กิวเมนต์ win คือ GtkApplicationWindow และ signal ที่เราจะส่ง (emit) คือ close-request และ before_close เป็น handler โดย G_CALLBACK เป็นการ cast สำหรับ handler ซึ่งเป็น callback function โดยจะ cast ไปยัง handler ชื่อ before_close และ NULL จะไม่ส่งข้อมูลอะไรไปให้ฟังค์ชัน (user_data)

 1 static gboolean
 2 before_close (GtkWindow *win, gpointer user_data) {
 3   GtkWidget *nb = GTK_WIDGET (user_data);
 4   GtkWidget *scr;
 5   GtkWidget *tv;
 6   GFile *file;
 7   char *pathname;
 8   GtkTextBuffer *tb;
 9   GtkTextIter start_iter;
10   GtkTextIter end_iter;
11   char *contents;
12   unsigned int n;
13   unsigned int i;
14 
15   n = gtk_notebook_get_n_pages (GTK_NOTEBOOK (nb));
16   for (i = 0; i < n; ++i) {
17     scr = gtk_notebook_get_nth_page (GTK_NOTEBOOK (nb), i);
18     tv = gtk_scrolled_window_get_child (GTK_SCROLLED_WINDOW (scr));
19     file = tfe_text_view_get_file (TFE_TEXT_VIEW (tv));
20     tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
21     gtk_text_buffer_get_bounds (tb, &start_iter, &end_iter);
22     contents = gtk_text_buffer_get_text (tb, &start_iter, &end_iter, FALSE);
23     if (! g_file_replace_contents (file, contents, strlen (contents), NULL, TRUE, G_FILE_CREATE_NONE, NULL, NULL, NULL)) {
24       pathname = g_file_get_path (file);
25       g_print ("ERROR : Can't save %s.", pathname);
26       g_free (pathname);
27     }
28     g_free (contents);
29   }
30   return FALSE;
31 }

บรรทัดต่อไปนี้เป็นรายละเอียดที่ควรทำความเข้าใจ

  • 15: หาจำนวนแท็บทั้งหมดที่ nb มี
  • 16-29: วนลูปตามจำนวน index ของแต่ละหน้า
  • 17-19: หา GtkScrolledWindow, TfeTextView และพอยเตอร์ที่ชี้ไปยัง GFile โดย pointer จะถูกสร้างใน handler ชื่อ app_open 
  • 20-22: หา GtkTextBuffer และข้อมูลภายใน start_iter และ end_iter เป็นตัวบ่งชี้ภายใน buffer 
  • 23-27: เขียนเนื้อหาลงในไฟล์ ถ้าไม่สำเร็จให้แสดงข้อความ Error 
  • 28: ยกเลิกการจองหน่วยความจำ contents.

Source code of tfe1.c

  1 #include <gtk/gtk.h>
  2 
  3 /* Define TfeTextView Widget which is the child object of GtkTextView */
  4 
  5 #define TFE_TYPE_TEXT_VIEW tfe_text_view_get_type ()
  6 G_DECLARE_FINAL_TYPE (TfeTextView, tfe_text_view, TFE, TEXT_VIEW, GtkTextView)
  7 
  8 struct _TfeTextView
  9 {
 10   GtkTextView parent;
 11   GFile *file;
 12 };
 13 
 14 G_DEFINE_TYPE (TfeTextView, tfe_text_view, GTK_TYPE_TEXT_VIEW);
 15 
 16 static void
 17 tfe_text_view_init (TfeTextView *tv) {
 18 }
 19 
 20 static void
 21 tfe_text_view_class_init (TfeTextViewClass *class) {
 22 }
 23 
 24 void
 25 tfe_text_view_set_file (TfeTextView *tv, GFile *f) {
 26   tv -> file = f;
 27 }
 28 
 29 GFile *
 30 tfe_text_view_get_file (TfeTextView *tv) {
 31   return tv -> file;
 32 }
 33 
 34 GtkWidget *
 35 tfe_text_view_new (void) {
 36   return GTK_WIDGET (g_object_new (TFE_TYPE_TEXT_VIEW, NULL));
 37 }
 38 
 39 /* ---------- end of the definition of TfeTextView ---------- */
 40 
 41 static gboolean
 42 before_close (GtkWindow *win, gpointer user_data) {
 43   GtkWidget *nb = GTK_WIDGET (user_data);
 44   GtkWidget *scr;
 45   GtkWidget *tv;
 46   GFile *file;
 47   char *pathname;
 48   GtkTextBuffer *tb;
 49   GtkTextIter start_iter;
 50   GtkTextIter end_iter;
 51   char *contents;
 52   unsigned int n;
 53   unsigned int i;
 54 
 55   n = gtk_notebook_get_n_pages (GTK_NOTEBOOK (nb));
 56   for (i = 0; i < n; ++i) {
 57     scr = gtk_notebook_get_nth_page (GTK_NOTEBOOK (nb), i);
 58     tv = gtk_scrolled_window_get_child (GTK_SCROLLED_WINDOW (scr));
 59     file = tfe_text_view_get_file (TFE_TEXT_VIEW (tv));
 60     tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
 61     gtk_text_buffer_get_bounds (tb, &start_iter, &end_iter);
 62     contents = gtk_text_buffer_get_text (tb, &start_iter, &end_iter, FALSE);
 63     if (! g_file_replace_contents (file, contents, strlen (contents), NULL, TRUE, G_FILE_CREATE_NONE, NULL, NULL, NULL)) {
 64       pathname = g_file_get_path (file);
 65       g_print ("ERROR : Can't save %s.", pathname);
 66       g_free (pathname);
 67     }
 68     g_free (contents);
 69   }
 70   return FALSE;
 71 }
 72 
 73 static void
 74 app_activate (GApplication *app, gpointer user_data) {
 75   g_print ("You need to give filenames as arguments.\n");
 76 }
 77 
 78 static void
 79 app_open (GApplication *app, GFile ** files, gint n_files, gchar *hint, gpointer user_data) {
 80   GtkWidget *win;
 81   GtkWidget *nb;
 82   GtkWidget *lab;
 83   GtkNotebookPage *nbp;
 84   GtkWidget *scr;
 85   GtkWidget *tv;
 86   GtkTextBuffer *tb;
 87   char *contents;
 88   gsize length;
 89   char *filename;
 90   int i;
 91 
 92   win = gtk_application_window_new (GTK_APPLICATION (app));
 93   gtk_window_set_title (GTK_WINDOW (win), "file editor");
 94   gtk_window_maximize (GTK_WINDOW (win));
 95 
 96   nb = gtk_notebook_new ();
 97   gtk_window_set_child (GTK_WINDOW (win), nb);
 98 
 99   for (i = 0; i < n_files; i++) {
100     if (g_file_load_contents (files[i], NULL, &contents, &length, NULL, NULL)) {
101       scr = gtk_scrolled_window_new ();
102       tv = tfe_text_view_new ();
103       tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
104       gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (tv), GTK_WRAP_WORD_CHAR);
105       gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scr), tv);
106 
107       tfe_text_view_set_file (TFE_TEXT_VIEW (tv),  g_file_dup (files[i]));
108       gtk_text_buffer_set_text (tb, contents, length);
109       g_free (contents);
110       filename = g_file_get_basename (files[i]);
111       lab = gtk_label_new (filename);
112       gtk_notebook_append_page (GTK_NOTEBOOK (nb), scr, lab);
113       nbp = gtk_notebook_get_page (GTK_NOTEBOOK (nb), scr);
114       g_object_set (nbp, "tab-expand", TRUE, NULL);
115       g_free (filename);
116     } else if ((filename = g_file_get_path (files[i])) != NULL) {
117         g_print ("No such file: %s.\n", filename);
118         g_free (filename);
119     } else
120         g_print ("No valid file is given\n");
121   }
122   if (gtk_notebook_get_n_pages (GTK_NOTEBOOK (nb)) > 0) {
123     g_signal_connect (win, "close-request", G_CALLBACK (before_close), nb);
124     gtk_widget_show (win);
125   } else
126     gtk_window_destroy (GTK_WINDOW (win));
127 }
128 
129 int
130 main (int argc, char **argv) {
131   GtkApplication *app;
132   int stat;
133 
134   app = gtk_application_new ("com.github.ToshioCP.tfe1", G_APPLICATION_HANDLES_OPEN);
135   g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
136   g_signal_connect (app, "open", G_CALLBACK (app_open), NULL);
137   stat =g_application_run (G_APPLICATION (app), argc, argv);
138   g_object_unref (app);
139   return stat;
140 }
  • 107: กำหนด ตัวชี้ ชี้ไปยัง GFile ใน TfeTextView. files[i] เป็นพอยเตอร์ไปยัง GFile structure และจะถูก freed โดยระบบ ดังนั้นเราต้องคัดลอกโดยใช้ g_file_dup เพื่อคัดลอก GFile structure 
  • 123: เชื่อม signal ชื่อ "close-request" ไปยัง before_close โดยอาร์กิวเมนต์ตัวที่ 4 เป็น user_data ที่จะส่งไปให้ handler หรือฟังค์ชันรองรับ โดย nb ที่ส่งให้before_close เป็นอาร์กิวเมนต์ลำดับที่ 2

สรุปง่ายๆ

1. ประกาศ Object ด้วยคำสั่ง G_DECLARE_FINAL_TYPE ดังนั้น

G_DECLARE_FINAL_TYPE(MyObject, my_object, MY, OBJECT, GObject)

โดยการประกาศค่าจะต้องสร้าง MyObject และMyObjectClass โดย MyObject จะสร้างในโค้ด C ของเราส่วน MyObjectClass จะถูกสร้างขึ้นจากมาโคร


2. ใช้มาโครมากำหนดฟังค์ชันต่างๆ เช่น MY_OBJECT() MY_OBJECT_CLASS() สำหรับ casting และ MY_IS_OBJECT() และ MY_ISOBJECT_CLASS() สำหรับตรวจสอบ และ MY_OBJECT_GET_CLASS() สำหรับเรียกดูคลาสสตรักเจอร์จากอินสแตนซ์

#define MY_TYPE_OBJECT my_object_get_type()


3. สร้าง Object โดยใช้ struct และ G_DEFINE_TYPE()

struct _MyObject{

  GObject parent_instance;

  /* other member */

G_DEFINE_TYPE(MyObject, my_object, G_TYPE_OBJECT)


4. สร้าง Static ฟังค์ชันรองรับ

static void

my_object_class_init(MyObjectClass *klass){

}

static void

my_object_init(MyObject *self){

}

สองฟังค์ชันนี้ต้องมีและระบบจะเรียกสองตัวนี้ขณะที่สร้างเสมอ


5. สร้าง Constructor 

GObject *

my_object_new(void){

}


การใช้งาน

GObject *myobj = my_object_new();

การยกเลิก

g_object_unref(myobj);


แค่นี้แหละ เดี๋ยวต่อไปจะหาวิธีการสร้าง Object แบบต่างๆ มานำเสนออีกครั้งหนึ่ง

วันอาทิตย์ที่ 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.



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

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