Mit Perl und dem Tk-Toolkit lassen sich schnell mausgesteuerte graphische Oberflächen mit Menüs und Dialogen erstellen.
Eigentlich dachte ich ja, die Zeiten konventioneller graphischer Benutzerschnittstellen für Applikationen seien vorbei. Seit man alles über Browser bedienen kann, lohnt sich der Aufwand kaum noch, ein Graphical User Interface (GUI) zu schreiben, denn mit einem CGI-Skript und dem auf der Linux-Box laufenden Web-Server gehen Interaktionsaufgaben spielend leicht von der Hand. Muss eine Applikation allerdings sofort auf Interaktionen des Anwenders reagieren, gehen Web-Requests schnell auf die Nerven, weil sie zu langsam sind. Ein eigenes GUI muss her.
So geschehen im Japanisch-Kurs: Da paukt man eine Unmenge von Vokabeln, und am besten geht das mit Karteikarten, mit dem englischen Ausdruck auf der Vorder- und dem japanischen auf der Rückseite. Im dritten Jahrtausend schreibt man natürlich keine Karteikarten mehr, sondern eine Textdatei, die zeilenweise Übersetzungen liefert:
Hello/Welcome. (Restaurant/Shop) -- Irasshaimase. One, please. (Restaurant) -- Hitori onegai shimasu. Table for two, please. -- Futari onegai shimasu. ...
Auf der linken Seite steht der englische Ausdruck und auf der rechten,
getrennt durch zwei Gedankenstriche, der japanische.
Diese Datei wählt unser heute vorgestelltes Skript tkpauk
mit
einem File-Selektor-Dialog aus
(siehe Abbildung 1), liest die Daten ein, würfelt sie durcheinander,
präsentiert jeweils den linken Teil und wartet auf eine Benutzereingabe.
Abbildung 1: Der Benutzer wählt eine Übersetzungsdatei aus. |
Der Benutzer versucht dann jeweils durch verstärktes Grübeln, die
Übersetzung zu finden und kriegt mit dem nächsten Tastendruck
oder Mausklick die Lösung
auf den Schirm (Abbildung 2).
War die Lösung richtig, haut er auf die linke
Maustaste oder die ``Enter''-Taste,
worauf das Wort aus dem Abfrage-Pool verschwindet.
War sie falsch,
belässt die Leertaste oder die rechte Maustaste
den Ausdruck im Pool und verfrachtet ihn an dessen
Ende. In beiden Fällen fährt das Programm mit dem nächsten Ausdruck
fort, und das geht so lange weiter, bis die Übersetzung aller Ausdrücke
geklappt hat und der Zähler
unten rechts im GUI auf 0
steht.
Abbildung 2: Der Pauker in Aktion |
Bekanntlich kann man ja mit Perl und dem Perl/Tk-Toolkit blitzartig
graphische Oberflächen mit allem Schnickschnack zaubern, die dann nicht
nur auf Unix-Systemen, sondern unverändert
auch in der Windows-Hölle laufen -- vorausgesetzt, man installiert
dort das gute Activestate-Perl und mit dem Tool ppm
das Paket Tk
.
Unter Unix geht die Installation wie immer mit der CPAN-Shell:
perl -MCPAN -e'install Tk'
-- und fertig, das war's schon.
Aber nun zum Programm: Mit use strict
und use warnings
schaltet tkpauk
zunächst strenge Programmierkonventionen und hilfreiche
Warnungen an.
Mit use Tk
und use Tk::FileSelect
zieht
es in den Zeilen 8 und 9 die notwendigen Zusatzmodule
für das GUI und einen sofort einsatzbereiten
File-Select-Dialog herein.
Die Zeilen 11 bis 13 definieren einige GUI-Größen:
Das Applikationsfenster misst 400 Pixel Breite und die Label-Widgets,
die Frage und Antwort anzeigen, sind jeweils 50 Zeichen breit
und 10 Zeichen hoch.
001 #!/usr/bin/perl 002 ################################################## 003 # tkpauk -- Mike Schilli, 2001 (m@perlmeister.com) 004 ################################################## 005 use warnings; 006 use strict; 007 008 use Tk; 009 use Tk::FileSelect; 010 011 my $width = 400; # Fensterbreite 012 my $lheight = 10; # Texthöhe 013 my $lwidth = 50; # Textbreite 014 015 my @SHUFFLED; 016 my %TRANS; 017 018 my $TOP = MainWindow->new(-width => $width); 019 my $menubar = $TOP->Menu(-type => 'menubar'); 020 my $f = $menubar->cascade(-label => 'File'); 021 022 $f->command(-label => 'Open', 023 -command => \&file_select); 024 $f->command(-label => 'Exit', 025 -command => sub { exit(0) }); 026 $TOP->configure(-menu => $menubar); 027 028 my $QUESTION = $TOP->Label( 029 -width => $lwidth, 030 -height => $lheight, 031 -wraplength => 7/10*$width, 032 )->pack(); 033 034 my $ANSWER = $TOP->Label( 035 -width => $lwidth, 036 -height => $lheight, 037 -wraplength => 7/10*$width, 038 )->pack(); 039 040 my $FOOTLINE = $TOP->Frame(); 041 my $COUNTER = $FOOTLINE->Label(); 042 my $MESSAGE = $FOOTLINE->Label(); 043 044 $MESSAGE->pack(-expand => 'yes', -side => 'left'); 045 $COUNTER->pack(-expand => 'no', -anchor => 'e'); 046 $FOOTLINE->pack(-expand => 'yes', -fill => 'x'); 047 048 state_init(); 049 MainLoop(); 050 051 ################################################## 052 sub footline { 053 ################################################## 054 my($text) = @_; 055 056 my $nof_items = @SHUFFLED; 057 $COUNTER->configure(-text => $nof_items); 058 $MESSAGE->configure(-foreground => 059 "darkgrey"); 060 $MESSAGE->configure(-text => "$text"); 061 } 062 063 ################################################## 064 sub file_select { 065 ################################################## 066 my $f = $TOP->FileSelect(-directory => ".", 067 -filter => '*.txt'); 068 my $file = $f->Show(); 069 state_ask(), return unless defined $file; 070 071 %TRANS = (); 072 073 open DAT, "<$file" or die "Cannot open $file"; 074 while(my $line = <DAT>) { 075 my($q, $a) = split /\s*--\s*/, $line, 2; 076 $TRANS{$q} = $a; 077 } 078 close DAT; 079 @SHUFFLED = shuffle(keys %TRANS); 080 state_ask(); 081 } 082 083 ################################################## 084 sub shuffle { 085 ################################################## 086 my @array = @_; 087 for(my $i=@array; --$i; ) { 088 my $j = int rand ($i+1); 089 next if $i == $j; 090 @array[$i,$j] = @array[$j,$i]; 091 } 092 return @array; 093 } 094 095 ################################################## 096 sub state_init { 097 ################################################## 098 footline("Please select a data file"); 099 $QUESTION->configure(-text => ""); 100 $ANSWER->configure(-text => ""); 101 102 map { $TOP->bind("<$_>" => "") } 103 qw(Return space Button-1 Button-3); 104 105 file_select(); 106 } 107 108 ################################################## 109 sub state_ask { 110 ################################################## 111 return unless @SHUFFLED; 112 113 footline("Enter: See translation"); 114 $QUESTION->configure(-text => $SHUFFLED[0]); 115 $ANSWER->configure(-text => ""); 116 117 $TOP->bind("<Key-q>" => \&state_init); 118 119 map { $TOP->bind("<$_>" => \&state_show) } 120 qw(Return space Button-1 Button-3); 121 } 122 123 ################################################## 124 sub state_show { 125 ################################################## 126 footline( 127 "Enter: Item processed Space: Keep item"); 128 $QUESTION->configure(-text => $SHUFFLED[0]); 129 $ANSWER->configure(-text => 130 $TRANS{$SHUFFLED[0]}); 131 132 $TOP->bind("<Button-1>" => \&success); 133 $TOP->bind("<Return>" => \&success); 134 $TOP->bind("<Button-3>" => \&failure); 135 $TOP->bind("<space>" => \&failure); 136 } 137 138 ################################################## 139 sub state_congrats { 140 ################################################## 141 footline(""); 142 $QUESTION->configure(-text => 143 "Congratulations!!!"); 144 $ANSWER->configure(-text => ""); 145 map { $TOP->bind("<$_>" => \&state_init) } 146 qw(Return space Button-1 Button-3); 147 } 148 149 ################################################## 150 sub success { 151 ################################################## 152 shift @SHUFFLED; 153 @SHUFFLED ? state_ask() : state_congrats(); 154 } 155 156 ################################################## 157 sub failure { 158 ################################################## 159 my $current = shift @SHUFFLED; 160 push @SHUFFLED, $current; 161 state_ask(); 162 }
Die globalen Variablen @SHUFFLED
und %TRANS
enthalten später die
Übersetzungsdaten. @SHUFFLED
ist ein Array mit den Fragen
und %TRANS
ordnet ihnen jeweils die richtige Antwort zu.
Weil GUI-getriebene Programme nicht von oben nach unten sondern
ereignisgesteuert ablaufen, sieht ihr Source-Code etwas unkonventionell
aus: Erst definiert man die einzelnen Bestandteile der Oberfläche, die
sogenannten Widgets, und legt fest, was passiert, wenn der Benutzer
wohin klickt. Dann ordnet der Packer die Widgets nach den angegebenen
Kriterien auf dem Bildschirm an, das Programm springt in die
Haupt-Event-Schleife (MainLoop()
in Zeile 49) und der Reigen geht los.
Abbildung 3: C |
Abbildung 4: Die gepackten Widgets der Oberfläche |
Wie Abbildung 3 zeigt,
kennt tkpauk
das Hauptfenster vom Typ MainWindow
, dessen Referenz
im Skript
in $TOP
liegt. Weiter gibt es den Menubalken $menubar
,
ein Label-Widget für die gestellten Fragen ($QUESTION
) und
darunter eine weiteres Label-Widget für die Antworten
($ANSWER
). Darunter wiederum stehen ein
Label-Widget ($MESSAGE
) für Nachrichten an den Benutzer und ein weiteres
Label-Widget ($COUNTER
), das die Anzahl der
sich im Pool befindenden Einträge
anzeigt. Die beiden liegen aus Layout-Gründen
in einem Frame-Container ($FOOTLINE
). Alle großgeschriebenen Variablen
werden global auch in den weiter unten definierten Funktionen verwendet.
Nikolas Wirth dreht sich im Grabe herum.
Die Zeilen 18 bis 26 bauen das Hauptfenster,
den Menübalken, den Eintrag File
in ihm und dessen Pulldown-Einträge Open und Quit
auf. Der Menübalken wird in Zeile 19 als Kind des
Hauptfensters erzeugt, dessen Referenz in $TOP
liegt. Zeile 26
teilt außerdem noch dem Hauptfenster mit,
dass es sich bei $menubar
um einen Menubalken handelt, der
dann
entsprechend den GUI-Richtlinien der gerade verwendeten Plattform
platziert wird.
Die cascade()
-Methode in Zeile 20 hängt ein Pulldown-Menü namens
File
in den Balken,
die command()
-Methoden in Zeile 22 und 25
bevölkern es mit Einträgen wie Open
und Quit
. command()
nimmt auch noch eine Referenz auf die
Funktion entgegen, die die GUI-Maschine anspringen soll, falls der
Benutzer den Menüpunkt auswählt: So zeigt der Quit-Eintrag
auf eine anonyme Subroutine, die einfach nur exit(0)
ausführt
und der Open-Eintrag führt, falls er ausgewählt wird, die
weiter unten definierte Funktion file_select()
aus.
Die Label-Widgets für Frage und Antwort,
$QUESTION
und $ANSWER
, rufen gleich nach ihrer Erzeugung
die pack()
-Methode auf, die sie untereinander in ihr
Eltern-Widget $TOP
packt -- und zwar noch unter den
Menubalken, den sein eigenes, internes
pack()
schon vorher oben ins Fenster pflanzte.
Die Parameter für -width
und
-height
der Label-Widgets für Frage und Antwort geben deren Breite bzw.
Höhe in Zeichen (nicht Pixeln!) an. Der Parameter
für -wrapwidth
legt fest, ab welcher Breite der Text vom Widget
zu Darstellungszwecken umgebrochen wird. Dieser Wert muss nun
kurioserweise wieder in Pixeln vorliegen, was die Zeilen 31 und 37
einfach über 70% der Gesamtbreite des Elternwidgets $TOP
ausrechnen.
Die zu $MESSAGE
und $COUNTER
gehörigen Widgets sollen nebeneinander
unter dem Widget zu $ANSWER
liegen, und zwar so, dass
der Zähler immer ganz rechts hängt, während das Nachrichten-Widget
sich mittig im restlichen verfügbaren Platz der Zeile
positioniert. Deswegen packen wir beide einfach in einen
unsichtbaren Frame-Container namens
$FOOTLINE
und positionieren diesen mit pack()
unter das
$ANSWER
-Widget.
$MESSAGE
und $COUNTER
werden als Kinder von $FOOTLINE
erzeugt. Der Packer packt $MESSAGE
mit der Option -side => left
nach links und das nachfolgende $COUNTER
-Widget dann automatisch
nach rechts. Wegen der Option -expand => yes
beansprucht
das Widget dann, falls die Größe des Eltern-Widgets dies zulässt,
zusätzlichen Platz und schwimmt in dessen Mitte. Das Zählerwidget
hingegen soll nicht umeinanderschwimmen, sondern immer den gleichen
Platz verbrauchen und rechts am Rand kleben, deswegen
-expand => no
und -anchor => e
, da e
für East
also Ost also rechts steht.
Der Packer hat so seine Eigenheiten, in [2] habe ich vor einigen Jahren
einmal aufgeschrieben, wie er genau funktioniert. Statt pack()
gibt's auch noch grid()
, das mit dem Grid
-Geometriemanager
schnackt, und ein bißchen einfacher zu handhaben ist.
Der Tk-Pauker reagiert auf Maus- und Tastendrücke. Was er genau macht, hängt davon ab, in welchem Zustand er sich befindet. Vier Zustände habe ich definiert:
init
ask
.
ask
show
-Zustand.
Die 'q'-Taste springt zurück zum Zustand init
.
Sind keine Fragen mehr da, springt der Pauker zum Zustand
congrats
.
show
ask
.
congrats
init
.
Der Übersichtlichkeit halber verpackt tkpauk
die Aktionen für
bestimmte Zustände in eigene Funktionen:
state_init()
,
state_ask()
,
state_show()
,
state_congrats()
leiten den jeweiligen Zustand init
, ask
, show
oder congrats
ein.
Abbildung 5: Die Zustände, in die der Benutzer C |
Die ab Zeile 64 definierte Funktion
file_select()
initialisiert mit der FileSelect()
-Methode des
Hauptfensters den
File-Selektor-Dialog. Die Show()
-Methode fährt ihn hoch
und liefert den Eintrag, den der Benutzer auswählte, als
Textstring zurück -- oder undef
, falls der Benutzer sich nicht
entscheiden konnte, und den Cancel-Button drückte.
Die while
-Schleife ab Zeile 74 iteriert
über alle Zeilen der ausgewählten Datei, trennt linke und rechte Seite
mit der split
-Funktion am Doppel-Gedankenstrich
und speichert die Beziehungen zwischen Fragen und Antworten
im globalen Hash %TRANS
ab. Zeile 79 ruft die aus [3]
abgekupferte Mischfunktion shuffle()
auf, die die Fragen
nach dem Fisher-Yates-Verfahren durcheinanderwürfelt und
sie so im Array @SHUFFLED
ablegt. state_ask()
springt
anschließend in den ask
-Zustand für die erste Frage.
Dort ändert footline()
die in der Farbe darkgrey
gesetzte
Nachricht in der Fußzeile, damit
der Benutzer weiß, was gerade von ihm erwartet wird. footline()
wurde ab Zeile 52 definiert und gibt nicht nur die ihr übergebene Nachricht
im Message-Widget am unteren Rand des Hauptfenster aus, sondern zeigt auch
gleich noch im Counter-Widget an, wieviele Einträge sich im Vokabel-Pool
befinden.
Die Zeile 114
setzt mit der configure()
-Methode des Frage-Widgets dessen Text
auf das erste Element des @SHUFFLED
-Arrays.
Das Antwort-Widget wird in Zeile 115 mit dem Leerstring
darauf getrimmt, zunächst nichts anzuzeigen.
Die sogenannten Bindings der Zeilen 117 bis 120 legen fest, auf
welche Tasten- und Mausaktionen das GUI im ask
-Zustand
hören soll. Für das
Hauptfenster $TOP
geht das prinzipiell so:
$TOP->bind("<EVENT>" => \&aktion)
ruft für das Ereignis EVENT
die Funktion aktion()
auf.
Dabei spezifiziert EVENT
entweder einen Maus- oder
Tastaturereignis: Return
signalisiert die gedrückte
Return-(Enter-)Taste, space
die Leertaste,
Key-q
die Taste q
, Button-1
die
linke und Button-3
die rechte Maustaste (die mittlere
hört auf den Namen Button-2
). Der map
-Befehl
in Zeile 119 ruft die bind
-Methode einfach für alle im Array
eingetragenen Ereignisse auf:
$TOP->bind("<Return>" => \&state_show) ... $TOP->bind("<Button-2>" => \&state_show)
In jedem der Fälle oben soll das GUI in den Zustand show
springen, also
die Antwort auf die bereits sichtbare Frage anzuzeigen.
Drückt der Benutzer hingegen die Taste ``q'', bricht das Skript
wegen der bind
-Methode in Zeile 117
den aktuellen Paukerlauf ab und springt zurück
zum init
-Zustand, der den File-Selektor hochfährt.
Das erste Element von @SHUFFLED
, der Eintrag $SHUFFLED[0]
enthält immer den String der nächsten Frage, auf deren
zugehörige Antwort man einfach über den Hash %TRANS
kommt:
$TRANS{$SHUFFLED[0]}
ist die Antwort auf die erste Frage.
state_show()
nutzt dies, um die Antwort anzuzeigen. In den
Zeilen 132 bis 135 verbindet es die Return-Taste und die linke Maustaste
mit der Funktion success()
sowie die Leertaste und die rechte Maustaste
mit failure()
.
success()
ist ab Zeile 150 definiert, wirft einfach mit shift()
das erste Element von @SHUFFLED
weg und verzweigt zu state_ask()
,
falls es noch Fragen in @SHUFFLED
gibt und zu state_congrats()
,
falls nicht.
failure()
ab Zeile 157 entfernt das Element am Anfang des
Arrays @SHUFFLED
, speichert es in $current
und fügt es
anschließend mit push()
am Ende des Arrays wieder ein.
So bleibt die nicht beantwortete Frage im Pool, kommt aber
für längere Zeit nicht mehr dran.
Wer meine Englisch-Japanisch-Dateien nutzen will, kann sie natürlich kostenlos unter [5] runterladen. Und auch andere Sprachen und Lerninhalte lassen sich pauken -- mit eigenen Quizdateien, die man einfach mittels eines Editors im geschilderten einfachen Format erstellt. Lernt fleißig!
Michael Schilliarbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com. |