Haskell/GUI
Haskell has at least four toolkits for programming a graphical interface:
- wxHaskell - provides a Haskell interface to the cross-platform wxWidgets toolkit which supports Windows, OS X, and Gtk+ on GNU/Linux, among others.
- 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
In this tutorial, we will focus on the wxHaskell toolkit.
Getting and running wxHaskell
[edit | edit source]To install wxHaskell, look for your version of instructions at: GNU/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 where you installed it) 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.
Shortcut for Debian and Ubuntu
[edit | edit source]If your operating system is Debian or Ubuntu, you can simply run these commands from the terminal:
sudo apt-get install g++ sudo apt-get install libglu-dev sudo apt-get install libwxgtk2.8-dev
Hello World
[edit | edit source]Here's the basic Haskell "Hello World" program:
module Main where
main :: IO ()
main = putStr "Hello World!"
It will compile just fine, but how do we actually do GUI work with this? First, you must import the wxHaskell library Graphics.UI.WX
. Graphics.UI.WXCore
has some more stuff, but we won't need that now.
To start a GUI, use 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
which has the type [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 (Frame ())
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 ())
. The start
function has the type IO a -> IO ()
. 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 | edit source]From here on, its good practice to keep a browser window or tab open with the wxHaskell documentation. It's also available in $wxHaskellDir/doc/index.html. |
A text label
[edit | edit source]A simple frame doesn't do much. In this section, we're going to add some more elements. Let's start simple with a label. wxHaskell has a label
, but that's a layout thing. We won't be doing layout until next section. What we're looking for is a staticText
. It's in Graphics.UI.WX.Controls
. The staticText
function takes a Window
as argument along with 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:
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 | edit source]Now for a little more interaction. A button. We're not going to add functionality to it until the section about events, but already 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:
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.
Layout
[edit | edit source]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 there 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:
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 section below 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 that. 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:
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 |
---|
|
After having completed the exercises, the end result should look like this:
You could have used different spacing for row
and column
or have the options of the radiobox displayed horizontally.
Attributes
[edit | edit source]After all this, you might be wondering: "Where did that set
function suddenly come from?" and "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 | edit source]In a wxHaskell program, you can set the properties of the widgets in two ways:
- during creation:
f <- frame [ text := "Hello World!" ]
- using the
set
function:set f [ layout := widget st ]
The set
function takes two arguments: something of type w
along with properties of w
. In wxHaskell, these will be the widgets and the properties of these widgets. Some properties can only be set during creation, such as 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...
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. That 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 almost 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, 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 | edit source]Now the second question. Where do we go to determine that text
is an attribute of all those things? Go to the documentation…
Let's see what attributes a button has: Go to Graphics.UI.WX.Controls
. 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, there's a list of "Instances". For the normal button
function, we see Commanding -- Textual, Literate, Dimensions, Colored, Visible, Child, Able, Tipped, Identity, Styled, Reactive, Paint. That's 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
, and that's a synonym for some kind of Window
. When looking at the Textual
class, it says that Window
is an instance of it. That's 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 | edit source]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:
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