In this lesson we build a simple digital clock application using GTK+GTK+ and the gtk3
packagegtk3
on Hackage which serves as a Haskell interface over it. Like FLTK which we saw in the previous lesson, GTK+ is also a fully-featured GUI toolkit with an extensive library of widgets to choose from. It doesn’t happen to come with a clock, though.
So we’re going to use a drawing area,GtkDrawingArea about which the GTK documentation has this to say:
The GtkDrawingArea widget is used for creating custom user interface elements. It’s essentially a blank widget; you can draw on it.
First, though, let’s start by writing a program that opens a window. Then we’ll start drawing in it.
A plain window
At a minimum, we need to do these things to get a program that shows a window:
initGUI
Documentation for GTK initialization and the main event loop:Graphics.UI.Gtk.General.General
initializes GTK. This has to happen before any other GTK function is used.windowNew
Documentation for the GTK window widget:Graphics.UI.Gtk.Windows.Window
creates the window. This gives us a value of typeWindow
.- Every widgetDocumentation for the GTK widget concept:
Graphics.UI.Gtk.Abstract.Widget
is either visible or hidden, and they’re initially hidden when you first create them. When you create a new widget, you need to show it before it can actually appear on screen. ThewidgetShow
function tells GTK that we’re not just creating a window for future use that’s invisible for the moment; we do in fact want this window to appear on screen right now. mainGUI
hands control over to GTK+ for the remainder of the program’s execution. This action blocks (and thus the program continues running) until something runs themainQuit
action to stop it (thus finally reaching the end ofmain
and stopping the program).
Behold the fruit of our labor:
Knowing when to quit
I said that mainGUI
runs until we stop it with mainQuit
. Notice that we haven’t actually used mainQuit
anywhere yet, and so we have a program but no way to halt it. Before we go on with the clock drawing, let’s fix that.
When the user closes a window (such as by pressing the “×” in the top-right corner of the title bar in the Gnome window manager), typically the program that launched the window should stop. GTK doesn’t give us that behavior automatically, because it’s not always what we want; sometimes a program might continue running “in the background” after the window is closed. But I want our window to exhibit the standard program-stopping behavior. We’re going to do this by adding a handler for the window’s “delete” event.
The documentation for the deleteEvent
signal says:deleteEvent
The
deleteEvent
signal is emitted if a user requests that a toplevel window is closed.
The way we express “every time event X happens, react by taking action Y” is with the on
function, which comes from System.Glib.Signals
System.Glib.Signals
in the glib
package (and is re-exported by Graphics.UI.Gtk
):
on :: object
-> Signal object callback
-> callback
-> IO (ConnectId object)
We use this any time we’re adding a handler for some event (or, to use an alternative phrasing, when we want to perform an action in response to some signal).
on
returns a unique ConnectId
that identifies this particular attachment, which you need to retain if you might need to subsequently use signalDisconnect
to what we’re setting up here. Since we are unlikely to need that in this case, in my quitOnWindowClose
function I’ve chosen to just discard the ConnectId
and return ()
instead.
The reaction that we take is “stop the application,” using Gtk.mainQuit
.Gtk.mainQuit
True or False? – In the GTK documentation,GTK documentation for widget signals we see that the description of the return value for a lot of actions says:If you’re familiar with JavaScript, you may find this difficult to remember because True/False has the opposite meaning: because this is the opposite of how JavaScript events work. A JavaScript handler cancels event propagation by returning false, whereas a GTK+ handler cancels event propagation by returning true.
TRUE
to stop other handlers from being invoked for the event.FALSE
to propagate the event further.
The corresponding documentation in the Haskell packageHaskell gtk3
documentation for widget signals is somewhat more sparse. This is a good time to suggest a general tip: Whenever you’re using a library that exists mostly to provide foreign bindings for another library (like the Haskell gtk3
package wraps GTK+ which is written in C), anticipate that the wrapper probably doesn’t repeat all of the documentation from the underlying library; this means that for a fuller understanding you’ll probably have to consult the GTK+ Reference ManualGTK+ Reference Manual sometimes in addition to the documentation for the Haskell API.
The Haskell documentation does however replicate this useful bit of information from the GTK+ manual:
The default handler for this signal destroys the window.
Since I do want the window to be destroyed, I’m going to return False
from the handler to indicate that I want the default behavior. It doesn’t really matter, because quitting the entire program will result in destroying the window anyway, but we might as well say what we mean.
Getting interrupted
There’s one other situation I want to make sure we handle: When you launch a program from the command line and press ctrl+C, you expect the program to quit. This is another thing that, although it is normal and expected, won’t just happen unless we make it happen.
What’s happening when you kill a program with ctrl+C? The shell is sending a signal to the process – specifically, INT
, which stands for interrupt. A signal is a message you’re sending to a program; There are a lot of signals, and each means something different, but most of them are ways of telling the program to stop running. The one called “interrupt” triggered by ctrl+C is the most polite, non-urgent way of saying “please stop”.
This is a POSIX thing, not a GTK+ thing, so we need to go to a different package for this: unix
.System.Posix.Signals
in the unix
package
The installHandler
function from unix
expresses much the same idea as the on
function from gtk3
: “every time event X happens, react by taking action Y.” In this case, the event is “when the process receives an interrupt signal,” and the reaction again is “stop the application” (Gtk.mainQuit
).
With these two actions inserted, main
now looks like this:
quitOnInterrupt
is the very first thing that happens inmain
, so that the program can be able to properly respond to interrupts as soon as possible.quitOnWindowClose
has to happen after we create the window, but it can come before we show the window.
The blank window still opens exactly as it did before, but now it closes upon request like a window ought to.
The main thread
There’s a function I used in the last section that calls for some explanation: postGUIAsync
. First let’s back up and discuss how GTK+ uses threads.See thread concepts for some background on how Haskell threads work. We can get this from reading the documentation on the Graphics.UI.Gtk.General.General
module.Documentation for the GTK main event loop: Graphics.UI.Gtk.General.General
First, the comment on initGUI
gives us this warning (slightly paraphrased here):
In a multi-threaded application, it is your obligation to ensure that all calls to GTK happen in a single OS thread.
This means that once we run initGUI
, the thread that ran it is now designated as the only thread that should be doing GTK stuff from then on. But sometimes events originating outside of GTK need to affect the GTK thread, like when our interrupt handler tells GTK to stop running. This will also come up again later when we start actually turning this application into a clock, because we’ll have a thread that’s responsible for updating the time once a second. Fortunately, there is a mechanism for inter-thread communication. The documentation goes on to say:
If you want to make calls to GTK functions from another thread, you will have to use
postGUISync
orpostGUIAsync
. These will execute their arguments from the main loop, that is, from the OS thread of GTK, thereby ensuring that any GTK and OS function can be called.
:: IO a -> IO a
Gtk.postGUISync :: IO () -> IO () Gtk.postGUIAsync
This is why we wrote Gtk.postGUIAsync Gtk.mainQuit
in the signal handler instead of just Gtk.mainQuit
.
Notice that when we used mainQuit
in quitOnWindowClose
, we did not use a “post” function. That’s because that was a handler for a GTK signal, and so it’s already running in the GUI thread; there’s no inter-thread communication required.
Widget nesting
Now it’s time to start putting interesting things into the window. We’ll use two widgets:
- Most of the screen will be filled by a drawing areaDocumentation for the drawing area widget:
Graphics.UI.Gtk.Misc.DrawingArea
will serve a blank canvas onto which we shall paint a clock; - We’ll put the drawing area inside a frameDocumentation for the frame widget:
Graphics.UI.Gtk.Ornaments.Frame
, a border that goes around the drawing and has a label.The documentation for GtkFrame includes a sample image demonstrating what a frame looks like.
import Graphics.UI.Gtk (AttrOp ((:=)))
import qualified Graphics.Rendering.Cairo as Cairo
import qualified Graphics.Rendering.Pango as Pango
main :: IO ()
main =
do
quitOnInterrupt
_ <- Gtk.initGUI
drawingArea <- Gtk.drawingAreaNew -- 1
Gtk.set drawingArea
[ Gtk.widgetWidthRequest := 300
, Gtk.widgetHeightRequest := 100
]
_ <- Gtk.on drawingArea Gtk.draw render
frame <- Gtk.frameNew -- 2
Gtk.set frame
[ Gtk.containerChild := drawingArea
, Gtk.frameLabel := "What time is it"
, Gtk.frameLabelXAlign := 0.5
, Gtk.frameLabelYAlign := 1
, Gtk.widgetMarginTop := 20
, Gtk.widgetMarginRight := 20
, Gtk.widgetMarginBottom := 20
, Gtk.widgetMarginLeft := 20
]
window <- Gtk.windowNew -- 3
Gtk.set window
[ Gtk.windowTitle := "Clock"
, Gtk.containerChild := frame
, Gtk.windowDefaultWidth := 500
, Gtk.windowDefaultHeight := 250
]
quitOnWindowClose window
Gtk.widgetShowAll window
Gtk.mainGUI
Each of the three widgets follows the same pattern: There’s a function called [...]New
that constructs the widget, and then we use Gtk.set
to set a handful of properties on it.Properties that apply to all widgets are documented in Graphics.UI.Gtk.Abstract.Widget
. Others are documented in the modules for their respective widgets.
For the
DrawingArea
, we set its requested width and height. The requested size of a widget is the size that it wants to be. If the widget were nested within a more complex layout with side-by-side widgets, GTK would take this into consideration when deciding what size everything should be. For this simple application, the requested size has the effect of limiting how much the user is able to resize the window. We set a minimum size of 300 × 100 pixels to make sure the drawing area never gets too small to fit the clock.The
Frame
widget specifies that it contains the drawing area. Documentation for widgets that contain other widgets (such asWindow
andFrame
):Graphics.UI.Gtk.Abstract.Container
Label alignment of 0.5 on the X axis means the text “What time is it” is centered horizontally, and alignment of 1 on the Y axis means the label text appears above the frame’s border. We also gave the frame a margin of 20 pixels on all sides; otherwise it would be directly touching the window edge and it would be difficult to see that it’s there.The window’s title (“Clock”) is what appears in the title bar (which is drawn by the window manager, not by our application). Just as the frame contains the drawing area, so too does the window contain the frame; or in other words, the frame is the window’s child. We set a default size for the window, which is the size of the window when it first opens.
Drawing some text
There’s one line above that we haven’t mentioned yet:
<- Gtk.on drawingArea Gtk.draw render _
The draw
signalGTK+ documentation for the draw
signal indicates that a widget (or some portion of it) needs to be drawn to the screen:
:: Signal Gtk.DrawingArea (Cairo.Render ()) Gtk.draw
This type tells us that the handler we need to specify for the draw
signal has type Cairo.Render ()
. Cairo is the graphics library that you use with GTK, and Render
is the monad in which you construct Cairo drawings. Whenever you’re trying to understand a new monad, it’s usually useful to look at the source code to see how it’s defined. If we look at the source code in the cairo
package, we find this:
newtype Render m =
Render { runRender :: ReaderT Cairo IO m }
This means that Render
has two defining aspects:
ReaderT Cairo
– This tells us that allRender
actions take place within some context, and you can expect that the library will provide us some actions in theRender
monad that obtain information from that context.IO
– This tells us that we can liftIO
actions into the render monad. We didn’t have to look at the source code to know this; we also could have noticed by looking at the list of typeclass instances forRender
that it has an instance ofMonadIO
.
For this application, we’re going to have the rendering consist of two steps: First draw a background (fill the area with a solid color), then draw some text on top of the background.
The way we draw with Cairo is probably familiar if you’ve used any other graphics libraries, but you might find the imperative style a little unusual if you mostly write Haskell. Drawing consists of a sequence of state mutations. For example, there’s a function to set the drawing color,Cairo documentation for setting the color: cairo_set_source_rgb
which will color all subsequent drawing operations until the color is changed again.
Drawing text requires a whole other library called Pango, Pango documentation: Graphics.Rendering.Pango
because rendering text turns out to be a much more complicated problem than it might seem! To draw text, we create what Pango calls a layout – a way in which the characters are arranged, or laid out, in a two-dimensional space.
We now have an application that displays something!
Centering the text
We really want to center the text, both horizontally and vertically, within the drawing window. GTK does not provide us with an easy way to do this, so we have to do some math.
We’re going to replace Pango.showLayout
with showPangoCenter
, a function that we’re about to write. In order to know where the center of the drawing area is, we need to know the size of the drawing area, so we’re also adding a Gtk.DrawingArea
parameter to the function.
Since we will be doing arithmetic in two dimensions, we’re going to make it a little easier by using the linear
package.The linear
package contains a lot of math that we don’t know anything about. But we can still make basic use of the V2
type from the Linear.V2
module to represent points on the screen.
import Linear.V2
That provides this type:
data V2 a = V2 a a
You don’t really need to know anything about linear algebra to use this package.We’ll only need the standard arithmetic operations: addition, subtraction, multiplication, division. Think of V2
as a pair of numbers (representing a horizontal distance and a vertical distance), and any arithmetic operations we perform on them happen to both numbers.
λ> V2 4 5 * 2
V2 8 10
λ> V2 4 5 + V2 1 2
V2 5 7
This part took a lot of thinking and some trial-and-error to get right. Here’s the idea:
Before when we just used Pango.showLayout
, the layout appeared with its top-left corner in the top-left corner of the drawing area. Now, we’re going to use Cairo.moveTo
first, and so the layout will appear with its top-left corner in the position that we’ve moved to.
The movement, depicted in the figure above, is calculated as the distance between the center of the text and the center of the drawing area.
showPangoCenter :: Gtk.DrawingArea -> Gtk.PangoLayout -> Cairo.Render ()
showPangoCenter drawingArea layout =
do
clipCenter <- getClipCenter
textCenter <- getTextCenter
let (V2 x y) = clipCenter - textCenter
Cairo.moveTo x y
Pango.showLayout layout
where
getClipSize :: Cairo.Render (V2 Int) =
do
Gtk.Rectangle _x _y w h <- liftIO (Gtk.widgetGetClip drawingArea)
return (V2 w h)
getClipCenter :: Cairo.Render (V2 Double) =
do
size <- getClipSize
return ((realToFrac <$> size) / 2)
getTextCenter :: Cairo.Render (V2 Double) =
do
(_, Gtk.PangoRectangle x y w h) <-
liftIO (Pango.layoutGetExtents layout)
return (V2 x y + (V2 w h / 2))
Now we are looking very well-aligned.
That was a lot of setup! We will conclude this lesson here and turn this application into a clock next time. Now that we have the mechanics in place, getting the time and changing the text shouldn’t be too much trouble.