Copyright © 1995 by O'Reilly/International Thomson Verlag

Bitte denken Sie daran: Sie dürfen zwar die Online-Version ausdrucken, aber diesen Druck nicht fotokopieren oder verkaufen.

Wünschen Sie mehr Informationen zu der gedruckten Version des Buches "Linux - Wegweiser zur Installation & Konfiguration", dann klicken Sie hier.


Kapitel 6

Programmieren mit Tcl und Tk

Die Tool Command Language oder Tcl (»tickel« gesprochen) ist eine einfache, interpretierte Sprache, die in einigen Aspekten Ähnlichkeiten mit der Bourne-Shell und Perl aufweist. Der größte Vorteil von Tcl liegt darin, daß es sowohl erweiterbar ist als auch in andere Anwendungen eingebunden werden kann. Tcl ist besonders beliebt in Verbindung mit der Erweiterung Tk (Toolkit), die das wohl einfachste Interface zur Programmierung mit Fenstern bietet.

Mit »Erweiterung« ist gemeint, daß Sie der Sprache Tcl eigene Befehle hinzufügen können, indem Sie einfach ein paar C-Routinen schreiben. Mit »einbinden« ist gemeint, daß Sie ein C-Programm mit den Tcl-Libraries binden können, so daß dieses Programm den vollen Zugriff auf die Sprache Tcl erhält. Obwohl die meisten Tcl-Programme in Form von Skripts entstehen und von einem präkompilierten Tcl-Interpreter ausgeführt werden, haben Sie auch die Möglichkeit, die Interpreterroutinen in Ihre eigene Anwendung einzubinden.

Nehmen wir beispielsweise an, daß Sie einen Debugger schreiben wollen, der ähnlich wie gdb von der Befehlszeile aus gesteuert wird. Der Debugger würde sich mit einem Prompt melden und der Benutzer hätte die Möglichkeit, Befehle wie step und breakpoint einzugeben.

Solange die Syntax der Befehle für Ihren Debugger noch einfach ist, könnten Sie ohne Schwierigkeiten Ihre eigenen Routinen in C schreiben, die einen Befehl einlesen und verarbeiten. Dies wird allerdings schon wesentlich schwieriger, wenn der Benutzer auch in der Lage sein soll, Variablen, Makros, neue Funktionen usw. zu definieren.

Statt von Grund auf neue Routinen zu schreiben, könnten Sie ganz einfach einen Tcl-Interpreter in Ihren Debugger einbinden. Die Befehle, die der Benutzer eingibt, würden dann von den Interpreter-Routinen ausgewertet. Diese Routinen stehen in Form von C-Library-Funktionen zur Verfügung.

Die Sprache Tcl selbst enthält bereits viele, viele Befehle. Sie kennt Kontrollstrukturen wie while- und for-Schleifen, hat die Fähigkeit, Funktionen zu definieren, kennt Routinen zur Bearbeitung von Strings und Listen sowie arithmetischen Funktionen usw.

Zusätzlich zu diesen Tcl-eigenen Routinen müßte Ihr Debugger Befehle wie die bereits erwähnten step und breakpoint zur Verfügung stellen. Sie würden solche Befehle innerhalb Ihrer Anwendung in C schreiben und dem Tcl-Interpreter mitteilen, wie sie zu benutzen sind.

Damit kann Ihr Debugger auf den vollen Funktionsumfang von Tcl zugreifen. So könnte z.B. die Konfigurationsdatei des Debuggers aus einem einfachen Tcl-Skript bestehen. Innerhalb dieses Skripts hätte der Benutzer dann die Möglichkeit, neue Funktionen und Variablen zu definieren und könnte dabei die in Tcl enthaltenen Fähigkeiten nutzen.

Zu den vielen Erweiterungen von Tcl gehört Tk, das viele Befehle enthält, die Ihre Anwendung unter dem X Window System laufen lassen.

(Wir beschreiben X im Abschnitt » Das X Window System « in Kapitel 5 .) Es ist erstaunlich einfach, X-basierte Anwendungen als Tk-Skript zu schreiben. Die folgende Tcl/Tk-Anwendung z.B. zeigt ein Textfenster an, in das ein Dateiname eingegeben werden kann. Anschließend startet ein xterm mit einem vi -Prozeß darin, um die Datei zu editieren.

#!/usr/local/bin/wish -f

# Label-Widget namens .l erzeugen
label .l -text "Filename:"
# Eingabe-Widget namens .e erzeugen
entry .e -relief sunken -width 30 -textvariable fname

# Beide Widgets im Fenster der Anwendung plazieren
pack .l -side left
pack .e -side left -padx 1m -pady 1m

# Nach RETURN im Eingabefenster xterm starten
bind .e <Return> {
  exec xterm -e vi $fname
}

Wir werden gleich auf die Syntax des Skripts eingehen -- Sie können aber jetzt schon erkennen, daß wir in weniger als 20 Zeilen Code eine nicht-triviale X-Anwendung geschrieben haben. Wenn dieses Skript ausgeführt wird, sieht das Ergebnis aus wie Abbildung 6--1.

Abbildung 6-1. Von Tk erzeugtes, einfaches Fenster

Für solche Tcl-Anwendungen, die nur interne Tcl-Routinen benutzen, steht der präkompilierte Interpreter tclsh zur Verfügung. Dieser Interpreter liest einfach einen Tcl-Befehl nach dem anderen ein und führt ihn aus. Für unseren Debugger würden wir ein neues Programm erstellen, das dann mit den Library-Routinen des Tcl-Interpreters gebunden wird.

In ähnlicher Weise gibt es für Tk-Anwendungen, die nur die Standard-Tcl-Befehle und Tk-Widgets benutzen, den Interpreter wish (Window-Shell). Wie Sie sehen, wird das o.a. Skript von wish ausgeführt. Wenn Sie neue Tcl-Befehle und Tk-Widgets einführen wollten, könnten Sie ein C-Programm schreiben und mit den Tcl- und Tk-Bibliotheken binden. In diesem Abschnitt befassen wir uns mit der Erstellung von einfachen Tcl- und Tk-Skripts, die unter tclsh oder wish ausgeführt werden.

Ein Intensivkurs in Tcl

Die Sprache Tcl ist sehr einfach zu lernen. Wenn Sie überhaupt schon mit anderen Skript-Sprachen wie der Bourne- oder C-Shell vertraut sind, brauchen Sie sich vor Tcl/Tk nicht zu fürchten.

Deshalb werden wir nicht allzu viel Zeit auf die Sprache Tcl selbst verwenden. Sie ist überschaubar und kann mit Hilfe der verschiedenen Tcl-Manual-Pages oder John Ousterhouts hervorragendem Buch Tcl and the Tk Toolkit erlernt werden. Dieses Buch beschreibt nicht nur, wie Tcl- und Tk-Skripts geschrieben werden, sondern auch, wie Sie die Tcl/Tk-Libraries in Ihren eigenen Anwendungen einsetzen.

Lassen Sie uns mit einem einfachen Beispiel beginnen. Das folgende Tcl-Skript zählt die Anzahl der Zeilen in einer bestimmten Datei.

1  #!/usr/local/bin/tclsh -f
2
3  if {$argc != 1} {
4    error "lc < filename >"
5  }
6
7  set thefile [open [lindex $argv 0] r]
8  set count 0
9
10 while {[gets $thefile line] >= 0} {
11   set count [expr $count + 1]
12 }
13
14 puts "Read $count lines."

Die Zeilen 3 bis 5 sollen mit einer einfachen if-Anweisung sicherstellen, daß das Skript mit einem Argument aufgerufen wird -- nämlich dem Namen der Datei, in der die Zeilen gezählt werden sollen. Der Befehl if hat zwei Argumente -- einen Ausdruck und einen Codeblock, der ausgeführt wird, wenn der Ausdruck wahr ist. (Wie in C ist null = falsch; andere Werte erfüllen die Bedingung.)

Die beiden Argumente zum if-Befehl sind eingeklammert. Klammern werden einfach benutzt, um eine Gruppe von Wörtern (oder Zeilen) zu einem einzelnen Argument zusammenzufassen. Obwohl diese Syntax vielleicht an C oder Perl erinnert, ist die Art und Weise, in der Tcl die Befehle auswertet, doch recht einfach. So darf sich ein Befehlsargument (in diesem Fall der Code in den Zeilen 3 bis 5, der auch den Befehl error enthält), nur dann über mehr als eine Zeile erstrecken, wenn die öffnende Klammer am Ende einer Zeile steht. Wenn wir diese if-Anweisung als:

if {$argc != 1}
  { error "lc < filename >" }

geschrieben hätten, hätte Tcl folgende Fehlermeldung ausgegeben:

Error: wrong # args: no script following "$argc != 1" argument
wrong # args: no script following "$argc != 1" argument
    while executing
"if {$argc != 1} "
    (file "./lc.tcl" line 3)

Mit anderen Worten: Tcl weiß nicht, daß das zweite Argument zu if in der nächsten Zeile steht.

Der eigentliche if-Befehl in Zeile 4 ruft den Befehl error auf, um eine Fehlermeldung anzuzeigen und das Tcl-Skript zu beenden.

In Zeile 7 öffnen wir die Datei, deren Name als erstes Argument auf der Befehlszeile übergeben wurde, und weisen der Variablen thefile den resultierenden Dateizeiger zu. Der Befehl set wird benutzt, um einer Variablen einen Wert zuzuweisen. Das liegt daran, daß alle Tcl-Befehle mit dem Namen eines Befehls beginnen müssen; wir können die Variable a nicht auf 1 setzen, indem wir so etwas wie:

a = 1

eingeben, weil a der Name einer Variablen ist, und nicht der Name eines Befehls. Statt dessen schreiben wir:

set a 1

Wenn wir später auf den Wert der Variablen a zugreifen, werden wir $a schreiben.

Das erste Argument zu set ist der Name der Variablen, das zweite Argument ist der Wert. In diesem Fall haben wir:

set thefile [open [lindex $argv 0] r]

Eckige Klammern bezeichnen ein Subskript (untergeordnetes Skript), also Befehle, die in einen anderen Befehl eingebettet werden. Das Subskript wird ausgeführt und durch seinen Rückgabewert ersetzt.

Lassen Sie uns das Subskript:

open [lindex $argv 0] r

betrachten. Dieses Skript ruft den Befehl open auf, um die Datei zu öffnen, deren Name als erstes Argument genannt wird. Das zweite Argument, r, zeigt an, daß die Datei zum Lesen geöffnet wird.

Das erste Argument zu open ist das Subskript:

lindex $argv 0

Der Befehl lindex indiziert Listen und Arrays. In diesem Fall suchen wir das 0te Element des Arrays $argv, das die Befehlszeilenargumente des Programms enthält, aber nicht den Befehl selbst. (Hier ist der Gebrauch von argv anders als in C-Programmen.) Das 0te Element von $argv ist also das erste Argument auf der Befehlszeile.

Lassen Sie uns annehmen, daß wir unser Skript lc.tcl genannt haben und es mit:

eggplant$ lc.tcl /etc/passwd

aufrufen. Innerhalb des Befehls:

set thefile [open [lindex $argv 0] r]

wird deshalb das eingebettete Subskript:

open [lindex $argv 0] r

durch:

open "/etc/passwd" r

ersetzt, das wiederum durch den Wert des Dateizeigers ersetzt wird, der auf /etc /passwd verweist. Als Ergebnis nimmt die Variable thefile den Wert des Dateizeigers an.

In Zeile 8 setzen wir den Wert der Variablen count auf 0 -- das wird unser Zeilenzähler.

Die Zeilen 10 bis 12 enthalten eine einfache while-Schleife, die so lange Zeilen aus der Datei einliest, bis ein EOF (end of file; Dateiende) auftaucht.

while {[gets $thefile line] >= 0} {
   set count [expr $count + 1]
 }

Wie Sie sehen, hat der Befehl while zwei Argumente: eine Bedingung und einen Anweisungsblock, der ausgeführt wird, solange die Bedingung wahr ist. Die Schleifenbedingung lautet in diesem Fall:

[gets $thefile line] >= 0

Das darin enthaltene Subskript:

gets $thefile line

führt den Befehl gets aus. Dieser liest über den Dateizeiger $thefile eine einzelne Zeile und weist sie der Variablen line zu. gets gibt entweder die Anzahl der gelesenen Zeichen zurück oder -1, wenn EOF erreicht wird. Die while-Schleife wird also so lange Zeilen aus der Datei lesen, bis gets einen anderen Wert als null liefert.

Der Hauptteil der while-Schleife besteht aus:

set count [expr $count + 1]

womit der Wert von count erhöht wird. Erinnern Sie sich, daß Tcl-Anweisungen mit dem Namen eines Befehls eingeleitet werden müssen. Arithmetische Ausdrücke werden deshalb mit dem Befehl expr verarbeitet. Das Subskript:

expr $count + 1

liefert den Wert der Variablen count plus 1 zurück. Dies ist in Tcl die übliche Methode, Variablen hochzuzählen.

In Zeile 14 schließlich finden wir:

puts "Read $count lines."

Hier wird puts benutzt, um einen String auf der Standardausgabe anzuzeigen.

Rufen wir das Skript einmal auf:

eggplant$ lc.tcl /etc/passwd 
Read 144 lines.

Tk-Anwendungen schreiben

Schon mit den Tcl-Kenntnissen aus dem vorhergehenden Abschnitt sind Sie in der Lage, auch mit Tk Anwendungen zu schreiben -- der Tcl-Erweiterung für das X Window System. Tk ist im wesentlichen eine Sammlung von Tcl-Befehlen, mit denen X-Widgets erzeugt und bearbeitet werden -- etwa Schaltflächen (Buttons), Bildlaufleisten (Scrollbars), Menüs usw. Wir werden noch sehen, daß Tk ausgesprochen vielseitig ist und das Erzeugen von grafischen Bedienoberflächen unter X wesentlich vereinfacht.

In diesem Abschnitt werden wir Tk anhand einer einfachen Anwendung vorstellen, die dem Benutzer die Möglichkeit gibt, in einem »Leinwand«-Widget zu malen. Dieses Leinwand-Widget ist eine Art von Grafik-Widget, die viele Objekttypen wie z.B. Ovale, Linien, Text usw. unterstützt. In dieser Anwendung wollen wir das Leinwand-Widget interaktiv benutzen, um aus Ovalen und Rechtecken Grafiken zu erzeugen. Nach dem Start sieht diese Anwendung etwa so aus, wie es Abbildung 6--2. zeigt.

Abbildung 6-2. Fenster eines Tk-Programms

Lassen Sie uns einen Blick auf den Quellcode unserer Anwendung draw.tcl werfen:

#!/usr/local/bin/wish -f
# Globale Variablen; für Objekte und Positionen
set oval_count 0
set rect_count 0
set orig_x 0
set orig_y 0

Völlig problemlos -- wir initialisieren nur ein paar Variablen, die wir benutzen, um uns die erzeugten ovalen und rechteckigen Objekte mit ihrer Position zu merken.

Der nächste Abschnitt des Quellcodes sieht vielleicht eher abschreckend aus:

# Diese Prozedur aktiviert Ovale
proc set_oval {} {
  # Wir bekommen Zugriff auf diese globalen Variablen
  global oval_count orig_x orig_y

  # Wenn Taste-1 gedrückt wird, erzeuge ein Oval
  bind .c <ButtonPress-1> {
    set orig_x %x
    set orig_y %y
    set oval_count [expr $oval_count + 1]
    .c create oval %x %y %x %y -tags "oval$oval_count" -fill red
   }

  # Wenn wir Taste-1 ziehen, lösche und ersetze das aktuelle Oval
  bind .c <B1-Motion> {
    .c delete "oval$oval_count"
    .c create oval $orig_x $orig_y %x %y -tags "oval$oval_count" -fill red
  }
}

Wir definieren hier die Prozedur set_oval und benutzen dazu den Tcl-Befehl proc. Das erste Argument zu proc ist die Liste der Argumente, die die Prozedur mitbekommt -- in diesem Fall gibt es keine Argumente. Das zweite Argument ist die eigentliche Prozedur. Diese Prozedur wird aufgerufen, wenn wir im Menü »Objects« den Punkt »Ovals« wählen; wir werden dieses Menü weiter unten erzeugen.

set_oval definiert zunächst die Variablen oval_count, orig_x und orig_y als globale Variablen -- Tcl würde sonst annehmen, daß wir diese Variablen nur innerhalb dieser Prozedur benutzen.

Der nächste Schritt besteht darin, mit dem Ereignis ButtonPress (Tastendruck) im Leinwand-Widget, in das wir zeichnen werden, eine Aktion zu verbinden (binding). Wir nennen dieses Widget .c; es wird weiter unten erzeugt. Die Tk-Widgets werden hierarchisch benannt. Das Widget . (ein Punkt) bezeichnet das Hauptfenster der Anwendung. Alle Widgets, die innerhalb dieses Fensters erzeugt werden, erhalten einen Namen, der mit einem Punkt beginnt, etwa .c (für canvas widget; Leinwand-Widget), .mbar (für menu bar; Menüleiste) usw. Natürlich steht es dem Programmierer frei, die Widget-Namen selbst zu wählen, aber sie müssen mit einem Punkt beginnen. Wir werden noch sehen, daß Widgets in anderen Widgets enthalten sein können -- z.B. ist ein Menü Bestandteil einer Menüleiste. Ein Widget namens:

.mbar.file.menu

könnte das Menü menu bezeichnen, das im Menüpunkt file enthalten ist, der wiederum in der Menüleiste .mbar enthalten ist. Wir werden das weiter unten noch zeigen.

Mit dem Befehl bind wird ein Ereignis mit einem bestimmten Widget verknüpft. (Ein Ereignis ist einfach eine »Nachricht«, die der X-Server als Antwort auf eine Benutzeraktion erzeugt. Ein Beispiel: Wenn der Benutzer in einem bestimmten Fenster die linke Maustaste drückt, wird das Ereignis ButtonPress-1 an dieses Fenster geschickt.) Das erste Argument zu bind bezeichnet das Widget, in dem die Verknüpfung erzeugt werden soll; das zweite Argument enthält den Code, der ausgeführt werden soll, wenn dieses Ereignis auftritt.

In unserem Beispiel möchten wir immer dann ein Oval zeichnen, wenn der Benutzer innerhalb des Leinwand-Widgets die linke Maustaste drückt. Der Code dieser Verknüpfung setzt die Variablen orig_x und orig_y auf %x bzw. %y. Innerhalb einer Verknüpfung bezeichnen %x und %y die x - und y -Koordinaten des betreffenden Ereignisses. In unserem Beispiel wäre das die Position, an der die Maustaste gedrückt wird. Wir wollen uns für den Fall, daß das Oval in der Größe verändert wird, diese Position merken. Außerdem wird der Wert der Variablen oval_count erhöht.

Die Verknüpfung mit ButtonPress-1 führt auch den Befehl:

.c create oval %x %y %x %y -tags "oval$oval_count" -fill red

aus. Damit erzeugen wir ein Objekt oval innerhalb des Leinwand-Widgets .c. Die Koordinaten »oben links« und »unten rechts« des Ovals werden in %x und %y übergeben, der Position des Ereignisses ButtonPress. Wir malen das Oval rot aus (fill red).

Die Option -tags zum Befehl create gibt dem gerade erzeugten Objekt oval einen »Namen« (tag). Auf diese Weise können wir dieses spezielle Oval innerhalb des Leinwand-Widgets unter seinem Namen ansprechen. Wir sorgen dafür, daß jedes Oval einen eindeutigen Namen bekommt, indem wir die Variable oval_count benutzen, die mit jedem neu erzeugten Oval hochgezählt wird.

Wenn wir die Maus mit gedrückter linker Taste ziehen, soll die Größe des Ovals entsprechend angepaßt werden. Dazu definieren wir im Leinwand-Widget eine Verknüpfung für das Ereignis B1-Motion. Diese Verknüpfung führt zwei Befehle aus:

.c delete "oval$oval_count"
.c create oval $orig_x $orig_y %x %y -tags "oval$oval_count" -fill red

Der Leinwand-Befehl delete löscht das Objekt, das den entsprechenden Namen trägt. Wir zeichnen das Oval dann mit den ursprünglichen Oben-links-Koordinaten neu, aber mit den Unten-rechts-Koordinaten, die durch die Position des Ereignisses B1-Motion bestimmt werden. Mit anderen Worten: Wir ersetzen das ursprüngliche Oval-Objekt durch ein neues Oval mit anderen Koordinaten, die der Position der Maus entsprechen. Der Effekt ist, daß wir die Größe des Ovals verändern, indem wir die Maus mit gedrückter linker Taste über die Leinwand ziehen.

Analog dazu definieren wir die Funktion set_rect, die mit der gerade besprochenen fast identisch ist; allerdings werden auf der Leinwand rectangle-Objekte (Rechtecke) erzeugt.

# Identisch mit set_oval, aber für Rechtecke
proc set_rect {} {
  global rect_count orig_x orig_y
  bind .c <ButtonPress-1> {
    set orig_x %x
    set orig_y %y
    set rect_count [expr $rect_count + 1]
    .c create rectangle %x %y %x %y -tags "rect$rect_count" -fill blue
  }
  bind .c <B1-Motion> {
    .c delete "rect$rect_count"
    .c create rectangle $orig_x $orig_y %x %y -tags "rect$rect_count" \
      -fill blue
  }
}

Eine andere Methode, Rechtecke und Ovale zu zeichnen, wäre die Benutzung einer Funktionsklasse namens »draw object« (Objekt zeichnen), die eine Variable wie z.B. $objtype benutzt, um sich den aktuellen Objekttyp zu merken. Man würde die Menüeinstellungen (weiter unten beschrieben) benutzen, um den Objekttyp auszuwählen, indem man den Wert dieser Variable setzt. In der Zeichenfunktion könnten wir einfach einen Leinwand-Befehl wie:

.c create $objtype %x %y %x %y -tags "obj$obj_count" -fill blue

benutzen. Dies setzt allerdings voraus, daß alle Objekte in derselben Weise gezeichnet werden (indem an einer Stelle geklickt wird, um dann durch Ziehen der Maus die Größe zu bestimmen. Wenn wir für jedes Objekt eine eigene Funktion benutzen, haben wir die Möglichkeit, die Schnittstelle besser an die einzelnen Objekte anzupassen -- falls wir darauf Wert legen.

Jetzt sind wir soweit, daß wir die verschiedenen Widgets definieren können, aus denen unsere Anwendung besteht. Als erstes brauchen wir ein Rahmen-Widget (frame), das wir als Menübalken benutzen werden. Ein Rahmen-Widget ist nichts anderes als ein Container für andere Widgets.

# Rahmen-Widget für Menübalken erzeugen
frame .mbar -relief groove -bd 3
pack .mbar -side top -expand yes -fill x

Damit erzeugen wir das Rahmen-Widget .mbar. Die Option -relief bestimmt das Aussehen des Rahmens -- wir haben uns für einen Menübalken mit einer »Nut« rund um die Außenkante entschieden. Die Option -bd legt die Breite der Umrahmung fest; in diesem Fall bestimmt sie die Breite der Nut.

Der Befehl pack ordnet die Widgets innerhalb des Hauptfensters oder innerhalb anderer Widgets an. pack ist einer von mehreren »Geometrie-Managern« für Tk. Damit ein Widget innerhalb der Anwendung angezeigt werden kann, muß ein Geometrie-Manager aufgerufen werden, der das Widget auf dem Bildschirm plaziert. pack ist Bestandteil von Tcl/Tk und ist für die meisten Anwendungen flexibel genug. Mit pack haben Sie die Möglichkeit, die Anordnung von Widgets relativ zueinander festzulegen, ohne daß Sie absolute Positionen angeben müssen.

In unserem Beispiel plazieren wir das Widget .mbar am oberen Rand des übergeordneten Widgets, nämlich . (das Hauptfenster der Anwendung). Die Option -fill x zeigt pack an, daß das Widget die ganze Breite des Fensters ausfüllen soll, in dem es enthalten ist; die Option -expand bewirkt, daß das Widget »wächst«, um die gesamte Breite auszufüllen. Falls Sie an den Feinheiten von pack interessiert sind, finden Sie in der Manual-Page eine sehr detaillierte Beschreibung.

Als nächstes erzeugen wir innerhalb dieses Menübalkens zwei menubutton-Widgets -- die Menüs File und Object:

# Zwei Menüpunkte erzeugen
menubutton .mbar.file -text "File" -menu .mbar.file.menu
menubutton .mbar.obj -text "Object" -menu .mbar.obj.menu
pack .mbar.file .mbar.obj -side left

Die beiden Widgets heißen .mbar.file und .mbar.obj. Sie stammen also direkt vom Widget .mbar ab, nicht vom Hauptfenster. Mit pack plazieren wir die beiden Widgets am linken Rand des übergeordneten Menübalkens.

Die Option -menu zum Befehl menubutton bestimmt, welches menu-Widget angezeigt werden soll, wenn dieses Pull-down-Menü ausgewählt wird. Wir werden die Widgets .mbar.file.menu und .mbar.obj.menu weiter unten erzeugen.

# File-Menü mit einzigem Punkt "Quit" erzeugen
menu .mbar.file.menu
.mbar.file.menu add command -label "Quit" -command { exit }

Wir erzeugen zuerst das Menü File selbst und fügen dann einen einzigen command-Menüpunkt hinzu. Ein command funktioniert wie eine Schaltfläche -- wenn man darauf klickt, wird der Code ausgeführt, den die Option -command bestimmt. In unserem Beispiel beendet diese Option das Tk-Skript.

# Object-Menu mit zwei Radiobuttons erzeugen
menu .mbar.obj.menu
.mbar.obj.menu add radiobutton -label "Ovals" -variable objtype \
  -command { set_oval }
.mbar.obj.menu add radiobutton -label "Rectangles" -variable objtype \
  -command { set_rect }

Mit diesem Codeabschnitt definieren wir das Menü Objects und fügen zwei Objekte vom Typ radiobutton hinzu. Radiobuttons definieren eine Reihe von Optionen, von denen zu einem beliebigen Zeitpunkt nur eine aktiviert werden kann. Wenn beispielsweise Ovals ausgewählt wird, leuchtet es auf und Rectangles wird dunkel dargestellt.

Um die beiden Radiobuttons miteinander zu »koppeln« (so daß immer nur einer gewählt werden kann), definieren wir mit der Option -variable eine »unechte« Variable (dummy), in der der aktuelle Zustand der Radiobuttons gespeichert wird. Man kann die Option -variable auch zusammen mit -value benutzen, und auf diese Weise der Variablen einen Wert zuweisen, wenn dieser Menüpunkt gewählt wird. Wir haben statt dessen beschloßen, eine Prozedur aufzurufen (mit der Option -command), wenn dieser Menüpunkt gewählt wird; damit wird auch -value nicht gebraucht.

Im nächsten Schritt erzeugen wir unser Leinwand-Widget (canvas) und bringen es im Fenster der Anwendung unter:

# Leinwand-Widget (canvas) .c erzeugen
canvas .c
pack .c -side top

Schließlich aktivieren wir die Option Ovals, indem wir den entsprechenden Menüpunkt auf künstliche Weise auswählen. Das ist genau das, was auch passiert, wenn ein Benutzer diesen Punkt mit der Maus anklickt:

# Ovale aktivieren durch Auswahl des ersten Punktes im Object-Menü
.mbar.obj.menu invoke 0

Wir haben soeben mit einigen Dutzend Codezeilen eine komplette und ziemlich komplexe X-Anwendung erstellt. Es ist leicht möglich, dieses Programm auf vielfältige Weise zu erweitern -- Sie könnten neue Objekttypen hinzufügen, dem Benutzer die Möglichkeit geben, die erzeugten »Bilder« zu speichern und zu laden usw. Das Leinwand-Widget unterstützt sogar eine Option, um den Inhalt der Leinwand als PostScript-Datei zu formatieren, die Sie anschließend ausdrucken können.

Tcl und Tk in andere Anwendungen einbinden

Wir haben bereits erwähnt, daß Tcl und Tk zusammen mit anderen Sprachen wie C und Perl benutzt werden können. Es ist möglich, auch komplexe Programme als Tcl/Tk-Skript zu schreiben; das Skript wäre wahrscheinlich langsamer als ein kompiliertes Programm, weil Tcl eine interpretierte Sprache ist. Obwohl auch Perl eine Interpreter-Sprache ist, eignet es sich für einige Aufgaben, die in Tcl oder C schwieriger zu realisieren sind.

Die übliche Methode, Tcl und Tk zusammen mit einem C-Programm zu benutzen, ist das Binden der Tcl/Tk-Libraries mit dem C-Code. Tcl und Tk haben sowohl einfache statische Bibliotheken ( .a ) als auch, auf manchen Systemen, Shared Libraries ( .so ). Der Tcl-Interpreter besteht aus einer Reihe von Funktionen, die Ihr Programm aufruft.

Die Idee dahinter ist, daß Sie neue Tcl-Befehle in Form von C-Funktionen einführen, und daß der Tcl-Interpreter diese Funktionen aufruft, wenn einer dieser Befehle benutzt wird. Damit das funktioniert, müssen Sie Ihr Programm so schreiben, daß der Tcl-Interpreter initialisiert wird und daß es innerhalb einer Tcl-»Hauptschleife« abläuft, die die Tcl-Befehle irgendwo liest (z.B. in einer Datei), um sie dann auszuführen. Das ist in etwa dasselbe, als ob Sie Ihren eigenen tclsh - oder wish -Interpreter mit zusätzlichen, in C verfaßten Tcl/Tk-Befehlen schreiben.

Das ist vielleicht nicht für alle Anwendungen die beste Vorgehensweise. Zunächst einmal werden Sie den Aufbau mancher Programme ändern müssen; anschließend steht die Anwendung unter der Kontrolle des Tcl-Interpreters -- und nicht umgekehrt. Außerdem gilt: Solange Sie nicht die Shared Libraries von Tcl und Tk benutzen, kann die ausführbare Datei durch den komplett eingebundenen Tcl/Tk-Interpreter ziemlich groß werden -- weit mehr als ein Megabyte. Es kann auch sein, daß Ihre Anwendung von einem Tcl-Skript gesteuert wird; das bedeutet, daß die Anwendung alleine nicht lauffähig ist -- Sie brauchen zusätzlich das Skript.

Eine andere Lösung wäre eine in C oder Perl geschriebene Anwendung, die den Interpreter wish als eigenen Prozeß ausführt und durch Pipes mit ihm kommuniziert. In diesem Fall würden Sie zwei Pipes brauchen; in der einen schickt das C-Programm Befehle an wish , in der anderen liest das C-Programm die Antworten von wish . Das läßt sich auch mit einer Pipe bewerkstelligen, aber die Synchronisation wird schwieriger. Ein Beispiel: Die Antworten von wish können asynchron eintreffen -- durch solche Ereignisse wie eine gedrückte Maustaste hervorgerufen -- dadurch wird die Benutzung einer einzelnen Pipe ziemlich schwierig. (2)

Die direkteste Methode ist in diesem Fall eine C-Funktion, die folgendes erledigt (in Pseudocode):

Erzeuge zwei Pipes durch zweifachen Aufruf von pipe();
Starte mit fork() einen Kindprozeß;
  Im Kindprozeß:
    Schließe den Lese-Kanal der einen Pipe und den Schreib-Kanal der
anderen;
    Kopiere mit dup2() stdin und stdout in die entsprechenden Pipes;
    Starte wish mit execlp();
  Im Elternprozeß:
    Schließe den Lese-Kanal der Write-Pipe und den Schreib-Kanal der
Read-Pipe;
    Öffne mit fdopen() beide Pipes, um einen FILE-Deskriptor für
fprintf()
      und fscanf() zu bekommen;

Natürlich müssen Sie einiges von der Systemprogrammierung unter UNIX verstehen, um dieses Beispiel nutzen zu können, aber für die Wagemutigen haben wir es hier aufgeschrieben.

Der Elternprozeß (Ihr C-Programm) kann anschließend Tcl/Tk-Befehle in den Schreib-Kanal schreiben und die Antworten von wish aus dem Lese-Kanal lesen. Mit der Funktion select können Sie den Lese-Kanal auf eingehende Daten abfragen (pollen), falls Ihre Anwendung weiterarbeiten soll, während sie auf Daten vom wish -Interpreter wartet.

Auf diese Weise benutzen wir wish als »Server« für X-Window-System-Routinen. Ihr Programm schickt dann die Befehle zur Erzeugung von Widgets in die Write-Pipe. wish könnte eine Meldung an die Standardausgabe schicken, wenn von der Anwendung eine Reaktion erfolgen soll. So könnte man beispielsweise ein Button-Widget erzeugen, das den String »OK gedrückt« ausgibt, wenn der Benutzer darauf klickt. Ihr Programm würde diese Meldung aus der Read-Pipe lesen und darauf reagieren. wish könnte andere Teile der Anwendung kontrollieren, ohne daß Ihr Programm davon weiß. Die rechenintensiven, zeitkritischen Teile der Anwendung würde man in C schreiben, und wish würde sich um die Benutzerschnittstelle kümmern.

Wir hoffen, daß dieser Überblick ausreicht, um Ihnen eine Vorstellung davon zu geben, wie ein C- oder Perl-Programm in dieser Weise wish benutzen kann.

Sie sollten auch ein Buch über die Systemprogrammierung unter UNIX lesen, in dem die Kommunikation zwischen Prozessen mittels Pipes besprochen wird; z.B. Advanced Programming in The Unix Environment .


Fußnoten

(2)
Erinnern Sie sich, daß eine Pipe ein einfacher Datenstrom in eine Richtung ist, den ein Prozeß an einen anderen schickt. Die Shell gestattet die Benutzung einfacher Pipes zwischen Befehlen, etwa so: cat foo.txt.gz | gunzip -c | more.


Inhaltsverzeichnis Vorherige Abschnitt Nächste Abschnitt