Haskell/GUI

From Wikibooks, open books for an open world
Jump to: navigation, search

Haskell has at least four toolkits for programming a graphical interface:

  • wxHaskell - provides a Haskell interface to the wxWidgets toolkit
  • Gtk2Hs - provides a Haskell interface to the GTK+ library
  • hoc (documentation at sourceforge) - provides a Haskell to Objective-C binding which allows users to access to the Cocoa library on MacOS X
  • qtHaskell - provides a set of Haskell bindings for the Qt Widget Library from Nokia

In this tutorial, we will focus on the wxHaskell toolkit, as it allows you to produce a native graphical interface on all platforms that wxWidgets is available on, including Windows, Linux and MacOS X.

Getting and running wxHaskell[edit]

To install wxHaskell, look for your version of instructions at: Linux Mac Windows


or the wxHaskell download page and follow the installation instructions provided on the wxHaskell download page. Don't forget to register wxHaskell with GHC, or else it won't run (automatically registered with Cabal). To compile source.hs (which happens to use wxHaskell code), open a command line and type:

ghc -package wx source.hs -o bin

Code for GHCi is similar:

ghci -package wx

You can then load the files from within the GHCi interface. To test if everything works, go to $wxHaskellDir/samples/wx ($wxHaskellDir is the directory you installed it in) and load (or compile) HelloWorld.hs. It should show a window with title "Hello World!", a menu bar with File and About, and a status bar at the bottom, that says "Welcome to wxHaskell".

If it doesn't work, you might try to copy the contents of the $wxHaskellDir/lib directory to the ghc install directory.

Hello World[edit]

Here's the basic Haskell "Hello World" program:

module Main where

main :: IO ()
main = putStr "Hello World!"

It will compile just fine, but it isn't really fancy. We want a nice GUI! So how to do this? First, you must import Graphics.UI.WX. This is the wxHaskell library. Graphics.UI.WXCore has some more stuff, but we won't be needing that now.

To start a GUI, use (guess what) start gui. In this case, gui is the name of a function which we'll use to build the interface. It must have an IO type. Let's see what we have:

module Main where

import Graphics.UI.WX

main :: IO ()
main = start gui

gui :: IO ()
gui = do
  --GUI stuff

To make a frame, we use frame. Check the type of frame. It's [Prop (Frame ())] -> IO (Frame ()). It takes a list of "frame properties" and returns the corresponding frame. We'll look deeper into properties later, but a property is typically a combination of an attribute and a value. What we're interested in now is the title. This is in the text attribute and has type (Textual w) => Attr w String. The most important thing here, is that it's a String attribute. Here's how we code it:

gui :: IO ()
gui = do
  frame [text := "Hello World!"]

The operator (:=) takes an attribute and a value, and combines both into a property. Note that frame returns an IO (Frame ()). You can change the type of gui to IO (Frame ()), but it might be better just to add return (). Now we have our own GUI consisting of a frame with title "Hello World!". Its source:


module Main where

import Graphics.UI.WX

main :: IO ()
main = start gui

gui :: IO ()
gui = do
  frame [text := "Hello World!"]
  return ()

The result should look like the screenshot. (It might look slightly different on Linux or MacOS X, on which wxhaskell also runs)

Controls[edit]

A text label[edit]

Simply a frame doesn't do much. In this chapter, we're going to add some more elements. Let's start with something simple: a label. wxHaskell has a label, but that's a layout thing. We won't be doing layout until next chapter. What we're looking for is a staticText. It's in Graphics.UI.WX.Controls. As you can see, the staticText function takes a Window as argument, and a list of properties. Do we have a window? Yup! Look at Graphics.UI.WX.Frame. There we see that a Frame is merely a type-synonym of a special sort of window. We'll change the code in gui so it looks like this:

Hello StaticText! (winXP)
gui :: IO ()
gui = do
  f <- frame [text := "Hello World!"]
  staticText f [text := "Hello StaticText!"]
  return ()

Again, text is an attribute of a staticText object, so this works. Try it!

A button[edit]

Now for a little more interaction. A button. We're not going to add functionality to it until the chapter about events, but at least something visible will happen when you click on it.

A button is a control, just like staticText. Look it up in Graphics.UI.WX.Controls.

Again, we need a window and a list of properties. We'll use the frame again. text is also an attribute of a button:

Overlapping button and StaticText (winXP)
gui :: IO ()
gui = do
  f <- frame [text := "Hello World!"]
  staticText f [text := "Hello StaticText!"]
  button f [text := "Hello Button!"]
  return ()

Load it into GHCi (or compile it with GHC) and... hey!? What's that? The button's been covered up by the label! We're going to fix that next, in the layout chapter.

Layout[edit]

The reason that the label and the button overlap, is that we haven't set a layout for our frame yet. Layouts are created using the functions found in the documentation of Graphics.UI.WXCore.Layout. Note that you don't have to import Graphics.UI.WXCore to use layouts.

The documentation says we can turn a member of the widget class into a layout by using the widget function. Also, windows are a member of the widget class. But, wait a minute... we only have one window, and that's the frame! Nope... we have more, look at Graphics.UI.WX.Controls and click on any occurrence of the word Control. You'll be taken to Graphics.UI.WXCore.WxcClassTypes and it is here we see that a Control is also a type synonym of a special type of window. We'll need to change the code a bit, but here it is.

gui :: IO ()
gui = do
  f <- frame [text := "Hello World!"]
  st <- staticText f [text := "Hello StaticText!"]
  b <- button f [text := "Hello Button!"]
  return ()

Now we can use widget st and widget b to create a layout of the staticText and the button. layout is an attribute of the frame, so we'll set it here:

StaticText with layout (winXP)
gui :: IO ()
gui = do
  f <- frame [text := "Hello World!"]
  st <- staticText f [text := "Hello StaticText!"]
  b <- button f [text := "Hello Button!"]
  set f [layout := widget st]
  return ()

The set function will be covered in the chapter about attributes. Try the code, what's wrong? This only displays the staticText, not the button. We need a way to combine the two. We will use layout combinators for this. row and column look nice. They take an integer and a list of layouts. We can easily make a list of layouts of the button and the staticText. The integer is the spacing between the elements of the list. Let's try something:

A row layout (winXP)
Column layout with a spacing of 25 (winXP)
gui :: IO ()
gui = do
  f <- frame [text := "Hello World!"]
  st <- staticText f [text := "Hello StaticText!"]
  b <- button f [text := "Hello Button!"]
  set f [layout := 
          row 0 [widget st, widget b]
        ]
  return ()

Play around with the integer and see what happens, also change row into column. Try to change the order of the elements in the list to get a feeling of how it works. For fun, try to add widget b several more times in the list. What happens?

Here are a few exercises to spark your imagination. Remember to use the documentation!


Exercises
  1. Add a checkbox control. It doesn't have to do anything yet, just make sure it appears next to the staticText and the button when using row-layout, or below them when using column layout. text is also an attribute of the checkbox.
  2. Notice that row and column take a list of layouts, and also generates a layout itself. Use this fact to make your checkbox appear on the left of the staticText and the button, with the staticText and the button in a column.
  3. Can you figure out how the radiobox control works? Take the layout of the previous exercise and add a radiobox with two (or more) options below the checkbox, staticText and button. Use the documentation!
  4. Use the boxed combinator to create a nice looking border around the four controls, and another one around the staticText and the button. (Note: the boxed combinator might not be working on MacOS X - you might get widgets that can't be interacted with. This is likely just a bug in wxhaskell.)


After having completed the exercises, the end result should look like this:

Answer to exercises

You could have used different spacing for row and column, or the options of the radiobox are displayed horizontally.

Attributes[edit]

After all this, you might be wondering things like: "Where did that set function suddenly come from?", or "How would I know if text is an attribute of something?". Both answers lie in the attribute system of wxHaskell.

Setting and modifying attributes[edit]

In a wxHaskell program, you can set the properties of the widgets in two ways:

  1. during creation: f <- frame [ text := "Hello World!" ]
  2. using the set function: set f [ layout := widget st ]

The set function takes two arguments: one of any type w, and the other is a list of properties of w. In wxHaskell, these will be the widgets and the properties of these widgets. Some properties can only be set during creation, like the alignment of a textEntry, but you can set most others in any IO-function in your program, as long as you have a reference to it (the f in set f [stuff]).

Apart from setting properties, you can also get them. This is done with the get function. Here's a silly example:

gui :: IO ()
gui = do
  f <- frame [ text := "Hello World!" ]
  st <- staticText f []
  ftext <- get f text
  set st [ text := ftext]
  set f [ text := ftext ++ " And hello again!" ]

Look at the type signature of get. It's w -> Attr w a -> IO a. text is a String attribute, so we have an IO String which we can bind to ftext. The last line edits the text of the frame. Yep, destructive updates are possible in wxHaskell. We can overwrite the properties using (:=) anytime with set. This inspires us to write a modify function:

modify :: w -> Attr w a -> (a -> a) -> IO ()
modify w attr f = do
  val <- get w attr
  set w [ attr := f val ]

First it gets the value, then it sets it again after applying the function. Surely we're not the first one to think of that...

And nope, we aren't. Look at this operator: (:~). You can use it in set, because it takes an attribute and a function. The result is a property, in which the original value is modified by the function. This means we can write:

gui :: IO ()
gui = do
  f <- frame [ text := "Hello World!" ]
  st <- staticText f []
  ftext <- get f text
  set st [ text := ftext ]
  set f [ text :~ ++ " And hello again!" ]

This is a great place to use anonymous functions with the lambda-notation.

There are two more operators we can use to set or modify properties: (::=) and (::~). They do the same as (:=) and (:~), except a function of type w -> orig is expected, where w is the widget type, and orig is the original "value" type (a in case of (:=), and a -> a in case of (:~)). We won't be using them now, though, as we've only encountered attributes of non-IO types, and the widget needed in the function is generally only useful in IO-blocks.

How to find attributes[edit]

Now the second question. Where did I read that text is an attribute of all those things? The easy answer is: in the documentation. Now where in the documentation to look for it?

Let's see what attributes a button has, so go to Graphics.UI.WX.Controls, and click the link that says "Button". You'll see that a Button is a type synonym of a special kind of Control, and a list of functions that can be used to create a button. After each function is a list of "Instances". For the normal button function, this is Commanding -- Textual, Literate, Dimensions, Colored, Visible, Child, Able, Tipped, Identity, Styled, Reactive, Paint. This is the list of classes of which a button is an instance. Read through the Classes and types chapter. It means that there are some class-specific functions available for the button. Textual, for example, adds the text and appendText functions. If a widget is an instance of the Textual class, it means that it has a text attribute!

Note that while StaticText hasn't got a list of instances, it's still a Control, which is a synonym for some kind of Window, and when looking at the Textual class, it says that Window is an instance of it. This is an error on the side of the documentation.

Let's take a look at the attributes of a frame. They can be found in Graphics.UI.WX.Frame. Another error in the documentation here: It says Frame instantiates HasImage. This was true in an older version of wxHaskell. It should say Pictured. Apart from that, we have Form, Textual, Dimensions, Colored, Able and a few more. We're already seen Textual and Form. Anything that is an instance of Form has a layout attribute.

Dimensions adds (among others) the clientSize attribute. It's an attribute of the Size type, which can be made with sz. Please note that the layout attribute can also change the size. If you want to use clientSize you should set it after the layout.

Colored adds the color and bgcolor attributes.

Able adds the Boolean enabled attribute. This can be used to enable or disable certain form elements, which is often displayed as a greyed-out option.

There are lots of other attributes, read through the documentation for each class.

Events[edit]

There are a few classes that deserve special attention. They are the Reactive class and the Commanding class. As you can see in the documentation of these classes, they don't add attributes (of the form Attr w a), but events. The Commanding class adds the command event. We'll use a button to demonstrate event handling.

Here's a simple GUI with a button and a staticText:

Before (winXP)
gui :: IO ()
gui = do
  f <- frame [ text := "Event Handling" ]
  st <- staticText f [ text := "You haven\'t clicked the button yet." ]
  b <- button f [ text := "Click me!" ]
  set f [ layout := column 25 [ widget st, widget b ] ]

We want to change the staticText when you press the button. We'll need the on function:

  b <- button f [ text := "Click me!"
                , on command := --stuff
                ]

The type of on: Event w a -> Attr w a. command is of type Event w (IO ()), so we need an IO-function. This function is called the Event handler. Here's what we get:



gui :: IO ()
gui = do
  f <- frame [ text := "Event Handling" ]
  st <- staticText f [ text := "You haven\'t clicked the button yet." ]
  b <- button f [ text := "Click me!"
                , on command := set st [ text := "You have clicked the button!" ]
                ]
  set f [ layout := column 25 [ widget st, widget b ] ]

Insert text about event filters here