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:
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.
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+, 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.
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.
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.
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.
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 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.
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.
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.
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.
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.
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.
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.