Lesson 2: Drawing Text with
In this lesson we build a simple digital clock application using GTK+GTK+ and the
gtk3 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:
initGUIDocumentation for GTK initialization and the main event loop:
Graphics.UI.Gtk.General.Generalinitializes GTK. This has to happen before any other GTK function is used.
windowNewDocumentation for the GTK window widget:
Graphics.UI.Gtk.Windows.Windowcreates the window. This gives us a value of type
- Every widgetDocumentation for the GTK widget concept:
Graphics.UI.Gtk.Abstract.Widgetis 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. The
widgetShowfunction 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.
mainGUIhands control over to GTK+ for the remainder of the program’s execution. This action blocks (and thus the program continues running) until something runs the
mainQuitaction to stop it (thus finally reaching the end of
mainand 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:
deleteEventsignal 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 in the
glib package (and is re-exported by
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
The reaction that we take is “stop the application,” using
TRUEto stop other handlers from being invoked for the event.
FALSEto 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.
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”.
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” (
With these two actions inserted,
main now looks like this:
quitOnInterruptis the very first thing that happens in
main, so that the program can be able to properly respond to interrupts as soon as possible.
quitOnWindowClosehas 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:
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
postGUIAsync. 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.
This is why we wrote
Gtk.postGUIAsync Gtk.mainQuit in the signal handler instead of just
Notice that when we used
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.
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.DrawingAreawill 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.
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.
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.
Framewidget specifies that it contains the drawing area. Documentation for widgets that contain other widgets (such as
Graphics.UI.Gtk.Abstract.ContainerLabel 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:
draw signalGTK+ documentation for the
draw signal indicates that a widget (or some portion of it) needs to be drawn to the screen:
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:
This means that
Render has two defining aspects:
ReaderT Cairo– This tells us that all
Renderactions take place within some context, and you can expect that the library will provide us some actions in the
Rendermonad that obtain information from that context.
IO– This tells us that we can lift
IOactions 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 for
Renderthat it has an instance of
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
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 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.
That provides this type:
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.
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.