Windowing classes

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.

A minimal example

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:

(screenshot: a Motif window with the title Greetings!, containing the text Hello, world!)

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.


Exercise 1

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.


Input

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.

Derived classes

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:

(screenshot: a window featuring an introductory text and a Click here button)

After the user has played with the program for a while, it looks like this:

(screenshot: the same window, with the text changed to Number of clicks: 42)

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)

Exercise 2

Revise the click-tallier program so that its user interface includes a Dismiss button as well as the Click here button.


Exercise 3

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.


Exercise 4

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:

(screenshot: a Motif slider)

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

John David Stone (stone@cs.grinnell.edu)