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.