Tutoriel Gtk2Hs

6.1 Fenêtres avec défilement

Les fenêtres avec défilement sont utiles pour créer une zone déroulante avec un autre widget à l’intérieur. Vous pouvez insérer n’importe quel type de widget dans une fenêtre déroulante qui sera accessible quelque soit sa taille en utilisant les barres de défilement.

La fonction suivante sert à créer une nouvelle fenêtre avec défilement.

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

Le premier argument est le réglage pour la direction horizontale et le deuxième est pour le réglage vertical. Ils sont, la plupart du temps définis à Nothing.

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

Cette fonction sert à définir la politique d’affichage des barres de défilement horizontales et verticales. Le constructeur PolicyAlways affiche toujours les barres de défilement, PolicyNever ne les affichent jamais et PolicyAutomatic les affichent seulement si la taille de la page est plus grande que la fenêtre. La valeur par défaut est PolicyAlways.

Pour placer un objet dans la fenêtre avec défilement, on utilise containerAdd si cet objet a une fenêtre qui lui est associée. Si il n’en a pas, un Viewport est nécessaire, mais on peut en ajouter un automatiquement avec:

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

Voici un exemple qui empile un tableau de 100 boutons bascules dans une fenêtre avec défilement. Cet exemple est tiré de par Hal Daumé III. Ce tutoriel est librement disponible sur le site internet de Haskell. Ce programme laisse l’utilisateur trouver un nombre entre 1 et 100 en lui indiquant si le nombre qu’il indique est trop grand, trop petit ou correct. Le nombre aléatoire est généré avec la fonction randomRIO du module System.Random.

Cet exemple implémente ce programme avec une interface graphique.

Gtk Gtk2Hs Scrolled Window

Dans la fenêtre principale, on utilise une boite verticale pour empaqueter une étiquette (pour guider l’utilisateur) un séparateur horizontal, une fenêtre avec défilement, un séparateur horizontal et une boite horizontale pour deux boutons. La fenêtre avec défilement est empaquetée avec PackGrow pour qu’elle puisse être redimensionnée avec la fenêtre principale. Les boutons Play et Quit sont empaquetés aux bords opposés de la boite horizontale.

Les 100 boutons sont créés avec :

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

La fonction numButton est définie par:

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

Chaque bouton a le numéro approprié comme étiquette.

A l’intérieur de la fenêtre avec défilement, on créé un tableau de 10 par 10 pour les 100 boutons. Pour positionner les boutons, on utilise la fonction cross.

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

La fonction attachButton prend un tableau, un bouton et un couple de coordonnées pour placer le bouton dans le tableau. (Voir Chapitre 3.3 pour plus d’informations sur l’empaquetage des tableaux).

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

Le morceau de code suivant empaquette tous les boutons dans le tableau avec buttonlist décrit précédemment.

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

Chaque fois que l’utilisateur appuie le bouton Play, un nombre aléatoire est généré qui peut ensuite être comparé au choix de l’utilisateur. Mais le gestionnaire de signaux de Gtk2Hs onClicked prend un bouton et une fonction sans paramètre de type IO (). Il faut donc quelque chose qui ressemble à des variables globales qui sont apportées par le module Data.IORef. On peut alors utiliser les morceaux de code suivant dans différentes fonctions pour initialiser, écrire et lire le nombre aléatoire.

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

Le premier reçoit une variable de type IORef Int et l’initialise à 50. Le second est implémenté dans la fonction 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)

la fonction actionButton implémente la lecture de randstore. Elle compare alors le nombre obtenu de l’étiquette du bouton qui a été cliqué et affiche l’information appropriée sur info.

Enfin, il faut surveiller tous les 100 boutons pour trouver celui qui a été pressé.

     sequence_ (map (actionButton info randstore) buttonlist)

Le code ci-dessus est similaire aux autres combinaisons de sequence et map que l’on a déjà utilisé mais dans le cas présent, seul un des 100 signaux sera déclenché lorsque l’utilisateur presse ce bouton en particulier.

Le code complet de l’exemple est:

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)