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)