Grundlagen GUI GTK+ Glade Beispiele Fortgeschrittene Funktionale Programmierung mit Haskell Graphische Benutzeroberflächen mit GTK Steffen Jost LFE Theoretische Informatik, Institut für Informatik, Ludwig-Maximilians Universität, München 21. Januar 2016 Steffen Jost FFP 10-1 Grundlagen GUI GTK+ Glade Beispiele Existentielle Quantifikation IORef Spracherweiterungen Existentielle Datentypen {-# LANGUAGE ExistentialQuantification #-} data Obj = forall a. (Show a) => Obj a list = [Obj 1, Obj True, Obj 3, Obj "Foo"] :: [Obj] Spezialfall eines Typen höheren Ranges Erlauben es, z.B. heterogene Listen über Klassen zu erstellen Benennung Konstruktor Obj hat ja den Typ Show a => a -> Obj ∃-Quantor taucht in einer negativer Position auf, nach den logischen Regeln entspricht dies Quantifikation über allen: ∀x.(P(x) → Q) ≡ (∃x.P(x)) → Q data Obj = forall a. Show a => Obj a entspricht also data Obj = Obj (exists a. Show a => a) kein exists in Haskell Steffen Jost FFP 10-2 Grundlagen GUI GTK+ Glade Beispiele Existentielle Quantifikation IORef Spracherweiterungen Fallstricke data Foo = forall a . Eq a => FooA a a | forall b c . Show c => FooB b (b -> c) good :: Foo -> String good (FooA x y) | x==y = "equal" | otherwise = "not" good (FooB z f) = show $ f z bad (FooA x _, FooA z _) = x==z bad (FooA x _, _ ) = x where (FooA _ y) = f -- Problem 1 -- Problem 2 -- Problem 3 Probleme 1 Verschiedene Eq-Instanzen sind nicht miteinander vergleichbar! 2 Rückgabetyp ist nicht feststellbar – dieser ist ja durch das Argument bestimmt im Gegensatz etwa zu undefined :: t 3 Kein Pattern-Matching solcher Datentypen in let/where, kein deriving, kein newtype technische Einschränkung Steffen Jost FFP 10-3 Grundlagen GUI GTK+ Glade Beispiele Existentielle Quantifikation IORef Spracherweiterungen Unterschied -- Klasseneinschränkung (Veraltet) data Old a = Show a => Old a -- Klasseneinschränkung mit GADTs data Better a where Better :: Show a => a -> Better a -- Existentielle Quantifikation data Obj = forall a. (Show a) => Obj a okay :: Show a => Old a -> a okay (Old a) = a better :: Show a => Better a -> a better (Better a) = a wrong :: Show a => Obj -> a wrong (Obj a) = a Steffen Jost -- typing impossible FFP 10-4 Grundlagen GUI GTK+ Glade Beispiele Existentielle Quantifikation IORef Spracherweiterungen Veränderliche Variablen IO-Monade erlaubt echt-veränderliche Variablen Module Data.IORef definiert: newIORef :: a -> IO (IORef a) Referenz erzeugen readIORef :: IORef a -> IO a Referenz auslesen writeIORef :: IORef a -> a -> IO () Referenz schreiben modifyIORef :: IORef a -> (a -> a) -> IO () Referenz bearbeiten, strikte Variante: modifyIORef’ Einfachste Art von Variablen, funktioniert wie MVar, TVar, etc. Operationen sind nicht atomar! ⇒ Entweder nur in einem Thread verwenden, oder Race-Conditions durch TVar vermeiden. Operationen können in anderer Reihenfolge ablaufen! ⇒ Nicht für Locks einsetzen, sondern MVar einsetzen. Steffen Jost FFP 10-5 Grundlagen GUI GTK+ Glade Beispiele Existentielle Quantifikation IORef Spracherweiterungen Functional Dependencies MultiParamTypeClasses Erlaupt Typklassen mit mehreren Parametern: Wiederholung 8-11 class Monad m => VarMonad m v where new :: a -> m (v a) get :: v a -> m a put :: v a -> a -> m () instance VarMonad STM TVar where ... instance VarMonad IO IORef where ... FunctionalDependencies Erlaubt es, Abhängigkeiten zwischen Parametern auszudrücken: class Monad m => VarMonad m v | m -> v where ... d.h. Typ m legt auch schon v eindeutig fest. Unsinnige Instanz instance VarMonad IO TVar nicht mehr zusätzlich möglich. Hilft Typinferenz und liefert bessere Fehlermeldungen. Steffen Jost FFP 10-6 Grundlagen GUI GTK+ Glade Beispiele Existentielle Quantifikation IORef Spracherweiterungen Functional Dependencies vs. Typ Familien Functional Dependencies wurden inzwischen von Erweiterung Typ-Familien nahezu abgelöst, siehe Folie 8-9ff.: class Monad m => VarMonad m where type Ref m :: * -> * new :: a -> m (Ref m a) get :: Ref m a -> m a put :: Ref m a -> a -> m () Typ-Familien sind flexibler, aber etwas mehr Schreibarbeit, da in jeder Instanzdeklaration der Typ definiert werden muss. Steffen Jost FFP 10-7 Grundlagen GUI GTK+ Glade Beispiele Übersicht Viele verschiedene Ansätze für grafische Benutzeroberfläche/ Graphical User-Interface (GUI) in Haskell. High-Level Ansätze, primär basierend auf Functional Reactive Programming, momentan experimentell oder Plattform-gebunden. threepenny-gui FRP, stabil, unabhängig, benutzt Browser als Display Keera Hails FRP, recht neu, primär für Spiele gedacht, Backends für Qt, Gtk & Android Widgets Mid-Level Ansätze benutzen etablierte, imperative, Plattform-übergreifende Frameworks: Qt qtHaskell oder HsQML für Qt Quick wxWidgets wxHaskell wohl etabliert GTK+ Gtk2Hs wohl etabliert Steffen Jost FFP 10-8 Grundlagen GUI GTK+ Glade Beispiele Beispiel: threepenny-gui main = startGUI defaultConfig setup setup :: Window -> UI () setup window = void $ do return window # set title "Currency Converter" dollar <- UI.input euro <- UI.input getBody window #+ [ column [ grid [[string "Dollar:", element dollar] ,[string "Euro:" , element euro ]] , string "Amounts update while typing." ]] euroIn <- stepper "0" $ UI.valueChange euro dollarIn <- stepper "0" $ UI.valueChange dollar let rate = 0.7 :: Double withString f = maybe "-" (printf "%.2f") . fmap f . readMay dollarOut = withString (/ rate) <$> euroIn euroOut = withString (* rate) <$> dollarIn element euro # sink value euroOut element dollar # sink value dollarOut Steffen Jost FFP 10-9 Grundlagen GUI GTK+ Glade Beispiele Übersicht Viele verschiedene Ansätze für grafische Benutzeroberfläche/ Graphical User-Interface (GUI) in Haskell. High-Level Ansätze, primär basierend auf Functional Reactive Programming, momentan experimentell oder Plattform-gebunden. threepenny-gui FRP, stabil, unabhängig, benutzt Browser als Display Keera Hails FRP, recht neu, primär für Spiele gedacht, Backends für Qt, Gtk & Android Widgets Mid-Level Ansätze benutzen etablierte, imperative, Plattform-übergreifende Frameworks: Qt qtHaskell oder HsQML für Qt Quick wxWidgets wxHaskell wohl etabliert GTK+ Gtk2Hs wohl etabliert Steffen Jost FFP 10-10 Grundlagen GUI GTK+ Glade Beispiele Ereignisschleife Widgets Container GTK+ GTK+ steht für GIMP ToolKit, ein Plattform-übergreifendes Framework für Graphische Benutzeroberflächen, in C geschrieben Plattformen: Linux, Mac OS X, Windows,. . . Desktops: GNOME, Unity, Xfce, . . . Apps: GIMP, Geany, Chromium <V35,. . . Komponenten GLib: Kernbibliothek, Kompatibilitätsschicht,Thread-Handling Cairo: Bibliothek für 2D Vektor-Grafik GDK: Rendern, Bitmaps Pango: Textdarstellung, Internationalisierung ATK: Zugänglichkeit, z.B. Vergrößern, Vorlesen Glade: Grafisches GUI Design Tool Steffen Jost FFP 10-11 Grundlagen GUI GTK+ Glade Beispiele Ereignisschleife Widgets Container Gtk2Hs Gtk2Hs bietet Anbindung von Gtk+ 2.x an Haskell Benutzung rein durch Haskell möglich Plattform-unabhängig Unterstützung Unicode unterstützt, Typklasse GlibString string hat Instanzen für Data.Text oder String Unterstützt auch Gtk+ Tools: GUI-Beschreibungen mit Glade V3 für Gtk+ 2.x im Format GtkBuilder früher auch noch Libglade Achtung: Gtk+ 3.x Unterstützung noch immer sehr experimentell Dokumentation Tutorials & API auf https://wiki.haskell.org/Gtk2Hs Am einfachsten: Hooglen mit Suchstring +gtk, oder auch direkt mit Hayoo suchen Steffen Jost FFP 10-12 Grundlagen GUI GTK+ Glade Beispiele Ereignisschleife Widgets Container Installation Installation etwas schwierig; Entwicklerpakete von GTK+ 2.x müssen vorhanden sein! Mit Stack einfacher, siehe Alex’ Info-Seite. Benutzung über CIP-Pool der Informatik ohne Stack local> ssh -Y remote.cip.ifi.lmu.de cip> export PATH=~/.cabal/bin/:$PATH cip> cabal update cip> cabal install cabal-install cip> cabal update cip> cabal install gtk2hs-buildtools cip> cabal install gtk Damit sollte es funktionieren. 3 ersten Befehle ggf. nicht notwendig Falls es nicht klappt: anderen CIP-Rechner ausprobieren Versionsunterschiede Lokal installierte GHC Pakete löschen, d.h Verzeichnisse ~/.ghc und ~/.cabal einfach komplett löschen Steffen Jost FFP 10-13 Grundlagen GUI GTK+ Glade Beispiele Ereignisschleife Widgets Container Imperative GUI-Programmierung Benutzerinteraktion durch imperatives I/O: Eingabe: Ereignisse (Clicks, Tastendrücke, interne Nachrichten, Nachrichten von anderen Applikationen). Ausgabe: Graphische Darstellung von Inhalten und Steuerung ⇒ GUI-Programmierung mit Gtk2Hs erfolgt in IO-Monade / Gtk+ ist Ereignis-gesteuert: Hauptthread durchläuft eine Schleife in Gtk+ Ereignisse werden mit callback-Funktion behandelt Probleme/Konsequenzen: Ereignisbehandler bennötigen Zugriff auf einen gemeinsamen Zustand Steffen Jost FFP 10-14 Grundlagen GUI GTK+ Glade Beispiele Ereignisschleife Widgets Container Beispiel: Hello world! import Graphics.UI.Gtk main :: IO () main = do initGUI window <- windowNew button <- buttonNew set window [ containerBorderWidth := 10, containerChild := button ] set button [ buttonLabel := "Hello World" ] onClicked button callback1 onDestroy window mainQuit widgetShowAll window mainGUI callback1 :: IO () callback1 = putStrLn "Hello World" Steffen Jost FFP 10-15 Grundlagen GUI GTK+ Glade Beispiele Ereignisschleife Widgets Container Initialisierung und Ereignisschleife Module Graphics.UI.Gtk.General.General definiert: initGUI :: IO [String] Initialisierung von Gtk+ gemäß Kommandozeilenparameter Rückgabe ungenutzter Kommandozeilenparameter mainGUI :: IO () Übergibt die Kontrolle des Hauptthreads an die Gtk+ Ereignisschleife mainQuit :: IO () Beendet die Gtk+ Ereignisschleife Wird ein Ereignis ausgelöst, so wird die Ereignisschleife unterbrochen, um die entsprechende callback-Funktion auszuführen. Steffen Jost FFP 10-16 Grundlagen GUI GTK+ Glade Beispiele Ereignisschleife Widgets Container Beispiel: Hello world! (Teil 2) main = do initGUI window <- windowNew button <- buttonNew set window [ containerBorderWidth := 10, containerChild := button ] set button [ buttonLabel := "Hello World" ] onClicked button $ callback2 button onDestroy window mainQuit widgetShowAll window mainGUI callback2 :: ButtonClass o => o -> IO () callback2 b = do l <- get b buttonLabel -- GUI Call set b [ buttonLabel := (l ++ "!") ] -- GUI Call Steffen Jost FFP 10-17 Grundlagen GUI GTK+ Glade Beispiele Ereignisschleife Widgets Container Beispiel: Eingefrorene GUI (Teil 1) callback3 :: ButtonClass o => o -> IO () callback3 b = do l1 <- get b buttonLabel let n = read $ (words l1) !! 1 let fin = show $ fib n let l2 = "Fib " ++ (show $ n+1) ++ " = " ++ fin set b [ buttonLabel := l2 ] GUI friert hier bei der Berechnung größerer Fibonacci-Zahlen ein, d.h. reagiert nicht mehr auf Knöpfe, Größenänderung, etc. Steffen Jost FFP 10-18 Grundlagen GUI GTK+ Glade Beispiele Ereignisschleife Widgets Container Thread-Safety Wird ein Ereignis ausgelöst, so wird die Ereignisschleife unterbrochen, um die entsprechende callback-Funktion im Hauptthread auszuführen. Problem Berechnung der callback-Funktion friert GUI ein ⇒ aufwändige Berechnung im callback mit forkIO abhandeln Gtk+ ist jedoch nicht Thread-Safe, d.h. GUI-Funktionen dürfen nur aus dem Hauptthread heraus aufgerufen werden, sonst drohen unvorhersehbare Abstürze! postGUISync :: IO a -> IO a Führt Aktion im Hauptthread aus und blockiert aktuellen Thread bis Ergebnis zurückgegeben wird postGUIAsync :: IO () -> IO () Führt Aktion im Hauptthread aus, aktueller Thread läuft weiter Steffen Jost FFP 10-19 Grundlagen GUI GTK+ Glade Beispiele Ereignisschleife Widgets Container Thread-Safety timeoutAdd :: IO Bool -> Int -> IO HandlerId timeoutAddFull :: IO Bool -> Priority -> Int -> IO HandlerId IO-Aktion wird regelmäßig im Hauptthread ausgeführt, bis False zurückgegeben wird; Optionen: 1 Aufrufe von GUI-Funktion im Hauptthread mit MVar anfordern, welche regelmäßig abgefragt werden 2 Falls nicht mit -threaded kompiliert wird, reicht es, Ereignisschleife regelmäßig mit yield zu unterbrechen: timeoutAddFull (yield >> return True) priorityDefaultIdle 100 -- ms Fallstrick Diese Funktionen aus Modul Graphics.UI.Gtk.General.General nicht mit gleichnamigen Modul System.Glib.MainLoop verwechseln, da letztere nicht im Gtk+ Hauptthread ausgeführt werden Steffen Jost FFP 10-20 Grundlagen GUI GTK+ Glade Beispiele Ereignisschleife Widgets Container Beispiel: Eingefrorene GUI (Teil 1) callback3 :: ButtonClass o => o -> IO () callback3 b = do l1 <- get b buttonLabel let n = read $ (words l1) !! 1 let fin = show $ fib n let l2 = "Fib " ++ (show $ n+1) ++ " = " ++ fin set b [ buttonLabel := l2 ] GUI friert hier bei der Berechnung größerer Fibonacci-Zahlen ein, d.h. reagiert nicht mehr auf Knöpfe, Größenänderung, etc. Steffen Jost FFP 10-21 Grundlagen GUI GTK+ Glade Beispiele Ereignisschleife Widgets Container Beispiel: Eingefrorene GUI (Teil 2) callback4 :: ButtonClass o => o -> IO () callback4 b = do l1 <- get b buttonLabel void $ forkIO $ do let n = read $ (words l1) !! 1 let fin = show $ fib n let l2 = "Fib " ++ (show $ n+1) ++ " = " ++ fin seq l2 $ postGUIAsync $ set b [ buttonLabel := l2 ] Einfrieren verhindert: Berechnung auslagert in eigenen Thread seq notwendig um Berechnung zu erzwingen postGUIAsync um zu Gtk+-Thread zurückzukehren Wichtig, geht nut falls mit -threaded kompiliert; oder Ereignisschleife mit yield regelmäßig unterbrechen: timeoutAddFull (yield >> return True) priorityDefaultIdle 100 Steffen Jost FFP 10-22 Grundlagen GUI GTK+ Glade Beispiele Ereignisschleife Widgets Container Widgets Element einer GUI wird oft als Widget bezeichnet = Window & Gadget Widgets haben verschiedene Attribute, welche zur Laufzeit verändert werden können Beispiel: Text von einem Label oder einem Knopf Widgets können andere Widgets enthalten Beispiele: Window, Dialog, etc. Die Anordnung enthaltener Widgets wird durch das übergeordnete Widget bestimmt; die Attribute der Widgets können dies beeinflussen Beispiel: Padding Attribut von Container-Widgets Steffen Jost FFP 10-23 Grundlagen GUI GTK+ Glade Beispiele Ereignisschleife Widgets Container Attribute lesen Modul System.Glib.Attributes definiert: data ReadWriteAttr o a b Attribut eines Objekts o mit Lesetyp a und Schreibtyp b type Attr o a = ReadWriteAttr o a a Gewöhnliches Attribut mit identischem Typ für beide Zugriffe type ReadAttr o a = ReadWriteAttr o a () Attribut kann nur gelesen werden type WriteAttr o b = ReadWriteAttr o () b Attribut kann nur geschrieben werden get :: o -> ReadWriteAttr o a b -> IO a Attribut auslesen Beispiel value <- get button buttonLabel Steffen Jost FFP 10-24 Grundlagen GUI GTK+ Glade Beispiele Ereignisschleife Widgets Container Attribute schreiben Modul System.Glib.Attributes definiert: set :: o -> [AttrOp o] -> IO () data AttrOp o = = forall a b. (ReadWriteAttr o a b) := b | forall a b. (ReadWriteAttr o a b) :~ (a -> b) | forall a b. (ReadWriteAttr o a b) :=> (IO b) | ... Attribute werden mit speziellen Schreipoperationen geändert AttrOp o ist ein existentieller Datentyp, d.h. z.B. in einer Liste des Typs [AttrOp o] können die Elemente verschiedene Typen für a und b besitzen Beispiel set button [ buttonLabel := "OK", buttonFocusOnClick := False ] Steffen Jost FFP 10-25 Grundlagen GUI GTK+ Glade Beispiele Ereignisschleife Widgets Container Attribute schreiben Modul System.Glib.Attributes definiert: := Zuweisung eines Wertes :~ Anwendung einer Funktion auf aktuellen Wert :=> Zuweisung des Wertes einer IO Aktion :~> Anwendung einer IO-Funktion auf aktuellen Wert ::= Anwendung einer Funktion, welche als Argument das aktuelle Objekt bekommt ::~ Anwendung einer Funktion, welche als Argumente aktuelles Objekt und aktuellen Wert bekommt Alle Attribut-Operationen sind als infixr 0 deklariert Steffen Jost FFP 10-26 Grundlagen GUI GTK+ Glade Beispiele Ereignisschleife Widgets Container Widgets Allgemein Modul Graphics.UI.Gtk.Abstract.Widget definiert: widgetShowAll :: WidgetClass self => self -> IO () Zeigt vorbereitetes Widget auf dem Bildschirm an widgetHideAll :: WidgetClass self => self -> IO () Versteckt Widget vorübergehend widgetDestroy :: WidgetClass self => self -> IO () Entfernt Widget dauerhaft onDestroy :: WidgetClass w => w -> IO () -> IO (ConnectId w) Wird beim Schliessen des Widgets ausgeführt Typ data ConnectId o stammt aus Modul System.Glib.Signals Steffen Jost FFP 10-27 Grundlagen GUI GTK+ Glade Beispiele Ereignisschleife Widgets Container Buttons Zu jeder wichtigen Art von Widgets gibt es entsprechende Module, welche spezialisierte Funktionen zur Verfügung stellen. Modul Graphics.UI.Gtk.Buttons.Button definiert: onClicked :: ButtonClass b => b -> IO () -> IO (ConnectId b) Registriert Behandler für Ereignis, Knopf gedrückt onPressed :: ButtonClass b => b -> IO () -> IO (ConnectId b) Bereits wenn der Knopf gedrückt wird onReleased :: ButtonClass b => b -> IO () -> IO (ConnectId b) Wenn der Knopf losgelassen wird onEnter :: ButtonClass b => b -> IO () -> IO (ConnectId b) Wenn der Cursor auf den Knopf kommt Steffen Jost FFP 10-28 Grundlagen GUI GTK+ Glade Beispiele Ereignisschleife Widgets Container Container Container-Widgets können Kinder/Children-Widgets enthalten. Typklasse Container: Widgets mit ein oder mehreren Kindern containerAdd :: ConatinerClass self, WidgetClass widget => self -> widget -> IO () Kind hinzufügen containerRemove :: ConatinerClass self, WidgetClass widget => self -> widget -> IO () Kind entfernen containerGetChildren :: ConatinerClass self => self -> IO [Widget] Kinder auslesen containerForeach :: ConatinerClass self => self -> (Widget -> IO ()) -> IO () Funktion auf Kinder anwenden Geordnetes Hinzufügen über spezielle Unterklassen-Funktion Hinzufügen auch über Attribute wie im “Hello Gtk”-Beispiel Spezielle Kind-Attribute , z.B. zur Anordnung, vorhanden set container [ attr child := value ] Steffen Jost FFP 10-29 Grundlagen GUI GTK+ Glade Beispiele Ereignisschleife Widgets Container Windows Beispiel: Window ist einer Unterklasse von Container und Bin Typklasse Bin: Widgets mit genau einem Kind binGetChild :: BinClass self => self -> IO (Maybe Widget) Kind auslesen Ein Fenster beinhaltet also nur genau ein Kind-Objekt. Modul Graphics.UI.Gtk.Windows.Window definiert: windowNew :: IO Window erstellt neues Fenster windowDefaultWidth :: WindowClass self => Attr self Int Breite bei Initialisierung windowTitle :: (WindowClass self) => Attr self string Fenstertitel setzen windowPresent :: WindowClass self => self -> IO () Fenster nach vorne schieben Steffen Jost FFP 10-30 Grundlagen GUI GTK+ Glade Beispiele Ereignisschleife Widgets Container Elemente anordnen Container zur horizontalen Ausrichtung aus Modul Graphics.UI.Gtk.Layout.HBox Vertikale Ausrichtung analog mit VBox hBoxNew :: Bool -> Int -> IO HBox Container erstellen; 1.Arg: Kinder mit gleichmäßigem Platz anordnen, 2.Arg: Freie Pixel zwischen Kindern Festlegen wie Elemente sich anpassen mit data Packing aus Modul Graphics.UI.Gtk.Abstract.Box: PackGrow Kinder wachsen mit Eingabefeld PackNatural Kinder bestimmen eigene Größe PackRepel Rand um Kind wächst Menu Leiste Knöpfe Einfügen der Kinder mit boxPackEnd oder boxPackStart :: (BoxClass self, WidgetClass child) => self -> child -> Packing -> Int -> IO () Int-Wert legt zusätzlichen Abstand fest Steffen Jost FFP 10-31 Grundlagen GUI GTK+ Glade Beispiele Ereignisschleife Widgets Container Beispiel: Horizontale Packung main = do initGUI window <- windowNew hbox <- hBoxNew True 10 button1 <- buttonNewWithLabel "Button 1" button2 <- buttonNewWithLabel "Button 2" set window [ windowDefaultWidth := 200, windowDefaultHeight := 200, containerBorderWidth := 10, containerChild := hbox ] boxPackStart hbox button1 PackGrow 0 boxPackStart hbox button2 PackGrow 0 ... Steffen Jost FFP 10-32 Grundlagen GUI GTK+ Glade Beispiele Ereignisschleife Widgets Container Ausrichtung Box erlaubt keine Festlegung der Ausrichtung. Alignment-Widgets aus Modul Graphics.UI.Gtk.Layout.Alignment machen das. Alignment-Widgets sind in der BinClass, d.h. haben genau ein Kind, dessen Ausrichtung diese bestimmen. alignmentNew :: Float -> Float -> Float -> Float -> IO Alignment .. <- alignmentNew xalign yalign xscale yscale xalign, yalign Ausrichtung: 0=Links/Oben bis 1=Rechts/Unten; 0.5=Mitte xscale, yscale Skalierung des Kindes: 0=wächst nicht; 1=wächst voll ⇒ Skalierung=1 impliziert, das Ausrichtung keine Auswirkung hat, da das Kind sowieso den vollen Raum einnimmt! Steffen Jost FFP 10-33 Grundlagen GUI GTK+ Glade Beispiele Ereignisschleife Widgets Container Tabellen Tabellen erlauben detaillierte Platzierung; Container-Unterklasse Aus Modul Graphics.UI.Gtk.Layout.Table: tableNew :: -> -> -> Int -- Anzahl Zeilen Int -- Abzahl Spalten Bool -- Alle Felder gleich groß? IO Table tableAttachDefaults ::(TableClass self,WidgetClass widget) => self -> widget -- Tablle, einzufügendes Kind -> Int -> Int -- Linke und Rechte Spalte -> Int -> Int -- Obere und Untere Zeile -> IO () Kind-Widgets können mehrere Zellen einer Tabelle belegen tableAttach erlaubt zusätzliche Angaben zu Rand und Verhalten bei Größenänderung. Defaults: [Expand,Fill] Steffen Jost FFP 10-34 Grundlagen GUI GTK+ Glade Beispiele Ereignisschleife Widgets Container Beispiel für Tabellenlayout createButton :: Table -> (Int, Int) -> IO Button createButton table (x,y) = do b <- buttonNew onClicked b (myButtonPressAction (x,y)) tableAttachDefaults table b x (x+1) y (y+1) return b main = do ... table <- tableNew ysize xsize True let rows = [ [ (x,y) | x <- [0..xsize-1] ] | y <- [0..ysize-1] ] buttons <- mapM (mapM (createButton table)) rows ... myButtonPressAction :: (Int, Int) -> IO () Steffen Jost FFP 10-35 Grundlagen GUI GTK+ Glade Beispiele Menüs und Werkzeugleisten Glade Glade ist ein grafisches Werkzeug zum Erstellen statischer GUIs Glade V3 für Gtk+ 2.x, GtkBuilder Format Man legt damit fest: Welche Widgets gibt es in der Anwendung Welches Widget ist in welchem anderen wo enthalten Welche Attribute anfangs eingestellt sind, z.B. Ausrichtung, Größe, Beschriftung, etc. Das Ergebnis wird sofort auf dem Bildschirm angezeigt Die GUI wird durch eine XML Datei beschrieben, welche erst zur Laufzeit eingelesen wird und mitgeliefert werden muss. Alternativ kann die XML Beschreibung als auch String im Haskell Code untergebracht werden. Steffen Jost FFP 10-36 Grundlagen GUI GTK+ Glade Beispiele Menüs und Werkzeugleisten Builder Modul Graphics.UI.Gtk.Builder: explizit importieren builderNew :: IO Builder Leeres Builder Objekt erzeugen builderAddFromFile :: GlibFilePath fp => Builder -> fp -> IO () Definition aus Datei zu Builder hinzufügen builderAddFromString :: Builder -> string -> IO () Definition aus Datei zu Builder hinzufügen builderGetObject :: (GObjectClass cls, GlibString string) => Builder -> (GObject -> cls) -> string -> IO cls 2.Arg.: Type-cast, z.B. castToWindow, castToButton, . . . 3.Arg.: Namen-Attribut des gesuchten Objektes in XML Einlesen des XML wird erst zur Laufzeit durchgeführt. Hier können Laufzeitfehler entstehen! falsch geschriebene Namen, falsche Casts, etc. Steffen Jost FFP 10-37 Grundlagen GUI GTK+ Glade Beispiele Menüs und Werkzeugleisten Zentraler GUI Datentyp Problem Einlesen des XML wird erst zur Laufzeit durchgeführt. Hier können Laufzeitfehler entstehen! falsch geschriebene Namen, falsche Casts, etc. Empfehlung Bei Initialisierung der GUI alle Elemente auf einmal anlegen und in zentralen Datentypen ablegen. Passt XML Beschreibung nicht zum Code, werden Fehler am Programmstart erkannt; danach garantiert Typsystem alles weitere. Es müssen nur die Widgets eingelesen werden, auf die auch Zugriffe notwendig sind Alignment, Box,. . . Dynamische Elemente wie bisher später hinzufügen GUI Datentyp kann an Callback-Funktionen übergeben werden Evtl. sinnvoller, einen Datentyp pro Fenster-Klasse anzulegen siehe Aufgabe A14-1 Steffen Jost FFP 10-38 Grundlagen GUI GTK+ Glade Beispiele Menüs und Werkzeugleisten Beispiel: Builder data MainGUI = MainGUI { mainWindow :: Window , mainVbox :: VBox , mainLabel :: Label , startButton :: Button } main = do initGUI gui <- loadGUI -- selbstdefiniert fkt onDestroy (mainWindow gui) mainQuit onClicked (startButton gui) $ fooAction gui widgetShowAll (mainWindow gui) -- zeigt alle Kinder, nicht mainGUI -- nur die gui enthaltenen loadGUI = do -- all XML loading errors must occur here builder <- builderNew builderAddFromFile builder "myGUI.glade" mainWindow <- builderGetObject builder castToWindow mainVbox <- builderGetObject builder castToVBox mainLabel <- builderGetObject builder castToLabel startButton <- builderGetObject builder castToButton return $ MainGUI {..} Steffen Jost FFP "window1" "vbox1" "label1" "button1" 10-39 Grundlagen GUI GTK+ Glade Beispiele Menüs und Werkzeugleisten Menü und Werkzeugleiste Menü, Werkzeugleisten und Tastenkombinationen starten Aktionen, welche durch den Typ Action repräsentiert werden. Aktionen können aktiviert und deaktiviert werden. actionNew :: String -> String -> Maybe String -> Maybe String -> IO Action onActionActivate ----- name : unique name for the action label : displayed in items & btns tooltip stockId: icon to be displayed :: ActionClass self => self -> IO () -> IO (ConnectId self) actionSetVisible :: ActionClass self => self->Bool->IO() actionSetSensitive :: ActionClass self => self->Bool->IO() Steffen Jost FFP 10-40 Grundlagen GUI GTK+ Glade Beispiele Menüs und Werkzeugleisten XML Menübeschreibung uiDecl = "\ \<ui>\ \ <menubar>\ \ <menu action=\"FILE_MENU\">\ \ <menuitem action=\"QUIT\"/>\ \ </menu>\ \ </menubar>\ \ <toolbar>\ \ <toolitem action=\"QUIT\"/>\ \ </toolbar>\ \</ui>" Steffen Jost FFP 10-41 Grundlagen GUI GTK+ Glade Beispiele Menüs und Werkzeugleisten Menu erstellen createMenu :: VBox -> IO () createMenu box = do actFileMenu <- actionNew "FILE_MENU" "File" Nothing Nothing actQuit <- actionNew "QUIT" "Quit" (Just "Exit Tic Tac Toe") (Just stockQuit) onActionActivate actQuit mainQuit actGroup <- actionGroupNew "ACTION_GROUP" mapM (actionGroupAddAction actGroup) [actFileMenu,actQuit] ui <- uiManagerNew uiManagerAddUiFromString ui uiDecl uiManagerInsertActionGroup ui actGroup 0 Just menubar <- uiManagerGetWidget ui "/ui/menubar" boxPackStart box menubar PackNatural 0 Just toolbar <- uiManagerGetWidget ui "/ui/toolbar" boxPackStart box toolbar PackNatural 0 Steffen Jost FFP 10-42 Grundlagen GUI GTK+ Glade Beispiele Tic Tac Toe Solitaire Beispielapplikation: Tic Tac Toe Idee für sehr simple GUI: Erstelle 9 Buttons, blank, d.h. ohne Label. Klick auf Button ist Zug des menschlichen Spielers Ereignisbehandlung eines Buttons: Label des Buttons setzen Antwort des Computerspielers berechnen Globales Label für Statusanzeige ⇒ Ereignisbehandler eines Buttons benötigt Zugriff auf Status-Label Spielzustand alle anderen Button-Labels Steffen Jost FFP 10-43 Grundlagen GUI GTK+ Glade Beispiele Tic Tac Toe Solitaire API für Tic Tac Toe Modell Logik des Spieles in seperatem Modul, welches u.a. bereitstellt: data State = State { turn, winner :: Maybe Player , board :: Board (Maybe Player) , lastMove :: Move, size :: Int } data Player = X | O newtype Pos = Pos (Int,Int) type Move = (Player, Pos) newGame showStatus makeMove computerMove :: :: :: :: deriving (Eq, Ord, Show) deriving (Eq, Ord, Show) Int -> Int -> Player -> State State -> String State -> Move -> Maybe State Int -> State -> Maybe Move Pos beginnt bei (1,1) und endet bei (size state, size state) computerMove bekommt Spielstärke als Argument Steffen Jost FFP 10-44 Grundlagen GUI GTK+ Glade Beispiele Tic Tac Toe Solitaire Versuch einer zirkulären Konstruktion data App = App { stateRef :: IORef State -- Zustand Spiel , buttons :: Map.Map Pos Button } buttonPress :: App -> Pos -> IO () -- btn event handler createButton :: App -> Table -> Pos -> IO Button createButton app table (Pos (x,y)) = do b <- buttonNew onClicked b (buttonPress app (x,y)) tableAttachDefaults table b x (x+1) y (y+1) return b let app = App { stateRef = newIORef initialState , buttons = fold (createButton app table) Map.empty [Pos (x,y)| x <- [1..gameSize], y <- [1..gameSize]] } -- DOES NOT WORK: createButton is monadic! Alternative: Event-Handler mit 2. Schleife installieren Steffen Jost FFP 10-45 Grundlagen GUI GTK+ Glade Beispiele Tic Tac Toe Solitaire Zirkuläre monadische Programme Monadische Rekursion: module Control.Monad.Fix where class Monad m => MonadFix m where mfix :: (a -> m a) -> m a instance MonadFix IO Den Knoten zuziehen (tying the knot): do app <- mfix $ \ app' -> do buttons <- foldM (createButton app' table) Map.empty [Pos (x,y)| x<-[1..gameSize], y<-[1..gameSize]] return $ App { stateRef = newIORef initialState , buttons = buttons } Steffen Jost FFP 10-46 Grundlagen GUI GTK+ Glade Beispiele Tic Tac Toe Solitaire Rekursive Do-Notation Let-Definition in Haskell sind immer (wechselseitig) rekursiv: > let { foo x =x:bar(x+1); bar y =y:foo(-y) } in take 9 $ foo 0 [0,1,-1,0,0,1,-1,0,0] Mit GHC-Spracherweiterung DoRec auch monadisch erlaubt: {-# LANGUAGE DoRec #-} do -- normale monadische Aktionen rec -- rekursiver Abschnitt beginnt buttons <- foldM (createButton app table) Map.empty [Pos (x,y)| x<-[1..gameSize], y<-[1..gameSize]] let app = App { stateRef = newIORef initialState , buttons = buttons } Steffen Jost FFP 10-47 Grundlagen GUI GTK+ Glade Beispiele Tic Tac Toe Solitaire Solitaire Code-Beispiel aus Blog-Eintrag vom 19. März 2013 “Creating board games in Haskell in 100 lines of code” im Keera Studios Blog http://keera.co.uk/blog > git clone https://github.com/keera-studios/gtk-helpers.git 100 spezielle Zeilen + Framework für generische Brettspiele von Keera Studios + Grafiken Steffen Jost FFP 10-48 Grundlagen GUI GTK+ Glade Beispiele Tic Tac Toe Solitaire API: Game.Board.BasicTurnGame class PlayableGame a index tile player piece | a -> index, a -> tile, a -> player, a -> piece where curPlayer :: a -> player allPieces :: a -> [(index, index, player, piece)] allPos :: a -> [(index, index, tile)] moveEnabled :: a -> Bool canMove :: a -> player -> (index, index) -> Bool canMoveTo :: a -> player -> (index, index) -> (index, index) -> Bool move :: a -> player -> (index, index) -> (index, index) -> [GameCha activateEnabled :: a -> Bool canActivate :: a -> player -> (index, index) -> Bool activate :: a -> player -> (index, index) -> [GameChange index player p applyChange :: a -> GameChange index player piece -> a applyChanges :: a -> [GameChange index player piece] -> a data GameChange index player piece = AddPiece (index, index) player piece | RemovePiece (index, index) | MovePiece (index, index) (index, index) data GameState index tile player piece = GameState { curPlayer' :: player , boardPos :: [(index, index, tile)] , boardPieces' :: [(index, index, player, piece)] } Steffen Jost FFP 10-49 Grundlagen GUI GTK+ Glade Beispiele Quellen GTK+ Projekt: http://www.gtk.org/ gtk2hs Projekt-Homepage und Dokumentation: http://projects.haskell.org/gtk2hs/ Glade Tutorials auf Gtk2Hs Projektseite “Reflections on programming” Blogpost von Rickard Lindberg Real World Haskell, Kapitel 23 “GUI programming” von Bryan O’Sullivan, Don Stewart, John Goerzen Keera Studios, http://keera.co.uk/blog Blog über Spieleentwicklung mit Haskell für Gtk und Android threepenny-gui: http://github.com/HeinrichApfelmus/threepenny-gui Steffen Jost FFP 10-50