C++ Builder lets you get a lot of windowing action out of a small amount of code.
Introduction
BCB (Borland C++ Builder) is a great Windows 32-bit application building tool and generally a joy to work with. I encourage anyone struggling with other manufacturers' so-called "visual" development environments to give it a try. To date, not much has been written about BCB in CUJ, so I would like to fill in the gap a little.
BCB's most important feature is its Visual Component Library, VCL for short. VCL is a class hierarchy (very well designed and implemented, in my opinion) that encapsulates the Win32 API in a real object-oriented and intuitive manner. The level of abstraction and consistency is superior to that of MFC, but if you want or must, you can still use Win32 API functions.
During program development, VCL displays as a component palette on the screen. A developer can choose building blocks for the application from this palette, drop them on a form, and customize their properties and behaviors in the form designer. The entire user interface and some "behind the scenes" features can be built visually, while simultaneously giving full programmatic access to all components. Of course, the inner workings of the application need to be programmed "by hand" in C++ source code, for which BCB offers a lot of helpful classes.
BCB (version 4 as of this writing) comes with more than 100 ready-made components. In addition, it allows you to create your own custom components or add them from other sources. Many useful freeware and shareware components are available on the Internet [1]. Also, BCB can use Delphi components.
For an application I was working on, I needed an interactive drawing area, such as the type used in all kinds of vector-oriented, graphical-design programs. I could not find one suitable to my needs. There were some graphing components, but none seemed to support interactive drawing. So, I wrote my own custom component. I present the implementation here and share some of my experiences in creating a custom BCB component, which I call TDrawBox.
TDrawBox Requirements
The component should provide an interactive, scrollable, and zoomable 2-D drawing surface; work in real-world coordinates: centimeters, seconds, feet, etc.; support rubber-band marking (with scrolling), printing, copying to the clipboard, and exporting drawings to disk; and have a reasonable set of drawing functions that can be easily extended as needed. Furthermore, the component should have crosshairs if desired.
Although not necessary for many applications, the program for which TDrawBox was originally created needed a permanently visible and usable text cursor (a caret). I decided not to leave this up to the application, but rather included it in the component. (I will not discuss this feature further here.)
TDrawBox must know how to repaint the contents of the drawing surface when needed. Originally, I thought of incorporating some form of drawing memory into TDrawBox, so an application programmer would not have to worry about it. However, the requirements for such a drawing memory could be very different from one application to the next, so I decided not to do this. Thus, to use TDrawBox in an application, a programmer must supply a repaint routine, which is common practice in Windows programming.
Creating a Custom BCB Component
A component is a self-contained, visual or non-visual building block for an application, such as a button, listbox, html viewer, timer, color selection dialog, text editor, or MIDI player. All share the same basic interface because they are all ultimately derived from the TComponent base class. Every component must be derived from an existing component class, must have properties, events, and methods, and must be registered with BCB to be included in the component palette, so it can be manipulated at design time.
Ironically, writing custom components in BCB is a non-visual affair. At first, I thought of using a TImage inside a TScrollBox (two standard BCB components) as a basis for my TDrawBox component. GraphEx, a BCB sample application, does this. A TImage provides a drawing memory for free. However, this memory can only be handled as a bitmap, and I wanted vectors. Furthermore, a TScrollBox has a lot of properties and events that I did not want in my component. As with normal C++ inheritance, properties that are present in a base component class cannot be hidden in a derived component class.
Therefore, I decided to go one level up in the BCB component class hierarchy. Thus, the new component class TDrawBox is derived from TScrollingWinControl (the ancestor of TScrollBox). TDrawBox takes care of the scrolling and zooming and handles all interactions with the application. It contains a private instance of a second new class, TDrawBoxSurface, which is derived from TPaintBox and provides the drawing surface proper. The most important property that TDrawBoxSurface inherits from TPaintBox is the TCanvas member, which encapsulates the Windows GDI (Graphics Device Interface) API in a clever and programmer-friendly manner. A canvas is similar to a device context, but it takes care of all the ugly and error-prone bookkeeping for you. No more forgotten SelectObject(hdc, oldPen) and the like causing resource leaks! The header file of the TDrawBox component can be seen in Figure 1 and the source file in Figure 2.
Working with World Coordinates
In 2-D graphical design programs, a rectangular part of the real world is displayed in a window on a computer screen. In graphics programming terminology, that rectangular part of the world is commonly called the "window," and the window on the screen is called the "viewport." (This terminology can be a bit confusing. It would seem more intuitive to call the window on the screen the "window." Nevertheless, many graphics systems define the terminology as described above.) Figure 3 illustrates the difference between a window and a viewport. The window uses world coordinates (e.g., millimeters or inches) while the viewport uses device coordinates (generally pixels). A program has to be able to map between the two.
To map from world to screen coordinates, the Windows GDI provides a family of functions, such as SetWindowExtEx. However, TCanvas currently does not support these functions. I do not find this a serious omission, however, because these functions force a programmer to use integer world coordinates. In addition, having worked with GKS (Graphics Kernel System) in the past, I find the Windows notion of viewports and windows a bit difficult. So I made my own coordinate mapping routines for TDrawBox [2].
In TDrawBox, the viewport origin is by definition always 0,0 at the top-left corner. The ranges of the scrollbars determine the (positive) coordinates of the viewport's lower-right corner. TDrawBox provides a member function (a method in BCB component terms), SetWindowExtents, which tells the component the window's top-left and bottom-right coordinates expressed in world units. The direction of one or both axes can thus be reversed if necessary.
The part of the viewport that is visible on the screen depends on the application's window size (the "Windows window" that is); the rest can be scrolled into view (see Figure 3). The scrolling is transparent to the application. All TDrawBox functions and events use world coordinates. Zooming is a simple matter of changing the window and/or the viewport extents.
Creating Properties and Events
In source code, BCB component properties look exactly like variables. They give the application developer the illusion of setting or reading the value of such a variable, while allowing the component writer to hide the underlying data structure or to implement special processing when the value is read or modified. Properties are accessible at design time when they are declared __published. For example, when a developer sets the font or color of a component during design, he/she can immediately see what it looks like. To give such visual feedback, I have written a simple repaint function that is executed only during design to give the developer information on how some of the properties affect the appearance of the TDrawBox. Incidentally, BCB makes no distinction between a main window and a dialog box; both are designed exactly the same.
In BCB terminology, an event is a special property that invokes code in response to a run-time occurrence, such as a mouse click or a keystroke. The code that is executed when an event occurs is called an event handler. TDrawBox needs event handlers that use world coordinates instead of device coordinates. This is the main reason for not using TScrollBox; its already-defined OnClick event passes the mouse coordinates to the application in device units. It was therefore necessary to define my own event types. Some event handlers are patterned after those of TScrollBox and some, notably OnMarked, are new.
BCB uses closures to implement events. A closure is a special pointer type that points to a specific method in a specific class instance. The type of the event property must be a closure type. Because C++ has no support for closures, this is a BCB-specific language extension. BCB defines closures for all of its standard events. Defining my own events led to rather illegible declarations like the following, but they work:
typedef void __fastcall (__closure *TDrawBoxMarkEvent) (System::TObject* Sender, double Left, double Top, double Right, double Bottom); __property TDrawBoxMarkEvent OnMarked = {read=FOnMarked, write=FOnMarked};The result of this code is that when a programmer double clicks on the OnMarked event box in the form designer's object inspector, code is generated as follows:
void __fastcall TMainForm::DrawBoxMarked (TObject *Sender, double Left, double Top, double Right, double Bottom) { // Event handling code to be // added here }TDrawBox calls this function whenever a region is marked. The coordinates of the marked region are automatically passed to the application in world units, because TDrawBox handles the coordinate mapping for you.
Printing, Exporting, and Copying Images
TDrawBox can export (part of) its drawing area to a file or copy it to the clipboard. The image can be exported both as a bitmap and as a metafile. As can be seen in the TDrawBox::Save function (see Figure 2), BCB directly provides most of the support for this through its TBitmap, TMetafile, and TClipboard classes. Printing is a bit more elaborate, but using the BCB TPrinter class and guidance from The BCB HOW-TO [3] make it a relatively easy task.
For all these actions, I make TDrawBox create a memory metafile first (see TDrawBox::CreateMetafile, Figure 2). Then the Canvas property of TDrawBox is temporarily set to the metafile's canvas, and TDrawBox's OnPaint handler is called. This repaints the image to the metafile. After resetting the Canvas to the TDrawBoxSurface canvas again, the metafile is either saved to disk, copied to the clipboard, or drawn on the canvas of a bitmap or the printer. All these operations are supported directly by the BCB TBitmap, TMetafile, TClipboard, and TPrinter classes.
The printing is done in a separate thread. This was not strictly required, but I wanted to do it as an exercise in multithreading with BCB. To my surprise and joy, by using the examples in The BCB HOW-TO [3], this went fine on the first try!
Marking with Scrolling
In programs like MS Paint, it is not possible to mark beyond the visible part of the drawing. However, there are programs that do scroll the drawing area during marking when the user drags the mouse outside the drawing surface. I wanted to incorporate this behavior in TDrawBox. This turned out to be no trivial task. While it is possible to capture the mouse and receive all mouse messages until the capture is released again, the mouse sends messages only when it is moved or when a button is pressed or released. Simply keeping a button depressed without movement does not generate any messages. So how would TDrawBox know when to scroll?
After some experimentation, I solved this problem by adding a TTimer component to TDrawBoxSurface. The timer is started whenever the left mouse button is pressed and stopped when it is released again. The OnTimer event calls TDrawBox::ScrollIfNeeded (Figure 2). This function determines the position of the mouse cursor on the screen. If it is not inside the visible rectangle of the drawing surface, the surface is scrolled with an amount that is proportional to the distance between the mouse cursor and the nearest edge of the surface. Keeping the mouse near the edge makes scrolling slow and moving it further away makes scrolling faster (similar to MS Word or Excel). There may be other solutions, but mine results in a smooth scrolling action during marking.
DrawBoxDemo, an Example Application
I built a small example application, DrawBoxDemo, to show most of the features of TDrawBox. The full source code for this application is available electronically (see p. 3 for downloading instructions). Figure 4 shows DrawBoxDemo in action. It supports a few common drawing functions, all triggered by an OnMarked event handler that TDrawBox calls whenever a rectangle has been marked. It also can save and load drawings in its own format. This serialization is handled by the drawing memory (see below).
DrawBoxDemo can also export (part of) the drawing as a bitmap or a metafile and copy (part of) the drawing to the clipboard. TDrawBox takes care of these actions completely. TDrawBox also handles all printing, either of the entire image or of a marked region. The OnMouseMove handler shows the position of the mouse cursor or the crosshairs in world coordinates at the bottom right.
DrawBoxDemo did not take much time to develop. TDrawBox takes care of most of the details, so things came down to designing and "wiring" the user interface (which is really fun in BCB) and providing a drawing memory that could handle the repaint routine and also serialize the drawings to disk.
The Drawing Memory
I designed the classes TDrawList and TDrawItem as an example of a very simple drawing memory. Every item in the list knows how to load, save, and draw itself. The header file can be seen in Figure 5 and the source file in Figure 6. TDrawList uses an STL list container to store all TDrawItem objects. The implementation is rather straightforward, but there are two features that I would like to discuss.
First, the TDrawList object can store itself in a disk file and read itself back in. The items in the list are all derived from an abstract class TDrawItem. The Save function of every derived class, like TLineItem, always stores an identifier first before storing its data. To restore an item to an instance of the correct derived class, TDrawItem uses a variation of the factory method or virtual constructor idiom. The static public function TDrawItem* TDrawItem::Load(ifstream &ifp) first reads the identifier from disk and then calls the appropriate constructor (e.g., return new TLineItem(ifp);), which reads the item's data.
Second, the Edit|Delete function in DrawBoxDemo provides an example of drawing object manipulation. The OnClick handler of the demo program searches the list for the first object with a bounding rectangle encompassing the clicked point. The selected object is highlighted with four corner handles. If there is more than one object on the same spot, a second click there selects the second one and so on. This type of circular object selection is common practice in design programs [2]. The selected object can then be deleted. Other object manipulation could be added in the same way. Please note the difference with the Edit|Copy function as used here. The latter shows that TDrawBox can copy the entire drawing or the marked rectangle (but not the selected object of which it has no knowledge) to the clipboard in metafile format. Of course, you could add object manipulation here as well.
Conclusion
Overall, developing TDrawBox has been a real pleasure. There were only two frustrating things. First, I was left wondering why the arrow keys did not trigger the OnKeyPress event. Fortunately, I found the solution to this on the Internet [4]. Second, VCL's inner workings seem to stand in the way of font color changes during the handling of one repaint message (see the source code for TTextItem::Draw in Figure 6).
When thinking about improvements to TDrawBox, three things come to my mind. First, the font handling could be improved in at least two ways. The standard BCB TFont class cannot change a font's width independently of its height, so correct text scaling is impossible. For TrueType fonts, Charles Petzold [5] describes a way to create fonts in an easy manner with independent widths and heights. I plan to incorporate his EzFont in a derived TFont class and use that in TDrawBox in the future. Also, as noted above, you have to cheat a bit with the BCB VCL to display more than one font color during the handling of one repaint message. This should be handled transparently by TDrawBox and/or the new TFont derived class and not be left to application code.
The second improvement would be an internal clipping routine. Currently, when copying a marked region as a metafile, the entire image is copied with only a clipping region applied to it. Objects that fall entirely outside of the marked region are not displayed, but are present and do occupy space in the metafile nonetheless.
The third improvement would be the addition of a print preview function. This might be done by incorporating one of the public domain, print-preview components (e.g., one of the two that use metafiles to do the previewing and that would possibly integrate smoothly with TDrawBox's use of metafiles in its export routines [6]).
I hope readers will start using TDrawBox. If you think of other improvements, or perhaps make some yourself, please let me know. I hope my story will elicit more articles about BCB programming in CUJ in the future. I for one would welcome these very much.
References
[1] Possibly the biggest Delphi and BCB component collection on the Internet is The Delphi Super Page (http://SunSITE. Informatik.RWTH-Aachen.DE/delphi/ and many mirror sites).
[2] In their excellent book, Power Graphics Using Turbo C (John Wiley & Sons, 1989, ISBN 0-471-61909-4), Keith Weiskamp, Loren Heiny, and Namir Shammas describe window/viewport transformations in Chapter 5 and drawing object selection in a CAD program in Chapter 12.
[3] In my opinion, the most useful book about BCB is Borland C++ Builder HOW-TO by John Miano, Tom Cabanski, and Harold Howe (Waite Group Press, 1997, ISBN 1-57169-109-X). Chapter 10 covers printing and Chapter covers 12 working with threads.
[4] At Harold Howe's website BCBDEV.COM there are lots of very useful FAQs. http://www.bcbdev.com/faqs/faq78.htm describes how to catch the arrow keys inside a key press handler and http://www.bcbdev.com/faqs/faq34.htm describes ways to reduce flicker in TPaintBox components.
[5] Charles Petzold's massive and authoritative work, Programming Windows 95 (Microsoft Press, 1996, ISBN 1-55615-667-6), contains the source code for the EzFont system in Chapter 4.
[6] Ryan Peterson's print-preview component, RyPrinter, is available as ryprev32.zip from [1]. John Biddiscombe has made an enhanced version, ppreview.zip, which is also available from [1].
Anneke Sicherer-Roetman started her career as a research chemist and holds a doctorate in chemisty. She then became a Unix systems manager and a programmer, and practiced both occupations for more than ten years. Last year she became a software engineer in a company that produces software and instrumentation for behavioral research (Windows 95/NT platform). She has also run a small educational software company together with her husband for 13 years. She has been coding in C for 13 years and in C++ for 7 years. She can be reached at sicherer@sichemsoft.nl or a.sicherer@noldus.nl.