vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbacknächstes Kapitel


Woche 3

Tag 21

Einführung in die GUI- Programmierung mit GTK+

Die heutige Lektion ist der Erstellung von Programmen mit grafischen Benutzeroberflächen (abgekürzt GUI für »Graphical User Interface«) gewidmet. Sie werden erfahren, wie man mit der Programmierbibliothek GTK+ fensterbasierte Anwendungen schreibt. Im Einzelnen lernen Sie:

Geschichte

Bisher haben wir in allen Programmierbeispielen den Standardeingabe-Stream zum Einlesen von der Tastatur und den Standardausgabe-Stream zum Anzeigen von Informationen auf dem Bildschirm genutzt. In der heutigen Lektion werden Sie erfahren, wie man unter Linux grafische Benutzeroberflächen programmiert. Die GUI-Schnittstelle (GUI steht für »Graphical User Interface«, was soviel bedeutet wie »grafische Benutzerschnittstelle« oder »grafische Benutzeroberfläche«) von Linux basiert auf dem X Window System, das oft auch einfach nur X genannt wird.

Die Entwicklung von X begann 1984 am Massachusetts Institute of Technology (MIT), wo man daran arbeitete, eine einheitliche, für Netzwerke geeignete, grafische Benutzeroberfläche zu konzeptionieren, die auf möglichst vielen Workstations qualitativ hochwertige grafische Anzeigen liefern sollte. Der Code wurde gänzlich in C geschrieben und als Quelltext freigegeben. Jeder durfte den Code kopieren, verändern und darauf aufbauende EXE-Dateien verkaufen, solange gewährleistet war, dass jede Kopie des Quelltextes oder der EXE-Dateien mit einem Copyright-Vermerk des MIT versehen war.

X-Konzepte

Aus Sicht des Anwenders mag das X Window System anderen fensterbasierten Umgebungen wie Microsoft Windows oder der Oberfläche des Apple Macintosh sehr ähnlich sein, doch sind die zugrunde liegenden Konzepte recht verschieden. Die grafischen Oberflächen von MS Windows und des Macintosh sind eng verwoben mit den Maschinen, auf denen die Programme ausgeführt werden. Hingegen kann ein korrekt implementiertes X-Programm - ohne weitere Anstrengungen von Seiten des Programmierers - auf jeder beliebigen Linux/Unix-Maschine ausgeführt werden und seine Ausgaben auf jeder Maschine anzeigen, die X als GUI-Oberfläche verwendet und mit der ersten Maschine über ein Netzwerk verbunden ist. Ob beide Maschinen den gleichen Mikroprozessor und das gleiche Betriebssystem verwenden oder nicht, ob sie zusammen in einem Raum stehen oder über den halben Globus verteilt sind, spielt dabei keine Rolle. Wichtig ist nur, dass beide Maschinen über ein verlässliches Highspeed-Netzwerk miteinander verbunden sind.

Die besondere Netzwerkunterstützung des X Window System resultiert aus der Aufspaltung der Funktionalität in zwei Komponenten: den X-Server und den X-Client. Der Server wird auf der Maschine ausgeführt, die für den Aufbau der GUI-Oberflächen verantwortlich ist. Der Client ist ein Programm, das vom Server Ereignisse empfängt, die es über Maus- und Tastatureingaben informieren, und das seinerseits Anforderungen an den Server sendet, wenn Teile der Oberfläche neu gezeichnet werden müssen. Die Kommunikation zwischen Client und Server erfolgt gemäß einem speziellen botschaftenorientierten Protokoll, dem X-Protokoll. Das Client-Programm sollte die X-Protokollbotschaften, die es sendet, nicht selbst erzeugen, sondern dazu die entsprechenden Bibliotheksfunktionen verwenden, die in der X-Bibliothek Xlib definiert sind. Die Xlib-Bibliothek stellt eine C-Schnittstelle zum Botschaftensystem von X zur Verfügung.

Die Funktionalität von Xlib ist recht begrenzt und beschränkt sich im Wesentlichen auf die Erzeugung von Fenstern, das Zeichnen einfacher Objekte und die Übertragung von Maus- und Tastaturereignissen vom Server zum Client. Hinter dem spartanischen Design von Xlib steckt durchaus Absicht; es fördert die Entwicklung höherer Bibliotheken, die auf Xlib aufbauen und deren Funktionalität erweitern. So gibt es mittlerweile eine Vielzahl von GUI-Bibliotheken (im englischen Sprachraum häufig als widget toolkits1 bezeichnet), die mit einer grossen Bandbreite an Optionen und Schnittstellen aufwarten. Zu diesen Bibliotheken gehören zum Beispiel: XForms, Motif, OpenLook, EZWGL, FLTK, Xaw, Qt und GTK+.

GTK+ - die Gimp-Werkzeugsammlung

GTK+, das Gimp-Toolkit, entstand quasi als Nebenprodukt bei der Entwicklung eines Programms mit dem Namen Gimp, dem »GNU Image Manipulation Tool2«. GTK+ wurde in C geschrieben und stellt eine C-Schnittstelle zu seinen vielfältigen Widgets (Schaltflächen, Bildlaufleisten, Texteingabefelder, Fortschrittsanzeigen, statische Textfelder, Optionsschalter, Menüs und viele andere mehr) zur Verfügung. GTK+ ist relativ schlank programmiert, frei verfügbar, zwischen allen Maschinen portierbar, die das X Window System unterstützen, und unterliegt der GNU Lesser General Public License, was bedeutet, dass jede Art von Software, sei sie Public Domain, Freeware, Shareware, GPL oder kommerziell, GTK+ dynamisch einbinden kann, ohne Nutzungs- oder Lizenzgebühren entrichten zu müssen.

Im X-Window-Sprachgebrauch bezeichnet man als »Widgets« Elemente der grafischen Benutzerschnittstelle, mit denen der Anwender via Maus oder Tastatur interagieren kann (im Deutschen auch als Oberflächenelemente bezeichnet). Es sind voll funktionsfähige, eigenständige Komponenten, aus denen man grafische Benutzeroberflächen wie aus Bausteinen aufbauen kann. Zu den bekanntesten Widgets gehören Schaltflächen, Menüleisten, Dialogfenster und Bildlaufleisten. Alle Widgets verfügen über eine wohldefinierte grafische Schnittstelle, die zur Laufzeit auf dem Bildschirm angezeigt werden kann. Die Funktionsweise der Widgets kann durch Bearbeitung ihrer Eigenschaften und zur Laufzeit durch Abfangen der zugehörigen Ereignisse angepasst werden. Es sind wirklich erstaunliche kleine Codemodule, die die Programmentwicklung revolutioniert haben.

Mehr als eine kurze Einführung in die Konzepte der GTK+-Programmierung ist im Rahmen dieses Buches leider nicht möglich. Eine ausführlichere und gut verständliche Beschreibung des GTK+-Toolkits finden Sie in den GNU-Infopages. Wenn Sie sich intensiver mit den Techniken der GTK+-Programmierung vertraut machen wollen, sollten Sie unbedingt die GTK+-Infopages lesen (wozu Sie den Gnome-Hilfe-Browser oder das info-Programm verwenden können). Eine weitere gute Informationsquelle mit vielen Tipps und Tricks zur GTK+-Programmierung sind die vielen GTK+- Programme, die unter der GNU General Public License mit vollständigem Quelltext vertrieben werden.

Wo befindet sich das GTK+?

Am Tag 20, »Compiler für Fortgeschrittene«, haben Sie gelernt, wie man dem gcc mitteilt, dass er in bestimmten Verzeichnissen nach Header-Dateien und Bibliotheken sucht. Dies kommt uns nun zugute, denn für die Programmierung mit den GTK+- Widgets benötigt man eine Reihe von Header-Dateien und Bibliotheken, die je nach Installation in unterschiedlichen Verzeichnissen abgelegt sein können. Glücklicherweise haben die Leute, die die GTK+-Widgets entwickelt haben, auch daran gedacht, einen Weg zur Verfügung zu stellen, wie man auf einfache Weise herausfinden kann, wo diese Dateien und Bibliotheken installiert sind. Zusammen mit dem GTK+ wird ein Programm namens gtk-config installiert. Dieses Programm weiß, wo die benötigten Header-Dateien und Bibliotheken installiert sind. Wenn Sie gtk-config im Terminal-Fenster ausführen, sollten Sie ungefähr folgende Ausgabe sehen:

[erik@coltrane Tag21]$ gtk-config
Usage: gtk-config [OPTIONS] [LIBRARIES]
Options:
[ --prefix[=DIR]]
[ --exec-prefix[=DIR]]
[ --version]
[ --libs]
[ --cflags]
Libraries:
gtk
gthread

Wenn in Ihrer Konsole statt der obigen Ausgabe eine »command not found«-Meldung erscheint, ist dies ein Hinweis darauf, dass GTK+ auf Ihrem System nicht installiert ist. In diesem Fall müssen Sie den GTK+ von Ihrer Installations-CD installieren oder von der Website Ihrer Linux-Distribution herunterladen.

Einige Linux-Distributionen, wie zum Beispiel Red Hat, Mandrake oder SuSE, teilen den GTK+ in zwei getrennte Pakete auf: eines für die eigentlichen Bibliotheken und ein zweites für die Header-Dateien und die GTK+-Entwicklungstools. Der Name des zweiten Pakets wird vermutlich mit gtk+-devel oder einem ähnlichen Präfix beginnen.

Das gtk-config-Programm weist auf eine Reihe von Optionen hin, von denen uns im Moment allerdings nur die beiden letzten interessieren. Die Option --cflags gibt eine Liste der Include-Verzeichnisse aus, die für den gcc benötigt werden. Die Option --libs führt die zusätzlichen Bibliotheken auf, die von GTK+-Programmen benötigt werden. Auf meinem System geben die beiden gtk-config-Optionen folgende Informationen aus (die auf Ihrem System abweichen können):

[erik@coltrane Tag21]$ gtk-config  --cflags
-I/usr/X11R6/include -I/usr/lib/glib/include
[erik@coltrane Tag21]$ gtk-config --libs
-L/usr/lib -L/usr/X11R6/lib -lgtk -lgdk -rdynamic -lgmodule -lglib -ldl
-lXext -lX11 -lm

Wir werden das gtk-config-Programm im weiteren Verlauf dieses Kapitels für die Kompilierung unserer GTK+-Programme nutzen.

Grafische Oberflächen und Ereignisse

Die Programme, mit denen wir es bisher in diesem Buch zu tun hatten, wurden im Wesentlichen linear vom Anfang bis zum Ende ausgeführt. Die einzige Ausnahme waren die Programme von Tag 19, »Prozesse und Signale«, die Signale abfingen. Wie Sie sich sicherlich erinnern werden, richteten diese Programme Signal- Bearbeitungsroutinen ein - Funktionen, die als Antwort auf bestimmte Ereignisse (beispielsweise die Eingabe von Steuerzeichen im Terminal-Fenster) asynchron vom Betriebssystem aufgerufen werden.

Programme mit grafischen Benutzeroberflächen müssen ebenfalls auf asynchrone Ereignisse, die vom Anwender ausgelöst werden, reagieren. Denken Sie zum Beispiel an ein GUI-Programm, das eine Reihe von GUI-Widgets wie Schaltflächen, Bildlaufleisten und Texteingabefelder enthält. Der Programmierer kann in diesem Falle weder wissen, wann der Anwender welche dieser Widgets verwendet, noch in welcher Reihenfolge der Anwender mit den Widgets interagiert.

Wann immer der Anwender mit einem GUI-Widget interagiert (es beispielsweise anklickt), sendet der X-Server, der auf der Maschine des Anwenders ausgeführt wird, eine X-Protokoll-Botschaft an das Client-Programm. Zum Abfangen und Bearbeiten solcher asynchroner, vom Anwender ausgelöster Ereignisse bedient man sich üblicherweise so genannter Callback-Funktionen. Der Mechanismus, der dahinter steht, gleicht der Verwendung der Signal-Bearbeitungsroutine (siehe Tag 19). Für jedes Widget, das erzeugt wird, gibt man gleichzeitig eine Ereignisbehandlungsroutine oder Callback-Funktion an. Wenn der Anwender mit dem Widget interagiert, erzeugt der X-Server ein entsprechendes Ereignis, woraufhin Xlib oder das jeweilige Widget-Toolkit die zugehörige Callback-Funktion aufruft. Üblicherweise wird dabei für jedes Widget eine eigene Callback-Funktion aufgesetzt.

Da die Callback-Funktionen der GUI-Programme asynchron aufgerufen werden, ist das Debuggen dieser Programme nicht einfach. Oft weiß der Programmierer weder, welche Funktionen aufgerufen werden, noch, in welcher Reihenfolge dies geschieht. Auch der Einsatz eines Debuggers, beispielsweise des DDD, ist üblicherweise nur wenig hilfreich, da die GUI-Programme nicht linear ausgeführt werden, sondern in mehr oder weniger unvorhersehbarer Folge von Funktion zu Funktion springen. Der beste Weg, GUI-Programme zu debuggen, besteht daher meist darin, printf()- Anweisungen einzufügen und die GUI-Anwendung dann von der Konsole aus aufzurufen, damit die Ausgaben der printf()-Anweisungen in der Konsole angezeigt werden.

Ein erstes GTK+-Programm

Lassen Sie uns in medias res gehen und uns an einem ersten, einfachen GTK+- Programm versuchen. Das Programm aus Listing 21.1 macht nichts wirklich Sinnvolles, aber es erlaubt uns, einige der grundlegenden Konzepte auszuloten, die hinter der GTK+-Programmierung stehen. Kompilieren Sie das Programm mit dem folgenden Befehl:

gcc -Wall -ggdb `gtk-config  --cflags` `gtk-config  --libs` list2101.c \
-o list2101

Unglücklicherweise musste der obige Befehl wegen der beschränkten Seitenbreite mit Hilfe des Backslash umbrochen werden. Lassen Sie den Backslash bei der Eingabe bitte weg.

Der gtk-config-Befehl wird hier dazu verwendet, dem gcc-Compiler die Verzeichnisse mit den zusätzlichen Header- und Bibliotheksdateien anzugeben, die für die Kompilierung des GTK+-Programms benötigt werden. Beachten Sie, dass die Argumente gtk-config --cflags und gtk-config --libs in einfache, nach vorne abfallende (und nicht nach vorne aufsteigende) Anführungsstriche gesetzt werden. Die nach vorne abfallenden Schrägstriche finden Sie auf der deutschen Tastatur neben der Rückstelltaste. Halten Sie die [ª]-Taste gedrückt und drücken Sie danach zuerst die Taste mit den Anführungsstrichen und dann die Leertaste. Das einfache, gerade Anführungszeichen kann hier nicht verwendet werden.

Wenn Sie das Programm nach der Kompilierung starten, sollte ein kleines Fenster erscheinen, das wie das links gelegene Fenster aus Abbildung 21.1 aussieht. In der Titelleiste des Fensters sollte list2101.c zu lesen sein. Wenn Sie auf die Schließen- Schaltfläche des Fensters klicken (üblicherweise ein kleines Kästchen mit einem Kreuz, das ganz rechts in der Titelleiste des Fensters zu finden ist), wird das Fenster des Programms geschlossen und auf der Konsole, von der das Programm ausgerufen wurde, eine Meldung ausgegeben.

Abbildung 21.1:  Die ersten drei Programme der heutigen Lektion erzeugen die Fenster mit den Titeln list2101.c, list2102.c und list2103.c. Das Fenster mit dem Titel Beenden? ist ein Dialogfenster, das zu dem Programm list2103.c gehört.

Da Aussehen und Bedienung der Oberfläche des X Window System konfigurierbar sind, ist es gut möglich, dass die Fenster auf Ihrem System etwas anders aussehen. Das Erscheinungsbild der X-GUI-Oberfläche wird vom Window-Manager bestimmt, der Titelzeile und Rahmen der Fenster zeichnet und die Funktionsweise der Maus festlegt. Abbildung 21.1 wurde unter der KDE-Desktop-Umgebung erstellt. Andere häufig genutzte Window-Manager sind Window Maker, Gnome, AfterStep, NeXtStep, and FVWM.

Listing 21.1: Ein minimales GTK+-Programm.

1 : /* list2101.c - Ein minimales GTK+-Programm. */
2 :
3 : #include <gtk/gtk.h>
4 :
5 : void loeschen_funk(GtkWidget *widget, gpointer daten);
6 :
7 : int main(int argc, char *argv[])
8 : {
9 : GtkWidget *hauptfenster;
10:
11: gtk_init(&argc, &argv);
12:
13: hauptfenster = gtk_window_new(GTK_WINDOW_TOPLEVEL);
14: gtk_widget_set_usize(GTK_WIDGET(hauptfenster), 180, 120);
15: gtk_window_set_title(GTK_WINDOW(hauptfenster), __FILE__);
16:
17: gtk_signal_connect(GTK_OBJECT(hauptfenster), "destroy",
18: GTK_SIGNAL_FUNC(loeschen_funk), NULL);
19:
20: /* Fenster sichtbar machen. */
21: gtk_widget_show(hauptfenster);
22:
23: gtk_main();
24:
25: g_print("main() wird nun beendet.\n");
26: return 0;
27: }
28:
29: void loeschen_funk(GtkWidget *widget, gpointer zdaten)
30: {
31: g_print("Beenden : Destroy-Signal wurde empfangen.\n");
32: gtk_main_quit();
33: }

Dieses Listing bindet eine einzige Header-Datei ein: gtk/gtk.h. Die Header-Datei stdio.h, die wir bisher in jedem unserer Programme verwendet haben, wird nicht benötigt. Die Header-Datei gtk/gtk.h bildet den Haupt-Header für die GTK+- Programmierung und definiert eine Reihe von Funktionen und Typen, die auch in diesem Programm verwendet werden. Zu den neuen Typen gehören GtkWidget und gpointer, deren Bedeutung im Laufe der Analyse des Programms klar werden wird.

In Zeile 5 steht der Funktionsprototyp für die Funktion loeschen_funk(), die in den Zeilen 29 bis 33 vollständig definiert ist. Die main()-Funktion beginnt in Zeile 7. Sie übernimmt die Kommandozeilenargumente argc und argv[], wertet sie aber nicht selbst aus, sondern reicht sie an die Funktion gtk_init() weiter (Zeile 11), die die GTK+-Bibliotheken initialisiert. Beachten Sie, dass gtk_init() nicht argc und argv[], sondern Zeiger auf argc und argv[] übergeben werden. Durch diesen Trick kann das GTK+-Toolkit auf die main()-Variablen argc und argv[] zugreifen und Kommandozeilenargumente, für deren Verarbeitung es vorbereitet ist, auswerten und aus der Liste der Kommandozeilenargumente löschen, so dass die main()-Funktion nicht mehr mit deren Bearbeitung belastet wird. Natürlich setzt dies voraus, dass das Programm die Kommandozeilenargumente erst nach dem Aufruf von gtk_init() auswertet (falls überhaupt).

In Zeile 13 wird die Funktion gtk_window_new() aufgerufen, die ein Top-Level-Fenster erzeugt und einen Zeiger auf ein Objekt des Typs GtkWidget zurückliefert. Wie schon für die File*-Zeiger, mit denen wir am Tag 15, »Mit Dateien arbeiten«, zu tun hatten, gilt auch für den Typ GtkWidget, dass die genaue Definition dieses Typs nicht so wichtig ist; wir brauchen den zurückgelieferten Zeiger lediglich als Argument für spätere Aufrufe weiterer GTK+-Funktionen. Die anfängliche Größe des Hauptfensters wird durch einen Aufruf der Funktion gtk_widget_set_usize() festgelegt (Zeile 14); der Namenszug für die Titelleiste wird in Zeile 15 durch einen Aufruf der Funktion gtk_window_set_title() gesetzt. Beachten Sie die jeweils ersten Argumente der beiden Funktionen: GTK_WIDGET(hauptfenster) in Zeile 14 und GTK_WINDOW(hauptfenster) in Zeile 15. Diese Argumente verwenden Makros, die in der Header-Datei gtk/gtk.h definiert sind und den Typ des übergebenen Arguments in den Typ umwandeln, der von der Funktion gefordert wird. So erwartet beispielsweise die zweite Funktion, gtk_window_set_title(), als erstes Argument einen GtkWindow-Zeiger. Die Variable hauptfenster ist aber als GtkWidget* definiert. Der Makroaufruf GTK_WINDOW(hauptfenster) prüft daher, ob eine Umwandlung des Zeigers hauptfenster vom Typ GtkWidget* in GtkWindow* möglich ist, und führt diesen gegebenenfalls durch. Ist die Typumwandlung nicht möglich, wird eine Fehlermeldung ausgegeben. Leider gibt es nach einem solchen Fehler für die Anwendung keine Möglichkeit mehr, die normale Programmausführung wieder aufzunehmen, weswegen das Programm danach mit [Strg]+[C] beendet werden sollte. In der Regel tritt dieser Fehler allerdings nur während der Entwicklungsphase und dem Debuggen neuer Programme auf.

Nachdem das Hauptfenster erzeugt wurde, braucht man eine Möglichkeit, das Fenster wieder zu schließen und die Anwendung zu beenden. Die meisten Window-Manager blenden zu diesem Zweck in der rechten oberen Ecke des Fensters eine kleine Schaltfläche mit einem Kreuz ein. Für die meisten Anwendungen gilt, dass man die Anwendung durch Klick auf diese Schaltfläche schließen kann. Wie Sie nach meinen Ausführungen über die X-Protokoll-Ereignisse vermuten werden, löst das Klicken dieser Schaltfläche eine asynchrone Botschaft aus, die die Anwendung darüber informiert, dass sie sich beenden soll. In der GTK+-Programmierung bezeichnet man diese asynchronen Botschaften als Signale. Jedes Signal trägt einen eindeutigen Namen, den man im Programmcode verwenden kann, um sich auf das Signal zu beziehen. Das Signal, das mit der Schließen-Schaltfläche aus der Titelleiste verbunden ist, heißt beispielsweise »destroy3«.

Um eine Callback-Funktion mit einem speziellen GTK+-Signal zu verbinden, bedient man sich der Funktion gtk_signal_connect() (siehe Zeile 17). Die gtk_signal_connect()-Funktion übernimmt vier Argumente, von denen das erste ein Zeiger auf ein GtkObject ist. Ein GtkObject-Zeiger ist nicht das Gleiche wie ein GtkWidget-Zeiger, aber es ist möglich, einen GtkWidget-Zeiger mit Hilfe des Makros GTK_OBJECT() in einen GtkObject-Zeiger umzuwandeln. Das zweite Argument, das gtk_signal_connect() übergeben wird, ist ein String, der den Namen des Signals angibt (in unserem Beispiel also »destroy«). Das dritte Argument ist ein Zeiger auf die Callback- Funktion, der mit Hilfe des Makros GTK_SIGNAL_FUNC() in einen GtkSignalFunc-Zeiger umgewandelt wird. Das letzte Argument ist generisch und wird in unserem Fall auf NULL gesetzt.

Nach der Einrichtung der Callback-Funktion für das destroy-Ereignis wird in Zeile 21 die Funktion gtk_widget_show() aufgerufen, die das Hauptfenster auf dem Bildschirm sichtbar macht. Gleich darunter, in Zeile 23, folgt der Aufruf von gtk_main(). Die Funktion gtk_main() startet den Mechanismus für die Verarbeitung der GUI-Ereignisse und kehrt erst zurück, wenn das Programm beendet wird.

Die Funktion loeschen_funk(), die in den Zeilen 29 bis 33 definiert wird, ist die Callback-Funktion für das destroy-Signal. Wenn die Funktion loeschen_funk() aufgerufen wird, gibt sie zuerst mit Hilfe von g_print() eine Meldung aus (g_print() wird praktisch in der gleichen Weise verwendet wie die Funktion printf(), die Sie aus den vorangegangenen Tagen kennen). Danach ruft sie die Funktion gtk_main_quit() auf, die dafür sorgt, dass die Funktion gtk_main() aus der main()-Funktion des Programms zurückkehrt. Das Programm aus Listing 19.7 von Tag 19 wurde auf ähnliche Weise beendet: Das Programm führte eine Endlosschleife aus, die erst beendet wurde, nachdem die SIGINT-Signal-Bearbeitungsroutine den Wert einer bestimmten Variablen änderte.

Wie Sie der Ausgabe, die das Programm in den stdout-Stream schreibt, entnehmen können:

erik@coltrane Tag21]$ ./list2101
Beenden : Destroy-Signal wurde empfangen.
main() wird nun beendet.
erik@coltrane Tag21]$

gibt die Funktion loeschen_funk() ihre Meldung (Zeile 31) aus, bevor gtk_main() zurückkehrt und die Meldung aus Zeile 25 ausgegeben wird.

Schaltflächen

Das GTK+-Programm aus Listing 21.1 demonstrierte zwar einige grundlegende Konzepte der GUI-Programmierung, tat sonst aber nichts. Das nächste Programm sollte da schon etwas interessanter sein und möglichst wie das Fenster mit dem Titel list2102.c aus Abbildung 21.1 aussehen.

Listing 21.2: Ein einfaches GTK+-Programm.

1 : /* list2102.c - Ein GTK+-Programm mit zwei Schaltflächen. */
2 :
3 : #include <gtk/gtk.h>
4 :
5 : void schalter_funk(GtkWidget *widget, gpointer daten);
6 : void loeschen_funk(GtkWidget *widget, gpointer daten);
7 :
8 : int main(int argc, char *argv[])
9 : {
10: GtkWidget *fenster;
11: GtkWidget *schalter;
12: GtkWidget *vbox;
13: int bdaten1, bdaten2;
14:
15: gtk_init(&argc, &argv);
16:
17: /* Das Hauptfenster erzeugen. */
18: fenster = gtk_window_new(GTK_WINDOW_TOPLEVEL);
19: gtk_widget_set_usize(GTK_WIDGET(fenster), 180, 120);
20: gtk_window_set_title(GTK_WINDOW(fenster), __FILE__);
21:
22: gtk_signal_connect(GTK_OBJECT(fenster), "destroy",
23: GTK_SIGNAL_FUNC(loeschen_funk), NULL);
24: gtk_container_set_border_width(GTK_CONTAINER(fenster), 20);
25:
26: vbox = gtk_vbox_new(TRUE, 0);
27: gtk_container_add(GTK_CONTAINER(fenster), vbox);
28:
29: /* Ersten Schalter einrichten. */
30: bdaten1 = 0;
31: schalter = gtk_button_new_with_label("Schalter1");
32: gtk_signal_connect(GTK_OBJECT(schalter), "clicked",
33: GTK_SIGNAL_FUNC(schalter_funk), (gpointer)&bdaten1);
34: gtk_box_pack_start(GTK_BOX(vbox), schalter, TRUE, FALSE, 0);
35: gtk_widget_show(schalter);
36:
37: /* Zweiten Schalter einrichten. */
38: bdaten2 = 1;
39: schalter = gtk_button_new_with_label("Schalter2");
40: gtk_signal_connect(GTK_OBJECT(schalter), "clicked",
41: GTK_SIGNAL_FUNC(schalter_funk), (gpointer)&bdaten2);
42: gtk_box_pack_start(GTK_BOX(vbox), schalter, TRUE, FALSE, 0);
43: gtk_widget_show(schalter);
44:
45: /* Alles sichtbar machen. */
46: gtk_widget_show(vbox);
47: gtk_widget_show(fenster);
48:
49: gtk_main();
50:
51: return 0;
52: }
53:
54: void schalter_funk(GtkWidget *widget, gpointer zdaten)
55: {
56: static int count [2] = { 0, 0 };
57: int index ;
58:
59: index = *((int*)zdaten);
60: count [index]++;
61: g_print("Schalter %d wurde zum %d-ten Mal angeklickt.\n",
index+1,count [index]);
62: }
63:
64: void loeschen_funk(GtkWidget *widget, gpointer zdaten)
65: {
66: g_print("Beenden : Destroy-Signal wurde empfangen.\n");
67: gtk_main_quit();
68: }

Ebenso wie Listing 21.1 wird in diesem Programm als einzige Header-Datei gtk/ gtk.h eingebunden. In den Zeilen 5 und 6 werden die Prototypen für die beiden Callback-Funktionen schalter_funk() und loeschen_funk() deklariert. Letztere Funktion ist identisch mit der gleichnamigen Funktion aus dem vorhergehenden Programm. Die main()-Funktion beginnt mit der Definition einer Reihe von lokalen Variablen (Zeilen 10 bis 13). Die ersten drei, fenster, schalter und vbox, sind vom Typ GtkWidget und werden bei der Initialisierung der GUI-Elemente verwendet. Die anderen beiden Variablen, bdaten1 und bdaten2, sind vom Typ int.

Die Zeilen 15 bis 23 entsprechen den Zeilen 11 bis 18 aus Listing 21.1. In Zeile 24 stoßen wir auf eine neue Funktion: gtk_container_set_border_width(). Bei der GTK+-Programmierung bezeichnet man Fenster, die andere Fenster als Kindfenster enthalten, als Container. Die Funktion gtk_container_set_border_width() legt die Breite des Rahmens fest, wodurch verhindert wird, dass Kindfenster zu nahe an den Rand des übergeordneten Fensters gesetzt werden. Bei GTK_CONTAINER() handelt es sich um eines der Makros, die Sie schon aus dem vorangegangenen Beispiel kennen. Es führt eine gesicherte Typumwandlung von GtkWidget* nach GtkContainer* durch. In Zeile 26 wird durch Aufruf von gtk_vbox_new() ein neues Widget erzeugt und mit Hilfe von gtk_container_add() als Kindfenster in das Hauptfenster eingefügt. Bei dem Widget, das mit gtk_vbox_new() erzeugt wird, handelt es sich um ein rechteckiges Box-Widget mit vertikalem Layout. (Rechteckige Box-Widgets mit horizontalem Layout können mit gtk_hbox_new() erzeugt werden.)

Box-Widgets sind Container-Widgets, die alle anderen Arten von Widgets in sich aufnehmen können. Wenn die Größe eines Box-Widgets geändert wird, werden die Widgets in der Box alle entsprechend neu skaliert. In den Zeilen 34 und 42 können Sie sehen, wie Schaltflächen in das vertikale Box-Widget eingefügt werden. Wenn die Größe des Fensters später während der Ausführung des Programms verändert wird, werden diese Schaltflächen automatisch angepasst, damit das Layout des Fensters erhalten bleibt.

Der erste Schalter wird in Zeile 31 erzeugt. In Zeile 32 wird das »clicked«-Signal mit der Funktion schalter_funk() verbunden, so dass jedes Mal, wenn der Schalter angeklickt wird, die Funktion schalter_funk() aufgerufen wird. Das letzte Argument zu gtk_signal_connect() ist ein Zeiger auf einen Integer, der in Zeile 30 auf Null gesetzt wurde. Bei der Einrichtung des zweiten Schalters (Zeilen 38 bis 43) wird ebenfalls schalter_funk() als Callback-Funktion verwendet, aber beim Aufruf von gtk_signal_connect() wird ein anderer Zeiger auf einen anderen Integer-Wert übergeben. Dieser Zeiger wird der Funktion schalter_funk() später bei Ausführung des Programms jedes Mal, wenn der Schalter angeklickt wird, als zweites Argument übergeben. Auf diese Weise kann die Callback-Funktion durch Prüfen des Wertes, auf den der übergebene Zeiger verweist, in Erfahrung bringen, welcher Schalter angeklickt wurde.

Nachdem die Callback-Funktion für den ersten Schalter eingerichtet ist, wird der Schalter mit Hilfe der Funktion gtk_box_pack_start() in die vertikale Box eingefügt (Zeile 34). Die Funktion übernimmt einen GtkBox* als erstes Argument und das aufzunehmende Widget als zweites Argument. Der dritte und der vierte Parameter steuern die Art und Weise, in der das Kind-Widget den Raum der Box ausfüllt, und können einen der Werte TRUE und FALSE annehmen. Der letzte Parameter zu gtk_box_pack_start() legt den Füllmechanismus fest und wird in unserem Beispiel auf 0 gesetzt. Experimentieren Sie ruhig mit diesen drei Parametern und weisen Sie ihnen verschiedene Wertekombinationen zu.

Der letzte Schritt vor dem Aufruf von gtk_main() besteht wiederum darin, alle Widgets sichtbar zu machen. Dies geschieht für die Schalter in den Zeilen 35 und 43, für die vertikale Box in Zeile 46 und für das Hauptfenster in Zeile 47.

In den Zeilen 54 bis 62 folgt die Callback-Funktion schalter_funk(). Sie definiert zuerst ein statisches Array mit zwei Integer-Werten, die beide mit 0 initialisiert werden. Wie Sie wissen, behalten lokale Variablen, die als statisch deklariert sind, zwischen den Funktionsaufrufen ihre Werte. Gehen wir noch einmal zurück zu den Zeilen 32 und 40, wo schalter_funk() als Callback-Funktion eingerichtet wurde. Das letzte Argument, das in diesen Zeilen an gtk_signal_connect() übergeben wurde, war ein Zeiger auf einen Integer-Wert: bdaten1 (mit dem Wert 0) für den ersten Schalter und bdaten2 (mit dem Wert 1) für den zweiten Schalter. Jedes Mal, wenn schalter_funk() als Antwort auf das Drücken eines der Schalter aufgerufen wird, wird der Funktion einer dieser Zeiger als zweites Argument übergeben. Wird Schalter 1 gedrückt, weist der Zeiger auf bdaten1, wird Schalter 2 gedrückt, weist der Zeiger auf bdaten2. Der Zeiger selbst ist ein generischer Zeiger - vergleichbar einem void-Zeiger -, der in Zeile 59 in einen Zeiger auf int zurückverwandelt und dereferenziert wird. Der Wert, auf den der Zeiger verweist, wird dabei der Variablen index zugewiesen. Anschließend wird index dazu verwendet, einen der beiden Werte des statischen Arrays count zu inkrementieren (Zeile 60). Zum Schluss wird eine Meldung in Zeile 61 ausgegeben.

Wenn Sie das Programm von einem Konsolenfenster aus aufrufen, erscheint bei jedem Klicken eines Schalters auf der Konsole eine Meldung aus Zeile 61. Der Meldung können Sie entnehmen, welcher Schalter gedrückt wurde und zum wie vielten Male dies geschah.

Dialogfenster erzeugen

Dialogfenster sind ein weit verbreitetes Element grafischer Benutzeroberflächen. Mit ihrer Hilfe kann ein GUI-Programm Benutzereingaben anfordern und entgegennehmen - beispielsweise eine Ja/Nein-Entscheidung oder den Namen einer Datei, die das Programm laden oder in der es Daten abspeichern soll. Im Gegensatz zum Hauptfenster des Programms, das während der gesamten Laufzeit des Programms angezeigt wird, werden Dialogfenster üblicherweise nur bei Bedarf erzeugt und danach direkt wieder aufgelöst. Das nachfolgende Programm beispielsweise erzeugt einen einfachen Ja/Nein-Dialog, prüft, ob der Anwender den Ja- oder den Nein-Schalter des Dialogs gedrückt hat, und reagiert dann entsprechend.

Wenn Sie in einem der beiden Beispielprogramme aus den Listings 21.1 und 21.2 auf die oben rechts gelegene Schließen-Schaltfläche des Window-Managers geklickt haben, wurde die Anwendung sofort beendet. Für Programme, in die der Anwender Daten eingibt, die nicht automatisch abgespeichert werden, ist dieses Verhalten nicht akzeptabel. Die meisten GUI-Programme rufen daher vor dem Beenden ein Dialogfenster auf, das dem Anwender Gelegenheit gibt, seine Daten zuvor noch zu speichern. Das kleine Beispielprogramm aus diesem Abschnitt verfügt allerdings über keine Daten, die man speichern könnte. Es zeigt daher einen einfachen Ja/Nein- Dialog an, der feststellt, ob der Anwender das Programm wirklich beenden will.

Das Programm besteht aus drei getrennten Quelltextdateien: dem Quelltext für das Hauptprogramm (Listing 21.3), dem Code für das Dialogfenster, der in einer Datei namens janein.c abgespeichert ist (Listing 21.4), und einer Header-Datei janein.h (Listing 21.5). Wie man Programme aus mehreren Quelltextdateien kompiliert, wurde am Tag 20 beschrieben. Mit Hilfe der folgenden Compiler-Befehle können Sie aus den drei Quelltextdateien ein ausführbares Programm erstellen:

gcc -Wall -ggdb `gtk-config  - cflags` -c list2103.c
gcc -Wall -ggdb `gtk-config - cflags` -c janein.c
gcc -Wall -ggdb `gtk-config - libs` list2103.o janein.o -o list2103

Unter Umständen lohnt es sich auch, eine Makefile-Datei für die Erstellung des Programms aufzusetzen (Makefile-Dateien wurden ebenfalls an Tag 20 besprochen).

Listing 21.3: Ein einfaches Programm mit einem Dialogfenster.

1 : /* list2103.c - Ein einfaches Programm */
2 : /* mit einem Dialogfenster. */
3 :
4 : #include <gtk/gtk.h>
5 : #include "janein.h"
6 :
7 : int loeschen_funk(GtkWidget *widget, gpointer daten);
8 : void schalter_funk(GtkWidget *widget, gpointer daten);
9 :
10: int main(int argc, char *argv[])
11: {
12: GtkWidget *fenster;
13: GtkWidget *schalter;
14:
15: gtk_init(&argc, &argv);
16:
17: /* Das Hauptfenster erzeugen. */
18: fenster = gtk_window_new(GTK_WINDOW_TOPLEVEL);
19: gtk_widget_set_usize(GTK_WIDGET(fenster), 180, 120);
20: gtk_window_set_title(GTK_WINDOW(fenster), __FILE__);
21:
22: gtk_signal_connect(GTK_OBJECT(fenster), "delete_event",
23: GTK_SIGNAL_FUNC(loeschen_funk), NULL);
24: gtk_container_set_border_width(GTK_CONTAINER(fenster), 20);
25:
26: /* Schalter einrichten. */
27: schalter = gtk_button_new_with_label("Schalter");
28: gtk_signal_connect(GTK_OBJECT(schalter), "clicked",
29: GTK_SIGNAL_FUNC(schalter_funk), NULL);
30:
31: gtk_container_add(GTK_CONTAINER(fenster), schalter);
32:
33: /* Alles sichtbar machen. */
34: gtk_widget_show_all(fenster);
35:
36: gtk_main();
37:
38: return 0;
39: }
40:
41: int loeschen_funk(GtkWidget *widget, gpointer zdaten)
42: {
43: if (ja_nein_dialog("Beenden?", "Anwendung beenden?"))
44: {
45: gtk_main_quit();
46: return FALSE;
47: }
48: return TRUE;
49: }
50:
51: void schalter_funk(GtkWidget *widget, gpointer zdaten)
52: {
53: g_print("Klick\n");
54: }

Listing 21.4: Ein allgemeines Dialogfenster mit Ja/Nein-Schaltern.

1 : /* janein.c - ein allgemeiner Dialog. */
2 :
3 : #include <gtk/gtk.h>
4 : #include "janein.h"
5 :
6 : static void janein_funk(GtkWidget *widget, gchar *janein);
7 :
8 : /* Statische Variable für den Ja/Nein-Rückgabewert. */
9 : static gint janein;
10:
11: int ja_nein_dialog(gchar *titel, gchar *labeltext)
12: {
13: GtkWidget *dialog, *label, *jbutton, *nbutton;
14: GtkWidget *vbox, *hbox;
15:
16: /* Die erforderlichen Widgets erzeugen. */
17: dialog = gtk_window_new(GTK_WINDOW_TOPLEVEL);
18: label = gtk_label_new(labeltext);
19: jbutton = gtk_button_new_with_label(" Ja ");
20: nbutton = gtk_button_new_with_label(" Nein ");
21: vbox = gtk_vbox_new(FALSE, 0);
22: hbox = gtk_hbox_new(FALSE, 0);
23:
24: /* Widgets packen. */
25: gtk_box_pack_start(GTK_BOX(vbox), label, TRUE, FALSE, 0);
26: gtk_box_pack_start(GTK_BOX(hbox), jbutton, TRUE, FALSE, 0);
27: gtk_box_pack_start(GTK_BOX(hbox), nbutton, TRUE, FALSE, 0);
28: gtk_box_pack_start(GTK_BOX(vbox), hbox, TRUE, FALSE, 0);
29: gtk_container_add(GTK_CONTAINER(dialog), vbox);
30:
31: gtk_widget_set_usize(GTK_WIDGET(dialog), 140, 80);
32: gtk_window_set_title(GTK_WINDOW(dialog), titel);
33: gtk_window_set_modal(GTK_WINDOW(dialog), TRUE);
34:
35: /* Signale mit Routinen verbinden. */
36: gtk_signal_connect(GTK_OBJECT(jbutton), "clicked",
37: GTK_SIGNAL_FUNC(janein_funk), "ja");
38: gtk_signal_connect(GTK_OBJECT(nbutton), "clicked",
39: GTK_SIGNAL_FUNC(janein_funk), "nein");
40: gtk_signal_connect(GTK_OBJECT(dialog), "delete_event",
41: GTK_SIGNAL_FUNC(gtk_main_quit), dialog);
42:
43: /* Alles sichtbar machen. */
44: gtk_widget_show_all(dialog);
45:
46: /* gtk_main() des Dialogfensters starten. */
47: gtk_main();
48:
49: /* Dialogfenster auflösen. */
50: gtk_widget_destroy(dialog);
51:
52: return janein;
53: }
54:
55: static void janein_funk(GtkWidget *widget, gchar *janeinstr)
56: {
57: if (janeinstr [0] == 'j')
58: janein = TRUE;
59: else
60: janein = FALSE;
61:
62: gtk_main_quit();
63: }

Listing 21.5: Ein allgemeiner Ja/Nein-Dialog.

1 : /* janein.h - Header-Datei für janein.c. */
2 :
3 : /* Erzeuge Dialogfenster mit zwei Schaltern "Ja" */
4 : /* und "Nein". */
5 : /* Wenn der Anwender auf "Ja" klickt, liefere TRUE. */
6 : /* Wenn der Anwender auf "Nein" klickt, liefere FALSE. */
7 :
8 : int ja_nein_dialog (gchar *titel, gchar *labeltext);

Listing 21.3 ist Listing 21.2 sehr ähnlich, enthält aber nur einen Schalter (in Listing 21.2 waren es zwei). Der Schalter wird in Zeile 27 erzeugt. In Zeile 28 wird das »clicked«-Ereignis des Schalters mit einer Callback-Funktion verbunden. Diese Callback-Funktion ist in den Zeilen 51 bis 54 definiert und macht nichts weiter, als bei jedem Drücken des Schalters eine »Klick«-Meldung auszugeben.

Einer der wichtigsten Unterschiede zwischen Listing 21.3 und den vorangegangenen Listings begegnet uns gleich in Zeile 22, wo die Funktion gtk_signal_connect() aufgerufen wird. Während die Funktion in den vorangegangenen Listings stets zur Einrichtung einer Behandlungsroutine für das destroy-Signal aufgerufen wurde, wird in Zeile 22 eine Verknüpfung mit dem delete_event-Signal hergestellt. Welcher Unterschied besteht zwischen diesen beiden Signalen? Wenn der Anwender auf die Schließen-Schaltfläche in der Titelleiste des Fensters klickt, sendet der Window Manager zwei Signale an das Programm. Das erste Signal ist das delete_event-Signal. Wenn für dieses Signal keine Callback-Funktion definiert wurde oder die betreffende Callback-Funktion einen Wert zurückliefert, der dem Booleschen Wert falsch entspricht, sendet der Window Manager ein zweites Signal - das destroy-Signal, das bewirkt, dass alle Fenster geschlossen werden. Wenn die Callback-Funktion des delete_event-Signal dagegen einen Wert zurückliefert, der dem booleschen Wert wahr entspricht, wird das destroy-Signal nicht gesendet. Die Callback-Funktion, die mit dem delete_event-Signal verbunden wird, ist in den Zeilen 41 bis 49 definiert. In der Callback-Funktion wird die ja_nein_dialog()-Funktion aufgerufen, die in der Header- Datei janein.h, die in Zeile 5 eingebunden wurde, definiert ist. Wenn Sie das Programm später ausführen, werden Sie sehen, dass der Aufruf der ja_nein_dialog()-Funktion ein kleines Fenster mit dem Titel »Beenden?« auf dem Bildschirm anzeigt. In diesem Fenster befinden sich zwei Schalter, Ja und Nein, und darüber der Text »Anwendung beenden?«. Die Funktion ja_nein_dialog()-Funktion liefert den Wert TRUE (wahr) zurück, wenn der Ja-Schalter gedrückt wurde, und den Wert FALSE (unwahr), wenn der Nein-Schalter gedrückt wurde. Im ersten Falle, also wenn der Wert TRUE zurückgeliefert wird, ruft die Callback-Funktion loeschen_funk() die Funktion gtk_main_quit() auf, damit gtk_main() beendet wird, und liefert danach selbst FALSE zurück.

Ein weiterer interessanter Unterschied zwischen Listing 21.3 und Listing 21.2 ist der Aufruf von gtk_widget_show_all() in Zeile 34. Diese Funktion macht nicht nur das Widget sichtbar, das ihr als Argument übergeben wurde - in unserem Falle fenster - , sondern auch alle Widgets, die in fenster enthalten sind. Der Aufruf ist also eine Kurzform, die es uns erspart, gtk_widget_show() für fenster and alle Kind-Widgets einzeln aufrufen zu müssen (wie wir es in Listing 21.2 getan haben).

Listing 21.4 enthält den Code für die Funktion ja_nein_dialog(). In den Zeilen 3 und 4 werden die Header-Datei gtk/gtk.h und die eigene Header-Datei eingebunden. In Zeile 6 wird der Prototyp der Funktion janein_funk() deklariert, die als Callback-Funktion für die beiden Schalter des Dialogs dienen soll. Damit diese Funktion die Information darüber, welcher Schalter gedrückt wurde, an die Funktion ja_nein_dialog() weiterreichen kann, wird in Zeile 9 die statische janein-Variable definiert. Da die Variable global deklariert wird, kann sie nach der Deklaration überall in der Datei verwendet werden, da sie gleichzeitig mit dem Schlüsselwort static deklariert wurde, ist sie vor Zugriffen aus anderen Quelltextdateien geschützt.

Die Funktion ja_nein_dialog()ist in den Zeilen 11 bis 53 deklariert. Sie gleicht in vielerlei Hinsicht der main()-Funktion der Listings 21.1 und 21.2. In den Zeilen 13 und 14 wird eine Reihe von Variablen des Typs GtkWidget* deklariert. Neu ist das GtkLabel-Widget, das durch Aufruf der Funktion gtk_label_new() erzeugt wird. GtkLabel-Widgets reagieren weder auf Mausklicks, noch kann der Anwender den Text des Widgets über die Tastatur ändern. Es sind einfache Bausteine, in denen man Texte anzeigen kann.

Die Widgets für den Dialog werden in den Zeilen 19 bis 22 erzeugt und anschließend in Boxen gepackt (Zeilen 25 bis 29). Größe und Titel des Dialogfensters werden in den Zeilen 31 und 32 festgelegt. Der Aufruf von gtk_window_set_modal() in Zeile 33 macht aus dem Hauptfenster des Dialogs ein modales Fenster. Ein modales Fenster ist ein Fenster, das die Ausführung seines Elternfensters so lange anhält, bis es selbst beendet wird. Im Falle unseres Ja/Nein-Dialogs wollen wir nicht, dass der Anwender mit dem Hauptfenster weiterarbeitet, bevor er nicht den Dialog beantwortet hat. Dialogfenster werden häufig als modale Fenster aufgerufen.

Das Dialogfenster kann auf drei Signale reagieren. Die Callback-Funktionen für die Signale werden mit Hilfe der Funktion gtk_signal_connect() in den Zeilen 36 bis 41 eingerichtet. Die ersten beiden Signale sind die clicked-Ereignisse, die ausgelöst werden, wenn der Anwender den Ja- beziehungsweise den Nein-Schalter drückt. Beide Signale werden zwar mit der gleichen Callback-Funktion - janein_funk() - verbunden, erhalten aber je nach gedrücktem Schalter unterschiedliche Argumente. Wenn der Ja-Schalter gedrückt wird, ist das zweite Argument zu janein_funk() ein Zeiger auf den String "ja"; wenn der Nein-Schalter gedrückt wird, ist das zweite Argument ein Zeiger auf den String "nein". Das dritte Signal, für das eine Callback- Funktion eingerichtet wird, ist das destroy-Signal des Dialogfensters. Als Callback- Funktion zu diesem Signal geben wir gtk_main_quit() an - warum, wird im Laufe der Analyse noch klar werden.

Nachdem die Signalbehandlung eingerichtet ist, werden die verschiedenen Widgets durch einen Aufruf von gtk_widget_show_all() sichtbar gemacht (Zeile 44). Schließlich wird in Zeile 47 die gtk_main()-Funktion aufgerufen. Beachten Sie, dass die ja_nein_dialog()-Funktion, die wir hier analysieren, von einer Callback-Funktion des Hauptprogramms aus Listing 21.3 aufgerufen wird. Bevor ja_nein_dialog() aufgerufen werden kann, muss also bereits der Aufruf von gtk_main() in der main()- Funktion von Listing 21.3 erfolgt sein. Die Funktion gtk_main() wird also rekursiv aufgerufen - was erlaubt ist, aber mit Vorsicht gehandhabt werden sollte.

Die Funktion janein_funk() dient als Callback-Funktion für die Ja- und Nein-Schalter des Dialogs. In den Zeilen 36 bis 39, wo die clicked-Signale mit der Funktion verbunden wurden, haben wir der Funktion gtk_signal_connect() als letztes Argument jeweils ein Zeiger auf einen der Strings "ja" (für den Ja-Schalter) beziehungsweise "nein" (für den Nein-Schalter) übergeben. Wird im laufenden Programm einer dieser Schalter gedrückt, zeigt der zweite Parameter der Funktion janein_funk() - der Zeiger janeinstr - danach auf den String, der den gedrückten Schalter identifiziert. Die if-Anweisung in Zeile 57 prüft danach, ob das erste Zeichen in dem String ein »j« ist. Wenn ja, wird die statische Variable janein auf TRUE gesetzt, sonst wird janein auf FALSE gesetzt. Wurde einer der Schalter gedrückt, hat das Dialogfenster seinen Zweck erfüllt und kann aufgelöst werden. Zu diesem Zweck wird in Zeile 62 die Funktion gtk_main_quit() aufgerufen, die bewirkt, dass die gtk_main()-Funktion aus Zeile 47 zurückkehrt. Nach dem Aufruf von gtk_main_quit() liefert die ja_nein_dialog()-Funktion den Wert der Variablen janein zurück (Zeile 52).

Beachten Sie, dass der Aufruf von gtk_main_quit() in Zeile 62 nur die zuletzt aufgerufene gtk_main()-Funktion beendet. Die gtk_main()-Funktion in Listing 21.3 wird davon nicht betroffen - es sei denn mittelbar, wenn der Anwender den Ja- Schalter drückt. Wird der Nein-Schalter gedrückt, wird nur die letzte gtk_main()- Funktion beendet, so dass das Dialogfenster zwar aufgelöst wird, das Hauptfenster aber wie gewünscht erhalten bleibt.

Die Funktion ja_nein_dialog() wurde so allgemein wie möglich gehalten. Die Frage, die der Anwender durch Drücken des Ja- beziehungsweise Nein-Schalters beantworten soll, wird als String-Argument an die Funktion übergeben, so dass der Dialog ohne Mühe angepasst werden kann. Ein Programm, das vom Anwender an irgendeiner Stelle eine Ja/Nein-Entscheidung verlangt, braucht die Funktion nur mit den passenden Argumenten aufzurufen. Auf diese Weise kann die Funktion in vielen unterschiedlichen Programmen wieder verwendet werden, und es müssen nicht jedes Mal spezielle Adaptionen der Funktion implementiert werden. In diesem Sinne ist die Funktion ein gutes Beispiel für den sinnvollen Einsatz der modularen Programmiertechniken.

Ein einfacher Texteditor

Die Beispiele, mit denen wir es bisher in dieser Lektion zu tun hatten, waren zwar interessant, aber nicht besonders nützlich. Das nächste Programm ist da schon komplexer; es enthält eine Menüleiste und verwendet ein Dialogfenster, in dem der Anwender eine Datei zum Speichern von Daten auswählen kann. Der Code des Programms ist auf die folgenden drei Listings (21.6 bis 21.8) verteilt. Listing 21.6 enthält die main()-Funktion und den Code, der zum Einrichten des Hauptfensters benötigt wird. Listing 21.7 ist die Header-Datei für das eigenständige Dialogfenster zur Dateiauswahl, das in Listing 21.8 definiert ist. Der Code für das Dialogfenster zur Dateiauswahl wurde in eine eigene Datei ausgelagert, damit das Dialogfenster in anderen Projekten, die Bedarf an einem Dialog zur Auswahl einer Datei haben, wieder verwertet werden kann.

Wie die Überschrift zu diesem Abschnitt bereits vermuten lässt, handelt es sich bei dem Programm um einen einfachen Texteditor. Der Texteditor ist nicht voll funktionsfähig, unterstützt aber das Ausschneiden und Einfügen von Text sowie das Abspeichern des eingegebenen Textes in einer Datei. Dass man all dies mit weniger als 200 Zeilen Code erreichen kann, liegt daran, dass man dabei auf die Implementierung eines noch grundlegenderen Texteditor-Widgets zurückgreifen kann, das von den Autoren des GTK+ implementiert wurde. Was noch fehlt, sind Optionen wie das Entgegennehmen eines Dateinamens über die Befehlszeilenargumente, die Implementierung eines »Datei öffnen«-Dialogs sowie weiter fortgeschrittene Befehle, wie zum Beispiel das Suchen und Ersetzen von Text.

Zur Kompilierung des Texteditor-Programms können Sie die folgenden Befehle verwenden:

gcc -Wall -ggdb `gtk-config  - cflags` -c list2106.c
gcc -Wall -ggdb `gtk-config - cflags` -c dateiname.c
gcc -Wall -ggdb `gtk-config - libs` list2106.o dateiname.o -o list2106

Listing 21.6: Ein einfacher GTK+-Texteditor.

1 : /* list2106.c - Ein einfacher (unvollständiger) GTK+-Texteditor. */
2 : #include <gtk/gtk.h>
3 : #include <stdio.h>
4 : #include "dateiname.h"
5 :
6 : void loeschen_funk(GtkWidget *widget, gpointer daten);
7 : void menue_funk(GtkWidget *widget, guint zahl);
8 : GtkWidget *menue_erzeugen(GtkWidget *fenster);
9 : void dat_save_dlg(GtkWidget *widget, gpointer zdaten);
10: int save_datei(char *dateiname);
11:
12: static GtkWidget *textbox ;
13:
14: int main(int argc, char *argv[])
15: {
16: GtkWidget *fenster, *menueleiste, *vbildlauf;
17: GtkWidget *vbox, *hbox;
18: GdkFont *textfont ;
19: GtkStyle *stil;
20:
21: gtk_init(&argc, &argv);
22:
23: /* Das Hauptfenster erzeugen. */
24: fenster = gtk_window_new(GTK_WINDOW_TOPLEVEL);
25: gtk_widget_set_usize(GTK_WIDGET(fenster), 400, 300);
26: gtk_window_set_title(GTK_WINDOW(fenster), __FILE__);
27:
28: gtk_signal_connect(GTK_OBJECT(fenster), "destroy",
29: GTK_SIGNAL_FUNC(loeschen_funk), NULL);
30:
31: vbox = gtk_vbox_new(FALSE, 0);
32: hbox = gtk_hbox_new(FALSE, 0);
33: gtk_container_add(GTK_CONTAINER(fenster), vbox);
34:
35: /* Menüleiste erzeugen. */
36: menueleiste = menue_erzeugen(fenster);
37: gtk_box_pack_start(GTK_BOX(vbox), menueleiste, FALSE, TRUE, 0);
38: gtk_widget_show(menueleiste);
39:
40: /* Textfeld erzeugen. */
41: textbox = gtk_text_new(NULL, NULL);
42: vbildlauf = gtk_vscrollbar_new(GTK_TEXT(textbox)->vadj);
43:
44: gtk_text_set_editable(GTK_TEXT(textbox), TRUE);
45: gtk_text_set_line_wrap(GTK_TEXT(textbox), FALSE);
46:
47: if ((textfont = gdk_font_load("7x13")) != NULL)
48: {
49: stil = gtk_widget_get_style(textbox);
50: stil->font = textfont;
51: gtk_widget_set_style(textbox, stil);
52: }
53:
54: /* Widgets packen. */
55: gtk_box_pack_start(GTK_BOX(hbox), textbox, TRUE, TRUE, 0);
56: gtk_box_pack_start(GTK_BOX(hbox), vbildlauf, FALSE, FALSE, 1);
57: gtk_box_pack_start(GTK_BOX(vbox), hbox, TRUE, TRUE, 0);
58:
59: /* Alles sichtbar machen. */
60: gtk_widget_show_all(fenster);
61:
62: gtk_main();
63:
64: return 0;
65: }
66:
67: void loeschen_funk(GtkWidget *widget, gpointer zdaten)
68: {
69: gtk_main_quit();
70: }
71:
72: /* Aufbau der Menüleiste. */
73:
74: static GtkItemFactoryEntry menu_array[] =
75: {
76: { "/_Datei" , NULL , NULL , 0, "<Branch>" },
77: { "/Datei/_Öffnen", "<control>O", menue_funk , 0, NULL },
78: { "/Datei/_Speichern", "<control>S", dat_save_dlg, 0, NULL },
79: { "/Datei/sep1" , NULL , NULL , 0, "<Separator>" },
80: { "/Datei/Beenden" , "<control>B", gtk_main_quit, 0, NULL },
81:
82: { "/_Zwei" , NULL, NULL , 0, "<Branch>" },
83: { "/Zwei/_Eins", NULL, menue_funk, 1, NULL },
84: { "/Zwei/_Zwei", NULL, menue_funk, 2, NULL },
85:
86: { "/_Menue3" , NULL, NULL, 0, "<Branch>" },
87: { "/Menue3/Test1", NULL, NULL, 0, "<ToggleItem>" },
88: { "/Menue3/sep1" , NULL, NULL, 0, "<Separator>" },
89: { "/Menue3/Test2", NULL, NULL, 0, "<RadioItem>" },
90: { "/Menue3/Test3", NULL, NULL, 0, "/Menue3/Test2" },
91: { "/Menue3/Test4", NULL, NULL, 0, "/Menue3/Test2" },
92:
93: { "/_Hilfe" , NULL, NULL, 0, "<LastBranch>" },
94: { "/_Hilfe/Info", NULL, NULL, 0, NULL },
95: };
96:
97: GtkWidget *menue_erzeugen(GtkWidget *fenster)
98: {
99: GtkWidget *menueleiste;
100: GtkItemFactory *itemfact;
101: GtkAccelGroup *accelgroup;
102:
103: gint msize = sizeof(menu_array) / sizeof(menu_array[0]);
104:
105: accelgroup = gtk_accel_group_new();
106: gtk_accel_group_attach(accelgroup, GTK_OBJECT(fenster));
107:
108: itemfact = gtk_item_factory_new(GTK_TYPE_MENU_BAR, "<main>",
109: accelgroup);
110: gtk_item_factory_create_items(itemfact, msize, menu_array, NULL);
111:
112: menueleiste = gtk_item_factory_get_widget(itemfact, "<main>");
113:
114: return menueleiste;
115: }
116:
117: void menue_funk(GtkWidget *widget, guint zahl)
118: {
119: if (zahl)
120: g_print("Zahl : %d\n", zahl);
121: else
122: g_print("Hallo!\n");
123: }
124:
125: /* Callback-Funktionen zu Datei_Speichern-Dialog. */
126: void dat_save_dlg(GtkWidget *widget, gpointer zdaten)
127: {
128: gchar dateiname [512];
129:
130: if (dateiname_dialog("Speichern unter:", dateiname, 512))
131: save_datei(dateiname);
132: }
133:
134: int save_datei(char *dateiname)
135: {
136: FILE *datei;
137: int laenge;
138: char *czgr;
139:
140: if ((datei = fopen(dateiname, "w")) == NULL)
141: return 1 ;
142:
143: laenge = gtk_text_get_length(GTK_TEXT(textbox));
144: czgr = gtk_editable_get_chars(GTK_EDITABLE(textbox),0,laenge);
145:
146: if (czgr)
147: fwrite(czgr, laenge, 1, datei);
148:
149: fclose(datei);
150: return 0 ;
151: }

Listing 21.7: dateiname.h: die Header-Datei für das Dateiauswahl-Dialogfenster aus Listing 21.8.

/* dateiname.h - Header-Datei für ein eigenständiges            */
/* Dialgofenster zur Dateiauswahl. */

/* Der Titel-Parameter setzt den Titel des Dialogfensters. */
/* Der vom Anwender ausgewählte Dateiname wird im dateiname- */
/* Parameter abgespeichert. Das für diesen Parameter übergebene */
/* String-Argument muss mind. l Zeichen lang sein. */

/* Bei Erfolg wird TRUE zurückgeliefert, sonst (wenn */
/* der Anwender auf Abbrechen drückt) FALSE. */

int dateiname_dialog(gchar *titel, gchar *dateiname, gint l);

Der Hauptteil des Programms steht in Listing 21.6 und beginnt mit dem Einbinden des Standard-Headers gtk/gtk.h, der Header-Datei stdio.h, die für die Dateifunktionen zum Öffnen, Schreiben und Schließen benötigt wird, und der Datei dateiname.h, die den Prototyp für die Funktion des Dateiauswahl-Dialogs enthält. Danach werden in den Zeilen 6 bis 12 fünf Funktionsprototypen und eine statische Variable vom Typ GtkWidget* (Zeile 12) deklariert. Letztere repräsentiert das GTK+- Widget, das uns die Textbearbeitungsfunktionen zur Verfügung stellt. Der Grund für die globale Deklaration der Variablen wird später noch deutlich werden.

Die main()-Funktion, die in Zeile 14 beginnt, deklariert zuerst alle benötigten Variablen (Zeilen 16 bis 19) und führt dann bis Zeile 35 in etwa die gleichen Operationen durch, die Sie aus den vorangehenden Beispielen kennen. Die Zeilen 36 bis 38 erzeugen die Menüleiste der Anwendung. Die meiste Arbeit leistet dabei die Funktion menue_erzeugen(), die in den Zeilen 79 bis 114 definiert ist.

Der Fensterbereich für die Texteingabe wird in den Zeilen 41 bis 52 eingerichtet. Dabei können wir wie gesagt auf ein vordefiniertes und leicht einzusetzendes Texteditor-Widget zurückgreifen, das uns die Arbeit wesentlich vereinfacht. Das Widget wird in Zeile 41 durch einen Aufruf der Funktion gtk_text_new() erzeugt. Die zugehörige vertikale Bildlaufleiste wird mit Hilfe der Funktion gtk_vscrollbar_new() erzeugt. Interessant ist vor allem das Argument, das dieser Funktion übergeben wird: GTK_TEXT(textbox)->vadj. Der GtkWidget*-Zeiger textbox wird mit Hilfe des GTK_TEXT()-Makros in einen GtkText-Zeiger umgewandelt. Der GtkText-Zeiger - ein Zeiger auf eine GtkText-Struktur - wird dereferenziert und es wird auf das vadj- Element der GtkText-Struktur zugegriffen. Wenn Sie einen Blick in die Header-Datei gtktext.h werfen (die Header-Datei ist höchstwahrscheinlich im Verzeichnis /usr/ include/gtk/gtktext.h zu finden), können Sie sehen, dass es sich bei vadj um einen GtkAdjustment*-Zeiger handelt. Wenn Sie des Weiteren im GTK-Abschnitt der Infopages die Dokumentation zum GtkVScrollbar-Widget nachschlagen, finden Sie dort für die gtk_vscrollbar_new()-Funktion die folgende Deklaration:

GtkWidget *gtk_vscrollbar_new (GtkAdjustment* adj);

Die Übergabe eines GtkAdjustment-Zeigers an gtk_vscrollbar_new() war also genau richtig.

In den Zeilen 44 und 45 wird das Standardverhalten des GtkText-Widgets angepasst. Wenn ein GtkText-Widget erzeugt wird, startet es in einem Modus, in dem der Programmierer Text in dem Widget anzeigen, der Anwender diesen Text aber nicht über die Tastatur verändern kann. Dieses Verhalten kann durch einen Aufruf von gtk_text_set_editable() abgewandelt werden. Wenn Sie der Funktion TRUE als Argument übergeben, ist das Widget editierbar, wenn Sie als Argument FALSE übergeben, ist es schreibgeschützt. Analog wird in Zeile 45 durch einen Aufruf der Funktion gtk_text_set_line_wrap() der automatische Zeilenumbruch deaktiviert - eine Option, die nur von wenigen Textverarbeitungsprogrammen unterstützt wird.

Die Standardschriftart des GtkText-Widgets ist ein proportionaler Font, was bedeutet, dass der Buchstabe i in der Breite weit weniger Platz einnimmt als der Buchstabe w. Eine solche Schriftart eignet sich für normale Texteditoren, nicht aber für Editoren zur Bearbeitung von Programmquelltexten. Aus diesem Grund wird in den Zeilen 47 bis 52 ein nichtproportionaler Font für das GtkText-Widget ausgewählt. Nichtproportionale Fonts verwenden für alle Zeichen die gleiche Zeichenbreite - unabhängig davon, ob es sich um ein i oder ein w handelt. Um den Font zu laden, ruft man die Funktion gdk_font_load() auf, der man den Namen des gewünschten Fonts als String übergibt. »7x13« ist der Name eines weit verbreiteten nichtproportionalen Fonts, der auf jeder Maschine verfügbar sein sollte. Wenn der Aufruf von gdk_font_load() erfolgreich war, textfont also kein NULL-Zeiger ist, wird in den Zeilen 49 bis 51 der Font des GtkText-Widgets ersetzt. Zu diesem Zweck benötigt man das GtkStyle-Widget, das Teil des GtkText-Widgets ist. Im GtkStyle-Widget werden bestimmte Eigenschaften des übergeordneten Widgets festgehalten, beispielsweise die Farbe oder eben die Schriftart. Mit Hilfe der Funktion gtk_widget_get_style() beschafft sich das Programm einen Zeiger auf die GtkStyle-Struktur des GtkText- Widgets (Zeile 49). Danach wird der neue Font im font-Element des GtkStyle-Widgets abgespeichert (Zeile 50) und die überarbeitete GtkStyle-Komponente mit Hilfe von gtk_widget_set_style() wieder in das GtkText-Widget geladen (Zeile 51).

Nachdem das GtkText-Widget ordnungsgemäß eingerichtet ist, werden die verschiedenen Widgets in die Boxen gepackt (Zeilen 55 bis 57). Der Aufruf der Funktion gtk_widget_show_all() in Zeile 60 sorgt dafür, dass alle Widgets im fenster sichtbar sind, und in Zeile 62 folgt der obligatorische gtk_main()-Aufruf.

Die Menüleiste des Programms wird in den Zeilen 74 bis 115 erzeugt. Es gibt zwei verschiedene Methoden, um mit GTK+ ein Menü aufzubauen. Die hier beschriebene Methode ist die einfachere. Der Code besteht aus einem statischen Array mit Elementen des Typs GtkItemFactoryEntry, das in Zeile 74 definiert und in den Zeilen 76 bis 94 initialisiert wird. Der Typ GtkItemFactoryEntry ist in der Header-Datei /usr/ include/gtk/gtkitemfactory.h wie folgt definiert:

struct _GtkItemFactoryEntry
{
gchar *path;
gchar *accelerator;

GtkItemFactoryCallback callback;
guint callback_action;

/* Mögliche Werte:
* NULL -> "<Item>"
* "" -> "<Item>"
* "<Title>" -> erzeugt ein Titel-Element
* "<Item>" -> erzeugt ein einfaches Element
* "<CheckItem>" -> erzeugt ein markierbares Element
* "<ToggleItem>" -> erzeugt ein Schaltelement
* "<RadioItem>" -> erzeugt ein Optionselement
* <path> -> Pfad zu verknüpftem Optionselement
* "<Separator>" -> erzeugt eine Trennlinie
* "<Branch>" -> erzeugt ein Menü
* "<LastBranch>" -> erzeugt ein rechts ausgerichtetes Menü
*/
gchar *item_type;
};

Die Struktur enthält fünf Elemente: path, accelerator, callback, callback_action und item_type. Die Kommentare der GTK+-Autoren zeigen Ihnen an, welche Werte Sie dem item_type-Element zuweisen können. Bedeutung und Verwendung der restlichen Elemente der GtkItemFactoryEntry-Struktur sollten deutlich werden, wenn Sie die Initialisierung des Arrays menu_array in den Zeilen 76 bis 94 mit dem Verhalten der Menüleiste des Programms vergleichen. Das erste Strukturelement, path, gibt Name und Position des Menüelements an. Der erste Eintrag im Array (Zeile 76) definiert das Dateimenü, das in der Hierarchie der Menüelemente in der obersten Ebene steht (also direkt in der Menüleiste angezeigt wird). Im zweiten Eintrag wird path der Wert / Datei/_Öffnen zugewiesen, wodurch das Öffnen-Menüelement als untergeordnetes Element des Dateimenüs eingerichtet wird. Beachten Sie auch die Unterstriche in den path-Werten. Der Unterstrich zeigt an, dass das nachfolgende Zeichen im path-String als [Alt]-Tastenkombination zum Aufruf des Menübefehls verwendet werden kann. Sie können dies im laufenden Programm überprüfen. Wenn Sie die [Alt]-Taste gedrückt halten und dann die [F]-Taste drücken, sollte das Dateimenü aufgeklappt werden. Danach können Sie mit den Pfeiltasten zwischen den Befehlen im Menü hin- und herwechseln.

Das zweite Element in der GtkItemFactoryEntry-Struktur heißt accelerator. Sie können diesem Element einen optionalen String zuweisen, der ein Tastaturkürzel für den Aufruf des Menübefehls angibt. Für das /Datei/_Speichern-Element wurde als Tastaturkürzel beispielsweise <control>S angegeben, wodurch der GTK+-Bibliothek mitgeteilt wird, dass das Drücken der Tasten [Strg]+[S] genauso behandelt werden soll wie die Auswahl des Datei/Speichern-Befehls aus der Menüleiste. Wenn Sie dem accelerator-Element den Wert NULL zuweisen, wird kein Tastaturkürzel für das Menüelement eingerichtet.

Über das dritte und vierte Element der GtkItemFactoryEntry-Struktur, callback und callback_action kann man ein Menüelement mit einer Callback-Funktion verbinden. Der Name der Callback-Funktion wird in callback angegeben, callback_action kann man einen Integer-Wert zuweisen, der beim Aufruf der Callback-Funktion dieser als zweites Argument übergeben wird. Im Menü Zwei gibt es zwei Menüelemente die beide menue_funk() als Callback-Funktion angeben, aber jeweils unterschiedliche Zahlen als callback_action-Werte spezifizieren. Wenn Sie das Programm ausführen und die beiden Menüelemente nacheinander auswählen, können Sie sehen, wie die Callback-Funktion menue_funk() die zu den Menüelementen gehörenden Zahlenwerte ausgibt.

Das letzte Element der GtkItemFactoryEntry-Struktur ist item_type. Bis auf wenige Ausnahmen wurden alle möglichen Werte für dieses Strukturelement in unserem Beispielprogramm benutzt. Wann immer diesem Strukturelement der Wert NULL zugewiesen wird, wird aus dem betreffenden Menüelement ein Standardelement. Der Wert <Branch> gibt an, dass bei Auswahl des Menüelements ein untergeordnetes Menü aufspringen soll. Menüelemente, für die item_type der Wert <Separator> zugewiesen wurde, können nicht aufgerufen werden - sie dienen lediglich der Gruppierung der anderen Menüelemente (siehe Dateimenü, wo der Beenden-Befehl von den Menübefehlen zum Öffnen und Speichern abgesetzt wurde). Im Menü Menue3 werden noch weitere Werte für item_type verwendet, beispielsweise Schaltelemente und Optionen, aber ihre Einsatzmöglichkeiten werden nicht mehr vollständig aufgezeigt.

In den Zeilen 97 bis 115 ist die Funktion menue_erzeugen() definiert. Als Parameter übernimmt die Funktion das Fenster, dem das fertige Menü zugewiesen werden soll. Erzeugt wird das Menü auf der Basis der GtkItemFactoryEntry-Struktur menu_array. Zuvor werden noch die benötigten Variablen deklariert (Zeilen 99 bis 101), und mit Hilfe eines kleinen Tricks wird die Anzahl der Elemente in menu_array bestimmt. Die Funktion teil die Größe des gesamten Arrays durch die Größe des ersten Elements. Dies ist besser als die Verwendung einer Konstanten, da das Ergebnis immer korrekt ist (würde man eine Konstante verwenden, müsste man sie jedes Mal, wenn die Menüstruktur geändert wird, anpassen). Der berechnete Wert, msize, wird weiter unten in der Funktion benötigt.

Die Verwendung von Tastaturkürzeln für Menüs wurde bereits weiter oben beschrieben. Die Tastaturkürzel müssen vor dem Menü eingerichtet (Aufruf der Funktion gtk_accel_group_new() in Zeile 105) und mit dem Hauptfenster verbunden werden (Aufruf der Funktion gtk_accel_group_attach() in Zeile 106). Danach werden sie als Argument an die Funktion gtk_item_factory_new() übergeben, die einen Zeiger auf ein GtkItemFactory-Objekt zurückliefert. Letzteres wird zusammen mit menu_array (definiert in den Zeilen 74 bis 94) an die Funktion gtk_item_factory_create_items() übergeben, die das Objekt gemäß den Daten aus menu_array initialisiert. Damit das Hauptprogramm später bei Bedarf auf die Menüleiste zugreifen kann, wird durch Aufruf der Funktion gtk_item_factory_get_widget() in Zeile 112 ein GtkWidget*-Zeiger erzeugt, der dann in Zeile 114 an die aufrufende Funktion zurückgeliefert wird.

Die nächste Funktion, die in Listing 21.6 definiert wird, ist die Callback-Funktion menue_funk() (Zeilen 117 bis 123), die mit den Befehlen aus dem Menü Zwei verbunden ist. Der zweite Parameter dieser Funktion ist vom Typ guint - einem unsigned int-Typ, der in den GTK+-Header-Dateien definiert ist. Wenn diesem Parameter der Wert Null übergeben wird, gibt die Callback-Funktion einfach einen »Hallo!«-Gruß auf die Konsole aus. Ist der übergebene Wert ungleich Null wird der betreffende Wert ausgegeben.

Die Funktion dat_save_dlg() ist die Callback-Funktion, die aufgerufen wird, wenn der Anwender den Speichern-Befehl im Dateimenü auswählt. Die meiste Arbeit wird dabei an die Funktion dateiname_dialog() delegiert, die in der Header-Datei dateiname.h deklariert (Listing 21.7) und in dateiname.c definiert ist (Listing 21.8). Wenn dateiname_dialog() einen TRUE-Wert zurückliefert, sollte im String dateiname ein korrekter Dateiname stehen. Dieser wird dann an die Funktion save_datei() übergeben, die in den Zeilen 134 bis 151 definiert ist. Die Funktion save_datei() öffnet die Datei zum Lesen (Zeile 140) und fragt alle weiter benötigten Informationen von dem GtkText-Widget textbox ab, das zu Anfang der Datei als statische globale Variable definiert wurde. Die Funktion gtk_text_get_length() gibt die Länge des Textes zurück, der im GtkText-Widget angezeigt wird. Die Funktion gtk_editable_get_chars(), die danach aufgerufen wird, liefert einen char*-Zeiger auf ein Array zurück, in dem der gesamte Text - von dem angegebenen Startpunkt bis zum Endpunkt - enthalten ist. In unserem Fall lässt sich das Programm alle Zeichen (von 0 bis laenge) zurückliefern. Wenn der Aufruf von gtk_editable_get_chars() erfolgreich ist, gibt die Funktion einen Zeiger zurück, der ungleich NULL ist, und die Funktion fwrite() kann den kompletten Text in einem Rutsch in die Datei schreiben. Danach wird die Datei geschlossen und save_datei() kehrt zurück.

Listing 21.8: dateiname.c: ein Dialogfenster zur Dateiauswahl.

1 : /* dateiname.c - Dialogfenster zur Dateiauswahl. */
2 : #include <gtk/gtk.h>
3 : #include <string.h>
4 : #include "dateiname.h"
5 :
6 : typedef struct
7 : { GtkWidget *gewaehlt;
8 : gchar *dateiname;
9 : gint laenge ;
10: } FILEDATEN ;
11:
12: static void dateiname_ok(GtkWidget *widget, FILEDATEN *fdaten);
13: static void dateiname_cancel(GtkWidget *widget, GtkWidget *gewaehlt);
14:
15: int dateiname_dialog(gchar *title, gchar *dateiname, gint l)
16: {
17: GtkWidget * gewaehlt;
18: FILEDATEN fdaten;
19:
20: if (dateiname == NULL)
21: return FALSE;
22: dateiname[0] = 0;
23:
24: gewaehlt = gtk_file_selection_new("Speichern unter :");
25: gtk_window_set_modal(GTK_WINDOW(gewaehlt), TRUE);
26: gtk_widget_show(gewaehlt);
27:
28: fdaten.gewaehlt = gewaehlt;
29: fdaten.dateiname = dateiname;
30: fdaten.laenge = l;
31:
32: gtk_signal_connect(
33: GTK_OBJECT(GTK_FILE_SELECTION(gewaehlt)->ok_button),
34: "clicked", (GtkSignalFunc) dateiname_ok, &fdaten);
35: gtk_signal_connect(
36: GTK_OBJECT(GTK_FILE_SELECTION(gewaehlt)->cancel_button),
37: "clicked", (GtkSignalFunc) dateiname_cancel, gewaehlt);
38: gtk_signal_connect(GTK_OBJECT(gewaehlt), "destroy",
39: GTK_SIGNAL_FUNC(dateiname_cancel), gewaehlt);
40:
41: gtk_main();
42:
43: if (strlen (dateiname) > 0)
44: return TRUE;
45:
46: return FALSE;
47: }
48:
49: static
50: void dateiname_ok(GtkWidget *widget, FILEDATEN *fdaten)
51: {
52: gchar *ausgewaehlt;
53:
54: ausgewaehlt = gtk_file_selection_get_filename(
55: GTK_FILE_SELECTION(fdaten->gewaehlt));
56:
57: if (strlen (ausgewaehlt) < fdaten->laenge)
58: strcpy (fdaten->dateiname, ausgewaehlt);
59:
60: gtk_widget_destroy(fdaten->gewaehlt);
61: gtk_main_quit();
62: }
63:
64: static
65: void dateiname_cancel(GtkWidget *widget, GtkWidget * gewaehlt)
66: {
67: gtk_widget_destroy(gewaehlt);
68: gtk_main_quit();
69: }

Das Dialogfenster zur Dateiauswahl ist in Listing 21.8 definiert. Listing 21.8 bindet zuerst die erforderlichen Header-Dateien ein: gtk/gtk.h, string.h für die Funktionen strlen() and strcpy() sowie die eigene Header-Datei dateiname.h. In den Zeilen 6 bis 10 steht eine Struktur, die weiter unten benötigt wird. Die Zeilen 12 und 13 deklarieren die Prototypen zweier Funktionen, die später als Callback-Funktionen verwendet werden.

Die Hauptfunktion des Dialogs heißt dateiname_dialog() und beginnt in Zeile 15. Diese Funktion gleicht der Funktion für den Ja/Nein-Dialog aus Listing 21.4. Sie beginnt mit der Definition der benötigten Variablen (Zeilen 17 und 18). Danach prüft sie, ob dem dateiname-Parameter kein NULL-Zeiger übergeben wurde (Zeile 20), bevor sie dem ersten Zeichen im dateiname-String das Nullzeichen zuweist. Dies geschieht, damit der Dialog einen leeren String zurückliefert, wenn der Anwender den Abbrechen-Schalter drückt. Das Dateiauswahl-Widget wird in Zeile 24 durch Aufruf der Funktion gtk_file_selection_new() erzeugt, in Zeile 24 als modal deklariert und in Zeile 25 sichtbar gemacht.

In den Zeilen 28 bis 30 wird die Strukturvariable fdaten initialisiert. Danach enthält sie einen Zeiger auf das Dateiauswahl-Widget, einen char*-Zeiger, in den der Dateiname kopiert werden soll, und die Angabe der maximalen Länge für den Dateinamen. In Zeile 32 wird die Strukturvariable an die Funktion gtk_signal_connect() übergeben, die die Funktion dateiname_ok() als Callback-Funktion für das Klickereignis des OK- Schalters des Dateiauswahl-Dialogs einrichtet. Wenn die Callback-Funktion dateiname_ok() aufgerufen wird, übernimmt Sie einen Zeiger auf die fdaten-Struktur, über die sie den Dateinamen, den der Anwender ausgewählt hat, an die dateiname_dialog()-Funktion zurückgibt. Außer dem Klickereignis des OK-Schalters werden auch noch das Klickereignis des Abbrechen-Schalters und das destroy Ereignis für das Dateiauswahl-Widget abgefangen. Diese beiden Signale werden von der Funktion dateiname_cancel() behandelt.

Nachdem die Callback-Funktionen eingerichtet sind, wird die Funktion gtk_main() aufgerufen, die erst zurückkehrt, wenn einer der Dateiauswahl-Schalter oder der Schließen-Schalter aus der Titelleiste des Dateiauswahl-Widgets gedrückt wird. Wenn der Anwender einen Dateinamen auswählt und den OK-Schalter drückt, enthält der String dateiname den ausgewählten Dateinamen. Wenn der Anwender den Abbrechen-Schalter drückt, hat der String dateiname die Länge Null (vergleiche Zeile 22). Ist die Länge des Strings dateiname größer als Null, wird der Wert TRUE zurückgeliefert (Zeile 44), ansonsten der Wert FALSE (Zeile 46).

Die Callback-Funktion dateiname_ok(), die in den Zeilen 49 bis 62 definiert ist, wird aufgerufen, wenn der Anwender den OK-Schalter des Dateiauswahl-Widgets anklickt. Dabei wird ihr als zweites Argument ein Zeiger auf die fdaten-Struktur übergeben - so wie es in der dateiname_dialog()-Funktion vorgesehen ist. Wenn aufgerufen, versucht die Funktion, den Dateinamen zu ermitteln, der in dem Dateiauswahl-Widget ausgewählt wurde. Zu diesem Zweck ruft sie in Zeile 54 die Funktion gtk_file_selection_get_filename() auf. Wenn der ausgewählte Dateiname kürzer als die maximal erlaubte Dateinamenlänge ist, wird der Dateiname in den String kopiert, der im Aufruf von strcpy() in Zeile 58 übergeben wird. Der Dateiauswahl-Dialog hat damit seinen Zweck erfüllt und kann durch einen Aufruf von gtk_widget_destroy() geschlossen werden (Zeile 60). Die gtk_main()-Funktion aus der Funktion dateiname_dialog() wird durch den Aufruf von gtk_main_quit() in Zeile 61 beendet.

Die Callback-Funktion dateiname_cancel(), die in den Zeilen 49 bis 62 definiert ist, wird aufgerufen, wenn der Anwender den Abbrechen-Schalter oder den Schließen- Schalter aus der Titelleiste des Dateiauswahl-Widgets anklickt. Aufgabe dieser Callback- Funktion ist das Schließen des Fensters, wozu die Funktion gtk_widget_destroy() und danach gtk_main_quit() aufgerufen werden.

Zusammenfassung

In der heutigen Lektion drehte sich alles um die GUI-Programmierung mit der GTK+- Bibliothek. Sie haben ein wenig über die Geschichte von X und die hinter X stehenden Konzepte erfahren. Sie haben gelernt, was GUI-Ereignisse und GTK+- Signale sind. Sie haben eine Reihe der gebräuchlichsten GUI-Elemente, beispielsweise Schaltflächen, Menüleisten und Dialogfelder, kennen und verwenden gelernt. Sie haben gesehen, wie man eigenständige, wieder verwertbare Dialogfenster - wie zum Beispiel den Ja/Nein-Dialog oder den Dateiauswahl-Dialog - implementiert. Das letzte Programm der heutigen Lektion hat etliche dieser Konzepte und Ideen zu einem einfachen, wenn auch unvollständigen GUI-Texteditor gebündelt.

Leider konnten wir im Rahmen dieses Kapitels nur an die Oberfläche der GUI- Programmierung mit der GTK+-Widget-Bibliothek rühren - ein Thema zu dem man ohne Probleme ein ganzes Buch schreiben könnte. Bauen Sie auf den in der heutigen Lektion erworbenen GUI-Kenntnissen auf. Schauen Sie sich bestehende GTK+- Programme an, lesen Sie die GTK+-Infopages und arbeiten Sie sich weiter in diese elegante GUI-Programmierbibliothek ein.

Fragen und Antworten

Frage:
Warum wurden die Funktionen aus der heutigen Lektion nicht in der gleichen Ausführlichkeit erklärt wie die Funktionen der vorangegangenen Tage?

Antwort:
Dafür gibt es eine Reihe von Gründen. Zum einen der Platzmangel. In der heutigen Lektion wurden über 30 neue Funktionen angesprochen. Alle diese Funktionen mit Prototyp aufzuführen und ausführlich zu beschreiben, war einfach nicht möglich. Andererseits sind diese Funktionen in den Infopages, die mit der GTK+-Bibliothek ausgeliefert werden, recht gut erklärt. Hinzu kommt, dass dies die letzte Lektion dieses Buches ist, nach deren Lektüre Sie auf sich alleine gestellt sind. Sie müssen daher lernen, mit der Dokumentation zu arbeiten, die Ihnen zur Verfügung steht, selbst Code auszuprobieren und Probleme, die sich Ihnen bei der Programmierung stellen, eigenständig zu meistern.

Frage:
Handelt es sich bei den GTK+-Signalen um die gleichen Signale, die an Tag 19 beschrieben wurden?

Antwort:
Die Idee ist ähnlich, aber die Signale sind nicht die gleichen. Die Signale, die an Tag 19 besprochen wurden, waren überwiegend Signale, die vom Betriebssystem ausgelöst wurden. Die GTK+-Signale werden durch Aktionen des Anwenders ausgelöst, der mit den Elementen der grafischen Oberfläche arbeitet.

Workshop

Der Workshop enthält Quizfragen, die Ihnen helfen sollen, Ihr Wissen zu festigen, sowie Übungen, die Sie anregen sollen, das Gelernte umzusetzen und eigene Erfahrungen zu sammeln. Die Lösungen zu den Fragen und den Übungen finden Sie in Anhang C.

Quiz

  1. Wie teilt man dem gcc mit, wo die GTK+-Headerdateien zu finden sind?
  2. Wie teilt man dem gcc mit, wo die GTK+-Bibliotheken zu finden sind?
  3. Was ist eine Callback-Funktion?

Übungen

  1. Das Ja/Nein-Dialogfenster aus Listing 21.4 hat einen kleinen Makel. Wenn die Frage, die dem Parameter labeltext übergeben wird, zu lang ist, wird der Text der Frage am Rand des Dialogfensters abgeschnitten. Schreiben Sie die ja_nein_dialog()-Funktion so um, dass sie die Länge des labeltext-Parameters bestimmt und die Breite des Dialogfenster anpasst.
  2. Überarbeiten Sie die ja_nein_dialog()-Funktion aus Listing 21.4, so dass die Funktion nicht nur die Breite des Dialogfensters anpasst, sondern gegebenenfalls auch die Frage in mehrere Zeilen aufteilt.
123

vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbackKapitelanfangnächstes Kapitel


© Markt&Technik Verlag, ein Imprint der Pearson Education Deutschland GmbH