Part IX

Classes and Interfaces

0.1  Simple Graphical User Interfaces (GUIs)

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.

A gui consists of a frame.

A frame contains a control.

A control is either

  1. a horizontal panel that contains controls
  2. a vertical panel that contains controls, or
  3. a radio-box,
  4. a list-box,
  5. a slider,
  6. a button, or
  7. a check-box.

Figure 23:  A Data Definition for GUIs

0.1.1  Elements of Graphical Dialogues

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.

0.1.2  Elements of Graphical Dialogues: Details

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:

mred:frame% (make-object mred:frame% null string)
mred:vertical-panel% (make-object mred:vertical-panel FP)
mred:horizontal-panel% (make-object mred:horizontal-panel FP)

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 ...) 
The 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) 
brings frame to the top of the desktop and
 (send frame show #f) 
hides it. There are no methods for panels that are of concern to us.

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:

mred:message% (make-object mred:message% panel string)
mred:button% (make-object mred:button% panel bCB string)
mred:slider%

(make-object mred:slider% panel 
             sCB 
             null 80 0 100 1)

mred:radio-box%

(make-object mred:radio-box% panel 
             rCB 
             null -1 -1 -1 -1 
             (list-of string) 
             0 wx:const-horizontal)

mred:choice%

(make-object mred:choice% panel 
             cCB 
             null -1 -1 -1 -1 
             (list-of string))

mred:list-box%

(make-object mred:list-box% panel 
             rCB 
	     null wx:const-single -1 -1 -1 -1
             (list-of string))

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.

0.1.3  Parallel Actions, Events, and Semaphores

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.

0.1.4  Designing a Simple Grader Dialogue

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:

  1. The function displays the name of a student.

  2. The user types in a grade ...

  3. ...and then hits the <enter> key.

  4. At that point, the function reads the grade, inserts it into the record, and returns the new record as its result.

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 to semaNEXT
(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 to semaNEXT
(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.

0.2  Why map?

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. ¤

0.3  How to Run It All, Again

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.