วันอังคารที่ 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 แบบต่างๆ มานำเสนออีกครั้งหนึ่ง

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

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

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

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