Signalgeber
Motif war lange Zeit die einzige Wahl für X11-Programmierer, die portabel entwickeln wollten. Mit dem Aufstieg von Linux wuchs der Bedarf an freien Widgets, denn viele Linuxer mißtrauten kommerzieller Software, zu deren Quellen sie keinen Zugang hatten. Gtk schickt sich an, diese Lücke zu schließen.
- Rolf Herzog
Mit dem Gimp Tool Kit Gtk steht nun die zweite GUI-Bibliothek am Start, um Motif den Rang des Marktführers abzulaufen. Anders als Qt [1], die C++-Bibliothek der norwegischen Firma Troll Tech, steht Gtk unter der LGPL (Library Gnu Public Licence). Man kann es deshalb auch in modifizierter Form frei weiterverbreiten, vorausgesetzt, die Quellen sind dabei. Deshalb ist Gtk Grundlage des Gnome-Projekts, das ähnlich wie KDE einen freien Desktop für Unix bereitstellen möchte [2].
Gtk war bereits von Anfang an als Alternative zu Motif geplant, denn Peter Mattis, Spencer Kimball und Josh MacDonald entwickelten es, um für Gimp [3] eine freie GUI-Bibliothek statt Motif verwenden zu können. Inzwischen wird es unabhängig von Gimp weiterentwickelt und erreichte lange vor ihm die Versionsnummer 1.0. Dieser Artikel behandelt die derzeit aktuelle stabile Version 1.0.4.
Mehr im Netz zu gtk
- Gtk-Homepage
- Gtk-Mailing-Liste gtk-list-request@redhat.com
- CVS-Server des Gtk-Projekts
- Gtk Application Repository
- Zwei Interface-Builder für Gtk, beide im Alpha-Stadium: GUB (www.softhome.net/pub/users/timj/gubi/), Glade
- Gtk - C++-API für Gtk (www.cs.tut.fi/~p150650/gtk/gtk--.html)
- Gtk Font Selection Widget
- imlib, Grafikbibliothek für Gtk (ftp://labs.redhat.com/pub/imlib)
- Perl-Modul für Gtk (ftp://ftp.cpan.org/modules/by-module/Gtk)
Die Source-Distribution von Gtk enthält zwei weitere Bibliotheken: das Gimp Display Kit (GDK), das alle direkten Xlib-Aufrufe kapselt, und Glib, eine Bibliothek mit häufig benötigten Funktionen etwa für verkettete Listen, das Speichermanagement und Fehlerbehandlungsroutinen. Glib ist inzwischen nicht mehr Teil der Gtk-Quellen und auch ohne Gtk einsetzbar.
Objektorientierte Schnittstelle in C
Obwohl in C und nicht in C++ geschrieben, verwendet Gtk ein objektorientiertes Interface. Alle Widgets sind als Objekte implementiert, die ihre Eigenschaften von abstrakten Basisklassen `erben'. Der Ursprung dieser Hierarchie ist GtkObject und die dazugehörige Klasse GtkObjectClass.
Realisiert sind diese Objekte und Klassen als typisierte Datenstrukturen. Die Struktur jeder Klasse und jedes Objekts enthält als erstes Element einen Zeiger auf das jeweilige Elternobjekt (beziehungsweise auf die -klasse). Es muß immer am Beginn der Datenstruktur stehen, denn das gewährleistet, daß die Kindobjekte typkompatibel mit ihren Eltern und allen davon abgeleiteten Objekten bleiben. Gtk funktioniert darum nur mit Compilern, die die Elemente einer Struktur in der Reihenfolge im Speicher ablegen, in der sie deklariert sind. Bisher ist das Gtk-Team allerdings auf keinen Compiler gestoßen, der diese Bedingung nicht erfüllt.
Objekte besitzen zusätzlich einen Zeiger auf die Klasse, zu der sie gehören. Dadurch ist es möglich, virtuelle Funktionen in C zu implementieren, indem Funktionszeiger in der Datenstruktur der Objektklasse abgelegt werden.
Die Datenfelder eines Objekts sind als verkettete Liste organisiert, deren Ursprungszeiger ebenfalls in der Datenstruktur des Objekts zu finden ist. Deshalb können Anwendungsprogrammierer eigene Daten in den Widgets deponieren. Dazu dienen die Funktionen gtk_object_set_data, gtk_object_get_data und gtk_object_remove_data.
Wer eine feinere Anpassung benötigt, kann eigene Gtk-Widgets von vorhandenen ableiten. Dazu muß er eine neue Klassenstruktur erstellen, die als erstes Element einen Zeiger auf die Elternklasse enthält. Das eigentliche Widget wird dann nach dem Vorbild von GtkObject modelliert. Die Klasse benötigt einen `Konstruktor', der beim Generieren des ersten Widget Speicherplatz für Funktionszeiger und Daten reserviert, die allen Objekten dieses Typs gemeinsam sind. Wird die letzte Instanz dieser Klasse gelöscht, muß auch die Klassenstruktur selbst aus dem Speicher verschwinden, und dafür sorgt Gtk. Als Vorlage für die Initialisierung kann dabei gtk_object_class_init dienen. Um das Löschen der Klasse kümmert sich Gtk.
Signalkonzept aus Qt
Auch das Objekt benötigt eine Initialisierung, die die Datenfelder des Widget füllt und den Referenzzähler erhöht. Außerdem müssen Klasse und Objekt Funktionen bereitstellen, die Auskunft über den jeweiligen Typ geben. Dies ist für den Casting-Mechanismus notwendig.
Diese Art der objektorientierten Implementierung in C hat zwei Vorteile: Zum einen erlaubt diese Sprache eine leichte Anbindung an andere Programmiersprachen, so existieren bereits Module für C++, Objective C, Scheme, Python und Perl. Zum anderen muß man trotzdem nicht auf den Organisationsvorteil objektorientierter Programmierung verzichten.
Von Qt hat Gtk das Konzept der Signale übernommen. Jedes Widget verfügt über eine Menge definierter Signale, die es bei verschiedenen Benutzeraktionen sendet. So schickt ein Button beim Drücken das Signal `pressed' oder ein Eingabefeld das Signal `changed' beim Eingeben von Text. Der Programmierer kann nun diese Signale mit eigenen Funktionen verbinden (mit gtk_signal_connect()) und auf die Ereignisse reagieren. Auch die Events des X-Servers sind auf diese Weise zu verarbeiten.
Gtks Widgets decken alles ab, was in der GUI-Programmierung nötig ist. Neben den Basiselementen wie Buttons, List-, Combo-, und Radioboxen sowie Eingabefeldern gibt es ein Text-Widget, das Texte beliebiger Länge verwaltet und mit Hilfe von `Properties', ähnlich wie Emacs, einzelnen Textteilen verschiedene Schriften und Farben zuordnen kann. Ein Tree-Widget und ein Notebook mit Registern, die wahlweise an einer der vier Seiten erscheinen können, runden das Angebot ab. Beinahe jedes Widget kann als Container für andere Widgets dienen. Dadurch sind zum Beispiel Buttons mit Grafiken kombinierbar.
Daneben existieren zusammengesetzte Widgets wie Datei- und Farbauswahl. Die Herkunft von Gimp zeigt sich am Widget zur Darstellung von Gammakurven und dem `aspected Frame' deutlich, der eine Veränderung der Fenstergröße unter Beibehaltung der Seitenverhältnisse erlaubt.
Im Unterschied zu Qt besaß Gtk von Beginn an zwei verschiedene Geometriemanager, die eine relative Positionierung von Widgets ohne explizite Koordinatenangaben ermöglichen: GtkBox, das horizontales oder vertikales Positionieren erlaubt, und GtkTable, das die einzelnen Widgets wie Zellen einer Tabelle behandelt. Unterstützung für Drag and Drop ist ebenfalls vorhanden, es fehlen jedoch ein Druckdialog sowie die Möglichkeit, Grafik zu drucken.
Fensterlose Widgets sparen Ressourcen
Eine Besonderheit von Gtk ist der schonende Umgang mit den Systemressourcen durch Verwendung fensterloser Widgets, ähnlich den Gadgets in Motif. Einige Widgets, wie GtkLabel, besitzen kein eigenes X-Fenster, sondern schreiben ihre Ausgabe einfach in das des Eltern-Widget. Dadurch entfällt der Verwaltungsaufwand für Fenster, die bei simplen Widgets wie Labels ohnehin überflüssig sind. Sollte der Programmierer allerdings doch ein eigenes Fenster für solche Widgets wünschen, etwa weil er einen Mausklick auf ein GtkLabel abfangen oder zu lange Label abschneiden will, muß er das Fenster explizit anfordern. Dazu eignet sich etwa eine GtkEventBox oder ein GtkViewport. Ist zum Beispiel ein Label erforderlich, das zu langen Text abschneidet, wird das Label-Widget in eine GtkEventBox mit exakter Größenvorgabe verpackt. Auf diese Weise kann man auch Buttons mit Icons und Text kombinieren, indem GtkButton als Container für ein GtkLabel und GtkPixmap dient.
Um das Kompilieren eigener Anwendungen zu vereinfachen, enthält Gtk das Programm gtk-config. Man kann es in den Makefile einbauen, um die Positionen von Header-Dateien und Bibliotheken zu erfahren. Ein typischer Compiler-Aufruf sieht dann etwa so aus:
gcc programm.c -o programm `gtk-config -cflags` \
`gtk-config -libs`
Als praktischer Einstieg in die Gtk-Programmierung soll das Menübeispiel in Listing 1 dienen. Gtk stellt mit GtkMenuFactory ein Widget bereit, das die Menüerstellung erleichtert. Die einzelnen Menüpunkte erscheinen in den Zeilen 6 bis 16 in Form eines Feldes aus GtkMenuEntry-Strukturen. Das erste Feld dieser Datenstruktur enthält den Namen des Menüpunktes in Form einer Pfadangabe. In Feld zwei kann man ein Tastaturkürzel für diesen Menüpunkt definieren. Das dritte Feld nimmt einen Zeiger auf die Callback-Funktion auf und das vierte Parameter für diese Funktion. In komplexen Programmen kann man die gesamte Menüstruktur in einem einzigen Array unterbringen, Gtk besitzt die Möglichkeit, das Array mit den Menüeinträgen auf mehrere Menüs zu verteilen. Eine explizite Unterstützung der Lokalisierung, also der Anpassung an verschiedene Landessprachen, fehlt allerdings noch.
Listing 1
1 #include <gtk/gtk.h>
2
3 void file_quit_cmd_callback(GtkWidget *widget, gpointer data);
4 void file_open_cmd_callback (GtkWidget *widget, gpointer data);
5
6 static GtkMenuEntry menu_items[] =
7 {
8 {"File/New", "<control>N", NULL, NULL},
9 {"File/Open", "<control>O", file_open_cmd_callback, NULL},
10 {"File/Save", "<control>S", NULL, NULL},
11 {"File/Save as", NULL, NULL, NULL},
12 {"File/<separator>", NULL, NULL, NULL},
13 {"File/Quit", "<control>Q", file_quit_cmd_callback,
14 "...und Tschüß!\n"},
15 {"Options/Test", "<control>T", NULL, NULL}
16 };
17 int nmenu_items = sizeof(menu_items) / sizeof(menu_items[0]);
18
19 GtkWidget *window;
20 GtkWidget *main_vbox;
21 GtkWidget *menubar;
22 GtkMenuFactory *mfactory;
23
24
25 void
26 file_open_cmd_callback(GtkWidget *widget, gpointer data)
27 {
28 GtkWidget *filew;
29
30 filew = gtk_file_selection_new ("File selection");
31
32 gtk_signal_connect_object (GTK_OBJECT
33 (GTK_FILE_SELECTION (filew)->ok_button),
34 "clicked", (GtkSignalFunc) gtk_widget_destroy,
35 GTK_OBJECT (filew));
36
37 gtk_signal_connect_object (GTK_OBJECT
38 (GTK_FILE_SELECTION (filew)->cancel_button),
39 "clicked", (GtkSignalFunc) gtk_widget_destroy,
40 GTK_OBJECT (filew));
41 gtk_widget_show(filew);
42 }
43
44 void
45 file_quit_cmd_callback (GtkWidget *widget, gpointer data)
46 {
47 g_print ("%s\n", (char *) data);
48 gtk_exit(0);
49 }
50
51 int
52 main(int argc, char *argv[]) {
53
54 gtk_init(&argc, &argv);
55
56 gtk_rc_parse("menutestrc");
57
58 window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
59 gtk_window_set_title(GTK_WINDOW(window), "Gtk Menu Factory");
60 gtk_widget_set_usize(GTK_WIDGET(window), 300, 200);
61
62 gtk_signal_connect(GTK_OBJECT(window), "destroy",
63 GTK_SIGNAL_FUNC(file_quit_cmd_callback),
64 "WM destroy");
65
66 main_vbox = gtk_vbox_new(FALSE, 1);
67 gtk_container_border_width(GTK_CONTAINER(main_vbox), 1);
68 gtk_container_add(GTK_CONTAINER(window), main_vbox);
69 gtk_widget_show(main_vbox);
70
71 mfactory = gtk_menu_factory_new(GTK_MENU_FACTORY_MENU_BAR);
72 gtk_menu_factory_add_entries(mfactory, menu_items, nmenu_items);
73 gtk_window_add_accelerator_table(GTK_WINDOW(window),
74 mfactory->table);
75 menubar = mfactory->widget;
76 gtk_box_pack_start(GTK_BOX(main_vbox), menubar, FALSE, TRUE, 0);
77 gtk_widget_show(menubar);
78
79 gtk_widget_show(window);
80 gtk_main();
81
82 return(0);
83 }
Die Zeilen 25 bis 42 definieren eine Callback-Funktion, die der Menüpunkt `File/Open' auslöst. Sie zeigt eine (hier funktionslose) Dateiauswahl an, die ein Klick auf `OK' oder `Cancel' schließt.
Auslöser beider Aktionen sind Signale, denen die Zeilen 32 bis 40 mit gtk_signal_connect() Funktionen zuordnen. Hier fängt diese Routine das Signal `clicked' ab und verbindet es mit der Gtk-internen Funktion gtk_widget_destroy. Als letzter Parameter steht das zu schließende Fenster im Aufruf.
Das Beispiel löscht den Dialog beim Schließen vollständig. In einer realen Anwendung würde man eher die Funktion gtk_widget_unmap verwenden. Sie schließt das Fenster, ohne Daten aus dem Speicher zu entfernen. Die Callback-Funktion file_open_cmd_callback müßte dann natürlich erst prüfen, ob der Dialog schon existiert.
Eigene statt X-Ressourcen
Mit der Initialisierung des Gtk-Toolkits in Zeile 54 beginnt die main-Funktion. Dieser Funktionsaufruf darf in keinem Gtk-Programm fehlen. Er interpretiert neben der Initialisierung des Objektmechanismus einige Kommandozeilenparameter wie `-display'. Die Auswertung einer mit `-geometry' angegebenen Fenstergröße bleibt jedoch der Anwendung überlassen. In Zeile 56 wird die Ressource-Datei des Testprogramms eingelesen und verarbeitet. Gtk benutzt keine X-Ressourcen zur Konfiguration, sondern ein eigenes Ressourcen-Format.
Ressourcen einmal anders
Jedes gute Gtk-Programm sollte die Möglichkeit bieten, Farb- und Schrifteinstellungen zur Laufzeit zu verändern. Gtk geht hier einen anderen Weg als traditionelle X-Programme. Es benutzt nicht die X-Ressourcen-Datenbank, sondern liest mit gtk_rc_parse() eigene Konfigurationsdateien ein. Mit ihrer Hilfe kann man das Aussehen jedes einzelnen Widget beeinflussen.
Listing 2 zeigt die für das Beispielprogramm verwendete gtkrc-Datei. Ihr Format ist recht einfach: Das style-Kommando definiert beliebige Kombinationen von Schriften, Vorder- und Hintergrundfarben. Diese Stile ordnet das widget_class-Kommando den einzelnen Widget-Klassen zu.
Der Entwickler kann die Einstellmöglichkeiten noch verfeinern, wenn er einem Widget mit der Funktion gtk_widget_set_name einen eigenen Namen gibt. Danach kann er ihm einen eigenen Stil zuweisen, der sich von dem der Widget-Klasse unterscheidet. Mit dieser Funktion können auch Fenster einen Namen erhalten, und man kann Widgets in einem spezifischen Fenster ansprechen. So bekommt mit
mainwin = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_widget_set_name(mainwin,"toplevel");
das Hauptfenster einer Anwendung den Namen `toplevel'. Mit dem gtkrc-Eintrag widget
"toplevel.*Button" style "yellowbutton"
können nun die Buttons im Hauptfenster den Stil `yellowbutton' erhalten.
Der Code in den Zeilen 58 bis 60 erzeugt das Hauptfenster der Anwendung und legt Titel und Größe fest.
In Zeile 62 verarbeitet gtk_signal_connect das `destroy'-Signal, das das Hauptfenster schickt, wenn der Window-Manager es schließen will. Ohne diese Zeilen würde die Anwendung nicht darauf reagieren und ließe sich nur durch ein explizites kill beenden. Dieses Beispiel zeigt, daß Gtk noch manche grundlegende Operation dem Programmierer überläßt.
Da das Menü nicht in der Mitte des Fensters erscheinen soll, sondern am oberen Rand, ist ein Geometriemanager nötig. Der in Zeile 66 erzeugte VBox-Manager kümmert sich um die vertikale Ausrichtung seiner Widgets. Jedes erhält genau den benötigten Platz und zwischen zweien fügt er einen Abstand von einem Pixel ein. Damit der Manager seine Arbeit tun kann, muß er als Child-Widget des Hauptfensters angemeldet sein (Zeile 68).
gtk_menu_factory_new erzeugt in Zeile 71 nun ein Widget zur Aufnahme der Menüstruktur. Enthält das Array mit den Menüeinträgen noch weitere Menüs, muß für jedes ein zusätzliches Widget bereitgestellt und mit gtk_menu_add_subfactory initialisiert werden. In diesem einfachen Fall liest gtk_menu_factory_add_entries in Zeile 72 das gesamte Array. Dies erzeugt automatisch eine Accelerator-Tabelle mit den gewünschten Tastaturkürzeln, die gtk_window_add_accelerator_table in der nächsten Zeile nur noch mit dem Hauptfenster verbinden muß. Anschließend bekommt der VBox-Manager die Kontrolle über das Menü, der es am oberen Fensterrand plaziert. Die Funktion gtk_box_pack_end hätte es am Fuß des Fensters positioniert. Mit gtk_main in Zeile 80 tritt das Programm schließlich in die Eventloop ein und wartet auf Benutzeraktionen.
Dokumentation noch nicht komplett
Einer der größten Nachteile von Gtk ist die unvollständige Dokumentation. Es wird zwar ein Referenzhandbuch im Texinfo-Format mitgeliefert, das besteht jedoch neben einigen allgemeinen Erläuterungen nur aus Listen der Funktionsnamen mit Parametern, die gelegentlich näher kommentiert und dazu noch unvollständig sind. Für GDK sind nur Kapitelüberschriften enthalten. So bleibt einem Entwickler nichts anderes übrig, als die reichlich vorhandenen Beispielprogramme und die Gtk-Quellen zu studieren. Beim Einstieg hilft das ebenfalls mitgelieferte Tutorial zwar etwas, es kann eine detaillierte Dokumentation jedoch nicht ersetzen. Da die Gtk-Entwickler kein Dokumentationstool verwenden und der Code nahezu frei von Kommentaren ist, dürfte dieser unbefriedigende Zustand noch einige Zeit anhalten.
Abgesehen davon ist Gtk jedoch eine ausgereifte und gut durchdachte GUI-Bibliothek, die ihre Feuertaufe mit der Nutzung in Gimp bestanden hat. Sie findet bei den Entwicklern freier Software zur Zeit regen Zuspruch (ein Gtk-Port von Netscape befindet sich in Arbeit, das GNOME-Projekt basiert auf Gtk), und auch an der Bibliothek selbst arbeiten die Programmierer intensiv. Eine Entwicklerversion von Gtk 1.1 ist schon über einen anonymen CVS-Account zugänglich. Sie enthält neben Geschwindigkeitsoptimierungen einige Erweiterungen: einen Dialog zur Schriftauswahl, den Layoutmanager GtkPacker, der wie der Packer von Tk arbeitet. Ferner sind Unterstützung für das Einlesen mehrerer gtkrc-Dateien sowie eine vereinfachte Menüerstellung mit GtkItemFactory bereits implementiert. Ein Druckdialog und bessere Dokumentation sind als zukünftige Erweiterungen ebenso vorgesehen wie die Überarbeitung des Erscheinungsbilds. Der Benutzer eines Gtk-Programms wird über `Themes' das Layout der Anwendung (wie Nextstep, Motif oder ein eigener davon abweichender Entwurf) verändern können.
ROLF HERZOG
ist eigentlich Student der Sozialwissenschaften, hat inzwischen jedoch aus seinem Nebenjob als EDV-Berater und Gelegenheitsprogrammierer einen Beruf gemacht.
Literatur
[1] Christian Kirsch; Klassenstärke; C++-Klassenbibliothek für X und Windows; iX 6/97, S. 144 ff.
[2] Jochem Huhmann; Freiheitsbewegung; Objektorientierte Umgebung Gnome; iX 8/98, S. 50 ff.
[3] Ingo Lütkebohle, Sven Neumann; Bildwerfer; GIMP 0.99.x; iX 7/98, S. 50 ff.
iX-TRACT
- Gtk ist eine Widget-Bibliothek, die unter der GPL steht. Sie erlaubt objektorientiertes Programmieren, ist aber in C geschrieben.
- Von Qt hat Gtk den Signalmechanismus übernommen, ähnlich wie Motif bietet es fensterlose Widgets an. Es benutzt nicht die X-Ressourcen, sondern ein eigenes Format.
- Gimp und das gesamte GNOME-Projekt setzen Gtk als Basis für ihr GUI ein.
- Es gibt Schnittstellen zu Perl, C++, Objective C, Python und Scheme.
Listing 2
pixmap_path "./"
style "default"
{
font = "b&h-lucidabright-demibold-i-normal--14-100-100-100-p-80-iso8859-1"
bg_pixmap[NORMAL] = "bg_normal.xpm"
bg_pixmap[INSENSITIVE] = "bg_normal.xpm"
bg_pixmap[ACTIVE] = "bg_active.xpm"
bg_pixmap[PRELIGHT] = "bg_prelight.xpm"
}
style "label"
{
bg[NORMAL] = {0.5, 0.5, 0.5}
fg[NORMAL] = {1.0, 1.0, 1.0}
}
style "menu"
{
bg[NORMAL] = {0.5, 0.5, 0.5}
fg[NORMAL] = {1.0, 1.0, 1.0}
}
widget_class "*Menu*" style "menu"
widget_class "*Label*" style "label"
widget_class "*EventBox*" style "label"
widget_class "*Button*" style "label"
widget_class "*Scrollbar*" style "menu"
widget_class "*" style "default"
(ck)