GTK mit Builder: Builder

Widgets konstruieren mit dem Builder Bearbeiten

In modernen Anwendungen werden grafische Benutzeroberflächen aus XML-Dateien geladen. Diese Dateien werden erzeugt durch Werkzeuge, in denen man grafische Benutzeroberflächen zusammenklickt. Wir wollen Ihnen in diesem Kapitel zeigen, wie man solche Dateien in einem GTK+-Programm einliest und verarbeitet. Wir gehen in diesem Kapitel absichtlich nicht auf ein bestimmtes Werkzeug ein, mit dem man Benutzeroberflächen erstellt. So lernen Sie den allgemeinen Prozess und sehen, was im Hintergrund geschieht. Das Wissen über diese Zusammenhänge brauchen wir in späteren Kapiteln.

Alle Programme in diesem Kapitel stellen ein Fenster bereit, welches einen Knopf anzeigt. Drückt man auf diesen Knopf, wird eine Callback-Funktion aufgerufen. Die Programme unterscheiden sich darin, wie die Callback-Funktionen mit den Widgets verknüpft werden.

Manuelle Signalverknüpfung Bearbeiten

Unser erstes Beispiel lädt eine solche XML-Beschreibung auf der Basis eines im Programmtext fixierten Textes und verknüpft die Callback-Funktionen in bekannter Weise, wobei die zugehörigen Widgets aus dem Builder-Objekt extrahiert werden:

C
#include <gtk/gtk.h>
#include <string.h>

static const gchar *interface =
    "<interface>"
    "  <object class=\"GtkWindow\" id=\"main-window\">"
    "    <child>"
    "      <object class=\"GtkButton\" id=\"my-button\">"
    "        <property name=\"label\">Hallo, Welt!</property>"
    "      </object>"
    "    </child>"
    "  </object>"
    "</interface>";

static void on_button_clicked (GtkWidget *w, gpointer d)
{
    g_print ("Hallo, Welt!\n");
}

int main (int argc, char *argv[])
{
    GtkBuilder *builder;
    GError *error = NULL;
    GtkWidget *window, *button;

    gtk_init (&argc, &argv);
    builder = gtk_builder_new ();
    gtk_builder_add_from_string (builder, interface, strlen (interface), &error);
    window = GTK_WIDGET(gtk_builder_get_object (builder, "main-window"));
    g_signal_connect (window, "destroy", G_CALLBACK(gtk_main_quit), NULL);
    button = GTK_WIDGET(gtk_builder_get_object (builder, "my-button"));
    g_signal_connect (button, "clicked", G_CALLBACK(on_button_clicked), NULL);
    gtk_widget_show_all (window);
    gtk_main ();
    return 0;
}

Beginnen wir mit der Erläuterung der XML-Beschreibung. Auf oberster Ebene der XML-Beschreibung ist das Element „interface“. Die nächste Ebene bilden die Objekte. Zu einem Objekt gehört eine Klasse. Die Klasse ist unmittelbar ein Widget eines bestimmten Typs, wie zum Beispiel GtkWindow, GtkButton, GtkLabel, und so fort. Jedes Objekt sollte einen Namen haben, den man mit dem „id“-Attribut frei vergibt. Über diesen Namen lassen sich die Objekte während das Programm läuft wiederfinden. Darum sollten diese Namen auch eindeutig sein. Jedes Objekt kann ein „child“-Element haben. Dies könnte beim Hauptfenster ein Layout sein oder, wie in unserem Fall, schlicht ein Knopf. Über „property“-Elemente werden einem Objekt zusätzliche Eigenschaften mitgegeben. In unserem Fall soll der Knopf eine Beschriftung haben und diese soll „Hallo, Welt!“ sein. Der Abschnitt in der XML-Beschreibung zur ID „my-button“ ist genau das, was die Funktion gtk_button_new_with_label() machen würde.

Es folgt die Erläuterung zur main()-Funktion. Das Builder-Objekt wird erzeugt durch einen Aufruf von gtk_builder_new(). Nun muss noch die Beschreibung der grafischen Oberfläche geladen werden, das erledigt gtk_builder_add_from_string() für uns. Hier wird das Builder-Objekt benötigt, der Text, der unsere grafische Oberfläche beschreibt wie auch die Textlänge und ein Zeiger auf eine Variable für Fehlermeldungen, die mit NULL initialisiert werden muss. Statt die Länge mit der Funktion strlen() aus string.h zu ermitteln könnten wir alternativ auch -1 als Länge angeben, was wir einfach im nächsten Beispiel tun werden. In dem Fall ermittelt der Builder dann für uns die Länge des übergebenen Strings.

Alternativ kann man mit der Funktion gtk_builder_add_from_file() die Beschreibung aus einer externen Datei laden. Dies machen wir in einem späteren Kapitel.

Nun ist die grafische Benutzeroberfläche geladen, aber weder sind die Ereignisse verknüpft, noch wird die Oberfläche angezeigt. Wir benötigen also die Widgets. Diese erhalten wir mit der Funktion gtk_builder_get_object(). Diese Funktion lädt ein beliebiges benanntes Objekt aus dem Builder anhand des Namens, den wir in der XML-Beschreibung mit dem Attribut „id“ vergeben haben. Dieses Objekt wird in ein Widget verwandelt und dann der schon bekannten Funktion g_signal_connect() übergeben. Das machen wir einmal mit dem Fenster und dem Knopf. Anschließend werden die Elemente innerhalb der XML-Beschreibung mit gtk_widget_show_all() angezeigt.

Automatische Signalverknüpfung Bearbeiten

Das zweite Beispiel zeigt, wie man in der XML-Beschreibung direkt mitteilt, welche Signale wie zu verknüpfen sind:

C
#include <gtk/gtk.h>

static const gchar *interface = 
    "<interface>"
    "  <object class=\"GtkWindow\" id=\"main-window\">"
    "    <signal name=\"destroy\" handler=\"gtk_main_quit\"/>"
    "    <child>"
    "      <object class=\"GtkButton\" id=\"my-button\">"
    "        <property name=\"label\">Hallo, Welt!</property>"
    "        <signal name=\"clicked\" handler=\"on_button_clicked\"/>"
    "      </object>"
    "    </child>"
    "  </object>"
    "</interface>";

G_MODULE_EXPORT void on_button_clicked (GtkWidget *w, gpointer d)
{
    g_print ("Hallo, Welt!\n");
}

int main (int argc, char *argv[])
{
    GtkBuilder *builder;
    GError *error = NULL;
    GtkWidget *window;

    gtk_init (&argc, &argv);
    builder = gtk_builder_new ();
    gtk_builder_add_from_string (builder, interface, -1, &error);
    gtk_builder_connect_signals (builder, NULL);
    window = GTK_WIDGET(gtk_builder_get_object (builder, "main-window"));
    gtk_widget_show_all (window);
    gtk_main ();
    return 0;
}

Um dieses Beispielprogramm zu übersetzen müssen Sie die Bibliothek „gmodule-2.0“ einbinden, etwa wie folgt:

  Shell

user@localhost:~$ gcc -Wall `pkg-config --cflags --libs gtk+-3.0 gmodule-2.0` builder2.c -o builder2

Beachten Sie bitte, dass die Callback-Funktion in diesem Beispiel nicht static („privat“) deklariert sein darf, sonst funktioniert das Verfahren nicht, da der Builder die Funktion dann nicht finden kann.

Unter Windows muss die Funktion auch mit G_MODULE_EXPORT deklariert sein, damit sie gefunden werden kann.

Die XML-Beschreibung enthält jetzt zusätzlich „signal“-Tags. Im ersten Fall wird mit diesem Tag das Signal namens „destroy“ mit dem Namen der Callback-Funktion gtk_main_quit() verknüpft.

Die ganze Magie, den Namen der Callback-Funktion aus der Symboltabelle des Linkers zu finden steckt in der Funktion gtk_builder_connect_signals(), deren zweiter Parameter optional einen Zeiger auf Daten anbietet, die allen Callback-Funktionen als Datenargument angeboten wird.

Nun muss man nur noch das Hauptfenster aus dem Builder erfragen, um es anzeigen zu können. Das erledigt in bekannter Weise gtk_builder_get_object() für uns.

Volle Kontrolle Bearbeiten

Die volle Kontrolle über den Prozess, bei dem Signale mit Callbacks verknüpft werden, die ihrerseits in der XML-Beschreibung der Benutzeroberfläche vorliegen, bekommt man, wenn man eine Verbindungs-Funktion einrichtet, die jedes Mal aufgerufen wird, wenn eine solche Verknüpfung ansteht. Das folgende Beispielprogramm verdeutlicht das Vorgehen:

C
#include <gtk/gtk.h>

static const gchar *interface = 
    "<interface>"
    "  <object class=\"GtkWindow\" id=\"main-window\">"
    "    <signal name=\"destroy\" handler=\"gtk_main_quit\"/>"
    "    <child>"
    "      <object class=\"GtkButton\" id=\"my-button\">"
    "        <property name=\"label\">Hallo, Welt!</property>"
    "        <signal name=\"clicked\" handler=\"on_button_clicked\"/>"
    "      </object>"
    "    </child>"
    "  </object>"
    "</interface>";

static void on_button_clicked (GtkWidget *w, gpointer d)
{
    g_print ("Hallo, Welt!\n");
}

static void connection_mapper (GtkBuilder *builder, GObject *object,
	const gchar *signal_name, const gchar *handler_name,
	GObject *connect_object, GConnectFlags flags, gpointer user_data)
{
    g_print ("Verbinde %s mit %s\n", signal_name, handler_name);

    if (g_strcmp0 (handler_name, "gtk_main_quit") == 0)
        g_signal_connect (object, signal_name, G_CALLBACK(gtk_main_quit), 0);
    else if (g_strcmp0 (handler_name, "on_button_clicked") == 0)
        g_signal_connect (object, signal_name, G_CALLBACK(on_button_clicked), 0);
    else
        g_print ("unbekannte Callback\n");
}

int main (int argc, char *argv[])
{
    GtkBuilder *builder;
    GError *error = NULL;
    GtkWidget *window;

    gtk_init (&argc, &argv);
    builder = gtk_builder_new ();
    gtk_builder_add_from_string (builder, interface, -1, &error);
    gtk_builder_connect_signals_full (builder, connection_mapper, NULL);
    window = GTK_WIDGET(gtk_builder_get_object (builder, "main-window"));
    gtk_widget_show_all (window);
    gtk_main ();
    return 0;
}

In der XML-Beschreibung liegen wieder zwei Signale vor, die mit Callbacks verknüpft werden sollen. Diese Verknüpfungen werden durch den Aufruf der Funktion gtk_builder_connect_signals_full() initiiert. Der zweite Parameter dieser Funktion ist eine Funktion, die jedes Mal aufgerufen wird, wenn ein Signal mit ihrer Callback-Funktion verbunden werden soll. Der letzte Parameter ist ein Zeiger auf Daten, die dieser Verbindungsfunktion übergeben werden.

Die Verbindungsfunktion hat eine Reihe von formalen Parametern, wobei uns nur wenige davon interessieren. Wir wollen bei diesem Beispiel lediglich wissen, welches Signal eines Objektes mit welcher Funktion zu verknüpfen ist. Hierzu dienen die Parameter, die wir „signal_name“, „object“ und „handler_name“ genannt haben. Da „handler_name“ ein Text ist, müssen wir ihn in der Verbindungsfunktion mit g_strcmp0() mit uns bekannten Callbacks vergleichen und anschließend g_signal_connect() aufrufen, um die Verknüpfung durchzuführen.

Die anderen Parameter sind das Builder-Objekt, ein so genanntes „connect_object“, welches dazu dient, ein Objekt zu liefern für die Verknüpfungsfunktion g_signal_connect_object() . Mit den „flags“ kann man das Verhalten steuern, ob eine Callback-Funktion vor oder nach einer eventuell schon vorhandenen Callback aufgerufen wird. Der letzte Parameter sind die Daten, die von gtk_builder_connect_signals_full() übergeben werden.

Zusammenfassung Bearbeiten

In diesem Kapitel haben wir Ihnen dargelegt, wie eine XML-Beschreibung einer grafischen Benutzeroberfläche genutzt werden kann, um eine Anwendung aufzubauen. Problematisch daran sind lediglich Signale und ihre zugehörigen Callbacks. Sie kennen nun drei verschiedene Wege, wie man mit Signalen umgehen kann. In den folgenden Kapiteln werden wir Ihnen Details zu dieser XML-Beschreibung liefern, die Ihnen dabei hilft, Aussehen der Anwendung und Programmlogik voneinander zu trennen.