Libraries 2: Erste Schritte mit GTK


Einleitung
Was ist GTK?
Oberflächen gestalten mit Glade
Das Textview-Widget
Eine Scrollbar für den Textbereich
Eingabefeld und Tastendrücke
Menüs
Grössere Dialoge
Kleinere Standard-Dialoge
Regelmässige Ereignisse, eigenständige Abläufe
Weitergabe von GTK-Programmen

Einleitung

Endlich kommen wir zur echten Fenster-Programmierung. Mit Menüs und Buttons und Schiebereglern. Es gibt mit Freebasic viele Möglichkeiten, das zu realisieren. Man benötigt noch nicht einmal eine eigene Lib dazu, da das Betriebsystem in der Regel eine API dafür bereitstellt. Tatsache ist, dass mir bisher nicht viele GUI-FB-Programme, nur auf Windows-API programmiert, begegnet sind. Die Hintergründe wurden in Kapitel2 dieses Teils schon erläutert. Es geht halt einfacher und schneller, eine Lib zu benutzen.

Mögliche Libs, die diesem Zweck dienen, sind:

Theoretisch gäbe es noch wesentlich mehr davon, doch nicht alle sind kostenlos und vor allem nicht alle funktionieren unter Linux, Windows und Mac gleichermassen.

Hier wird GTK zur Sprache kommen. Aber das soll keine Wertung bzgl. der anderen Libs sein. Grundsätzlich sind GTK und Qt die bekanntesten und grössten OS-übergreifenden Toolkits. "Toolkit" meint, dass es sich dabei um wesentlich mehr als nur im eine GUI-Lib handelt. Sie decken auch Speichermanagement, Stringbehandlung, Datenträgerzugriff, Datenbankzugriff und einiges mehr ab - fast so eine Art Betriebssystem im Betriebssystem. Daher kann man Qt- und GTK-Programme unverändert auf Linux, Windows oder Mac kompilieren und hat lauffähige Programme.

Der Grund, warum ich hier GTK ausgewählt habe, ist übrigens einfach: Freebasic hat zu Qt noch kein Binding.

Was ist GTK?

GTK heisst "GIMP Tool Kit". Und Gimp ist ein mächtiges Zeichen- und Fotobearbeitungsprogramm, das 1996 unter Unix von Peter Mattis geschrieben wurde. Zu dieser Zeit gab es nur ein Toolkit jeweils unter Windows und Unix/Linux. Das Toolkit unter Unix hiess "Motif" und kostete pro Arbeitsplatz schlappe 2000 DM und wurde proprietär von einem Konsortium grosser Firmen entwickelt. Die erste Version von Gimp kam noch unter Motif heraus, doch dann schrieb Peter Mattis sein eigenes Toolkit: GTK. Es wurde von der Entwicklergemeinde aufgenommen. Heute bauen Firmen wie Sun oder IBM darauf. Open Office, Inkscape und der Desktop Gnome sind GTK-Programme.

Oberflächen gestalten mit Glade

So ähnlich wie bei den Microsoft-Visual-Tools und den bekannten Tools von Borland/Codegear (Delphi, Builders) gibt es auch bei GTK die Möglichkeit, die ganze Oberfläche mit der Maus zu gestalten. Das entsprechende Tool heisst Glade und die Anfang 2009 aktuelle Version ist Glade3.

Installation von Glade3

Der einfachste Weg führt über die Wikipedia und den entsprechenden Link. Das müsste dieser hier sein. Es gibt eigentlich nur einen Knackpunkt: Die Version von Glade und die Version von GTK müssen genau zusammenpassen. Daher sollte man sicherheitshalber genau die GTK-Version nachinstallieren, die unter dem Glade-Link steht.

Sicherheitshalber sollte man dann das System neu starten, damit die Pfade und Umgebungsvariablen richtig gesetzt werden. Oder man setzt sie einstweilen manuell mit einem Skript selbst, der bei mir so aussieht:

set path=%path%;D:\program\fbc020;D:\program\fbc020\bin\win32
set INCLUDE=D:\program\gtk212\INCLUDE;D:\program\gtk212\INCLUDE\GTK-2.0;D:\program\gtk212\INCLUDE\GLIB-2.0;D:\program\gtk212\INCLUDE\PANGO-1.0;D:\program\gtk212\INCLUDE\CAIRO;D:\program\gtk212\INCLUDE\ATK-1.0;D:\program\gtk212\INCLUDE\GTKGLEXT-1.0;D:\program\gtk212\LIB\GTK-2.0\INCLUDE;D:\program\gtk212\LIB\GLIB-2.0\INCLUDE;D:\program\gtk212\LIB\GTKGLEXT-1.0\INCLUDE;D:\program\gtk212\INCLUDE\LIBGLADE-2.0;D:\program\gtk212\INCLUDE\LIBXML2
set LIB=D:\program\gtk212\LIB;

Erste Schritte: Eine Oberfläche entwerfen

Starten wir Glade. Wir landen in einem Fenster, das ungefähr so aussieht:

Das Prinzip besteht nun damit, sich von der Palette links Elemente, s.g. Widgets, also Buttons, Menüleisten, Dropdown-Boxen etc. zu holen und sie in der Mitte im Fenster zu platzieren. Aber langsam. Das geht im Moment noch nicht, denn wir haben noch gar kein Fenster. Rechts unten sehen wir einen zur Zeit noch leeren Abschnitt mit einigen Registerkarten. Hier werden die Eigenschaften eines Elements angezeigt, wenn wir es markieren. Links oben ist der s.g. Widgetbaum - der ist erstmal nicht so wichtig.

Als allererstes brauchen wir ein Fenster. Das finden wir durch Klicken auf das Symbol ganz links oben unter "Oberste Ebenen". Es erscheint sofort im mittleren Fenster.

Wollen wir seine Grösse ändern, müssen wir zuerst ganz rechts oben in der Menüleiste von Glade auf "Ziehen und Grösse ändern" klicken. Erst dann können wir wie unter Windows gewohnt die Fenstergrösse durch Ziehen an den Ecken verändern.

Fixiert und nicht fixiert:

Eine Spezialität von GTK ist es, Fenster mitsamt ihrem Inhalt zu "skalieren". Das heisst, alles wird automatisch grösser und kleiner, wenn wir das Fenster grösser oder kleiner machen. Das ist nett, macht aber für den Anfang die Gestaltung von Fenstern kompliziert. Leider ist dieses Skalierungsfeature von vornherein eingebaut. Wir müssen also jetzt umgekehrt ein Element einschalten, das dieses Feature ausschaltet. Das ist der Fixier-Container:

Jetzt müssen wir allerdings markieren, wo wir diesen reintun wollen - also unser neues Fenster anklicken. Anschliessend müssten die kleinen Punkte des Fix-Containers unser neues Fenster bedecken.

Ein Button

Jetzt kann's losgehen mit den eigentlichen Elementen - Widgets genannt. Nehmen wir uns als erstes einen Knopf, einen Button. Den finden wir in der Palette links im Bereich "Steuerung und Darstellung" ganz links oben - ein kleines Viereck mit "OK" draufgeschrieben. Den platzieren wir links oben in unserem Fenster.

Textausgabe-Widget:

Dann wollen wir noch ein zweites Widget, einen Bereich zur Textausgabe. Schliesslich ist es das, was wir in einem normalen Konsolenprogramm sofort können: Text ausgeben. Wir finden das Widget zwei Zeilen unterhalb vom Button. Es heisst "Textansicht". Ordnen wir das Ganze noch ein wenig an, dann sieht das bei mir aus aus:

Wieviele Widgets haben wir jetzt? Das verrät uns ein Blick auf den Widget-Baum links oben. Über ihn können wir auch Widgets aktivieren, die im Fenster unter anderen vollkommen verborgen sind - unser Fenster z.B.. Es ist vollkommen verdeckt durch den Fixed-Container. Wenn wir den "Kopf" des Baumes links oben anklicken, ist das Basis-Fenster aktiviert und wir können rechts unten seine Eigenschaften betrachten und bearbeiten.

Eigenschaften? Klingt nach Objekten... Richtig: In GTK ist alles objektorientiert organisiert. Und jedes Widget stellt ein eigenes Objekt dar.

Tragen wir also unter "name" erstmal einen passenden Namen ein, z.B. "mainwindow". Wichtig ist allerdings auch der Fenstertitel, der oben erscheinen soll. "Mein erstes GTK Programm" wäre möglich. Wir können noch andere nette Dinge eingeben: Soll die Größe des Fensters veränderlich sein? Wo soll es erscheinen? Usw. Wir wollen uns hier allerdings nicht in Details verspielen, sondern möglichst bald zum Ergebnis kommen.

Signale

Wichtig ist allerdings die Registerkarte "Signale". Hier unterscheidet sich Glade von einem netten Zeichenprogramm. Unser GUI soll ja nicht nur auf den Bildschirm gezeichnet werden, sondern es soll ja auch auf Aktionen, d.h. Mausklicks und Tastatureingaben reagieren. Im allgemeinen sprechen wir hier von "Ereignissen" oder "Signalen". Und den einzelnen Widgets muss erklärt werden, was sie tun sollen, wenn so ein Signal eintrifft. Und - was sollen sie tun? Doofe Frage, sie sollen dafür sorgen, dass ein Programm ausgeführt wird - unser Freebasic-Programm. Genauer: Teile unseres Freebasic-Programms. Hier beginnt nun die grundlegende Umstellung: Wir müssen unser Programm in Teile zerlegen. Jeder Teil ist für ein Signal zuständig. "Button geklickt? - Programmteil "Button_click" ist zuständig". "Textfenster geklickt? - Programmteil Textausgabe ist zuständig". Usw.

Was kann an unserem Hauptfenster angeklickt werden? Der "Schließen"-Knopf z.B. Klar sollte dann das Fenster geschlossen werden. Aber wie? Einfach so? Oder mit Rückfrage? Sollten vorher noch Ressourcen freigegeben werden? Mit anderen Worten: Das kann nicht einfach durch GTK automatisch gemacht werden. Sondern wir müssen den entsprechenden Programmteil, wir sagen dazu "Callback-Funktion", zur Verfügung stellen.

Sehen wir uns nun an, was unter "Signale" auftaucht: Eine Liste mit GtkWindow, GtkContainer, GtkWidget usw. Was sagt uns das? Das sagt uns, dass wir unter all diesen Einträgen einzelne Signale finden, denen wir Callback-Funktionen zuordnen können. Das Signal, das wir suchen, heisst "destroy". Wir finden es unter GtkObject. Das heisst, dass jede Klasse vom Typ GtkObject auf ein Signal "destroy" horcht. Insbesondere gehören alle Widgets zur Familie GtkObject. Und ein Window ist ein spezielles Widget.

Wir können nun diesem Destroy-Ereignis/Signal eine Callbackfunktion zuordnen. Nennen wir sie "on_mainwindow_destroy". Das Feld "Benutzerdaten" lassen wir leer. Es könnte Argumente aufnehmen, die unserer Callback-Funktion übergeben werden sollten.

Button-Eigenschaften

Klicken wir rechts oben das Button-Widget an. Auch der Button braucht einen Namen und eine Beschriftung. Gehen wir also auf das Eigenschaftsfenster/Allgemein, lassen den Namen bei "button1" und geben ihm eine nette Beschriftung, z.B. "Click me!". Dann gehen wir wieder auf "Signale" und gehen auf "clicked". Dort nennen wir unsere Callback-Funktion "on_button1_clicked". Auch hier brauchen wir keine Benutzerdaten.

Textview-Eigenschaften

Hier müssen wir gar nichts tun. Das Textfeld soll nicht auf den Benutzer reagieren, es soll einfach nur Text ausgeben, wenn das Programm das anweist. Also brauchen wir keine Signale und keine Callback-Funktion. Es heisst by default "textview1" und das lassen wir dann auch so.

Erste Schritte: Das Programm schreiben

Spätestens jetzt sollten wir unser Glade-Projekt speichern. Am Besten in einen eigenen Projektordner. Nennen wir es "gtktest1". Nun schauen wir an, was im Projektordner steht: Eine einzige Datei mit der Endung "glade". Es handelt sich allerdings dabei um eine xml-Datei, also eine reine Textdatei. Öffnen wir diese Datei mit einem Editor, am Besten einem, der xml highlighten kann, z.B. Notepad++. Wir sehen eine Textdatei mit vielen spitzen Klammern, so ähnlich wie HTML. Suchen wir nach den Zeilen, die mit beginnen. Das sind die Zeilen, die uns interessieren. Denn zu ihnen müssen wir jeweils eine passende Callback-Funktion schreiben.

Die ersten Zeilen eines GTK-Programms, das Glade-Dateien verarbeitet, müssen immer so aussehen:


#INCLUDE "gtk/gtk.bi"
#INCLUDE "gtk/libglade/glade-xml.bi"

#ifndef NULL
#define NULL 0
#endif

DIM SHARED xml AS GladeXML PTR
DIM SHARED AS GtkWidget PTR toplevel

Das erste include lädt die Bindungen an die GTK-Libraries. Das zweite verbindet unser Programm mit der GTK-xml-Schnittstelle. Und die ist eine ziemlich geniale Sache. Sie erlaubt es Glade, seine ganzen Ergebnisse in einer einzigen und noch dazuhin recht übersichtlichen xml-Datei zu speichern. D.h., wir können jederzeit auch ohne Glade weiter an der Oberfläche basteln. Glade-xml liest dann zur Laufzeit die xml-Datei und generiert daraus die notwendigen Widgets - eine explizite Deklaration aller Buttons, Scrollbalken etc, wie sie in anderen Programmierschnittstellen notwendig sind, entfällt. Uns bleibt die Konzentration auf das Wesentliche: Die Signale zu verarbeiten.

Als nächstes kommen zwei globale Variablen. Nicht schön, aber notwendig. Der Zeiger xml ist unser Zugang zur xml-Verwaltung unseres GUIs. Und diese wiederum verschafft uns Zugang zu allen Widgets. Wir haben ja ansonsten in unserem Programm keine individuellen Objekte definiert - den Ballast hat uns lib-glade erspart. Toplevel hat eine ähnliche Funktion, falls wir zeichnen und malen möchten. Dazu müssen wir auf die "unter" den Widgets liegende Datenstruktur zugreifen, die die Elemente des Fensters enthält - u.a. auch die Pixelbuffer, die wir dann manipulieren wollen. Aber dazu später mehr.

Als nächstes deklarieren wir alle Callback-Funktionen. In unserem Fall sind es zwei:

DECLARE SUB on_button1_clicked                        CDECL ALIAS "on_button1_clicked" (BYVAL object AS GtkObject  PTR, BYVAL user_data AS gpointer)


DECLARE SUB on_mainwindow_destroy                   CDECL ALIAS "on_mainwindow_destroy" (BYVAL menuitem AS GtkMenuItem  PTR, BYVAL user_data AS gpointer)

Sieht auf den ersten Blick kompliziert aus, ist es aber nicht. Etwas neu ist das "cdecl alias...". cdecl sagt lediglich, dass die Argumente intern wie bei der Programmiersprache C an die Library-Funktionen übergeben werden müssen. Das muss in jede Callback-Funktion rein. Der alias identifiziert die Callback-Funktion, die das GTK-Hauptprogramm sucht, mit derjenigen, die wir dafür in unserem Programm vorsehen. Die Argumente sind je nach Signal und Widget ein bisschen unterschiedlich, fast immer haben sie jedoch einen "user_data"-Zeiger dabei. Der war vor glade-xml einmal sehr wichtig, denn er ermöglichte es erst, innerhalb unseres Callback-Aufrufs Informationen zum Zustand des GUI's zu diesem Zeitpunkt zu verarbeiten. Heute brauchen wir ihn kaum noch, da uns die xml-Schnittstelle jedes gewünschte Objekt aus der Oberfläche über seinen Namen liefert. Die restlichen Argumente brauchen uns ohnehin nicht zu beunruhigen - meist müssen wir nichts darüber wissen.

Unverzichtbar: Das GTK-Reference-Manual

Woher weiss ich aber, wie die Callback-Funktion zu einem bestimmten Widget und Signal überhaupt aussehen muss? Guter Punkt. Aber lösbar. Wir gehen ins GTK+ Reference Manual. Da finden wir ne Menge Informationen zu GTK, im 3. Kapitel eine Übersicht über alle Widgets. Gehen wir z.B. mal auf "GtkButton". Unter "Signals" sehen wir nochmals die ganzen Signale, die ein GtkButton verarbeiten kann (keinesfalls nur "clicked"!). Folgen wir dem Link zu "clicked" wird die Callback-Funktion (hier user_function) genannt, angezeigt. Et voila...

Und weiter geht's. Mit dem Deklarieren ist es ja nicht getan. Jetzt müssen wir auch noch einen Inhalt schreiben. Den machen wir jetzt maximal einfach:

' ------------------------------------------------------------------------------------------------------------

SUB on_button1_clicked                        CDECL  (BYVAL object AS GtkObject  PTR, BYVAL user_data AS gpointer) EXPORT

  ? "on_Button1_clicked"

END SUB


' ------------------------------------------------------------------------------------------------------------

SUB on_mainwindow_destroy CDECL (BYVAL menuitem AS GtkMenuItem  PTR, BYVAL user_data AS gpointer) EXPORT

  ? "...Peng, wir schliessen..."
  SLEEP
  gtk_main_quit()

END SUB

Sehr wichtig: VERGISS NIE DAS "EXPORT"!

Das ist so ein richtig haariger Fehler, der einen halbe Tage kosten kann. GTK findet seine Callback-Funktion nicht und man starrt auf seinen Programmtext und versteht nicht, warum dieses elende GTK so blind ist...

Und nun? Wieso können wir hier einfach einen Basic-Print-Befehl abschicken? Nun, wenn wir ganz normal kompilieren, haben wir weiterhin auch ein Konsolenfenster zur Verfügung. Mehr als das. Wir können auch ein klassisches gfx-Fenster öffnen und mit unseren normalen Befehlen dort Grafiken produzieren. Das GUI kriegen wir zusätzlich, nicht anstatt der bisherigen Schnittstelle. Das ist sehr praktisch. Auch deswegen, weil wir über das Konsolenfenster jederzeit ein GTK-Programm mit Ctrl-C abbrechen können, auch wenn wir dummerweise die Signalverarbeitung im GUI völlig abgewürgt haben. Aber wir können auch jederzeit in die Konsole loggen oder zum Testen mit sleeps den Programmablauf anhalten usw.

Jetzt fehlt noch das Hauptprogramm. Das muss nichts anderes tun, als den GTK-Apperat in Gang zu setzen - und ihm die Kontrolle übergeben. Das eigentliche Hauptprogramm läuft ja dann innerhalb der GTK-Lib-Routinen ab.

' ----------------------------------------------------------------------------

gtk_init( NULL, NULL )
xml = glade_xml_new( "test1.glade", NULL, NULL )
toplevel = glade_xml_get_widget( xml, "mainwindow" )
gtk_widget_show_all( toplevel )
glade_xml_signal_autoconnect( xml )
gtk_main( )


g_object_unref( xml )
end

Diese Zeilen kann man bei jedem GTK-Programm unverändert lassen. Lediglich der Verweis auf die richtige Glade-Datei muss natürlich ausgetauscht werden.

(test3.exe:1336): Gtk-CRITICAL **: gtk_widget_show_all: assertion `GTK_IS_WIDGET (widget)' failed

Haben Sie so eine Fehlermeldung bekommen? Und kein GUI ist erschienen? Keine Panik. Es bedeutet nicht, dass nun irgendetwas tief in den Eingeweiden des Betriebssystems nicht mit GTK zusammenspielt. Es bedeutet vielmehr höchstwahrscheinlich, dass Sie sich bei der Benennung irgendeiner Funktion oder insbesondere des Toplevel-Widgets vertan haben. Steht in glade_xml_get_widget() statt "mainwindow" z.B. "nainwindow", dann bekommen Sie diese Fehlermeldung.

Erwähnt werden sollte noch die Zeile

glade_xml_signal_autoconnect( xml )

Sie sieht total harmlos aus. Aber Sie glauben gar nicht, wieviel hakelige Arbeit Sie Ihnen erspart. Bevor es die xml-Schnittstelle gab, musste man nämlich die Signale von Hand an die entsprechenden Wigdets binden. Das sah dann z.B. so aus:

gtk_signal_connect( GTK_OBJECT(toplevel), "destroy", GTK_SIGNAL_FUNC(@on_mainwindow_destroy), 0 )       

Das war natürlich eine nette potenzielle Fehlerquelle. Vergessen wir's!

Erste Schritte: Das Ergebnis

Die Kompilierung erfolgt ganz normal, ohne Compilerzusätze:

fbc test1.bas

Sie dauert allerdings etwas länger. Die Ausführung sollte dann so aussehen:

Beim Klicken auf den Knopf sollte im Konsolenfenster die Rückmeldung kommen.

Erste Schritte: Auf Widgets zugreifen

Wir wollen nun Text in unser Textausgabefenster schreiben. Das Widget heisst "textview1". In Glade. Das hilft uns nicht viel. Da das GUI zur Laufzeit aufgebaut wird, gibt es kein Objekt namens "textview1", das wir in einem Objektbaum ansprechen könnten. Aber es gibt unseren xml-Zeiger. Und es gibt die Methode glade_xml_get_widget( xml, <widgetname> ). Mit ihr können wir einen Zeiger auf jedes Widget bekommen, das wir ins GUI eingebaut haben.

DIM AS GtkWidget PTR textview1

  textview1=glade_xml_get_widget( xml, "textview1" )

Das Textview-Widget

Als nächstes schauen wir mal in unserer GTK-Referenz nach dem Wigdet "GtkTextview". Googeln hilft an dieser Stelle zusätzlich. Und nach etwas Lesen stellen wir fest, dass wir nicht einfach einen String ins Textview-Fenster schreiben können. Textview will ein anderes Objekt haben, ein GtkTextBuffer-Objekt. Das ist weitaus mehr als ein einfacher String. Man kann da Lesezeichen (Tags) setzen, hat eine Cursorposition usw. Jeder Textview hat einen solchen Textbuffer als Eigenschaft. Wie bekommen wir den entsprechenden Zeiger drauf? Mit gtk_text_view_get_buffer (). Als Argument benötigt die Routine den entsprechenden Textview. Den Zeiger haben wir allerdings nur als allgemeinen GtkWidget-Zeiger. Daher müssen wir diesen noch in einen spezifischen GtkTextView-Zeiger umwandeln. Das Ganze sieht dann so aus:

buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW(textview1))

Jetzt wollen wir noch einen String in den Buffer bekommen. Nach ein bisschen Stöbern im Reference Manual unter GtkTextBuffer erscheinen dafür die beiden Methoden gtk_text_buffer_set_text() und gtk_text_buffer_insert_at_cursor(). Das Prinzip der letzteren ist, dass Text an der Stelle einer (logischen) Cursorposition eingefügt und der Cursor dann ans Ende des eingefügten Textes gestellt wird. Auf diese Art und Weise können wir also Text im "Append"-Modus ausgeben.

Wir wollen nun eine ultraeinfache Anwendung schreiben: Bei jedem Mausklick wird ein Zähler um 2 hochgesetzt und angezeigt. Die Frage ist: Wo deklarieren wir den Zähler? Nun, was immer geht: Eine globale Klasse definieren und dort die eigenen Daten unterbringen. In grösseren Programmen kann in dieser globalen Klasse der eigene Objektbaum verankert werden. In unserem Beispiel besteht er aus einer einzigen Integer-Variablen "sum". Hier unsere ausgebaute Methode button1_clicked:

'Globale Deklaration von mydata

type tmydata
  sum AS INTEGER
END type

DIM SHARED AS tmydata mydata


'(...)

SUB on_button1_clicked                        CDECL  (BYVAL object AS GtkObject  PTR, BYVAL user_data AS gpointer) EXPORT

  ? "on_Button1_clicked"

  DIM AS GtkTextBuffer PTR buffer
  DIM AS GtkWidget PTR textview1

  textview1=glade_xml_get_widget( xml, "textview1" )

  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW(textview1))

  gtk_text_buffer_insert_at_cursor(buffer, "Hello, current sum is "+STR(mydata.sum)+CHR(10), -1)
  mydata.sum=mydata.sum+2
  'gtk_text_buffer_set_text (buffer, "Hello, this is some text", -1)

END SUB

'(...)

'In main mydata initialisieren!

...und das Ergebnis:

Eine Scrollbar für den Textbereich

Gehen wir den ganzen Ablauf der Programmierung eines Widgets anhand einer Scrollbar nochmals durch:

1. Element zeichnen

Wir gehen nach Glade zurück und zeichnen eine Scrollbar neben unser Textfeld. Das muss noch nicht völlig exakt sein - wir können die genaue Höhe und Breite dann in der xml-Datei anpassen.

2. Signale

Wir gehen ins Eigenschaftfenster, lassen die allgemeinen Eigenschaften und betrachten die Signale. Klar ist, dass die Scrollbar ein Signal aussendet, wenn sich ihr Wert ändert. Welches Signal ist das? Dummerweise gibt's da zwei Kandidaten: value_changed() und change_value(). Bevor wir hier rumexperimentieren, sollten wir das im Reference-Manual nachlesen.

Gehen wir allerdings nach "GtkVscrollbar", dann sehen wir dort nicht viel von Signalen. In diesem Fall ist das Widget aus einem allgemeineren Widget abgeleitet. Wie der Vererbungsbaum ausschaut, ist oben auf der Seite immer sehr schön abgebildet:

Wenn wir die Signale (oder eine Eigenschaft oder Methode) nicht im speziellen Widget finden, sollten wir bei den Eltern und Grosseltern nachschauen. Bei der "Mama" GtkScrollbar finden wir auch nichts. Also gehen wir zur "Grossmama" GtkRange. Und siehe da: Dort sind unsere Signale. Na ja, hätten wir auch gleich sehen können: Schliesslich werden die Signale in Glade auch unter dem Punkt "GtkRange" und nicht "GtkVScrollbar" angezeigt...

change_value() klingt gut: Hier bekommen wir durch GTK den neuen Wert geliefert und haben selbst die Möglichkeit, durch die Rückgabe eines Booleans die automatische Weiterverarbeitung des Scrolls durch GTK zu ermöglichen (false) oder zu verhindern (true).

Komische Datentypen

Dass GTK einige eigene komplexe Datentypen mitbringt, ist OK, aber was soll ein "gint" oder "gdouble" sein? Ihre Benutzung ist nicht zwingend, aber es kann nützlich sein. GTK ist eine Plattform, die auf vielen Betriebssystemen läuft, tw. 32-Bit, tw. 64-Bit. Benutzen wir in unserem Programm nur Datentypen und Komponenten von GTK, dann stellen wir sicher, dass der Quelltext sich mit jedem Freebasic-Compiler auf jedem Betriebssystem kompilieren lässt und das Programm dann dort auch läuft. Z.B. könnte es sein, dass bei Freebasic auf einem 64-Bit-Unix die integer ebenfalls 64-Bit sind. Das ergäbe dann ev. Probleme in Zusammenarbeit mit dem dortigen GTK+. Die Nutzung von gint verhindert dies.

Nun fügen wir unseren Scrollbar-Signal-Handler hinzu:

DECLARE SUB on_vscrollbar1_change_value           CDECL ALIAS "on_vscrollbar1_change_value" (BYVAL range AS GtkRange PTR, BYVAL scroll AS GtkScrollType, BYVAL value AS gdouble, BYVAL user_data AS gpointer)

' ------------------------------------------------------------------------------------------------------------

SUB on_vscrollbar1_change_value (BYVAL range AS GtkRange PTR, BYVAL scroll AS GtkScrollType, BYVAL value AS gdouble, BYVAL user_data AS gpointer) EXPORT

  textview1=glade_xml_get_widget( xml, "textview1" )
  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW(textview1))
  'Schreibe Value ins Textfenster und auch in die Konsole:
  gtk_text_buffer_set_text (buffer, STR(v), -1)
  ? "Value: ",v


END SUB

Das sollte dann so aussehen:

Fertig. Na ja, zumindest, was das reine Widget anbelangt. An sich fängt die Arbeit jetzt erst an: Wahrscheinlich wollen wir ja mit dem Scrollwert etwas anstellen - naheliegender Weise den Text im Textview scrollen. Diese Verbindung zwischen Scrollwert und Textbuffer herzustellen, das bleibt uns überlassen. Und da müssen wir uns in die Materie des GtkTextBuffers und GtkTextviews etwas einlesen.

Vielleicht haben Sie auch folgendes herausgefunden: Unser Textview kennt eine Methode gtk_text_view_scroll_to_iter(). Die scrollt zu einem - was zum Teufel ist ein "Iter"? Ein Text-Iterator. Eine Art unsichtbarer Cursor. Einfach ein Speicher für eine Position in buffer. Im Prinzip könnte das eine Zeichenzahl sein "55. Zeichen", "167. Zeichen". Es ist ein bisschen mehr, es ist ein Objekt, GtkTextIter. Und mit seinen Methoden können wir im Handumdrehen einen komfortablen Editor programmieren: Hüpfen von Wort zu Wort, von Satz zu Satz, Abschnitt zu Abschnitt - alles dort eingebaut. Wir müssen nur etwas ganz ordinäres machen: Zu einer bestimmten Zeile hüpfen. Den Iter einer bestimmten Zeile bekommen wir mit gtk_text_buffer_get_iter_at_line().

Die Frage ist allerdings, was unser Scrollwert mit der Zeilenposition zu tun hat. Unser Scrollwert geht von 0 bis 100. Na ja, wenn wir die Zeilenlänge des Buffers hätten, könnten wir das umrechnen. Haben wir: gtk_text_buffer_get_line_count(). Und schon ist das ganze Scrollen fertig:

FUNCTION on_vscrollbar1_change_value CDECL (BYVAL r AS GtkRange PTR, BYVAL s AS GtkScrollType, BYVAL v AS gdouble, BYVAL user_data AS gpointer) AS gboolean EXPORT

  DIM AS GtkTextBuffer PTR buffer
  DIM AS GtkWidget PTR textview1
  DIM AS GtkTextIter it
  DIM AS gint nline,posline

  'Get textview handle
  textview1=glade_xml_get_widget( xml, "textview1" )
  'Get textview textbuffer handle
  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW(textview1))

  'Count line numbers in text buffer
  nline=gtk_text_buffer_get_line_count(buffer)
  'Calculate to which line the scroll value points to
  posline=INT(v/100*nline+0.5)
  'Funny alternative: Scrolling makes the text oscillating in the textview
  'posline=int((sin(v/100*9)+1)/2*nline+0.5)
  ? "Value: ";v,"nline: ";nline,"posline: ";posline
  'Get the iter for the line position
  gtk_text_buffer_get_iter_at_line (buffer, @it, posline)
  '...and  scroll to this iter:
  gtk_text_view_scroll_to_iter(GTK_TEXT_VIEW(textview1),@it,0.4,FALSE,0.5,0)

  RETURN false

END FUNCTION

Testen sollte man das mit einen grösseren Text, den man sich durch Klick auf den Button holen kann. Wenn man in die Click-Event-Methode noch eine Schleife einbaut muss man nicht 100-mal klicken...

Ein Eingabefeld und Tastendrücke

Übung zum Eingabefeld:

Ziel sei es nun, den Startwert unserer Summation vom User eingeben zu lassen. Suchen Sie ein Eingabe-Widget und erweitern Sie damit unser Programm.

Meine Lösung:

Ich habe ein Label und ein GtkEntry-Widget eingefügt. Dann Vorsicht Falle: Das Entry-Feld braucht nicht unbedingt eine Signalanbindung. Es reicht, den aktuell eingestellten Wert bei der Verarbeitung des Button-Clicks auszuwerten:

dim as zstring ptr sz
entry1=glade_xml_get_widget( xml, "entry1" )
sz=gtk_entry_get_text(GTK_ENTRY(entry1))
? "This text was retrieved:",*sz
mydata.sum=val(*sz)

Hier sehen wir noch eine Eigenart der Freebasic-Anbindung von GTK: Strings werden keineswegs durch die GTK-eigenen gchar-Pointer vertreten, sondern durch die freebasic-eigenen zstring-Pointer. Das ist ganz angenehm, da hier das ein oder andere Casting erspart wird.

Tastaturbedienung:

Allerdings sollte ein Textfeld auch auf bestimmte Tastendrücke reagieren. Z.B. sorgt das Drücken der Enter-Taste in der Regel, dass das Ergebnis des Eingabefeldes "abgeschickt" wird - ersetzt also in unserem Fall einen Button-Click. Das Drücken der Tab-Taste schickt den Fokus ein Eingabefeld weiter. Usw.. Wie können wir das realisieren?

Wir konzentrieren uns auf die Verarbeitung der Enter-Taste. Zunächst ist es ratsam, alle Inhalte unserer bisherigen on_button1_click()-Methode in ein eigenes sub auszulagern, da sie ja jetzt vom Button und Entry-Feld gleichermassen benutzt werden.

Die Abfrage der Tastatur finden wir auf der GtkWidget-Signal-Ebene unseres GtkEntry's. Jedem GtkWidget kann also eine Tastaturverarbeitung zugeordnet werden:

FUNCTION on_entry1_key_press_event           CDECL (BYVAL widget AS GtkWidget PTR, BYVAL event AS GdkEventKey PTR, BYVAL user_data AS gpointer) AS gboolean EXPORT

Sobald innerhalb des Widgets eine Taste gedrückt wird, landen wir dann hier. Die Frage ist nur, welche Taste. Das liefert uns der event-Zeiger vom Typ GdkEventKey. Gdk? Schreibfehler? Soll das nicht Gtk heissen? Nicht ganz. Die Gtk-Lib nutzt eine Reihe von "primitiveren" Libs, um seine ganzen Widgets zu definieren. Die Bibliothek, die die einfachen grafischen Befehle und Formen liefert, heisst "Gdk" = "Graphics Drawing Kit". Damit kann man Linien und Vierecke zeichnen - und sie kümmert sich auch um Maus und Tastatur, ist also so etwas wie die Gfx-Lib.

Auch zur Gdk gibt es ein Reference-Manual und dort erfahren wir, wie GdkEventKey aufgebaut ist:

typedef struct {
  GdkEventType type;
  GdkWindow *window;
  gint8 send_event;
  guint32 time;
  guint state;
  guint keyval;
  gint length;
  gchar *string;
  guint16 hardware_keycode;
  guint8 group;
  guint is_modifier : 1;
} GdkEventKey;

Es ist also eine Verbundvariable und keyval gibt uns den Tastencode zurück. Die Sondertasten (Shift, Alt, Strg) können wir mit state abfragen. Das Ganze zusammen ist dann ein Ersatz für Multikey (das ja nicht mehr funktioniert, da wir zur eigentlichen Hauptschleife in Gtk keinen Zugang haben.) Die Frage ist: Welche Tastaturcodes zu welcher Taste? Einfach Ausprobieren!

FUNCTION on_entry1_key_press_event           CDECL (BYVAL widget AS GtkWidget PTR, BYVAL event AS GdkEventKey PTR, BYVAL user_data AS gpointer) AS gboolean EXPORT

  ? "Key pressed",event->keyval,event->string

RETURN false

END FUNCTION

Wir sehen, dass jede Taste zwei Bytes zurückgibt. Massgeblich ist aber nur das zweite Byte.

Jetzt müssten wir alles haben. Wir fragen ab, ob im Textfeld die ENTER-Taste gedrückt wurde:

FUNCTION on_entry1_key_press_event           CDECL (BYVAL widget AS GtkWidget PTR, BYVAL event AS GdkEventKey PTR, BYVAL user_data AS gpointer) AS gboolean EXPORT

  'UTF-8-coded, take 2nd byte:
  IF event->keyval MOD &h100 = 13 THEN textview1_produce

  RETURN false

END FUNCTION

Springen unsere neue Prozedur an und lesen dort wie oben den Wert des Eingabefeldes aus und füllen unseren Textview. Fertig.

Menüs

Eine Menüleiste ist auch nur ein Widget. Wir finden sie unter den Containern - das Symbol, auf dem "File" steht. Bringen wir es in unser Hauptfenster und machen wir es mind. 400 Punkte breit, dann sehen wir, dass unsere Leiste keineswegs leer ist. Wir haben schon Standard-Menüs drin für "Datei", "Bearbeiten", "Ansicht" und "Hilfe". Das ist mehr als nur ein Vorschlag. Das ist eine Empfehlung. Wir sind als Nutzer froh, bekannte Menüs wiederzufinden und uns schnell orientieren zu können. Daher sollten wir als Programmierer genau prüfen, was wir von den Standardfunktionen "Datei öffnen", "Datei speichern" etc. besetzen sollten und was nicht.

Wenn die Menüleiste drin ist, speichern Sie Ihr Glade-Projekt einmal ab. Dann nehmen Sie Ihr bisheriges Programm, kompilieren und starten es (die xml-Datei muss natürlich auf das neue Glade-Projekt verweisen). Und siehe da - schon verfügt Ihr Programm über eine vollständige Menüleiste. Man kann die Menüs auch ausklappen und anklicken. Nur passiert dann nichts - klar, was sollte auch passieren?

Die gesamte Menüleiste ist ein ganzer Baum von Widgets. Rechts oben im Widget-Browser können wir ihn am Besten durchforsten. Gehen wir zu dem GtkImageMenuItem "Speicher unter". Dort gibt es ein activate-Signal. Das müssen wir besetzen. Die Frage ist nur: Was soll da rein?

Grössere Dialoge

Ganz einfach: Wir brauchen ein komplett neues Fenster. Ein Sub-Fenster, in dem der Name eingetragen wird. Aber nicht nur das: Normalerweise bekommen wir hier die Möglichkeit, den Verzeichnisbaum zu durchwandern, ggf. sogar ein neues Verzeichnis anzulegen, das Laufwerk zu wechseln. Puh. Da haben wir wohl ein ganzes Stück Arbeit vor uns, oder?

Nö. Das alles nimmt uns ein vernünftiges Toolkit auf einen Schlag ab. Das Ganze heisst "GtkFileChooserDialog" und wir finden ihn unter "oberste Ebenen" in Glade. Führen wir darauf einen Doppelklick aus, verschwindet unser Hauptfenster und wir bekommen ein neues Form. Das aber ist schon ganz gut mit Elementen gefüllt:

Es ist ein fertiger Dateiauswahl-Dialog. Nur die Aktionsbuttons ("OK", "Abbrechen") und das Feld für den Dateinamen fehlen. Darum kümmern wir uns erstmal nicht. Was uns eher Sorge macht: Wo ist unser Hauptfenster jetzt hin? Einfach im Widget-Browser oben rechts auf "mainwindow" doppelklicken.

Was müssen wir tun, damit der Dialog aufpoppt, wenn wir auf "Speichern unter" klicken? In der Referenz sehen wir, dass unser FileChooser-Dialog ein Kind von GtkDialog ist. Und dieser wiederum hat die Methode gtk_dialog_run()Die ist unser Freund. Sie fügen wir in unseren Signal-Handler ein:

SUB on_saveasitem_activate CDECL (BYVAL menuitem AS GtkMenuItem PTR, BYVAL user_data AS gpointer) EXPORT

  DIM AS GtkWidget PTR fcdialog

  fcdialog = glade_xml_get_widget( xml, "filechooserdialog1" )

  gtk_dialog_run(GTK_DIALOG(fcdialog))

END SUB

Damit wäre der Rohbau fertig. Was noch fehlt, kann leicht mit dem Gelernten umgesetzt werden: Ein Entry-Feld, ein OK-Button, ein Abbrechen-Button. Ah, Abbrechen!

Dialoge sind Input-Funktionen. Das sollte man nie vergessen. Deshalb ist es wichtig, in jedem Fall einen Funktionswert zurückzuliefern. Der obige Aufruf von gtk_dialog_run() als Sub ist also tief suboptimal. Das steht übrigens auch in der Beschreibung von gtk_dialog_run() im Reference-Manual. Grundsätzlich kann man jedes Widget mittels gtk_widget_destroy() verschwinden lassen. Das müssen wir z.B. in den Signal-Handlern der Aktionsbuttons: Nachdem draufgeklickt wurde, soll der Speichern-Dialog verschwinden. Wir sollten jedoch an jeder solchen destroy-Stelle vorher der Dialogfunktion einen Wert zuweisen. Das machen wir mit gtk_dialog_response(). Das Ganze sieht dann anhand des Beispiels eines Cancel-Buttons so aus:

SUB on_saveasitem_activate CDECL (BYVAL menuitem AS GtkMenuItem PTR, BYVAL user_data AS gpointer) EXPORT

  DIM AS GtkWidget PTR fcdialog
  DIM AS gint resp

  fcdialog = glade_xml_get_widget( xml, "filechooserdialog1" )

  resp=gtk_dialog_run(GTK_DIALOG(fcdialog))
  ? "on_saveasitem_activate:",resp

END SUB

' ------------------------------------------------------------------------------------------------------------

SUB on_cancel_save_dialog_clicked                        CDECL (BYVAL object AS GtkObject  PTR, BYVAL user_data AS gpointer) EXPORT

  DIM AS GtkWidget PTR fcdialog

  fcdialog = glade_xml_get_widget( xml, "filechooserdialog1" )
  ? "on_cancel_save_dialog_clicked"

  gtk_dialog_response(GTK_DIALOG(fcdialog),2)
  gtk_widget_destroy(fcdialog)

END SUB

Kleinere Standard-Dialoge

Oft gibt es nur kleine Dialoge, die eine kleine Nachricht ausgeben plus OK-Button oder eine Ja/Nein-Frage. Dazu braucht man nicht mühsam neue Forms zu entwickeln. Mit gtk_message_dialog_new() kann man solche Standarddialoge mit einer Programmzeile aufrufen. Ein Beispiel wäre eine Speichernabfrage beim Verlassen des Programms:


SUB on_mainwindow_destroy CDECL (BYVAL menuitem AS GtkMenuItem  PTR, BYVAL user_data AS gpointer) EXPORT

  DIM AS GtkWidget PTR dialog
  DIM AS gint res

  dialog = gtk_message_dialog_new (0, _
                                  GTK_DIALOG_DESTROY_WITH_PARENT, _
                                  GTK_MESSAGE_QUESTION, _
                                  GTK_BUTTONS_YES_NO, _
                                  "Speichern?",0)

  res=gtk_dialog_run (GTK_DIALOG(dialog))
  'if (res=GTK_RESPONSE_YES) then ...

  ? "...Peng, wir schliessen..."
  'Muss nicht sein:
  gtk_widget_destroy(GDK_WIGDET(dialog))

  gtk_main_quit()

END SUB

Wichtig an gtk_message_dialog_new() sind die Argumente 3 bis 5:

Regelmässige Ereignisse, eigenständige Abläufe

Eventuell haben Sie das alles hier bisher durchgelesen und sind zunehmend verärgert, weil etwas, was bisher ganz einfach war, hier überhaupt nicht mehr seinen Platz findet: Der regelmässige Aufruf von Routinen aus einer Endlosschleife heraus. Das Herzstück jeder kontinuierlichen Aktivität in einem Programm, z.B. das Entstehen einer Grafik, die Bewegung eines Sprites usw. Wir haben keinen Zugriff mehr auf die Hauptschleife und können nur noch auf Tastendrücke und Scrolls reagieren - wie wollen wir da z.B. ein Spiel programmieren?

Den Ausweg nur mit der Gtk-Dokumentation herauszufinden, ist etwas mühselig. Denn er besteht in einem ganz kleinen Handgriff. Und er hat kein eigenes Widget zugeordnet. Ergo ist er in der Widget-Übersicht nicht zu finden.

Kurz und bündige Lösung: Wir müssen einen timeout programmieren. Und diese Timeouts finden wir im Reference-Manual ganz oben unter "Main Loop and Events". Aha, es gibt also einen Abschnitt, bei dem wir Infos zu Eingriffen in die Hauptschleife bekommen!

Der Aufruf lautet:

dim as guint timeout1id
timeout1id=g_timeout_add(200,@timeout_callback,NULL)

200 steht für die Anzahl Millisekunden, die der Timeout warten soll, bis er das nächste Mal die Callback-Funktion aufruft. Selbige ist wie folgt zu deklarieren:

DECLARE FUNCTION timeout_callback                       CDECL (BYVAL user_data AS gpointer) AS gboolean

Was wir dann innerhalb dieser Function machen, ist unsere Sache. Der Rückgabewert ist allerdings nicht egal: Mit FALSE schalten wir den Timeout ab. Wenn wir Bedarf daran haben, können wir sogar mehrere regelmässige Ereignisse, sprich Timeouts, verwenden. Dafür ist die timeoutid, die von g_timeout_add() zurückgegeben wird. Sie brauchen wir, wenn wir diesen Timeout wieder stoppen wollen. Das Stoppen funktioniert über:

g_source_remove(timeoutid)

Zu beachten: Es ist sehr zu empfehlen, in die on_mainwindow_destroy()-Routine gleich als erstes das g_source_remove() einzufügen. Zu diesem Zeitpunkt sind nämlich schon alle sichtbaren Widgets zerstört, der Timeout läuft aber immer noch, mit dem Ergebnis, dass die Timeout-Routine auf Widgets zugreifen möchte, die gar nicht mehr da ist - was zum Glück zunächst keinen harten Crash verursacht, aber unschöne Fehlermeldungen auf der Konsole. Was passiert, wenn hier geschludert und während des Timeouts auf dem Heap operiert wird, will ich lieber nicht wissen...

Übung:

Programmieren Sie unser kleines Testprogramm hier so um, dass es sich den Wert aus dem Entry-Feld schnappt und aller 200ms von selbst um zwei hochzählt und den Wert ins Textfeld schreibt - und gleich ans Ende scrollt, damit man die Werte auch sieht. Drückt man auf den Button, stoppt die Berechnung.

Und die Erweiterung: Wenn man einen neuen Wert ins Entryfeld schreibt, soll es diesen Wert im laufenden Betrieb übernehmen.

Kurze Skizze meiner Lösung:

Die Timeout-Funktion erledigt nur zwei Aufgaben: Sie ruft textview_produce() auf, dann eine neue Routine textview1_showtail().textview_produce() ist etwas modifiziert: Der Buffer wird nicht jedesmal gelöscht und es wird jeweils nur ein Wert hochgezählt. textview1_showtail() ist eine naheliegende Abwandlung unserer Scroll-Routine.

Um die Sache mit dem Entryfeld anzugehen, habe ich in mydata zwei weitere Variablen aufgenommen: start1 als Boolean und startvalue als integer. Gleich zu Anfang des Hauptprogramms wird start1 auf true gesetzt. Die entscheidende Passage in textview_produce() ist dann:

IF mydata.STOP1 THEN EXIT SUB

  textview1=glade_xml_get_widget( xml, "textview1" )
  entry1=glade_xml_get_widget( xml, "entry1" )
  sz=gtk_entry_get_text(GTK_ENTRY(entry1))

  IF mydata.start1 THEN
    mydata.sum=VAL(*sz)
    mydata.startvalue=mydata.sum
    mydata.start1=false
  END IF

  IF mydata.startvalue<>val(*sz) THEN
    mydata.startvalue=VAL(*sz)
    mydata.sum=mydata.startvalue
  END IF

Das funktioniert allerdings nur halb gut: Leider reagiert das Programm auf diese Weise zu schnell und setzt schon dann den Startwert neu, wenn die erste Ziffer im Entryfeld sich ändert. Besser wäre es, dass der effektive Wert erst dann aktualisiert wird, wenn die Eingabe mit einem ENTER abgeschlossen ist. Das umzusetzen, ist nicht schwer: Man übernimmt den Entry-Wert in textview_produce() nicht direkt, sondern von einer Puffervariablen (ebenfalls in mydata deklariert). Diese wird erst in on_entry1_key_press_event() mit dem Entrywert abgeglichen.

Hier die Programmdateien: oma_gtk_timeoutex1.zip (15K)

Weitergabe von GTK-Programmen

Ein kurzes Wort noch zur Frage: "Wie gebe ich mein Programm weiter, wenn ich es mit Hilfe von GTK geschrieben habe?" Und die naheliegende Frage: Gibt es eine Möglichkeit, GTK in mein Programm statisch hineinzulinken, damit ich bei meinem Freund/Bekannten etc. die Installation von GTK nicht voraussetzen muss?

Nun, selbst wenn es möglich ist, vom statischen Linken würde ich dringend abraten. Es ist keine Art, 100-Zeilen-Programme weiterzugeben, die 10 MB auf der Platte zumüllen.

Stattdessen: Das Programm erstmal ohne GTK starten und prüfen, ob GTK installiert ist. Falls nicht, dann einen Text ausgeben (auf Windows: notepad aufrufen), in dem die Installation von GTK erklärt wird.

Ob GTK installiert ist oder nicht, sieht man leicht:

IF (LEN(ENVIRON("GTK_BASEPATH"))=0) THEN
  SHELL("notepad.exe gtk_install.txt")
  END
END IF

Der Installer für das reine Runtime-Environment umfasst übrigens ca. 7 MB, was deutlich unter dem Umfang einer Java- oder .NET-Engine liegt.

Im nächsten Kapitel werden wir auf Zeichenfunktionen, Bitmaps und Sprites mit GTK eingehen.