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.