Contents Previous Next

11 The DrawingArea Widget


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.

11.1 Creating a DrawingArea Widget

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.

11.2 Using DrawingArea Callback Functions

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 XmN­resizeCallback 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.

11.2.1 Handling Input Events

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.

figs.eps/V6a.10.01.eps.png
Output of drawing.c

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.

11.2.2 Redrawing a DrawingArea

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.

11.3 Using Translations on a DrawingArea

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.

figs.eps/V6a.10.02.eps.png
Output of free_hand.c.

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.

11.4 Using Color in a DrawingArea

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.

figs.eps/V6a.10.03.eps.png
Output of color_draw.c

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.

11.5 Summary

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.

11.6 Exercises

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.


Contents Previous Next