15.4 Grundlagen der GTK+-Programmierung
Anwendungen mit einer grafischen Oberfläche (GUI) können sehr komplex werden. Daher werden Sie erst die grundlegenden Schritte kennen lernen, die nötig sind, um eigene GTK+-Anwendungen zu schreiben. Wenn Sie die grundlegenden Eigenschaften kennen und schätzen gelernt haben, wird es Ihnen nicht mehr sonderlich schwer fallen, komplexere GTK+-Programme zu schreiben, denn im Endeffekt wird immer nach demselben Schema verfahren. Folgende Schritte sind (fast) immer nötig, um eine GTK+-Anwendung zu erstellen:
|
die Umgebung initialisieren |
|
Widgets erzeugen und ggf. die Attribute setzen |
|
eine Callback-Funktion einrichten, um Events abzufangen |
|
die hierarchische Anordnung der Widgets definieren |
|
Widgets anzeigen |
|
Signale und Events abfangen und bearbeiten – (Events-)Verarbeitungsschleife |
Auf den folgenden Seiten soll nun auf die einzelnen Schritte etwas genauer eingegangen werden, ohne jetzt schon die einzelnen Widgets im Detail zu beschreiben.
15.4.1 Die Umgebung initialisieren
Der erste Schritt einer jeden GTK+-Anwendung ist es, die Umgebungsvariablen zu initialisieren. Dabei werden die Standardwerte für z. B. Farben vergeben. Die Funktion, mit der Sie dies realisieren, lautet:
#include <gtk/gtk.h>
void gtk_init(int *argc_ptr, char ***argv_ptr);
Bei argc_ptr handelt es sich um eine Adresse des ersten Parameters der Standard-C-Funktion main(), welche die Anzahl der Argumente aus der Kommandozeile beinhaltet. Der zweite Parameter argv_ptr ist dann eine Adresse auf den zweiten Parameter der main()-Funktion, und der beinhaltet die Werte der einzelnen Argumente aus der Kommandozeile. Mit gtk_init() wird die Verbindung zum X-Server aufgebaut, daher können Sie in der Kommandozeile mit der Option -display display_name die Anwendung auf einem anderen Display ausführen. Für die Syntax des Namens des Displays lesen Sie bitte die Dokumentation, da diese Angabe je nach System variieren kann. Des Weiteren wird mit der Funktion gtk_init() automatisch mit atexit() eine Clean-up-Funktion eingerichtet.
Hinweis Es können auch zwei X-Server lokal laufen, es muss nicht ein anderer Rechner sein.
|
15.4.2 Widgets erzeugen und ggf. die Attribute setzen
Widgets können mit den gtk_xxx_new()-Funktionen erzeugt werden – wobei xxx für den Typ des Widgets steht, das erzeugt werden soll. Es gibt eine enorme Anzahl von Widgets, die verständlicherweise nicht alle im Buch erwähnt werden können. Die Funktionen gtk_xxx_new() sind als gleichwertig zu einem Konstruktor in C++ oder Java anzusehen. Mit den Funktionen wird Speicher für ein neues Objekt alloziiert.
Noch umfangreicher ist die Anzahl der Routinen, mit denen Sie die Attribute der Widgets verändern können. Für einzelne Widgets gibt es häufig mehrere Funktionen. Die Funktionen, mit denen Sie die Attribute von Widgets verändern können, haben alle die Syntax gtk_xxx_set_yyy(). xxx steht dabei für das Widget und yyy für das Attribut, das verändert werden soll. Wenn Sie z. B. den Titel des Fensters setzen wollen, so geschieht dies mit dem Aufruf gtk_window_set_title( ... ).
Alle Widgets sind Unterklassen der GtkWidget-Basisklasse, weshalb Sie auch den Typ GtkWidget verwenden können, um ein Widget zu referenzieren; wobei im Beispiel des Buchs meistens immer der Typ des Widgets angegeben wird. Anstatt ein Fenster wie folgt zu referenzieren:
GtkWidget *win;
wird im Buch immer folgende Schreibkonvention verwendet:
GtkWindow *win;
Daran erkennt man gleich, worum es sich bei dem Widget handelt, und somit lässt sich auch schneller die entsprechende Referenzseite des Widgets finden. Was Sie später in der Praxis verwenden, bleibt natürlich Ihnen überlassen. (Verfallen Sie aber nicht der ungarischen Notation, wie z. B. Microsoft sie gerne verwendet, d. h. GtkWindow *wMain, *wPanel etc., sie ist viel zu fehleranfällig.) Wenn Sie zum Erzeugen von neuen Widgets den Konstruktor GtkWidget verwenden, benötigen einige Funktionen einen genaueren Typ als GtkWidget. In solch einem Fall gibt es spezielle Makros, die den Typen casten.
Die zweite Möglichkeit, ein Widget zu erzeugen, die auch in diesem Buch hier vorwiegend verwendet wird, besteht in der Funktion g_object_new().
GtkObject* g_object_new (
GtkType type, const gchar *first_arg_name, ... );
Hiermit erzeugen Sie ein Objekt (Widget) type mit den Eigenschaften, die Sie mit den weiteren Parametern (first_arg_name) angeben können. Abgeschlossen wird diese variabel lange Argumentenliste mit NULL. Zurückgegeben wird ein Zeiger auf das neue Objekt (Widget). Das Prinzip ist einfach und typisch objektorientiert. Sie wollen z. B. ein einfaches Fenster erzeugen:
GtkWindow *win;
...
win = g_object_new( GTK_TYPE_WINDOW, NULL );
Anstatt also den Funktionsaufruf wie gtk_xxx_new() zu verwenden, führen Sie hierbei g_object_new(GTK_TYPE_XXX, NULL) aus. Aber im Gegensatz zu gtk_xxx_new() können Sie beim Erzeugen eines neuen Objektes gleich weitere Eigenschaften des Objektes mit angeben. Um beim Beispiel des Fensters zu bleiben:
/* Fenster mit Eigenschaften anlegen */
win = g_object_new( GTK_TYPE_WINDOW,
"title", "Ein leeres Fenster",
"default-width", 300,
"default-height", 200,
"resizable", TRUE,
"window-position", GTK_WIN_POS_CENTER,
"border-width", 5,
"icon", pic,
NULL );
Mit den Funktionen gtk_xxx_set_yyy() müssten diese Angaben häufig mühevoll mit mehreren Funktionen übergeben werden. An den konstanten Namen des ersten Parameters zu kommen, sollte im Prinzip keine Probleme bereiten. Wollen Sie z. B. ein Objekt vom Typ GtkSpinButton erzeugen, lässt sich dies ganz einfach auf den ersten Parameter projizieren.
GtkSpinButton => GTK_TYPE_SPIN_BUTTON
Gtk wird zu Beginn des Widgets entfernt, und vor jeden Großbuchstaben des Widgets wird ein Unterstrichzeichen hingesetzt. Natürlich wird, wie bei Konstanten üblich, der Typ großgeschrieben (dies ist keine Regel, sondern nur ein Tipp). Aber Sie werden dies besser verstehen, wenn Sie die Beispiele in der Praxis einsetzen werden.
15.4.3 Eine Callback-Funktion einrichten, um Events abzufangen
Was Events sind und wie Sie darauf regieren können, haben Sie bereits im Kapitel zur X-Programmierung erfahren. Jetzt wollen Sie sicherlich wissen, wie eine solche Ereignisverarbeitung in GTK+ realisiert wird. In GTK+ wird hierbei zwischen Ereignissen (Events) und Signalen unterschieden; wobei diese Signale hier nicht mit den Linux-typischen Signalen zu vergleichen sind! Tritt z. B. ein Ereignis ein (wie ein Mausklick auf einen Button), wird programmintern ein Signal gesendet (in diesem Fall spricht man vom emittiert). Gewöhnlich führt allerdings jedes Ereignis zur Auslösung eines oder mehrerer Signale. Ereignissignale sind zum Beispiel an der Endung -event zu erkennen. Hier ist wieder derjenige von Vorteil (also Sie mit diesem Buch), der bereits Vorkenntnisse mit der X-Programmierung hat. Allerdings müssen Sie sich jetzt nicht mehr um die Implementierung kümmern, sondern vielmehr darum, wie Sie mit den Ereignissignalen arbeiten müssen. Auf die Events und deren Behandlung wird noch extra in einem Abschnitt eingegangen.
Hinweis Da mittlerweile die Version 2.x von GTK+ vorliegt, aber auf vielen Rechnern höchstwahrscheinlich zusätzlich noch die Version 1.2 von GTK+ installiert ist, gibt es hier einige Veränderungen. In der Version 1.2 wird zum Einrichten einer Callback-Funktion gtk_signal_connect() verwendet. In der Version 2.0. wurde diese Funktion in die Glib ausgelagert und hat somit jetzt kein gtk_ mehr voranstehen, sondern ein g_, also g_signal_connect().
|
Der Aufbau von Signalen entspricht einem typischen Klassenbaum (siehe Abb. 14.2), z. B. bei den Buttons. Ganz oben in der Klasse steht das Widget GtkObject mit dem Signal destroy – was die Beendigung bedeutet. Anschließend folgt im Hierarchiebaum GtkWidget, das weiterhin unabhängig vom eigentlichen Widget bestimmte Signale empfangen und verarbeiten kann. So ist die Größe, ob sichtbar oder versteckt, ein Signal, das alle GtkWidget besitzen und zurückgegeben werden kann. Ein Klasse tiefer finden Sie GtkContainer, das wiederum speziell für den Container eingehende Signale verarbeiten kann. Tiefer steigend befindet sich dann das Widget GtkBin, das keinerlei Signale empfangen kann. Jetzt erst kommen Sie im Klassenbaum zum Widget GtkButton. Dem Button können Sie die Signale clicked, enter, leave, pressed oder released erteilen – sprich, diese Signale können Sie für einen Button mittels g_signal_connect() einrichten (registrieren).
Wenn Sie eine Callback-Funktion geschrieben haben, müssen Sie diese registrieren, um sie aufzurufen, wenn ein bestimmtes Signal emittiert wird. Registriert wird eine Callback-Funktion mit der Funktion:
gulong g_signal_connect( gpointer *object,
const gchar *name,
GCallback func,
gpointer func_data );
Die Bedeutungen der einzelnen Parameter sind folgende:
|
object – Das Objekt, womit Sie die Callback-Funktion verbinden wollen. Hier wird häufig ein Casting (GTK_OBJECT( widget )) verwendet. |
|
name – Das Signal, womit die Callback-Funktion aufgerufen werden soll, wie clicked, wenn ein Button mit der Maustaste geclicked wurde. |
|
func – Die Callback-Funktion, die Sie registrieren wollen. Bei der g_-Version (GTK+ 2.x) müssen Sie diese Funktion mit G_CALLBACK(func) casten – bei der gtk_-Version (GTK+ 1.2) war dies GTK_SIGNAL_FUNC(func). |
|
func_data – Hier können Sie der Callback-Funktion noch zusätzliche Daten übergeben, wenn diese aufgerufen wird. Wollen Sie der Callback-Funktion keine Daten übergeben, können Sie hier auch NULL angeben. |
Mit g_signal_connect() haben Sie eine Callback-Funktion eingerichtet. Die Syntax einer solchen Callback-Funktion, die Sie gewöhnlich selbst schreiben müssen, sieht folgendermaßen aus:
type function( parameter_liste );
So sieht beim Anklicken eines Buttons Ihre Callback-Funktion aus:
static void my_buttonCB( GtkButton *clicked, gpointer data ) {
/* Ausgabe für die Konsole */
g_print("Callback my_buttonCB aufgerufen (%s)\n",
(gchar *)data );
}
clicked ist hierbei der Button, der angeklickt wurde, und data sind die Daten, die beim Einrichten des Signalhandlers mittels g_signal_connect() mit übergeben werden (falls nicht NULL). Diese Funktion registrieren Sie in der Praxis so:
g_signal_connect(GTK_OBJECT ( button ), /* für welches Widget */
"clicked", /* welches Signal */
G_CALLBACK(my_buttonCB),/* welche Funktion */
"Daten"); /* Daten für Funktion */
15.4.4 Eine GTK+-Anwendung beenden
Zwar wird das Thema der Events noch in einem anderen Kapitel ausführlicher behandelt, doch Folgendes gehört zur Grundausstattung einer GTK+-Anwendung: Gemeint ist die Beendigung einer Anwendung. Wenn Sie Ihre GTK+-Anwendung starten und vom Rahmen des Window-Managers aus beenden wollen, wird sich das Fenster Ihrer Anwendung zwar schließen lassen, wohl aber nicht beenden, wenn Sie nicht spezielle Vorkehrungen treffen. Einfach gesagt, die Anwendung läuft still und ruhig im Hintergrund als aktiver Prozess weiter.
Intern passiert dabei Folgendes: Sie schließen das GTK+-Fenster mit dem Window-Manager, und es wird ein delete-event-Signal emittiert. Dieses Signal zeigt an, dass der Anwender das Programm beenden will. Jetzt müssen Sie als Programmierer entscheiden, ob das so in Ordnung geht. Wenn Sie das Beenden erlauben, wird das Signal destroy gesendet, und die Anwendung wird komplett beendet. Somit benötigen Sie zuerst eine Callback-Routine, die das Signal delete-event bearbeiten kann. Der Prototyp dieser Funktion sieht wie folgt aus:
static gboolean my_delete_Event( GtkWidget *widget,
GdkEvent *event,
gpointer data )
Der Parameter widget wird benötigt und beinhaltet u. a. die Kennungs-ID des Widgets, welches das Signal delete-event erzeugt hat. Der event-Parameter ist die Adresse einer Struktur, worin sich alles befindet, was Sie für das Event benötigen (darauf wird noch genauer eingegangen). Mit data können Sie beim Registrieren des Signalhandlers Daten an die Funktion mit übergeben. Wichtig ist der Rückgabewert der Funktion my_delete_Event(). Wenn dieser TRUE ist, wird verhindert, dass die Anwendung beendet wird. FALSE hingegen beendet die Anwendung, schließt also das Fenster und emittiert das Signal destroy. Somit sieht eine komplette Callback-Funktion zum Beenden der Anwendung über den Window-Manager folgendermaßen aus:
static gboolean my_delete_Event( GtkWidget *widget,
GdkEvent *event,
gpointer data ) {
/* Ausgabe für die Konsole */
g_print("Der Window-Manager will die Anwendung beenden\n");
/* Unsern Segen zur Beendigung soll er haben */
return FALSE;
}
Jetzt muss diese Funktion nur noch mit dem Fenster der Anwendung verbunden werden, da das Fenster die einzige Anwendung ist, die mit dem Window-Manager kommuniziert. Hierzu müssen Sie nur die g_signal_connect()-Funktion wie folgt verwenden:
g_signal_connect( GTK_OBJECT (fenster), "delete-event",
G_CALLBACK(my_delete_Event), NULL);
Jetzt hat unser Fenster das destroy-Event erhalten. Natürlich müssen Sie auch hierzu noch eine Callback-Funktion einrichten:
g_signal_connect ( win, "destroy",
G_CALLBACK (end), NULL);
Die Callback-Funktion end sorgt nun mit dem Aufruf der Funktion gtk_main_quit() (dazu in Kürze mehr) für das absolute Ende der GTK+-Anwendung:
static void end (GtkWidget * widget, gpointer daten) {
g_print ("Und tschuess!\n");
/* Die Verarbeitungsschleife beenden */
gtk_main_quit ();
}
Sicherlich mag Ihnen dieser Weg auf dem ersten Blick ein wenig umständlich vorkommen – und ja, Sie können die GTK+-Anwendung auch schneller beenden (ohne den aufwändigeren Weg). Allerdings sollten Sie dann bedenken, falls sich in der Anwendung noch nicht gespeicherte Daten befinden (wie bei einem Texteditor), dass diese bei der Beendigung auch weg sind, wenn Sie keine Vorkehrungen getroffen haben. Mit dem eben gezeigten Weg können Sie z. B. in der Callback-Funktion my_delete_Event() noch überprüfen, ob irgendwelche zusätzlichen Arbeiten vor Beendigung der Anwendung zu erledigen sind, und ggf. TRUE (anstatt FALSE) zurückgeben, womit die Anwendung nicht beendet wird (weil dadurch das Signal destroy nicht emittiert wird). Ein Beispiel dazu finden Sie etwas später mit dem Listing gtk1b.c.
15.4.5 Die hierarchische Anordnung der Widgets definieren
Wenn Sie z. B. ein Fenster- und ein Button-Widget erzeugen, haben Sie noch lange nicht die Eltern-Kind-Beziehung zwischen den Widgets zustande gebracht. Vereinfacht heißt das, Sie müssen die Hierarchie errichten, damit GTK+ weiß, wie und wo die Widgets auf dem Bildschirm angezeigt werden sollen.
Um eine Eltern-Kind-Beziehung einzurichten, müssen Sie erst ein Kind-Widget zum verwaltenden Eltern-Widget hinzufügen. Der Funktionsaufruf hängt vom Eltern-Widget ab. Im Beispiel wird davon ausgegangen, dass das Fenster (GtkWindow) das Eltern-Widget ist – was gleichzeitig auch ein Abkömmling des GtkContainer-Widgets ist. Daher können Sie folgende Funktion verwenden:
void gtk_container_add( GtkContainer *container,
GtkWidget *widget );
Mit container geben Sie das Eltern-Widget an und mit widget das, das Sie zum container hinzufügen wollen.
Hinweis Container sind Widgets, die wiederum andere Widgets enthalten können, und sind meistens für das Layout der Anwendung verantwortlich. Häufig werden Fenster als Container (zu Deutsch: Behälter) verwendet, worin sich alle anderen Widgets des Programms befinden.
|
Vorwiegend werden Box- und Tabellen-Widgets als Behälter für andere Widgets verwendet. Mit den Box- bzw. Tabellen-Widgets ist es recht einfach, die Widgets in einer bestimmten Lage zueinander anzuordnen. Natürlich lassen sich auch solche Behälter ineinander verschachteln, um die Form und Gestalt der Anwendung zu bestimmen. Wenn man ein Widget in einen Behälter steckt, spricht man von Packen. Die Widgets in einem Behälter werden als Kind-Widgets bezeichnet, wobei kein Kind-Widget gleichzeitig in verschiedenen Behältern vorkommen kann.
In den (bald) folgenden Listings werden Sie z. B. eine vertikale Box als Kind-Widget in den Behälter stecken – in einem Fall ist der Behälter z. B. ein Fenster. Womit Sie praktisch einen Behälter in einen Behälter packen – die Box ist zwar ein Widget, aber gleichzeitig ist sie auch ein Behälter. Die einfachste Möglichkeit, jetzt weitere Widgets in die Box zu packen, wäre mit der Funktion:
void gtk_box_pack_defaults( GtkBox *box, GtkWidget *widget );
Damit können Sie z. B. nacheinander weitere Widgets mit einer Standardeinstellung packen. Wurde eine Box z. B. mit der Funktion gtk_vbox_new() erzeugt, bedeutet dies, dass die weiteren Widgets mit der Funktion gtk_pack_defaults() vertikal angeordnet hinzugefügt werden.
15.4.6 Widgets anzeigen
Jetzt haben Sie zwar Widgets erzeugt, die Anordnung und Hierarchie festgelegt, aber noch existieren diese Daten im Verborgenen (im Hauptspeicher). Sie haben jetzt zwei Möglichkeiten, die Widgets anzeigen zu lassen.
void gtk_widget_show( GtkWidget *widget );
void gtk_widget_show_all( GtkWidget *eltern_widget );
Mit der Funktion gtk_widget_show() können Sie die einzelnen Widgets nacheinander auf dem Bildschirm zeichnen lassen. Allerdings ist es doch sehr umständlich, wenn man bedenkt, dass umfangreichere Anwendungen schon mal ein paar Dutzend Widgets besitzen – schnell wird dabei ein Widget vergessen. Einfacher können Sie es sich mit der Funktion gtk_widget_show_all() machen. Hierfür müssen Sie nur das Eltern-Widget angeben, und alle sich darunter befindlichen Kinder-Widgets werden mit angezeigt (Ähnliches hatten Sie ja schon im Kapitel zum Xt-Toolkit gehabt).
Natürlich können Sie auch einzelne oder alle Widgets verstecken. Dies wird mit den entsprechenden Gegenstücken zu den eben erwähnten Funktionen gemacht:
void gtk_widget_hide( GtkWidget *widget );
void gtk_widget_hide_all( GtkWidget *eltern_widget );
Wird ein Widget nicht mehr benötigt, so sollte dieses mit der Funktion gtk_widget_destroy() freigegeben werden.
void gtk_widget_destroy ( GtkWidget *widget );
15.4.7 Signale und Events abfangen und bearbeiten –
(Events-)Verarbeitungsschleife
Wie schon bei der Xt-Bibliothek befindet sich die Verarbeitungsschleife am Ende des Codes. Die Syntax der Verarbeitungsschleife ist einfach:
void gtk_main();
Bis zur Funktion gtk_main() läuft das Programm erst iterativ ab. Nun wartet das Programm in der gtk_main()-Funktion auf ein Ereignis. Das Programm befindet sich praktisch in einer Endlosschleife. Tritt ein Ereignis auf, arbeitet GTK das Ereignis ab (vorausgesetzt, es wurde eine entsprechende Funktion für das Ereignis registriert) und springt anschließend wieder zur gtk_main()-Funktion zurück – außer bei diesem Ereignis handelt es sich um einen Aufruf der Funktion gtk_main_quit(), womit die Verarbeitungsschleife beendet und die Ausführung des Programms hinter gtk_main() fortgeführt wird.
void gtk_main_quit();
Wird gtk_main_quit() in einer Callback-Funktion aufgerufen, wird das Programm nicht unmittelbar beendet, sondern kehrt vorher noch hinter die Ausführung von gtk_main() zurück. Befindet sich hinter der gtk_main()-Funktion noch Code, wird dieser natürlich ausgeführt. Wollen Sie eine Anwendung unwiderruflich, egal von welcher Stelle aus beenden, können Sie dies mit der Funktion gtk_exit(), die gleichwertig zur Standard-C-Funktion exit() ist.
Hier der grafische Vorgang der Verarbeitungsschleife (gtk_main()) und gleichzeitig auch das Signalhandler-Modell von GTK+.
15.4.8 GTK+ und Umlaute (Zeichenkodierung)
Sofern Sie Umlaute bzw. Nicht-ASCII-Zeichen in einer GTK+-Anwendung verwenden bzw. darstellen wollen, ohne eine Fehler- bzw. Warnmeldung zurückzubekommen wie z. B. [Invalid UTF-8], muss die Quelldatei in UTF-8 kodiert bzw. abgespeichert werden. Sofern Sie z. B. den GNU Emacs verwenden, sind Sie fein raus, denn dann reicht es aus, am Anfang des Listings folgende Steuerzeile einzufügen:
/* -*-coding: utf-8;-*- */
Bei anderen Editoren müssen Sie selbst nachsehen, ob eine solche Funktionalität vorhanden ist. Eventuell kann es sinnvoll sein, den Text mit dem Konverter iconv selbst vom latin1-Format zum utf-8-Format zu konvertieren. Ein Beispiel dazu könnte wie folgt aussehen:
$ iconv -f latin1 -t utf-8 gtk1.c > gtk1_utf8.c
Hiermit konvertieren Sie aus der Quelldatei gtk1.c, die sich im latin1-Format befindet, eine UTF-8-codierte Datei namens gtk1_utf8.c, die jetzt in der Lage ist, auch Umlaute und Nicht-ASCII-Zeichen anzuzeigen.
Hinweis Eine weitere Möglichkeit zur Lokalisierung von Software stellt GNU Gettext da. Mehr hierzu können Sie der Manual Page von gettext entnehmen.
|
UTF-8
Die UTF-8-Kodierung ist eine der wichtigsten, so dass es sich lohnen kann, sich näher damit zu befassen. UTF-8 ist kompatibel zur ASCII-Kodierung. Wie Sie wissen, werden ASCII-Zeichen mit einem Byte dargestellt. Somit ist jeder reine ASCII-Text auch ein reiner UTF-8-Text. Leider besteht hier auch das Problem, dass ein UTF-8-Text keine Reihe von gleich breiten Zeichen hat und somit iterativ zeichenweise zerlegt werden muss. Bei Verwendung von Nicht-ASCII-Zeichen, wie in unserem Land mit den Umlauten, führt diese Zerlegung zu unschönen Effekten. Um dieses Problem zu umgehen, finden Sie in der Glib hierzu einige nützliche Funktionen, die alle mit dem Präfix g_utf8_ beginnen.
Damit Sie sich nicht schon während der ersten Schritte mit GTK+ mit den verschiedenen Zeichenkodierungen herumschlagen müssen, wurde weitgehend auf Verwendung von Umlauten und Nicht-ASCII-Zeichen in den Beispielen verzichtet.
|