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