Author: Christian.Queinnec@lip6.fr

## Événements

La gestion des événements produits dans la fenêtre graphique permet de construire une interaction conviviale entre l'utilisateur et le programme. Les événements traités par `Graphics` sont l'appui d'une touche au clavier, le clic de la souris et le mouvement de celle-ci.
Le style de programmation change ainsi que l'organisation du programme. Celui-ci devient une boucle sans fin d'attente d'événements. À chaque nouveau déclenchement d'un événement, celui-ci est traité, puis le programme revient dans la boucle d'attente sauf lorsqu'un événement est prévu pour provoquer la sortie du programme.

### Types et fonctions sur les événements

La fonction principale d'attente d'événements est `wait_next_event` de type event list -> status.
Les différents événements sont donnés par le type somme event.

    type event = Button_down | Button_up | Key_pressed | Mouse_motion | Poll ;;

Les quatre premières valeurs correspondent, respectivement, à l'appui ou au relâchement d'un bouton de souris, au déplacement de la souris et à l'appui d'une touche. L'attente est bloquante, sauf si le constructeur `Poll` est ajouté à la liste des événements. Cette fonction retourne une valeur de type status :

    type status =
      { mouse_x : int;
        mouse_y : int;
        button : bool;
        keypressed : bool;
        key : char};;

C'est un enregistrement contenant la position de la souris, un booléen indiquant si un bouton est pressé, un autre booléen pour le clavier et un caractère correspondant au caractère de la touche utilisée. Les fonctions suivantes exploitent les données de l'enregistrement des événements :

- `mouse_pos` : unit -> int * int : retourne la position de la souris par rapport à la fenêtre, si la souris est ailleurs, les coordonnées sont en dehors des bornes de la fenêtre.
- `button_down` : unit -> bool : indique la pression sur un bouton de la souris.
- `read_key` : unit -> char : renvoie un caractère tapé au clavier; l'attente est bloquante.
- `key_pressed` : unit -> bool : indique si une touche du clavier est pressée; l'attente est non bloquante.

La gestion d'événements proposée par `Graphics` est vraiment minimale pour la construction d'interfaces interactives. Néanmoins, le code est portable sur différents systèmes graphiques comme Windows, MacOS ou X-Windows. C'est d'ailleurs pour cela que cette bibliothèque ne tient pas compte des différents boutons de la souris. En effet les Mac n'en possèdent qu'un seul. Les autres événements : exposition et changement de taille de la fenêtre ne sont pas accessibles et sont laissés à la charge de la bibliothèque.

### Squelette de programme

Tous les programmes d'interface utilisateur comprennent une boucle potentiellement infinie d'attente d'action de l'utilisateur. Une fois celle-ci transmise, le programme effectue le travail lié à cette action. La fonction suivante possède cinq paramètres fonctionnels. Les deux premiers servent à démarrer et à fermer l'application. Les deux suivants sont les fonctions de traitement des événements clavier et souris. Le dernier permet la gestion des exceptions lors des différents calculs de l'application. On suppose que les événements associés à la sortie de l'application déclenchent l'exception `Fin`.

    # exception Fin;;
    exception Fin
    # let squel f_init f_end f_key f_mouse f_except =
      f_init () ;
      try
          while true do
            try
              let s = Graphics.wait_next_event
                        [Graphics.Button_down; Graphics.Key_pressed]
              in if s.Graphics.keypressed then f_key s.Graphics.key
                 else if s.Graphics.button
                      then f_mouse s.Graphics.mouse_x s.Graphics.mouse_y
            with
                 Fin -> raise Fin
               |  e  -> f_except e
          done
      with
          Fin  -> f_end () ;;
    val squel :
      (unit -> 'a) ->
      (unit -> unit) ->
      (char -> unit) -> (int -> int -> unit) -> (exn -> unit) -> unit = <fun>

Le squelette est utilisé pour programmer une mini-machine à écrire. L'appui d'une touche affiche le caractère tapé. Le clic d'une souris change le point courant. Le caractère '§' fait sortir du programme. La seule difficulté de ce programme est le saut de ligne. On suppose, pour simplifier, que la hauteur des caractères n'excède pas douze pixels.

    # let next_line () =
       let (x,y) = Graphics.current_point()
       in if y>12 then Graphics.moveto 0 (y-12)
          else Graphics.moveto 0 y ;;
    val next_line : unit -> unit = <fun>
    # let trait_char c = match c with
         '§'  -> raise Fin
       | '\n' -> next_line ()
       | '\r' -> next_line ()
       |   _  -> Graphics.draw_char c ;;
    val trait_char : char -> unit = <fun>
    # let go () = squel
       (fun () -> Graphics.clear_graph () ;
                  Graphics.moveto 0 (Graphics.size_y() -12) )
       (fun () -> Graphics.clear_graph())
       trait_char
       (fun x y -> Graphics.moveto x y)
       (fun e -> ()) ;;
    val go : unit -> unit = <fun>

Ce programme ne gère pas l'effacement d'un caractère par l'appui de la touche `DEL`.

### Exemple : le télécran

Le télécran est ce petit jeu de dessin pour la coordination des mouvements. Un point apparaît sur une ardoise. Ce point peut se déplacer en X et en Y en utilisant deux boutons de contrôle de ces axes, sans jamais relever le crayon. On cherche à simuler ce comportement pour illustrer l'interaction entre un programme et un utilisateur. Pour cela on reprendra le squelette décrit précédemment. On utilisera certaines touches du clavier pour indiquer un mouvement sur un des axes.
On définit tout d'abord le type etat qui est un enregistrement décrivant la taille de l'ardoise en nombre de positions en X et en Y, la position courante du point, le facteur d'échelle pour la visualisation, la couleur du trait, la couleur du fond et la couleur pour repérer le point courant.

    # type etat = {maxx:int; maxy:int; mutable x : int; mutable y :int;
                  scale:int;
                  bc : Graphics.color;
                  fc: Graphics.color; pc : Graphics.color} ;;

La fonction `draw_point` affiche un point en lui donnant comme paramètres les coordonnées, le facteur d'échelle et la couleur.

    # let draw_point x y s c=
       Graphics.set_color c;
       Graphics.fill_rect (s*x) (s*y) s s ;;
    val draw_point : int -> int -> int -> Graphics.color -> unit = <fun>

Toutes les fonctions d'initialisation, de gestion de l'interaction et de sortie du programme recevront un paramètre correspondant à l'état. Les quatre premières fonctions sont définies ci-après :

    # let t_init e () =
       Graphics.open_graph (":0 " ^ (string_of_int (e.scale*e.maxx)) ^
         "x" ^ (string_of_int (e.scale*e.maxy))) ;
       Graphics.set_color e.bc ;
       Graphics.fill_rect 0 0 (e.scale*e.maxx+1) (e.scale*e.maxy+1) ;
       draw_point  e.x e.y e.scale e.pc ;;
    val t_init : etat -> unit -> unit = <fun>
    # let t_end e () =
       Graphics.close_graph() ;
       print_string "A bientôt..."; print_newline() ;;
    val t_end : 'a -> unit -> unit = <fun>
    # let t_mouse e x y = () ;;
    val t_mouse : 'a -> 'b -> 'c -> unit = <fun>
    # let t_except e ex = () ;;
    val t_except : 'a -> 'b -> unit = <fun>

La fonction `t_init` ouvre la fenêtre graphique et affiche le point courant, `t_end` ferme cette fenêtre et affiche un message, `t_mouse` et `t_except` ne font rien. Le programme ne gérera pas les événements souris, ni les exceptions qui risquent de se déclencher à l'exécution. La fonction importante est celle de la gestion du clavier `t_key` :

    # let t_key e c =
      draw_point e.x e.y e.scale e.fc;
       (match c with
       '8' -> if e.y < e.maxy then  e.y <- e.y + 1;
     | '2' -> if e.y > 0 then e.y <- e.y - 1
     | '4' -> if e.x > 0 then e.x <- e.x - 1
     | '6' -> if e.x < e.maxx then e.x <- e.x + 1
     | 'c' ->   Graphics.set_color e.bc;
                Graphics.fill_rect 0 0 (e.scale*e.maxx+1) (e.scale*e.maxy+1);
                Graphics.clear_graph()
     | 'f' -> raise Fin
     | _ -> ());
      draw_point e.x e.y e.scale e.pc;;
    val t_key : etat -> char -> unit = <fun>

Elle affiche le point courant de la couleur du trait, et selon le caractère passé modifie, si possible, les coordonnées du point courant (caractères : '2', '4', '6', '8'), efface l'écran (caractère : 'c') ou déclenche l'exception `Fin` (caractère : 'f'), puis affiche le nouveau point courant. Les caractères autres que ceux mentionnés sont ignorés. Le choix des caractères de déplacement du curseur provient de la disposition du pavé numérique : les touches choisies correspondent aux chiffres indiqués et aux flèches de direction. Il est utile alors d'activer le pavé numérique pour l'ergonomie du programme.
On définit enfin un état et on applique la fonction squelette de la manière suivante :

    # let etel = {maxx=120;maxy=120; x= 60; y= 60;
                 scale=4; bc=Graphics.rgb 130 130 130;
                 fc=Graphics.black; pc = Graphics.red};;
    val etel : etat =
      {maxx=120; maxy=120; x=60; y=60; scale=4; bc=8553090; fc=0; pc=16711680}
    # let ardoise () =
       squel (t_init etel)  (t_end etel) (t_key etel)
        (t_mouse etel) (t_except etel);;
    val ardoise : unit -> unit = <fun>

L'appel à `ardoise` affiche la fenêtre graphique puis attend une interaction clavier. La figure 5.8 montre un dessin réalisé par ce programme.

> Figure 5.8 : Le télécran