Calculatrice graphique
Nous reprenons l'exemple de la calculatrice décrite dans le chapitre
précédent sur la programmation impérative (voir page
??). Nous la dotons d'une interface graphique la rendant
plus facile à être utilisée comme une calculette de bureau.
L'interface graphique matérialisera l'ensemble des touches (chiffres
et fonctions) et une zone de visualisation des résultats. Une touche
pourra être activée soit via l'interface graphique (et la souris)
soit directement au clavier.
La figure 5.9 montre l'interface que l'on désire construire.
Figure 5.9 : Calculatrice graphique
On réutilise les fonctions de tracé de boîtes décrites à la page
??. On définit le type suivant :
# type etat_calc =
{ e : etat; t : (box_config * touche * string ) list; v : box_config } ;;
Il permet de conserver l'état de la calculatrice, la liste des
boîtes correspondant aux touches et la boîte de visualisation. On
désire construire une calculatrice graphique facilement modifiable. On
paramètre alors la construction de l'interface par une liste
d'associations :
# let descr_calc =
[ (Chiffre 0,"0"); (Chiffre 1,"1"); (Chiffre 2,"2"); (Egal, "=");
(Chiffre 3,"3"); (Chiffre 4,"4"); (Chiffre 5,"5"); (Plus, "+");
(Chiffre 6,"6"); (Chiffre 7,"7"); (Chiffre 8,"8"); (Moins, "-");
(Chiffre 9,"9"); (MemoireOut,"RCL"); (Par, "/"); (Fois, "*");
(Off,"AC"); (MemoireIn, "STO"); (Clear,"CE/C")
] ;;
Génération des boîtes des touches
À partir de cette description on construit la liste des boîtes des
touches. La fonction gen_boxes prend pour paramètres une
description
(descr), le nombre de colonnes (n), la
séparation entre boîtes (wsep), la séparation entre le
texte et les bords d'une boîte (wsepint) et la taille des
bords (wbord). Cette fonction retourne la liste des boîtes
des touches ainsi que la boîte de visualisation. Pour calculer ces
placements, on définit les fonctions auxiliaires
max_xy de calcul des tailles maximales d'une liste de
couples d'entiers et max_lbox de calcul des positions
maximales d'une liste de boîtes.
# let gen_xy vals comp o =
List.fold_left (fun a (x,y) -> comp (fst a) x,comp (snd a) y) o vals ;;
val gen_xy : ('a * 'a) list -> ('b -> 'a -> 'b) -> 'b * 'b -> 'b * 'b = <fun>
# let max_xy vals = gen_xy vals max (min_int,min_int);;
val max_xy : (int * int) list -> int * int = <fun>
# let max_boxl l =
let bmax (mx,my) b = max mx b.x, max my b.y
in List.fold_left bmax (min_int,min_int) l ;;
val max_boxl : box_config list -> int * int = <fun>
Voici la fonction principale gen_boxes de création de
l'interface.
# let gen_boxes descr n wsep wsepint wbord =
let l_l = List.length descr in
let nb_lig = if l_l mod n = 0 then l_l / n else l_l / n + 1 in
let ls = List.map (fun (x,y) -> Graphics.text_size y) descr in
let sx,sy = max_xy ls in
let sx,sy= sx+wsepint ,sy+wsepint in
let r = ref [] in
for i=0 to l_l-1 do
let px = i mod n and py = i / n in
let b = { x = wsep * (px+1) + (sx+2*wbord) * px ;
y = wsep * (py+1) + (sy+2*wbord) * py ;
w = sx; h = sy ; bw = wbord;
r=Top;
b1_col = grey1; b2_col = grey3; b_col =grey2}
in r:= b::!r
done;
let mpx,mpy = max_boxl !r in
let upx,upy = mpx+sx+wbord+wsep,mpy+sy+wbord+wsep in
let (wa,ha) = Graphics.text_size " 0" in
let v = { x=(upx-(wa+wsepint +wbord))/2 ; y= upy+ wsep;
w=wa+wsepint; h = ha +wsepint; bw = wbord *2; r=Flat ;
b1_col = grey1; b2_col = grey3; b_col =Graphics.black}
in
upx,(upy+wsep+ha+wsepint+wsep+2*wbord),v,
List.map2 (fun b (x,y) -> b,x,y ) (List.rev !r) descr;;
val gen_boxes :
('a * string) list ->
int ->
int ->
int -> int -> int * int * box_config * (box_config * 'a * string) list =
<fun>
Interaction
Comme l'on désire aussi reprendre le squelette proposé page
?? pour l'interaction, on définit les
fonctions de gestion du clavier et de la souris qui s'intègrent à ce
squelette. La fonction de gestion du clavier est fort simple. Elle
passe la traduction du caractère en valeur de type touche à
la fonction transition de la calculatrice, puis affiche le
texte de l'état de la calculatrice.
# let f_clavier ec c =
transition ec.e (traduction c);
draw_string_in_box Right (string_of_int ec.e.vaf) ec.v Graphics.white ;;
val f_clavier : etat_calc -> char -> unit = <fun>
La gestion de la souris est un peu plus complexe. Elle nécessite de
vérifier que la position du clic souris est bien dans une des boîtes
des touches. Pour cela on définit tout d'abord la fonction auxiliaire
mem qui vérifie l'appartenance d'une position à un rectangle.
# let mem (x,y) (x0,y0,w,h) =
(x >= x0) && (x< x0+w) && (y>=y0) && ( y<y0+h);;
val mem : int * int -> int * int * int * int -> bool = <fun>
# let f_souris ec x y =
try
let b,t,s =
List.find (fun (b,_,_) ->
mem (x,y) (b.x+b.bw,b.y+b.bw,b.w,b.h)) ec.t
in
transition ec.e t;
draw_string_in_box Right (string_of_int ec.e.vaf ) ec.v Graphics.white
with Not_found -> ();;
val f_souris : etat_calc -> int -> int -> unit = <fun>
La fonction f_souris cherche si la position de la souris
lors du clic est bien dans une des boîtes correspondant à une
touche, si oui elle passe la touche correspondante à la fonction de
transition puis affiche le résultat, sinon elle ne fait rien.
La fonction f_exc gère les exceptions pouvant se
déclencher lors de l'exécution de ce programme.
# let f_exc ec ex =
match ex with
Division_by_zero ->
transition ec.e Clear;
draw_string_in_box Right "Div 0" ec.v (Graphics.red)
| Touche_non_valide -> ()
| Touche_off -> raise Fin
| _ -> raise ex;;
val f_exc : etat_calc -> exn -> unit = <fun>
Si une division par zéro survient, elle remet dans l'état initial la
calculatrice et affiche un message d'erreur à l'écran de la
calculatrice. Une touche non valide est simplement ignorée. Enfin
l'exception Touche_off déclenche l'exception Fin de
sortie de la boucle du squelette.
Initialisation et sortie
L'initialisation de la calculatrice nécessite de calculer la taille de
la fenêtre. La fonction suivante engendre les informations graphiques
des boîtes à partir d'une association touche-texte et retourne la
taille de la fenêtre principale.
# let create_e t =
Graphics.close_graph ();
Graphics.open_graph " 10x10";
let mx,my,v,lb = gen_boxes t 4 4 5 2 in
let s = {dce=0; dta = false; doa = Egal; vaf = 0; mem = 0} in
mx,my,{e=s; t=lb;v=v};;
val create_e : (touche * string) list -> int * int * etat_calc = <fun>
La fonction d'initialisation utilise le résultat de la fonction
précédente.
# let f_init mx my ec () =
Graphics.close_graph();
Graphics.open_graph (":0 "^(string_of_int mx)^"x"^(string_of_int my));
Graphics.set_color grey2;
Graphics.fill_rect 0 0 (mx+1) (my+1);
List.iter (fun (b,_,_) -> draw_box b) ec.t;
List.iter
(fun (b,_,s) -> draw_string_in_box Center s b Graphics.black) ec.t ;
draw_box ec.v;
draw_string_in_box Right "hello" ec.v (Graphics.white);;
val f_init : int -> int -> etat_calc -> unit -> unit = <fun>
Enfin la fonction de sortie ferme la fenêtre graphique.
# let f_fin e () = Graphics.close_graph();;
val f_fin : 'a -> unit -> unit = <fun>
La fonction go, paramétrée par une description lance la boucle d'interaction.
# let go descr =
let mx,my,e = create_e descr in
squel (f_init mx my e) (f_fin e) (f_clavier e) (f_souris e) (f_exc e);;
val go : (touche * string) list -> unit = <fun>
L'appel go descr_calc correspond à la figure 5.9.