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 familiar way.
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.
MrEd-message-objects need 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 150) (send greeting-frame min-height 50)
This ensures that the window in which greeting-message is
displayed is always at least 150 pixels wide and fifty pixels high.
(As this example shows, the creators of this class do not follow the useful convention that the name of a message should begin with a colon, nor do they use exclamation points in the names of state-changing messages.)
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:
(define greeting-frame (make-object frame% "Greetings!")) (define greeting-message (make-object message% "Hello, world!" greeting-frame)) (send greeting-frame min-width 150) (send greeting-frame min-height 50) (send greeting-frame show #t)
Save them in a file -- say, hello.ss -- and you can run the program by loading the file.
Create your own variant of the greeting program, replacing the frame title and the MrEd-message-object label with strings of your own choice. Run it. Use the frame buttons to maximize the window (so that it fills the screen), and then to restore it to normal size. Resize the window by tugging at a side or corner of the frame.
Suppose, now, that we want to add to this user interface a Dismiss
button, which the user can click on when she's 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:
(define click-tallying-button%
(class button% (label parent callback)
(private
(click-count 0))
(public
(:report (lambda () click-count)))
(sequence
(super-init label
parent
(lambda (button event)
(set! click-count (+ click-count 1))
(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-init procedure is invoked, 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.
(define click-tallier-frame
(make-object frame% "Click tallier"))
(define click-tallier-message
(make-object message%
"I'll count how many times you click the button."
click-tallier-frame))
(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)))))))
(send click-tallier-frame min-width 300)
(send click-tallier-frame min-height 50)
(send click-tallier-frame show #t)
Revise the click-tallier program so that its user interface
includes a Dismiss button as well as the Click here button.
An alternative approach to the click-tallier procedure would
be to create an object of the tally% class defined in the lab on classes and objects in
DrScheme, and to use a normal, non-tallying Click here button
in which the callback procedure first sends a :bump! message
to the tally and then changes the text of
click-tallier-message to reflect the new state of the tally.
Revise the click-tallier program so that it uses this
approach.
One advantage of the approach described in the previous exercise is that the same tally can receive messages from various sources. Revise the program again so that its user interface includes a Reset button that resets the tally to zero.
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:
(define slider-example-frame
(make-object frame% "Slider example"))
(define slider-message
(make-object message%
"You're tuned to AM 1410"
slider-example-frame))
(define get-frequency
(lambda (slider)
(* 10 (round (/ (send slider get-value) 10)))))
(define sample-slider
(make-object slider%
"AM frequency (kHz)"
640
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))
(send slider-example-frame min-width 500)
(send slider-example-frame min-height 50)
(send slider-example-frame show #t)
The slider% class, and others, are described in the ``Windowing
Toolbox'' section of the MrEd manual. (A local on-line
copy of this manual is also available, but the link works only on
MathLAN.)
This document is available on the World Wide Web as
http://www.cs.grinnell.edu/~stone/courses/windowing-classes.xhtml
created May 3, 2000
last revised May 4, 2000