| Allegro CL version 8.2 Moderately revised from 8.1. 8.1 version |
This document contains the following sections:
1.0 What to draw onThis document is an overview of Common Graphics functionality for drawing arbitrary graphical output in an application, including lines and curves, filled areas, pixmaps (bitmaps), and text. (Despite the name, "Common Graphics" also includes windowing and event-handling functionality and other facilities needed for a complete user interface.) This page points to a variety of individual graphical functions that you can link to for further information.
An application can draw in any window, as well as on certain special objects such as the screen itself, printers, and memory bitmaps that may be used to temporarily hold output to be copied to a visible window later. A given drawing function such as draw-line will work essentially the same no matter which type of object it is drawn on.
When a window is the drawing destination, a redisplay-window method generally should be used to encapsulate the drawing code. And certain types of windows have other particular constraints, as explained in this and the following sections.
The following techniques can be used on (almost) all windows.
After calling make-window to create a window in which to draw, an application could immediately start calling drawing functions such as draw-line on the window. But instead, an application normally should write a redisplay-window method for the window, and place all of the code that draws in the window into the redisplay-window method.
The reason for always using a redisplay-window method is that whenever part of the interior of a window becomes uncovered, its redisplay-window method will be called automatically to redraw the window's contents that was erased from the screen when the window became covered. If drawing is done outside a redisplay-window method, then in the general case an application will not know when the output that was drawn has been lost.
To define a custom redisplay-window method, you should
(as usual) first define your own window subclass and add the method to
that, to avoid adding methods to built-in classes. The following
example shows the basic technique for defining a custom window class
and what gets drawn in the windows that are instantiated from it. A
frame-window
is the simplest type of window to draw on, and is subclassed here.
(defclass my-window (frame-window)()) (defmethod redisplay-window ((window my-window) &optional box) (declare (ignore box)) (call-next-method) (draw-line window (make-position 0 0)(make-position 1000 1000))) (make-window :one :class 'my-window)
When the call to make-window is done, the window is created and is "uncovered" for the first time, and so Common Graphics calls the redisplay-window method, which simply draws a line in the window. This method will be called in the future whenever the window's interior is partly uncovered.
The method above calls (call-next-method)
. This
calls the default redisplay-window method, defined on
basic-pane
,
which simply erases the entire background of the window
interior. Without this call, whatever had been on the screen before
the window was uncovered would remain in the window interior, with our
line being drawn directly on top of this screen garbage. So a redisplay-window method
is responsible for drawing the entire window contents.
To achieve the same effect as calling
(call-next-method)
, a redisplay-window method could
instead pass the box argument to erase-contents-box to draw the
uncovered part of the window interior in the window's current background-color.
The optional box argument to redisplay-window specifies a rectangular portion of the window interior that includes all of the parts that were uncovered. This argument is ignored above, but may be used for efficiency. See redisplay-window for details.
An application should never call redisplay-window itself, allowing Common Graphics to call it when needed instead. If you know that a window needs to be redrawn for some reason that Common Graphics can't know about (such as if its contents should change), then you should call the function invalidate on the window. This tells Common Graphics that the window interior (or some part of it) is out of date, and Common Graphics will call redisplay-window itself.
The reason for this design is that when an application and/or Common Graphics notices that a window needs to be redrawn, this may be noticed multiple times or in multiple places in the code. If redisplay-window were called in each case, the window would be redrawn multiple times when it only needs to be redrawn once. This not only slows down the application, but can cause annoying flashing effects. The general solution to this problem in Common Graphics (in the operating system design actually) is to not call redisplay-window immediately when invalidate is called, but instead to wait until the window's thread is done responding to events, and then to call redisplay-window the minimum necessary number of times to account for all of the queued invalidations.
When using the above techniques involving redisplay-window and invalidate, it may happen that a window does not redraw itself as soon as desired, because the gesture that uncovered the window also executed code that consumes considerable time, and redisplay-window is called only afterwards. If this is a problem, an application can call update-window on the window at the time that it should be redrawn. This causes redisplay-window to be called immediately rather than after the currently running code has completed.
If you were to substitute non-refreshing-window
for frame-window
in the
initial example in Section 1.1 Drawing on windows in general, you
would not see a line drawn in the window. The reason is that this
window class is a subclass of frame-with-single-child
, and so
creating an instance with make-window automatically creates a
second window in the interior of the new window to serve as a main
pane. Anything drawn on the non-refreshing-window
returned by
make-window will
not be seen because it is covered entirely by the pane. Instead you
must call frame-child to find the pane, and
draw on the pane.
The reason for this design is not that you cannot draw on a frame
window, since you can draw on any window. Instantiating a non-refreshing-window
is mainly useful if you want to add a toolbar or status-bar to the
window. If you add a toolbar to a frame-window
and then draw on the
frame-window
,
you would need to know how tall the toolbar is in order to draw below
it. And scrolling the window (if it has scrollbars) would scroll the
toolbar right off the window. A frame-with-single-child
window, on
the other hand, would place its toolbar alongside the frame-child pane,
allowing you to draw on the pane right at the top as usual, and the
pane window will have the scrollbars rather than the frame window so
that scrolling leaves the toolbar in place.
To update the first example to work with a frame-with-single-child
class, you
would need to subclass the window class of the pane, since that is
where you need to define a redisplay-window method. This means
that you also need to add a default-pane-class method to tell
the frame window what class to use for its pane window, and so you
must also subclass the frame window class in order to add this method.
Here's the example as updated to work with a custom non-refreshing-window
subclass.
(defclass my-frame (non-refreshing-window)()) (defclass my-pane (non-refreshing-pane)()) (defmethod default-pane-class ((window my-frame)) 'my-pane) (defmethod redisplay-window ((window my-pane) &optional box) (declare (ignore box)) (call-next-method) (draw-line window (make-position 0 0)(make-position 1000 1000))) (let* ((window (make-window :one :class 'my-frame :scrollbars t))) (add-toolbar window) (add-common-status-bar window) window)
The functions add-toolbar and add-common-status-bar are called here to illustrate how the drawing still begins at the upper-left corner of the drawing area (the pane window) rather than being obscured by the (empty) toolbar.
Note also how scrolling the window downward draws more of the line that had extended off the bottom of the window. Scrolling is another case of part of the window being uncovered, and the redisplay-window method is called automatically to redraw the window at its new scroll position. Common Graphics will automatically shift the part of the drawing that was already visible and which will still be visible after the scroll, and then call redisplay-window with a box argument that covers only the newly-scrolled-on part of the drawing. The scrolling of the line will look smooth even though we draw the whole line, because the clipping-box of the window is set to the box argument during the call to redisplay-window, so that the shifted part of the line is not erased and then redrawn.
An application does not need to concern itself with the current scroll-position of a window. Instead the app draws on the logical canvas that is scrolled across the window, and Common Graphics maps what is drawn to the visible window based on its current scroll-position.
The Navigator dialog has a less trivial
example of drawing on a frame-with-single-child
window,
called "Custom Windows: subclassing and event-handling".
A bitmap-window
is another frame-with-single-child
window
class, and instantiates a bitmap-pane
as the frame-child. A bitmap-pane
is a
special window that uses a backing-store memory bitmap to remember
what was drawn in it when it becomes covered. It has a built-in
redisplay-window
method that simply copies the memory bitmap to the visible window when
it is uncovered. Therefore, when using a bitmap-pane
you should not add a
redisplay-window
method. But you do need to draw directly on the window to produce the
drawing that the window will remember from then on. So a bitmap-pane
is an
exception to the rule of calling invalidate rather than
drawing directly on the window.
A bitmap-pane
is useful when redrawing the window contents from scratch in a redisplay-window method
each time the window is uncovered is not fast enough, since doing a
single copy from the backing-store pixel array is usually faster than
this. The drawback is that a lot of memory is used for the memory
bitmap, and so you generally don't want to use bitmap-pane
s except when needed.
If the contents of a bitmap-pane
need to change, you can
call clear-page
to draw the window's background in the window's current background-color, and
then begin a new drawing. clear-page calls erase-contents-box on the page-box of the window to
erase the entire scrollable canvas (or page) of the window. Erasing
only the visible-box of a bitmap-pane
would not be sufficient
if the window is scrollable, because a bitmap-pane
also
remembers the contents that are currently scrolled out of view.
The drawable
control is a widget that is provided simply to display an arbitrary
drawing on a widget that exists among other widgets. You could instead
add a regular child window to a dialog and draw on that, but the
drawable
control may be useful when you want to treat the drawing window as a
widget, such as by dragging it around on a form being laid out
interactively, or allowing the user to tab to it.
You draw on a drawable
control just as you would
on a regular window, except that you place the drawing code into an
on-redisplay
function rather than into a redisplay-window method. A drawable
control may
alternately have a backing store memory bitmap similar to that of a
bitmap-pane
,
if you turn on its use-bitmap-stream property. The Navigator dialog has
a complete example of using a drawable
control.
Normally an application would not draw on the window of an arbitrary
widget (an instance of the class dialog-item
), because each widget
is designed to be drawn in a standard way that it knows about and
handles itself. But since it is possible to draw on any window, you
can draw on a widget if you really want to. The one constraint (if the
window is of the class widget-window
rather than lisp-widget-window
) is
that the calls to drawing functions must be wrapped in a call to
with-device-context, since Common
Graphics does not give a permanent device context to widgets supplied
by the operating system. Such custom drawing would probably be done in
a redisplay-window :after method to
add some sort of annotation to what the main redisplay-window for the control
normally draws.
Though text-edit-pane
appears in the
Common Graphics class hierarchy as a regular window, it is internally
an operating system control, and therefore knows how to draw itself
and normally is not drawn on by an application. It still can be drawn
on as with any OS control, though, and in fact the IDE editor draws
parenthesis matching marks in text-edit-panes.
The screen itself may be drawn on, though this is generally dangerous
because it draws over whatever windows happen to lie at the specified
screen positions (even if they are in other applications), and these
windows will not know that they have been obscured and so they will
not refresh themselves. Generally, an application would need to be
careful that it draws only over its own windows while they are in
front and that it refreshes these windows at some point by calling
invalidate on each of them. (Drawing on the screen will even draw over
window frames, which is not otherwise possible in Common
Graphics. Such drawing could be removed later by passing the :frame
argument to invalidate.) The screen is
returned by (screen *system*)
(see screen and *system*
).
One way to draw on the screen that is relatively safe is to use the
po-xor
(or
po-invert
)
paint-operation. If something is drawn twice this way, the second time
will erase what was drawn the first time, but not if someone (such as
another application) draws something in the same place between the two
calls. So caution is still warranted. The Common Graphics functions
such as get-line
and get-screen-box use this technique
to temporarily draw a rubberband line or box anywhere on the
screen. The "Grabbing pixmaps from the screen" example in the Navigator dialog
calls get-screen-box to allow you to capture a pixmap from anywhere on
the screen.
A bitmap-stream
is a memory bitmap
that is never visible but on which you can draw just as you would on a
window. (A pixmap, on the other hand cannot be drawn on.) bitmap-stream
s may be
useful for storing drawings that are to be very quickly copied at
various times to visible windows by calling copy-stream-area. A bitmap-pane
uses an
internal memory bitmap that is similar to a bitmap-stream, and the
bitmap-stream
class is provided for similar uses that don't quite fit the typical
bitmap-pane model. Call open-stream rather than make-window
to create a bitmap-stream
. The Navigator dialog has
a complete example called "Bitmap-streams: custom backing store".
You can draw on a printer stream just as you would draw on a window.
There are a few special considerations: You must call new-page to advance to a
new page; the paper is not ejected until you close the stream; the
pixel resolution may be very different than the screen and vary widely
between printers (see stream-units-per-inch and scaling-stream
), and
getting the right margins is a bit tricky due to the coordinates being
relative to a "hardware margin" (see *default-printer-left-margin*
).
And you call open-stream rather than make-window to open a
printer stream. There are a couple of printer examples in the Navigator dialog
that address these issues. See also with-output-to-printer.
Drawing functions accept arguments that use a coordinate system called "stream coordinates". By default, the position 0,0 in stream coordinates is at the top left of the entire "page" (or canvas) that is scrollable in a window (when the destination is a window), and the unit of distance is pixels. So drawing a line from 0,3 to 100,3 will draw a horizontal line that is 100 pixels long and which has one end near the top-left corner of the window interior whenever the window is scrolled to its top-left position. Common Graphics handles the scrolling for you, so that you can effectively draw directly on the scrollable canvas without worrying about the current scroll-position.
The 0,0 origin may be moved by calling the setf of stream-origin, but this is not
typically useful. The unit of distance may be changed by calling the
setf of stream-units-per-inch if the stream
is a scaling-stream
; this is mainly
useful to make the resolution of a printer stream match that of the
screen so that the same coordinates may be used to draw on either a
window or a printer. For more information about coordinate systems,
see cg-coordinates.htm.
Coordinates are usually passed to drawing functions by first grouping them into position and box objects. These are created by calling make-position and make-box, or allocated temporarily by using with-positions, with-boxes, and with-positions-and-boxes. (The "with-" macros may be used to reduce the consing of many temporary position and box objects). There are also many functions for combining position and box objects in various ways, such as position+, box-move, and inside-box-p.
A window's coordinate system applies only to its interior; there is no way to draw on the frame of a window except by drawing directly on the screen (see above).
To draw straight or curved lines, call such functions as draw-line, draw-to-x-y, draw-polygon, draw-circle, draw-ellipse-arc, and draw-bezier-curve (the latter for Bezier curves). To fill whole areas denoted by such lines rather than drawing the line only, call functions such as fill-box, fill-polygon, and fill-ellipse-sector. A more versatile type of fill is done by the function flood-fill.
Lines and areas may be "erased" by calling such functions as erase-line, erase-contents-polygon, and erase-ellipse-sector. But keep in mind that these functions are actually just drawing or filling the line or area using the current background-color of the window rather than the current foreground-color as the drawing and filling functions do. Common Graphics does not remember "objects" that were drawn for each of the drawing and filling functions in order to selectively remove them from a drawing. So it usually makes sense to use these functions only if no other part of the drawing overlaps what is being "erased". Erasing is more commonly done to whole rectangular areas by calling such functions as clear-page and erase-contents-box.
A function such as draw-line takes arguments only for the stream to draw on and the endpoints to draw between. To control other aspects of the line such as its width and color, an application calls other functions and macros beforehand to establish the current drawing style context that is used by all of the actual drawing functions.
In most cases there is a function for changing some aspect of the current drawing style permanently (or until it is changed again), and an associated macro for changing it only temporarily, guaranteeing that the style will be returned to whatever it was previously when the body of the macro call is exited. For line width, for example, the function (setf line-width) changes the current line width indefinitely, while the macro with-line-width changes it only during the body of the macro call. The function line-width returns the line width currently in effect.
Other functions and macros for establishing the current drawing style include:
Several style parameters may be combined into a single "graphic context" object (see copy-graphics-context), though the functions for individual parameters are probably handier in most cases.
On the Windows platform, an enhanced drawing style can be achieved by
binding the variables *antialiasing*
, *color-gradient-filling*
,
*color-gradient-intensity*
,
*color-gradient-direction*
, and
*color-gradient-blend*
.
Each drawing style parameter affects all of the drawing functions where it makes sense. The current foreground-color of a window, for example, is used for drawing lines and filling shapes as well as for drawing text (covered below), but not for drawing pixmaps, which define their own colors. The foreground-color of a window is usually an RGB color object, created with make-rgb, but may also be nil to mean use a default color. It may also be an integer to index into a palette; palettes are probably no longer necessary now that everyone seems to run in true-color mode, but for more information see cg-color-palettes.htm. The value cannot be an HLS color. You can create HLS colors (see make-hls) but must run them through hls-to-rgb and use the resulting RGB color.
Text is sometimes not thought of as graphical output, since so-called "non-graphical" computer terminals can display it. But in fact drawing a string in Common Graphics is drawing a graphic just as drawing a polygon is, and text may be mixed freely with other graphical output.
The main Common Graphics function for drawing a string is draw-string-in-box. But since every object on which you can draw (a window, screen, bitmap-stream, or printer) is also a Common Lisp stream, any Common Lisp stream output function such as format or princ may also be used to draw in a Common Graphics window or other stream. Common Graphics streams are set up as "interactive" streams, and so it should not be necessary to call force-output or finish-output; the strings should instead always appear as soon as they are drawn.
The Common Lisp stream output functions always start drawing at the "current position" of a window, as set by move-to or the setf of current-position. They also set the current position of the stream afterwards where the text output ended; therefore, drawing multiple strings in a row will concatenate them end to end, similar to a text terminal (though with no forced line wrapping). When a newline is printed to a Common Graphics stream, the current position is moved to the left-margin of the stream and downward by the line-height of the stream.
The current position is also used by certain other functions such as draw-to, though in those cases it is probably more straightforward to use corresponding functions such as draw-line that do not depend on the current position.
Text is always drawn in a Common Graphics stream using the stream's current font. A Common Graphics stream always has a current font, which is returned by calling font. A stream's default font could be used, but typically an application will want to specify its own fonts, often mixing a number of fonts in a single window.
A font is created by calling make-font-ex. (If an existing equivalent font is found, make-font-ex returns that font rather than creating a new font object, so make-font-ex may be called frequently without concern of excessive consing.) font-faces returns a list of the faces that may be passed to make-font-ex. font-sizes returns the available sizes for a particular face, though a vector font (such as a TrueType font) may be any size, unlike a raster font. ask-user-for-font also returns a font object, as chosen interactively by the user. Functions such as ansi-var-font and ansi-fixed-font return suggested default fonts.
Once a font object is in hand, it may be assigned to a window by calling either with-font or the setf of font. Text that is drawn with either draw-string-in-box or the Common Lisp stream output functions will then appear in this font. Functions such as stream-string-width and line-height may be called before a string is actually drawn to determine how much space the string would use up.
A number of functions return font attributes that may be helpful in deciding what font to use or how much space a font will require. Some of the functions, such as font-family, font-face, font-size, and font-style, may be called on a font object directly. To use other functions, such as font-ascent, font-leading, font-fixed-width-p, and font-vector-p, you must first assign the font to a stream, then retrieve a fontmetrics object from the stream (by calling either fontmetrics or nfontmetrics), and then call the function on the fontmetrics object.
A pixmap, also known as a bitmap or raster image, is a rectangular array of pixels that is copied as a block. Common Graphics includes pixmap objects to encapsulate such images. A pixmap may be loaded from a .bmp file by calling load-pixmap, or created on the fly with make-instance. A pixmap is drawn on a stream by calling copy-to-stream. Pixmaps are described in detail in cg-pixmaps.htm.
A rectangular area of one stream may be copied to another stream by calling copy-stream-area, without using pixmap objects.
An icon is a pixmap-like object that may be used in special places such as window title-bars and the Windows Taskbar. They are covered in cg-icons.htm.
Common Graphics does not offer much direct support for animation, but there are a few facilities that may be of help.
There is a Navigator example showing how to do
limited animation in a drawable
control; this code is also
shown on the page for the drawable class. This example uses the
optional backing store of the drawable to quickly update the image to
each next frame. A timer is used to update the image at regular
intervals.
Similar techniques could be used with a regular window, such as
drawing the next frame of an animation on an unseen bitmap-stream
, then
using copy-to-stream to copy that next image to a visible window
whenever a timer fires. This usage of backing store avoids flashing
that would result if the next frame were drawn directly on the visible
window. This type of backing store can be done automatically by using
the double-buffered
window property or the with-double-buffering macro. Timers are
detailed in cg-timers.htm.
A bitmap-pane
has this sort of behavior built in when the with-delayed-redraw macro is used
with it. If the code to draw the next frame of an animation on a
bitmap-pane
is
placed within a call to with-delayed-redraw, then the
output will be sent to the bitmap-pane
's backing-store memory
bitmap but not to the visible window itself. Then when the bitmap-pane
is next
redisplayed (either by passing :invalidate t
to
with-delayed-redraw or by calling
invalidate
explicitly later), then the backing store will be copied quickly to
the visible window to reveal the next image.
Here is an example of a simple animation to inform the user that something is still happening during a busy loop. It uses the double-buffered property to illustrate smooth (non-flashy) updating of a drawing.
It also illustrates how to draw frames at a particular speed. Here 30 frames a second are drawn, though you would probably want to use fewer in a real application if it were only to inform the user of progress, as in this example. Otherwise it would likely slow down the real task significantly, and the real task's main loop would probably not come around that often to check whether it's time for another frame.
;; TYPO NOTE: in earlier versions of this document, ;; 'notification-window' was consistently misspelled 'notifcation-window'. ;; Because the misspelling was consistent, the code worked. We have ;; corrected the misspelling but users who grabbed the old code ;; may find inconsistent behavior. (defclass notification-window (dialog) ;; Define this class for a window to be shown while ;; the application is in a busy loop doing some work. ;; Give it a couple of slots to hold information about ;; the animation in the window. ((task-count :initform 0 :accessor task-count) (animation-count :initform 0 :accessor animation-count) (animation-pixmap :initform nil :accessor animation-pixmap))) (defmethod redisplay-window ((window notification-window) &optional clipping-box) (declare (ignore clipping-box)) ;; This will be called each time we invalidate the window ;; to make it update its displayed information. ;; The default redisplay-window method will erase whatever was ;; drawn on the window already (which in this case is our ;; drawing at the previous animation step). (call-next-method) ;; Determine where to draw the picture at this animation step. ;; The dx value will make Melvin move back and forth. (let* ((count (incf (animation-count window))) ;; Using 40 steps to make Melvin move back and forth one ;; time and drawing 30 frames a second will cause Melvin's ;; movement cycle every one and third seconds. (steps-each-way 20) (total-steps (* 2 steps-each-way)) (dx (abs (- (mod (+ count steps-each-way) total-steps) steps-each-way))) ;; Find the pixmap just one time lazily. (pixmap (or (animation-pixmap window) (setf (animation-pixmap window) (find-pixmap :melvin)))) (margin 12) (double-margin (* 2 margin))) ;; These scratch positions and boxes are for efficiency, ;; to avoid consing these objects every time and creating ;; garbage to be collected. (with-positions-and-boxes (pos1 pos2)(box1) (nmake-position pos1 (+ double-margin dx) double-margin) ;; Draw the pixmap at the current dx offset. (copy-to-stream pixmap window pos1) ;; Draw an informative string. (with-font (window (make-font-ex nil "Arial" 20)) (move-to-x-y window double-margin (+ double-margin (height pixmap) margin)) (format window "Now at loop ~a. Please wait ..." (task-count window))) ;; Draw a box around both. ;; These three picture elements will all appear ;; at the same time because the double-buffered ;; property is enabled for the window. (nmake-box box1 margin margin (+ (position-x (ncurrent-position window pos2)) margin) (+ (position-y (ncurrent-position window pos2)) (line-height window) margin)) (with-line-width (window (floor margin 4)) (draw-box window box1)) )))) ;;; Evaluate this form to run the animation, and close ;;; the window to stop it. (let* ((window (make-window :working-message :class 'notification-window ;; This tells the window to use a backing store ;; memory pixmap to avoid flashing whenever it is ;; redrawn. See double-buffered . :double-buffered t :title "Close This Window to Cancel" :scrollbars nil :centered t :width 500 :height 160)) (thirtieth-second (round internal-time-units-per-second 30)) (next-time (+ (get-internal-real-time) thirtieth-second)) (task-count 0) now-time) (select-window window) (loop ;; The application's real work would be done right here. ;; Our example simply increments a number. (incf task-count) ;; Periodically check whether it's time to do ;; the next animation step. ;; If your real task is in a busy loop, then you'll ;; need to call process-pending-events to allow ;; asynchronous window messages to come in and be ;; handled. Alternately the animation could be ;; done in a separate process than the real task, ;; though you may want to set the mp:process-quantum ;; of the real task's process to a shorter time to ;; allow the animation to run more often. When using ;; a separate process, a timer could be used to regulate ;; the speed, rather than checking the current real ;; time in a loop as we do here. (process-pending-events) ;; For this demonstration, we'll exit our busy loop ;; when the user has closed the window. In a real app, ;; it might happen whenver the real task is done. (unless (windowp window) (return)) ;; This will update the animation if a thirtieth of a ;; second has passed since the previous animation step. (when (>= (setq now-time (get-internal-real-time)) next-time) (incf next-time thirtieth-second) (setf (task-count window) task-count) (invalidate-window window) ;; This cause the window to be redrawn now, rather ;; then when the next window messages are handled ;; after this loop returns. (update-window window))))
Copyright (c) 1998-2016, Franz Inc. Oakland, CA., USA. All rights reserved.
This page has had moderate revisions compared to the 8.1 page.
Created 2016.6.21.
| Allegro CL version 8.2 Moderately revised from 8.1. 8.1 version |