Standard Scheme does not provide any facilities for graphics programming, but for particular implementations of Scheme it is often possible to obtain libraries that create windows, draw lines and curves onto them, add text in various fonts and colors, provide text-entry forms and labelled buttons and sliders and other ``widgets,'' and generally integrate Scheme programs with the multimedia resources of the machines on which they run.
As a small example of graphics programming in Scheme, we'll use Elk, an implementation of Scheme created by Oliver Laumann, to develop a plotting program. The program will open up a new window on your workstation's screen and draw the graph of a function into it.
The file /u2/stone/courses/scheme/html/plot-project.scm contains a working, but primitive, version of this plotting program. Your project will be to refine it, adding features of various sorts to the graphical display.
Begin by opening a dtterm window and using the shell's
cp command to make a copy of this program in your working
directory.
To run the program, type the command elk -i -l plot-project.scm at the prompt in the dtterm window (naming the file in which you stored your copy of the program). A square window appears, presenting a white background against on which the plot will subsequently be drawn. Pressing any key on the keyboard has some effect on the window: The Q key closes the window and exits from the program, and any other key causes the plot to be displayed. (Initially, the plot shows the function x3 - 8x - 4 in the range from -4 to +5.)
Run the program as described and exit from it as described (with the Q command).
Start XEmacs and look at the last few lines of the source code for the
program. Note that the function that is being graphed, f(x) =
x3 - 8x - 4, appears in the Scheme code as a
lambda-expression that names a procedure that computes that
function. Replace this lambda-expression with your favorite
function from real numbers to real numbers and re-run the program. If you
don't have a favorite, try one of the following:
Initially, too, the graph of the function shows only the values for
arguments in the range from -4 to +5. This too is determined
by arguments to the plotter procedure; if you like, you may
experiment with changing those arguments.
On MathLAN workstations, windows are normally created and displayed with the assistance of a ``window manager'' program, dtwm, by invoking procedures in a library known as ``X Windows.'' Although this library is intended primarily for programmers using the C and C++ languages, Laumann provided us with an interface that allows us to invoke X Windows procedures from inside Elk, using Scheme syntax, and thus to integrate X Windows programming with Scheme programming. A full list of the procedures that make up Elk's X Windows library can be found in the Elk/Xlib reference manual. Here, however, we shall cover only the twenty or so that are used in the plotting program.
One of X Windows data structures is the display, which contains
information about the output device on which any window we create will
appear. Open-display is a procedure of zero arguments that
allocates and returns an appropriately initialized display structure
describing the workstation's monitor; it returns #f if the
monitor is not suitable for displaying windows (which might happen if, for
instance, one tried to run an X Windows program from a VAX terminal).
Display? is a type predicate that tests whether its argument
is a display structure.
One of the components of a display is its color map, a vector
containing all of the various colors that appear simultaneously in the
display. Our workstations are capable of displaying 16777216 different
colors, but no more than 256 of them can be on screen at the same time, and
the elements of the color map are the ones that have been selected by the
various programs that are running as ones that they wish to use. The
display-colormap procedure is a selector that extracts the
color map from a given display.
In order to add a new color to the color map, making it eligible for use on
screen, one invokes a procedure of two arguments named
alloc-color. The first argument to alloc-color
is the color map to which the new color is to be added; the second is a
structure indicating the intensities of the red, green, and blue components
of the new color, as real numbers in the range from 0.0 to 1.0. The
make-color procedure is the constructor for color structures;
it takes three arguments -- the red, green, and blue intensities -- and
returns the color characterized by those intensities. Here's a typical use
of these procedures:
(alloc-color current-color-map (make-color 0.0 1.0 0.0))
This expression builds a bright green color and adds it to the current
color map. Alloc-color returns the index of the position
within the color map at which the color was installed. Often this index is
given a mnemonic name that suggests the color:
(define sky-blue (alloc-color current-color-map (make-color 0.4 0.6 1.0)))
Another element of the display is its root window, which is the
window that typically occupies the entire display and serves as a backdrop
against which other windows are placed. The
display-root-window procedure selects this component from the
display structure.
The internal representation of one specific window, such as the one in
which the plotting program draws its graphics, is also a structure. The
create-window procedure allocates and returns such a
structure; it takes a variable number of parameters -- always, however, an
even number -- which are alternately symbols naming the components that
need to be initialized and values to store at those positions in the
structure. Some typical components of a window structure are
parent (another window within which the new one is to be
placed), width (the number of pixels from the left edge of the
window to the right edge), height (the number of pixels from
the top edge to the bottom), and background-pixel (the color
map's index for the color to be used as the background within the new
window).
In order to perform any drawing operations within a window, we'll also need
a structure called a graphics context, which keeps track of
various quantities that control the drawing style -- the foreground color
in which new graphic features are drawn, the width of lines, the font in
which text is to be displayed, and so on. The root window of a display
already possesses such a graphics context, and one common way to get
started in a new window is to make a fresh copy of the root window's
graphics context and associate it with the new window. (This makes it
possible to change some of the elements of the graphics context without
affecting the appearance of any other window.) A procedure named
copy-gcontext constructs and returns a copy of a given
graphics context structure.
Elk provides mutator procedures for several elements of a graphics context, and the plotting program uses two of these:
The set-gcontext-line-width! procedure takes two arguments, a
graphics context and a positive integer n, and sets the
line-width element so that any lines subsequently drawn will
be n pixels wide. (In effect, this controls the width of the
pen or brush with which lines and arcs are drawn.)
The set-gcontext-foreground! procedure takes two arguments, a
graphics context and a color-map index representing a pixel color, and sets
the foreground element so that any lines or shapes
subsequently drawn will appear in the specified color.
The plotting program invokes both of these procedures before each drawing operation, to determine the line width and color used for that operation.
Find the place in the program at which colors are added to the color map
with alloc-color. Add an additional color of your choice,
giving it a name as shown above. Then find the place in the program at
which the color of the function's graph (red) is actually determined by a
call to set-gcontext-foreground!; put in the name of your
color instead. Save the program from XEmacs and re-run it in Elk Scheme;
confirm that the program now uses your color for the graph.
From time to time, the plotting program will pause to process and
respond to input from the user, in the form of keystrokes. The pause is
produced by a call to a procedure called handle-events, which
examines all the ``input events'' that have occurred since the last such
pause and invokes an appropriate ``event handler'' procedure for each one.
The event handler is expected to work out exactly what happened and where
(which key was pressed, for instance) and to act on it. Initially, the
responses of the plotting program are rather elementary: If the user types
the letter Q, the plotter closes the window and exits, and if the user
presses any other key, the plotter draws (or redraws) the plot of the
current function. The event handler must be in place before X Windows will
actually agree to draw the window onto the screen.
Finally, the dtwm window manager keeps structures containing
information about the windows it helps to put up, and there are procedures
for mutating various components of those structures, with such names as
set-wm-hints!, set-wm-normal-hints!,
set-wm-class!, set-window-event-mask!,
set-wm-name!, and set-wm-icon-name!. Perhaps the
most interesting of these is set-window-event-mask!, which
determines which of the various kinds of ``input events'' -- moving the
mouse, pressing and releasing mouse buttons, pressing and releasing
keyboard keys, and so on -- the window will pay attention to.
Once the structure for a window has been created, and its event handler has
been made ready, and the window manager has been made aware of the
existence and nature of the new window, one can ask for the window to be
drawn onto the display by invoking the map-window procedure,
which takes the window structure as its only argument.
X Windows includes several procedures for drawing shapes into a window. Here are some of the ones that you're most likely to use, with sample invocations:
The draw-line procedure takes six arguments: the window
within which the line is to be drawn, the graphics context that indicates
the style in which it is to be drawn, and the x- and y-coordinates of the
endpoints of the line segment. The coordinates are natural numbers
indicating the number of pixels from the left or top boundary of the window
to a designated point. (A pixel is one of the tiny colored dots
of which the image is composed. On our workstations, the screen is 1280
pixels wide and 1024 pixels from top to bottom. The window for the
plotting program measures 512 by 512 pixels.)
So, for example, the procedure call (draw-line my-window
current-gcontext 0 64 128 192) causes a line to be drawn in
my-window, with the graphics context
current-gcontext, starting at the left edge of the window 64
pixels from the top, and proceeding downwards and to the right to a point
that is 128 pixels from the left edge and 192 pixels from the top.
In the plotting program, the draw-line procedure is used to
draw in the axes.
The draw-lines procedure takes three arguments: the window,
the graphics context, and a vector of pairs, each pair having the
x-coordinate of a point as its car and the corresponding y-coordinate as
its cdr. Draw-lines connects each pair of adjacent points in
the vector by a straight line. For instance, (draw-lines my-window
current-gcontext '#((0 . 0) (0 . 100) (50 . 100) (50 . 0) (0 . 0)))
will draw the outline of a rectangle fifty pixels wide and one hundred
pixels high in the upper left-hand corner of the window. Note that
draw-lines needs a vector containing five pairs of coordinates
in order to draw four lines, since the lines connect points that are
adjacent elements of the vector.
This procedure is used to plot the curve of the function; the program
actually computes a lot of points on the graph and then uses
draw-lines to connect the dots.
The clear-area procedure, which is used to erase part or
all of a window (restoring the background color) takes six arguments: the
window, the x- and y-coordinates of the upper left-hand corner of the
region to be erased, the x- and y-coordinates of the lower right-hand
corner of that region, and a final Boolean argument which in this
application will always be #t. For example, the call
(clear-area my-window 0 0 100 200 #t) erases a
100-by-200-pixel rectangle in the upper left-hand corner of
my-window.
One page of the Elk/Xlib manual contains a list of the Xlib graphics functions; look it over to see whether there might be other useful library procedures. (Unfortunately, the abundant cross-references of the form ``See XDrawLine'' are intended for people who already know the X Window System well -- they are references to procedures in the original X Window System library -- so it's sometimes a little difficult to determine what the various procedures do.)
The credit line (``Drawn by PLOTTER'') appearing in the lower
left-hand corner of the window is drawn onto the window by the
text-drawing procedure draw-image-text. This
procedure takes six arguments -- the window, the graphics context, the x-
and y-coordinates of the upper-left-hand corner of the rectangle within
which the text is to be written, the text itself (as a vector of character
codes), and finally a symbol that indicates the amount of storage occupied
by each character in the character set (for instance, the symbol
1-byte would be used for ASCII characters, while for Unicode
characters you'd use the symbol 2-byte).
Elk Scheme also provides a procedure named translate-text,
which takes a string as argument and returns a vector containing the
character codes corresponding to the characters in that string --
basically, it applies char->integer to each character in
the string and accumulates the results in a vector. So a typical call to
draw-image-text would look like this:
(draw-image-text my-window current-gcontext 48 64
(translate-text "Hi, Mom!") '1-byte)
This causes the string "Hi, Mom!" (without the enclosing
quotation marks) to be drawn into my-window, with the
upper-left-hand pixel of the H placed forty-eight pixels from
the left edge of the window and sixty-four pixels down from the top edge.
Have plotter add another line of text in the upper left-hand
corner, giving the date on which the plot was made. Initially, you may
want to type the date string in by hand. However, Elk also provides
facilities for building a date-stamp automatically; if you add the Unix
``feature'' by placing (require 'unix) at the beginning of an
Elk Scheme program, the following procedure returns the current date and
time as a string whenever it is invoked:
(define datestamp
(lambda ()
(let* ((almost (unix-time->string (unix-decode-localtime (unix-time))))
(len (string-length almost)))
(substring almost 0 (- len 1)))))
When the program has finished its work, an appropriate sequence of X
Windows procedures must be invoked to free the various resources: The
window allocated by create-window must be deallocated with a
call to the destroy-window procedure, the color map returned
by display-colormap must be released with a call to
free-colormap, the graphics context constructed by
copy-gcontext must be freed by calling
free-gcontext, and finally the display obtained from
open-display must be released with a call to
close-display.
Read through the source code for the plotting program. (After this long introduction, I'm hoping that you'll be able to figure out most of the details.)
It would be helpful to have some labelled tick marks along the axes,
marking the coordinates at those points. Add them. The tick marks can be
drawn in with draw-line, once you figure out exactly
where they should go; the labels can be drawn in with
draw-image-text, once you figure out exactly what
they should say! (This is a difficult exercise.)
This document is available on the World Wide Web as
http://www.math.grin.edu/courses/Scheme/fall-1997/plot-project.html
created November 19, 1997
last revised December 9, 1997