1 New, Open and Save button
在上一节中,我们制作了一个非常简单的编辑器。它在程序开始时读取文件,在程序结束时将文件写出来。它可以工作,但不是很好。如果我们有“新建”、“打开”、“保存”和“关闭”按钮就更好了。本节介绍如何在窗口中放置这些按钮。
上面的截图展示了布局。源代码tfe2.c中的app_open函数如下所示。
1 static void
2 app_open (GApplication *app, GFile ** files, gint n_files, gchar *hint) {
3 GtkWidget *win;
4 GtkWidget *nb;
5 GtkWidget *lab;
6 GtkNotebookPage *nbp;
7 GtkWidget *scr;
8 GtkWidget *tv;
9 GtkTextBuffer *tb;
10 char *contents;
11 gsize length;
12 char *filename;
13 int i;
14
15 GtkWidget *boxv;
16 GtkWidget *boxh;
17 GtkWidget *dmy1;
18 GtkWidget *dmy2;
19 GtkWidget *dmy3;
20 GtkWidget *btnn; /* button for new */
21 GtkWidget *btno; /* button for open */
22 GtkWidget *btns; /* button for save */
23 GtkWidget *btnc; /* button for close */
24
25 win = gtk_application_window_new (GTK_APPLICATION (app));
26 gtk_window_set_title (GTK_WINDOW (win), "file editor");
27 gtk_window_set_default_size (GTK_WINDOW (win), 600, 400);
28
29 boxv = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
30 gtk_window_set_child (GTK_WINDOW (win), boxv);
31
32 boxh = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
33 gtk_box_append (GTK_BOX (boxv), boxh);
34
35 dmy1 = gtk_label_new(NULL); /* dummy label for left space */
36 gtk_label_set_width_chars (GTK_LABEL (dmy1), 10);
37 dmy2 = gtk_label_new(NULL); /* dummy label for center space */
38 gtk_widget_set_hexpand (dmy2, TRUE);
39 dmy3 = gtk_label_new(NULL); /* dummy label for right space */
40 gtk_label_set_width_chars (GTK_LABEL (dmy3), 10);
41 btnn = gtk_button_new_with_label ("New");
42 btno = gtk_button_new_with_label ("Open");
43 btns = gtk_button_new_with_label ("Save");
44 btnc = gtk_button_new_with_label ("Close");
45
46 gtk_box_append (GTK_BOX (boxh), dmy1);
47 gtk_box_append (GTK_BOX (boxh), btnn);
48 gtk_box_append (GTK_BOX (boxh), btno);
49 gtk_box_append (GTK_BOX (boxh), dmy2);
50 gtk_box_append (GTK_BOX (boxh), btns);
51 gtk_box_append (GTK_BOX (boxh), btnc);
52 gtk_box_append (GTK_BOX (boxh), dmy3);
53
54 nb = gtk_notebook_new ();
55 gtk_widget_set_hexpand (nb, TRUE);
56 gtk_widget_set_vexpand (nb, TRUE);
57 gtk_box_append (GTK_BOX (boxv), nb);
58
59 for (i = 0; i < n_files; i++) {
60 if (g_file_load_contents (files[i], NULL, &contents, &length, NULL, NULL)) {
61 scr = gtk_scrolled_window_new ();
62 tv = tfe_text_view_new ();
63 tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
64 gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (tv), GTK_WRAP_WORD_CHAR);
65 gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scr), tv);
66
67 tfe_text_view_set_file (TFE_TEXT_VIEW (tv), g_file_dup (files[i]));
68 gtk_text_buffer_set_text (tb, contents, length);
69 g_free (contents);
70 filename = g_file_get_basename (files[i]);
71 lab = gtk_label_new (filename);
72 gtk_notebook_append_page (GTK_NOTEBOOK (nb), scr, lab);
73 nbp = gtk_notebook_get_page (GTK_NOTEBOOK (nb), scr);
74 g_object_set (nbp, "tab-expand", TRUE, NULL);
75 g_free (filename);
76 } else if ((filename = g_file_get_path (files[i])) != NULL) {
77 g_print ("No such file: %s.\n", filename);
78 g_free (filename);
79 } else
80 g_print ("No valid file is given\n");
81 }
82 if (gtk_notebook_get_n_pages (GTK_NOTEBOOK (nb)) > 0) {
83 gtk_window_present (GTK_WINDOW (win));
84 } else
85 gtk_window_destroy (GTK_WINDOW (win));
86 }
函数app_open在主应用程序窗口中构建构件。
- 25-27:创建一个GtkApplicationWindow实例,并设置标题和默认大小。
- 29-30:创建GtkBox实例boxv。它是一个垂直框,是GtkApplicationWindow的一个子构件。boxv有两个子构件。第一个子构件是一个水平的boxh。第二个构件是一个GtkNotebook。
- 32-33:创建一个GtkBox实例boxh,并将其作为第一个子构件附加到boxv中。
- 35-40:创建3个假标签。标签dmy1和dmy3的字符宽度为10。另一个标签dmy2具有hexpand属性,该属性被设置为TRUE。这使得标签尽可能地水平扩展。
- 41-44:创建四个按钮。
- 46-52:添加这些GtkLabel和GtkButton到boxh。
- 54-57:创建一个gtnotebook实例,并设置hexpand和vexpand属性为TRUE。这使得它在水平和垂直方向上尽可能扩展。它作为第二个子构件添加到boxv中。
构件件构建行的数量是33(=57-25+1)。我们还需要许多变量(boxv、boxh、dmy1等),其中大多数只用于构建窗口组件。有什么好的解决方案来减少这些工作吗?
Gtk提供GtkBuilder。它读取用户界面(UI)数据并构建窗口。它减少了这种繁琐的工作。
2 The UI File
Look at the UI file tfe3.ui that defines widget structure.
1 <?xml version="1.0" encoding="UTF-8"?>
2 <interface>
3 <object class="GtkApplicationWindow" id="win">
4 <property name="title">file editor</property>
5 <property name="default-width">600</property>
6 <property name="default-height">400</property>
7 <child>
8 <object class="GtkBox" id="boxv">
9 <property name="orientation">GTK_ORIENTATION_VERTICAL</property>
10 <child>
11 <object class="GtkBox" id="boxh">
12 <property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
13 <child>
14 <object class="GtkLabel" id="dmy1">
15 <property name="width-chars">10</property>
16 </object>
17 </child>
18 <child>
19 <object class="GtkButton" id="btnn">
20 <property name="label">New</property>
21 </object>
22 </child>
23 <child>
24 <object class="GtkButton" id="btno">
25 <property name="label">Open</property>
26 </object>
27 </child>
28 <child>
29 <object class="GtkLabel" id="dmy2">
30 <property name="hexpand">TRUE</property>
31 </object>
32 </child>
33 <child>
34 <object class="GtkButton" id="btns">
35 <property name="label">Save</property>
36 </object>
37 </child>
38 <child>
39 <object class="GtkButton" id="btnc">
40 <property name="label">Close</property>
41 </object>
42 </child>
43 <child>
44 <object class="GtkLabel" id="dmy3">
45 <property name="width-chars">10</property>
46 </object>
47 </child>
48 </object>
49 </child>
50 <child>
51 <object class="GtkNotebook" id="nb">
52 <property name="hexpand">TRUE</property>
53 <property name="vexpand">TRUE</property>
54 </object>
55 </child>
56 </object>
57 </child>
58 </object>
59 </interface>
是一个XML文件。标签以<
开始,以>
结束。标签有两种类型:开始标签和结束标签。例如,<interface>
是开始标签,</interface>
是结束标签。UI文件以interface标签开始和结束。有些标签,例如对象标签,可以在其开始标签中具有class和id属性。
- 1: XML声明。它指定XML版本为1.0,编码为UTF-8。
- 3-6:一个带有GtkApplicationWindow类和id=win的对象标签。这是顶层窗口。并定义了窗口的三个属性。title属性是"file editor", default-width属性是600,default-height属性是400。
- 7:child标签意味着子构件。例如,第7行告诉我们,id属性为"boxv"的GtkBox对象是win的一个子构件。
比较这个ui文件和tfe2.c源代码中的第25 ~ 57行。两者都使用其后代构件构建相同的窗口。
你可以使用gtk4-builder-tool检查ui文件。
gtk4-builder-tool validate <ui file name>
对ui文件进行验证。如果ui文件包含一些语法错误,gtk4-builder-tool会打印出错误。gtk4-builder-tool simplify <ui file name>
简化ui文件并打印结果。如果指定了--replace
选项,它会将ui文件替换为简化的文件。如果ui文件指定了一个属性值,但它是默认值,那么它将被删除。有些值被简化了。例如,“TRUE"和"FALSE"分别变成了"1"和"0”。然而,“TRUE或“FALSE”更利于维护。
在编译之前检查你的ui文件是个好主意。
3 GtkBuilder
GtkBuilder基于ui文件建造构件。
GtkBuilder *build;
build = gtk_builder_new_from_file ("tfe3.ui");
win = GTK_WIDGET (gtk_builder_get_object (build, "win"));
gtk_window_set_application (GTK_WINDOW (win), GTK_APPLICATION (app));
nb = GTK_WIDGET (gtk_builder_get_object (build, "nb"));
函数gtk_builder_new_from_file读取作为参数给出的文件。然后,它构建小构件并创建GtkBuilder对象。函数gtk_builder_get_object (build, “win”)返回指向属性name=win的指针,也就是ui文件中的id。所有部件都是基于ui文件中描述的父子关系连接的。对于下面的程序,我们只需要win和nb。这减少了C源文件中的行数。
$ cd tfe; diff tfe2.c tfe3.c
58a59
> GtkBuilder *build;
60,103c61,65
< GtkWidget *boxv;
< GtkWidget *boxh;
< GtkWidget *dmy1;
< GtkWidget *dmy2;
< GtkWidget *dmy3;
< GtkWidget *btnn; /* button for new */
< GtkWidget *btno; /* button for open */
< GtkWidget *btns; /* button for save */
< GtkWidget *btnc; /* button for close */
<
< win = gtk_application_window_new (GTK_APPLICATION (app));
< gtk_window_set_title (GTK_WINDOW (win), "file editor");
< gtk_window_set_default_size (GTK_WINDOW (win), 600, 400);
<
< boxv = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
< gtk_window_set_child (GTK_WINDOW (win), boxv);
<
< boxh = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
< gtk_box_append (GTK_BOX (boxv), boxh);
<
< dmy1 = gtk_label_new(NULL); /* dummy label for left space */
< gtk_label_set_width_chars (GTK_LABEL (dmy1), 10);
< dmy2 = gtk_label_new(NULL); /* dummy label for center space */
< gtk_widget_set_hexpand (dmy2, TRUE);
< dmy3 = gtk_label_new(NULL); /* dummy label for right space */
< gtk_label_set_width_chars (GTK_LABEL (dmy3), 10);
< btnn = gtk_button_new_with_label ("New");
< btno = gtk_button_new_with_label ("Open");
< btns = gtk_button_new_with_label ("Save");
< btnc = gtk_button_new_with_label ("Close");
<
< gtk_box_append (GTK_BOX (boxh), dmy1);
< gtk_box_append (GTK_BOX (boxh), btnn);
< gtk_box_append (GTK_BOX (boxh), btno);
< gtk_box_append (GTK_BOX (boxh), dmy2);
< gtk_box_append (GTK_BOX (boxh), btns);
< gtk_box_append (GTK_BOX (boxh), btnc);
< gtk_box_append (GTK_BOX (boxh), dmy3);
<
< nb = gtk_notebook_new ();
< gtk_widget_set_hexpand (nb, TRUE);
< gtk_widget_set_vexpand (nb, TRUE);
< gtk_box_append (GTK_BOX (boxv), nb);
<
---
> build = gtk_builder_new_from_file ("tfe3.ui");
> win = GTK_WIDGET (gtk_builder_get_object (build, "win"));
> gtk_window_set_application (GTK_WINDOW (win), GTK_APPLICATION (app));
> nb = GTK_WIDGET (gtk_builder_get_object (build, "nb"));
> g_object_unref(build);
138c100
< app = gtk_application_new ("com.github.ToshioCP.tfe2", G_APPLICATION_HANDLES_OPEN);
---
> app = gtk_application_new ("com.github.ToshioCP.tfe3", G_APPLICATION_HANDLES_OPEN);
144a107
>
60,103c61,65表示44(=103-60+1)行更改为5(=65-61+1)行。因此,减少了39行代码。使用ui文件不仅缩短了C源文件的长度,而且使部件的结构更加清晰。
现在我将展示C文件tfe3.c中的app_open函数。
1 static void
2 app_open (GApplication *app, GFile ** files, gint n_files, gchar *hint) {
3 GtkWidget *win;
4 GtkWidget *nb;
5 GtkWidget *lab;
6 GtkNotebookPage *nbp;
7 GtkWidget *scr;
8 GtkWidget *tv;
9 GtkTextBuffer *tb;
10 char *contents;
11 gsize length;
12 char *filename;
13 int i;
14 GtkBuilder *build;
15
16 build = gtk_builder_new_from_file ("tfe3.ui");
17 win = GTK_WIDGET (gtk_builder_get_object (build, "win"));
18 gtk_window_set_application (GTK_WINDOW (win), GTK_APPLICATION (app));
19 nb = GTK_WIDGET (gtk_builder_get_object (build, "nb"));
20 g_object_unref(build);
21 for (i = 0; i < n_files; i++) {
22 if (g_file_load_contents (files[i], NULL, &contents, &length, NULL, NULL)) {
23 scr = gtk_scrolled_window_new ();
24 tv = tfe_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_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scr), tv);
28
29 tfe_text_view_set_file (TFE_TEXT_VIEW (tv), g_file_dup (files[i]));
30 gtk_text_buffer_set_text (tb, contents, length);
31 g_free (contents);
32 filename = g_file_get_basename (files[i]);
33 lab = gtk_label_new (filename);
34 gtk_notebook_append_page (GTK_NOTEBOOK (nb), scr, lab);
35 nbp = gtk_notebook_get_page (GTK_NOTEBOOK (nb), scr);
36 g_object_set (nbp, "tab-expand", TRUE, NULL);
37 g_free (filename);
38 } else if ((filename = g_file_get_path (files[i])) != NULL) {
39 g_print ("No such file: %s.\n", filename);
40 g_free (filename);
41 } else
42 g_print ("No valid file is given\n");
43 }
44 if (gtk_notebook_get_n_pages (GTK_NOTEBOOK (nb)) > 0) {
45 gtk_window_present (GTK_WINDOW (win));
46 } else
47 gtk_window_destroy (GTK_WINDOW (win));
48 }
tfe3.c的整个源代码保存在目录src/tfe中。
4 使用ui字符串
GtkBuilder可以使用字符串建立构件。使用gtk_builder_new_from_string而不是gtk_builder_new_from_file。
char *uistring;
uistring =
"<interface>"
"<object class="GtkApplicationWindow" id="win">"
"<property name=\"title\">file editor</property>"
"<property name=\"default-width\">600</property>"
"<property name=\"default-height\">400</property>"
"<child>"
"<object class=\"GtkBox\" id=\"boxv\">"
"<property name="orientation">GTK_ORIENTATION_VERTICAL</property>"
... ... ...
... ... ...
"</interface>";
build = gtk_builder_new_from_string (uistring, -1);
这种方法有利有弊。这样做的好处是ui字符串是在源代码中编写的。因此,在运行时不需要ui文件。缺点是编写C字符串有点麻烦,因为有双引号。如果你想使用这个方法,你应该写一个脚本来把ui文件转换成C-string。
- 在每个双引号之前添加反斜杠。
- 在每行字符串的左右分别添加双引号。
5 Gresource
使用Gresource类似于使用string。但是Gresource是压缩的二进制数据,而不是文本数据。还有一个编译器可以将ui文件编译为Gresource。它不仅可以编译文本文件,还可以编译图像、声音等二进制文件。在编译之后,它将它们打包成一个Gresource对象。
resource编译器glib-compile-resources需要一个xml文件。它描述了资源文件。
1 <?xml version="1.0" encoding="UTF-8"?>
2 <gresources>
3 <gresource prefix="/com/github/ToshioCP/tfe3">
4 <file>tfe3.ui</file>
5 </gresource>
6 </gresources>
- 2: gresources标签可以包含多个gresources (gresource标签)。然而,这个xml只有一个gresource。
- 3: gresource的前缀为/com/github/ToshioCP/tfe3。
- 4: gresource的名字是tfe3.ui。资源指向/com/github/ToshioCP/tfe3/tfe3。ui由GtkBuilder。模式是“prefix”+“name”。如果想添加更多文件,请将它们插入第4行和第5行之间。
将这个xml文本保存到tfe3.gresource.xml。gresource编译器glib-compile-resources通过参数 --help显示其用法。
$ LANG=C glib-compile-resources --help
Usage:
glib-compile-resources [OPTION?] FILE
Compile a resource specification into a resource file.
Resource specification files have the extension .gresource.xml,
and the resource file have the extension called .gresource.
Help Options:
-h, --help Show help options
Application Options:
--version Show program version and exit
--target=FILE Name of the output file
--sourcedir=DIRECTORY The directories to load files referenced in FILE from (default: current directory)
--generate Generate output in the format selected for by the target filename extension
--generate-header Generate source header
--generate-source Generate source code used to link in the resource file into your code
--generate-dependencies Generate dependency list
--dependency-file=FILE Name of the dependency file to generate
--generate-phony-targets Include phony targets in the generated dependency file
--manual-register Don?t automatically create and register resource
--internal Don?t export functions; declare them G_GNUC_INTERNAL
--external-data Don?t embed resource data in the C file; assume it's linked externally instead
--c-name C identifier name used for the generated source code
-C, --compiler The target C compiler (default: the CC environment variable)
现在运行编译器。
$ glib-compile-resources tfe3.gresource.xml --target=resources.c --generate-source
然后生成一个C源文件resources.c。修改tfe3.c,并保存为tfe3_r.c。
#include "resources.c"
... ... ...
... ... ...
build = gtk_builder_new_from_resource ("/com/github/ToshioCP/tfe3/tfe3.ui");
... ... ...
... ... ...
函数gtk_builder_new_from_resource从resource构建小部件。
然后,编译并运行它。出现一个窗口,它与本页开始时的截图相同。
一般来说,resource是C语言最好的表达方式。如果您使用其他语言,如Ruby, string比resource更好。