An event in Gtk2Hs is something that is sent to a widget, by the main loop, usually as a result of an action performed by the user. The widget then responds by emitting a signal, and this is the 'signal' to the program to 'do something'. To the Gtk2Hs application programmer, however, an event is just a Haskell data type with named fields. Many of those are described in the Graphics.UI.Gtk.Gdk.Events section in the API documentation. Look, for example, at the widget signal:
onButtonPress :: WidgetClass w => w -> (Event -> IO Bool) -> IO (ConnectId w)
This is not to be confused with the signal emitted when a
Button
type widget has been pressed; the button
here is a mouse button and the signal is emitted when a mouse
button has been pressed when the mouse is over that widget. The
handler is a function which takes an event, which has to have
the
Button
constructor, and has a IO boolean value. The
API lists the following fields for
Button
:
eventSent :: Bool eventClick :: Click eventTime :: TimeStamp eventModifier :: [Modifier] eventButton :: MouseButton eventXRoot, eventYRoot :: Double
The first is used for the return. It occurs in all
Event
constructors like
Motion, Expose, Key, Crossing, Focus, Configure, Scroll,
WindowState and Proximity.
From
Events
you can extract all kinds of information
about what the user is doing. A simple example is this code
snippet:
onButtonPress eb (\x -> if (eventButton x) == LeftButton then do widgetSetSensitivity eb False return (eventSent x) else return (eventSent x))
Here parameter
eb
is the widget covered by the mouse, and the
anonymous function is of the type as described above. Something
is done (see the example below) if the left mouse button has
been pressed and then
eventSent
returns the appropriate boolean. If
another mouse button has been pressed, nothing happens, and
only the boolean is returned.
Now, some widgets don't have associated windows, so they
just draw on their parents. Because of this, they cannot
receive events and if they are incorrectly sized, they don't
clip so you can get messy overwriting (but we won't discuss
this further). An
EventBox
provides an X window for its child widget.
It is a subclass of
Bin
which also has its own window and which is a
subclass of
ContainerClass
.
To create a new EventBox widget, use:
eventBoxNew :: IO EventBox
To add a child we can just use the well known:
containerAdd :: (ContainerClass self, WidgetClass widget) => self -> widget -> IO ()
The window may be visible or invisible, and the event box may be above or below its child in the widget tree. This is determined by:
eventBoxVisibleWindow :: Attr EventBox Bool -- default True eventBoxAboveChild :: Attr EventBox Bool -- default False
If you just want to trap events, then set the window to be
invisible. If the
eventBox
is above its child, all events will go to
it first. If it is below, windows in child widgets of the child
will be reached first.
A Button Box is just a box which can be used to pack buttons in a standard way. There are two kinds, horizontal and vertical ones, and you construct them with:
hbuttonBoxNew :: IO HButtonBox vButtonBoxNew :: IO VButtonBox
The functionality is in the
ButtonBoxClass.
buttonBoxSetLayout :: ButtonBoxClass self => self -> ButtonBoxStyle -> IO ()
The style is one of the following:
ButtonBoxDefaultStyle, ButtonBoxSpread, ButtonBoxEdge,
ButtonBoxStart, ButtonBoxEnd
. You don't pack buttons as
in ordinary horizontal and vertical boxes, but you use the
containerAdd
function instead.
The second feature of button boxes is that you can define one or more of your buttons to be in a secondary group. These will then be treated differently when the button box is resized. For example, a help button can be kept visually apart from the others. The function is:
buttonBoxSetChildSecondary :: (ButtonBoxClass self, WidgetClass child) => self -> child -> Bool -> IO ()
This illustrates the use of event boxes and button boxes:
The buttons are packed into a vertical button box, with the play button as a secondary child. This is also a mnemonic button, with Alt-P as the accellerator key. The images are placed into event boxes with visible windows, and their background color is set to a shade of green with:
widgetModifyBg eb StateNormal (Color 0 35000 0)
As mentioned in Chapter 5.3 the
StateType
can be
StateNormal, StateActive, StatePrelight, StateSelected or
StateInsensitive
.
Note that the images above are not all the same size. This does not matter, but some care has to be taken to make the main window large enough. Otherwise borders will disappear when the pictures are switched.
When the user clicks the left mouse button when the mouse is over an event box, it will be set to insensitive with:
widgetSetSensitivity :: WidgetClass self => self -> Bool -> IO ()
This changes the
StateType
to
StateInsensitive
and the widget will no longer
respond to any user events. Furthermore, its appearance changes.
In the example we've also set the background color to a shade
of grey.
We've used tooltips to tell the user the images can be
frozen. As mentioned in Chapter 4.4 they don't always work in
GHCi but they do in the compiled version. To flip the images
randomly, we've used function RandomRIO, as in the previous
chapter. You may wonder why a tuple of
EventBox
and
Image
has been used, instead of just getting the
Image
from the
containerChild
attribute of the event boxes. This
is because it is a write only attribute, it can be
set
but not retrieved with
get
.
Finally, if the images are not available in your source code directory, or if you want to expand the slot machine with more slots, there is an ample supply of Brazilian fish at Peixes. They have been classified into salt water (água salgado) and fresh water (água doce) fish for your convenience.
import Graphics.UI.Gtk import System.Random (randomRIO) main :: IO () main= do initGUI window <- windowNew set window [windowTitle := "Slot Machine", containerBorderWidth := 10, windowDefaultWidth := 350, windowDefaultHeight := 400] hb1 <- hBoxNew False 0 containerAdd window hb1 vb1 <- vBoxNew False 0 boxPackStart hb1 vb1 PackGrow 0 vbb <- vButtonBoxNew boxPackStart hb1 vbb PackGrow 0 resetb <- buttonNewWithLabel "Reset" containerAdd vbb resetb quitb <- buttonNewWithLabel "Quit" containerAdd vbb quitb playb <- buttonNewWithMnemonic "_Play" containerAdd vbb playb set vbb [buttonBoxLayoutStyle := ButtonboxStart, (buttonBoxChildSecondary playb) := True ] let picfiles = ["./jacunda.gif", "./pacu.gif", "./tucunaream.gif"] evimls <- sequence (map (initEvent vb1) picfiles) tips <- tooltipsNew sequence_ $ map ((myTooltip tips) . fst) evimls onClicked playb (play evimls picfiles) onClicked resetb $ sequence_ (zipWith reSet evimls picfiles) onClicked quitb (widgetDestroy window) widgetShowAll window onDestroy window mainQuit mainGUI initEvent :: VBox -> FilePath -> IO (EventBox, Image) initEvent vb picfile = do eb <- eventBoxNew boxPackStart vb eb PackGrow 0 slot <- imageNewFromFile picfile set eb[containerChild := slot, containerBorderWidth := 10 ] widgetModifyBg eb StateNormal (Color 0 35000 0) widgetModifyBg eb StateInsensitive (Color 50000 50000 50000) onButtonPress eb (\x -> if (eventButton x) == LeftButton then do widgetSetSensitivity eb False return (eventSent x) else return (eventSent x)) return (eb, slot) reSet :: (EventBox, Image) -> FilePath -> IO () reSet (eb, im) pf = do widgetSetSensitivity eb True imageSetFromFile im pf play :: [(EventBox, Image)] -> [FilePath] -> IO () play eilist fplist = do let n = length fplist rands <- sequence $ replicate n (randomRIO (0::Int,(n-1))) sequence_ (zipWith display eilist rands) where display (eb, im) rn = do state <- widgetGetState eb if state == StateInsensitive then return () else imageSetFromFile im (fplist !! rn) myTooltip :: Tooltips -> EventBox -> IO () myTooltip ttp eb = tooltipsSetTip ttp eb "Click Left Mouse Button to Freeze" ""