GTK mit Builder: Listen und Bäume

Listen- und Baumansichten

Bearbeiten

In diesem Kapitel geht es um die Darstellung von Informationen mit Hilfe von Listen und Bäumen. Diese Gruppe von Klassen ist für Einsteiger in die GTK+-Programmierung oft der schwierigste Teil, weil hierbei durch das „Model-View-Controller“-Design viele Klassen ineinander greifen. Wir haben uns in diesem Kapitel bemüht, die Beispiele einfach zu gestalten. Im Prinzip haben wir Widgets, die etwas anzeigen können (GtkTreeView und GtkComboBox) sowie die eigentlichen Daten, die in einem Modell vorliegen. Die Basisklasse für die Modelle ist GtkTreeModel. Wir benutzen zwei Implementationen von GtkTreeModel, nämlich GtkListStore, um Listen anzuzeigen und GtkTreeStore, um baumartige Darstellungen zu realisieren.

Wir beginnen wie üblich mit der XML-Beschreibung des Programms. Sie finden hier zwei Objekte auf der Hauptebene vor, nämlich GtkListStore, ein Modell zur Realisierung einer einfachen Liste und das Hauptfenster, in dem sich zur Darstellung der Liste ein Objekt vom Typ GtkTreeView befindet:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<interface>
    <object class="GtkListStore" id="modell-zur-ansicht">
    <columns>
        <column type="gchararray"/>
        <column type="gchararray"/>
        <column type="gint"/>
    </columns>
    <data>
    <row>
        <col id="0">Hans</col>
        <col id="1">Meier</col>
        <col id="2">23</col>
    </row>
    <row>
        <col id="0">Petra</col>
        <col id="1">Schmidt</col>
        <col id="2">45</col>
    </row>
    </data>
    </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="GtkTreeView" id="ansicht1" >
            <property name="model">modell-zur-ansicht</property>
            <child>
                <object class="GtkTreeViewColumn" id="test-column-1">
                <property name="title">Vorname</property>
                <child>
                    <object class="GtkCellRendererText" id="test-renderer-1"/>
                    <attributes>
                        <attribute name="text">0</attribute>
                    </attributes>
                </child>
                </object>
            </child>
            <child>
                <object class="GtkTreeViewColumn" id="test-column-2">
                <property name="title">Name</property>
                <child>
                    <object class="GtkCellRendererText" id="test-renderer-2"/>
                    <attributes>
                        <attribute name="text">1</attribute>
                    </attributes>
                </child>
                </object>
            </child>
            <child>
                <object class="GtkTreeViewColumn" id="test-column-3">
                <property name="title">Alter</property>
                <child>
                    <object class="GtkCellRendererText" id="test-renderer-3">
                    <property name="editable">TRUE</property>
                    <property name="editable-set">TRUE</property>
                    <signal name="edited" handler="text_wurde_geaendert"/>
                    </object>
                    <attributes>
                        <attribute name="text">2</attribute>
                    </attributes>
                </child>
                </object>
            </child>
            </object>
            <packing>
                <property name="expand">FALSE</property>
                <property name="fill">FALSE</property>
            </packing>
        </child>
        <child>
            <object class="GtkLabel" id="mein-label-1">
            <property name="label">Eine Anwendung mit GtkTreeView</property>
            </object>
        </child>
        </object>
    </child>
    </object>
</interface>

Das Modell mit der Id „modell-zur-ansicht“ besteht aus drei Spalten, diese sind vom Typ „gchararray“, was schlicht einer Zeichenkette entspricht und „gint“, was eine Zahl repräsentiert. In diesem Beispiel haben wir in der XML-Datei schon einige Datenzeilen eingebracht. Diese einzelnen Zeilen haben ihrerseits eine eigene Id, die zur Spalte gehört, auf die später verwiesen wird.

Das Objekt von Typ GtkTreeView referenziert nun dieses Modell in der Beschreibung. Damit stellt dieses Objekt also genau die vorher definierte Liste dar. Die einzelnen Spalten der Liste werden als Objekte vom Typ GtkTreeViewColumn repräsentiert. Diese Spalten können eine Spaltenüberschrift haben und bilden zusammen eine Tabelle.

Zu jeder Spalte gehört ein Objekt, welches die Daten, die in dieser Spalte angezeigt werden, auch darstellen kann. So kann Text anders dargestellt werden als ein Bild oder ein Wahrheitswert. Wir beschränken uns in diesem Beispiel auf die Darstellung von Text mit der Klasse GtkCellRendererText. Wollen Sie, wie in diesem Beispiel, dass eine Spalte durch den Benutzer editierbar wird, müssen sie „editable“ und „editable-set“ auf TRUE setzen. Wurde, wie in der letzten Spalte möglich, der Text geändert, soll eine Callback-Funktion aufgerufen werden, in diesem Beispiel die Funktion text_wurde_geaendert().

Durch die Elemente <attribute name="text">0</attribute> wird die Id der Spalte des Modelles mit der aktuellen Spalte in der Ansicht verknüpft. Damit weiß die konkrete Spalte, welchen Teil des Modells sie darzustellen hat.

Hier das Programm dazu:

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


enum {
    SPALTE_VORNAME,
    SPALTE_NACHNAME,
    SPALTE_ALTER,
    ANZAHL_SPALTEN
};

void text_wurde_geaendert (GtkCellRendererText *renderer, gchar *pfad,
    gchar *neues_alter, gpointer user_data)
{
    GtkTreeIter iter;
    gchar *vorname, *name;
    gint alter;
    GtkBuilder *builder_lokal = user_data;
    g_printf ("Pfad: \"%s\", Neues Alter: \"%s\"\n", pfad, neues_alter);
    GtkTreeModel *model = GTK_TREE_MODEL(gtk_builder_get_object (builder_lokal, "modell-zur-ansicht"));
    gtk_tree_model_get_iter_from_string (model, &iter, pfad);
    /* Jetzt holen wir uns die Daten aus der geänderten Zeile */
    gtk_tree_model_get (model, &iter,
        SPALTE_VORNAME, &vorname,
        SPALTE_NACHNAME, &name,
        SPALTE_ALTER, &alter,
        -1);
    g_printf ("Daten: %s %s %d...", vorname, name, alter);
    /* Daten setzen */
    gtk_list_store_set (GTK_LIST_STORE(model), &iter,
        SPALTE_VORNAME, vorname,
        SPALTE_NACHNAME, name, 
        SPALTE_ALTER, atoi (neues_alter),
        -1);
    g_printf ("wurden geändert\n");
    /* Aufräumen */
    g_free (vorname);
    g_free (name);
}

int main (int argc, char *argv[])
{
    GError *errors = NULL;
    GtkWidget *window;
    GtkBuilder *builder;
    
    gtk_init (&argc, &argv);
    builder = gtk_builder_new ();
    gtk_builder_add_from_file (builder, "tree1.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;
}

Damit wir nicht mit Zahlen operieren müssen, um die einzelnen Spalten des Modells anzusprechen, benutzen wir eine C-Aufzählung. Auf diese Weise können wir Spalten, die in der XML-Beschreibung vorliegen, im Programm nutzbar machen.

Die Funktion text_wurde_geaendert() wird aufgerufen, wenn in der letzten Spalte Text geändert wurde. In diesem Fall werden uns als Parameter die betreffende Instanz des GtkCellRendererText mitgegeben, eine Pfad, der in diesem Fall einen Text enthält, der die betreffende Zeile kennzeichnet, sowie der geänderte Text. Der letzte Parameter ist wie üblich ein Zeiger auf das Builder-Objekt. Hier wird es nun darum gehen, den geänderten Text auch zu speichern.

Aus der Liste der Objekte, die der Builder verwaltet, besorgen wir uns das Modell. Nun fehlt uns noch die betreffende Datenzeile in der Tabelle, in der Text geändert wurde. Diese Zeile wird durch einen Verweis vom Objekt vom Typ GtkTreeIter repräsentiert und von der Funktion gtk_tree_model_get_iter_from_string() übergeben. Der Grund dafür, eine eigene Struktur für Verweise auf Datenzeilen einzuführen ist, dass man über die einzelnen Zeilen auch iterieren können möchte. Bitte beachten Sie, dass man sich auf Iteratoren nur so lange verlassen kann, wie keine weiteren Änderungen an den Daten erfolgt. Sie werden dann sofort ungültig und verweisen nicht mehr auf die zu erwartenden Daten.

Mit gtk_tree_model_get() besorgen wir uns die aktuell in der Modellzeile gespeicherten Daten. Dieser Funktion übergeben wir die Nummer der Spalte und einen Zeiger auf abzulegende Daten. Dies wiederholen wir innerhalb der Funktion für alle Spalten. Den Abschluss dieser Liste bildet der Wert „-1“.

Wir könnten nun den neuen Wert darauf prüfen, ob dieser echt numerisch ist, so wie wir es verlangen oder schauen, ob sich das Alter innerhalb eines sinnvollen Bereiches befindet.

Die neuen Daten setzt man mit der Funktion gtk_list_store_set() an die Stelle, an die der Iterator verweist. Auch dieser Funktion werden wieder Tupel aus Spaltennummer und Daten übergeben. Der geänderte Text in der letzten Spalte wird mit der Funktion atoi() ungeprüft in das Modell geschrieben.

Zum Abschluss werden die von gtk_tree_model() belegten Speicherplätze wieder freigegeben.

Zusammenfassend müssen sie folgendes tun, um geänderte Daten zu lesen und wieder zu schreiben: Sie besorgen sich in der Callback-Funktion das Modell, mit dem Modell können Sie einen Verweis auf die Datenzeile in Form eines Iterators bekommen. Anschließend lesen Sei die Daten mit gtk_tree_model_get() und schreiben sie mit gtk_list_store_set() wieder zurück. Schon sind Sie fertig.


Manche Daten sind in natürlicher Weise hierarchisch strukturiert, zum Beispiel Abteilungen innerhalb einer Firma, XML-Dokumente, Dateisysteme oder der Warenbestand eines Gemüsehändlers. Baumartige Darstellungen geben solche Sachverhalte natürlich wieder. Die folgende Anwendung gib Ihnen einen Einblick in Programmierung mit dem Modell GtkTreeStore.

Hier zunächst die XML-Beschreibung zu einem Programm, welches den Warenbestand eines Gemüsehändlers notiert. Sie finden auf der Hauptebene statt eines GtkListStore-Objektes ein GtkTreeStore-Objekt vor.

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<interface>
    <object class="GtkTreeStore" id="modell-zur-ansicht">
    <columns>
        <column type="gchararray"/>
        <column type="gint"/>
    </columns>
    </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="GtkTreeView" id="ansicht1" >
            <property name="model">modell-zur-ansicht</property>
            <child>
                <object class="GtkTreeViewColumn" id="test-column-1">
                <property name="title">Produkt</property>
                <child>
                    <object class="GtkCellRendererText" id="test-renderer-1"/>
                    <attributes>
                        <attribute name="text">0</attribute>
                     </attributes>
                </child>
                </object>
            </child>
            <child>
                <object class="GtkTreeViewColumn" id="test-column-2">
                <property name="title">Anzahl</property>
                <child>
                    <object class="GtkCellRendererText" id="test-renderer-2">
                    <property name="editable">TRUE</property>
                    <property name="editable-set">TRUE</property>
                    <signal name="edited" handler="text_wurde_geaendert"/>
                     </object>
                    <attributes>
                        <attribute name="text">1</attribute>
                     </attributes>
                </child>
                </object>
            </child>
            </object>
            <packing>
            	<property name="expand">FALSE</property>
            	<property name="fill">FALSE</property>
            </packing>
        </child>
        <child>
            <object class="GtkLabel" id="mein-label-1">
            <property name="label">Eine Anwendung mit GtkTreeView</property>
            </object>
        </child>
        </object>
    </child>
    </object>
</interface>

Wie Sie sehen, hat sich innerhalb der Beschreibung des Hauptfensters nichts getan. Wir können also mit derselben Ansicht ein anderes Modell darstellen. Das Objekt vom Typ GtkTreeStore haben wir einfacher dargestellt. Es enthält nicht mehr konkrete Daten, das Eintragen überlassen wir dem Hauptprogramm. Außerdem behandeln wir nur noch zwei Spalten.

Das Hauptprogramm enthält nun eine Funktion namens gtks_next_topmodel(), in dem die Tomaten und Äpfel des Gemüsehändlers in das Modell geschrieben werden und die zum obigen Beispiel unwesentlich veränderte Funktion text_wurde_geaendert(), mit dem neue Bestandszahlen eingetragen werden können:

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


typedef struct _Artikel {
    const gchar *artikelname;
    gint anzahl;
    struct _Artikel *weitere_artikel;
} Artikel;


enum {
    SPALTE_ARTIKEL,
    SPALTE_ANZAHL,
    ANZAHL_SPALTEN
};


static Artikel tomaten[] = {
    {"aus Holland", 2000, NULL},
    {"aus Deutschland", 1000, NULL}
};
static gint anzahl_tomaten = sizeof(tomaten) / sizeof(tomaten[0]);

static Artikel artikel[] = {
    {"Tomaten", 3000, tomaten},
    {"Äpfel", 2, NULL}
};
static gint anzahl_artikel = sizeof(artikel) / sizeof(artikel[0]);


static void gtks_next_topmodel (GtkBuilder *builder_lokal)
{
    gint i, j;
    GtkTreeIter iter, iter2;
    GtkTreeStore *model = GTK_TREE_STORE(gtk_builder_get_object (builder_lokal,
	"modell-zur-ansicht"));
    for (i = 0; i < anzahl_artikel; i++)
    {
        gtk_tree_store_prepend (model, &iter, NULL);
        gtk_tree_store_set (model, &iter, SPALTE_ARTIKEL, artikel[i].artikelname, SPALTE_ANZAHL, artikel[i].anzahl, -1);
        if (artikel[i].weitere_artikel != NULL)
        {
            for (j = 0; j < anzahl_tomaten; j++)
            {
                gtk_tree_store_append (model, &iter2, &iter);
                gtk_tree_store_set (model, &iter2, SPALTE_ARTIKEL, tomaten[j].artikelname, SPALTE_ANZAHL, tomaten[j].anzahl, -1);
            }
        }
    }
}


void text_wurde_geaendert (GtkCellRendererText *renderer, gchar *pfad, gchar *neuer_text, gpointer user_data)
{
    GtkTreeIter iter;
    gchar *artname;
    gint anzahl;
    GtkBuilder *builder_lokal = user_data;
    GtkTreeModel *model = GTK_TREE_MODEL(gtk_builder_get_object (builder_lokal, "modell-zur-ansicht"));
    gtk_tree_model_get_iter_from_string (model, &iter, pfad);
    /* Jetzt holen wir uns die Daten aus geänderter Zeile */
    gtk_tree_model_get (model, &iter,
      SPALTE_ARTIKEL, &artname,
      SPALTE_ANZAHL, &anzahl,
      -1);
    /* Daten setzen */
    gtk_tree_store_set (GTK_TREE_STORE(model), &iter,
      SPALTE_ARTIKEL, artname,
      SPALTE_ANZAHL, atoi (neuer_text),
      -1);
    g_free (artname);
}


int main (int argc, char *argv[])
{
    GError *errors = NULL;
    GtkWidget *window;
    GtkBuilder *builder;
    
    gtk_init (&argc, &argv);
    builder = gtk_builder_new ();
    gtk_builder_add_from_file (builder, "tree2.xml", &errors);
    gtk_builder_connect_signals (builder, builder);
    window = GTK_WIDGET(gtk_builder_get_object (builder, "hauptfenster"));
    gtks_next_topmodel (builder);
    gtk_widget_show_all (window);
    gtk_main ();
    return 0;
}

In diesem Programm finden wir die zwei Spalten „Artikel“ und „Anzahl“ vor. Die Artikel bekommen eine eigene Datenstruktur, die aus einem Artikelnamen und einer Zahl sowie einem Zeiger auf weitere Daten besteht. Mit diesem Typ erstellen wir zwei vorbelegte Felder für Tomaten und Äpfel und lassen vom Programm die Anzahl der Elemente in diesen so erstellten Feldern berechnen. Auf diese Weise können wir im Programm „sprechende Variablen“ verwenden und können für Testzwecke schnell weitere Elemente dazu nehmen, ohne an vielen Stellen Änderungen im Quelltext vornehmen zu müssen.

In gtks_next_topmodel() geht es darum, diese Daten dem Modell hinzuzufügen. Wir benötigen hierfür unser Modell, das wir aus dem Builder-Objekt extrahieren. Dann durchlaufen wir für die Hauptebene („Äpfel“, „Tomaten“) eine for-Schleife. In dieser besorgen wir uns mit gtk_tree_store_prepend() einen Iterator, um Elemente vor anderen Elementen einzufügen. Ob man hier gtk_tree_store_prepend() oder gtk_tree_store_append() benutzt ist schlicht Geschmackssache. Im Fall von gtk_tree_store_append() werden Daten hinten angehängt. Wir benutzen in diesem Programm beide Funktionen.

Hat man diesen Iterator, dann kann man mit gtk_tree_store_set() Daten hinzufügen, und zwar für jede Spalte die passenden Daten.

Wenn, wie im Fall von „Tomaten“, ein Datensatz untergeordnete Daten enthält, dann wird eine weitere for-Schleife aufgerufen, die diese Daten einfügt. Hier wird ein neuer Iterator mit gtk_tree_store_append() erzeugt, der auf dem alten Iterator basiert. Es ist ein so genannter Kind- oder Unteriterator. Mit diesem Unteriterator werden die speziellen Tomaten eingefügt.


Modelle mit der ComboBox

Bearbeiten

Nicht nur die Klasse GtkTreeView kann Modelle darstellen, sondern auch Objekte vom Typ GtkComboBox. Wir benutzen im folgenden Programm und der XML-Beschreibung dasselbe Modell wie im ersten Beispiel, um Ihnen zu zeigen, dass nicht nur eine Ansicht (View) verschiedene Modelle (Model) darstellen kann, sondern ein Modell auch durch verschiedene Ansichten darstellbar ist. Das ist der große Vorteil der Model-View-Controller-Abstraktion, die Sie in vielen Programmierbibliotheken vorfinden können.

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<interface>
    <object class="GtkListStore" id="modell-zur-ansicht">
    <columns>
        <column type="gchararray"/>
        <column type="gchararray"/>
        <column type="gint"/>
    </columns>
    <data>
    <row>
        <col id="0">Hans</col>
        <col id="1">Meier</col>
        <col id="2">23</col>
    </row>
    <row>
        <col id="0">Petra</col>
        <col id="1">Schmidt</col>
        <col id="2">45</col>
    </row>
    </data>
    </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="GtkComboBox" id="combo-box-1" >
            <property name="model">modell-zur-ansicht</property>
            <property name="active">1</property>
            <signal name="changed" handler="ausgewaehlt"/>
            <child>
                <object class="GtkCellRendererText" id="test-renderer-1"/>
                <attributes>
                    <attribute name="text">0</attribute>
                 </attributes>
            </child>
            <child>
                <object class="GtkCellRendererText" id="test-renderer-2"/>
                <attributes>
                    <attribute name="text">1</attribute>
                 </attributes>
            </child>
            <child>
                <object class="GtkCellRendererText" id="test-renderer-3"/>
                <attributes>
                    <attribute name="text">2</attribute>
                 </attributes>
            </child>
            </object>
            <packing>
            	<property name="expand">FALSE</property>
            	<property name="fill">FALSE</property>
            </packing>
        </child>
        <child>
            <object class="GtkLabel" id="mein-label-1">
            <property name="label">Eine Anwendung mit GtkComboBox</property>
            </object>
        </child>
        </object>
    </child>
    </object>
</interface>

Innerhalb dieser Beschreibung referenziert das Objekt vom Typ GtkComboBox das Modell vom Typ GtkListStore. Emittiert die Klappbox das „changed“-Signal, weil der angezeigte Wert geändert wurde, dann wird im Hauptprogramm die Funktion ausgewaehlt() aufgerufen. Innerhalb der Klappbox gibt es keine Spaltenüberschriften, wie sie im ersten Beispiel dieses Kapitels angefügt waren. Stattdessen benutzen wir ohne Umwege GtkCellRendererText, um Zeilenfelder darzustellen.

Der Quelltext dazu zeigt, wie man mit ausgewählten Zeilen umgehen kann:

C
#include <gtk/gtk.h>

enum {
    SPALTE_VORNAME,
    SPALTE_NACHNAME,
    SPALTE_ALTER,
    ANZAHL_SPALTEN
};


void ausgewaehlt (GtkComboBox *combo, GtkBuilder *builder_lokal)
{
    GtkTreeIter iter;
    if (gtk_combo_box_get_active_iter (combo, &iter))
    {
        gchar *vorname, *nachname, label_text[128];
        gint alter;
        /* Auswahl besorgen */
        GtkTreeModel *model = GTK_TREE_MODEL(gtk_builder_get_object (builder_lokal, "modell-zur-ansicht"));
        gtk_tree_model_get (model, &iter, SPALTE_VORNAME, &vorname, SPALTE_NACHNAME, &nachname, SPALTE_ALTER, &alter, -1);
        /* Anzeige der Auswahl im Hauptfenster, Statuszeile */
        g_snprintf (label_text, 128, "<b>%s %s</b> <i>%d Jahre</i>\n", vorname, nachname, alter);
        GtkLabel *status_zeile = GTK_LABEL(gtk_builder_get_object (builder_lokal, "mein-label-1"));
        gtk_label_set_markup (status_zeile, label_text);
        /* Aufräumen */
        g_free (vorname);
        g_free (nachname);
    }
}

int main (int argc, char *argv[])
{
    GError *errors = NULL;
    GtkWidget *window;
    GtkBuilder *builder;
    
    gtk_init (&argc, &argv);
    builder = gtk_builder_new ();
    gtk_builder_add_from_file (builder, "combo1.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 ausgewählt() wird aufgerufen, wenn die Klappbox betätigt wurde. In diesem Fall bekommt man mit gtk_combo_box_get_active_iter() einen Verweis auf die aktuelle Datenzeile. Nun benötigt man, noch das Modell und kann anschließend mit gtk_tree_model_get() auf die Daten zugreifen. Der in der Klappbox ausgewählte Text wird dann verwendet, um ihn in der Textzeile am unteren Rand des Hauptfensters anzuzeigen.

Zusammenfassung

Bearbeiten

In diesem Kapitel haben sie zwei Modelle und zwei Ansichten für Modelle kennen gelernt. Wir haben Ihnen vorgeführt, wie man Daten in Modelle schreibt und wieder liest. Ebenfalls kennen Sie nun den Verwendungszweck der unterschiedlichen Modelle und können bei konkreten Aufgaben entscheiden, welchem Sie den Vorzug geben.