GTK+ Programming with Haskell
As described on its webpage, GTK+, or the GIMP Toolkit, is a multi-platform toolkit for creating user interfaces. In this video we will use the haskell-gi suite of packages to build a simple GTK+ application with Haskell.
Show Notes
We begin with an empty Cabal project called gtk-intro
.
name: gtk-intro
version: 0.1.0.0
license: MPL-2.0
license-file: LICENSE
build-type: Simple
cabal-version: >=1.10
executable gtk-intro
main-is: Main.hs
build-depends: base >=4.11 && <4.12
hs-source-dirs: src
default-language: Haskell2010
To write our GTK+ application, we’re going to use the haskell-gi suite of packages. For this simple application, we only need gi-gtk and haskell-gi-base.
executable gtk-intro
main-is: Main.hs
build-depends: base >=4.11 && <4.12
, gi-gtk
, haskell-gi-base
hs-source-dirs: src
default-language: Haskell2010
By default, the overloading feature of haskell-gi
is enabled, leveraging the OverloadedLabels
language extension of GHC. Overloading is used to refer to methods, properties, and signals, of GObject types, such as GTK+ widgets.
The downsides of overloading are longer compilation times and incompatibility with GHC 8.2. If you want to use GHC 8.2, add haskell-gi-overloading
with version 0.0.*
as a dependency in your Cabal file, and use the non-overloaded functions. We’ll see examples of using the GTK+ bindings, both with and without overloading, shortly.
An Empty Window
In Main.hs
, we begin by importing Data.GI.Base
, which provides the generic functions for working with GObject
types, and import GI.Gtk
qualified as Gtk
, which includes widgets and functions specific to GTK+.
import Data.GI.Base
import qualified GI.Gtk as Gtk
Next, we redefine the main
action to be a do block, initializing GTK+ with the init
function and Nothing
as a parameter, and starting the main loop using Gtk.main
.
main :: IO ()
main = do
Gtk.init Nothing
Gtk.main
We create a window using the new
function, the Window
constructor, and a list of properties, setting the title to “Introduction”. When the window is destroyed, we quit the main loop, so that the application exits when the window is closed by the user. Finally, we use the #showAll
function to make the window and all its children visible. Currently, we don’t have any child widgets in the window, but we’ll add a few soon.
main :: IO ()
main = do
Gtk.init Nothing
win <- new Gtk.Window [#title := "Introduction"]
on win #destroy Gtk.mainQuit
#showAll win
Gtk.main
To use overloaded labels and strings, we need to enable some language extensions at the top of the file.
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedStrings #-}
module Main where
OK, let’s run this program. With GHCi open, we type :main
to run the main
action.
> :main
We see a small window with no contents.
Overloading
The haskell-gi packages support overloaded labels. Already, we’ve used overloading for three different GTK+ concepts:
- Setting the
#title
, a property of theWindow
object - Attaching a handler to
#destroy
, a signal available on allWidget
objects - Calling
#showAll
, a function available on allWidget
objects
We could rewrite our code to not use overloading.
main :: IO ()
main = do
Gtk.init Nothing
win <- Gtk.windowNew Gtk.WindowTypeToplevel
Gtk.windowSetTitle win "Introduction"
Gtk.onWidgetDestroy win Gtk.mainQuit
Gtk.widgetShowAll win
Gtk.main
This involves:
- using prefixed versions of
new
, likewindowNew
that now requires aWindowType
parameter, - setting the title after creating the window, using
windowSetTitle
, - using widget-specific versions of signal functions, like
onWidgetDestroy
, to connect a signal to a callback, and - using prefixed functions for widgets, like
widgetShowAll
.
As I mentioned in the introduction, overloading is not supported together GHC 8.2.x, so you might not be able to use it, depending on your requirements.
Adding Child Widgets
Before we add some widgets to our window, we’ll make it bigger. The resize
function takes as arguments a window, a width, and a height, and resizes the window.
main = do
Gtk.init Nothing
win <- new Gtk.Window [#title := "Introduction"]
on win #destroy Gtk.mainQuit
#resize win 640 480
#showAll win
Gtk.main
Now, let’s add a label to our window. The #label
property is used to specify the text of the label. We add the label, here bound to msg
, to our window.
main = do
Gtk.init Nothing
win <- new Gtk.Window [#title := "Introduction"]
on win #destroy Gtk.mainQuit
#resize win 640 480
msg <- new Gtk.Label [#label := "Hello"]
#add win msg
#showAll win
Gtk.main
Reloading the module in GHCi, and again running the application, we see a small larger window with the “Hello” greeting.
Button Signal Handling
To make the application interactive, we want to add a button. In a real application, clicking the button could launch the missiles, but in this example we will only change the text of label.
We begin by adding a Box
to the window, in which we’ll place our button and our message label. The orientation will be OrientationVertical
, meaning that the widgets are laid out top-to-bottom, rather than left-to-right.
box <- new Gtk.Box [ #orientation := Gtk.OrientationVertical ]
#add win box
Next, we add our message label to the box, instead of adding it to the window.
msg <- new Gtk.Label [ #label := "Hello"]
#add box msg
Finally, we create a Button
, with a label “Click me!”, add the button to our box, and attach a signal handler for the #clicked
signal that changes the message label.
btn <- new Gtk.Button [ #label := "Click me!" ]
#add box btn
on btn #clicked (set msg [ #label := "Clicked!"])
We can now reload GHCi, and see our application featuring a label and a button. If we click the button, the label text changes.
Box Packing
Before we wrap up, we should make the application look nicer. By using the overloaded #add
function, a function available on Container
widgets, our widgets are laid out next to each other without any spacing.
Instead, we’ll use #packStart
, a function available on Box
, with which we can control how the child is laid out in the box container. The function takes as arguments:
- the box container,
- the child to add to the box,
- a
Bool
named expand, determining if the child should be given extra space allocated to the box, - a
Bool
named fill, determining if the child should use and grow to fill its allocated space, rather than being surrounded by padded empty space, and - a
Word32
named spacing, specifying the number of pixels of empty space to surround the child widget with.
We replace #add
with #packStart
when adding both the message label and the button. The message label will expand in the box, while the button will not expand. Both will have 10 pixels of extra spacing.
msg <- new Gtk.Label [ #label := "Hello"]
#packStart box msg True False 10
btn <- new Gtk.Button [ #label := "Click me!" ]
#packStart box btn False False 10
on btn #clicked (set msg [ #label := "Clicked!"])
We reload and run the application, and see that it now looks much nicer.
Boxes, and the #packStart
and #packEnd
functions, are useful for controlling the layout of your user interface. You can also use a limited version of CSS with GTK+, but that’s for another episode.
Summary
The haskell-gi family of packages, and specifically the gi-gtk package, lets us program graphical user interfaces with GTK+ and Haskell. While these are generated and very complete bindings, the API is imperative and object-oriented.
To overcome this mismatch, I’ve been working on a declarative layer on top of gi-gtk
, to let us program with GTK+ and Haskell in a pure functional style. In a future episode I hope to cover the gi-gtk-declarative package, and show you how to build a small application with it.
Thanks for watching!