It had been over two years since I last looked at them, but I returned to Haskell, GUI and GTK4 on Windows. This time, things were more straightforward. To set things up again, I added the following MSYS2 packages to the Stack-supplied MSYS2 with stack exec -- pacman -S:
|
1 2 3 4 5 6 |
stack exec -- pacman -Syu stack exec -- pacman -S mingw-w64-x86_64-pkgconf stack exec -- pacman -S mingw-w64-x86_64-gobject-introspection stack exec -- pacman -S mingw-w64-x86_64-gtksourceview5 stack exec -- pacman -S mingw-w64-x86_64-gtk4 stack exec -- pacman -S mingw-w64-x86_64-atk |
and set PKG_CONFIG_PATH to the mingw64\lib\pkgconfig directory and XDG_DATA_DIRS to the mingw64\share directory (of the Stack-supplied MSYS2). I found it was necessary to install the MSYS2 packages before building the Haskell packages that depend upon them.
MSYS2 package mingw-w64-x86_64-gtk4 does not have a dependancy on mingw-w64-x86_64-atk, but Haskell package gi-gtk >= 4.0 has a dependency on gi-atk. I asked a question about that at the haskell-gi GitHub repository.
A basic gktTest
I named the example package gtkTest, as before, with Main.hs as follows (being the haskell-gi repository’s current “Hello! World” program):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
{-# LANGUAGE ImplicitParams #-} {-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedStrings #-} module Main ( main ) where import Control.Monad ( void ) import Data.GI.Base ( AttrOp (..), new, set ) import qualified GI.Gtk as Gtk activate :: Gtk.Application -> IO () activate app = do button <- new Gtk.Button [ #label := "Click me" , On #clicked ( ?self `set` [ #sensitive := False , #label := "Thanks for clicking me" ] ) ] window <- new Gtk.ApplicationWindow [ #application := app , #title := "Hi there" , #child := button ] window.show main :: IO () main = do app <- new Gtk.Application [ #applicationId := "haskell-gi.example" , On #activate (activate ?self) ] void $ app.run Nothing |
with a package.yaml specifying dependencies:
|
1 2 3 4 |
dependencies: - base >= 4.7 && < 5 - gi-gtk >= 4.0 - haskell-gi-base |
and with Stack project-level configuration:
|
1 2 3 4 5 6 |
snapshot: lts-22.39 # GHC 9.6.6 extra-deps: - gi-gtk-4.0.9 - gi-gdk-4.0.9 - gi-gsk-4.0.8 |
I sought to understand how this program worked and, in particular, the use of ?self and app.run.
new
new is used in connection with app, window and button. Module Data.GI.Base.Constructible provides a type class Constructible with a default instance:
|
1 2 3 4 5 6 |
class Constructible obj (tag :: AttrOpTag) where new :: MonadIO m => (ManagedPtr obj -> obj) -> [AttrOp obj tag] -> m obj instance {-# OVERLAPPABLE #-} (GObject obj, tag ~ 'AttrConstruct) => Constructible obj tag where new = constructGObject |
In the code extract above, and other extracts, I have renamed type variables for consistency between extracts.
GI.Gtk.Objects.Application.Application is an instance of GObject.
Data.GI.Base.GObject.constructGObject makes use of Data.GI.Base.Signals.on:
|
1 2 3 4 5 6 7 8 9 10 11 |
on :: forall obj info m. (GObject obj, MonadIO m, SignalInfo info) => obj -> SignalProxy obj info -> ((?self :: obj) => HaskellCallbackType info) -> m SignalHandlerId on o p c = liftIO $ connectSignal @info o w SignalConnectBefore (proxyDetail p) where w :: obj -> HaskellCallbackType info w parent = let ?self = parent in c |
and on binds the implicit parameter ?self.
Given the language extension ImplicitParams, the implicit parameter ?self can be passed to activate.
AttrOp
Data.GI.Base.Attributes.AttrOp is a type with a number of data constructors, including (:=) (assign a value to an attribute) and On (connect the given signal to a signal handler.).
|
1 2 3 4 5 |
On :: forall obj info (tag :: AttrOpTag). (GObject obj, SignalInfo info) => SignalProxy obj info -> ((?self :: obj) => HaskellCallbackType info) -> AttrOp obj tag |
app.run Nothing
In the example, app.run means the same as #run app.
Given the language extension OverloadedRecordDot, app.run is equivalent to getField @"run" app.
Module GHC.Record provides type class HasField which promises getField:
|
1 2 |
class HasField t obj p | t obj -> p where getField :: obj -> p |
Given the language extension VisibleTypeApplications, @"run" in getField @"run" app specifies the type t in type class HasField t obj p. Given the language extentions DataKinds, "run" is a type-level literal of kind String. As app is of type Application, that specifies type obj in type class HasField t obj p.
Module GI.Gtk.Objects.Application provides an instance of HasField
|
1 2 3 4 5 6 7 8 9 10 |
import qualified Data.GI.Base.Overloading as O import qualified GHC.Records as R ... #if MIN_VERSION_base(4,13,0) instance ( info ~ ResolveApplicationMethod t Application , O.OverloadedMethod info Application p , R.HasField t Application p ) => R.HasField t Application p where getField = O.overloadedMethod @info #endif |
Including the instance head (HasField t Application p) as a constraint in the instance context is a ‘trick’ which permits polymorphic fields.
Data.GI.Base.Overloading.overloadedMethod is promised by type class OverloadedMethod:
|
1 2 |
class OverloadedMethod info obj signature where overloadedMethod :: obj -> signature |
Given the language extension VisualTypeApplications, the @info in O.overloadedMethod @info specifies the type info in type class OverloadedMethod info obj s. info is constrained to be equivalent to ResolveApplicationMethod t Application. When t is the type literal "run", that is equal to ApplicationRunMethodInfo (see further below).
Module GI.Gio.Objects.Application provides an instance of OverloadedMethod:
|
1 2 3 4 5 |
instance ( signature ~ (Maybe ([[Char]]) -> m Int32) , MonadIO m , IsApplication obj ) => O.OverloadedMethod ApplicationRunMethodInfo obj signature where overloadedMethod = applicationRun |
|
1 2 3 4 5 |
applicationRun :: (B.CallStack.HasCallStack, MonadIO m, IsApplication obj) => obj -> Maybe ([[Char]]) -> m Int32 |
and module GI.Gtk.Objects.Application provides a type family:
|
1 2 3 4 5 6 |
import qualified Data.Kind as DK import qualified GI.Gio.Objects.Application as Gio.Application ... type family ResolveApplicationMethod (t :: Symbol) (obj :: DK.Type) :: DK.Type where ... ResolveApplicationMethod "run" obj = Gio.Application.ApplicationRunMethodInfo |
A less basic gtkTest
I tried a less basic version of gtkTest, using a Grid of three Button values, with Main.hs:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
{-# LANGUAGE ImplicitParams #-} {-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedStrings #-} module Main ( main ) where import Control.Monad ( void ) import Data.GI.Base ( AttrOp (..), new, set ) import Data.Text ( Text ) import qualified GI.Gtk as Gtk activate :: Gtk.Application -> IO () activate app = do grid <- new Gtk.Grid [] button1 <- mkButton "Click me!" button2 <- mkButton "No, click me!" button3 <- mkButton "Better click me!" #attach grid button1 0 0 1 1 #attach grid button2 1 0 1 1 #attach grid button3 0 1 2 1 window <- new Gtk.ApplicationWindow [ #application := app , #title := "Hi there" , #child := grid ] window.show mkButton :: Text -> IO Gtk.Button mkButton label = new Gtk.Button [ #label := label , On #clicked ( ?self `set` [ #sensitive := False , #label := "Thanks for clicking me" ] ) , #hexpand := True ] main :: IO () main = do app <- new Gtk.Application [ #applicationId := "haskell-gi.example" , On #activate (activate ?self) ] void $ app.run Nothing |
The result was as below, but I had to specify #hexpand := True for the buttons to take advantage of the available horizontal space. That is because an ApplicationWindow is a single-child container while a Grid is a multi-child container; the default behaviour is different.

A gtkTest with input and output
I tried a further variation with input (two Entry values) and output (a Label value):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
{-# LANGUAGE ImplicitParams #-} {-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedStrings #-} module Main ( main ) where import Control.Monad ( void ) import Data.GI.Base ( AttrOp (..), get, new, on, set ) import Data.Text ( Text ) import qualified Data.Text as T import qualified Data.Text.Read as T import qualified GI.Gtk as Gtk activate :: Gtk.Application -> IO () activate app = do grid <- new Gtk.Grid [ #columnSpacing := 5 , #rowSpacing := 5 , #marginBottom := 5 , #marginEnd := 5 , #marginStart := 5 , #marginTop := 5 ] labelHeight <- mkLabel "Height:" labelRadius <- mkLabel "Radius:" labelResult <- mkLabel "Volume:" labelValue <- mkLabel "None" entryHeight <- mkEntry entryRadius <- mkEntry let update = do hText <- entryHeight `get` #text rText <- entryRadius `get` #text case (T.rational hText, T.rational rText) of (Right (h, _), Right (r, _)) -> labelValue `set` [ #label := T.show $ cone h r ] (_, _) -> labelValue `set` [ #label := "One or both of height and radius is invalid."] void $ on entryHeight #changed update void $ on entryRadius #changed update #attach grid labelHeight 0 0 1 1 #attach grid labelRadius 0 1 1 1 #attach grid labelResult 0 2 1 1 #attach grid labelValue 1 2 1 1 #attach grid entryHeight 1 0 1 1 #attach grid entryRadius 1 1 1 1 window <- new Gtk.ApplicationWindow [ #application := app , #title := "Volume of a cone" , #child := grid ] window.show mkLabel :: Text -> IO Gtk.Label mkLabel label = new Gtk.Label [ #label := label , #halign := Gtk.AlignStart ] mkEntry :: IO Gtk.Entry mkEntry = new Gtk.Entry [ #halign := Gtk.AlignStart ] main :: IO () main = do app <- new Gtk.Application [ #applicationId := "haskell-gi.example" , On #activate (activate ?self) ] void $ app.run Nothing cone :: Double -> Double -> Double cone h r = h * pi * r * r / 3.0 |
The use of Data.Text.show requires Haskell package text >= 2.1.2.
The default value of halign is AlignFill, which centres when there is no meaningful way to stretch.
The result was as below:
