6.2 Cajas de botones y cajas de eventos

Un evento en Gtk2Hs es algo que se envía a un widget, por el bucle principal, normalmente como resultado de una acción realizada por el usuario. El widget entonces responde emitiendo una señal, y esta es la señal para el programa de hacer algo. Para el programador de aplicaciones de Gtk2Hs, sin embargo, un evento no es más que un tipo de datos de Haskell con campos nombrados. La mayoría de estos están descritos en la sección de la API correspondiente a Graphics.UI.Gtk.Gdk.Events. Mira, por ejemplo, la señal del widget:

onButtonPress :: WidgetClass w => w -> (Event -> IO Bool) -> IO (ConnectId w)

Esto no debe ser confundido con la señal emitida cuando un widget de tipo Button es presionado; el botón aquí es un botón del ratón y la señal se emite cuando se ha pulsado un botón del ratón cuando el ratón estaba sobre ese widget. El manejador es una función que toma un evento, que tiene que tener el constructor Button, y tiene un valor IO boolean. La API lista los siguientes campos para Button :

eventSent :: Bool
eventClick :: Click
eventTime :: TimeStamp
eventModifier :: [Modifier]
eventButton :: MouseButton
eventXRoot, eventYRoot :: Double

El primero se usa para el retorno. Ocurre en todos los constructores Event como Motion, Expose, Key, Crossing, Focus, Configure, Scroll, WindowState and Proximity. (movimiento, exposición, llave, cruce, foco, configurar, desplazamiento, estado de la ventana y proximidad). De los Events puedes obtener todo tipo de información sobre lo que está haciendo el usuario. Tienes un ejemplo sencillo en este trozo de código:

onButtonPress eb 
                 (\x -> if (eventButton x) == LeftButton 
                           then do widgetSetSensitivity eb False 
                                   return (eventSent x)
                           else return (eventSent x))

Aquí, el parámetro eb es el widget sobre el que está el ratón, y la función anónima es del tipo descrito arriba. Algo se hace (mira el ejemplo inferior) si el botón izquierdo del ratón ha sido pulsado y entonces, eventSent devuelve el booleano apropiado. si se pulsa cualquier otro botón del ratón, no pasa nada y sólo se devuelve el booleano.

Ahora, algunos widwets no tienen ventanas asociadas, así que están dibujados en sus ventanas padre. Por eso no pueden recibir eventos y si su tamaño está puesto incorrectamente, no se superponen con lo que puedes organizar un lío de sobreescritura (no seguiremos discutiendo este aspecto). Un EventBox proporciona una ventana X window para su widget hijo. Es una subclase de Bin que también tiene su propia ventana y que es una subclase de ContainerClass .

Para crear un nuevo widget EventBox widget, usa:

eventBoxNew :: IO EventBox

Para añadir un hijo simplemente tenemos que usar el bien conocido:

containerAdd :: (ContainerClass self, WidgetClass widget) => self -> widget -> IO ()

La ventana puede ser visible o invisible, y la caja de eventos puede estar por encima o por debajo de su hijo en el árbol del widget. Esto se determina por:

eventBoxVisibleWindow :: Attr EventBox Bool    -- default True
eventBoxAboveChild :: Attr EventBox Bool       -- default False

Si simplemente quieres capturar los eventos, entonces establece la ventana como invisible. Si el eventBox está sobre su hijo, todos los eventos irán en primer lugar allí primero. Si está debajo, las ventanas en los widgets hijos del hijo serán alcanzados primero.

Una caja Button es simplemente una caja que puede ser usada para empaquetar botones de un modo estándar. Hay dos tipos, horizontales and verticales, y puedes construirlos con:

hbuttonBoxNew :: IO HButtonBox
vButtonBoxNew :: IO VButtonBox

La funcionalidad está en el ButtonBoxClass.

buttonBoxSetLayout :: ButtonBoxClass self => self -> ButtonBoxStyle -> IO ()

El estilo es uno de los siguientes: ButtonBoxDefaultStyle, ButtonBoxSpread, ButtonBoxEdge, ButtonBoxStart, ButtonBoxEnd . No se empaquetan los buttons como en las cajas normales verticales y horizontales, sino que debes usar la función containerAdd.

La segunda característica de las cajas button boxes es que puedes definir que uno o más de los botones estén en un grupo secundario. Estos serán tratados de modo diferente cuando se modifique el tamaño de la caja button. Por ejemplo, un botón de ayuda puede mantenerse visualmente separado de los otros. La función es:

buttonBoxSetChildSecondary :: (ButtonBoxClass self, WidgetClass child)
=> self -> child -> Bool -> IO ()

Esto ilustra el uso de cajas de eventos y cajas de botón:

Slot Machine

Los botones se empaquetan en una caja de botones verticales, con el botón play como un hijo secundario. Además es un botón mnemónico, con Alt-P como tecla aceleradora. Las imágenes se colocan en cajas de eventos con ventanas visibles, y su color de fondo se establece en un color verde con:

widgetModifyBg eb StateNormal (Color 0 35000 0)

Como se mencionó en el capítulo 5.3 el StateType puede ser StateNormal, StateActive, StatePrelight, StateSelected or StateInsensitive (normal, activo, preiluminado, seleccionado o insensible) .

Fíjate en que las imágenes de abajo no tienen todas el mismo tamaño. No tiene especial importancia, pero debemos tener cuidado de asegurarnos de que la ventana principal sea suficientemente grande. De otro modo los bordes desaparecerán cuando se cambien las imágenes.

Cuando el usuario pulsa el botón izquierdo del ratón cuando el ratón está sobre una caja de eventos, se puede colocar en el estado insensible con:

widgetSetSensitivity :: WidgetClass self => self -> Bool -> IO ()

Esto cambia el StateType a StateInsensitive y el widget no responderá más a los eventos del usuario. Ademas su apariencia cambia. En el ejemplo hemos cambiado también el color de fondo a un gris.

Slot Machine Insensitive

Hemos usado tooltips para indicar al usuario que las imágenes pueden ser congeladas. Como se mencionó en el Capítulo 4.4 no siempre funcionan con el intérprete pero si lo hacen con el compilador. Para cambiar las imágenes aleatoriamente, usamos la función RandomRIO, como en el capítulo anterior. Te puede sorprender el porque hemos usado una tupla de EventBox y Image, en vez de simplemente usar la Image del atributo containerChild de las cajas de evento. Esto se debe a que es un atributo de sólo escritura, puede set (establecerse) pero no recuperarse con get .

Finalmente, si no puedes disponer de las imágenes en tu directorio de código fuente, o si quieres extender la máquina con más posibilidades, tienes un gran surtido de peces brasileños en peces. Están clasificados en peces de agua salada (água salgado) y de agua dulce (água doce).

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" ""