IBM's VisualAge for C++ compiler contains several pleasant surprises, including some useful tools for making smart drawings.
Introduction
Most modern C++ development environments include a facility to graphically represent the relationship between classes. To be truly useful, such a diagramming utility should enable a user to manipulate the drawing in various ways, for instance, by adding new classes, defining inheritance relationships, and adding attributes to classes. It would be nice to have a diagramming tool that could also be hooked to code and controlled by a program. But developing such a module seems a daunting task, especially if it must be multi-platform. Fortunately, the building blocks already exist in IBM's Open Class Library (OCL), which comes bundled for free with IBM's VisualAge for C++ compiler. OCL is available on several platforms, and it contains a set of classes that support two-dimensional drawing. After several false starts, I was able to use OCL to produce my own diagramming module, ending up with surprisingly simple code.
The source code accompanying this article compiles into a very simple prototype of a diagramming tool. It displays a hard-coded "model" with three classes (Figure 1) . All you can do is move them around with the mouse. This will admittedly not impress your general manager, but you can always take the code as a starting point and extend it by the principles shown. I've tested the code on OS/2 and Windows NT 4.0, but it should compile on any other platform where VisualAge for C++ and the OCL are available.
Design Overview
My purpose in constructing this prototype was to get a feeling for what it would take to build a full-functional diagramming module. The major problems to be solved were:
- how to draw the appropriate symbols by using the graphic classes in OCL (at this stage, an approximation would be fine)
- how to detect which symbol the user has selected (hit testing)
- how to let the user move class symbols around and adjust the various other symbols accordingly
In the following sections I demonstrate how to perform these tasks using OCL.
Drawing Symbols with OCL
Drawing with the IBM OCL may require a shift of thinking, especially if you have been drawing in an Intel-based windowing environment. Traditionally, you do the drawing when the WM_PAINT message comes in. You find out what area needs repainting, look at the symbols that make up that area, maybe do some calculations, and then you draw lines, boxes, circles, or whatever it takes to build up your drawing. The graphics engine understands these drawing commands and immediately translates them into pixels. The OCL, however, uses so-called retained graphics. As before, you issue a set of drawing commands. But the graphics engine does not execute them straight away. Instead it keeps these commands around in a condensed format. The engine executes them later, whenever needed (which is probably at WM_PAINT time, but you don't have to worry about it).
The OS/2 Presentation Manager graphics engine has had retained graphics built in from day one. So the OS/2 version of the OCL simply wraps this engine up in a set of classes. On Windows, however, IBM had to implement equivalent functionality in OCL code (the sources are supplied on the VisualAge CD-ROM). As a user of OCL, I don't care what is inside, so I am loosely referring to "the graphics engine" to mean either mechanism.
At this point I want to walk through some code, introducting the major players along the way. By the way, all OCL class names start with an upper-case I, which stands for guess what IBM. The graphics classes start with IG. In the code presented here, the user-defined classes all start with SC.
An IDrawingCanvas is like the blank sheet of paper you'll be drawing on. It is a specialized widget that comes with a default paint handler attached. You can replace the handler if you don't like the default, but I haven't found that necessary yet. An IDrawingCanvas can contain an IGList, which is a list of graphic primitives. An IGList is itself a graphic primitive, so an IGList can contain other IGLists. In a minute I'll show why. When it's time to draw on the canvas, the paint handler just tells the IGList to draw itself, and that list in turn delegates drawing responsibility to every graphic primitive that it contains.
My own SCDrawingArea derives from IDrawingCanvas (Listing 1) . The constructor code (Listing 2) creates a new IGlist on-the-fly. Indeed, a drawing canvas without a graphic list would be pretty useless it would remain a blank sheet of paper forever.
My prototype application places the SCDrawingArea inside an IViewPort. IViewPort in turn sits inside the SCMainWindow (derived from IFrameWindow); SCMainWindow becomes the application main window. IViewPort takes care of resizing the canvas when the frame window is resized. This resizing code is pretty much standard OCL, so I won't go into detail here.
Instead, I briefly return to the SCDrawingArea constructor code and tighten up a few loose ends. The IGraphicContext class handles rendering of graphic objects on a particular device. IGraphicContext is basically a wrapper class around the OS/2 presentation space or the Windows device context, plus some drawing attributes that I set up here.
SCDrawingArea inherits from IMouseHandler as well as from IDrawingCanvas, so it can respond to mouse events. The constructor activates the handler by calling handleEventsFor(this).
Loading the model
In a real application, you would read a meta-model from a file on disk or some other persistent mechanism. You would also read in the layout of the diagram as it was last saved by the user. This would be the job of the openDiagram method, which is called at the end of SCDrawingArea's constructor. In this prototype application, openDiagram is a stub routine. I have hard-coded a set of two classes (Teacher and Student) that inherit from a third (Person). The diagram layout is hard-coded as well. The source code includes a file named SCModel.hpp. This file contains a few "really bare"-bones model classes for SCClass and SCGeneralization. These two classes form one half of the Model-View paradigm, with an SCClassSymbol class being the view class that displays the SCClass object to the user. The Model-View paradigm enables creation of multiple views for each model object, although I do not create more than one view here. I could easily have removed these classes from the source code (in fact, SCGeneralization is not used at all), but I leave them in for the sake of illustration.
Drawing the lines
A real tool would have many symbols, but the prototype has only two: an SCClassSymbol and an SCGeneralizationSymbol (used to show inheritance). Both derive from SCElementView, which in turn derives from IGList. The code for both symbols is similar, so I show only SCGeneralizationSymbol here (see Listing 3) .
SCGeneralizationSymbol's buildGraphics member function (Listing 4) is probably the most interesting. Through this member function SCGeneralizationSymbol adds to itself (remember, it's an IGList) all the graphic primitives needed to display an arrow with a hollow head, pointing from the child class to the parent class to show inheritance. The first thing buildGraphics does when invoked is clean up SCGeneralizationSymbol by calling removeAll. removeAll is a member function inherited from SCGeneralizationSymbol's grandparent, IGList. When removeAll is called on an IGList object, it removes all the graphics primitives from that object.
buildGraphics must clean up every time it's invoked because not only will it be called at construction time, but also whenever the appearance of the symbol changes. In the case of the SCGeneralizationSymbol, a change to the symbol occurs whenever its start point or endpoint changes. Next, buildGraphics creates a graphics bundle. Without going into the details, a graphics bundle determines the properties of the drawing, such as line color and thickness. Each of the symbol classes has a static IGraphicBundle attribute, holding the properties for all symbols of that class. Then begins the real work: calculate the start and endpoints from the generic SCElementView-inherited position and size attributes; draw a line from start to end; and add a polygon in the shape of a triangle to form the arrowhead. The arrowhead is drawn in horizontal position, and then rotated flush with the body of the arrow. It is in computationally intensive code like this where the advantage of retained graphics becomes apparent.
The buildGraphics routine for SCClassSymbol (Listing 5) looks very similar to the one for SCGeneralizationSymbol. SCClassSymbol::buildGraphics draws a rectangle with two horizontal lines to produce three compartments; then it draws the class name as a text string in the top compartment. The rectangle width is determined from the length of the name string and the font specified when the SCClassSymbol object is constructed.
The two other compartments are intended to show the attributes and the members of the class, respectively, but I have not yet implemented this feature. The prototype just leaves these compartments empty.
Hit Detection
The design calls for letting the user move class symbols with the mouse. This feature requires detecting the exact symbol that was clicked on; this is where the OCL's retained graphics really come into play.
Because the graphics engine remembers the structure of the diagram, it is able to correlate mouse position and graphics elements. IGList provides two methods for precisely this purpose. You can call topGraphicUnderPoint with a "hit point." A hit point is an IPoint object (basically a pair of x,y coordinates) that represents the mouse position. The IGList will then return the IGraphic that sits on this point.
To be more precise, the engine returns the graphic primitive that occupies (part of) a small square centered around the hit point. The size of the square is called the aperture size; by setting this size you determine how precisely the user must position the mouse above the point. An aperture size of one pixel requires the mouse hot spot to be exactly above the graphic very annoying when trying to hit thin lines. Setting the aperture size too big will turn the mouse into a kind of sledgehammer. Wherever you click, you'll hit almost everthing.
The aperture size is a member of IGraphicContext. In this prototype application it is set to a constant five pixels inside the SCDrawingArea constructor.
When there is more than one graphic primitive within the square, the IGList::topGraphicUnderPoint function returns the topmost one. Topmost in this context simply means the one that is last in the list. IGList also has a bottomGraphicUnderPoint method. When no graphic exists at the hit point, both methods conveniently return a null pointer. If you need all the graphics at the hit point, you can construct a special version of the IGList cursor and iterate over the list.
Recall that the primary IGList inside the SCDrawingArea contains separate IGLists for each symbol, because each symbol is a kind of IGList in itself. It is important to note that an IGList will return the entire IGraphic under the hit point. In other words, if the IGraphic is actually an IGList containing a host of other IGraphic primitives, one of which is under the mouse, these functions return a pointer to the entire sublist, not to the single element. In this case, when you click on an arrowhead (which is part of an inheritance symbol), and you query the IGList within SCDrawingArea, it returns the entire inheritance symbol, not just the arrowhead (the polygon). If you want the arrowhead, you must query the symbol, not the drawing area.
So, that's all there is to it. Get the position of the mouse and ask which graphic element is underneath. You'll get a pointer, just like that, and you can start manipulating it. Magic!
Life would be boring if everything were so simple. In fact, there is a serious string attached here. The code as presented is perfectly valid, and will definitely work. But as the number of symbols in the diagram grows, response times hit the roof. If you know how the graphics engine tests for hits, it becomes perfectly clear why. Whenever you ask the engine to test for a hit, it goes over the entire list, and draws everything it finds in memory. That is, the engine converts every element into pixels, and it examines each and every pixel to see if it's inside the aperture area. In effect, the whole diagram is redrawn every time you call topGraphicUnderPoint or its sibling an expensive undertaking.
This hit-detection method is a "feature" of the OS/2 Presentation Manager graphics engine, and it has been a long-standing source of frustration among OS/2 graphics programmers. Of course, people started looking for ways around it as soon as they discovered what was going on. One obvious way to speed up the search is to limit the number of elements to search. In my case, this could be as easy as iterating over the SCDrawingArea list, at each symbol checking whether the mouse position falls within the area occupied by the symbol.
This area is stored inside SCElementView as a position and a size. These two attributes form a bounding box around the symbol. A few tests will then quickly determine whether the symbol is a likely candidate. If the mouse pointer is outside the bounding box, forget it. If it is inside, you can call topGraphicUnderPoint on the symbol. If this function returns anything other than a null pointer, you have hit on the symbol.
If you look at the OCL source code (it comes free of charge with the Windows version of VisualAge C++), you will see that IBM does pretty much the same here. It will be interesting to find out what it will look like in the next release. One of IBM's goals in redesigning the 2-D drawing classes was performance improvement. Anyway, it will probably be difficult to come up with a generic high-speed hit-testing algorithm. All the algorithms I have seen like the one outlined above rely on knowledge of the objects being drawn.
My prototype does not implement any such mechanism, even though it already calculates the start and end points of the arrow from the position and size of the bounding box around the arrow. I'm confident that speeding things up is possible if necessary. Above all, I'm waiting to see what the next release will bring. One of IBM's goals for the redesign of the 2-D drawing classes was precisely performance improvement. Maybe the problem will have gone.
Moving Symbols Around
Once you know which symbol the user has selected, you can move it around. Moving poses no particular problem for the class symbol itself. It is actually a piece of cake, since most of the work is done by OCL. When the mouse button goes down, you detect the selected graphic and set a tracking rectangle around it (Listing 6) . When the button goes up, calculate the new position, clean up, and you're done (Listing 7) . (Note that the code uses the right mouse button for the drag operation. This is because I'm used to the OS/2 way.)
The most difficult part is in moving the inheritance symbols that are attached to the class symbols. I tried several approaches, but none felt exactly right until I turned to the notifier-observer pattern.
I've added two simple observers to the code. The first is called SCElementShapeObserver (Listing 8) . It watches the shape of any symbol on behalf of the SCDrawingArea that displays it. The observer enables itself to watch these shapes by registering itself with the symbol, which is also an IStandardNotifier. SCElementShapeObserver registers itself within its own constructor, by making a call to the symbol's handleNotificationsFor member function.
When a symbol's shape changes, the symbol notifies all observers, and the observer in turn calls on the area to refresh and redraw the symbol, with a little help from the symbol's buildGraphics routine. Therefore, any change in position or size will trigger a redraw automatically.
Similarly, I added two objects of a new class called SCClassSymbolInheritanceObserver for every inheritance symbol (the code is very similar to the previous, so it is not reproduced here). Each of these observers will track the shape of one of the class symbols to which the inheritance symbol is connected. When the position or size of a symbol changes, the observer is notified. It can then make sure that the connecting lines get repositioned accordingly.
OCL Upgrade Caveats
IBM's Open Class Library is an obvious candidate for implementing a drawing module, because of its multi-platform availibility and because it comes bundled with the IBM VisualAge for C++ compiler at no extra cost. However, the graphic classes in the OCL were due for an overhaul in the next release. The documentation pretty much says "don't use these classes for new designs." By the time you read this, VisualAge for C++ release 4.0 should be in the shops, but when I designed this prototype there was not even a beta available to see where it would be going. All I had back then was Version 3.0 for OS/2 and Version 3.5 for Windows 95 and NT (another reason to keep the prototype simple).
Conclusion
With this simple prototype up and running, I gained confidence that a full-fledged design tool was feasible. Indeed, work is well underway. I've tried to avoid designing around soon-to-be-obsolete features in the next release of VisualAge by postponing lots of minor implementation details. Thus, the prototype leaves a lot to be done, but it has all the basic mechanisms in place to add extensions as you wish. o
Luc Pardon has 18 years experience, first on IBM midrange platforms, later on PC (mostly OS/2 and C++). Two years ago, he started his own company, Skopós Consulting, to help others conquer the wonderful world of objects. He lives in Belgium, but loves all things Greek, especially music. When he's not attending a concert, he can be reached at 100111.1222@compuserve.com.