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