A common feature needed by an application is to have a number of objects displayed in a window and to make events affect the object underneath the cursor. The CAPI provides the ability to create graphical objects, to place them into a window at a specified size and position, and to display them as necessary. Also a function is provided to determine which object is under any given point so that events can be dispatched correctly.
These graphical objects are called pinboard objects, as they can only be displayed if they are contained within a pinboard-layout.
Like simple panes, you display a pinboard-object by putting it in the description of a layout, but in the case of a pinboard-object the layout must be either a pinboard-layout or a layout that is a descendant of a pinboard-layout (to any depth). Adding or removing pinboard-objects can be done using the standard mechanism of the :description
initarg and (setf layout-description)
, but normally it should be done by manipulate-pinboard. This is much more efficient and causes much less flickering, which is important when there are many objects.
CAPI provides built-in pinboard object classes for several simple cases including item-pinboard-object for displaying text, line-pinboard-object, rectangle, ellipse and arrow-pinboard-object for simple shapes, and image-pinboard-object for displaying an image. To display more complex drawing, you can use drawn-pinboard-object, which takes a display-callback which actually does the drawing. For greater control, you can subclass pinboard-object, and define the method draw-pinboard-object to do the drawing, and if needed also draw-pinboard-object-highlighted. You can also subclass any of the specialized pinboard-object subclasses if it is useful.
pinboard-objects have geometry like simple-pane, that is x, y, width and height. These can be specified initially by the initargs :x
and :y
and geometry hints (see 6.4 Specifying geometry hints), and can be read and set later by static-layout-child-position and static-layout-child-size. They can also be read by using the binding inside with-geometry, but setting should be done only by (setf static-layout-child-position)
and (setf static-layout-child-size)
.
For line-pinboard-object and its subclasses, you would normally specify the start and end points, rather than the rectangle that encloses it (which would require computations taking into account the line width and the position of any label). This is done when making the object using the initargs :start-x
, :start-y
, :end-x
and :end-y
, and later by the function move-line. The function line-pinboard-object-coordinates can be used to find the start and end points of an object.
The graphics args that are used to draw the objects in built-in subclasses of pinboard-object can be specified by supplying the initarg :graphics-args
, and modified dynamically by (setf pinboard-object-graphics-args)
and (setf pinboard-object-graphics-arg)
. For example, the following code displays a line and after 2 seconds changes its color:
(progn (setq po (capi:contain (make-instance 'capi:line-pinboard-object :start-x 50 :end-x 250 :start-y 50 :end-y 50 :graphics-args '(:thickness 10 :foreground :red)))) (sleep 2) (capi:apply-in-pane-process po #'(lambda () (setf (capi:pinboard-object-graphics-arg po :foreground) :blue))))
For pinboard object classes which you define, the drawing functions that you call need to do the drawing using the Graphics Ports drawing functions (see 13.4 Drawing functions). They take their coordinates with respect to the pinboard-layout (not the object), so you need to use the x and y to compute the arguments for the drawing functions. This is how the specialized classes mentioned above know where to draw. You need to keep the drawing inside the geometry (that is inside the rectangle defined by x, y, width and height), because the pinboard-layout decides which objects need redrawing using these values.
pinboard-objects can be highlighted. You need to use the functions highlight-pinboard-object and unhighlight-pinboard-object to switch the highlight state of objects. The function pinboard-object-highlighted-p can be used to check whether an object is in the highlighted state. By default, CAPI calls draw-pinboard-object-highlighted to add the highlight after drawing the object. In many cases, it is better to do the highlight in the drawing function (either the method of draw-pinboard-object or the display-callback for drawn-pinboard-object) rather than separately. Use the initarg :no-highlight
with value t
when making the pinboard-object, and pinboard-object-highlighted-p inside the drawing function to check whether it needs to highlight. These examples both use this technique:
(example-edit-file "capi/graphics/circled-graph-nodes")
(example-edit-file "capi/graphics/tracking-pinboard-layout")
It is possible to set an element such that its geometry changes automatically when the pinboard-layout is resized, by using either the initarg :automatic-resize
or calling set-object-automatic-resize. See:
(example-edit-file "capi/layouts/automatic-resize")
Note: pinboard-objects are implemented as graphics on a native window. Compare this with simple-pane and its subclasses, where each instance is itself a native window. A consequence of this is that simple-panes do not work well within a pinboard-layout, since they always appear above the pinboard-objects. For example, to put labels on a pinboard, use item-pinboard-object rather than display-pane or title-pane.
Note: The pinboard-layout displays the pinboard objects via its own display-callback function pinboard-layout-display. If you want do other drawing too, see the entry for pinboard-layout-display. It is also possible to draw the pinboard objects of a pinboard-layout to another graphics port (for example, a pixmap) using draw-pinboard-layout-objects.
Here is an example of the built-in pinboard object class item-pinboard-object which displays its text like a title-pane. Note that the function contain always creates a pinboard-layout as part of the wrapper for the object to be contained, and so it is possible to test the display of pinboard-objects in just the same way as you can test other classes of CAPI object.
(contain ;; CONTAIN makes a pinboard-layout if needed, so we don't ;; need one explicitly in this example. ;; You will need an explicit pinboard-layout if you define ;; your own interface class. (make-instance 'item-pinboard-object :text "Hello world"))
A pinboard object
Here is another example illustrating item-pinboard-object:
(example-edit-file "capi/graphics/pinboard-object-text-pane")
Where the display of an output-pane is complex you may see flickering on screen on some platforms. Typically this occurs in a pinboard-layout with many pinboard objects, or some other characteristic that makes the display complex.
The flickering can be avoided by passing the draw-with-buffer initarg which causes the drawing to go to an off-screen pixmap buffer. The screen is then updated from the buffer.
Note: GTK+ and Cocoa always buffer, so the draw-with-buffer initarg is ignored on these platforms.
To find the top pinboard-object at a supplied position (x, y), which is typically needed when processing user input, use pinboard-object-at-position. To decide whether a pinboard object is at a position, pinboard-object-at-position uses the generic function over-pinboard-object-p. over-pinboard-object-p has a default method that return true when the position is in the rectangle of the object, and a method for line object (subclasses of line-pinboard-object) that return true if the position is close to the line. You add methods to over-pinboard-object-p for your own classes. For example, if your pinboard object displays a thunder picture, you may want an over-pinboard-object-p method that computes whether the position is inside the thunder drawing.
There is also the generic function pinboard-object-overlap-p, with a default method that determines whether the rectangle of the object overlaps the rectangle specified by the other arguments.
One of the major uses the CAPI itself makes of pinboard objects is to implement graph panes. The graph-pane itself is a pinboard-layout and it is built using pinboard-objects for the nodes and edges. This is because each node (and sometimes each edge) of the graph needs to react individually to the user. For instance, when an event is received by the graph-pane, it is told which pinboard object was under the pointer at the time, and it can then use this information to change the selection.
Create the following graph-pane and notice that every node in the graph is made from an item-pinboard-object as described in the previous section and that each edge is made from a line-pinboard-object.
(defun node-children (node) (when (< node 16) (list (* node 2) (1+ (* node 2)))))
(contain (make-instance 'graph-pane :roots '(1) :children-function 'node-children) :best-width 300 :best-height 400)
A graph pane with pinboard object nodes
As mentioned before, pinboard-layouts can just as easily display ordinary panes inside themselves, and so the graph-pane provides the ability to specify the class used to represent the nodes. As an example, here is a graph-pane with the nodes made from push-buttons.
(contain (make-instance 'graph-pane :roots '(1) :children-function 'node-children :node-pinboard-class 'push-button) :best-width 300 :best-height 400)
A graph pane with push-button nodes
To create your own pinboard objects, the class drawn-pinboard-object is provided, which is a pinboard-object that accepts a display-callback to display itself. The following example creates a new subclass of drawn-pinboard-object that displays an ellipse.
(defun draw-ellipse-pane (gp pane x y width height) (with-geometry pane (let ((x-radius (1- (floor %width% 2))) (y-radius (1- (floor %height% 2)))) (gp:draw-ellipse gp (1+ (+ %x% x-radius)) (1+ (+ %y% y-radius)) x-radius y-radius :filled t :foreground (if (> x-radius y-radius) :red :yellow))))) (defclass ellipse-pane (drawn-pinboard-object) () (:default-initargs :display-callback 'draw-ellipse-pane :visible-min-width 50 :visible-min-height 50)) (contain (make-instance 'ellipse-pane) :best-width 200 :best-height 100)
An ellipse-pane class
The with-geometry macro is used to set the size and position, or geometry, of the ellipse drawn by the draw-ellipse-pane
function. The fill color depends on the radii of the ellipse - try resizing the window to see this. For more details of see the manual page for drawn-pinboard-object.
Now that you have a new ellipse-pane class, you can create instances of them and place them inside layouts. For instance, the example below creates nine ellipse panes and places them in a three by three grid.
(contain (make-instance 'grid-layout :description (loop for i below 9 collect (make-instance 'ellipse-pane)) :columns 3) :best-width 300 :best-height 400)
Nine ellipse-pane instances in a layout
simple-pinboard-layout is a subclass of pinboard-layout with only one child (a pane or a pinboard-object). It adopts the size constraints of its child. simple-pinboard-layout is useful when you want to arrange pinboard-objects using a layout pane (or a hierarchy of layouts). pinboard-objects need a pinboard-layout somewhere in the parent hierarchy, but using pinboard-layout would mean that the constraints computed by layout (top layout if it is a hierarchy) would not be automatically propagated to the next level. simple-pinboard-layout solves this problem. An example is the graph-pane, which is actually a subclass of simple-pinboard-layout, and as a child has a layout (of internal type) with a special algorithm that lays out the graph and displays it using pinboard-objects.
tracking-pinboard-layout is a subclass of pinboard-layout which tracks the motion of the mouse cursor, by highlighting the object underneath it (if any). Otherwise it behaves the same as pinboard-layout. It saves you from implementing the tracking when it is is desired.
(example-edit-file "capi/graphics/tracking-pinboard-layout")
CAPI User Guide and Reference Manual (Macintosh version) - 01 Dec 2021 19:31:22