GTK mit Builder: Hauptfensterwidgets

Widgets im Hauptfenster Bearbeiten

In Anwendungsprogrammen findet man üblicherweise eine Menüzeile, eine Werkzeugleiste darunter, dann ein Hauptelement wie eine Bildbearbeitung oder einen Editor und danach eine Statuszeile. In diesem Kapitel geht es um genau diese typische Abfolge von Elementen im Hauptfenster. Wir benutzen den Builder, eine XML-Beschreibung der Benutzeroberfläche und lassen Signale und Callbacks automatisch verbinden.

Menüzeile Bearbeiten

Im folgenden Programm wird eine Menüzeile dargestellt. Sie können das Programm über das Menü beenden und außerdem eine Callback aufrufen, die einen Nachrichtendialog bereitstellt.

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


void kleine_callback (GtkWidget *w, gpointer d)
{
    GtkWidget *dialog;
    dialog = gtk_message_dialog_new (NULL,
        GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_INFO,
        GTK_BUTTONS_CLOSE, "Hallo, Welt!");
    gtk_dialog_run (GTK_DIALOG (dialog));
    gtk_widget_destroy (dialog);
}

static const gchar * interface = 
"<interface>"
"    <object class=\"GtkUIManager\" id=\"uimanager\">"
"    <child>"
"        <object class=\"GtkActionGroup\" id=\"aktionen\">"
"        <child>"
"            <object class=\"GtkAction\" id=\"datei\">"
"            <property name=\"label\">_Datei</property>"
"            </object>"
"        </child>"
"        <child>"
"            <object class=\"GtkAction\" id=\"neu\">"
"            <property name=\"label\">_Neue Datei</property>"
"            <signal name=\"activate\" handler=\"kleine_callback\"/>"
"            </object>"
"        </child>"
"        <child>"
"            <object class=\"GtkAction\" id=\"exit\">"
"            <property name=\"label\">_Beenden</property>"
"            <property name=\"stock-id\">gtk-quit</property>"
"            <signal name=\"activate\" handler=\"gtk_main_quit\"/>"
"            </object>"
"        </child>"
"        <child>"
"            <object class=\"GtkAction\" id=\"bearbeiten\">"
"            <property name=\"label\">_Bearbeiten</property>"
"            </object>"
"        </child>"
"        </object>"
"    </child>"
"    <ui>"
"    <menubar name=\"menubar\">"
"        <menu action=\"datei\" >"
"            <menuitem action=\"neu\" />"
"            <separator />"
"            <menuitem action=\"exit\" />"
"        </menu>"
"        <menu action=\"bearbeiten\" />"
"    </menubar>"
"    </ui>"
"    </object>"
"   "
"    <object class=\"GtkWindow\" id=\"hauptfenster\" >"
"    <signal name=\"destroy\" handler=\"gtk_main_quit\"/>"
"    <child>"
"        <object class=\"GtkVBox\" id=\"vbox-layout\">"
"        <property name=\"homogeneous\">FALSE</property>"
"        <child>"
"       <object class=\"GtkMenuBar\" id=\"menubar\" constructor=\"uimanager\"/>"
"        </child>"
"        </object>"
"    </child>"
"    </object>"
"</interface>";


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

    gtk_init (&argc, &argv);
    builder = gtk_builder_new ();
    gtk_builder_add_from_string (builder, interface,
	strlen(interface), &errors);
    gtk_builder_connect_signals (builder, NULL);
    window = GTK_WIDGET(gtk_builder_get_object (builder, "hauptfenster"));
    gtk_widget_show_all (window);
    gtk_main ();
    return 0;
}

Beginnen wir mit der Erläuterung dieses Programms bei der XML-Beschreibung. Wir entdecken hier zwei Hauptelemente, nämlich ein Objekt mit der Id „uimanager“ und eines mit der Id „hauptfenster“. Der GtkUIManager ist ein Werkzeug, um Beschreibungen für Menüzeilen und Werkzeugleisten zu definieren. Mehr kann es nicht. Zu diesem Zweck werden in einer Klasse „GtkActionGroup“ einige „GtkAction“-Klassen gesammelt. Jedes dieser Aktionsobjekte hat ein Textfeld (Label-Property), ein optionales Piktogramm (stock-id-Property) und ein optionale Signal, auf das die Anwendung reagieren kann. Jedes Aktionsobjekt ist ein Kindelement der Aktionsgruppe. Die so genannten Stock-Ids sind eine Sammlung von vordefinierten Piktogrammen, die unter einem Namen in der GTK+-Bibliothek abgelegt sind.

Darüber hinaus finden wir im UI-Manager die Beschreibung der Menüzeile. Eingeschlossen in <menubar>-Tags werden Menüelemente definiert, die einen Verweis auf die Aktionsobjekte haben, in dem sie deren Ids referenzieren. Da die Aktionselemente schon die Texte der Menüelemente und gegebenenfalls auch die Signal-Callback-Verknüpfung bereitstellen, sind wir hier fertig. Neben <menuitem>-Tags, die die einzelnen Menüknöpfe in ihrer Reihenfolge und Zugehörigkeit repräsentieren, kann man noch <separator />-Tags einfügen, um zwischen zwei Menüknöpfen eine Trennlinie einzufügen.

Das zweite Objekt auf der Hauptebene der Beschreibung ist das Fenster. Dieses enthält eine vertikale Box, die Sie schon aus dem Kapitel über Layouts kennen. Der Box wird über ein Property-Tag zugewiesen, dass enthaltene Widgets nicht gleich groß gemacht werden. Dieses Tag spielt in diesem Programm praktisch keine Rolle, da das einzige Kindelement des Fensters die Menüzeile ist, die mit dem nächsten Objekt-Tag eingefügt wird. Bitte beachten Sie, dass dieses Objekt-Tag ein zusätzliches Attribut führt, nämlich „constructor“. Dieses Attribut stellt eine Verbindung her zwischen der nun eingefügten Menüzeile und dem GtkUIManager, den wir weiter oben in der XML-Beschreibung eingefügt haben.

Wird im Programm die Callback-Funktion kleine_callback() aufgerufen, so wird ein Nachrichtendialog mit gtk_message_dialog_new() gestartet. Dieser Dialog bekommt als Parameter einen Verweis auf das Fenster, zu dem er gehört, eine Auswahl an Dialogflags, den Nachrichtentyp, der in diesem Beispiel GTK_MESSAGE_INFO ist, aber auch GTK_MESSAGE_WARNING, GTK_MESSAGE_QUESTION, GTK_MESSAGE_ERROR oder GTK_MESSAGE_OTHER sein kann. Die Knöpfe, die dieser Dialog anzeigt sind eine Kombination aus GTK_BUTTONS_OK, GTK_BUTTONS_CLOSE, GTK_BUTTONS_CANCEL, GTK_BUTTONS_YES_NO, und GTK_BUTTONS_OK_CANCEL. Der letzte Parameter ist der anzuzeigende Text. Diesen so erzeugten Dialog startet man mit gtk_dialog_run(). Diese Funktion beendet sich mit einem Zahlenwert als Ergebnis, der anzeigt, welcher der Knöpfe gedrückt wurde. Der Dialog kann anschließend beendet werden durch gtk_widget_destroy().

Kalender Bearbeiten

Die Erfahrung aus dem letzten Programm zeigt uns, dass es spätestens bei größeren XML_Beschreibungen dringend angeraten ist, diese vom sonstigen Quellcode zu trennen. Das machen wir in diesem Beispiel. Außerdem nähern wir uns dem Anspruch dieses Kapitels, ein „vollständiges“ Hauptfenster zu bekommen dadurch, dass wir im mittleren Bereich ein Widget einfügen und unten eine Art Statusleiste. Das Hauptwidget ist ein Kalender und die Statusleiste besteht aus einem Textfeld.

Hier die vollständige XML-Datei:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<interface>
    <object class="GtkUIManager" id="uimanager">
        <child>
            <object class="GtkActionGroup" id="aktionen">
                <child>
                    <object class="GtkAction" id="datei">
                        <property name="label">_Datei</property>
                    </object>
                </child>
                <child>
                    <object class="GtkAction" id="neu">
                        <property name="label">_Neue Datei</property>
                        <signal name="activate" handler="kleine_callback"/>
                    </object>
                </child>
                <child>
                    <object class="GtkAction" id="exit">
                        <property name="label">_Beenden</property>
                        <property name="stock-id">gtk-quit</property>
                        <signal name="activate" handler="gtk_main_quit"/>
                    </object>
                </child>
                <child>
                    <object class="GtkAction" id="bearbeiten">
                        <property name="label">_Bearbeiten</property>
                    </object>
                </child>
            </object>
        </child>
        <ui>
            <menubar name="menubar">
                <menu action="datei" >
                    <menuitem action="neu" />
                    <separator />
                    <menuitem action="exit" />
                </menu>
                <menu action="bearbeiten" />
            </menubar>
        </ui>
    </object>
    <object class="GtkWindow" id="hauptfenster" >
        <signal name="destroy" handler="gtk_main_quit"/>
        <child>
            <object class="GtkVBox" id="vbox-layout">
                <property name="homogeneous">FALSE</property>
                <child>
                    <object class="GtkMenuBar" id="menubar" constructor="uimanager" />
                    <packing>
                        <property name="expand">FALSE</property>
                        <property name="fill">FALSE</property>
                    </packing>
                </child>
                <child>
                    <object class="GtkCalendar" id="calender">
                        <signal name="day-selected" handler="tag_auswaehlen" object="mein-label-1" />
                    </object>
                </child>
                <child>
                    <object class="GtkLabel" id="mein-label-1">
                        <property name="label">Und heute passierte folgendes...</property>
                    </object>
                    <packing>
            	        <property name="expand">FALSE</property>
                        <property name="fill">FALSE</property>
                    </packing>
                </child>
            </object>
        </child>
    </object>
</interface>

Die eigentliche Änderung an dieser Datei gegenüber der sonst in den Quelltext eingefügten Beschreibung ist, dass wir Zeichenketten nicht zu maskieren brauchen, und eine vollständige XML-Beschreibung verwenden.

Bezogen auf unser Hauptfenster wird in die vertikale Box eine Menüzeile, ein Kalender (GtkCalendar) und ein Textfeld eingefügt. Das Signal „day-selected“ des Kalenders wird mit einer Callback namens „tag_auswaehlen“ verknüpft. Hier übergeben wir ein zusätzliches Attribut namens „objekt“ und teilen so mit, dass die Callback-Funktion einen Verweis auf das Textfeld bekommt.

Beachten Sie bitte darüber hinaus, dass wir die Art, wie die Widgets in die Box eingefügt werden, explizit bestimmen. Lediglich das Kalender-Objekt darf sich breit machen, da für dieses Objekt keine Begrenzungen definiert wurden.

Das Programm sieht folgendermaßen aus:

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


/* Wird aus dem Menü aufgerufen */
void kleine_callback (GtkWidget *w, gpointer d)
{
    GtkWidget *dialog;
    dialog = gtk_message_dialog_new (NULL,
        GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_INFO,
        GTK_BUTTONS_CLOSE, "Hallo, Welt!");
    gtk_dialog_run (GTK_DIALOG (dialog));
    gtk_widget_destroy (dialog);
}


/* Wird aufgerufen, wenn ein Tag ausgewählt wurde. */
void tag_auswaehlen (GtkWidget *l, gpointer c)
{
    guint jahr, monat, tag;
    gchar text[32];
    GtkLabel *label = GTK_LABEL(l);
    GtkCalendar *calendar = GTK_CALENDAR(c);
    gtk_calendar_get_date (calendar, &jahr, &monat, &tag);
    g_snprintf (text, 32, "Ausgewählt: %02d.%02d.%d", tag, monat, jahr);
    gtk_label_set_text (label, text);
}


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

    gtk_init (&argc, &argv);
    builder = gtk_builder_new ();
    gtk_builder_add_from_file (builder, "menu2.xml", &errors);
    gtk_builder_connect_signals (builder, builder);
    window = GTK_WIDGET(gtk_builder_get_object (builder, "hauptfenster"));
    gtk_widget_show_all (window);
    gtk_main ();
    return 0;
}

Die Callback-Funktion tag_auswaehlen() wird aufgerufen, wenn im Kalender ein Tag mit der Maus ausgewählt wird. Als Parameter werden das Textfeld (l) und der Kalender (c) übergeben. Beide Parameter werden in die entsprechenden Klassen umgewandelt. Mit der Funktion gtk_calendar_get_date() werden Jahr, Monat und Tag aus dem Datumsfeld extrahiert. Diese Angaben werden in einen Text konvertiert und anschließend mit gtk_label_set_text() dem Textfeld übergeben. Dieses Textfeld zeigt also das aktuelle Datum an.

Die XML-Datei wird im Hauptprogramm mit gtk_builder_add_from_file() eingebunden. Diese Funktion erwartet das Builder-Objekt, einen Dateinamen und einen Zeiger auf ein Fehler-Array.

Werkzeugleiste Bearbeiten

Werkzeugleisten bieten einen Schnellzugriff auf häufig benötigte Funktionen. Viele Anwendungen setzen auf mehr als nur eine einzelne Leiste, mit der wir uns im folgenden Beispiel begnügen. Als Hauptwidget setzen wir ein Editor ein, dessen eingefügten Text wir in Fettschrift darstellen können. Sie können in der Anwendung auf einen Knopf drücken, und ihr gesamter Text erscheint in Fettschrift.

Hier die XML-Beschreibung (als < toolbar1.xml > Datei speichern):

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<interface>
    <object class="GtkUIManager" id="uimanager">
        <child>
            <object class="GtkActionGroup" id="aktionen">
                <child>
                    <object class="GtkAction" id="datei">
                        <property name="label">_Datei</property>
                    </object>
                </child>
                <child>
                    <object class="GtkAction" id="exit">
                        <property name="label">_Beenden</property>
                        <property name="stock-id">gtk-quit</property>
                        <signal name="activate" handler="gtk_main_quit"/>
                    </object>
                </child>
                <child>
                    <object class="GtkAction" id="bearbeiten">
                        <property name="label">_Bearbeiten</property>
                    </object>
                </child>
                <child>
                    <object class="GtkAction" id="fettschrift">
                        <property name="label">_fett</property>
                        <property name="stock-id">gtk-bold</property>
                        <signal name="activate" handler="fettschreiben" object="textview" />
                    </object>
                </child>
                <child>
                    <object class="GtkAction" id="kursivschrift">
                        <property name="label">_kursiv</property>
                        <property name="stock-id">gtk-italic</property>
                    </object>
                </child>
                <child>
                    <object class="GtkAction" id="normalschrift">
                        <property name="label">_normal</property>
                    </object>
                </child>
            </object>
        </child>
        <ui>
            <menubar name="menubar">
                <menu action="datei" >
                    <menuitem action="exit" />
                </menu>
                <menu action="bearbeiten">
                    <menuitem action="fettschrift" />
                    <menuitem action="kursivschrift" />
                    <menuitem action="normalschrift" />
                </menu>
            </menubar>
            <toolbar  name="toolbar1" >
                <toolitem name="fett" action="fettschrift" />
                <toolitem name="kursiv" action="kursivschrift" />
                <toolitem name="normal" action="normalschrift" />
            </toolbar>
        </ui>
    </object>
 
    <object class="GtkWindow" id="hauptfenster" >
        <signal name="destroy" handler="gtk_main_quit"/>
        <child>
            <object class="GtkVBox" id="vbox-layout">
                <property name="homogeneous">FALSE</property>
                <child>
                    <object class="GtkMenuBar" id="menubar" constructor="uimanager" />
                    <packing>
                        <property name="expand">FALSE</property>
                        <property name="fill">FALSE</property>
                    </packing>
                </child>
                <child>
                    <object class="GtkToolbar" id="toolbar1" constructor="uimanager" />
                    <packing>
                        <property name="expand">FALSE</property>
                        <property name="fill">FALSE</property>
                    </packing>
                </child>
                <child>
                    <object class="GtkTextView" id="textview" />
                </child>
                <child>
                    <object class="GtkLabel" id="mein-label-1">
                        <property name="label">Kleiner Editor</property>
                    </object>
                    <packing>
                        <property name="expand">FALSE</property>
                        <property name="fill">FALSE</property>
                    </packing>
                </child>
            </object>
        </child>
    </object>
</interface>

Innerhalb des Menüs hat sich etwas getan. Im Bearbeiten-Menü kommen die Menüeinträge „fettschrift“, „kursivschrift“ und „normalschrift“ hinzu. Die dazugehörigen Aktionen haben neue Piktogramme, auf die wir per Stock-Id verweisen. Allerdings ist lediglich die Aktion „fettschreiben“ mit einer Callback-Funktion verknüpft. Neben einem Menü gibt es in der Ui-Beschreibung noch eine Werkzeugleisten-Beschreibung. Die einzelnen Werkzeugknöpfe bekommen Namen und werden, wie Menüknöpfe auch, mit Aktionen verknüpft. Werkzeugknöpfe und Menüknöpfe teilen sich so die gleichen Piktogramme und verweisen gegebenenfalls auf die gleichen Callback-Funktionen.

Innerhalb der Beschreibung für das Hauptfenster wird nun nicht mehr nur das Menü über das „constructor“-Attribut mit dem Ui-Manager verknüpft, sondern auch die Werkzeugleiste vom Typ GtkToolbar. Damit hat man ein Menü, darunter eine Werkzeugleiste und als Hauptelement einen Editor (GtkTextView).

Hier das Programm dazu:

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


void fettschreiben (GtkWidget *t, gpointer d)
{
    GtkTextIter anfang, ende;
    GtkTextTag *fett;
    GtkTextTagTable *tag_tabelle;

    GtkTextView *textview = GTK_TEXT_VIEW(t);
    GtkTextBuffer *buffer = gtk_text_view_get_buffer (textview);
    /* Anfang und Ende vom Text */
    gtk_text_buffer_get_iter_at_offset (buffer, &anfang, 0);
    gtk_text_buffer_get_iter_at_offset (buffer, &ende, -1);
    /* Nachsehen, ob dieser tag schon gespeichert ist */
    tag_tabelle = gtk_text_buffer_get_tag_table (buffer);
    fett = gtk_text_tag_table_lookup (tag_tabelle, "fett_monospace");
    if (fett == NULL)
        /* sonst neu erstellen */
        fett = gtk_text_buffer_create_tag (buffer, "fett_monospace", "family", "Monospace", "weight", PANGO_WEIGHT_BOLD, NULL);
    /* tag anwenden */
    gtk_text_buffer_apply_tag (buffer, fett, &anfang, &ende);
}


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

    gtk_init (&argc, &argv);
    builder = gtk_builder_new ();
    gtk_builder_add_from_file (builder, "toolbar1.xml", &errors);
    gtk_builder_connect_signals (builder, builder);
    window = GTK_WIDGET(gtk_builder_get_object (builder, "hauptfenster"));
    gtk_widget_show_all (window);
    gtk_main ();
    return 0;
}

Von den drei Textattributen haben wir lediglich Fettschrift implementiert. Wird dieses aus dem Menü oder der Werkzeugleiste aufgerufen, wird die Callback-Funktion fettschreiben() aktiviert. Der erste übergebene Parameter ist ein Zeiger auf den Editor vom Typ GtkTextView. Der Editor hat einen Textpuffer, in dem der gesamte aktuell enthaltene Text gespeichert ist. Dieser Puffer ist vom Typ GtkTextBuffer und kann mit der Funktion gtk_text_view_get_buffer() ermittelt werden. Anfang und Ende des Textes kann man mit gtk_text_buffer_get_iter_at_offset() ermitteln. Der Anfang des Textes ist dabei die Position 0 und das Ende die Position -1. Mit dem Aufruf dieser Funktionen erhält man in den Variablen anfang und ende zwei Positionsmarkierungen, die so lange gültig sind, wie der zugehörige Puffer nicht verändert wurde. Mit Hilfe dieser Positionsmarkierungen vom Typ GtkTextIter kann man zum Beispiel Text kopieren, markieren oder dem Text eine Gestaltung geben, wie zum Beispiel einen neuen Font oder wie in diesem Beispiel Fettschrift.

Hierzu wird ein so genannter „tag“, also eine Textauszeichnung, mit der Funktion gtk_text_buffer_create_tag() erzeugt und in einer Auszeichnungstabelle abgelegt. Jede Textauszeichnung hat einen Namen, in unserem Beispiel „fett_monospace“. Diesen Namen kann man frei vergeben. Wir wollen diese Auszeichnung erstellen, wobei wir als Font-Familie „Monospace“ verwenden und als Font-Gewicht fett (PANGO_WEIGHT_BOLD) verwenden. Die Liste der Auszeichnungen wird beendet, in dem als letzter Parameter von gtk_text_buffer_create_tag() NULL verwendet wird.

Da wir darauf achten müssen, nicht bei jedem Aufruf eine neue Auszeichnung mit gleichem Namen zu erzeugen, müssen wir nachsehen, ob die Auszeichnung schon in der Auszeichnungstabelle enthalten ist. Wir besorgen uns also die Tabelle mit gtk_text_buffer_get_tag_table() und schauen mit gtk_text_tag_table_lookup() nach, ob unsere benannte Auszeichnung schon enthalten ist. Ist sie es, brauchen wir keine neue Textauszeichnung zu erstellen.

Die Textauszeichnung wird mit Hilfe der Funktion gtk_text_buffer_apply_tag() angewendet. Wir benötigen hierzu den Puffer, die Auszeichnung, sowie den Anfang wie auch das Ende des Bereiches, wo der Tag angewendet wird. Der Text wird sofort mit der neuen Gestaltung im Editor angezeigt.

Zusammenfassung Bearbeiten

Sie haben in diesem Kapitel kennen gelernt, wie man typische Elemente wie Menüs und Werkzeugleisten im Hauptfenster durch XML-Beschreibungen erzeugt und kennen nun zwei Varianten, diese einzubinden. Ebenfalls kennen Sie einige zusätzliche Widgets wie einen Texteditor, einen Nachrichtendialog und einen Kalender, den Sie in ihren Programmen verwenden können.