The interactive programs that we've so far written in Scheme have used
text interfaces -- users supply input through the keyboard and
Scheme reads it in by calling read or read-char, generating
interactive output by invoking display, write, write-char, and newline. All of the interactions are exchanges of
text information.
Often, however, users prefer programs that have graphical interfaces that can receive input from the mouse as well as the keyboard and can display output as graphs, pictures, and diagrams as well as text. In addition, many users are now accustomed to the look and feel of the Macintosh user interface or the Windows user interface and prefer programs that display windows, menus, on-screen buttons, scroll bars, and so on in a
DrScheme includes a ``graphical toolbox,'' MrEd, that comprises class definitions for the elements of familiar graphical interfaces. To write a DrScheme program that receives its input and displays its output through a graphical interface, we create objects belonging to these classes and send them appropriate messages.
Let's start with a tiny example: a program that just displays a cheerful greeting in a small window, which the user can minimize, resize, or close in whatever way is conventional in his familiar user interface.
We begin by creating an object of MrEd's primitive frame% class.
When displayed on screen, this object makes up the outer boundary of a
window, with a title bar at the top and buttons in one or more of the
corners to control the window. We'll call our frame greeting-frame,
and give it the title `Greetings!':
(define greeting-frame (make-object frame% "Greetings!"))
The title, which must be a string, is an initialization argument for
objects of the frame% class.
A frame has to enclose something. In this case, the only object that we're
going to put into the frame belongs to a class that the MrEd authors
decided to call message%, thus ensuring terminological confusion.
For the rest of this lab, I'll call objects of this class
``MrEd-message-objects,'' to distinguish them from messages in the sense of
instance variables that have procedures as values and can appear in send-expressions.
A MrEd-message-object needs two initialization arguments: one to specify the its label (that is, the string constituting the text that is to be displayed), and a second one to indicate the container -- in this case, the frame -- within which the MrEd-message-object will be displayed.
(define greeting-message (make-object message% "Hello, world!" greeting-frame))
If we allow MrEd to set the size of the frame, it chooses the smallest
possible frame that will fit around the MrEd-object-message. In this case,
since the text of the MrEd-object-message is quite short, that frame would
be awkwardly small. We can avoid the awkwardness by sending
greeting-frame some messages that modify the fields in which
it keeps track of the minimum width and height, as measured in pixels (the
individual dots that make up the screen image):
(send greeting-frame min-width 200) (send greeting-frame min-height 50)
This ensures that the window in which greeting-message is displayed
is always at least two hundred pixels wide and fifty pixels high.
(As this example shows, the creators of this class did not follow the useful convention that the name of a message should begin with a colon, nor did they use exclamation points in the names of messages that have side effects.)
Now we are ready to make the frame and its contents visible on screen. We
do this by sending it the show message, with the Boolean argument
#t:
(send greeting-frame show #t)
And our window appears:
The show message actually sets a Boolean field of the frame. When
that field is set to #f, the frame still exists, but it is not
visible on screen; when it is set to #t, the frame appears. It
continues to exist until the user closes it.
The two definitions and three send-expressions are the whole
program:
;;; hello: display a cheerful greeting in a separate window ;; Create the frame, GREETING-FRAME, for the window. (define greeting-frame (make-object frame% "Greetings!")) ;; Create the window containing the greeting. (define greeting-message (make-object message% "Hello, world!" greeting-frame)) ;; Establish its minimum dimensions. (send greeting-frame min-width 200) (send greeting-frame min-height 50) ;; Make it visible. (send greeting-frame show #t)
If you save them in a file -- say, hello.ss -- you can run the program by loading the file.
Suppose, now, that we want to add to this user interface a Dismiss button, which the user can click on when she has read and fully
appreciated everything that the window has to say. As you might expect, we
construct such a button by invoking make-object:
(define dismiss-button
(make-object button%
"Dismiss"
greeting-frame
(lambda (button event)
(send greeting-frame show #f))))
Here we provide three initialization arguments. The first, "Dismiss", is the label that is to be printed on the button. The second,
greeting-frame, is the container in which the button is to be
enclosed. The third is a callback procedure, which is invoked
when the user clicks on the button, with the dismiss-button itself as the
first argument and the symbol 'button automatically filled in as the
second argument. The effect of the callback procedure is to send greeting-frame the message that tells it to make itself invisible.
Suppose we want a kind of button that keeps track of how many times it's
been clicked. We can extend the button% class, adding a field
to hold the click count and a message that returns it:
;;; CLICK-TALLYING-BUTTON%: a button in a graphical user interface that ;;; keeps track of how many times it has been clicked. (define click-tallying-button% (class button% (init ctb-label ctb-parent ctb-callback) ;; Add a CLICK-COUNT field to tally the clicks. (field (click-count 0)) ;; The :REPORT message directs the object to return the number of ;; clicks. (define/public :report (lambda () click-count)) ;; Increment the tally when the button is clicked, then proceed to ;; perform the button's normal function. (super-new (label ctb-label) (parent ctb-parent) (callback (lambda (button event) (set! click-count (+ click-count 1)) (ctb-callback button event))))))
The click-tallying-button% class requires the three initialization
parameters mentioned above -- the button label, the enclosing container,
and a binary callback procedure. The first two of these are simply passed
through to the parent class, button%, when the super-new
expression is evaluated, but we tweak the callback procedure by inserting a
set!-expression that increments the click-count field every
time the button is clicked.
Now let's look at a program with a graphical interface that includes a click-tallying button. It sets up a window that initially looks like this:
After the user has played with the program for a while, it looks like this:
The callback procedure for the Click here button changes the
text of the MrEd-message-object that is displayed in the frame by sending
it a set-label message.
;;; click-tallier: display a button and keep track of how often it is ;;; clicked ;; Create the frame, CLICK-TALLIER-FRAME, for the window. (define click-tallier-frame (make-object frame% "Click tallier")) ;; Create the window that contains the button and, initially, an ;; explanation of what it does. (define click-tallier-message (make-object message% "I'll count how many times you click the button." click-tallier-frame)) ;; Create the button. (define click-here (make-object click-tallying-button% "Click here" click-tallier-frame (lambda (button event) (send click-tallier-message set-label (string-append "Number of button clicks: " (number->string (send button :report))))))) ;; Establish the minimum dimensions of the window. (send click-tallier-frame min-width 300) (send click-tallier-frame min-height 50) ;; Make it visible. (send click-tallier-frame show #t)
The MrEd toolbox contains several other classes for objects that invoke
callback procedures when activated. For instance, an object of the
slider% class is displayed as a slot with a handle protruding
from it:
With the mouse, the user can drag the handle to different positions in the slot. The slider's callback procedure is invoked whenever the handle is moved to a new position.
Here's the program that generated the slider shown in the graphic above:
;;; slider: display a typical slider in a separate window ;; Create the frame, SLIDER-EXAMPLE-FRAME, for the window. (define slider-example-frame (make-object frame% "Slider example")) ;; Create the window that contains the slider and reports its reading. (define slider-message (make-object message% "You're tuned to AM 1410" slider-example-frame)) ;; Create the slider. This one imitates a tuner for an AM radio. (define sample-slider (make-object slider% "AM frequency (kHz)" 540 1620 slider-example-frame (lambda (slider event) (send slider-message set-label (string-append "You're tuned to AM " (number->string (get-frequency slider))))) 1410)) ;; The GET-FREQUENCY procedure takes a reading from a given slider and ;; rounds it to the nearest multiple of 10 (since AM radio frequencies are ;; typically given as multiples of ten kilohertz). ;; Given: ;; SLIDER, a slider. ;; Result: ;; NUM, an exact integer. ;; Preconditions: ;; None. ;; Postconditions: ;; NUM is the exact multiple of 10 closest to the slider's current ;; reading. (define get-frequency (lambda (slider) (* 10 (inexact->exact (round (/ (send slider get-value) 10)))))) ;; Establish the minimum dimensions of the window. (send slider-example-frame min-width 500) (send slider-example-frame min-height 50) ;; Make it visible. (send slider-example-frame show #t)
The slider% class, and others, are described in the Windowing toolbox
section of the MrEd manual.