Zum Inhalt springen

GTK mit Builder/ Alles

Aus Wikibooks

Vorwort

[Bearbeiten]

Einleitung

[Bearbeiten]

Herzlich Willkommen zu GTK+ mit Builder. GTK+ ist eine Bibliothek, mit deren Hilfe Sie ansprechende grafische Benutzeroberflächen schreiben können. Sei es eine Steuerung für Modelleisenbahnen, eine Planungssoftware für Flugplätze oder ein Spiel, all diese Programme benötigen grafische Benutzeroberflächen und all diese Programme lassen sich unter Verwendung von GTK+ realisieren[1]. Wenn Sie dieses Buch durchgearbeitet haben, dann „können“ Sie GTK+, ohne jedes Detail zu wissen. Wir stellen in diesem Buch den Builder in den Vordergrund, weil moderne Programmierung nicht mehr bedeutet, seine grafischen Elemente über die Tastatur in einem Layout zu platzieren. Es gibt Programme wie Glade, die Ihnen diese Arbeit abnehmen. Der Builder ist hierbei ein Funktionsaufruf, der eine XML-Beschreibung einliest, in der ihr komplettes grafisches Programmlayout gespeichert ist.

Zusammenspiel der Bibliotheken

[Bearbeiten]

Die Bibliothek GTK+ stellt Ihnen die grafischen Elemente wie Knöpfe, Tabellen und Zeichenwerkzeuge zur Verfügung. Sie baut auf einer Reihe weiterer Bibliotheken auf, darunter

  • GLib, eine Bibliothek, die grundlegende Datentypen enthält, Funktionen zur Manipulation und Ausgabe von Zeichenketten, Unterstützung für mehrfädige Programmierung (Multi-Threading), Datums- und Zeitfunktionen und vieles mehr,
  • Pixbuf, eine Bibliothek zum Laden, Speichern und Manipulieren von Bildinformationen,
  • Pango, eine Bibliothek, die Text grafisch setzen und manipulieren kann,
  • GDK, eine Bibliothek, die unter anderem eine Abstraktionsschicht zwischen dem zugrunde liegenden Fenstersystem, zum Beispiel X11, und GTK+ bildet.

Mit Elementen aus diesen Bibliotheken kommen Sie in diesem Buch in Kontakt.

Hinweis an Leser

[Bearbeiten]

Dieses Buch richtet sich an Menschen mit Erfahrung in der Programmiersprache C. Vor dem Schreiben dieses Buches hat der Autor lange überlegt, welcher der vielen Sprachanbindungen, die es für GTK+ gibt, er den Vorzug geben soll. Da man es im Grunde genommen niemandem Recht machen kann, hoffen wir, mit dieser Entscheidung die wenigsten Leser abzuschrecken. Die Programmbeispiele wurden alle mit dem GNU C-Compiler unter Linux getestet. Dieses Buch wurde mit dem Hintergrund geschrieben, dass Sie sich in GTK+ einarbeiten wollen. Der Autor hat aus verschiedenen Gründen darauf verzichtet, jeden vorkommenden neuen Funktionsaufruf ausführlichst darzustellen. Zum Einen ist dieses Buch kein Referenzwerk und möchte die vorhandene API-Dokumentation, die Sie im Netz finden, nicht ersetzen. Zum Anderen ist es als Programmierer unerlässlich, sich in der API auszukennen. Nutzen Sie also die Gelegenheit und schlagen Sie jeden einzelnen Funktions-Aufruf und jede benannte Konstante, die wir im Programm vorstellen, in der API-Dokumentation nach. So lernen Sie mit dieser umzugehen und vertiefen Ihr Wissen. Wollen Sie dieses Buch durcharbeiten, so heißt das, jedes Beispiel zu nehmen und zu modifizieren. Stellen Sie sich kleine Aufgaben mit jedem Programm, denn programmieren lernt man nicht vom Zuschauen. ;-)

Wir wünschen Ihnen viel Spaß mit dem Buch!

Tandar, Mai 2011



Erste Schritte

[Bearbeiten]

Hallo, Welt!

[Bearbeiten]

Das erste GTK+ 3.x Programm soll ein Fenster erzeugen, in dem sich eine Beschriftung mit dem bekannten Text „Hallo, Welt!“ befindet.

C
#include <gtk/gtk.h>

static void on_window_closed (GtkWidget *widget, gpointer data)
{
    gtk_main_quit ();
}

int main (int argc, char *argv[])
{
    GtkWidget *window, *label;

    /* Initialisieren */
    gtk_init (&argc, &argv);
    /* Fenster erzeugen */
    window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    /* "destroy"-Ereignis mit einer Funktion verknüpfen */
    g_signal_connect (window, "destroy", G_CALLBACK(on_window_closed), NULL);
    /* Textfeld erzeugen */
    label = gtk_label_new ("Hallo, Welt!");
    /* Textfeld dem Fenster hinzufügen */
    gtk_container_add (GTK_CONTAINER(window), label);
    /* Alle erzeugten Widgets anzeigen lassen */
    gtk_widget_show (label);
    gtk_widget_show (window);
    /* Ereignisschleife */
    gtk_main ();

    return 0;
}
Ein "Hallo Welt"-Fenster

Innerhalb der main()-Funktion werden zwei Widgets deklariert. Widgets sind in GTK+ alle vom Benutzer sichtbaren Dinge in einem Programm. Hierzu gehören Knöpfe, Menüs, Textfelder, Fenster, Schieberegler und viele andere Dinge.

Ein GTK+-Programm wird am Anfang mit der Funktion gtk_init() initialisiert. Diese Funktion hat die Aufgabe, einige Standardargumente aus einer eventuell bereitgestellten Menge an Programmparametern zu filtern, wie auch das eigentliche Toolkit zu initialisieren. Wenn keine Kommunikation mit dem Fenstermanager möglich ist, dann sorgt diese Funktion dafür, dass das Programm sofort terminiert. Eine Alternative zu gtk_init() ist gtk_init_check(). Diese Funktion gibt FALSE zurück, wenn GTK+ nicht initialisiert werden kann und ermöglicht es Ihnen somit, alternativ eine Textoberfläche statt der erwarteten grafischen Benutzeroberfläche bereitzustellen.

Das Hauptfenster wird mit der Funktion gtk_window_new() erzeugt. Der Parameter kann entweder GTK_WINDOW_TOPLEVEL für normale Hauptfenster und Dialoge oder GTK_WINDOW_POPUP für undekorierte Hilfefenster, so genannte „Tooltips“, sein.

Widgets in GTK+ reagieren auf Ereignisse, so genannte „Events“. Diese Ereignisse werden zumeist durch Benutzerinteraktion ausgelöst. Wenn Sie einen Knopf drücken, ein Menü auswählen oder an einem Schieberegler ziehen werden Ereignisse generiert, auf die das Programm reagieren kann. In unserem einfachen Beispiel wird lediglich auf das „destroy“-Ereignis reagiert, das immer dann ausgesendet wird, wenn das Fenster geschlossen wird. Hierzu wird das Ereignis „destroy“ mit Hilfe der Funktion g_signal_connect() mit einer Funktion verknüpft, die aufgerufen wird, wenn das Ereignis eintritt. In unserem Fall heißt die zu verknüpfende Funktion on_window_closed(). Im „data“-Argument steht beim Aufruf genau das, was Sie als letztes Argument in der Funktion g_signal_connect() hingeschrieben haben. Dieser Parameter wird also durchgereicht und hilft bei der Kommunikation zwischen verschiedenen Teilen des Programms. Hier können Sie beispielsweise mitteilen, ob in einer Textverarbeitung der aktuelle Text noch gespeichert werden muss.

Textfelder werden mit gtk_label_new() erzeugt. Der Inhalt der Textfelder kann mehrzeilig sein und mit einer einfachen Auszeichnungssprache erzeugt werden. Das Textfeld wird mit der Funktion gtk_container_add() dem Fenster hinzugefügt. Das im Parameter genutzte Makro GTK_CONTAINER() deutet schon an, dass man Widgets auch anderen Widgets hinzufügen kann, nicht nur Fenstern.

Anschließend werden die im Programm erzeugten Widgets mit gtk_widget_show() angezeigt. Statt für jedes Widget einzeln hätten wir auch mit gtk_widget_show_all() das Hauptfenster wie auch das Textfeld mit einem Aufruf anzeigen lassen können. Mit der Funktion gtk_widget_hide() können wir übrigens einzelne Widgets verstecken.

Zum Schluss wird die Hauptschleife mit gtk_main() gestartet. In dieser Funktion kümmert sich das Toolkit hauptsächlich um Ereignisse, unabhängig davon, ob Sie durch den Benutzer ausgelöst wurden oder nicht. Diese Funktion kann explizit beendet werden durch einen Aufruf von gtk_main_quit(). Beachten Sie bitte, dass man Aufrufe von gtk_main() auch schachteln kann, auch wenn Sie vermutlich niemals in die Situation kommen werden. In diesem Fall beendet gtk_main_quit() die tiefste Ebene.

Das Programm erstellen Sie mit

Align=none Shell

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

Beachten Sie, dass hier rückwärtsgeneigte Hochkommata verwendet werden („Backticks“). Diese können Sie auf einer Deutschen Tastatur über die Tastenkombination <Shift> + <Taste rechts neben Fragezeichen> erzeugen. Oder Sie verwenden $( ) statt den „Backticks“:

Align=none Shell

user@localhost:~$ gcc fenster1.c -o fenster1 -Wall $(pkg-config --libs --cflags gtk+-3.0)


Hauptfenster

[Bearbeiten]

Dem Hauptfenster kann man einen Titel hinzufügen. Ebenfalls kann man dafür sorgen, dass das Fenster eine feste Ausdehnung einnimmt und sich diese Größe nicht ändern lässt.

C
#include <gtk/gtk.h>

static void on_window_closed (GtkWidget *widget, gpointer data)
{
    gtk_main_quit ();
}

int main (int argc, char *argv[])
{
    GtkWidget *window, *label;

    gtk_init (&argc, &argv);
    window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    g_signal_connect (window, "destroy", G_CALLBACK(on_window_closed), NULL);
    label = gtk_label_new ("Hallo, Welt!");
    gtk_container_add (GTK_CONTAINER(window), label);
    /* Fenstertitel anzeigen */
    gtk_window_set_title (GTK_WINDOW(window), "Mein kleines Fenster");
    /* Größe des Fensters festlegen */
    gtk_window_set_default_size (GTK_WINDOW(window), 640, 480);
    /* Keine Größenänderung des Fensters zulassen */
    gtk_window_set_resizable (GTK_WINDOW(window), FALSE);
    /* Alles anzeigen */
    gtk_widget_show_all (window);
    gtk_main ();

    return 0;
}

Der Fenstertitel wird mit dem Aufruf von gtk_window_set_title() gesetzt. Die Anfangsgröße des Fensters kann man mit dem Aufruf von gtk_window_set_default_size() festlegen. Man ermöglicht oder verhindert mit dem Aufruf von gtk_window_set_resizable() die Größenänderung des Fensters. Verhindern kann man das mit dem Parameter FALSE, wieder zulassen mit TRUE.

Da GTK+ jedoch anstrebt die Fenster-Elemente (genauer gesagt die Elemente der Klasse GtkWidget, von welchen die Klasse GtkWindow erbt) so kompakt wie möglich und so groß wie nötig zu zeichnen, kann es notwendig sein auch eine Minimal-Größe zu setzten, welche das Fenster mindestens haben soll. Ansonsten kann es sein, dass GTK+ die Fenster-Größe optimiert und nicht die gesetzte Größe gewählt wird, sondern die kompaktest-Mögliche (also nur das Text-Label). Das Setzten der Minimal-Größe kann mit folgender Zeile erreicht werden, welche einfache nach der gtk_window_set_resizable()-Methode eingefügt werde kann:

gtk_widget_set_size_request(GTK_WIDGET(window), 640, 480);

Beschriftung

[Bearbeiten]

Eine Beschriftung („Label“) kann Text enthalten, der mit HTML-ähnlichen Auszeichnungen („Markup“) versehen wurde. Die Beschriftung lässt sich auch drehen, wie folgender Code zeigt.

C
#include <gtk/gtk.h>

static void on_window_closed (GtkWidget *widget, gpointer data)
{
    gtk_main_quit ();
}

int main (int argc, char *argv[])
{
    GtkWidget *window, *label;

    gtk_init (&argc, &argv);
    window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    g_signal_connect (window, "destroy", G_CALLBACK(on_window_closed), NULL);
    label = gtk_label_new (NULL);
    /* Beschriftungstext mit Auszeichnungen */
    gtk_label_set_markup (GTK_LABEL(label),
            "<small>Kleiner Text</small>\n"
            "<big>Large</big>\n"
            "<b>Fett</b>\n"
            "<span foreground=\"#ff0000\">ROT</span>");
    /* Label um 45 Grad gegen den Uhrzeigersinn drehen */
    gtk_label_set_angle (GTK_LABEL(label), 45);
    gtk_container_add (GTK_CONTAINER(window), label);
    gtk_window_set_title (GTK_WINDOW(window), "Mein kleines Fenster");
    gtk_window_set_default_size (GTK_WINDOW(window), 640, 480);
    gtk_window_set_resizable (GTK_WINDOW(window), FALSE);
    gtk_widget_show_all (window);
    gtk_main ();

    return 0;
}

Die Funktion gtk_label_set_markup() übergibt der Beschriftung gestalteten Text. Die Markup-Sprache wird von der Schriftsatz-Bibliothek Pango bereitgestellt, die von GTK+ benutzt wird. Vertikale oder diagonale Beschriftungen lassen sich mit der Funktion gtk_label_set_angle() erzeugen, hier wird der Winkel in Dezimalgrad gegen den Uhrzeigersinn angegeben. 90 Grad bedeuten also vertikalen Text, den man von unten nach oben liest.

Druckknopf

[Bearbeiten]

Das folgende Programm demonstriert, wie man mit Knöpfen umgeht. Diese senden ein Signal aus, wenn sie gedrückt werden. Das Signal kann man abfangen und nutzen, wie das folgende Programm zeigt.

C
#include <gtk/gtk.h>

static void on_window_closed (GtkWidget *widget, gpointer data)
{
    gtk_main_quit ();
}

/* Liefert TRUE zurück, wenn gerade Zahl, FALSE, wenn ungerade */
static gboolean is_even (int number)
{
    return number % 2 == 0;
}

static void on_button_clicked (GtkWidget *widget, gpointer data)
{
    static int click_count;
    /* Jedes 2. Mal soll "Danke!" bzw "Nochmal?" ausgegeben werden */
    gtk_button_set_label (GTK_BUTTON(widget), is_even (click_count) ? "Danke!" : "Nochmal?");
    /* Anzahl der Klicks vergrößern */
    click_count++;
}

int main (int argc, char *argv[])
{
    GtkWidget *window, *button;

    gtk_init (&argc, &argv);
    window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    g_signal_connect (window, "destroy", G_CALLBACK(on_window_closed), NULL);
    /* Füge einen Druckknopf ein */
    button = gtk_button_new_with_label ("Drück mich!");
    /* Signal "draufklicken" wird verknüpft mit einer Rückruf-Funktion */
    g_signal_connect(button, "clicked", G_CALLBACK(on_button_clicked), NULL);
    gtk_container_add (GTK_CONTAINER(window), button);
    gtk_window_set_title (GTK_WINDOW(window), "Mein kleines Fenster");
    gtk_window_set_default_size (GTK_WINDOW(window), 200, 200);
    gtk_widget_show_all (window);
    gtk_main ();

    return 0;
}

Ein neuer Knopf wird mit der Funktion gtk_button_new_with_label() erzeugt. Wenn Sie keine Beschriftung im Knopf wünschen, oder eine eigene Beschriftung später einfügen wollen, reicht als Alternative auch gtk_button_new() aus. Ein Druckknopf kann verschiedene Signale aussenden. Wird die Maus über dem Knopf gedrückt und anschließend losgelassen wird das Signal „clicked“ ausgesendet, welches wir abfangen wollen. In unserem Fall wollen wir zählen, wie oft der Knopf gedrückt wurde.

In der Callback-Funktion on_button_clicked() haben wir dazu eine Zählvariable click_count vorgesehen. Diese ist statisch („static“) deklariert, damit ihr Wert nach Ende der Funktion nicht verloren geht, sondern beibehalten wird. Wurde der Knopf einmal gedrückt, so wird „Danke“ ausgegeben, beim zweiten Mal „Nochmal“ und dann wieder von vorne. Dabei hilft die von uns definierte Funktion is_even(), die uns sagt, ob eine Zahl gerade oder ungerade ist. Der Button-Text wird mit der Funktion gtk_button_set_label() gesetzt.

Zusammenfassung

[Bearbeiten]

In diesem Kapitel haben Sie die Grundlagen von GTK+ kennen gelernt. Sie kennen nun Fenster, Beschriftungen und Druckknöpfe und können diese mit einigen Funktionen manipulieren. Ebenfalls wissen Sie, dass Widgets Signale aussenden können und dass Signale mit Callbacks genutzt werden.



Arbeiten mit Layouts

[Bearbeiten]

In diesem Kapitel geht es darum, wie man mehrere Widgets gruppieren und in einem Fenster anordnen kann. Layouts werden in der Regel wirksam, wenn die Fenstergröße änderbar ist. In anderen Fällen könnte man die Widgets auch manuell platzieren, wie wir das im „freien Layout“ zeigen. Ändern Sie also beim Ausprobieren ruhig mal die Fenstergröße.

Manuelle Stretchbereiche

[Bearbeiten]

Bei manuellen Stretchbereichen (Panes) geht es darum, einen Bereich in genau zwei Teile zu teilen und die Größe durch den Benutzer festlegen zu lassen. Wir demonstrieren das am Beispiel horizontaler Stretchbereiche, wollen aber bemerken, dass es sich für vertikale analog verhält.

C
#include <gtk/gtk.h>

int main (int argc, char *argv[])
{
    GtkWidget *window, *hpanes, *buttons[2];
    gtk_init (&argc, &argv);
    window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    g_signal_connect (window, "destroy", G_CALLBACK(gtk_main_quit), NULL);
    
    hpanes = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);

    buttons[0] = gtk_button_new_with_label ("erster");
    buttons[1] = gtk_button_new_with_label ("zweiter");

    gtk_paned_add1 (GTK_PANED(hpanes), buttons[0]);
    gtk_paned_add2 (GTK_PANED(hpanes), buttons[1]);
    
    gtk_container_add (GTK_CONTAINER(window), hpanes);
    gtk_widget_show_all (window);
    gtk_main ();
    return 0;
}

In diesem Programm liegen zwei Druckknöpfe nebeneinander. Es wird mit gtk_paned_new() ein Layout erzeugt, welches zwei horizontal (oder ggf. vertikal) nebeneinander liegende Teile beinhaltet. Mit der Funktion gtk_paned_add1() und gtk_paned_add2() werden diesen Bereichen Widgets zugewiesen, in unserem Fall die beiden Knöpfe. Die Größe der Bereiche kann der Benutzer mit einem zwischen den Bereichen liegenden Regler mit der Maus vorgeben. Dem Fenster wird nun dieser Stretchbereich als Widget mit gtk_container_add() hinzugefügt.

Wollen Sie einen Bereich in zwei vertikal getrennte Bereiche teilen, so können Sie der Funktion gtk_paned_new() die Konstante GTK_ORIENTATION_VERTICAL übergeben.

Solche Stretchbereiche lassen sich bequem schachteln, wobei mehr als zwei Schachtelungsebenen schnell unübersichtlich werden. Das folgende Programm zeigt, wie man horizontale und vertikale Stretchbereiche schachtelt.

C
#include <gtk/gtk.h>

int main (int argc, char *argv[])
{
    GtkWidget *window, *hpanes, *vpanes, *buttons[3];
    gtk_init (&argc, &argv);
    window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    g_signal_connect (window, "destroy", G_CALLBACK(gtk_main_quit), NULL);
    
    /* Horizontale Stretchbereiche mit 2 Knöpfen */
    hpanes = gtk_paned_new (GTK_ORIENTATION_HORIZONTAL);
    buttons[0] = gtk_button_new_with_label ("erster");
    buttons[1] = gtk_button_new_with_label ("zweiter");
    gtk_paned_add1 (GTK_PANED(hpanes), buttons[0]);
    gtk_paned_add2 (GTK_PANED(hpanes), buttons[1]);

    /* Vertikaler Stretchbereich mit einem Knopf und obigem Bereich */
    vpanes = gtk_paned_new (GTK_ORIENTATION_VERTICAL);
    buttons[2] = gtk_button_new_with_label ("dritter");
    gtk_paned_add1 (GTK_PANED(vpanes), buttons[2]);
    gtk_paned_add2 (GTK_PANED(vpanes), hpanes);
    
    gtk_container_add (GTK_CONTAINER(window), vpanes);
    gtk_widget_show_all (window);
    gtk_main ();
    return 0;
}

In diesem Programm wurde ein horizontaler Stretchbereich erzeugt, in dem zwei Knöpfe liegen. Dieser Bereich wird zusammen mit einem weiteren Knopf einem vertikalen Stretchbereich hinzugefügt.

Tabellarisches Layout

[Bearbeiten]

Die tabellarischen Layouts GtkGrid und GtkTable ordnen Widgets tabellarisch an. Einzelne Widgets werden dem Layout hinzugefügt, in dem diese Widgets entlang gedachten Linien an den Rändern der Tabellenzellen eingefügt werden. Hat man eine Tabelle, mit zwei Zeilen und drei Spalten, so kann man beispielsweise ein Widget an die Stelle horizontal 0, 1 und vertikal 0, 1 hinzufügen, um so die Zelle links oben anzusprechen. Möchte man die gesamte letzte Zeile mit einem Widget ausfüllen, reicht horizontal 0, 2 und vertikal 2, 3 aus.

Tabellen können ihre Elemente so anordnen, dass alle Widgets die gleiche Größe bekommen. Hierfür ist ein Attribut namens „homogeneous“ (dt.: „Einheitlichkeit“) zu setzen.

Das folgende Programm zeigt das Tabellenlayout mit je drei Zeilen und Spalten. Mit einem Knopf kann man dafür sorgen, dass die Widgets gleiche Größe bekommen, obwohl sie unterschiedlich viel Platz benötigen.

C
#include <gtk/gtk.h>


static void change_homogenous (GtkWidget *widget, GtkTable *table)
{
    /* Homogenität ändern */
    gtk_table_set_homogeneous (table, !gtk_table_get_homogeneous (table));
}

int main (int argc, char *argv[])
{
    GtkWidget *window, *table, *buttons[5], *labels[4];
    gtk_init (&argc, &argv);
    window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    g_signal_connect (window, "destroy", G_CALLBACK(gtk_main_quit), NULL);

    /* Tabelle mit 3 Reihen und 3 Spalten, uneinheitliche Größe */
    table = gtk_table_new (3, 3, FALSE);

    /* Knöpfe und Texte erstellen */
    buttons[0] = gtk_button_new_with_label ("0");
    buttons[1] = gtk_button_new_with_label ("rechts oben");
    buttons[2] = gtk_button_new_with_label ("homogen?");
    g_signal_connect (buttons[2], "clicked",
	G_CALLBACK(change_homogenous), table);
    buttons[3] = gtk_button_new_with_label ("0");
    buttons[4] = gtk_button_new_with_label ("rechts unten");
    labels[0] = gtk_label_new ("N");
    labels[1] = gtk_label_new ("W");
    labels[2] = gtk_label_new ("O");
    labels[3] = gtk_label_new ("S");

    /* erste Reihe */
    gtk_table_attach_defaults (GTK_TABLE(table), buttons[0], 0, 1, 0, 1);
    gtk_table_attach_defaults (GTK_TABLE(table), labels[0], 1, 2, 0, 1);
    gtk_table_attach_defaults (GTK_TABLE(table), buttons[1], 2, 3, 0, 1);

    /* zweite Reihe */
    gtk_table_attach_defaults (GTK_TABLE(table), labels[1], 0, 1, 1, 2);
    gtk_table_attach_defaults (GTK_TABLE(table), buttons[2], 1, 2, 1, 2);
    gtk_table_attach_defaults (GTK_TABLE(table), labels[2], 2, 3, 1, 2);

    /* dritte Reihe */
    gtk_table_attach_defaults (GTK_TABLE(table), buttons[3], 0, 1, 2, 3);
    gtk_table_attach_defaults (GTK_TABLE(table), labels[3], 1, 2, 2, 3);
    gtk_table_attach_defaults (GTK_TABLE(table), buttons[4], 2, 3, 2, 3);

    gtk_container_add (GTK_CONTAINER(window), table);
    gtk_widget_show_all (window);
    gtk_main ();
    return 0;
}

Tabellen werden mit gtk_table_new() erzeugt. Man gibt hier die Anzahl der Zeilen und der Spalten an und, ob die enthaltenen Widgets gleich groß dargestellt werden. Einer Tabelle kann man mit der Funktion gtk_table_attach_defaults() ein Widget hinzufügen. Diesem Widget wird mit den letzten vier Parametern mitgeteilt, wo es sich zu befinden hat, und zwar horizontal und vertikal entlang der Begrenzungslinien.

Die erste Spalte wurde absichtlich mit Widgets gefüllt, die nur sehr wenig Text haben. Dadurch ist ihre Ausdehnung nur sehr gering. Ändert man in der Callback-Funktion change_homogeneous() den Wert für die Einheitlichkeit, werden alle Widgets gleich groß gezeichnet.

Man kann den Boole'schen Wert (also TRUE oder FALSE) für die Einheitlichkeit abfragen mit gtk_table_get_homogeneous() und setzen mit gtk_table_set_homogeneous().

Die Anzahl der Zeilen und Spalten, die eine Tabelle aufnehmen kann, kann man dynamisch zur Laufzeit ändern. Ebenfalls ist es möglich, den Abstand zwischen zwei Widgets durch einen Rand zu vergrößern.

Freies Layout

[Bearbeiten]

Unter einem freien Layout verstehen wir dasjenige Layout, bei dem Sie selbst bestimmen, wohin ihre Widgets positioniert werden. Das hierfür benötigte Layout heißt GtkLayout. Sie erhalten einen eigenen Container mit nahezu beliebiger Größe.

Das folgende Programm zeigt, wie man ein Textfeld und einen Knopf diesem Layout hinzufügt und auf Knopfdruck die Position eines Widgets verändert.

C
#include <gtk/gtk.h>


static void move_button (GtkWidget *button, GtkWidget *layout)
{
    gint x;
    gtk_container_child_get (GTK_CONTAINER(layout), button, "x", &x, NULL);
    x = x % 100 + 10;
    gtk_layout_move (GTK_LAYOUT(layout), button, x, 50);
}

int main (int argc, char *argv[])
{
    GtkWidget *window, *layout, *button, *label;
    gtk_init (&argc, &argv);
    window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    g_signal_connect (window, "destroy", G_CALLBACK(gtk_main_quit), NULL);

    layout = gtk_layout_new (NULL, NULL);

    label = gtk_label_new ("Test");
    button = gtk_button_new_with_label ("Drück mich!");
    
    gtk_layout_put (GTK_LAYOUT(layout), label, 10, 10);
    gtk_layout_put (GTK_LAYOUT(layout), button, 20, 100);
    g_signal_connect (button, "clicked", G_CALLBACK(move_button), layout);
    
    gtk_container_add (GTK_CONTAINER(window), layout);
    gtk_widget_show_all (window);
    gtk_main ();
    return 0;
}

Das Layout wird mit gtk_layout_new() bereitgestellt. Optional kann man diesem Container einen horizontalen und einen vertikalen Schieberegler mitgeben. Diese Schieberegler sind sinnvoll, wenn der Bereich sehr groß wird und man nur einen kleinen Bereich darstellen möchte.

Dem freien Layout kann man mit der Funktion gtk_layout_put() ein Widget hinzufügen, wobei man seine Position in Pixelkoordinaten angibt. Die obere linke Ecke ist dabei die Nullposition.

Drückt man auf den Knopf, wird die Funktion move_button() aufgerufen. Mit gtk_container_child_get() ermitteln wir die X-Position unseres Knopfes, modifizieren diese und bewegen mit gtk_layout_move() unseren Knopf an die neue Position.

An diesem Beispiel werden sogleich mehrere Sachen gezeigt. Ein Container kann andere Container enthalten, nicht nur Widgets. Widgets innerhalb von Containern kann man abfragen und modifizieren, in dem man ihre Attribute erfragt oder ändert. In unserem Beispiel wird mit der Funktion gtk_container_child_get() ein Widget innerhalb von GtkLayout abgefragt. Wir wollen etwas über das Attribut „x“ wissen und übergeben der Funktion darum den Attributnamen und einen Zeiger auf einen Integer, um das Ergebnis zu speichern. Da diese Funktion beliebig viele Attribute erfragen kann, wird die Liste der Paare aus Attributnamen und Speicherstelle mit einer NULL terminiert. Zu jedem Widget gibt es eine Vielzahl von Attributen, die sich setzen und abfragen lassen.

Kistenlayout

[Bearbeiten]

Beim Kistenlayout geht es darum, Widgets der Reihe nach horizontal oder vertikal anzuordnen. Hierbei stellt eine GtkVBox alle enthaltenen Widgets vertikal dar, eine GtkHBox horizontal. Verschachteltes Kistenlayout ist dasjenige Layout, welches Sie in Anwendungsprogrammen in der Regel verwenden werden. Kisten sind sehr flexibel und bieten viele Möglichkeiten, um Einfluss zu nehmen.

C
#include <gtk/gtk.h>


int main (int argc, char *argv[])
{
    GtkWidget *window, *vbox, *buttons[4];
    
    gtk_init (&argc, &argv);
    window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    g_signal_connect (window, "destroy", G_CALLBACK(gtk_main_quit), NULL);
    
    vbox = gtk_vbox_new (FALSE, 2);
    
    buttons[0] = gtk_button_new_with_label ("FALSE, FALSE");
    gtk_box_pack_start (GTK_BOX(vbox), buttons[0], FALSE, FALSE, 0);
    buttons[1] = gtk_button_new_with_label ("TRUE, FALSE");
    gtk_box_pack_start (GTK_BOX(vbox), buttons[1], TRUE, FALSE, 0);
    buttons[2] = gtk_button_new_with_label ("TRUE, TRUE");
    gtk_box_pack_start (GTK_BOX(vbox), buttons[2], TRUE, TRUE, 0);
    buttons[3] = gtk_button_new_with_label ("FALSE, TRUE");
    gtk_box_pack_start (GTK_BOX(vbox), buttons[3], FALSE, TRUE, 0);
    
    gtk_container_add (GTK_CONTAINER(window), vbox);
    gtk_widget_show_all (window);
    gtk_main ();
    return 0;
}

Eine vertikale Box wird erzeugt mit gtk_vbox_new(). Der erste Parameter bestimmt, ob allen enthaltenen Widgets der gleiche Platz zur Verfügung gestellt wird (TRUE) oder ob sich jedes Widget soviel vom Platz nimmt, wie es braucht (FALSE). Zwischen zwei eingefügten Widgets kann es zusätzlichen Leerraum geben, eine Art Abstand zwischen den Widgets. Dieser Leerraum wird mit dem zweiten Parameter gesteuert.

Der vertikalen Box werden nun einige Knöpfe von oben nach unten hinzugefügt. Dies geschieht mit der Funktion gtk_box_pack_start(). Mit den letzten drei Parametern steuert man die Art und Weise, wie der von der Box bereitgestellte Raum ausgenutzt wird.

Der dritte Parameter steuert, ob das hinzugefügte Widget ausgedehnt werden soll. Der zur Verfügung stehende Platz wird in gleichen Teilen unter allen Widgets aufgeteilt, die diesen Parameter auf „TRUE“ gesetzt haben.

Hat ein Widget nun einen bestimmten Raum zur Verfügung, kann man mit dem vierten Parameter steuern, ob es diesen Raum ausfüllen soll (TRUE) oder nicht (FALSE). Dieser Parameter wird nicht ausgewertet, wenn der dritte Parameter nicht auf „TRUE“ gesetzt wurde.

Innerhalb des zur Verfügung stehenden Platzes kann es zusätzlichen Leerraum geben, den man mit dem letzten Parameter steuert. In einer vertikalen Box betrifft dies nur den Raum nach oben und unten, nicht jedoch zu dem linken und rechten Rand.

Die Alternative zur vertikalen Box ist die horizontale Box, die mit gtk_hbox_new() erzeugt wird. Hier werden die Elemente von links nach rechts hinzugefügt. Möchte man in einer der Boxen genau anders herum anordnen, bietet sich gtk_box_pack_end() an.

Beim Design einer Box stellt man sich also zuerst die Frage, ob allen enthaltenen Widgets gleich viel Raum bereitgestellt wird. Dann entscheidet man, wie dieser Raum genutzt werden soll.

Das folgende Beispiel zeigt, wie sich horizontale und vertikale Boxen ineinander verschachteln lassen. Ein ähnliches Layout werden wir verwenden, wenn es um Fenster geht, die ein Menü, eine Statuszeile sowie einen Hauptbereich haben.

C
#include <gtk/gtk.h>


int main (int argc, char *argv[])
{
    GtkWidget *window, *hbox, *vbox, *buttons[5];
    
    gtk_init (&argc, &argv);
    window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    g_signal_connect (window, "destroy", G_CALLBACK(gtk_main_quit), NULL);

    vbox = gtk_vbox_new (FALSE, 0);
    
    /* Obere Zeile */
    buttons[0] = gtk_button_new_with_label ("Menü");
    gtk_box_pack_start (GTK_BOX(vbox), buttons[0], FALSE, FALSE, 0);

    /* Mittlerer Bereich */
    hbox = gtk_hbox_new (TRUE, 0);
    buttons[1] = gtk_button_new_with_label ("TRUE, TRUE");
    gtk_box_pack_start (GTK_BOX(hbox), buttons[1], TRUE, TRUE, 0);
    buttons[2] = gtk_button_new_with_label ("FALSE, FALSE");
    gtk_box_pack_start (GTK_BOX(hbox), buttons[2], FALSE, FALSE, 0);
    buttons[3] = gtk_button_new_with_label ("TRUE, TRUE");
    gtk_box_pack_start (GTK_BOX(hbox), buttons[3], TRUE, TRUE, 0);
    gtk_box_pack_start (GTK_BOX(vbox), hbox, TRUE, TRUE, 0);

    /* Untere Zeile */
    buttons[4] = gtk_button_new_with_label ("Status");
    gtk_box_pack_start (GTK_BOX(vbox), buttons[4], FALSE, FALSE, 0);
    
    gtk_container_add (GTK_CONTAINER(window), vbox);
    gtk_widget_show_all (window);
    gtk_main ();
    return 0;
}

Dieses Programm zeigt, wie man in eine vertikale Box einen Knopf, dann eine horizontale Box und anschließend wieder einen Knopf packt. Sowohl der obere wie auch der untere Knopf bekommen lediglich die Ausdehnung, die sie benötigen. Der Mittlere Teil hingegen wurde sehr großzügig genutzt.

Zusammenfassung

[Bearbeiten]

In diesem Kapitel haben wir einige Layouts kennen gelernt und gezeigt, wie man Widgets einem Layout hinzufügt. Sie wissen nun, wie man Layouts verschachteln kann, und mit welchen Parametern man das Aussehen beeinflussen kann.


Widgets konstruieren mit dem Builder

[Bearbeiten]

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:

Align=none 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.


Widgets im Hauptfenster

[Bearbeiten]

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:

Align=none
<?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):

Align=none
<?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.


Dialoge im Eigenbau

[Bearbeiten]

Dialoge im Eigenbau

[Bearbeiten]

Dialoge zeigen zumeist kurzfristig Informationen an. Mit ihnen lassen sich auch Einstellungen vornehmen und Daten eingeben. Dialoge stellen manchmal eine Unterbrechung des aktuellen Betriebes dar, in Einzelfällen sollte man daher überlegen, ob man einen Dialog nicht durch Elemente im Hauptfenster ersetzt. In Textverarbeitungen hat es sich beispielsweise eingebürgert, auf Dialoge zur Änderung der Schriftart zu verzichten - zu Gunsten von aufklappbaren Listen in der Werkzeugleiste. Da, wo Dialoge gebraucht werden, bieten sie eine prima Möglichkeit zur strukturierten Interaktion mit dem Programmnutzer.

In diesem Kapitel stellen wir Ihnen drei Programme vor, die selbst gebaute Dialoge anzeigen. Gleichzeitig zeigen wir Ihnen, wie typische Widgets in diesen Dialogen funktionieren.

Eine kleine Auswahl

[Bearbeiten]

Das folgende Programm stellt Ihnen einen Dialog zur Verfügung, bei der Sie aus drei Optionen eine auswählen können. Klicken Sie beim Dialog auf „Ok“, dann erhalten Sie einen Hinweis auf Ihre Auswahl. Bei „Abbrechen“ passiert gar nichts.

Hier die XML-Beschreibung:

Align=none
<?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="dialog-action">
            <property name="label">_Datei</property>
            </object>
        </child>
        <child>
            <object class="GtkAction" id="neuer-dialog-action">
            <property name="label">_Neuer Dialog</property>
            <signal name="activate" handler="starte_dialog"/>
            </object>
        </child>
        <child>
            <object class="GtkAction" id="exit-action">
            <property name="label">_Beenden</property>
            <property name="stock-id">gtk-quit</property>
            <signal name="activate" handler="gtk_main_quit"/>
            </object>
        </child>
        </object>
    </child>
    <ui>
    <menubar name="menubar">
        <menu action="dialog-action" >
            <menuitem action="neuer-dialog-action" />
            <menuitem action="exit-action" />
        </menu>
    </menubar>
    </ui>
    </object>

    <object class="GtkDialog" id="mein-dialog1">
    <property name="title">Vielleicht Formatieren</property>
    <child internal-child="vbox">
        <object class="GtkVBox" id="vbox">
        <child>
            <object class="GtkFrame" id="frame1">
            <property name="label">Formatieren</property>
            <child>
                <object class="GtkVBox" id="vbox-im-frame1">
                <child>
                    <object class="GtkRadioButton" id="radiobutton1" >
                    <property name="label">Festplatte sicher formatieren</property>
                    </object>
                </child>
                <child>
                    <object class="GtkRadioButton" id="radiobutton2" >
                    <property name="label">Festplatte unsicher formatieren</property>
                    <property name="group">radiobutton1</property>
                    </object>
                </child>
                <child>
                    <object class="GtkRadioButton" id="radiobutton3" >
                    <property name="label">Ich überlege noch</property>
                    <property name="group">radiobutton1</property>
                    </object>
                </child>
                </object>
            </child>
            </object>
        </child>
        <child internal-child="action_area">
            <object class="GtkHButtonBox" id="knopfkiste">
            <child>
                <object class="GtkButton" id="knopf_cancel">
                <property name="use-stock">TRUE</property>
                <property name="label">gtk-cancel</property>
                </object>
            </child>
            <child>
                <object class="GtkButton" id="knopf_ok">
                <property name="use-stock">TRUE</property>
                <property name="label">gtk-ok</property>
                </object>
            </child>
            </object>
        </child>
        </object>
    </child>
    <action-widgets>
        <action-widget response="1">knopf_cancel</action-widget>
        <action-widget response="2">knopf_ok</action-widget>
    </action-widgets>
    </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="GtkLabel" id="mein-label-1">
            <property name="label">Starten Sie den Dialog über das Menü</property>
            </object>
        </child>
        </object>
    </child>
    </object>
</interface>

In dieser Beschreibung finden Sie ein Menü, dessen Aufbau Sie schon kennen. Mit der Callback-Funktion starte_dialog() wird im Programm Ihr Dialog aufgerufen.

Das in diesem Beispiel neue Objekt der Klasse GtkDialog mit der Id „mein-dialog1“ enthält eine vertikale Box, in der sich ein Rahmen (GtkFrame) befindet. In diesem Rahmen sind drei Radioknöpfe angebracht. Ebenfalls enthält der Dialog eine Knopfkiste (GtkHButtonBox), die sich so verhält wie eine horizontale Kiste, mit zwei Knöpfen drin. Darüber hinaus werden diese Knöpfe in einem so genannten „action-widgets“-Bereich referenziert, um den Dialog eindeutig beenden zu können.

Objekte vom Typ GtkDialog enthalten automatisch zwei Bereiche, ein Bereich ist eine vertikale Box, der im Beispiel als „internal-child=“vbox““ gekennzeichnet ist, der andere Bereich ist durch das Attribut „internal-child=“action_area““ markiert. Diese Bereiche sind vordefiniert, wobei die „action_area“ in der „vbox“ enthalten ist.

In der oberen vertikalen Box fügen wir einen Rahmen ein. Ein Rahmen kann einen beschreibenden Text im oberen Bereich enthalten. Sie können einen Rahmen unter anderem dahingehend verändern, dass Sie die Schattierung verändern. Dadurch kann er angehoben oder abgesenkt wirken.

Innerhalb des Rahmens bringen wir eine weitere vertikale Box an, die einige besondere Auswahlknöpfe der Klasse GtkRadioButton beinhalten. Diese Knöpfe sollen es ermöglichen, aus drei Optionen genau eine auswählen zu können. Diese Knöpfe haben einen beschreibenden Text. Zwei der Knöpfe verweisen auf den ersten Knopf und bilden so eine Gruppe (<property name="group">). Innerhalb diese Gruppe kann nur ein Knopf angewählt sein. Wählt man einen aus, dann verschwindet die Auswahl bei einem anderen Knopf.

In der „action_area“ kommen Knöpfe hinein, die den Dialog beenden sollen. Es stehen in diesem Beispiel „Ok“ und „Abbrechen“ zur Auswahl. Jeder der Knöpfe soll ein Piktogramm anzeigen. Etwas trickreich ist es hier, dass über das Attribut „label“ das Piktogramm und gleichzeitig der beschreibende Text des Knopfes ausgewählt wird. Hier unterstützt uns Gtk+ mit passenden vordefinierten Feldern.

Dem Hauptprogramm wollen wir, wenn der Dialog beendet wird, mitteilen, welcher Knopf gedrückt wurde. Die „action-widget“-Elemente geben uns genau dafür die Gelegenheit. Hier definieren wir mit dem Attribut „response“ diejenigen Zahlen, die beim Drücken der jeweiligen Knöpfe übermittelt werden. Bitte verwenden Sie hier nur positive Werte.

Der Quelltext sieht folgendermaßen aus:

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

void starte_dialog (GtkWidget *w, GtkBuilder *b)
{
    GtkWidget *dialog, *radio, *nachrichtendialog;
    gint knopf;
    dialog = GTK_WIDGET(gtk_builder_get_object (b, "mein-dialog1"));
    gtk_widget_show_all (dialog);
    knopf = gtk_dialog_run (GTK_DIALOG(dialog));
    if (knopf == 2)
    {
        /* "OK" wurde geklickt, herausfinden, Knopf gedrückt wurde */
        gchar *antwort;
        radio = GTK_WIDGET(gtk_builder_get_object (b, "radiobutton1"));
        if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (radio)))
            antwort = "sicher";
        else
        {
            radio = GTK_WIDGET(gtk_builder_get_object (b, "radiobutton2"));
            if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (radio)))
                antwort = "unsicher";
            else
                antwort = "gar nicht";
        }
        /* Dem User mitteilen, was nun passiert */
        nachrichtendialog = gtk_message_dialog_new (NULL,
            GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_INFO, GTK_BUTTONS_OK,
            "Ich werden nun ihre Festplatte %s formatieren...", antwort);
        gtk_dialog_run (GTK_DIALOG(nachrichtendialog));
        gtk_widget_destroy (nachrichtendialog);
    }
    gtk_widget_hide (dialog);
}

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, "dialog1.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 Funktion starte_dialog() wird unter anderem mit dem Builder-Objekt als Parameter aufgerufen. Dieser hilft uns dabei, den Dialog mit gtk_builder_get_object() zu finden und anzuzeigen. Der Dialog wird mit gtk_dialog_run() so gestartet, dass seine eigene Nachrichtenschleife läuft. Diese Funktion wird nur dann verlassen, wenn der Anwender auf einen der Knöpfe oder auf das Schließen-Symbol in der Titelleiste geklickt hat. Der Knopf, der gedrückt wurde, wird als Zahl wie in der XML-Beschreibung festgelegt, von der Funktion gtk_dialog_run() übermittelt.

Wurde „Ok“ geklickt, dann wird nach dem aktuell ausgewählten Auswahlknopf gesucht. Hierzu fragen wir mit gtk_toggle_button_get_active() jeden einzelnen Knopf ab, ob dieser ausgewählt wurde. Um dem Anwender Rückmeldung zu geben, starten wir in bekannter Weise einen Nachrichtendialog.


Widgets in Dialogen

[Bearbeiten]

Wir wollen Ihnen mit dem folgenden Beispiel einige Widgets zeigen, die sich gut in Dialoge einfügen lassen und helfen, Themen zu gruppieren. Hier bietet sich besonders die Klasse GtkNotebook an, die ähnlich wie Karteikarten funktioniert. Jede Karteikarte, also Seite im Sinne von GtkNotebook, enthält ein Thema. Auf jeder unserer im folgenden Beispiel zwei Seiten wird etwas vorgestellt. Einmal erhalten Sie Knöpfe, im anderen Fall eine Seite, in der Sie Personendaten eingeben können.

Die XML-Beschreibung:

Align=none
<?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="dialog-action">
            <property name="label">_Datei</property>
            </object>
        </child>
        <child>
            <object class="GtkAction" id="neuer-dialog-action">
            <property name="label">_Neuer Dialog</property>
            <signal name="activate" handler="starte_dialog"/>
            </object>
        </child>
        <child>
            <object class="GtkAction" id="exit-action">
            <property name="label">_Beenden</property>
            <property name="stock-id">gtk-quit</property>
            <signal name="activate" handler="gtk_main_quit"/>
            </object>
        </child>
        </object>
    </child>
    <ui>
    <menubar name="menubar">
        <menu action="dialog-action" >
            <menuitem action="neuer-dialog-action" />
            <menuitem action="exit-action" />
        </menu>
    </menubar>
    </ui>
    </object>

    <object class="GtkDialog" id="mein-dialog1">
    <property name="title">Konfigurieren</property>
    <child internal-child="vbox">
        <object class="GtkVBox" id="vbox">
        <child>
            <object class="GtkNotebook" id="notebook-1">
            <!-- Seite 1 -->
            <child>
                <object class="GtkFrame" id="frame-knoepfe-1">
                <property name="label">Knöpfe</property>
                <child>
                    <object class="GtkVBox" id="vbox-1-1">
                    <child>
                        <object class="GtkCheckButton" id="check-1-1-1">
                        <property name="label">Alle Daten automatisch überschreiben</property>
                        </object>
                    </child>
                    <child>
                        <object class="GtkCheckButton" id="check-1-1-2">
                        <property name="label">SPAM-Mails immer ausdrucken</property>
                        <property name="active">TRUE</property>
                        </object>
                    </child>
                    </object>
                </child>
                </object>
            </child>
            <child type="tab">
                <object class="GtkLabel" id="notebook-seitentitel-1">
                <property name="label">Knöpfe</property>
                </object>
            </child>
            <!-- Seite 2 -->
            <child>
                <object class="GtkFrame" id="frame-formular-2">
                <property name="label">Formular</property>
                <child>
                    <object class="GtkTable" id="table-2-2"> 
                    <property name="homogeneous">TRUE</property>
                    <property name="n-columns">2</property>
                    <property name="n-rows">3</property>
                    <child>
                        <object class="GtkLabel" id="label-2-2-1">
                        <property name="label">Anrede</property>
                        </object>
                        <packing>
                       	<property name="left-attach">0</property>
                       	<property name="right-attach">1</property>
                       	<property name="top-attach">0</property>
                       	<property name="bottom-attach">1</property>
                       	</packing>
                    </child>
                    <child>
                        <object class="GtkComboBoxText" id="combo-2-2-1">
                        <property name="entry-text-column">0</property>
                        <items>
                            <item>Frau</item>
                            <item>Herr</item>
                        </items>
                        </object>
                        <packing>
                       	<property name="left-attach">1</property>
                       	<property name="right-attach">2</property>
                       	<property name="top-attach">0</property>
                       	<property name="bottom-attach">1</property>
                       	</packing>
                    </child>
                    <child>
                        <object class="GtkLabel" id="label-2-2-2">
                        <property name="label">Name</property>
                        </object>
                        <packing>
                       	<property name="left-attach">0</property>
                       	<property name="right-attach">1</property>
                       	<property name="top-attach">1</property>
                       	<property name="bottom-attach">2</property>
                       	</packing>
                    </child>
                    <child>
                        <object class="GtkEntry" id="entry-2-2-2">
                        <property name="text">Müller</property>
                        </object>
                        <packing>
                       	<property name="left-attach">1</property>
                       	<property name="right-attach">2</property>
                       	<property name="top-attach">1</property>
                       	<property name="bottom-attach">2</property>
                       	</packing>
                    </child>
                    <child>
                        <object class="GtkLabel" id="label-2-2-3">
                        <property name="label">Alter</property>
                        </object>
                        <packing>
                       	<property name="left-attach">0</property>
                       	<property name="right-attach">1</property>
                       	<property name="top-attach">2</property>
                       	<property name="bottom-attach">3</property>
                       	</packing>
                    </child>
                    <child>
                        <object class="GtkAdjustment" id="adjustment-2-2-3">
                        <property name="lower">1</property>
                        <property name="upper">121</property>
                        <property name="value">27</property>
                        <property name="step-increment">1</property>
                        <property name="page-increment">10</property>
                        </object>
                        <object class="GtkSpinButton" id="spinbutton-2-2-3">
                        <property name="digits">0</property>
                        <property name="adjustment">adjustment-2-2-3</property>
                        </object>
                        <packing>
                       	<property name="left-attach">1</property>
                       	<property name="right-attach">2</property>
                       	<property name="top-attach">2</property>
                       	<property name="bottom-attach">3</property>
                       	</packing>
                    </child>
                    </object>
                </child>
                </object>
            </child>
            <child type="tab">
                <object class="GtkLabel" id="notebook-seitentitel-2">
                <property name="label">Formular</property>
                </object>
            </child>
            </object>
        </child>
        <child internal-child="action_area">
            <object class="GtkHButtonBox" id="knopfkiste">
            <child>
                <object class="GtkButton" id="knopf_cancel">
                <property name="use-stock">TRUE</property>
                <property name="label">gtk-cancel</property>
                </object>
            </child>
            <child>
                <object class="GtkButton" id="knopf_ok">
                <property name="use-stock">TRUE</property>
                <property name="label">gtk-ok</property>
                </object>
            </child>
            </object>
        </child>
        </object>
    </child>
    <action-widgets>
        <action-widget response="1">knopf_cancel</action-widget>
        <action-widget response="2">knopf_ok</action-widget>
    </action-widgets>
    </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="GtkLabel" id="mein-label-1">
            <property name="label">Starten Sie den Dialog über das Menü</property>
            </object>
        </child>
        </object>
    </child>
    </object>
</interface>

Innerhalb des Objektes der Klasse GtkDialog liegt ein Objekt vom Typ GtkNotebook. Über „<child>“-Elemente werden die Seiten eingefügt. Zu jeder Seite gibt es zwei solcher Elemente. Einmal handelt es sich um den Inhalt, der auf der Seite dargestellt werden soll. Danach kommt ein Element mit dem zusätzlichen Attribut „type=tab“. Hierbei handelt es sich um den Kopf der Karteikartenseite. Beachten Sie bitte, dass die Reihenfolge zwingend einzuhalten ist. Wenn Sie in diesem Beispiel eine Seite hinzufügen wollen, dann müssen Sie zuerst eine Inhaltsseite per <child>-Element einfügen und anschließend das Kopffeld mit <child type=“tab“>.

Die erste Seite enthält in einem Rahmen zwei Knöpfe vom Typ GtkCheckButton. Diese Sorte Knöpfe können, ähnlich wie Knöpfe vom Typ GtkRadioButton, zwei Zustände einnehmen, an oder aus. Sie sind dazu gedacht, unabhängig voneinander zu sein, werden also nicht gruppiert.

Die zweite Seite enthält eine Tabelle, diese kennen Sie schon aus einem früheren Kapitel. Diese Tabelle hat zwei Spalten und drei Reihen. In die linke Spalte werden Textfelder eingefügt. Das Einfügen von Widgets in eine Tabelle ist etwas aufwendig denn statt eines einfachen Funktionsaufrufs von gtk_table_attach_defaults(), werden hier vier <property>-Elemente benötigt, um ein Widget zu positionieren.

In die rechte Spalte der zweiten Seite werden verschiedene Widgets eingefügt. Zuerst eine Menübox vom Typ GtkComboBoxText. Diese enthält eine Liste mit Texteinträgen, von denen man sich eines aussuchen kann. Sie bietet also eine Alternative zu GtkRadioButton. In diesem Fall werden zwei Texte hinzugefügt, nämlich „Herr“ und „Frau“. Diese Liste kann aufgeklappt werden, und man sucht sich den passenden Eintrag aus. Diese Box kann einen alternativen Editor haben, bei dem der Benutzer eigenständig Text eingibt, der vielleicht nicht in der Liste steht.

Darunter folgt eine Editorzeile vom Typ GtkEntry. In diese Zeile haben wir schon etwas für Sie hineingeschrieben. Dieser Text kann aber ersetzt werden.

Anschließend folgt ein Widget, bei dem Sie eine Zahl auswählen können. Dieses Widget ist zweigeteilt. Das Objekt der Klasse GtkAdjustment sorgt dafür, dass Sie die Möglichkeit erhalten, von 1 bis 121 zu zählen. Es hat den voreingestellten Wert von 27, den Sie in Einer-Schritten vergrößern können. Wenn Sie auf die „Seite-Nach-Oben“-Taste auf der Tastatur drücken, dann soll in 10er-Schritten vergrößert werden. Ist dieses Objekt fertig konfiguriert, dann kann man es in die Anzeige vom Typ GtkSpinButton per Referenz einbringen. Dieser Regler enthält dann alle Dinge so, wie sie von GtkAdjustment konfiguriert wurden.

Im Programm wird es jetzt vornehmlich um das Auslesen der Eingaben gehen:

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

void starte_dialog (GtkWidget *w, GtkBuilder *b)
{
    GtkWidget *dialog, *nachrichtendialog, *widget;
    gint knopf;
    dialog = GTK_WIDGET(gtk_builder_get_object (b, "mein-dialog1"));
    gtk_widget_show_all (dialog);
    knopf = gtk_dialog_run (GTK_DIALOG(dialog));
    
    if (knopf == 2)
    {
        /* Wird SPAM gewünscht? */
        gboolean will_spam;
        gchar *spam;
        widget = GTK_WIDGET(gtk_builder_get_object (b, "check-1-1-2"));
        will_spam = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON(widget));
        if (will_spam) 
            spam = "";
        else
            spam = "keine ";
        /* Anrede */
        gchar *anrede;
        widget = GTK_WIDGET(gtk_builder_get_object (b, "combo-2-2-1"));
        anrede = gtk_combo_box_text_get_active_text(GTK_COMBO_BOX_TEXT(widget));
        /* Name */
        const gchar *name;
        widget = GTK_WIDGET(gtk_builder_get_object (b, "entry-2-2-2"));
        name = gtk_entry_get_text (GTK_ENTRY(widget));
        /* Alter */
        int alter;
        widget = GTK_WIDGET(gtk_builder_get_object (b, "spinbutton-2-2-3"));
        alter = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON(widget));
        /* Daten ausgeben */
        nachrichtendialog = gtk_message_dialog_new (NULL, GTK_DIALOG_DESTROY_WITH_PARENT,
            GTK_MESSAGE_INFO, GTK_BUTTONS_OK,
            "%s %s (%d Jahre) wünscht %sSPAM-Mails!", anrede, name, alter, spam);
        gtk_dialog_run (GTK_DIALOG(nachrichtendialog));
        gtk_widget_destroy (nachrichtendialog);
        /* Aufräumen */
        g_free (anrede);
    }
    gtk_widget_hide (dialog);
}

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, "dialog2.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;
}

Auch in diesem Beispiel wird der Dialog über die Funktion starte_dialog() geöffnet. Drückt der Anwender am Schluss der Eingabe in den Dialog auf „Ok“, dann werden die Widgets ausgelesen und es erscheint ein passender Text in einem Nachrichtendialog. Alle Widgets in der XML-Beschreibung haben eine eindeutige Id, deswegen können wir an jeder Stelle über das Builder-Objekt auf sie zugreifen.

GtkRadioButton und GtkCheckButton werden auf die gleiche Weise ausgelesen. Man benutzt hier gtk_toggle_button_get_active(). Der Grund dafür ist, dass die beide Knöpfe von der Klasse GtkToggleButton abgeleitet werden. Man erhält TRUE, wenn der Knopf gedrückt wurde.

Der Text, der im Objekt vom Typ GtkComboBoxText steht, kann mit der Funktion gtk_combo_box_text_get_active_text() ausgelesen werden. Das funktioniert aber nur, wenn in der dazugehörigen XML-Beschreibung „entry-text-column“ gesetzt wurde.

Der einzeilige Editor vom Typ GtkEntry kann mit gtk_entry_get_text() ausgelesen werden. Man bekommt hier einen konstanten Text zurückgeliefert, den man nicht modifizieren kann.

GtkSpinButton-Objekte können mit der Funktion gtk_spin_button_get_value_as_int() ausgelesen werden. Man kann das dazugehörige GtkAdjustment so konfigurieren, dass es Gleitkommawerte verarbeitet. Möchte man diese auslesen, benutzt man gtk_spin_button_get_value().


Blockierend oder nicht blockierend

[Bearbeiten]

Alle Dialoge in den bisherigen Beispielen waren so gestaltet, dass man im Programm nichts anderes tun konnte, als den Dialog zu bedienen. Die Menüs des Hauptprogramms waren nicht mehr zugänglich. Öffnete ein Konfigurationsdialog einen Nachrichtendialog, so kam man nicht mehr an den Konfigurationsdialog, sondern musste zuerst den Nachrichtendialog beenden. In vielen Fällen ist genau dieses Verhalten gewünscht. Ein Anwendungsfall von Dialogen ist es aber auch, dauerhaft eine Werkzeugpalette zur Verfügung zu stellen, oder den Status eines Programms anzuzeigen.

In der Sprache von Gtk+ sind Fenster, die keine Interaktion mit dem übrigen Programm erlauben „modal“, sie blockieren also alle anderen Fenster. Nicht blockierende Fenster erlauben es hingegen, auch auf andere Fenster zuzugreifen. Dies gilt nicht alleine für Dialoge, sondern für alle Arten von Fenstern.

Wir stellen in den folgenden Beispielen zwei Dialoge vor, einen modalen und einen nicht-modalen und beginnen wie üblich mit der XML-Beschreibung:

Align=none
<?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="dialog-action">
            <property name="label">_Datei</property>
            </object>
        </child>
        <child>
            <object class="GtkAction" id="neuer-dialog-action-modal">
            <property name="label">_Modaler Dialog</property>
            <signal name="activate" handler="starte_modal_dialog"/>
            </object>
        </child>
        <child>
            <object class="GtkAction" id="neuer-dialog-action-non-modal">
            <property name="label">_Nicht-Modaler Dialog</property>
            <signal name="activate" handler="starte_nichtmodal_dialog"/>
            </object>
        </child>
        <child>
            <object class="GtkAction" id="exit-action">
            <property name="label">_Beenden</property>
            <property name="stock-id">gtk-quit</property>
            <signal name="activate" handler="gtk_main_quit"/>
            </object>
        </child>
        </object>
    </child>
    <ui>
    <menubar name="menubar">
        <menu action="dialog-action" >
            <menuitem action="neuer-dialog-action-modal" />
            <menuitem action="neuer-dialog-action-non-modal" />
            <menuitem action="exit-action" />
        </menu>
    </menubar>
    </ui>
    </object>
    <!-- Modaler Dialog -->
    <object class="GtkDialog" id="mein-dialog-modal">
    <property name="title">Ich bin modal!</property>
    <property name="modal">TRUE</property>
    <child internal-child="vbox">
        <object class="GtkVBox" id="vbox-modal">
        <child>
            <object class="GtkLabel" id="mein-dialog-modal-label-1">
            <property name="label">Ich bin modal</property>
            </object>
        </child>
        <child internal-child="action_area">
            <object class="GtkHButtonBox" id="knopfkiste-modal">
            <child>
                <object class="GtkButton" id="knopf-cancel-modal">
                <property name="use-stock">TRUE</property>
                <property name="label">gtk-cancel</property>
                </object>
            </child>
            <child>
                <object class="GtkButton" id="knopf-ok-modal">
                <property name="use-stock">TRUE</property>
                <property name="label">gtk-ok</property>
                </object>
            </child>
            </object>
        </child>
        </object>
    </child>
    <action-widgets>
        <action-widget response="1">knopf-cancel-modal</action-widget>
        <action-widget response="2">knopf-ok-modal</action-widget>
    </action-widgets>
    </object>
    <!-- Nicht-Modaler Dialog -->
    <object class="GtkDialog" id="mein-dialog-nicht-modal">
    <property name="title">Ich bin nicht modal!</property>
    <property name="modal">FALSE</property>
    <child internal-child="vbox">
        <object class="GtkVBox" id="vbox">
        <child>
            <object class="GtkLabel" id="mein-dialog-label-1">
            <property name="label">Ich bin nicht modal</property>
            </object>
        </child>
        <child internal-child="action_area">
            <object class="GtkHButtonBox" id="knopfkiste">
            <child>
                <object class="GtkButton" id="knopf-cancel">
                <property name="use-stock">TRUE</property>
                <property name="label">gtk-cancel</property>
                </object>
            </child>
            <child>
                <object class="GtkButton" id="knopf-ok">
                <property name="use-stock">TRUE</property>
                <property name="label">gtk-ok</property>
                </object>
            </child>
            </object>
        </child>
        </object>
    </child>
    <action-widgets>
        <action-widget response="1">knopf-cancel</action-widget>
        <action-widget response="2">knopf-ok</action-widget>
    </action-widgets>
    </object>
    <!-- Hauptfenster -->
    <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="GtkLabel" id="mein-label-1">
            <property name="label">Starten Sie den Dialog über das Menü</property>
            </object>
        </child>
        </object>
    </child>
    </object>
</interface>

In dieser Beschreibung finden Sie ein erweitertes Menü, das Hauptfenster und zwei Dialoge vor. Die Dialoge sind gleich gestaltet, bis auf den beschreibenden Text und dem Property-Element

Align=none

{{{1}}}


Hiermit wird festgelegt, ob der Dialog blockierend sein soll. Da unabhängig von diesem Element alle Dialoge, die über gtk_dialog_run() gestartet werden blockierend sind, zeigen wir die Unterschiede ohne den Aufruf der Ereignisschleife im Hauptprogramm:

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

void starte_modal_dialog (GtkWidget *w, GtkBuilder *b)
{
    GtkWidget *dialog;
    dialog = GTK_WIDGET(gtk_builder_get_object (b, "mein-dialog-modal"));
    gtk_widget_show_all (dialog);
    g_signal_connect_swapped (dialog, "response",
        G_CALLBACK (gtk_widget_hide), dialog);
}

void starte_nichtmodal_dialog (GtkWidget *w, GtkBuilder *b)
{
    GtkWidget *dialog;
    dialog = GTK_WIDGET(gtk_builder_get_object (b, "mein-dialog-nicht-modal"));
    gtk_widget_show_all (dialog);
    g_signal_connect_swapped (dialog, "response", 
        G_CALLBACK (gtk_widget_hide), dialog);
}

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, "dialog3.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 Ereignisschleife gtk_dialog_run() wird beendet, wenn das Signal „response“ empfangen wird. Das machen wir uns in diesem Beispiel zu Nutze, in dem wir das entsprechende Signal mit der Callback-Funktion gtk_widget_hide() verknüpfen. Wir verzichten dabei auf eine Auswertung der Rückgabewerte. Diese würde in einer separaten Callback erfolgen.

Der modale Dialog blockiert, der nicht blockierende hingegen gibt dem Anwender die Möglichkeit, weiterhin auf das Menü zuzugreifen. Startet man nacheinander beide Dialoge, so blockiert der modale Dialog auch den nicht-modalen.

Zusammenfassung

[Bearbeiten]

In diesem Kapitel haben Sie viele Details aus der Welt der Dialoge kennen gelernt. Sie sind nun in der Lage, eigene Dialoge zu bauen und anzuzeigen. Ebenfalls haben wir eine Reihe von Widgets vorgestellt, die Sie in Ihre Programme einbinden und auswerten können.


Listen- und Baumansichten, Modell-View-Controller

[Bearbeiten]

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.

Liste

[Bearbeiten]

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:

Align=none
<?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.


Bäume

[Bearbeiten]

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.

Align=none
<?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.

Align=none
<?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.



Zeichnen mit Cairo

[Bearbeiten]

Zeichnen mit Cairo

[Bearbeiten]

Cairo ist eine Vektorgrafikbibliothek mit direkter Unterstützung durch Gtk+. Mit Funktionen aus dieser Bibliothek können Sie Pfade zeichnen, die sich anschließend transformieren lassen und gezeichnete Elemente in SVG, PDF und anderen Formaten ausgeben. Wenn Sie eine externe Bibliothek wie RSVG dazu nehmen, können Sie sogar SVG-Dateien laden und ansehen. In diesem Kapitel geht es darum zu zeigen, wie man mit Cairo zeichnet und Zeichnungen rotieren lässt.


Diagramme nach Wahl

[Bearbeiten]

Das folgende Programm stellt ein Balken- und ein Tortendiagramm dar. Zwischen den Diagrammtypen schalten Sie mit einem Knopf in der Statusleiste um. Hier zunächst die XML-Beschreibung:

Align=none
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<interface>
    <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="GtkDrawingArea" id="leinwand-1">
            <signal name="draw" handler="neu_malen" />
            </object>
        </child>
        <child>
            <object class="GtkHBox" id="hbox-layout-1">
            <property name="homogeneous">TRUE</property>
            <child>
                <object class="GtkButton" id="wechsel-ansicht-knopf">
                <property name="label">gtk-convert</property>
                <property name="use-stock">TRUE</property>
                <signal name="clicked" handler="wechsel_ansicht" />
                </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">Cairo auf einer Leinwand</property>
                </object>
                <packing>
                    <property name="expand">FALSE</property>
                    <property name="fill">FALSE</property>
                </packing>
            </child>
            </object>
            <packing>
                <property name="expand">FALSE</property>
                <property name="fill">FALSE</property>
            </packing>
        </child>
        </object>
    </child>
    </object>
</interface>

Innerhalb des Hauptfensters befindet unter anderem eine Zeichenfläche vom Typ GtkDrawingArea. In dieses Widget werden wir zeichnen. Immer, wenn ein Neuzeichnen erforderlich ist, wird automatisch das Signal „draw“ emittiert. Darauf reagieren wir mit der Callback-Funktion neu_malen().

Das Hauptprogramm besteht aus einigen Funktionen, die verschiedene Bereiche zeichnen und den Callback-Funktionen, um die Diagrammtypen umzuschalten wie auch das Neuzeichnen zu erledigen. Ebenfalls verwenden wir nicht mehr nur noch ein Builder-Objekt, um es den Callback-Funktionen mitzugeben, sondern eine C-Struktur, die unter anderem den Builder enthält sowie eine Kennzeichnung, welcher Diagrammtyp nun gezeichnet werden soll.

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

/* Ein Objekt, das allen Callbacks übergeben wird */
typedef struct _CallbackObjekt
{
    GtkBuilder *builder;
    GtkWidget *zeichenflaeche;
    gboolean tortendiagramm;
    
} CallbackObjekt;

/* Stimmabgaben */
static gint wahlen[3] = {50, 70, 22};

/* Achse zeichnen */
static void zeichne_achsen (cairo_t *cr, gint breite, gint hoehe)
{
    cairo_set_source_rgb (cr, 0, 0, 0);
    /* X-Achse */
    cairo_move_to (cr, 3, hoehe - 5);
    cairo_line_to (cr, breite - 3, hoehe - 5);
    
    /* Y-Achse */
    cairo_move_to (cr, 5, hoehe - 3);
    cairo_line_to (cr, 5, 3);
    cairo_stroke (cr);
}

static void balkendiagramm (cairo_t *cr, gint breite, gint hoehe)
{
    gint blockbreite = (breite - 20) / 6;
    gint blockhoehe;
    /* wahlen[0] */
    blockhoehe = (hoehe - 6) * wahlen[0] / 100;
    cairo_rectangle (cr, blockbreite,  (hoehe - 6) - blockhoehe, 
      blockbreite, blockhoehe);
    cairo_set_source_rgb (cr, 1, 0, 0);
    cairo_fill(cr);
    /* wahlen[1] */
    blockhoehe = (hoehe - 6) * wahlen[1] / 100;
    cairo_rectangle (cr, 3 * blockbreite,  (hoehe - 6) - blockhoehe,
      blockbreite, blockhoehe);
    cairo_set_source_rgb (cr, 0, 1, 0);
    cairo_fill(cr);
    /* wahlen[0] */
    blockhoehe = (hoehe - 6) * wahlen[2] / 100;
    cairo_rectangle (cr, 5 * blockbreite,  (hoehe - 6) - blockhoehe,
      blockbreite, blockhoehe);
    cairo_set_source_rgb (cr, 0, 0, 1);
    cairo_fill(cr);
}


static void tortendiagramm (cairo_t *cr, gint breite, gint hoehe)
{
    /* Winkel pro Stimme */
    double stimmwinkel = (2.0 * M_PI) / (wahlen[0] + wahlen[1] + wahlen[2]);
    double winkel0 = wahlen[0] * stimmwinkel;
    double winkel1 = wahlen[1] * stimmwinkel;
    double winkel2 = wahlen[2] * stimmwinkel;

    /* Mittelpunkt */
    double mitte_x = breite / 2.0;
    double mitte_y = hoehe / 2.0;
    
    /* Radius */
    double radius = (breite < hoehe ? breite : hoehe) / 3.0;
    
    /* Tortenstück für Partei 0 */
    cairo_set_source_rgb (cr, 1, 0, 0);
    cairo_move_to (cr, mitte_x, mitte_y);
    cairo_arc (cr, mitte_x, mitte_y, radius, 0.0, winkel0);
    cairo_fill(cr);

    /* Tortenstück für Partei 1 */
    cairo_set_source_rgb (cr, 0, 1, 0);
    cairo_move_to (cr, mitte_x, mitte_y);
    cairo_arc (cr, mitte_x, mitte_y, radius, winkel0, winkel0 + winkel1);
    cairo_fill(cr);
    
    /* Tortenstück für Partei 2 */
    cairo_set_source_rgb (cr, 0, 0, 1);
    cairo_move_to (cr, mitte_x, mitte_y);
    cairo_arc (cr, mitte_x, mitte_y, radius, winkel0 + winkel1,
      winkel0 + winkel1 + winkel2);
    cairo_fill(cr);
}


gboolean neu_malen (GtkWidget *zeichenflaeche, cairo_t *cr, CallbackObjekt *obj)
{
    gint breite, hoehe;
    breite = gtk_widget_get_allocated_width (zeichenflaeche);
    hoehe = gtk_widget_get_allocated_height (zeichenflaeche);
    if (obj->tortendiagramm)
    {
        tortendiagramm (cr, breite, hoehe);
    }
    else
    {
        zeichne_achsen (cr, breite, hoehe);
        balkendiagramm (cr, breite, hoehe);
    }
    return TRUE;
}

void wechsel_ansicht (GtkWidget *button, CallbackObjekt *obj)
{
    GtkLabel *textfeld;
    gint breite, hoehe;
    textfeld = GTK_LABEL(gtk_builder_get_object (obj->builder, "mein-label-1"));
    obj->tortendiagramm = !obj->tortendiagramm;
    breite = gtk_widget_get_allocated_width (obj->zeichenflaeche);
    hoehe = gtk_widget_get_allocated_height (obj->zeichenflaeche);
    gtk_label_set_text (textfeld, obj->tortendiagramm ? "Tortendiagramm" :
      "Balkendiagramm");
    gtk_widget_queue_draw_area (obj->zeichenflaeche, 0, 0, breite, hoehe);
}

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

    gtk_init (&argc, &argv);
    
    callobj = g_malloc (sizeof(CallbackObjekt));
    callobj->tortendiagramm = TRUE;
    
    callobj->builder = gtk_builder_new ();
    gtk_builder_add_from_file (callobj->builder, "cairo1.xml", &errors);
    gtk_builder_connect_signals (callobj->builder, callobj);
    window = GTK_WIDGET(gtk_builder_get_object (callobj->builder,
      "hauptfenster"));
    callobj->zeichenflaeche = GTK_WIDGET(gtk_builder_get_object (
      callobj->builder, "leinwand-1"));
    gtk_widget_set_size_request (callobj->zeichenflaeche, 200, 200);
    gtk_widget_show_all (window);
    gtk_main ();
    g_free (callobj);
    return 0;
}

In diesem Beispiel verwenden wir ein CallbackObjekt, um es allen Callback-Funktionen mitzugeben. Für dieses Objekt müssen wir mit g_malloc() Speicherplatz bereitstellen, ab da kann gtk_builder_connect_signals() dieses Objekt an alle Callback-Funktionen senden.

Eine dieser Callback-Funktionen ist wechsel_ansicht(). Diese Funktion wird aufgerufen, sobald der Anwender die Diagrammdarstellung wechseln möchte. In dieser Funktion wird die Ansicht geändert, zum Beispiel von Tortendiagramm auf Balkendiagramm gewechselt, das Textfeld mit einem passenden Hinweis beschrieben und dann mit gtk_widget_queue_draw_area() ein Neuzeichnen der Zeichenfläche angefordert. Dieser Funktion gibt man die Ausdehnung eines Rechtecks mit, das neu gezeichnet werden soll.

Sobald neu gezeichnet werden soll, wird die Callback-Funktion neu_malen() aufgerufen. Diese bekommt als Parameter die Zeichenfläche, einen Zeichen-Kontext und einen Verweis auf das Callback-Objekt mitgegeben. Das eigentliche Zeichnen wird an die Funktionen tortendiagramm(), zeichne_achsen() und balkendiagramm() weitergereicht. Hierzu wird lediglich der Zeichen-Kontext und die Größe der Zeichenfläche übergeben.

Die Funktion zeichne_achsen() setzt mit cairo_set_source_rgb() die Zeichenfarbe auf Schwarz, bewegt den Stift mit cairo_move_to() an eine Anfangsposition, zeichnet eine Linie mit cairo_line_to() bis zur angegebenen Endposition und stellt die so entstandenen Koordinatenachsen mit cairo_stroke() dar.

Balkendiagramme werden mit der Funktion „balkendiagramm()“ gezeichnet. Hier werden zunächst passende Balkenbreiten bestimmt. Um drei Balken zu zeichnen, wird der zur Verfügung stehende Platz in sechs Teile geteilt, damit immer ein Balken neben einer Lücke platziert werden kann. Dann sorgen wir mit cairo_rectangle() dafür, dass ein Rechteck gezeichnet wird. Das Rechteck soll gefüllt erscheinen, das erledigt cairo_fill() für uns.

Das Tortendiagramm wird mit der gleichnamigen Funktion gezeichnet. Hier wird zunächst der Winkel pro Wahlstimme ausgerechnet und dann die Winkel für die jeweiligen Parteien bestimmt. Um ein gefülltes Tortenstück zu zeichnen, legt man die Zeichenfarbe fest, dann beginnt man einen Pfad in der Mitte der Torte. An diesen Pfad wird ein Wegstück eines Kreises mit angegebenem Radius und Winkel mit cairo_arc() angefügt. Die Funktion cairo_fill() sorgt nun ihrerseits dafür, das der Pfad wieder zum Mittelpunkt geschlossen wird und füllt die entstandene Fläche mit der angegebenen Farbe.

Alles dreht sich

[Bearbeiten]

Im folgenden Beispiel zeigen wir anhand eines sich drehenden Textes, wie man Transformationen auf einen Pfad anwendet. Der Pfad ist Text, den der Benutzer in einer Editorzeile selbst eingeben kann, es würde aber mit jeder anderen Zeichnung auch funktionieren. Damit sich der Text selbstständig dreht, also animiert wird, muss in bestimmten Intervallen ein Zeitsignal an das Programm gesendet werden.

Zunächst die XML-Beschreibung des Programms:

Align=none
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<interface>
    <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="GtkDrawingArea" id="leinwand-1">
            <signal name="draw" handler="neu_malen" />
            </object>
        </child>
        <child>
            <object class="GtkHBox" id="hbox-layout-1">
            <property name="homogeneous">TRUE</property>
            <child>
                <object class="GtkEntry" id="entry-1">
                <property name="text">Hallo</property>
                <property name="max-length">10</property>
                <signal name="activate" handler="neuer_text" />
                </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">Cairo auf einer Leinwand</property>
                </object>
                <packing>
                    <property name="expand">FALSE</property>
                    <property name="fill">FALSE</property>
                </packing>
            </child>
            </object>
            <packing>
                <property name="expand">FALSE</property>
                <property name="fill">FALSE</property>
            </packing>
        </child>
        </object>
    </child>
    </object>
</interface>

Auf der Zeichenfläche vom Typ GtkDrawingArea werden wir wie im ersten Beispiel die Zeichenoperationen ausführen. Die Editorzeile vom Typ GtkEntry legt die maximale Textlänge auf 10 Zeichen fest und sorgt dafür, dass immer, wenn der Anwender eingegebenen Text mit der Enter-Taste bestätigt die Callback-Funktion neuer_text() aufgerufen wird.

Hier das Programm dazu:

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


/* Ein Objekt, das allen Callbacks übergeben wird */
typedef struct _CallbackObjekt
{
    GtkBuilder *builder;
    GtkWidget *zeichenflaeche;
    double winkel;
    gchar anzeigetext[20];
} CallbackObjekt;


gboolean neu_malen (GtkWidget *zeichenflaeche, cairo_t *cr, CallbackObjekt *obj)
{
    gint breite, hoehe;
    cairo_text_extents_t ausdehnung;
    
    breite = gtk_widget_get_allocated_width (zeichenflaeche);
    hoehe = gtk_widget_get_allocated_height (zeichenflaeche);
    cairo_set_source_rgb (cr, 1, 0, 0);
    /* Mittelpunkt festlegen */
    cairo_translate (cr, breite / 2.0, hoehe / 2.0);
    /* Font vergrößern */
    cairo_set_font_size (cr, 100);
    /* Text rotieren lassen */
    cairo_rotate (cr, obj->winkel);
    /* Textausdehnung ermitteln */
    cairo_text_extents (cr, obj->anzeigetext, &ausdehnung);
    /* Anfangspunkt für Text verschieben */
    cairo_move_to (cr, -ausdehnung.width / 2.0, ausdehnung.height / 2.0);
    /* Text ausgeben */ 
    cairo_show_text (cr, obj->anzeigetext);
    return TRUE;
}


gboolean zeitgeber (CallbackObjekt *obj)
{
    gint breite, hoehe;
    breite = gtk_widget_get_allocated_width (obj->zeichenflaeche);
    hoehe = gtk_widget_get_allocated_height (obj->zeichenflaeche);
    /* Winkel vergrößern */
    obj->winkel = obj->winkel + 0.063;
    /* Zum Zeichnen auffordern */
    gtk_widget_queue_draw_area (obj->zeichenflaeche, 0, 0, breite, hoehe);
    return TRUE;
}


void neuer_text (GtkEntry *texteditor, CallbackObjekt *obj)
{
    /* neuen Text eintragen */
    g_snprintf (obj->anzeigetext, 20, "%s", gtk_entry_get_text (texteditor));
}


int main (int argc, char *argv[])
{
    CallbackObjekt *callobj;
    GError *errors = NULL;
    GtkWidget *window;
    GtkEntry *editor;

    gtk_init (&argc, &argv);
    callobj = g_malloc (sizeof(CallbackObjekt));
    callobj->builder = gtk_builder_new ();
    callobj->winkel = 0.0;
    gtk_builder_add_from_file (callobj->builder, "cairo2.xml", &errors);
    window = GTK_WIDGET(gtk_builder_get_object (callobj->builder, "hauptfenster"));
    editor = GTK_ENTRY(gtk_builder_get_object (callobj->builder, "entry-1"));
    g_snprintf (callobj->anzeigetext, 20, "%s", gtk_entry_get_text (editor));
    gtk_builder_connect_signals (callobj->builder, callobj);
    callobj->zeichenflaeche = GTK_WIDGET(gtk_builder_get_object (callobj->builder, "leinwand-1"));
    gtk_widget_set_size_request (callobj->zeichenflaeche, 400, 400);
    gtk_widget_show_all (window);
    g_timeout_add (80, (GSourceFunc)zeitgeber, callobj);
    gtk_main ();
    g_free (callobj);
    return 0;
}

Das hier verwendete CallbackObjekt enthält den Winkel und den anzuzeigenden Text. Dieser Text wird erstmals in der Funktion main() gesetzt, die den Text aus dem Builder-Objekt liest.

In regelmäßigen Abständen soll eine Funktion aufgerufen werden, die den Winkel des darzustellenden Textes verändert, so dass dieser sich dreht. Hierfür wird ein Wecker mit der Funktion g_timeout_add() gestellt. Ihr wird eine Zeit in Millisekunden übergeben, eine Callback-Funktion, die aufgerufen wird, wenn der Wecker klingelt und das CallbackObjekt. Die Funktion zeitgeber(), kann einen Wert zurückgeben. Ist dieser TRUE, dann wird der Wecker erneut gestellt.

Die Callback-Funktion zeitgeber() hat nun nichts weiter zu tun, als den Winkel um einen kleinen Bereich weiter zu drehen und zum Neuzeichnen aufzufordern.

Die Funktion neu_malen() legt nun mit cairo_source_rgb() die Zeichenfarbe auf Rot fest. Wir wollen um den Mittelpunkt der Zeichenfläche drehen. Da eine Rotation immer um den Nullpunkt passiert, müssen wir mit cairo_translate() den Nullpunkt auf die Mitte der Zeichenfläche verlagern. Sonst wäre dieser die linke obere Ecke. Damit man den Text auch groß sieht, legen wir die Schriftsatzgröße auf 100 fest, und rotieren dann die Zeichenfläche um den angegebenen Winkel. Bitte beachten Sie, dass dieser Winkel im Uhrzeigersinn notiert wird. In technischen Anwendungen werden positive Winkel zumeist gegen den Uhrzeigersinn aufgefasst. Anschließend fragen wir nach, welche Ausmaße der aktuelle Text haben wird. Die Mitte des Textes soll genau im neuen Nullpunkt liegen, deswegen müssen wir den Text etwas nach links unten rücken. Mit cairo_show_text() wird dann der eigentliche Text auf den Bildschirm gebracht.


Zusammenfassung

[Bearbeiten]

In diesem Kapitel haben Sie die Grundlagen des Zeichnens mit Cairo kennen gelernt. Sie kennen nun das Widget GtkDrawingArea als Zeichenfläche und einige Zeichenoperationen und Transformationen von Cairo. Ebenfalls kennen Sie nun eine Möglichkeit, zeitgesteuert Aufgaben von Ihrem Programm erledigen zu lassen.


Bilder darstellen und malen mit Pixbuf

[Bearbeiten]

Bilder darstellen und malen mit Pixbufs

[Bearbeiten]

Pixbufs sind Speicherbereiche, die ein Bild enthalten plus einiger zusätzlicher Informationen. Bilddaten können mit den dazugehörigen Funktionen geladen und gespeichert werden, außerdem hat man direkten Zugriff auf den Bildspeicher, den man so Bit für Bit manipulieren kann. In Client-Server-Grafikanwendungen repräsentieren Pixbufs die clientseitigen Grafiken, die nicht übertragen werden.

Bilder laden

[Bearbeiten]

Die folgende Anwendung zeigt, wie man Bilder mit Hilfe von Pixbufs lädt. Ein Bild wird in einen scrollbaren Bereich geladen, so dass unter Umständen nicht sofort das gesamte Bild zu sehen ist. Mit einem Knopf kann ein Bild über einen Dateidialog ausgewählt werden. Es wird dabei sicher gestellt, dass im Dateidialog nur Bilddateien angezeigt werden.

Hier zunächst die XML-Beschreibung:

Align=none
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<interface>
    <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="GtkScrolledWindow" id="scrollfenster-1">
            <property name="hscrollbar-policy">GTK_POLICY_ALWAYS</property>
            <property name="vscrollbar-policy">GTK_POLICY_ALWAYS</property>
            <child>
                <object class="GtkViewport" id="viewport-1">
                <child>
                    <object class="GtkImage" id="bildbereich-1" />
                </child>
                </object>
            </child>
            </object>
        </child>
        <child>
            <object class="GtkHBox" id="hbox-layout-1">
            <property name="homogeneous">TRUE</property>
            <child>
                <object class="GtkButton" id="datei-laden-knopf">
                <property name="label">gtk-floppy</property>
                <property name="use-stock">TRUE</property>
                <signal name="clicked" handler="datei_laden" />
                </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">Bildbetrachter</property>
                </object>
                <packing>
                    <property name="expand">FALSE</property>
                    <property name="fill">FALSE</property>
                </packing>
            </child>
            </object>
            <packing>
                <property name="expand">FALSE</property>
                <property name="fill">FALSE</property>
            </packing>
        </child>
        </object>
    </child>
    </object>
</interface>

In ein scrollbaren Fenster vom Typ GtkScrolledWindow, das je einen horizontalen und vertikalen Schieberegler hat, wird indirekt ein Bildbereich vom Typ GtkImage eingefügt. Indirekt deswegen, weil dieser Bildbereich selbst nicht über die Eigenschaft verfügt, scrollbar zu sein. Als Verbindungsstück zwischen den beiden Widgets dient der Adapter vom Typ GtkViewport, der enthaltenen Widgets die Fähigkeit verleiht, scrollbar zu sein.

Die Programmdatei hierzu:

C
#include <gtk/gtk.h>

typedef struct _CallbackObjekt
{
    GtkBuilder *builder;
    GtkImage *bild;
    GtkLabel *textfeld;
} CallbackObjekt;

void datei_laden (GtkWidget *button, CallbackObjekt *obj)
{
    GtkWidget *dateidialog;
    GtkFileFilter *filter;
    /* Nur Bildformate in der Dateiauswahl anzeigen */
    filter = gtk_file_filter_new ();
    gtk_file_filter_add_pixbuf_formats (filter);
    gtk_file_filter_set_name (filter, "Bildformate");
    /* Dialog öffnen, um Datei auszuwählen */
    dateidialog = gtk_file_chooser_dialog_new ("Bild öffnen",
        NULL, 
        GTK_FILE_CHOOSER_ACTION_OPEN,
        GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT,
        GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
        NULL);
    /* Filter einbringen */
    gtk_file_chooser_add_filter (GTK_FILE_CHOOSER(dateidialog), filter);
    if (gtk_dialog_run (GTK_DIALOG (dateidialog)) == GTK_RESPONSE_ACCEPT)
    {
        gchar *dateiname;
        dateiname = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dateidialog));
        /* Bild anzeigen */
        gtk_image_set_from_file (obj->bild, dateiname);
        gtk_label_set_text (obj->textfeld, dateiname);
        g_free (dateiname);
    }
    gtk_widget_destroy (dateidialog);
}

int main (int argc, char *argv[])
{
    CallbackObjekt *callobj;
    GError *errors = NULL;
    GtkWidget *window;
    
    gtk_init (&argc, &argv);
    callobj = g_malloc (sizeof(CallbackObjekt));
    callobj->builder = gtk_builder_new ();
    gtk_builder_add_from_file (callobj->builder, "pixbuf1.xml", &errors);
    gtk_builder_connect_signals (callobj->builder, callobj);
    window = GTK_WIDGET(gtk_builder_get_object (callobj->builder, "hauptfenster"));
    callobj->bild = GTK_IMAGE(gtk_builder_get_object (callobj->builder, "bildbereich-1"));
    callobj->textfeld = GTK_LABEL(gtk_builder_get_object (callobj->builder, "mein-label-1"));
    gtk_widget_set_size_request (window, 200, 200);
    gtk_widget_show_all (window);
    gtk_main ();
    g_free (callobj);
    return 0;
}

In der Callback-Funktion datei_laden() wird zunächst ein Filter erzeugt, der alle Dateien berücksichtigt, die Pixbuf laden kann (gtk_file_filter_add_pixbuf_formats()). Diesem Filter wird ein Name zugewiesen, so dass man ihn im Dateidialog erkennen kann. Anschließend wird der eigentliche Dateidialog mit gtk_file_chooser_dialog_new() erzeugt. Dieser soll zwei Knöpfe haben, einen „Öffnen“-Knopf mit einem eingebetteten Piktogramm und einen „Abbrechen“-Knopf. Drückt der Anwender nach der Auswahl auf den Öffnen-Knopf, so soll GTK_RESPONSE_ACCEPT als Wert von gtk_dialog_run() zurückgegeben werden. Im anderen Fall wird GTK_RESPONSE_CANCEL zurückgegeben. Der Filter wird mit dem Dialog über den Aufruf von gtk_file_chooser_add_filter() verknüpft.

Bestätigt nun der Anwender den Dialog, wird der Dateiname aus dem Dialog ermittelt und das Bild mit gtk_image_set_from_file() anhand dieses Dateinamens geladen. In diesem GtkImage steckt nun auch unser eigentliches Pixbuf. In der Textzeile soll noch der Dateiname angezeigt werden, dann wird der Dialog wieder gelöscht.

Jetzt haben wir einen universellen Leser für bekannte Dateiformate.

Bildbearbeitung

[Bearbeiten]

Mit Pixbufs hat man sofort die Möglichkeit an der Hand, Bilder zu Bearbeiten. Die Funktionen rund um Pixbufs stellen Ihnen allerdings keine Grafikprimitiven zum Zeichnen von Linien, Kreisen und dergleichen zur Verfügung. Allerdings haben Sie die Möglichkeit, jedes einzelne Pixel zu setzen und Pixbufs mit Cairo-Funktionen zu bearbeiten. Wir stellen Ihnen in dem folgenden Beispiel eine Anwendung vor, bei der Sie selbst Grafiken Pixel für Pixel in einer frei wählbaren Farbe malen können. Den Farbauswahldialog erreichen Sie über den Mausknopf drei, das ist zumeist die rechte Maustaste. Malen können sie mit der linken Maustaste.

Hier zunächst die XML-Beschreibung:

Align=none
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<interface>
    <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="GtkDrawingArea" id="malbereich-1" >
            <signal name="draw" handler="neu_malen" />
            <signal name="configure-event" handler="neu_konfigurieren" />
            <signal name="motion-notify-event" handler="maus_bewegt_sich" />
            <signal name="button-press-event" handler="mausknopf_gedrueckt" />
            </object>
        </child>
        <child>
            <object class="GtkLabel" id="mein-label-1">
            <property name="label">Malerei</property>
            </object>
            <packing>
                <property name="expand">FALSE</property>
                <property name="fill">FALSE</property>
            </packing>
        </child>
        </object>
    </child>
    </object>
</interface>

Diese Beschreibungsdatei enthält eine Malfläche vom Typ GtkDrawingArea. Die Malfläche reagiert auf vier Signale, die Aufforderung zum Neuzeichnen („draw“), Veränderung der Größe („configure-event“), Mausbewegungen („motion-notify-event“) und Maustastendrücke („button-press-event“). Die zwei letzten Signale müssen im Programm explizit freigeschaltet werden.

Hier das Programm:

C
#include <gtk/gtk.h>

typedef struct _CallbackObjekt
{
    GtkBuilder *builder;
    guchar *daten;
    GdkPixbuf *pixbuf;
    GtkLabel *textfeld;
    gint zeilenlaenge;
    guchar rot, gruen, blau;
} CallbackObjekt;

static void male_pixel (CallbackObjekt *obj, gint x, gint y)
{
    if (!obj->daten)
        return;
    if (x < 0)
        return;
    if (y < 0)
        return;
    obj->daten[y * obj->zeilenlaenge + 3 * x] = obj->rot;
    obj->daten[y * obj->zeilenlaenge + 3 * x + 1] = obj->gruen;
    obj->daten[y * obj->zeilenlaenge + 3 * x + 2] = obj->blau;
}

gboolean neu_malen (GtkWidget *malflaeche, cairo_t *cr, CallbackObjekt *obj)
{
    gdk_cairo_set_source_pixbuf (cr, obj->pixbuf, 0, 0);
    cairo_paint (cr);
    return TRUE;
}

gboolean neu_konfigurieren (GtkWidget *malflaeche,
	GdkEventConfigure *ereignis, CallbackObjekt *obj)
{
    gint breite, hoehe;
    breite = ereignis->width;
    hoehe = ereignis->height;
    obj->zeilenlaenge = 3 * breite;
    if (obj->pixbuf)
        g_object_unref (obj->pixbuf);
    if (obj->daten)
        g_free (obj->daten);
    /* Speicher für RGB-Daten (3 Bytes pro Pixel) anfordern */
    obj->daten = g_malloc (obj->zeilenlaenge * hoehe);
    obj->pixbuf = gdk_pixbuf_new_from_data (obj->daten, GDK_COLORSPACE_RGB, FALSE, 8, breite, hoehe, obj->zeilenlaenge, NULL, NULL);
    return FALSE;
}

gboolean maus_bewegt_sich (GtkWidget *malflaeche, GdkEventMotion *ereignis,
	CallbackObjekt *obj)
{
    gint x, y;
    gchar statustext[128];
    x = (gint) ereignis->x;
    y = (gint) ereignis->y;

    if (ereignis->state & GDK_BUTTON1_MASK)
    {
        male_pixel (obj, x, y);
        gtk_widget_queue_draw (malflaeche);
    } 
    
    g_snprintf (statustext, 128, "X: %d, Y: %d", x, y);
    gtk_label_set_text (obj->textfeld, statustext);
    return TRUE;
}

gboolean mausknopf_gedrueckt (GtkWidget *malflaeche, GdkEventButton *ereignis,
	CallbackObjekt *obj)
{
    gint x, y;
    x = (gint) ereignis->x;
    y = (gint) ereignis->y;
    if (ereignis->button == 1)
    {
        /* Pixel malen */
        male_pixel (obj, x, y);
        gtk_widget_queue_draw (malflaeche);
    }
    else if (ereignis->button == 3)
    {
        /* Farbdialog aufrufen, Farbe neu setzen */
        GtkWidget *dialog;
        GtkColorSelection *farbwaehler;
        GdkColor farbe;
        farbe.pixel = 0;
        farbe.red = obj->rot * 65535 / 255;
        farbe.green = obj->gruen * 65535 / 255;
        farbe.blue = obj->blau * 65535 / 255;
        dialog = gtk_color_selection_dialog_new ("Wählen Sie bitte eine Farbe aus");
        farbwaehler = GTK_COLOR_SELECTION(gtk_color_selection_dialog_get_color_selection(GTK_COLOR_SELECTION_DIALOG(dialog)));
        gtk_color_selection_set_current_color (farbwaehler, &farbe);
        gtk_dialog_run (GTK_DIALOG(dialog));
        gtk_color_selection_get_current_color (farbwaehler, &farbe);
        gtk_widget_destroy (dialog);
        obj->rot = 255 * farbe.red / 65535;
        obj->gruen = 255 * farbe.green / 65535;
        obj->blau = 255 * farbe.blue / 65535;
    }
    return TRUE;
}


int main (int argc, char *argv[])
{
    CallbackObjekt *callobj;
    GError *errors = NULL;
    GtkWidget *window, *malflaeche;
    
    gtk_init (&argc, &argv);
    callobj = g_malloc (sizeof(CallbackObjekt));
    callobj->daten = NULL;
    callobj->pixbuf = NULL;
    callobj->builder = gtk_builder_new ();
    callobj->rot = 255;
    callobj->gruen = 100;
    callobj->blau = 64;
    gtk_builder_add_from_file (callobj->builder, "pixbuf2.xml", &errors);
    gtk_builder_connect_signals (callobj->builder, callobj);
    callobj->textfeld =  GTK_LABEL(gtk_builder_get_object (callobj->builder, "mein-label-1"));
    window = GTK_WIDGET(gtk_builder_get_object (callobj->builder, "hauptfenster"));
    gtk_widget_set_size_request (window, 200, 200);
    malflaeche = GTK_WIDGET(gtk_builder_get_object (callobj->builder, "malbereich-1"));
    gtk_widget_add_events (malflaeche, GDK_POINTER_MOTION_MASK | GDK_BUTTON_PRESS_MASK);
    gtk_widget_show_all (window);
    gtk_main ();
    g_free (callobj);
    return 0;
}

Pixbufs selber enthalten die Bilddaten als Folgen von vorzeichenlosen Bytes. Die einzelnen Pixel haben dabei einen roten, grünen und blauen Anteil, der jeweils ein Byte ausmacht. Ein Grafikpixel besteht also aus drei aufeinander folgenden Bytes. Danach folgen wieder drei Bytes für das nächste Pixel und so fort, bis eine Zeile voll ist. Die Zeilenlänge, also die Anzahl der Bytes pro Farbzeile, ist dabei 3 * Anzahl der Pixel bei 24 Bit Farbtiefe, wie man es für ein RGB-Bild benötigt. Hier zwei Beispiele, um Pixelmanipulationen vorzunehmen. Das Vorgehen ist in der Funktion male_pixel() implementiert:

  • Möchte man das nullte Pixel verändern, dann schreibt man an die Stelle Null den Wert für Rot, an die Stelle Eins den Wert für Grün und an die Stelle Zwei den Wert für Blau.
  • Möchte man das zweite Pixel der dritten Zeile verändern, dann nimmt man im Datenarray 3 * Zeilenlänge + 2 * 3, und schreibt ab dort 3 Bytes mit den RGB-Farbinformationen hinein.

Die Callback-Funktion neu_konfigurieren() wird unter anderem aufgerufen, wenn die Größe der Malfläche verändert wird. In diesem Fall erzeugen wir einen neuen Datenarray, der so groß ist, dass er die gesamte zur Verfügung stehende Malfläche ausfüllen kann. Falls es schon ein Datenarray gibt, wird es gelöscht, um etwas Speicher zu sparen. Anschließend wird mit gdk_pixbuf_new_from_data() mit diesem Datenarray ein neues Pixbuf-Objekt erzeugt. Diese Funktion erwartet das Datenarray, den Farbraum, wobei hier zur Zeit als einzige Auswahl der angegebene RGB-Farbraum möglich ist, eine Angabe darüber, ob ein transparenter Bereich zur Verfügung steht, die Anzahl der Bits pro Farbelement, Breite und Höhe des Bildes, und die Zeilenlänge. Die letzten beiden Parameter können Sie mit einer Funktion füllen, die aufgerufen wird, wenn das Pixbuf gelöscht wird sowie Daten, die diese Funktion zusätzlich erhält. Jetzt haben wir ein Datenarray, in das wir hineinschreiben können und ein Pixbuf, das wir anzeigen, verändern und speichern können.

In der Callback-Funktion neu_malen() sehen Sie auch sogleich, wie man ein Pixbuf auf den Bildschirm bringt, nämlich mit Hilfe von Cairo. Dort könnten Sie auch weitere Cairo-Funktionen einsetzen, um das Bild zu manipulieren. Mit gdk_cairo_set_source_pixmap() wird das Pixmap Cairo übergeben, es wird durch einen Aufruf von cairo_paint() gezeichnet.

Wenn sich die Maus über der Malfläche bewegt, dann wird die Callback-Funktion maus_bewegt_sich() aufgerufen. Im Ereignis-Parameter steht unter anderem drin, wo sich die Maus gerade aufhält und welcher Mausknopf gleichzeitig gedrückt wurde. Wurde während der Bewegung der Mausknopf 1 gedrückt, dann soll an diese Stelle mit der aktuellen Farbe ein Punkt gemalt werden. Anschließend wird zum Neuzeichnen aufgefordert und der Statustext verändert.

Wird mit der Maus auf eine stelle im Malbereich geklickt, dann wird die Callback-Funktion mausknopf_gedrückt() aufgerufen. Der Ereignis-Parameter enthält ebenfalls die Position des Mauszeigers und gibt Auskunft darüber, welcher Knopf gedrückt wurde. Wenn der Mausknopf 1 gedrückt wurde, dann soll an diese Stelle ein Pixel gemalt werden. Wurde hingegen Mausknopf 3 gedrückt, dann soll ein Farbdialog aufgerufen werden.

Mit dem Farbdialog wird über eine Struktur vom Typ GdkColor kommuniziert. Diese verwendet 16 Bit Farbtiefe pro Farbkanal, wir verwenden in unserer male_pixel()-Funktion nur 8 Bit. Da wir die aktuelle Zeichenfarbe vorgeben wollen, müssen wir sie umrechnen. Anschließend starten wir den Dialog mit gtk_color_selection_dialog_new() und holen uns mit gtk_color_selection_dialog_get_color_selection() das Widget, das für die eigentliche Farbauswahl verantwortlich ist. Diesem wird nun unsere Farbe vom Typ GdkColor“ übermittelt. Der Dialog wird dann mit gtk_dialog_run() gestartet. Wird er beendet, so holen wir uns mit gtk_color_selection_get_current_color() die ausgewählte Farbe und konvertieren sie wieder in 8 Bit.

Damit wir Mausbewegungen und Maustastenereignisse gemeldet bekommen, werden mit gtk_widget_add_events() Benachrichtigungen darüber eingeschaltet. Wenn Sie Mausbewegungen verfolgen wollen, bei denen der Anwender eine Taste klicken können soll, muss GDK_BUTTON_PRESS_MASK zwingend eingeschaltet werden, auch, wenn sie nicht gesondert auf Maustastendrücke reagieren wollen. Sonst ist das entsprechende Feld in der Ereignisstruktur vom Typ GdkEventMotion nicht zu gebrauchen.

Zusammenfassung

[Bearbeiten]

In diesem Kapitel haben Sie Pixbufs auf zwei Weisen kennen gelernt, nämlich einmal „versteckt“ in einem GtkImage und einmal so, dass Sie jedes Pixel anfassen durften. Ganz nebenbei haben wir Ihnen einige Widgets wie GtkImage, GtkScrolledArea, GtkViewport und andere vorgestellt und Sie mit einigen Details der Ereignisbehandlung vertraut gemacht.


Nachwort

[Bearbeiten]

In diesem Buch haben Sie viel GTK+ kennen gelernt. Wir haben Ihnen sehr viele Widgets vorgestellt und Ihnen Methoden an die Hand gegeben, eigene Programme mit grafischen Benutzeroberflächen zu schreiben. Moderne grafische Benutzeroberflächen werden mit Werkzeugen wie „Glade“ entworfen. Sie erhalten, wie es in diesem Buch durchgängig gemacht wurde, eine XML-Datei und bauen darauf ihr Programm auf.

Wenn Sie sich nach dem Durcharbeiten dieses Buches noch unsicher fühlen, was die Programmierung mit GTK+ angeht, dann können wir Ihnen einige Ratschläge geben. Schauen sie sich Programme und Programmcode an, der mit GTK+ geschrieben wurde. Zu GTK+ gehört ein Programm namens „gtk3-demo“, das erläuterten Quelltext und ausführbare Programme bereitstellt. Schreiben Sie selber kleine Programme, mit bunten Piktogrammen in der Werkzeugleiste, einem Menü und einem ansprechenden Layout, vielleicht einen kleinen Editor oder einen Kalender. Ihr Wegbegleiter dabei ist die API-Referenz, die Sie jetzt gut kennen, wenn Sie den Hinweis im Vorwort beachtet haben.

Wenn Sie sich fit in GTK+ fühlen, dann schauen Sie sich doch einmal die Gnome-Bibliotheken an, die zu einem Teil auf GTK+ aufsetzen. Vielleicht packt Sie ja das Interesse, an Gnome und den Programmen rund herum mitzuwirken.

Es bleibt uns jetzt noch eins zu sagen: Viel Spaß beim Programmieren!


Anhang A: Quellen und weiterführende Literatur

[Bearbeiten]

Quellen

[Bearbeiten]

Als Quellen für dieses Buch dienten hauptsächlich

Weiterführende Weblinks

[Bearbeiten]

Bücher

[Bearbeiten]
  • Warkus, M: "Das GTK+/GNOME-Entwicklerhandbuch", dpunkt-Verlag - Das Buch behandelt in der Auflage von 2008 die Version 2.x von GTK+ und Teilen von GNOME.
  1. selbstverständlich existieren auch andere Werkzeuge wie z. B.  Motif, um diese geht es jedoch nicht in diesem Buch