Drawing classes

Canvases and device contexts

One of the things that we can place inside a frame, as part of a graphical user interface, is a canvas -- a window on which our programs can draw things. The only initialization argument that a canvas needs is the frame that will contain it.

(define drawing-frame
  (make-object frame% "Drawing example"))

(define drawing-canvas
  (make-object canvas% drawing-frame))

Initially, let's set up a blank canvas, two hundred by two hundred pixels:

(send drawing-canvas min-width 200)
(send drawing-canvas min-height 200)

We can ensure that the size of the canvas will never be changed by setting its ``stretchability'' fields to #f:

(send drawing-canvas stretchable-width #f)
(send drawing-canvas stretchable-height #f)

Although the user can still try to resize the enclosing frame, this will not change the size of a non-stretchable canvas. If the user makes the frame larger, the canvas will occupy only part of it. If the user tries to make the frame smaller than that canvas, the attempt will fail.

At this point, we're ready to make the frame (and therefore the canvas that it contains) visible.

(send drawing-frame show #t)

One of the fields of a canvas contains its device context, an object that translates messages it receives into low-level operations on the particular output device where the canvas will be displayed. To create pictures on the canvas, the program sends such messages to its drawing canvas. The canvas% class provides a selector message, get-dc, by which the programmer can obtain the canvas's device context, and since we'll be sending messages to it a lot it will be convenient to give it a name:

(define drawing-context
  (send drawing-canvas get-dc))

For example, we might ask the device context to draw a rectangle twenty-five pixels wide and seventy-five pixels high, with its upper left-hand corner fifty pixels from the left edge of the canvas and ninety pixels from the top edge:

(send drawing-context draw-rectangle 50 90 25 75)

(screenshot: a thin black rectangle on a white background)

Colors

By default, lines are drawn in black on a white background, but we can switch to different colors if we like. Colors, not surprisingly, are objects of DrScheme's primitive class color%. Their initialization arguments can be of one of two forms: If we want a color that has a common name (that appears in DrScheme's predefined color database), we can use that name (as a string) as the initialization argument.

(define cerulean (make-object color% "sky blue"))

To create a customized color, however, we have to describe it in terms of the intensities of the red, green, and blue components of light of that color. Each of these intensities is represented by an exact integer in the range from 0 up to, but not including, 256. These intensities are the initialization arguments for customized colors. So, for example, a pure but not very bright red color might have a red intensity of 128 (about half of the maximum), a green intensity of 0, and a blue intensity of 0:

(define dull-red (make-object color% 128 0 0))

A subtler color would combine light of different components:

(define peru (make-object color% 205 133 63))

To change the canvas background to some particular color, we send the device context a set-background message with the color as an argument:

(send drawing-context set-background cerulean)

The drawing context does not immediately repaint the background when it receives this message, but subsequent operations that use the background color will use the new one. For example, the clear message directs the drawing context to repaint the entire canvas using the current background color, so if we now execute

(send drawing-context clear)

we get a sky-blue canvas.


Exercise 1

Create a vector named palette, of length 16, in which the element at position x is a color with red intensity 16x and green intensity 16(15 - x).


Exercise 2

Define and test a procedure that takes two arguments, a device context and a vector of colors. When invoked, the procedure should first determine and save the current background color stored in the device context (by sending it a get-background message). Then it should run through the colors in the vector, repainting with each color in turn. Finally, it should repaint with the original background color.

The graphics operations on MathLAN workstations are so fast that a test run of your procedure will probably appear as nothing more than a quick flicker. You can slow your procedure down by inserting, in the body of your kernel, a call to the DrScheme procedure sleep. This procedure takes a positive real number as argument and causes DrScheme to suspend its processing for the specified number of seconds.


Pens and brushes

To draw a foreground figure, such as a rectangle, in a color other than black, we change the drawing context's pen. Pens are objects. When you create a pen, you specify its color, the width (in pixels) of the line it draws, and its ``style,'' which is a symbol indicating the structure of the line it draws. Most pens use the 'solid style, which draws a continuous line. (On rare occasions, you might want to use another style, such as 'short-dash, which draws a dashed line.)

(define peru-drawing-pen
  (make-object pen% peru 4 'solid))

Let's ask the device context to use this pen instead of its original one, then redraw the rectangle:

(send drawing-context set-pen peru-drawing-pen)
(send drawing-context draw-rectangle 50 90 25 75)

(screenshot: a thick, peru-colored rectangle on a sky-blue background)

A device context accepts messages for drawing points, straight lines, circular and elliptical arcs, polygons, rectangles with rounded corners, and splines. (A spline is a smooth curve from one given point to another that passes through a third given point, the spline's control point.) You can read about all of these in the MrEd Graphical Toolbox Manual, starting at the page dealing with device contexts.


Exercise 3

Define a pen that draws solid lines, two pixels wide, in dark blue. Use it to draw a circle in the middle of drawing-canvas. (To draw a circle, send the draw-ellipse message, giving it the same value for the width and height of the ellipse.)


A pen just draws the outline of a figure. If you want to fill in the interior of an ellipse or a polygon, you need a brush. A brush has a color and a style, like a pen, but since it is used to fill in areas rather than to draw lines it has no width field. A device context always has a ``current brush'' as well as a ``current pen,'' but we haven't seen the effects of the brush yet because the default brush uses a special style, 'transparent, that has no effect on the appearance of the canvas.

Let's create a brush, tell the drawing context to use it, and draw an ellipse:

(define red-drawing-brush (make-object brush% dull-red 'solid))
(send drawing-context set-brush red-drawing-brush)
(send drawing-context draw-ellipse 115 20 70 50)

(screenshot: the same rectangle, now accompanied by a peru-colored ellipse with a dull red interior)

You can turn ``filling'' on and off either by sending the device context set-brush messages, enclosing a solid-style brush to turn filling on and a transparent-style one to turn it off, or by sending the current brush set-style messages, enclosing the symbol 'solid to turn the filling on and 'transparent to turn it off.

A transparent style is also provided for pens, so that the programmer can control whether the enclosing boundary of a figure will be drawn in the same kind of way.


Exercise 4

What happens if you draw an arc with a transparent-style pen and a solid-style brush?


Adding text

It is possible to write text onto a canvas -- for instance, to label some component of the graphic -- by sending a draw-text message to the canvas's device context, specifying the string to be written, the distance in pixels from the left edge of the canvas to the beginning of the text, and the distance in pixels from the top edge of the canvas to the top of the text.

(send drawing-context draw-text "Hello, world!" 8 182)

(screenshot: the rectangle and the ellipse, now accompanied by a string expressing a cheerful greeting)

By default, the text is drawn in black, but you can send a set-text-foreground message to the device context to change the color. It is also possible to vary the type font, by sending the set-font message with a font argument. Fonts, too, are objects, requiring four initialization arguments: size (an exact integer in the range from 1 up to, but not including, 256), family (one of a limited range of symbols, including 'decorative, 'roman, 'script, 'swiss, and 'modern), style ('normal or 'italic), and weight ('normal, 'bold, or 'light).

For instance, here's how to create a dull-red 24-point pseudo-Helvetica bold font, install it in the device context, and print a monogram in the upper left corner of the canvas:

(define monogram-font
  (make-object font% 24 'swiss 'normal 'bold))

(send drawing-context set-font monogram-font)
(send drawing-context set-text-foreground dull-red)
(send drawing-context draw-text "JDS" 8 8)

(screenshot: the same graphic, now with the monogram JDS at upper left)


Exercise 5

Write a DrScheme program that displays a graphic of an archery target: on a gray background, concentric rings of white (the outermost), black, blue, and red, with a gold disk in the middle. The outer diameters of the rings are, respectively, five, four, three, and two times the diameter of the gold disk. If you like, print the point value of each color at the top of the ring or disk (1 for white, 2 for black, 3 for blue, 4 for red, and 5 for gold).


This document is available on the World Wide Web as

http://www.cs.grinnell.edu/~stone/courses/scheme/drawing-classes.xhtml

created May 5, 2000
last revised May 7, 2000

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