Drawing classes

Course links

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 fifty by two hundred fifty pixels:

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

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 one hundred forty pixels from the top edge:

(send drawing-context draw-rectangle 50 140 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, one 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 device 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 device 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.

Pens and brushes

To draw a foreground figure, such as a rectangle, in a color other than black, we change the device 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 140 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.

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 device 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 135 30 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.

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 232)

(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)