We made very simple editor in the previous section. It reads files at the start and writes them out at the end of the program. It works, but is not so good. It would be better if we had “New”, “Open”, “Save” and “Close” buttons. This section describes how to put those buttons into the window.
The screenshot above shows the layout. The function
app_open
in the source code tfe2.c
is as
follows.
static void
app_open (GApplication *app, GFile ** files, gint n_files, gchar *hint) {
GtkWidget *win;
GtkWidget *nb;
GtkWidget *lab;
GtkNotebookPage *nbp;
GtkWidget *scr;
GtkWidget *tv;
GtkTextBuffer *tb;
char *contents;
gsize length;
char *filename;
int i;
GError *err = NULL;
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);
for (i = 0; i < n_files; i++) {
if (g_file_load_contents (files[i], NULL, &contents, &length, NULL, &err)) {
scr = gtk_scrolled_window_new ();
tv = tfe_text_view_new ();
tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (tv), GTK_WRAP_WORD_CHAR);
gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scr), tv);
tfe_text_view_set_file (TFE_TEXT_VIEW (tv), g_file_dup (files[i]));
gtk_text_buffer_set_text (tb, contents, length);
g_free (contents);
filename = g_file_get_basename (files[i]);
lab = gtk_label_new (filename);
gtk_notebook_append_page (GTK_NOTEBOOK (nb), scr, lab);
nbp = gtk_notebook_get_page (GTK_NOTEBOOK (nb), scr);
g_object_set (nbp, "tab-expand", TRUE, NULL);
g_free (filename);
} else {
g_printerr ("%s.\n", err->message);
g_clear_error (&err);
}
}
if (gtk_notebook_get_n_pages (GTK_NOTEBOOK (nb)) > 0) {
gtk_window_present (GTK_WINDOW (win));
} else
gtk_window_destroy (GTK_WINDOW (win));
}
The function app_open
builds the widgets in the main
application window.
boxv
. It is a vertical
box and a child of GtkApplicationWindow. It has two children. The first
child is a horizontal box. The second child is a GtkNotebook.boxh
and appends it to
boxv
as the first child.dmy1
and
dmy3
has a character width of ten. The other label
dmy2
has hexpand property which is set to be TRUE. This
makes the label expands horizontally as long as possible.boxh
.boxv
as the second
child.The number of widget-build lines is 33(=58-26+1). We also needed many
variables (boxv
, boxh
, dmy1
, …)
and most of them used only for building the widgets. Are there any good
solution to reduce these works?
Gtk provides GtkBuilder. It reads user interface (UI) data and builds a window. It reduces this cumbersome work.
Look at the UI file tfe3.ui
that defines widget
structure.
<?xml version="1.0" encoding="UTF-8"?>
<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">
<property name="orientation">GTK_ORIENTATION_VERTICAL</property>
<child>
<object class="GtkBox">
<property name="orientation">GTK_ORIENTATION_HORIZONTAL</property>
<child>
<object class="GtkLabel">
<property name="width-chars">10</property>
</object>
</child>
<child>
<object class="GtkButton">
<property name="label">New</property>
</object>
</child>
<child>
<object class="GtkButton">
<property name="label">Open</property>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="hexpand">TRUE</property>
</object>
</child>
<child>
<object class="GtkButton">
<property name="label">Save</property>
</object>
</child>
<child>
<object class="GtkButton">
<property name="label">Close</property>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="width-chars">10</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkNotebook" id="nb">
<property name="hexpand">TRUE</property>
<property name="vexpand">TRUE</property>
</object>
</child>
</object>
</child>
</object>
</interface>
The is a XML file. Tags begin with <
and end with
>
. There are two types of tags, the start tag and the
end tag. For example, <interface>
is a start tag and
</interface>
is an end tag. The UI file begins and
ends with interface tags. Some tags, for example object tags, can have a
class and id attributes in their start tag.
GtkApplicationWindow
class and
win
id. This is the top level window. And the three
properties of the window are defined. The title
property is
“file editor”, default-width
property is 600 and
default-height
property is 400.win
.Compare this ui file and the lines 26-58 in the app_open
function of tfe2.c
. Both builds the same window with its
descendant widgets.
You can check the ui file with gtk4-builder-tool
.
gtk4-builder-tool validate <ui file name>
validates the ui file. If the ui file includes some syntactical error,
gtk4-builder-tool
prints the error.gtk4-builder-tool simplify <ui file name>
simplifies the ui file and prints the result. If --replace
option is given, it replaces the ui file with the simplified one. If the
ui file specifies a value of property but it is default, then it will be
removed. For example, the default orientation is horizontal so the
simplification removes line 12. And some values are simplified. For
example, “TRUE”and “FALSE” becomes “1” and “0” respectively. However,
“TRUE” or “FALSE” is better for maintenance.It is a good idea to check your ui file before compiling.
GtkBuilder builds widgets based on a ui file.
*build;
GtkBuilder
= gtk_builder_new_from_file ("tfe3.ui");
build = GTK_WIDGET (gtk_builder_get_object (build, "win"));
win (GTK_WINDOW (win), GTK_APPLICATION (app));
gtk_window_set_application = GTK_WIDGET (gtk_builder_get_object (build, "nb"));
nb (build); g_object_unref
The function gtk_builder_new_from_file
reads the file
tfe3.ui
. Then, it builds the widgets and creates GtkBuilder
object. All the widgets are connected based on the parent-children
relationship described in the ui file. We can retrieve objects from the
builder object with gtk_builder_get_object
function. The
top level window, its id is “win” in the ui file, is taken and assigned
to the variable win
, the application property of which is
set to app
with the gtk_window_set_application
function. GtkNotebook with the id “nb” in the ui file is also taken and
assigned to the variable nb
. After the window and
application are connected, GtkBuilder instance is useless. It is
released with g_object_unref
function.
The ui file reduces lines in the C source file.
$ cd tfe; diff tfe2.c tfe3.c
59a60
> GtkBuilder *build;
61,104c62,66
< 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
>
61,104c62,66
means that 44 (=104-61+1) lines are changed
to 5 (=66-62+1) lines. Therefore, 39 lines are reduced. Using ui file
not only shortens C source files, but also makes the widgets structure
clear.
Now I’ll show you app_open
function in the C file
tfe3.c
.
static void
app_open (GApplication *app, GFile ** files, gint n_files, gchar *hint) {
GtkWidget *win;
GtkWidget *nb;
GtkWidget *lab;
GtkNotebookPage *nbp;
GtkWidget *scr;
GtkWidget *tv;
GtkTextBuffer *tb;
char *contents;
gsize length;
char *filename;
int i;
GError *err = NULL;
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"));
g_object_unref(build);
for (i = 0; i < n_files; i++) {
if (g_file_load_contents (files[i], NULL, &contents, &length, NULL, &err)) {
scr = gtk_scrolled_window_new ();
tv = tfe_text_view_new ();
tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (tv), GTK_WRAP_WORD_CHAR);
gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scr), tv);
tfe_text_view_set_file (TFE_TEXT_VIEW (tv), g_file_dup (files[i]));
gtk_text_buffer_set_text (tb, contents, length);
g_free (contents);
filename = g_file_get_basename (files[i]);
lab = gtk_label_new (filename);
gtk_notebook_append_page (GTK_NOTEBOOK (nb), scr, lab);
nbp = gtk_notebook_get_page (GTK_NOTEBOOK (nb), scr);
g_object_set (nbp, "tab-expand", TRUE, NULL);
g_free (filename);
} else {
g_printerr ("%s.\n", err->message);
g_clear_error (&err);
}
}
if (gtk_notebook_get_n_pages (GTK_NOTEBOOK (nb)) > 0) {
gtk_window_present (GTK_WINDOW (win));
} else
gtk_window_destroy (GTK_WINDOW (win));
}
The whole source code of tfe3.c
is stored in the src/tfe
directory.
GtkBuilder can build widgets with string. Use
gtk_builder_new_from_string
instead of
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\">"
"<property name="orientation">GTK_ORIENTATION_VERTICAL</property>"
... ... ...
... ... ...
"</interface>";
= gtk_builder_new_from_string (uistring, -1); build
This method has an advantage and disadvantage. The advantage is that the ui string is written in the source code. So, no ui file is needed on runtime. The disadvantage is that writing C string is a bit bothersome because of the double quotes. If you want to use this method, you should write a script that transforms ui file into C-string.
Gresource is similar to string. But Gresource is compressed binary data, not text data. And there’s a compiler that compiles ui file into Gresource. It can compile not only text files but also binary files such as images, sounds and so on. And after compilation, it bundles them up into one Gresource object.
An xml file is necessary for the resource compiler
glib-compile-resources
. It describes resource files.
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/com/github/ToshioCP/tfe3">
<file>tfe3.ui</file>
</gresource>
</gresources>
gresources
tag can include multiple gresources
(gresource tags). However, this xml has only one gresource./com/github/ToshioCP/tfe3
.tfe3.ui
. The resource
will be pointed with /com/github/ToshioCP/tfe3/tfe3.ui
by
GtkBuilder. The pattern is “prefix” + “name”. If you want to add more
files, insert them between line 4 and 5.Save this xml text to tfe3.gresource.xml
. The gresource
compiler glib-compile-resources
shows its usage with the
argument --help
.
$ 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)
Now run the compiler.
$ glib-compile-resources tfe3.gresource.xml --target=resources.c --generate-source
Then a C source file resources.c
is generated. Modify
tfe3.c
and save it as tfe3_r.c
.
#include "resources.c"
... ... ...
... ... ...
= gtk_builder_new_from_resource ("/com/github/ToshioCP/tfe3/tfe3.ui");
build ... ... ...
... ... ...
The function gtk_builder_new_from_resource
builds
widgets from a resource.
Then, compile and run it.
$ comp tfe3_r
$ ./a.out tfe2.c
A window appears and it is the same as the screenshot at the beginning of this page.
Generally, resource is the best way for C language. If you use other languages like Ruby, string is better than resource.