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 the Window object
  • Attaching a handler to #destroy, a signal available on all Widget objects
  • Calling #showAll, a function available on all Widget 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, like windowNew that now requires a WindowType 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!