Another way to avoid bad inputs is to force users to enter valid inputs. While this is difficult with text-based i/o, a graphical interface permits exactly that. By using graphical dialogue elements, we can layout the exact set of available choices and force the user to pick one of them.
Roughly speaking, a graphical dialogue is constructed from basic items, so-called controls, such as:
Each of these items can be used to build a dialogue for our grade recording program. Other items are available, and we will discuss them as needed.
Items are arranged in horizontal lines, which in turn are arranged in vertical stacks. The former are called horizontal panels, the latter vertical panels. The two kinds of panels can be nested in an arbitrarily deep fashion. Finally, if we wish to display a panel, we must add it to a frame, which is a graphical element that windowing systems can add to the desktop. Figure 23 specifies the relationships in the fashion of a data definition.
All graphical elements are Scheme objects. An object is approximately
like a structure. The operation make-object creates objects from a
class and some additional arguments. The class is roughly speaking a
prototype for the structure. It pre-defines values for some of the
object's slots; the others are filled with the arguments.
There are three classes for arranging basic GUI items:
mred:frame%,
mred:vertical-panel%, and
mred:horizontal-panel%make-object. Here are typical ways
to create objects of these classes:
|
The term FP means that we can place the name of a panel or a frame
in the corresponding slot.
The key difference between a structure and an object is that the latter
also contains methods. A method is basically like a function, but
applying a method requires a special syntax:
(send object method-name arguments ...) send form selects the named method from the object and then
applies it to the arguments. The only interesting method of a frame is
show. Specifically,(send frame show #t) frame to the top of the desktop and (send frame show #f)
As mentioned already, the purpose of a panel is to arrange other graphical
items, including panels. Here are four interesting items and typical calls
to make-object:
|
The basic constants are values that ask the graphical run-time environment to display the GUI elements in their natural form.
For an illustration, consider the creation of a slider%:
(make-object mred:slider% panel sCB null 80 0 100 1)
|
The first two arguments, mred:slider% and panel, specify
that we wish to create a slider and that it belongs to panel. The
slider can record numbers between 0 and 100 and is
initially set to 80.
The third argument, named sCB, deserves a detailed explanation. It
is a function and it is invoked every time the user manipulates the
slider. Thus, if a user slides the bar of a mred:slider% object,
sCB is applied to two arguments: the slider and an object that
represents other aspects of the event. To understand its role more fully,
though, we need to study how the user and the program act in parallel and
coordinate their actions.
Take a look at the following four-line DrScheme program:
(define F (make-object mred:frame% null "My First GUI")) (define P (make-object mred:vertical-panel% F)) (define S (make-object mred:slider% P (lambda (this-slider an-event) (printf "~s ~n" (send this-slider get-value))) "A slider:" 80 0 100 1)) (send F show #t)
It first creates a frame and names it F. Then it inserts a panel,
P, into the frame. Finally, it adds a slider, S, to the
panel P. The last line causes the frame to show up on our
computer's desktop.
After we click on EXECUTE, DrScheme will evaluate the three
definitions and the send-expression and will then prompt us for
more Scheme expressions. At the same time, a window titled ``My First GUI''
appears with a labeled slider in it:
|
Now we can freely move the slider and every time it moves, the call-back
function is invoked. In our example, the call-back function prints the
current value of the slider in DrScheme's INTERACTIONS window. In
addition, we can also query the slider by sending it a
get-value message from the Interactions window:
(send S get-value). The result is 80 until we move the
slider.
More generally, when a program creates and displays a graphical user interface element, such as buttons and sliders, it permits to perform actions on this element in an independent fashion. Hence, a program that uses GUI elements and its user should be perceived as two independently acting, yet interacting agents. Interaction means that, on occasion, the program queries the state of the GUI gadgets or that a call-back function must be evaluated.
Since the call-back functions are a part of the program they cannot be
called in parallel to other parts of the program. That is, as long as some
function in our program is evaluated, no call-back can be evaluated.
Instead, all user events, e.g., clicks on a button, moves of a slider, etc. are queued and, when the program stops or explicitly yields control
to the call-backs, an event handler invokes the call-back functions that
correspond to the queued events. This situation is similar to a text-based
interactive program that yields control to the user by evaluating the
expression (read) and that resumes its execution when the user hits
<enter>.
Unfortunately, the situation is far more complex in a graphical interaction
context. Suppose our little sample program not only creates a slider but
also a FIXED button:
(define (qCB e i) (printf "user clicked on FIXED~n")) (define Q (make-object mred:button% P qCB "FIXED"))
In this case, the user cannot just move the slider but also click on the
button labeled "FIXED". Furthermore, the user can move the slider
by many units, can click on the button many times, and can switch back and
forth between the two actions as often as imaginable. As long as the
program runs, none of the events will be handled; when the program stops,
the event handler will call the call-back functions, which will print some
text to the INTERACTION window of DrScheme.
Now imagine a program is supposed to wait for a particular event. For
example, the program can only use the value of the slider after the
user clicked on the button labeled "FIXED" for the first time. The
program must not only yield control to the event handler and wait for the
execution of the call-backs, it must also wait for a specific call-back
function to be invoked. That is, we must coordinate the actions of the
program and call-back functions, which represent user-actions .
To coordinate actions, we use semaphores. A semaphore is a structure
that contains a single positive number. There are two important operations
on semaphores: wx:yield and semaphore-post. The latter
increases the value of the semaphore by 1. The former attempts to
decreases the semaphore's value. If that is possible, wx:yield
returns control to the program; otherwise it blocks the execution of the
program thread and yields control to the event handler. The event handler
invokes call-back functions until the semaphore is increased -- by one of
the call-backs. When the semaphore assumes a positive value,
wx:yields decreases the semaphore's value and returns.
Equipped with this simple model of events and semaphores, we are now ready to re-design our grade maintenance program.
A closer look at the text-based grade recording program suggests that the
key change concerns ask-and-add. It is this function that
primarily interacts with the user, so we must modify it to get a GUI
version of our program.
From the definition of ask-and-add, we know that a dialogue
consists of four actions:
By iterating this function with map over the entire list, we get a
new grade record for each student. At the very beginning the program also
prints a start-up message that reminds the user of how the program works.
To implement this functionality with a GUI interface, we need analogous GUI
elements. For the start-up message and for printing the name of the
student, we use mred:message% objects. Since the grade is a number
between 0 and 100, we use a mred:slider% object
for inputing the grade. Finally, we simulate the <enter> key with a
mred:button%. Clicking on this button will let the new version of
ask-and-add know that the grade is available on the slider.
Here is one way to arrange these four elements:
|
![]()
|
The GUI consists of two ``lines'' that are stacked on top of each other. The first contains the start-up message and the button that corresponds to the <enter> key. The second one contains the text field for displaying the student's name and the slider for picking a grade.
;; --- PICTURE: the basic frame and panel --- (define FRAME (make-object mred:frame% null "Grade Note Book")) (define VPANL (make-object mred:vertical-panel% FRAME)) ;; --- LINE 1: the panel for the message board and next button --- (define MSG&NEXT (make-object mred:horizontal-panel% VPANL)) (define MSG (make-object mred:message% MSG&NEXT "Select a grade & click here:")) (define NEXT (make-object mred:button% MSG&NEXT nextCB "NEXT")) ;; --- LINE 2: the panel for the student's name and grade --- (define NAME&GRADE (make-object mred:horizontal-panel% VPANL)) (define NAME (make-object mred:message% NAME&GRADE (make-string 10 #\space))) (define GRADE (make-object mred:slider% NAME&GRADE void null 80 0 100 10))
Figure 24: The Grader Program: The GUI Elements
;; gui-update: show frame, update grades using gui, hide frame (define (gui-update) (send FRAME show #t) (update-all-grades) (send FRAME show #f)) ; (mred:exit) ;; update-all-grades: read grades, get new grades interactively, write them out (define (update-all-grades) (save (map ask-and-add (retrieve)))) ;; retrieve : -> (list-of line) (define (retrieve) (call-with-input-file DATABASE read)) ;; save : (list-of line) -> void ;; writes its input to DATABASE, erases existing contents (define (save new-grades) (call-with-output-file DATABASE (lambda (op) (fprintf op ";; do not edit: this file is generated by a program~n") (pretty-print new-grades op)) 'truncate)) ;;ask-and-add : line -> line;; the result has one more grade than the input (define (ask-and-add a-line) (let ((name (first a-line))) (send NAME set-label (symbol->string name)) (wx:yield semaNEXT) (cons name (cons (send GRADE get-value) (rest a-line))))) ;;nextCB: a GUI button whose call-back posts tosemaNEXT(define (nextCB e i) (semaphore-post semaNEXT)) ;;semaNEXT: a semaphore that coordinates between ask and NEXT (define semaNEXT (make-semaphore 0))
Figure 25: The Grader Program: The Functions
Translating this description into Scheme definition is relatively
straightforward: see figure 24. The three parts, labeled
``PICTURE,'' ``LINE 1,'' and ``LINE 2,'' correspond to the ubiquitous
frame and the two lines in our sketch. The call-back functions for the
slider is void; the one for the button labeled ``NEXT'' will be
defined shortly.
With this setup, we can adapt the definition of ask-and-add by
translating it line by line. Instead of printing the name of the student,
we now send it to NAME using a set-label method. To
implement the equivalent of (read), we send the message
get-value to the slider:
;; ask-and-add : line -> line
;; the result has one more grade than the input
(define (ask-and-add a-line)
(let ((name (first a-line)))
(send NAME set-label (symbol->string name))
(wx:yield semaNEXT)
(cons name (cons (send GRADE get-value) (rest a-line)))))
The only unusual part is the next-to-last line: (wx:yield semaNEXT). It causes ask-and-add to wait for
some call-back function to post to the semaNEXT semaphore.
The semaphore coordinates between the user and the query program. Since the button labeled ``NEXT'' plays the role of the <enter> key, its call-back function must be the one that posts to the semaphore:
;;nextCB: a GUI button whose call-back posts tosemaNEXT(define (nextCB e i) (semaphore-post semaNEXT)) ;;semaNEXT: a semaphore that coordinates between ask and NEXT (define semaNEXT (make-semaphore 0))
Naturally the semaphore is initialized to 0, which says that the
user hasn't clicked on the button yet. Similarly, wx:yield's
resetting of the semaphore to 0 readies the semaphore for the next
interaction, which is initiated by map's interaction of
ask-and-add over the entire list of students records. The complete
program, minus the GUI parts, is summarized in figure 25.
When users edit files, they occasionally make mistakes. For example, a grader may discover that the grades entered so far are those of some old homework, not the latest one. In such cases, users (may) want to stop the program and cancel the session. Fortunately, our program organization renders this action trivial.
Both programs rely on map to iterate over the existing list of
records. For each record, ask-and-add creates a new record, which
map puts into a list. The final result of map is then
written to a file. If at any intermediate stage the user decides to abort
the update, a simple action suffices. In the case of the textual version, a
control-c suffices. Since no data is written until all new grades are
entered, all files will still be intact.
Exercise: Add a button labeled ``QUIT'' to the GUI
interface. When a user clicks on the button, the program should call
exit with -1, which will immediately abort the program
evaluation. ¤
In section 1, we saw how to turn a Scheme program into a stand-alone Unix script. For programs that use GUI elements, the process is similar but different. Instead of interpreting the program in the plain mzscheme interpreter, we need to use the mred run-time environment. Assuming the file with the Scheme program is called |gui.ss|, the script looks like this:
#!/bin/sh string=? ; exec mred -u -b -- -f $0 "$@" (load "the-gui.ss") (define DATEBASE "grades.dat") (gui-update)
Roughly speaking, we replace mzscheme by mred and use slightly
different labels. The more important difference, however, is that
processing command-line arguments is more complex in mred than in
mzscheme. Hence, we simply define DATABASE to be
"grades.dat" until we learn how to interpret the command-line.