|
Allegro CL version 11.0 |
This 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 document 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 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.
An application should supply a redisplay-window method for any window that it needs to draw in, and place all of the code that draws in the window into that redisplay-window method. (While it is possible to draw in a window outside of a redisplay-window method, this is not advised.) Exceptions to this rule, as described further below, are the bitmap-pane and bitmap-stream classes.
The main 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 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
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 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-panes 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-streams 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.html.
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).
Common Graphics allows you to draw lines, polygons, various curved shapes, pixmaps (bitmaps), and text, and to fill various shapes. There are many functions that may be found in the Help | Tree of Knowledge dialog under Common Graphics --> Graphics; this section mentions some of the functions as an overview.
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*, *color-gradient-blend*, and *alpha-blending*.
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.html. 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.html.
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.html.
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.html.
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) Franz Inc. Lafayette, CA., USA. All rights reserved.
|
Allegro CL version 11.0 |