Previous Contents Next

A Graphical Calculator

Let's consider the calculator example as described in the preceding chapter on imperative programming (see page ??). We will give it a graphical interface to make it more usable as a desktop calculator.

The graphical interface materializes the set of keys (digits and functions) and an area for displaying results. Keys can be activated using the graphical interface (and the mouse) or by typing on the keyboard. Figure 5.9 shows the interface we are about to construct.


Figure 5.9: Graphical calculator.


We reuse the functions for drawing boxes as described on page ??. We define the following type:

# type calc_state =
{ s : state; k : (box_config * key * string ) list; v : box_config } ;;
It contains the state of the calculator, the list of boxes corresponding to the keys and the visualization box. We plan to construct a calculator that is easily modifiable. Therefore, we parameterize the construction of the interface with an association list:

# let descr_calc =
[ (Digit 0,"0"); (Digit 1,"1"); (Digit 2,"2"); (Equals, "=");
(Digit 3,"3"); (Digit 4,"4"); (Digit 5,"5"); (Plus, "+");
(Digit 6,"6"); (Digit 7,"7"); (Digit 8,"8"); (Minus, "-");
(Digit 9,"9"); (Recall,"RCL"); (Div, "/"); (Times, "*");
(Off,"AC"); (Store, "STO"); (Clear,"CE/C")
] ;;


Generation of key boxes
At the beginning of this description we construct a list of key boxes. The function gen_boxes takes as parameters the description (descr), the number of the column (n), the separation between boxes (wsep), the separation between the text and the borders of the box (wsepint) and the size of the board (wbord). This function returns the list of key boxes as well as the visualization box. To calculate these placements, we define the auxiliary functions max_xy for calculating the maximal size of a list of complete pairs and max_lbox for calculating the maximal positions of a list of boxes.

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


Here is the principal function gen_boxes for creating the 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 = gray1; b2_col = gray3; b_col =gray2}
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 = gray1; b2_col = gray3; 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
Since we would also like to reuse the skeleton proposed on page ?? for interaction, we define the functions for keyboard and mouse control, which are integrated in this skeleton. The function for controlling the keyboard is very simple. It passes the translation of a character value of type key to the function transition of the calculator and then displays the text associated with the calculator state.

# let f_key cs c =
transition cs.s (translation c);
erase_box cs.v;
draw_string_in_box Right (string_of_int cs.s.vpr) cs.v Graphics.white ;;
val f_key : calc_state -> char -> unit = <fun>


The control of the mouse is a bit more complex. It requires verification that the position of the mouse click is actually in one of the key boxes. For this we first define the auxiliary function mem, which verifies membership of a position within a 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_mouse cs 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)) cs.k
in
transition cs.s t;
erase_box cs.v;
draw_string_in_box Right (string_of_int cs.s.vpr ) cs.v Graphics.white
with Not_found -> ();;
val f_mouse : calc_state -> int -> int -> unit = <fun>


The function f_mouse looks whether the position of the mouse during the click is reallydwell within one of the boxes corresponding to a key. If it is, it passes the corresponding key to the transition function and displays the result, otherwise it will not do anything.

The function f_exc handles the exceptions which can arise during program execution.

# let f_exc cs ex =
match ex with
Division_by_zero ->
transition cs.s Clear;
erase_box cs.v;
draw_string_in_box Right "Div 0" cs.v (Graphics.red)
| Invalid_key -> ()
| Key_off -> raise End
| _ -> raise ex;;
val f_exc : calc_state -> exn -> unit = <fun>


In the case of a division by zero, it restarts in the initial state of the calculator and displays an error message on its screen. Invalid keys are simply ignored. Finally, the exception Key_off raises the exception End to terminate the loop of the skeleton.

Initialization and termination
The initialization of the calculator requires calculation of the window size. The following function creates the graphical information of the boxes from a key/text association and returns the size of the principal window.

# let create_e k =
Graphics.close_graph ();
Graphics.open_graph " 10x10";
let mx,my,v,lb = gen_boxes k 4 4 5 2 in
let s = {lcd=0; lka = false; loa = Equals; vpr = 0; mem = 0} in
mx,my,{s=s; k=lb;v=v};;
val create_e : (key * string) list -> int * int * calc_state = <fun>


The initialization function makes use of the result of the preceding function.

# let f_init mx my cs () =
Graphics.close_graph();
Graphics.open_graph (" "^(string_of_int mx)^"x"^(string_of_int my));
Graphics.set_color gray2;
Graphics.fill_rect 0 0 (mx+1) (my+1);
List.iter (fun (b,_,_) -> draw_box b) cs.k;
List.iter
(fun (b,_,s) -> draw_string_in_box Center s b Graphics.black) cs.k ;
draw_box cs.v;
erase_box cs.v;
draw_string_in_box Right "hello" cs.v (Graphics.white);;
val f_init : int -> int -> calc_state -> unit -> unit = <fun>


Finally the termination function closes the graphical window.

# let f_end e () = Graphics.close_graph();;
val f_end : 'a -> unit -> unit = <fun>


The function go is parameterized by a description and starts the interactive loop.

# let go descr =
let mx,my,e = create_e descr in
skel (f_init mx my e) (f_end e) (f_key e) (f_mouse e) (f_exc e);;
val go : (key * string) list -> unit = <fun>


The call to go descr_calc corresponds to the figure 5.9.






Previous Contents Next