3.4 HTk

Werbung
EFP, WS 2006/07, Kapitel htk, 21. November 2006
3.4
3.4.1
1
HTk
Aufbau
Das System HTk ist eine GUI-Einbettung von Tcl/Tk in Haskell. In Haskell
wird das GUI monadisch programmiert. Es werden zahlreiche Funktionen und
Typklassen eingeführt, um die Funktionalität von Tcl/Tk nachzubilden. Zur
Laufzeit wird das System wish aufgerufen. Haskell kommuniziert über wish mit
dem Benutzer. Es werden per Tcl-Kommandos Informationen zwischen Haskell
und wish ausgetauscht.
Weitere Informationen sind im Internet unter
www.informatik.uni-bremen.de/htk/ zu finden.
3.4.2
Grundlagen
Im Rahmen von graphischen Benutzeroberflächen (GUIs) werden einige Begriffe
häufig verwendet, die im Folgenden erklärt werden.
Widgets sind kleine graphische Elemente, wie Knöpfe, Beschriftungen, Menüs,
Eingabefelder usw. Aus ihnen werden größere Objekte zusammen gesetzt.
Die Widgets selbst besitzen Konfigurationen, wie deren Farbe, Beschriftung,
Größe oder ähnliches.
Da graphische Benutzeroberflächen ereignisorientiert sind, treten Ereignisse
bzw. Events auf. Solche Events lassen sich unterscheiden, je nachdem von welchem Gerät sie kommen, so gibt es Mausereignisse (klicken, bewegen), Tastaturereignisse (Taste drücken, loslassen), Fenstereignisse (öffnen, schließen) – wobei
das Gerät“ in diesem Fall der Window-Mangager des Betriebssystems ist.
”
GUIs haben einen statischen Aspekt, d.h. wie die Oberfläche aussehen soll,
welche Widgets wo erscheinen sollen usw. Zum anderen gibt es den dynamischen
Aspekt, der Reaktion auf Ereignisse. In HTk werden beiden Aspekte mittels
Monaden realisiert. Die IO-Monade wird für den statischen Aspekt benutzt.
Für die Ereignisbehandlung stellt HTk die Event-Monade zur Verfügung.
Betrachten wir zunächst ein einfaches Beispiel, eines einzigen Buttons, das noch
keine Ereignisse behandelt.
main:: IO ()
main = do
main_window <- initHTk []
b <- newButton main_window [text "Klick mich!"]
pack b []
finishHTk
Funktionale Programmierung, WS 2006/07, Kap 3; htk, 21. November 2006 2
In der ersten Zeile im do-Statement wird das Hauptfenster mittels der Funktion initHTk initialisiert, und das Ergebnis (vom Typ HTk) an die Variable
main_window gebunden.
In der zweiten Zeile wird ein Button mittels der Funktion newButton erzeugt,
wobei diese Funktion als erstes Argument ein so genanntes Container-Widget
benötigt (hier das Hauptfenster), in dem es erscheinen soll. Das zweite Argument
von newButton ist eine Liste von Konfigurationen. Eine Konfiguration ist durch
den Config-Datentyp spezifiziert:
type Config w = w -> IO w
Wird ein Widget w auf eine Konfiguration angewendet, so erhält man eine IOAktion, die das Widget verändert.
Im Beispiel erhält der Button die Aufschrift Klick mich!“, wofür eine Konfigu”
ration mittels der Funktion text erzeugt wird.
Das Erzeugen des Buttons zeigt ihn jedoch noch nicht auf dem Bildschirm an,
dies geschieht erst in der dritten Zeile mit dem pack-Befehl auf den später
eingegangen wird.
Die Funktion finishHTk in der letzten Zeile, beendet das Programm, wenn die
gesamte GUI geschlossen wird. Bisher kann das Fenster allerdings nur geschlossen werden, wenn der Window-Manager eine enstsprechende Funktionalität anbietet.
3.4.3
Ereignisbehandlung
Ereignisse werden in HTk durch den abstrakten Datentyp Event repräsentiert,
wobei auf diesem folgende Operationen definiert sind:
• Der Operator sync :: Event a -> IO a synchronisiert ein Event, d.h. er
wartet, bis ein entsprechendes Event eintritt, um dieses dann abzuarbeiten.
• Die Sequenzoperatoren
(>>>=) :: Event a-> (a -> IO b) -> Event b
(>>>)
:: Event a-> IO b -> Event b
ermöglichen es, IO-Aktionen zu einem Event hinzuzufügen.
• Der Operator always :: IO a -> Event a verpackt eine IO-Aktion in
ein Event
Der Datentyp Event und die Operatoren (>>>=) und always formen eine Monade, aber wegen der Typen lassen sie sich so nicht zur Instanz von Monad
machen.
Funktionale Programmierung, WS 2006/07, Kap 3; htk, 21. November 2006 3
Wir wollen nun obiges Beispiel erweitern, so dass ein Klicken des Buttons bewirkt, dass die Beschriftung des Buttons um klick“ erweitert wird.
”
Hierfür muss das externe Ereignis, dass der Button gedrückt wurde, an ein
internes Event gebunden werden.
Für einfache GUI-Elemente wie Buttons, stellt HTk hierfür die Funktion
clicked :: HasCommand w => w -> IO (Event ())
zur Verfügung. Für alle GUI-Objekte existiert die Funktion bindSimple.
Unser Programm mit Ereignisbehandlung sieht nun wie folgt aus:
main = do
main_window <- initHTk []
b <- newButton main_window [text "Klick mich!"]
b_clicked <- clicked b -- b_clicked enthaelt das Ereignis
pack b []
sync (b_clicked >>> (do t <- getText b
b # text (t++ " klick")))
finishHTk
Der Operator (#) ist definiert als o # f = f o.
Nach einmaligem Klicken des Buttons hat dieser nun die Aufschrift Klick mich!
”
klick“ Weiteres Klicken verändert nichts mehr, da das Event nur beim ersten
Auftreten synchronisiert wird.
Für beliebig häufiges Bearbeiten des Events stellt HTk die Funktion
forever :: Event a -> Event () bereit, die ein Event unendlich oft mit sich
selbst verknüpft.
Wir wollen dem Beispiel nun einen weiteren Button hinzufügen, der dazu dienen
soll, das Programm zu beenden.
Es müssen somit zwei unterschiedliche Ereignisse (je nachdem, welcher Button
gedrückt wurde) abgefangen werden.
Der choice-Operator (+>) :: Event a -> Event a -> Event a verknüpft
zwei Events, so dass unterschiedliche Aktionen ausgeführt werden können,
abhängig davon, welches Ereignis eintritt.
Unser Programm hat nun folgende Form:
main =
do main_window <- initHTk []
b <- newButton main_window [text "Klick mich!"]
b2 <- newButton main_window [text "Exit"]
b_clicked <- clicked b
Funktionale Programmierung, WS 2006/07, Kap 3; htk, 21. November 2006 4
b2_clicked <- clicked b2
pack b2 []
pack b []
sync (forever ((b_clicked >>> (do t <- getText b
b # text (t++ " klick")
return ()))
+> (b2_clicked >>> destroy main_window)))
finishHTk
Der Aufruf destroy main_window zerstört das Hauptfenster.
3.4.4
Nebenläufigkeit
Im vorherigen Beispiel wird das Fenster zwar geschlossen, aber aufgrund des
forever terminiert das Programm nicht, da sync auf ein unendlich großes Ereignis wartet, dass nie eintritt.
Die Funktion spawnEvent :: Event () -> IO (IO ())
• erzeugt einen neuen konkurrierenden Thread
• synchronisiert das Event auf diesen Thread
• nach Ausführung der IO-Aktion, wird der Thread abgebrochen
Wenn wir die Eventbehandlung in einem nebenläufigen Thread ausführen, erhalten wir folgendes Programm:
main =
do main_window <- initHTk []
b <- newButton main_window [text "Klick mich!"]
b2 <- newButton main_window [text "Exit"]
b_clicked <- clicked b
b2_clicked <- clicked b2
pack b2 []
pack b []
spawnEvent
(forever
((b_clicked >>> (do t <- getText b
b # text (t++ " klick")
return ()))
+> (b2_clicked >>> destroy main_window)))
finishHTk
Nun terminiert das Programm, da der Haupt-Thread abgebrochen werden kann
und dann auch den nebenläufigen Thread schließt.
Es ist auch möglich für jedes Ereignis einen eigenen Thread zu erzeugen, was
zwar den (+>)-Operator erspart, allerdings mehr Ressourcen benötigt.
Funktionale Programmierung, WS 2006/07, Kap 3; htk, 21. November 2006 5
3.4.5
Überblick: GUI-Elemente
Einfache Widgets werden mit dem Befehl newX erzeugt, wobei X das entsprechende Widget ist.
Eine (unvollständige) Auflistung der elementaren Widgets, die HTk bereit stellt:
• Button: Knöpfe
• Label: Beschriftungen
• Message: Beschriftungen mit Zeilenumbruch
• Entry: Eingabefelder
• Scrollbar: Laufleisten
• ListBox: Auswahl von unterschiedlichen Einträgen
• Menu: Komplette Menüs
Die Klasse Container enthält Objekte, die wiederum selbst Widgets aufnehmen
können.
• Toplevel: Windows
• HTk: Das HTk-Hauptfenster
• Frame: Rahmen zur Gruppierung mehrerer GUI-Objekte
• Canvas: Zeichenoberfläche für Linien (LineItem), Ellipsen (Oval), Rechtecke (Rectangle), . . .
• Editor: Anzeigen und Bearbeiten von Texten
• ...
Attribute und Eigenschaften der einzelnen Objekte werden durch Klassen modelliert. Wenn ein Objekt eine bestimmte Eigenschaft besitzt, so ist es Instanz
der entsprechenden Klasse, welche Funktionen zum Verändern und Abfragen
der Eigenschaften bereit stellt.
Z.B. sind alle GUI-Objekte, die eine Beschriftung besitzen, Instanzen der Klasse
HasText:
class (GUIObject w, GUIValue v) => HasText w v where
text :: HasText w v => v -> Config w
getText :: HasText w v => w -> IO v
Funktionale Programmierung, WS 2006/07, Kap 3; htk, 21. November 2006 6
Die Funktionen text und getText haben wir bereits für Buttons benutzt.
Jedes GUI-Objekt ist Instanz der Klasse Destroyable, die nur die Funktion
destroy zum Zerstören des Objektes beinhaltet:
class Destroyable o where
destroy :: o -> IO ()
Einige weitere solcher Eigenschaftsklassen sind:
• HasColour: Farbe (Vordergrundfarbe, Hintergrundfarbe, . . . )
• HasSize: Größe (Höhe, Breite)
• HasPosition: Position
• HasValue : polymorpher Wert vom Typ GUIValue v
• HasBorder: Rahmen (Rahmenstil, Rahmenbreite)
• HasFont: Zeichensatz (Schriftart, Schriftgröße, . . . )
• HasJustify: Ausrichtung für Texte (links, rechts, zentriert)
3.4.6
Darstellen der Objekte: Packing
Wie bereits erwähnt, werden Widgets erst angezeigt, nachdem sie explizit ge”
packt“ wurden.
HTk stellt hierfür zwei unterschiedliche Methoden zur Verfügung.
Der Standard-Packer
pack::Widget w => w -> [PackOption] -> IO ()
platziert die Objekte nach der Reihenfolge der pack-Aufrufe und entsprechend
der angegebenen Optionen. Der Datentyp PackOption ist definiert als:
data PackOption =
|
|
|
Side SideSpec
Expand Toggle
IPadX Distance
PadX Distance
|
|
|
|
Fill FillSpec
Anchor Anchor
IPadY Distance
PadY Distance
Die beiden ersten Optionen sind die wichtigsten:
Side gibt an, an welcher Seite das Objekt ausgerichtet werden soll, hierbei kann
SideSpec die Werte AtTop, AtBottom, AtLeft und AtRight annehmen.
Fill gibt eine Achse an, in deren Richtung das Objekt expandieren soll, um
leeren Platz zu füllen.
Der Grid-Packer
Funktionale Programmierung, WS 2006/07, Kap 3; htk, 21. November 2006 7
grid :: Widget w => w -> [GridPackOption] -> IO ()
plaziert die Objekte anhand eines Gitters. Der Datentyp GridPackOption stellt
die entsprechenden Optionen bereit:
data GridPackOption =
|
|
|
Column Int | Row Int | GridPos (Int, Int)
Sticky StickyKind | Columnspan Int
Rowspan Int | GridPadX Int | GridPadY Int
GridIPadX Int | GridIPadY Int
Innerhalb eines Containers muss derselbe Pack-Algorithmus verwendet werden.
Zum Schluss noch ein ausführlicheres Beispiel: Folgendes Programm berechnet
die Fakultät für eine Zahl.
main =
do
main_window <- initHTk [text "Fakultaet"]
-- Drei Beschriftungen:
label_ein <- newLabel main_window [text "Eingabe: "]
label_ausg <- newLabel main_window [text "Ausgabe: "]
label_erg <- newLabel main_window [text ""]
-- Zwei Buttons:
button_calc <- newButton main_window [text "Berechne"]
button_exit <- newButton main_window [text "Exit"]
-- Ein Eingabefeld:
entry_e <- (newEntry main_window [value ""])::IO (Entry String)
-- Binden der Events
button_calc_clicked <- clicked button_calc
button_exit_clicked <- clicked button_exit
-- Packen der Objekte, mit dem Grid-Packer:
grid label_ein
[Column 1, Row 1]
grid entry_e
[Column 2, Row 1]
grid button_calc [Column 1, Row 2, Columnspan 3]
grid label_ausg [Column 1, Row 3]
grid label_erg
[Column 2, Row 3]
grid button_exit [Column 1, Row 4, Columnspan 3]
-- Ereignisbehandlung:
spawnEvent
Funktionale Programmierung, WS 2006/07, Kap 3; htk, 21. November 2006 8
(forever (
(button_calc_clicked
>>> do
txt <- (getValue entry_e)::(IO String) -- Wert lesen
label_erg # text (ausgabe txt) --label aktualisieren
done)
+> (button_exit_clicked >>> destroy main_window)))
finishHTk
-- ausgabe wandelt einen String in eine Zahl, berechnet
-- deren Fakultaet und gibt das Ergebnis als String zur\"uck:
ausgabe :: String -> String
ausgabe [] = ausgabe "0"
ausgabe xs = (show.fak.read) xs
-- Die Fakultaetsfunktion:
fak 0 = 1
fak x = x*fak(x-1)
Herunterladen