Skip to content

Latest commit

 

History

History
300 lines (257 loc) · 13.8 KB

sec08.md

File metadata and controls

300 lines (257 loc) · 13.8 KB

主页:教程介绍,上一节:第07节,下一节:第09节

8 定义子对象

一个简单的编辑器

在上一节中,我们制作了一个非常简单的文件查看器,现在我们继续重写它,把它变成非常简单的编辑器,它的源文件在 tfe1.c(text file editor 1)中。

GtkTextView 具有多行编辑功能。因此,我们不需要从头开始编写程序,我们只需在文件查看器中添加两个东西:

  • 指向 GFile 实例的指针
  • 用于写入文件的函数

有几种方法可以存储 GFile 的详细信息,例如:

  • 使用全局变量
  • 创建一个子对象,并在子对象中保存 GFile 实例的指针

使用全局变量很容易实现,例如定义一个足够大的指向 GFile 的指针数组:

GFile *f[20];

变量 f[i] 对应于与第 i 个 GtkNotebookPage 关联的文件,然而这有两个问题。第一个是数组的大小问题,如果用户提供了太多参数(上例中超过 20 个),则无法存储额外的指向 GFile 实例的指针。二是程序维护难度加大。到目前为止,我们的程序很简单,但是如果继续开发它,程序的大小将会增长。一般来说,程序越大,跟踪和维护全局变量就越困难,全局变量可以在整个程序的任何地方使用和更改。

就维护而言,创建子对象是一个不错的方法(如果你了解面向对象编程,应该很容易理解本节的内容)。您需要注意的一件事是 “子对象” 和 “子控件”之间的区别,子对象包含并扩展其父对象,子控件只涉及UI上的包含关系。

Child object of GtkTextView

我们将 TfeTextView 定义为 GtkTextView 的子对象,它拥有 GtkTextView 所拥有的一切。具体来说,TfeTextView 有一个 GtkTextbuffer,它对应于 TfeTextView 内部的 GtkTextView。另一个重要的事情是 TfeTextView 还有一个额外的指向 GFile 的指针。

这就是 GObjects 的工作方式,理解 Gobject 的理论比较困难的,特别是对于初学者。因此,我将向您展示如何编写代码并避免理论方面的问题。如果您想了解 GObject 系统,请参阅单独的教程

如何定义 GtkTextView 的子对象

让我们定义 TfeTextView 对象,它是 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 API Reference 或参考 GObject教程。对于初学者来说,不需要知道太多关于 GObject 的知识,只需记住以下说明就足够了:

  • TfeTextView 分为两部分:Tfe 和 TextView。Tfe 被称为前缀、命名空间或模块,TextView 被称为对象。
  • 有三种不同的标识符模式。TfeTextView(驼峰式)、tfe_text_view(这个用来写函数)和TFE_TEXT_VIEW(这个用来转换指向TfeTextView类型的指针)
  • 首先,将 TFE_TYPE_TEXT_VIEW 宏定义为 tfe_text_view_get_type(),名称始终为 (prefix)_TYPE_(object) 且字母为大写。替换文本始终是 (prefix)_(object)_get_type () 并且字母是小写的
  • 接下来,使用 G_DECLARE_FINAL_TYPE 宏,参数是驼峰式的子对象名称、带下划线的小写、前缀(大写)、对象(带下划线的大写)和父对象名称(驼峰式)
  • 声明结构体 _TfeTextView,下划线是必须的,第一个成员是父对象,请注意,这不是指针,而是对象本身,第二个成员和之后的成员是子对象的成员,TfeTextView 结构有一个指向 GFile 实例的指针作为成员
  • 使用 G_DEFINE_TYPE 宏。参数是驼峰大小写的子对象名称,带下划线的小写字母和父对象类型(前缀)_TYPE_(模块)
  • 定义实例初始化函数 (tfe_text_view_init),通常你不需要做任何事情
  • 定义类初始化函数(tfe_text_view_class_init),您无需在此对象中执行任何操作
  • 编写要添加的功能代码(tfe_text_view_set_file 和 tfe_text_view_get_file),tv 是一个指向 TfeTextView 对象实例的指针,它是一个用标签 _TfeTextView 声明的 C 结构,因此,该结构有一个成员 file 作为指向 GFile 实例的指针,tv->file = f 是将f 赋值给tv 指向的结构的成员file,这个例子显示了如何在子对象中扩展父对象的功能
  • 编写一个函数来创建一个实例,它的名字是(前缀)_(对象)_new,如果父对象函数需要参数,这个函数也需要它们,你也可以添加一些自己的参数,使用 g_object_new 函数创建实例,参数是 (prefix)_TYPE_(object)、初始化属性的列表和 NULL,在此代码中不需要初始化任何属性,并且返回值被强制转换为 GtkWidget。

这个程序并不完美,它有一些问题,稍后会进行修改。

close-request 信号

想象一下你正在使用这个编辑器,首先,您使用参数运行编辑器,参数是文件名。编辑器读取文件并显示包含文件文本的窗口,然后编辑文本。完成编辑后,退出编辑器,编辑器在窗口关闭之前更新文件。

GtkWindow 在关闭之前会发出 “close-request” 信号,我们可以将此信号和处理程序 “before_close” 进行连接,处理程序是一个 C 函数。当一个函数连接到某个信号时,我们称其为处理程序(handler)。发出信号 “close-request” 时调用函数 “before_close”。

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

参数 win 是一个 GtkApplicationWindow,其中定义了信号 “close-request”,before_close 是处理程序。G_CALLBACK 强制转换处理程序的类型为回调函数,before_close 的程序如下:

 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:关于每个页面的索引的 For 循环。
  • 17-19:获取 GtkScrolledWindow、TfeTextView 和指向 GFile 的指针,指针在 app_open 处理程序运行时存储,稍后会展示相关代码
  • 20-22: 获取 GtkTextBuffer 和 文件内容 “contents”。start_iterend_iter 是缓冲区的迭代器用于表示 buffer 中的位置,我现在不想解释它们,因为这需要很多时间,可以暂时记住这些术语。
  • 23-27:将 “contents” 写入文件,如果失败,则输出错误消息
  • 28:释放 “contents”

tfe1.c 代码

下面是完整的 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 结构的指针,它将被系统释放,所以你需要复制它。g_file_dup 复制给定的 GFile 结构。
  • 123:连接 “close-request” 信号和 “before_close” 处理程序,第四个参数称为用户数据,它被提供给信号处理程序,因此,nb 被作为第二个参数提供给 before_close

现在编译并运行它,tfe 目录中有一个示例文件,键入./a.out taketori.txt,修改内容并关闭窗口,确保文件已修改来测试关闭时的自动保存效果。

现在我们有了一个非常简单的编辑器,我们需要更多的功能,如打开、保存、另存为、更改字体等,我们将在下一节及之后添加这些功能。

主页:教程介绍,上一节:第07节,下一节:第09节