6.1 Ventanas con desplazamiento (scroll)

Las ventanas con desplazamiento se usan para crear un área desplazable con otro widget dentro de él. Puedes insertar cualquier tipo de widget en una ventana desplazable, y será accesible, sin tener en cuenta su tamaño, mediante el uso de las barras de desplazamiento (scroll bars)

La siguiente función se usa para crear una nueva ventana desplazable.

scrolledWindowNew :: Maybe Adjustment -> Maybe Adjustment -> IO ScrolledWindow

El primer argumento es el ajuste de la dirección horizontal, y el segundo el de la dirección vertical. Casi siempre se establecen a Nothing (nada) .

scrolledWindowSetPolicy :: ScrolledWindowClass self =>
self -> PolicyType -> PolicyType -> IO ()

Este establece la política a usar con respecto a las barras de desplazamiento verticales y horizontales. El constructor PolicyAlways muestra siempre la barra de desplazamiento, PolicyNever no la muestra nunca y PolicyAutomatic la muestra sólo si el tamaño de la página es mayor que la ventana. Por defecto usa PolicyAlways.

A continuación puedes colocar tu objeto en la ventana con desplazamiento usando containerAdd si el objeto tiene una ventana asociada con él. Si no lo tiene, necesitarás un Viewport , aunque puedes añadir una automáticamente con:

scrolledWindowAddWithViewport :: (ScrolledWindowClass self, WidgetClass child)
=> self -> child -> IO ()

Si te olvidas del viewport, GHCi produce un mensaje de error si usas containerAdd y no has debido hacerlo.

En el ejemplo empaquetamos una tabla con 100 botones toggle en una ventana con desplazamiento. Implementa un programa de 'Yet Another Haskell Tutorial' (y otro tutorial de Haskell) de Hal Daumé III. Este tutorial está disponible en forma gratuita en el web de Haskell. En la página 43 hay un programita que permite adivinar un numero entre 1 y 100, seleccionado al azar por la máquina, indicando si nuestra selección es menor, mayor o igual. El número se genera con la función randomRIO del módulo System.Random.

Nuestro ejemplo lo implementa con un interfaz gráfico.

Scrolled Window

En la ventana principal usamos una caja vertical para empaquetar una etiqueta (para información del usuario), un separador horizontal, una ventana con desplazamiento, un separador horizontal y una caja horizontal que contiene dos botones de stock. La ventana con desplazamiento se empaqueta con PackGrow, lo que le permite adaptarse a los cambios de tamaño de la ventana principal. Los botones nuevo (new) y salir (quit) se empaquetan en los extremos opuestos de la caja horizontal.

Los 100 botones se crean con:

     buttonlist <- sequence (map numButton [1..100])

donde la función numButton se define como:

numButton :: Int -> IO Button
numButton n = do
        button <- buttonNewWithLabel (show n)
        return button

Así, cada botón automáticamente obtiene el número apropiado como etiqueta.

Dentro de la ventana con desplazamiento creamos una tabla de 10 por 10 para los 100 botones. Para posicionar los botones usamos la función cross , que se basa en List monad. Esta función, un modo sencillo de obtener un producto cartesiano de dos o más listas, también está explicado en el tutorial ya citado.

cross :: [Int] -> [Int] -> [(Int,Int)]
cross row col = do 
        x <- row
        y <- col
        return (x,y)

La función attachButton parte de una tabla, un botón y una tupla de coordenadas para colocar un botón en la tabla. (Repasa el capítulo 3.3 para más información sobre empaquetado de tablas.)

attachButton :: Table -> Button -> (Int,Int) -> IO () 
attachButton ta bu (x,y) = tableAttachDefaults ta bu y (y+1) x (x+1)

Ahora, el siguiente segmento de código empaqueta todos los botones en la tabla con buttonlist según se ha descrito.

     let places = cross [0..9] [0..9]
     sequence_ (zipWith (attachButton table) buttonlist places)

Cada vez que el usuario pulsa el botón play se genera un número aleatorio, que habrá que ir comparando con la elección del usuario. Pero el manejador de señales de Gtk2Hs onClicked emplea un botón y una función sin parámetros y tiene un valor de tipo IO () . Necesitamos algo así como una variable global, y esta es aportada por el módulo Data.IORef. Ahora podemos usar los siguientes snippets, en diferentes funciones, para inicializar, escribir y leer el número aleatorio.

snippet 1   --  randstore <- newIORef 50  
snippet 2   --  writeIORef rst rand  
snippet 3   --  rand <- readIORef rst

El primero obtiene una variable de tipo IORef Int y la inicializa al valor 50. La segunda se implementa con la función aleatoria randomButton :

randomButton :: ButtonClass b => Label -> IORef Int -> b -> IO (ConnectId b)
randomButton inf rst b = 
    onClicked b $ do rand <- randomRIO (1::Int, 100)
                     writeIORef rst rand  
                     set inf [labelLabel := "Ready"]
                     widgetModifyFg inf StateNormal (Color 0 0 65535)

y después usa el siguiente snippet, donde info es la etiqueta que permite acceder a la información de usuario. (Mira el capítulo 5.3 para los colores y como cambiarlos.)

De un modo parecido a la escritura del número aleatorio, la función actionButton implementa la lectura de randstore. Entonces compara el número obtenido de la etiqueta del botón que ha sido pulsado, y muestra la información en la etiqueta info.

Finalmente debemos monitorizar los 100 botones para saber cual ha sido pulsado, si ha habido alguno.

     sequence_ (map (actionButton info randstore) buttonlist)

Lo anterior es análogo a las combinaciones de sequence _ y map que hemos usado, pero en este caso exactamente uno de los 100 manejadores de señal será activado, en el momento en que el usuario pulse un botón concreto.

A continuación tienes el código completo del ejemplo.

import Graphics.UI.Gtk
import Data.IORef 
import System.Random (randomRIO)

main:: IO ()
main= do
     initGUI
     window <- windowNew
     set window [ windowTitle := "Guess a Number", 
                  windowDefaultWidth := 300, windowDefaultHeight := 250]
     mb <- vBoxNew False 0
     containerAdd window mb

     info <- labelNew (Just "Press \"New\" for a random number")
     boxPackStart mb info PackNatural 7
     sep1 <- hSeparatorNew
     boxPackStart mb sep1 PackNatural 7
     
     scrwin <- scrolledWindowNew Nothing Nothing
     boxPackStart mb scrwin PackGrow 0

     table <- tableNew 10 10 True
     scrolledWindowAddWithViewport scrwin table

     buttonlist <- sequence (map numButton [1..100])
     let places = cross [0..9] [0..9]
     sequence_ (zipWith (attachButton table) buttonlist places)

     sep2 <- hSeparatorNew
     boxPackStart mb sep2 PackNatural 7
     hb <- hBoxNew False 0
     boxPackStart mb hb PackNatural 0
     play <- buttonNewFromStock stockNew
     quit <- buttonNewFromStock stockQuit
     boxPackStart hb play PackNatural 0
     boxPackEnd hb quit PackNatural 0
     
     randstore <- newIORef 50
     randomButton info randstore play

     sequence_ (map (actionButton info randstore) buttonlist)  

     widgetShowAll window
     onClicked quit (widgetDestroy window)
     onDestroy window mainQuit
     mainGUI

numButton :: Int -> IO Button
numButton n = do
        button <- buttonNewWithLabel (show n)
        return button

cross :: [Int] -> [Int] -> [(Int,Int)]
cross row col = do 
        x <- row
        y <- col
        return (x,y)

attachButton :: Table -> Button -> (Int,Int) -> IO ()
attachButton ta bu (x,y) = 
              tableAttachDefaults ta bu y (y+1) x (x+1)

actionButton :: ButtonClass b => Label -> IORef Int -> b -> IO (ConnectId b)
actionButton inf rst b = 
  onClicked b $ do label <- get b buttonLabel
                   let num = (read label):: Int
                   rand <- readIORef rst
                   case compare num rand of
                     GT -> do set inf [labelLabel :=  "Too High"]
                              widgetModifyFg inf StateNormal (Color 65535 0 0)
                     LT -> do set inf [labelLabel := "Too Low"]
                              widgetModifyFg inf StateNormal (Color 65535 0 0)
                     EQ -> do set inf [labelLabel := "Correct"]
                              widgetModifyFg inf StateNormal (Color 0 35000 0)

randomButton :: ButtonClass b => Label -> IORef Int -> b -> IO (ConnectId b)
randomButton inf rst b = 
    onClicked b $ do rand <- randomRIO (1::Int, 100)
                     writeIORef rst rand  
                     set inf [labelLabel := "Ready"]
                     widgetModifyFg inf StateNormal (Color 0 0 65535)