1 定义一个最终类
1.1 一个非常简单的编辑器
在上一节中,我们创建了一个非常简单的文件查看器。现在我们继续重写它,并将其转换为非常简单的编辑器。它的源文件是tfe目录下的tfe1.c(文本文件编辑器1)。
GtkTextView是一个多行编辑器。因此,我们不需要从头开始编写编辑器。我们只需要在文件查看器中添加两件事:
- 指向GFile实例的指针。
- 文本保存功能。
有两种方法可以存储指针。
- 使用全局变量
- 创建一个GtkTextView的子类,它的每个实例都保存一个指向GFile实例的指针。
使用全局变量很容易实现。定义一个指向GFile的指针数组。例如,
GFile * f [20];
变量f[i]对应于与第i个GtkNotebookPage关联的文件。
然而,有两个问题。第一个是数组的大小。如果用户提供了太多的参数(在上面的例子中超过20个),则不可能存储所有指向GFile实例的指针。二是程序维护困难。到目前为止,我们有一个小程序。但是,你开发的程序越多,它的规模就越大。一般来说,在大型程序中维护全局变量是非常困难的。在检查全局变量时,需要检查使用该变量的所有代码。
从维护的角度来看,创建子类是个好主意。我们更喜欢它而不是一个全局变量。
请注意,我们考虑的是“子类”,而不是“子构件”。子类和子部件完全不同。类是GObject系统的一个术语。如果你不熟悉GObject,请参阅:
- GObject API reference
- GObject tutorial for beginners
子类继承了父类的一切,而且还扩展了它的性能。我们将TfeTextView定义为GtkTextView的一个子类。它拥有GtkTextView拥有的一切,并添加了一个指向GFile的指针。
1.2 如何定义一个GtkTextView的子类
你需要了解GObject系统约定。首先,看看下面的程序。
#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));
}
- TfeTextView分为两部分。Tfe和TextView。Tfe称为前缀(prefix)或命名空间(namespace)。TextView被称为object。
有三种不同的标识符模式。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宏之前。
- G_DECLARE_FINAL_TYPE宏的参数是驼峰式的子类名、小写带下划线、前缀(大写)、对象(大写带下划线)和父类名(驼峰式)。在该宏的扩展中声明了下列两个C语言结构。
- typedef struct _TfeTextView TfeTextView
- typedef struct {GtkTextViewClass parent_class;} TfeTextViewClass;
这些声明告诉我们TfeTextView和TfeTextViewClass是C结构体。“TfeTextView”有两个含义,类名和C结构名。C语言结构体TfeTextView被称为object。类似地,TfeTextViewClass被称为class。
- 声明结构体_TfeTextView。下划线是必要的。第一个成员是父对象(C结构)。注意,这不是一个指针,而是对象本身。第二个成员和后面的成员是子对象的成员。TfeTextView结构有一个指向GFile实例的指针作为成员。
- G_DEFINE_TYPE宏。参数是子对象名称(驼峰大小写),带有下划线的小写和父对象类型(prefix)TYPE(module)。这个宏主要用于向类型系统注册新类。类型系统是GObject的基础系统。每个类都有自己的类型。GObject、GtkWidget和TfeTextView的类型分别是G_TYPE_OBJECT、GTK_TYPE_WIDGET和TFE_TYPE_TEXT_VIEW。这样的类型(例如,TFE_TYPE_TEXT_VIEW)是一个宏,它被扩展为一个函数(tfe_text_view_get_type())。它返回一个在所有GObject系统类中唯一的整数。
- 实例init函数(tfe_text_view_init)在创建实例时被调用。它与其他面向对象语言中的构造函数相同。
类init函数(tfe_text_view_class_init)在创建类时被调用。 - 两个函数tfe_text_view_set_file和tfe_text_view_get_file是公共函数。公共函数是开放的,你可以在任何地方调用它们。它们与其他面向对象语言中的public方法相同。tv是一个指向TfeTextView对象(C结构)的指针。它有一个成员file,它是被tv->file指向。
- TfeTextView实例创建函数是tfe_text_view_new。它的名字是(prefix)_(object)_new。它使用g_object_new函数创建实例。参数是(prefix)TYPE(object)、初始化属性的列表和NULL。NULL是属性列表的结束标记。这里没有初始化任何属性。返回值转换为GtkWidget。
这个程序展示了如何定义子类的大纲。
1.3 Close-request信号
假设您正在使用这个编辑器。首先,带着参数运行编辑器。参数是文件名。编辑器读取文件并显示一个包含文件文本的窗口。然后编辑文本。完成编辑后,退出编辑器。编辑器会在窗口关闭前更新文件。
GtkWindow在关闭之前发出"close-request"信号。我们将在before_close之前连接信号和处理程序。(处理程序是一个连接到信号的C函数。)before_close函数会在发出"close-request"信号时被调用。
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, GtkWidget *nb) {
3 GtkWidget *scr;
4 GtkWidget *tv;
5 GFile *file;
6 char *pathname;
7 GtkTextBuffer *tb;
8 GtkTextIter start_iter;
9 GtkTextIter end_iter;
10 char *contents;
11 unsigned int n;
12 unsigned int i;
13
14 n = gtk_notebook_get_n_pages (GTK_NOTEBOOK (nb));
15 for (i = 0; i < n; ++i) {
16 scr = gtk_notebook_get_nth_page (GTK_NOTEBOOK (nb), i);
17 tv = gtk_scrolled_window_get_child (GTK_SCROLLED_WINDOW (scr));
18 file = tfe_text_view_get_file (TFE_TEXT_VIEW (tv));
19 tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
20 gtk_text_buffer_get_bounds (tb, &start_iter, &end_iter);
21 contents = gtk_text_buffer_get_text (tb, &start_iter, &end_iter, FALSE);
22 if (! g_file_replace_contents (file, contents, strlen (contents), NULL, TRUE, G_FILE_CREATE_NONE, NULL, NULL, NULL)) {
23 if ((pathname = g_file_get_path (file)) != NULL) {
24 g_printerr ("Can't save %s.", pathname);
25 g_free (pathname);
26 } else
27 g_printerr ("Pathname not exist.\n");
28 }
29 g_free (contents);
30 g_object_unref (file);
31 }
32 return FALSE;
33 }
项目左边的数字是源代码中的行号。
- 14:将nb的总页数赋给n。
- 15-31:针对每个页面的索引进行for循环。
- 16-18: scr、tv和file被赋值给指向GtkScrolledWindow、TfeTextView和GFile的指针。当app_open处理程序被调用时,TfeTextView的GFile会被赋值(set)。稍后展示。
19-21: tb被分配给TfeTextView的GtkTextBuffer。使用迭代器访问缓冲区。迭代器指向缓冲区中的某个位置。函数gtk_text_buffer_get_bounds将缓冲区的开始和结束分别分配给start_iter和end_iter。然后函数gtk_text_buffer_get_text返回start_iter和end_iter之间的文本,即缓冲区中的整个文本。 - 22 ~ 28:文本保存到文件中。如果失败,则显示错误消息。
- 29:释放存储内容的内存。
- 30: GFile此时已经没用。g_object_unref减少GFile的引用计数。引用计数将在后面一节中解释。在这个程序中,引用计数将为0,GFile实例将销毁自己。(TfeTextView类没有定了处理file内存的函数,所以要自己unref)
1.4 Source code of tfe1.c
/* filename: tfe1.c */
1 #include <gtk/gtk.h>
2
3 /* Define TfeTextView Widget which is the child class 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 tv->file = NULL;
19 }
20
21 static void
22 tfe_text_view_class_init (TfeTextViewClass *class) {
23 }
24
25 void
26 tfe_text_view_set_file (TfeTextView *tv, GFile *f) {
27 tv->file = f;
28 }
29
30 GFile *
31 tfe_text_view_get_file (TfeTextView *tv) {
32 return tv -> file;
33 }
34
35 GtkWidget *
36 tfe_text_view_new (void) {
37 return GTK_WIDGET (g_object_new (TFE_TYPE_TEXT_VIEW, NULL));
38 }
39
40 /* ---------- end of the definition of TfeTextView ---------- */
41
42 static gboolean
43 before_close (GtkWindow *win, GtkWidget *nb) {
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 if ((pathname = g_file_get_path (file)) != NULL) {
65 g_printerr ("Can't save %s.", pathname);
66 g_free (pathname);
67 } else
68 g_printerr ("Pathname not exist.\n");
69 }
70 g_free (contents);
71 g_object_unref (file);
72 }
73 return FALSE;
74 }
75
76 static void
77 app_activate (GApplication *app) {
78 g_print ("You need to give filenames as arguments.\n");
79 }
80
81 static void
82 app_open (GApplication *app, GFile ** files, gint n_files, gchar *hint) {
83 GtkWidget *win;
84 GtkWidget *nb;
85 GtkWidget *lab;
86 GtkNotebookPage *nbp;
87 GtkWidget *scr;
88 GtkWidget *tv;
89 GtkTextBuffer *tb;
90 char *contents;
91 gsize length;
92 char *filename;
93 int i;
94
95 win = gtk_application_window_new (GTK_APPLICATION (app));
96 gtk_window_set_title (GTK_WINDOW (win), "file editor");
97 gtk_window_set_default_size (GTK_WINDOW (win), 600, 400);
98
99 nb = gtk_notebook_new ();
100 gtk_window_set_child (GTK_WINDOW (win), nb);
101
102 for (i = 0; i < n_files; i++) {
103 if (g_file_load_contents (files[i], NULL, &contents, &length, NULL, NULL)) {
104 scr = gtk_scrolled_window_new ();
105 tv = tfe_text_view_new ();
106 tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
107 gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (tv), GTK_WRAP_WORD_CHAR);
108 gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scr), tv);
109
110 tfe_text_view_set_file (TFE_TEXT_VIEW (tv), g_file_dup (files[i]));
111 gtk_text_buffer_set_text (tb, contents, length);
112 g_free (contents);
113 filename = g_file_get_basename (files[i]);
114 lab = gtk_label_new (filename);
115 gtk_notebook_append_page (GTK_NOTEBOOK (nb), scr, lab);
116 nbp = gtk_notebook_get_page (GTK_NOTEBOOK (nb), scr);
117 g_object_set (nbp, "tab-expand", TRUE, NULL);
118 g_free (filename);
119 } else if ((filename = g_file_get_path (files[i])) != NULL) {
120 g_print ("No such file: %s.\n", filename);
121 g_free (filename);
122 } else
123 g_print ("No valid file is given\n");
124 }
125 if (gtk_notebook_get_n_pages (GTK_NOTEBOOK (nb)) > 0) {
126 g_signal_connect (win, "close-request", G_CALLBACK (before_close), nb);
127 gtk_window_present (GTK_WINDOW (win));
128 } else
129 gtk_window_destroy (GTK_WINDOW (win));
130 }
131
132 int
133 main (int argc, char **argv) {
134 GtkApplication *app;
135 int stat;
136
137 app = gtk_application_new ("com.github.ToshioCP.tfe1", G_APPLICATION_HANDLES_OPEN);
138 g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
139 g_signal_connect (app, "open", G_CALLBACK (app_open), NULL);
140 stat =g_application_run (G_APPLICATION (app), argc, argv);
141 g_object_unref (app);
142 return stat;
143 }
- 110: TfeTextView的GFile指针设置为files[i],这是一个用命令行参数创建的GFile。但是GFile稍后会被系统销毁。所以它需要在赋值之前被复制。g_file_dup复制GFile文件。
- 126:“close-reques”信号连接到before_close处理程序。第四个参数被称为“用户数据”,它将是信号处理程序的第二个参数。因此,nb被赋给before_close作为第二个参数。
现在是时候编译和运行了。
$ cd src/tfe
$ comp tfe1
$ ./a.out taketori.txt`.
修改内容并关闭窗口。确保文件已被修改。
现在我们有了一个非常简单的编辑器。这并不明智。我们需要更多的功能,如打开,保存,保存,更改字体等。我们将在下一节和之后添加它们。