GtkBuilder and UI Files
New, Open, Save and Close Buttons
We made a 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.
- 26-28: Creates a GtkApplicationWindow instance and sets the title and default size.
- 30-31: Creates a GtkBox instance
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. - 33-34: Creates a GtkBox instance
boxhand appends it toboxvas the first child. - 36-41: Creates three dummy labels. The labels
dmy1anddmy3have ten characters wide. The other labeldmy2has the hexpand property set to TRUE. This makes the label expand horizontally to be as long as possible. - 42-45: Creates four buttons.
- 47-53: Appends these GtkLabel and GtkButton to
boxh. - 55-58: Creates a GtkNotebook instance and sets hexpand and
vexpand properties to be TRUE. This makes it expand horizontally
and vertically to be as big as possible. It is appended to
boxvas the second child.
The number of lines for building widgets is 33 (= 58-26+1).
We also needed many variables (boxv,
boxh, dmy1, …), and most of them are
used only for building the widgets. Is there a better way to
reduce this overhead?
Gtk provides GtkBuilder. It reads user interface (UI) data and builds a window. It simplifies this tedious process.
UI Files
Look at the UI file tfe3.ui that defines the
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 an 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 class and id attributes in
their start tag.
- 1: XML declaration. It specifies that the XML version is 1.0 and the encoding is UTF-8.
- 3-6: An object tag with
GtkApplicationWindowclass andwinid. This is the top level window. It defines three properties: thetitleproperty is “file editor”, thedefault-widthproperty is 600, and thedefault-heightproperty is 400. - 7: Child tag means a child widget. For example, line 7 tells
us that GtkBox object is a child widget of
win.
Compare this UI file with lines 26-58 in the
app_open function of tfe2.c. Both
build 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 contains any syntax errors,gtk4-builder-toolprints the error.gtk4-builder-tool simplify <ui file name>simplifies the UI file and prints the result. If the--replaceoption is given, it replaces the UI file with the simplified one. If the UI file specifies the default value of a property, that property will be removed. For example, the default orientation is horizontal so the simplification removes line 12. Some values are simplified too. For example, “TRUE” and “FALSE” become “1” and “0”, respectively. However, “TRUE” and “FALSE” are better for maintenance.
It is a good idea to check your UI file before compiling.
GtkBuilder
GtkBuilder builds widgets based on a UI file.
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);The function gtk_builder_new_from_file reads the
file tfe3.ui. Then, it builds the widgets and
creates a 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 using the
gtk_builder_get_object function. The top level
window, which has an id of “win” in the UI file, is taken and
assigned to the variable win. The window’s
application property is set to app with the
gtk_window_set_application function. The
GtkNotebook, which has the id “nb” in the UI file, is also taken
and assigned to the variable nb. After the window
and application are connected, we no longer need the GtkBuilder
instance. It is released with the 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 a UI file not only shortens C source files, but also makes
the widgets’ structure clear.
Now I’ll show you the 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.
Using UI String
GtkBuilder can build widgets with a 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>";
build = gtk_builder_new_from_string (uistring, -1);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 at runtime. The disadvantage is that defining the UI as a C string can be tedious, as the xml needs quoting and special characters need escaping. If you want to use this method, you should write a script that converts UI files into C string literals.
- Replace backslashes with two backslashes.
- Add a backslash before each double quote.
- Add double quotes at the left and right of the string in each line.
Or, if you have jq installed, you can use
jq -R < tfe3.ui to do the quoting and escaping
for you.
Gresources
A Gresource is similar to a string, except that a Gresource
is a binary data. The glib-compile-resources
program compiles UI files into Gresources. It can compile not
only text files but also binary files such as images, sounds and
so on. 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>- 2:
gresourcestag can include multiple gresources (gresource tags). However, this xml has only one gresource. - 3: The gresource has a prefix
/com/github/ToshioCP/tfe3. - 4: The name of the gresource is
tfe3.ui. The resource will be referred to as /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"
... ... ...
... ... ...
build = gtk_builder_new_from_resource ("/com/github/ToshioCP/tfe3/tfe3.ui");
... ... ...
... ... ...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, resources are the best for C programs. Strings are easier to handle in some interpreted languages, but resources are still a powerful option.