This chapter describes the Motif DrawingArea widget,
which provides a canvas for interactive drawing. The chapter does not
try to teach Xlib drawing, but rather it highlights, with numerous code
examples, the difficulties that may be encountered when working with
this widget. The chapter assumes some knowledge of Xlib. See Volume
One, Xlib Programming Manual, for additional information.
The DrawingArea widget provides a blank canvas for
interactive drawing using basic Xlib drawing primitives. The widget
does no drawing of its own, nor does it define or support any Motif
user-interface design style. Since it is subclassed from the Manager
widget class, the DrawingArea widget may also contain other widgets as
children, although there is no regimented layout policy. In short, the
DrawingArea is a free-form widget that you can use for interactive
drawing or object placement when conventional user-interface rules do
not apply.
The most intuitive use of the DrawingArea is for a
drawing or painting program. Here, the user can interactively draw
geometric objects and paint arbitrary colors. Another interesting
application demonstrated at a recent trade show used a DrawingArea
widget to display a map of the United States with dynamically-drawn
line segments representing the flight paths taken by airplanes. The
actual airplanes were represented by PushButton widgets displaying
pixmaps. Each airplane icon moved dynamically along its flight path
unless the user grabbed and moved it interactively in order to change
the flight path. Both of these examples demonstrate how certain
applications require visual or interactive interfaces that go beyond
the scope of the structured interface provided by Motif.
In order to support the widest range of uses for the
DrawingArea widget, the toolkit provides callback resources for
exposure, configure (resize), and input (button and key presses)
events. Each of these callbacks allows you to install very simple
drawing routines without doing substantial event-handling of your own.
Unfortunately, this level of event-handling support is usually
insufficient for most robust applications. As a result, most
applications install direct event handlers or action routines to manage
user input. The free-form nature of the DrawingArea makes it one of the
few Motif widgets where you can do handle events at this level without
risking non-compliance with the Motif Style Guide. (Most Motif
widgets either do not allow programmer-installed translations or
(silently) accept only a few override translations for fear that you
might inadvertently interfere with Motif GUI specifications.)
If you are using a DrawingArea as a manager widget,
there are two important things to keep in mind: translation tables and
widget layout management. As a Manager widget subclass, the DrawingArea
inherits certain translation and action tables that pass events to
gadget children and handle tab group traversal. Because of the
inherited translations, you must be careful about application-specific
translations that you may introduce into particular instances of the
DrawingArea. If you are planning to use the DrawingArea to contain
children and to have those children follow the standard Motif keyboard
traversal motions, you must be careful not to override the existing
translations.
However, if you need a manager widget in the
conventional sense, you should probably choose something other than a
DrawingArea widget, since the widget has no geometry management policy
of its own. The DrawingArea should probably only be used to manage
children when no structured widget layout policy is needed, as in the
airline application from the trade show. In this situation, the widget
assumes the dual responsibility of managing children and allowing for
application-defined interaction. As a result, there are going to be
some complexities and inconveniences with event handling, since the
application is trying to take advantage of both aspects of the widget
simultaneously.
Applications that wish to create DrawingArea widgets
must include the file <Xm/DrawingA.h>. To create a DrawingArea
widget, you can use the following call:
Widget drawing_a; drawing_a = XtVaCreateManagedWidget ("name", xmDrawingAreaWidgetClass, parent, resource-value-list, NULL);The parent of a DrawingArea must be either some type of Shell or a manager widget. It is quite common to find a DrawingArea widget as a child of a ScrolledWindow or a MainWindow, since drawing surfaces tend to be quite large, or at least dynamic in their growth potential.
If the DrawingArea widget is to have children, you
might want to follow the guidelines set forth in Chapter 8, Manager
Widgets, about creating the widget in an unmanaged state. The
widget can be managed with a call to XtManageChild() after its
children have been created. We do not demonstrate this technique, since
we are not going to use the widget as a traditional manager and there
is not going to be a great deal of parent-child interaction involving
geometry management.
The DrawingArea widget provides virtually no visual
resources and very few functional ones. The most important resources
are those that allow you to provide callback functions for handling
expose, resize, and input events. The DrawingArea is typically
input-intensive and, unlike most of the other Motif widgets, requires
the application to provide all of the necessary redrawing.
The callback routine for the XmNexposeCallback
is invoked whenever an Expose event is generated for the
widget. In this callback function, an application must repaint all or
part of the contents of the DrawingArea widget. If an application does
not redraw the contents of the widget, it appears empty, as the widget
is cleared automatically. Similarly, the XmNresizeCallback is
called whenever a ConfigureNotify event occurs as a result of
the DrawingArea being resized. The generalized XmNinputCallback
is invoked as a result of every keyboard and button event except button
motion events.
As discussed in Chapter 2, The Motif Programming
Model, callback routines are invoked by internal action routines
that are an integral part of all Motif widgets. Translation tables are
used to specify X event sequences that invoke the action routines.
Action functions typically invoke the appropriate application callback
functions associated with the widget's resources.
Most Motif widgets do not allow the application to
override or replace their default translations; the input model that
allows the application to conform to the Motif specifications is not to
be overridden by the application. However, because of the free-form
nature of the DrawingArea widget, you are free to override or replace
the default translation tables used for event-handling and notification
without non-compliant behavior. If you install your own translation
tables, you can have your action routines invoke callback routines as
is done by the existing DrawingArea actions, or you can have your
action functions do the drawing directly. For even tighter control over
event-handling, you can install event handlers at the X Toolkit
Intrinsics level.
There are a number of techniques available for doing
event management and we only demonstrate a few of them in this chapter.
The technique you choose is a matter of personal preference and the
intended extensibility of your application. Event handlers involve less
overhead, but translations are user-configurable. Either approach
provides more flexibility than using the default translation table and
callback resources of the DrawingArea. See Volume Four, X Toolkit
Intrinsics Programming Manual, for a detailed discussion of
translation tables and action routines and how they are associated with
callback functions.
Since the callback approach to event handling is the
simplest, we'll begin by discussing that approach. the source code
shows an extremely simple drawing program that associates a line
drawing function with the XmNinputCallback resource. Pressing
any of the pointer buttons marks the starting point of a line;
releasing the button marks the endpoint. You can only draw straight
lines. Even though the default translation table for the DrawingArea
widget selects key events and these events are passed to the callback
function, the callback function itself ignores them and thus key events
have no effect.
To demonstrate the complications inherent in using
the DrawingArea widget as a manager, the program also displays a
PushButton gadget that clears the window. A single callback function,
drawing_area_callback(), uses both the reason and the
event fields of the XmDrawingAreaCallbackStruct to
determine whether to draw a line or to clear the window.
This simple application draws directly into the
DrawingArea widget; the contents of its window is not saved anywhere.
The program does not support redrawing, since its purpose is strictly
to demonstrate the way input handling can be managed using the
XmNinputCallback. If the window is exposed due to the movement of
other windows, the contents of the window is not redrawn. A more
realistic drawing application would need code to handle both expose and
resize actions. The current application simply clears the window on
resize to further illustrate that the DrawingArea does not retain what
is in its window. XtSetLanguageProc() is only available in
X11R5; there is no corresponding function in X11R4.
/* drawing.c -- extremely simple drawing program that introduces * the DrawingArea widget. This widget provides a window for * drawing and some callbacks for getting input and other misc * events. It's also a manager, so it can have children. * There is no geometry management, tho. */ #include <Xm/DrawingA.h> #include <Xm/PushBG.h> #include <Xm/RowColumn.h> main(argc, argv) int argc; char *argv[]; { Widget toplevel, drawing_a, pb; XtAppContext app; XGCValues gcv; GC gc; void drawing_area_callback(); XtSetLanguageProc (NULL, NULL, NULL); toplevel = XtVaAppInitialize (&app, "Demos", NULL, 0, &argc, argv, NULL, XmNwidth, 400, XmNheight, 300, NULL); /* Create a DrawingArea widget. */ drawing_a = XtVaCreateWidget ("drawing_a", xmDrawingAreaWidgetClass, toplevel, NULL); /* add callback for all mouse and keyboard input events */ XtAddCallback (drawing_a, XmNinputCallback, drawing_area_callback, NULL); /* Since we're going to be drawing, we will be using Xlib routines * and therefore need a graphics context. Create a GC and attach * to the DrawingArea's XmNuserData to avoid having to make global * variable. (Avoiding globals is a good design principle to follow.) */ gcv.foreground = BlackPixelOfScreen (XtScreen (drawing_a)); gc = XCreateGC (XtDisplay (drawing_a), RootWindowOfScreen (XtScreen (drawing_a)), GCForeground, &gcv); XtVaSetValues (drawing_a, XmNuserData, gc, NULL); /* add a pushbutton the user can use to clear the canvas */ pb = XtVaCreateManagedWidget ("Clear", xmPushButtonGadgetClass, drawing_a, NULL); /* if activated, call same callback as XmNinputCallback. */ XtAddCallback (pb, XmNactivateCallback, drawing_area_callback, NULL); XtManageChild (drawing_a); XtRealizeWidget (toplevel); XtAppMainLoop (app); } /* Callback routine for DrawingArea's input callbacks and the * PushButton's activate callback. Determine which it is by * testing the cbs->reason field. */ void drawing_area_callback(widget, client_data, call_data) Widget widget; XtPointer client_data; XtPointer call_data; { static Position x, y; XmDrawingAreaCallbackStruct *cbs = (XmDrawingAreaCallbackStruct *) call_data; XEvent *event = cbs->event; if (cbs->reason == XmCR_INPUT) { /* activated by DrawingArea input event -- draw lines. * Button Down events anchor the initial point and Button * Up draws from the anchor point to the button-up point. */ if (event->xany.type == ButtonPress) { /* anchor initial point (i.e., save its value) */ x = event->xbutton.x; y = event->xbutton.y; } else if (event->xany.type == ButtonRelease) { /* draw full line; get GC and use in XDrawLine() */ GC gc; XtVaGetValues (widget, XmNuserData, &gc, NULL); XDrawLine (event->xany.display, cbs->window, gc, x, y, event->xbutton.x, event->xbutton.y); x = event->xbutton.x; y = event->xbutton.y; } } if (cbs->reason == XmCR_ACTIVATE) /* activated by pushbutton -- clear parent's window */ XClearWindow (event->xany.display, XtWindow (XtParent (widget))); }The output of the program is shown in the figure.
The callback routine that is used for the
XmNinputCallback takes the form of a standard callback routine. The
DrawingArea provides a XmDrawingAreaCallbackStruct for all of
its callbacks. This structure is defined as follows:
typedef struct { int reason; XEvent *event; Window window; } XmDrawingAreaCallbackStruct;The reason field identifies the type of occurrence that caused the callback to be invoked. For the XmNinputCallback, the value is XmCR_INPUT. The event field of the callback structure describes the event that caused the callback to be invoked. In older versions of the Motif toolkit, the pointer may be NULL if reason is XmCR_RESIZE. The window field is the window associated with the DrawingArea widget--this is the same value returned by calling XtWindow() on the widget.
Since the event itself is passed in as part of the
callback structure, we can look at the type field of the event
for more information than is provided by the callback reason alone.
(See Volume One, Xlib Programming Manual, for a detailed
description of XEvent structures and how to use them.) In
fact, since there are many possible events that can be associated with
the reason XmCR_INPUT, you have to look at the event structure
if you need any detail about what actually happened. shows the possible
event types for each of the DrawingArea callbacks. tab(@), linesize(2);
l | l | l lfCW | lfCW | lfCW. Callback@Reason@Event Type(s)
_
XmNexposeCallback@XmCR_EXPOSE@Expose
XmNresizeCallback@XmCR_RESIZE@ConfigureNotify
XmNinputCallback@XmCR_INPUT@ButtonPress, ButtonRelease, @@KeyPress,
KeyRelease
_ A common convention we've included in this program is the double
use of the drawing_area_callback() function. This technique is
known as function overloading, since the same function is used
by more than one source. We are using the routine as the input callback
for the DrawingArea widget, as well as the activate callback for the
PushButton gadget. Whenever the PushButton is activated, the callback
function is invoked and passed an XmPushButtonCallbackStruct
with the reason field set to XmCR_ACTIVATE.
It is beyond the scope of this book to discuss at
length or even introduce the use of Xlib; for that, see Volume One,
Xlib Programming Manual. However, there are a couple of details
concerning the use of Xlib functions that are noteworthy. For
efficiency in use of the X protocol, Xlib drawing calls typically do
not carry a lot of information about the drawing to be done. Instead,
drawing characteristics such as the foreground and background colors,
fill style, line weight, and so on, are defined in a graphics context
(GC), which is cached in the X server. Any drawing function that wishes
to use a particular GC must include the handle returned by a GC
creation call.
If many different routines are going to use the same
GC, the programmer should try to make the handle to it generally
available. The natural tendency is to declare the GC as a global
variable. However, as a program gets large, it is easy to get carried
away with the use of global variables. As a result, programs tend to
get overly complicated and decentralized. To avoid this problem, you
can use the XmNuserData resource (inherited from the Manager
widget class) as a temporary holding area for arbitrary pointers and
values. Since this program is small, it may not be worth the overhead
of a call to XtGetValues() to avoid a global variable. It is
up to you if you want to use the XmNuserData resource; this
particular example just shows one way of avoiding global variables.
If you play with the program a little, you will soon
find that you can draw right through the PushButton gadget in the
DrawingArea. Because gadgets do not have windows, the DrawingArea
widget indiscriminately allows you to draw through any gadget children
it may be managing. Similarly, activating the PushButton clears the
DrawingArea window, but it does not repaint the PushButton. None of the
manager widgets, including the DrawingArea, check if the user (or the
application) is overwriting or erasing gadgets. Changing the PushButton
from a gadget to a widget solves the immediate problem. However, it is
generally not a good idea to use a DrawingArea widget as both a drawing
canvas and as a place to have user-interface elements such as
PushButtons.
For conventional geometry management involving
DrawingArea widgets, you have two choices. You can write your own
geometry management routine (as demonstrated for BulletinBoard widgets
in Section #sbboard in Chapter 8, Manager Widgets) or you can
place the DrawingArea inside another manager that does more intelligent
geometry management. The nice part about this alternative is that the
other manager widgets are no more or less intelligent about graphics
and repainting than the DrawingArea widget. They don't provide a
callback for Expose events, but you can always add
translations for those events, if you need them.
In the source code when an Expose event or
a Resize event occurs, the drawing is not retained and as a
result the DrawingArea is always cleared. This problem was intentional
for the first example because we wanted to focus on the use of the
input callback routine. However, when you use the DrawingArea widget,
you must always be prepared to repaint whatever is supposed to be
displayed in the widget at any time.
As you may already know, most X servers support a
feature called backing store, which saves the contents of
windows, even when they are obscured by other windows, and repaints
them when they are exposed. When backing store is enabled and there is
enough memory available for the server, X will repaint all damaged
windows without ever notifying the application that anything happened.
However, you should never rely on this behavior, since you never know
if the X server supports backing store, or if it has enough memory to
save the contents of your windows. All applications are ultimately
responsible for redrawing their windows' contents whenever necessary.
For a painting application like that in the source
code the easiest way to make sure that a window can be repainted
whenever necessary is to draw both into the window and into an
offscreen pixmap. The contents of the pixmap can be copied back into
the window as needed. the source code demonstrates such a program. The
offscreen pixmap is copied back to the window with XCopyArea()
to redisplay the drawing when the XmNexposeCallback is called.
XtSetLanguageProc() is only available in X11R5; there is no
corresponding function in X11R4.
/* draw2.c -- extremely simple drawing program that demonstrates * how to draw into an off screen pixmap in order to retain the * contents of the DrawingArea widget. This allows us to redisplay * the widget if it needs repainting (expose events). */ #include <Xm/DrawingA.h> #include <Xm/PushBG.h> #include <Xm/RowColumn.h> #define WIDTH 400 /* arbitrary width and height values */ #define HEIGHT 300 Pixmap pixmap; /* used to redraw the DrawingArea */ main(argc, argv) int argc; char *argv[]; { Widget toplevel, drawing_a, pb; XtAppContext app; GC gc; void drawing_area_callback(); XtSetLanguageProc (NULL, NULL, NULL); toplevel = XtVaAppInitialize (&app, "Demos", NULL, 0, &argc, argv, NULL, XmNwidth, WIDTH, XmNheight, HEIGHT, NULL); /* Create a DrawingArea widget. */ drawing_a = XtVaCreateWidget ("drawing_a", xmDrawingAreaWidgetClass, toplevel, NULL); /* add callback for all mouse and keyboard input events */ XtAddCallback (drawing_a, XmNinputCallback, drawing_area_callback, NULL); XtAddCallback (drawing_a, XmNexposeCallback, drawing_area_callback, NULL); gc = XCreateGC (XtDisplay (drawing_a), RootWindowOfScreen (XtScreen (drawing_a)), 0, NULL); XtVaSetValues (drawing_a, XmNuserData, gc, NULL); XSetForeground (XtDisplay (drawing_a), gc, WhitePixelOfScreen (XtScreen (drawing_a))); /* create a pixmap the same size as the drawing area. */ pixmap = XCreatePixmap (XtDisplay (drawing_a), RootWindowOfScreen (XtScreen (drawing_a)), WIDTH, HEIGHT, DefaultDepthOfScreen (XtScreen (drawing_a))); /* clear pixmap with white */ XFillRectangle (XtDisplay (drawing_a), pixmap, gc, 0, 0, WIDTH, HEIGHT); /* drawing is now drawn into with "black"; change the gc for future */ XSetForeground (XtDisplay (drawing_a), gc, BlackPixelOfScreen (XtScreen (drawing_a))); /* add a pushbutton the user can use to clear the canvas */ pb = XtVaCreateManagedWidget ("Clear", xmPushButtonGadgetClass, drawing_a, NULL); /* if activated, call same callback as XmNinputCallback. */ XtAddCallback (pb, XmNactivateCallback, drawing_area_callback, NULL); XtManageChild (drawing_a); XtRealizeWidget (toplevel); XtAppMainLoop (app); } /* Callback routine for DrawingArea's input and expose callbacks * as well as the PushButton's activate callback. Determine which * it is by testing the cbs->reason field. */ void drawing_area_callback(widget, client_data, call_data) Widget widget; XtPointer client_data; XtPointer call_data; { static Position x, y; XmDrawingAreaCallbackStruct *cbs = (XmDrawingAreaCallbackStruct *) call_data; XEvent *event = cbs->event; Display *dpy = event->xany.display; if (cbs->reason == XmCR_INPUT) { /* activated by DrawingArea input event -- draw lines. * Button Down events anchor the initial point and Button * Up draws from the anchor point to the button-up point. */ if (event->xany.type == ButtonPress) { /* anchor initial point (i.e., save its value) */ x = event->xbutton.x; y = event->xbutton.y; } else if (event->xany.type == ButtonRelease) { /* draw full line; get GC and use in XDrawLine() */ GC gc; XtVaGetValues (widget, XmNuserData, &gc, NULL); XDrawLine (dpy, cbs->window, gc, x, y, event->xbutton.x, event->xbutton.y); /* draw into the pixmap as well for redrawing later */ XDrawLine (dpy, pixmap, gc, x, y, event->xbutton.x, event->xbutton.y); x = event->xbutton.x; y = event->xbutton.y; } } if (cbs->reason == XmCR_EXPOSE || cbs->reason == XmCR_ACTIVATE) { GC gc; if (cbs->reason == XmCR_ACTIVATE) /* Clear button pushed */ widget = XtParent (widget); /* get the DrawingArea widget */ XtVaGetValues (widget, XmNuserData, &gc, NULL); if (cbs->reason == XmCR_ACTIVATE) { /* Clear button pushed */ /* to clear a pixmap, reverse foreground and background */ XSetForeground (dpy, gc, WhitePixelOfScreen (XtScreen (widget))); /* ...and fill rectangle the size of the pixmap */ XFillRectangle (dpy, pixmap, gc, 0, 0, WIDTH, HEIGHT); /* don't foreget to reset */ XSetForeground (dpy, gc, BlackPixelOfScreen (XtScreen (widget))); } /* Note: we don't have to use WIDTH and HEIGHT--we could pull the * exposed area out of the event structure, but only if the reason * was XmCR_EXPOSE... make it simple for the demo; optimize as needed. */ XCopyArea (dpy, pixmap, event->xany.window, gc, 0, 0, WIDTH, HEIGHT, 0, 0); } }A frequent problem encountered in using the DrawingArea widget is the need to redraw after every Resize event. When you enlarge the DrawingArea window, an Expose event is automatically generated since more of the window becomes exposed. But, if you shrink the window, no Expose event is generated since no new part of the window is being exposed.
The reason why no Expose event is generated
when you shrink a DrawingArea widget is deep inside Xlib. The bit
gravity of a window indicates where new bits are placed automatically
by X when a window is resized. If you resize a window larger, then the
data in the window remains in the top-left corner and the application
gets a Resize event and an Expose event. The
Expose event just identifies the newly exposed area, not the entire
window. If you make the window smaller, all of the data in the window
gets pushed to the top left; there is no newly exposed area, so there
is no Expose event.
The solution is to make the window forget about bit
gravity, so every Resize event causes all of the bits to be
cleared. As a result, the Expose event identifies the entire
window as being exposed, instead of just the newly exposed region. This
technique has the side effect of generating an Expose event
even when the window is resized smaller.
There is no routine to set the bit gravity of a
window individually. It can be set only with
XChangeWindowAttributes(), as in the following code fragment:
XSetWindowAttributes attrs; attrs.bit_gravity = ForgetGravity; XChangeWindowAttributes (XtDisplay (drawing_area), XtWindow (drawing_area), CWBitGravity, &attrs);Once you do this, the DrawingArea widget gets Expose events when you resize it to be smaller.
As mentioned earlier, it is generally permissible to
override or replace the default translation table of the DrawingArea
widget with new translations. The only potential problem is if you plan
to use the DrawingArea as a manager for other widgets and you expect it
to follow the keyboard traversal mechanisms described by the Motif
Style Guide. In fact, handling keyboard traversal is pretty much
all that the default translations for the DrawingArea do. For example,
the following is a subset of the default translations for the
DrawingArea widget: This translation table lists only a subset of the
current translations in the DrawingArea widget; there is no guarantee
that the translations will remain the same in future revisions of the
toolkit.
<Key>osfSelect: DrawingAreaInput() ManagerGadgetSelect() <Key>osfActivate: DrawingAreaInput() ManagerParentActivate() <Key>osfHelp: DrawingAreaInput() ManagerGadgetHelp() <KeyDown>: DrawingAreaInput() ManagerGadgetKeyInput() <KeyUp>: DrawingAreaInput() <BtnMotion>: ManagerGadgetButtonMotion() <Btn1Down>: DrawingAreaInput() ManagerGadgetArm() <Btn1Down>,<Btn1Up>: DrawingAreaInput() ManagerGadgetActivate()These translations show that the manager widget part of the DrawingArea is responsible for tracking events for its gadget children. It is not necessary to support these translations if you are not going to use the DrawingArea to manage children. Most user-generated events also invoke DrawingAreaInput(), which does not do any drawing, but simply invokes the XmNinputCallback.
As you can see, the BtnMotion translation
is not passed to DrawingAreaInput(), which means that the
XmNinputCallback is not called for pointer motion events. When it
comes to more complex drawing than that done in the source code this
omission is a serious deficiency. To support rubberbanding or free-hand
drawing techniques, which require pointer motion events, you must
install either an event handler or a translation entry to handle motion
events.
The simplest approach would be to replace the
translation table entry for <BtnMotion> events. However, this
is not possible, due to a bug in the X Toolkit Intrinsics. The correct
thing to do is the following:
String translations = "<Btn1Motion>: DrawingAreaInput() ManagerGadgetButtonMotion()"; ... drawing_a = XtVaCreateManagedWidget ("drawing_a", xmDrawingAreaWidgetClass, main_w, ... NULL); XtOverrideTranslations (drawing_a, XtParseTranslationTable (translations)); XtAddCallback (drawing_a, XmNinputCallback, draw, NULL);With this new translation, the XmNinputCallback function ( draw()) would be notified of pointer motion while Button 1 is down.
XtOverrideTranslations() is the preferred
method for installing a new translation into the DrawingArea widget
because it is nondestructive. The routine only replaces translations
for which identical events are specified and leaves all other
translations in place. However, this routine does not work in this case
because there is already a translation for the Button 1 down-up
sequence in the DrawingArea translation table. In the current
implementation, once Button 1 goes down, the Xt event translator waits
for the Button 1 up event to match the partially finished translation.
Therefore, no Button 1 motion events can be caught. If we want to get
pointer motion events while the button is down, we have to resort to
other alternatives.
One such alternative is to replace the entire
translation table, regardless of whether we are adding new entries or
overriding existing ones. This is known as a destructive override
because the existing translation table is thrown out. This action has
the desired effect because the offending Button 1 translation is thrown
out. However, we must then take steps to re-install any other default
translations that are still required. To completely replace the
existing translations, the XmNtranslations resource can be set
as shown in the following code fragment:
String translations = "<Btn1Motion>: DrawingAreaInput() ManagerGadgetButtonMotion()"; ... drawing_a = XtVaCreateManagedWidget ("drawing_a", xmDrawingAreaWidgetClass, main_w, XmNtranslations, XtParseTranslationTable (translations), NULL); XtAddCallback (drawing_a, XmNinputCallback, draw, NULL);Once you go to the trouble of replacing the translation table, you may as well install your own action functions as well. Doing so allows you to do the drawing directly from the action functions, rather than using it as an intermediate function to call an application callback. This direct-drawing approach is demonstrated in the source code The program uses pointer motion to draw lines as the pointer is dragged with the button down, rather than when the button is pressed and released. You'll notice that we have used much the same design as in the source code but have moved some of the code into different callback routines and have placed the DrawingArea widget into a MainWindow widget for flexibility. None of these changes are required nor do they enhance performance in any way. They merely point out different ways of providing the same functionality. XtSetLanguageProc() is only available in X11R5; there is no corresponding function in X11R4.
/* free_hand.c -- simple drawing program that does freehand * drawing. We use translations to do all the event handling * for us rather than using the drawing area's XmNinputCallback. */ #include <Xm/MainW.h> #include <Xm/DrawingA.h> #include <Xm/PushBG.h> #include <Xm/RowColumn.h> /* Global variables */ GC gc; Pixmap pixmap; Dimension width, height; main(argc, argv) int argc; char *argv[]; { Widget toplevel, main_w, drawing_a, pb; XtAppContext app; XGCValues gcv; void draw(), redraw(), clear_it(); XtActionsRec actions; String translations = /* for the DrawingArea widget */ /* ManagerGadget* functions are necessary for DrawingArea widgets * that steal away button events from the normal translation tables. */ "<Btn1Down>: draw(down) ManagerGadgetArm() 0 <Btn1Up>: draw(up) ManagerGadgetActivate() 0 <Btn1Motion>: draw(motion) ManagerGadgetButtonMotion()"; XtSetLanguageProc (NULL, NULL, NULL); toplevel = XtVaAppInitialize (&app, "Demos", NULL, 0, &argc, argv, NULL, NULL); /* Create a MainWindow to contain the drawing area */ main_w = XtVaCreateManagedWidget ("main_w", xmMainWindowWidgetClass, toplevel, XmNscrollingPolicy, XmAUTOMATIC, NULL); /* Add the "draw" action/function used by the translation table */ actions.string = "draw"; actions.proc = draw; XtAppAddActions (app, &actions, 1); /* Create a DrawingArea widget. Make it 5 inches wide by 6 inches tall. * Don't let it resize so the Clear Button doesn't force a resize. */ drawing_a = XtVaCreateManagedWidget ("drawing_a", xmDrawingAreaWidgetClass, main_w, XmNtranslations, XtParseTranslationTable (translations), XmNunitType, Xm1000TH_INCHES, XmNwidth, 5000, /* 5 inches */ XmNheight, 6000, /* 6 inches */ XmNresizePolicy, XmNONE, /* remain this a fixed size */ NULL); /* When scrolled, the drawing area will get expose events */ XtAddCallback (drawing_a, XmNexposeCallback, redraw, NULL); /* convert drawing area back to pixels to get its width and height */ XtVaSetValues (drawing_a, XmNunitType, XmPIXELS, NULL); XtVaGetValues (drawing_a, XmNwidth, &width, XmNheight, &height, NULL); /* create a pixmap the same size as the drawing area. */ pixmap = XCreatePixmap (XtDisplay (drawing_a), RootWindowOfScreen (XtScreen (drawing_a)), width, height, DefaultDepthOfScreen (XtScreen (drawing_a))); /* Create a GC for drawing (callback). Used a lot -- make global */ gcv.foreground = WhitePixelOfScreen (XtScreen (drawing_a)); gc = XCreateGC (XtDisplay (drawing_a), RootWindowOfScreen (XtScreen (drawing_a)), GCForeground, &gcv); /* clear pixmap with white */ XFillRectangle (XtDisplay (drawing_a), pixmap, gc, 0, 0, width, height); /* drawing is now drawn into with "black"; change the gc */ XSetForeground (XtDisplay (drawing_a), gc, BlackPixelOfScreen (XtScreen (drawing_a))); pb = XtVaCreateManagedWidget ("Clear", xmPushButtonGadgetClass, drawing_a, NULL); /* Pushing the clear button calls clear_it() */ XtAddCallback (pb, XmNactivateCallback, clear_it, drawing_a); XtRealizeWidget (toplevel); XtAppMainLoop (app); } /* Action procedure to respond to any of the events from the * translation table declared in main(). This function is called * in response to Button1 Down, Up and Motion events. Basically, * we're just doing a freehand draw -- not lines or anything. */ void draw(widget, event, args, num_args) Widget widget; XEvent *event; String *args; int *num_args; { static Position x, y; XButtonEvent *bevent = (XButtonEvent *) event; if (*num_args != 1) XtError ("Wrong number of args!"); if (strcmp (args[0], "down")) { /* if it's not "down", it must either be "up" or "motion" * draw full line from anchor point to new point. */ XDrawLine (bevent->display, bevent->window, gc, x, y, bevent->x, bevent->y); XDrawLine (bevent->display, pixmap, gc, x, y, bevent->x, bevent->y); } /* freehand is really a bunch of line segments; save this point */ x = bevent->x; y = bevent->y; } /* Clear the window by clearing the pixmap and calling XCopyArea() */ void clear_it(pb, client_data, call_data) Widget pb; XtPointer client_data; XtPointer call_data; { Widget drawing_a = (Widget) client_data; XmPushButtonCallbackStruct *cbs = (XmPushButtonCallbackStruct *) call_data; /* clear pixmap with white */ XSetForeground (XtDisplay (drawing_a), gc, WhitePixelOfScreen (XtScreen (drawing_a))); XFillRectangle (XtDisplay (drawing_a), pixmap, gc, 0, 0, width, height); /* drawing is now done using black; change the gc */ XSetForeground (XtDisplay (drawing_a), gc, BlackPixelOfScreen (XtScreen (drawing_a))); XCopyArea (cbs->event->xbutton.display, pixmap, XtWindow (drawing_a), gc, 0, 0, width, height, 0, 0); } /* redraw is called whenever all or portions of the drawing area is * exposed. This includes newly exposed portions of the widget resulting * from the user's interaction with the scrollbars. */ void redraw(drawing_a, client_data, call_data) Widget drawing_a; XtPointer client_data; XtPointer call_data; { XmDrawingAreaCallbackStruct *cbs = (XmDrawingAreaCallbackStruct *) call_data; XCopyArea (cbs->event->xexpose.display, pixmap, cbs->window, gc, 0, 0, width, height, 0, 0); }The output of the program is shown in the figure.
In the source code the DrawingArea widget uses the
following translation string:
String translations = "<Btn1Down>: draw(down) ManagerGadgetArm() 0 <Btn1Up>: draw(up) ManagerGadgetActivate() 0 <Btn1Motion>: draw(motion) ManagerGadgetButtonMotion()";For each of the specified events, the translation describes two actions. The draw() action is our own function that actually draws into the DrawingArea. The ManagerGadget actions are standard DrawingArea actions (inherited from the Manager widget class) for passing events to a gadget child, as described earlier. We keep them in place because we are still using the PushButton gadget. We are not keeping the routines for managing keyboard traversal, but simply those required to arm and activate the button.
The draw() action routine tests whether it
has been called from a button up event, a button down event, or a
motion event. Since the action function is passed the event that
invoked it, we could simply test the type field of the event. However,
this example gives us a chance to exercise the Xt feature that supports
string arguments passed to action functions. Accordingly, the
draw() function determines what action to take by examining its
args[0] parameter, which contains the string passed as the single
parameter in the translation table. For example, draw(up)
passes the string "up" as the args[0] parameter in
response to a <Btn1Up> event.
Lines are drawn for both ButtonRelease and
ButtonMotion events, but not for ButtonPress events. A
line is drawn from the last anchor point to the current location of the
mouse. As the pointer moves from one point to the next, the anchor
point is always one step behind, so a line segment is drawn from that
location to the current location. The only time that a line segment is
not drawn is on the initial button press (and any motion events that
occur while the button is not down). The coordinate values are relative
to the current location of the pointer within the DrawingArea widget,
no matter how it is positioned in the MainWindow.
The draw() function draws into the window
and also into a pixmap. The MainWindow widget is configured to have its
XmNscrollingPolicy set to XmAUTOMATIC, so ScrollBars
are automatically installed over the DrawingArea when it is larger than
the MainWindow, which allows the user to view different parts of the
canvas interactively. Scrolling actions cause the contents of the newly
exposed portions of the canvas to be erased by default. Unless we
provide a mechanism by which the DrawingArea can redraw itself,
scrolling the DrawingArea loses previously drawn contents. To handle
this problem, we employ the same principle we used in the source code
We install a pixmap that is used by both the draw() and
redraw() functions.
The redraw() routine is installed as the
callback function for the XmNexposeCallback. The function
merely uses XCopyArea() to copy the pixmap onto the window of
the DrawingArea. We are not concerned with the position of the
DrawingArea with respect to the MainWindow in this routine. All we need
to do is copy the pixmap directly into the window. X ensures that the
visible portion of the window is clipped as necessary.
In this example, the ManagerGadget actions
don't do anything unless the pointer is inside the Clear button,
so the translation is relatively safe. However, you should be sure to
remember that both actions are called. If you press Button 1 inside the
PushButton and doodle around a bit before releasing it, the drawing is
still done, even though the result is hidden by the gadget. In another
application, the fact that actions for both the drawing area itself and
its gadget children are both called might lead to indeterminate
results.
The draw() action does not (and cannot)
know if the gadget is also going to react to the button event. This
problem does not exist with the standard DrawingAreaInput()
action routine used in the previous examples because that routine is
implemented by the Motif toolkit and it uses its own internal
mechanisms to determine if the gadget is activated as well. If the
DrawingArea does process the event on the gadget, the
DrawingAreaInput() action knows that it should not invoke the
callback function. However, this internal mechanism is not available
outside of the widget code. Reordering the action functions does not
help, since there is still no way to know, without making an educated
guess, whether or not the DrawingArea acted upon an event on behalf of
a gadget child.
As a result of this problem, draw() starts
drawing a line, even if it starts in the middle of the PushButton,
because the DrawingArea processes all of the action functions in the
list. If you drag the pointer out of the gadget before releasing the
mouse button, the starting point of the line is inside the gadget, but
it is hidden when the gadget repaints itself. However, in this
particular situation, you can do some guesswork. By installing an
XmNarmCallback function, you can tell whether or not the
DrawingArea activated a button, and by setting an internal state
variable, you can decide whether or not the draw() action
routine should do its drawing.
This confusing behavior is yet another reason why it
is best not to include children in DrawingArea widgets that are
intended for interactive graphics. If the DrawingArea does not have any
gadget children, installing these auxiliary actions in the translation
table is not necessary.
In this section, we expand on our previous examples
by incorporating color. The choice of colors is primarily supported by
a function we define called set_color(), which takes a widget
and an arbitrary color name and sets the global GC's
foreground color. By providing an array of colors in the form of
colored PushButtons, we've got a color paint program. We have removed
the PushButton gadget from the DrawingArea and created a proper control
panel to the left of the DrawingArea. The program uses a RowColumn
widget (see Section #srowcolumn in Chapter 8, Manager Widgets)
to manage a set of eighteen colored PushButtons. On a monochrome
screen, the program runs, but the buttons are either black or white,
depending on which is closer to the RGB values corresponding to the
color names chosen. You can only draw with the black buttons, since the
background is already white. The program that demonstrates these
techniques is shown in the source code XtSetLanguageProc() is
only available in X11R5; there is no corresponding function in X11R4.
/* color_draw.c -- simple drawing program using predefined colors. */ #include <Xm/MainW.h> #include <Xm/DrawingA.h> #include <Xm/PushBG.h> #include <Xm/PushB.h> #include <Xm/RowColumn.h> #include <Xm/ScrolledW.h> #include <Xm/Form.h> GC gc; Pixmap pixmap; /* dimensions of drawing area (pixmap) */ Dimension width, height; String colors[] = { "Black", "Red", "Green", "Blue", "White", "Navy", "Orange", "Yellow", "Pink", "Magenta", "Cyan", "Brown", "Grey", "LimeGreen", "Turquoise", "Violet", "Wheat", "Purple" }; main(argc, argv) int argc; char *argv[]; { Widget toplevel, main_w, sw, rc, form, drawing_a, pb; XtAppContext app; XGCValues gcv; void draw(), redraw(), set_color(), exit(), clear_it(); int i; XtActionsRec actions; String translations = /* for the DrawingArea widget */ "<Btn1Down>: draw(down)0 <Btn1Up>: draw(up) 0 <Btn1Motion>: draw(motion)"; XtSetLanguageProc (NULL, NULL, NULL); toplevel = XtVaAppInitialize (&app, "Demos", NULL, 0, &argc, argv, NULL, NULL); /* Create a MainWindow to contain the drawing area */ main_w = XtVaCreateManagedWidget ("main_w", xmFormWidgetClass, toplevel, NULL); /* Create a GC for drawing (callback). Used a lot -- make global */ gcv.foreground = WhitePixelOfScreen (XtScreen (main_w)); gc = XCreateGC (XtDisplay (main_w), RootWindowOfScreen (XtScreen (main_w)), GCForeground, &gcv); /* Create a 3-column array of color tiles */ rc = XtVaCreateWidget ("rc", xmRowColumnWidgetClass, main_w, XmNnumColumns, 3, XmNpacking, XmPACK_COLUMN, XmNleftAttachment, XmATTACH_FORM, XmNtopAttachment, XmATTACH_FORM, NULL); for (i = 0; i < XtNumber(colors); i++) { /* Create a single tile (pixmap) for each color */ pixmap = XCreatePixmap (XtDisplay (rc), RootWindowOfScreen (XtScreen (rc)), 16, 16, DefaultDepthOfScreen (XtScreen (rc))); set_color (rc, colors[i]); /* set the gc's color according to name */ XFillRectangle (XtDisplay (main_w), pixmap, gc, 0, 0, 16, 16); pb = XtVaCreateManagedWidget (colors[i], xmPushButtonWidgetClass, rc, XmNlabelType, XmPIXMAP, XmNlabelPixmap, pixmap, NULL); /* callback for this pushbutton sets the current color */ XtAddCallback (pb, XmNactivateCallback, set_color, colors[i]); } XtManageChild (rc); pb = XtVaCreateManagedWidget ("Quit", xmPushButtonGadgetClass, main_w, XmNleftAttachment, XmATTACH_FORM, XmNtopAttachment, XmATTACH_WIDGET, XmNtopWidget, rc, NULL); XtAddCallback (pb, XmNactivateCallback, exit, NULL); /* Clear button -- wait till DrawingArea is created so we can use * it to pass as client data. */ pb = XtVaCreateManagedWidget ("Clear", xmPushButtonGadgetClass, main_w, XmNleftAttachment, XmATTACH_WIDGET, XmNleftWidget, pb, XmNtopAttachment, XmATTACH_WIDGET, XmNtopWidget, rc, NULL); sw = XtVaCreateManagedWidget ("scrolled_win", xmScrolledWindowWidgetClass, main_w, XmNwidth, 300, XmNscrollingPolicy, XmAUTOMATIC, XmNscrollBarDisplayPolicy, XmAS_NEEDED, XmNtopAttachment, XmATTACH_FORM, XmNbottomAttachment, XmATTACH_FORM, XmNleftAttachment, XmATTACH_WIDGET, XmNleftWidget, rc, XmNrightAttachment, XmATTACH_FORM, NULL); /* Add the "draw" action/function used by the translation table * parsed by the translations resource below. */ actions.string = "draw"; actions.proc = draw; XtAppAddActions (app, &actions, 1); /* Create a DrawingArea widget. Make it 5 inches wide by 6 inches tall. * Don't let it resize so the Clear Button doesn't force a resize. */ drawing_a = XtVaCreateManagedWidget ("drawing_a", xmDrawingAreaWidgetClass, sw, XmNtranslations, XtParseTranslationTable (translations), XmNunitType, Xm1000TH_INCHES, XmNwidth, 5000, /* 5 inches */ XmNheight, 6000, /* 6 inches */ XmNresizePolicy, XmNONE, /* remain this a fixed size */ NULL); /* When scrolled, the drawing area will get expose events */ XtAddCallback (drawing_a, XmNexposeCallback, redraw, NULL); /* Pushing the clear button clears the drawing area widget */ XtAddCallback (pb, XmNactivateCallback, clear_it, drawing_a); /* convert drawing area back to pixels to get its width and height */ XtVaSetValues (drawing_a, XmNunitType, XmPIXELS, NULL); XtVaGetValues (drawing_a, XmNwidth, &width, XmNheight, &height, NULL); /* create a pixmap the same size as the drawing area. */ pixmap = XCreatePixmap (XtDisplay (drawing_a), RootWindowOfScreen (XtScreen (drawing_a)), width, height, DefaultDepthOfScreen (XtScreen (drawing_a))); /* clear pixmap with white */ set_color (drawing_a, "White"); XFillRectangle (XtDisplay (drawing_a), pixmap, gc, 0, 0, width, height); XtRealizeWidget (toplevel); XtAppMainLoop (app); } /* Action procedure to respond to any of the events from the * translation table declared in main(). This function is called * in response to Button1 Down, Up and Motion events. Basically, * we're just doing a freehand draw -- not lines or anything. */ void draw(widget, event, args, num_args) Widget widget; XEvent *event; String *args; int *num_args; { static Position x, y; XButtonEvent *bevent = (XButtonEvent *) event; if (*num_args != 1) XtError ("Wrong number of args!"); if (strcmp (args[0], "down")) { /* if it's not "down", it must either be "up" or "motion" * draw full line from anchor point to new point. */ XDrawLine (bevent->display, bevent->window, gc, x, y, bevent->x, bevent->y); XDrawLine (bevent->display, pixmap, gc, x, y, bevent->x, bevent->y); } /* freehand is really a bunch of line segements; save this point */ x = bevent->x; y = bevent->y; } /* Clear the window by clearing the pixmap and calling XCopyArea() */ void clear_it(pb, client_data, call_data) Widget pb; XtPointer client_data; XtPointer call_data; { Widget drawing_a = (Widget) client_data; XmPushButtonCallbackStruct *cbs = (XmPushButtonCallbackStruct *) call_data; /* clear pixmap with white */ XSetForeground (XtDisplay (drawing_a), gc, WhitePixelOfScreen (XtScreen (drawing_a))); /* this clears the pixmap */ XFillRectangle (XtDisplay (drawing_a), pixmap, gc, 0, 0, width, height); /* drawing is now done using black; change the gc */ XSetForeground (XtDisplay (drawing_a), gc, BlackPixelOfScreen (XtScreen (drawing_a))); /* render the newly cleared pixmap onto the window */ XCopyArea (cbs->event->xbutton.display, pixmap, XtWindow (drawing_a), gc, 0, 0, width, height, 0, 0); } /* redraw is called whenever all or portions of the drawing area is * exposed. This includes newly exposed portions of the widget resulting * from the user's interaction with the scrollbars. */ void redraw(drawing_a, client_data, call_data) Widget drawing_a; XtPointer client_data; XtPointer call_data; { XmDrawingAreaCallbackStruct *cbs = (XmDrawingAreaCallbackStruct *) call_data; XCopyArea (cbs->event->xexpose.display, pixmap, cbs->window, gc, 0, 0, width, height, 0, 0); } /* callback routine for when any of the color tiles are pressed. * This general function may also be used to set the global gc's * color directly. Just provide a widget and a color name. */ void set_color(widget, client_data, call_data) Widget widget; XtPointer client_data; XtPointer call_data; { String color = (String) client_data; Display *dpy = XtDisplay (widget); Colormap cmap = DefaultColormapOfScreen (XtScreen (widget)); XColor col, unused; if (!XAllocNamedColor (dpy, cmap, color, &col, &unused)) { char buf[32]; sprintf (buf, "Can't alloc %s", color); XtWarning (buf); return; } XSetForeground (dpy, gc, col.pixel); }The output of the program in shown in the figure.
One thing to note about the program is that the
callback routine for the Clear button is passed the DrawingArea
widget as the client data. This technique saves us from having to
declare a global variable, while still providing a handle to the
DrawingArea in the callback routine.
The DrawingArea widget is probably most useful when
it is used as a canvas for displaying raster images, animation, or a
mixture of text and graphics. It is also well-suited for tasks that
require interactive user input. The widget provides some rudimentary
input mechanisms in the form of callbacks that are invoked by button
events.
The translation and action tables supported by the X
Toolkit Intrinsics provide a simple mechanism for notifying
applications of user events such as double-mouse clicks, keyboard
events, and so on. By creatively modifying the default translations and
actions, you could build a rather intricate system of action functions
that produces interesting graphics based on various forms of user input
sequences.
However, what you can do with actions is simplistic
given the complexities that are involved in true paint or draw
applications. Applications that require a graphic front end should
probably dig deeper into the lower levels of Xt for event handling and
into Xlib for image rendering.
There are a number of different possibilities you
could explore in extending the DrawingArea widget. The following
exercises are intended to shine the light down some interesting paths
that you can take.