A comic-style balloon Callout control
Thiadmer develops multimedia and system software for his company, CompuPhase, based in the Netherlands. He can be contacted at http://www.compuphase.com/.
When you need to display a message that refers to another object, window, or user-interface element, a "callout" with an arrow to its target or a comic-style balloon are a better option than a message box. With this in mind, I designed a balloon-style Windows control that is sufficiently configurable to suit many purposes. I call this control a callout rather than a balloon to reduce the chance of confusion with balloon tooltips that recent versions of Microsoft Windows provide. Figure 1 shows a few of the layouts that the callout control supports.
A callout is not a tooltipit is not attached to other controls (and it does not subclass other controls). You may have multiple callouts visible at a time, as well. As an added bonus, you can add other controls (buttons, check boxes) inside a callout. A callout is similar to a tooltip in the sense that it adapts its size and position to its contents. A callout is not a message box, either, because it does not enter a modal loop; therefore, it is better suited to display informational messages without disrupting the user's current activity. The tail of the callout, pointing to the object or icon/button that the message applies to, lets you write shorter messages in many circumstances.
I designed the callout control to be just like other standard controlsyou create it with CreateWindow() and you configure the control by sending messages to it. The control supports standard messages such as WM_SETTEXT, WM_SETICON, and WM_SETFONT, plus a set of control-specific messages. For conformity, many style attributes, such as the border width and the radius of the balloon corners, are set with messages, too, rather than with style bits in the CreateWindow() call. There are only two basic styles for the callout tail: a triangular pointer or a sequence of three aligned ellipses. More variations are obtained by changing the dimensions of the tail or other attributes.
Using the Callout Control
The callout control reformats and repositions itself at any time that its contents or style changes. In typical usage, you set the text (and other contents) and the style of the callout while it is hidden, then show it.
The source code for the callout control can be compiled to a separate DLL. In that case, all that is required to create a callout is to load the DLL via LoadLibrary() and call CreateWindow() with the class name Callout. The DLL registers this (global) class name automatically when it is loaded. If, instead, you compile the source code for static linking to your application, the application must call CalloutInit() before it creates its first callout control. If so desired, the application can also call CalloutCleanup() at exit point to unregister the Callout class.
The window style used in CreateWindow() can be WS_CHILD if you want the callout to be snapped inside its parent window, but usually WS_POPUP is more appropriate. Style bits like those for a border and a caption are redundant, as the callout control removes them on creation. The window position and size parameters are also ignored; the callout control positions and sizes itself based on the text and the coordinate pair that the "tail" of the callout points to. This coordinate pair is called the "anchor" point, and you can set it by sending a message to the control. The callout chooses an appropriate position based on its attributes and the anchor point. Instead of sending messages, as in Example 1, you may also use wrapper macros as presented in Example 2.
The callout control recalculates its shape and position and repaints itself every time that its style or contents change, with two exceptions. First, if the control is "hidden," the routine skips a few steps in the calculation/repaint sequence. Second, nearly all custom messages that change the attributes of the control take a "repaint" parameter, and if this parameter is zero, the recalculation and repainting steps are suppressed (this parameter was inspired from the WM_SETFONT message). I think that it is best to create the callout control as hidden and set all attributes before showing it. If you have a sequence of attributes to set, you can avoid redundant calculations by setting the repaint parameter to True for the last attribute setting and to False for its predecessors.
Table 1 gives an overview of the standard and custom messages that you can send to the callout control to change its appearance or behavior. There are wrapper macros for each of these messages. Figures 2 and 3 may also be illustrative in describing many of the custom messages.
The control hides itself when users click in the callout with the left mouse button or after a timeout, which you must have set earlier. You can change this behavior by intercepting the WM_NOTIFY message that the control sends to its parent or owner window. The callout control sends a notification for the events NM_CLICK and NM_RCLICK, plus the custom event CN_POP, for when it is about to be automatically hidden. The default action of the callout control is to hide itselfit does not destroy itself.
To add additional controls in the callout, you must first reserve room in the callout for the controls. The callout assumes that you will place these controls below the text. That is, you reserve vertical space at the bottom of the text box area. The widest or right-most control also sets the minimum width that the text box should have. After setting all the other styles, your program can query the balloon text box rectangle and position its controls relative to that rectangle; see Example 3. The callout control already handles the coloring for the added controls (it intercepts the WM_CTLCOLORxxx messages), but it does not set the font for the controls. Typically, you will want to use a different font than the System font for the embedded controls; Example 3 copies the font for the balloon control itself to the added buttons.
Figures 2 and 3 show the anatomy of a callout control, where the dimensions of various attributes are controlled by the messages in Table 1. The size of the text box is calculated from the text, with the minimum width and the extra height for embedded controls taken into account. The icon area (Figure 3) is only present when you set an icon. Some settings are "hard"; if you set the border thickness to be four pixels, the border will be four pixels thickno more, no less. Other settings, such as the tail slant angle, are "soft": the callout control may change these attributes if it does not fit in the display, otherwise.
As you can see in Figure 2, the tail does not necessarily "touch" the anchor point: There is an optional vertical offset of the tail to the anchor. The offset is useful if you want to avoid that the tail overlaps part of the control that it points to. The default value for the tail offset is zero, though.
In the first step in calculating the size and position of the callout, the control calculates the size of the text box using the DrawText() function. It attempts to format the text so that the width of the text box is at most four times its heightto create aesthetically pleasing text boxes. The way that it does this is to first call DrawText() with the text string and only the flag DT_CALCRECT set. If the calculated rectangle is too wide, the callout control adjusts the rectangle's width based on the area (width times height) of the rectangle and makes a second call to DrawText(), now with the flag DT_WORDBREAK set in addition to DT_CALCRECT.
The DrawText() function only allows for minimal formatting (word wrapping and line breaks). If you wish to plug-in another text-formatting engine in the callout control (HTML formatting seems a popular request), you can set it by sending a CM_SETDRAWTEXTPROC message to the control. The only requirement is that the replacement is compatible with DrawText() at the call level, including the functioning of the DT_CALCRECT and DT_WORDBREAK flags.
Once you know the width and the height of the text portion, you add space for an optional icon and margins for the balloon. Then the callout control goes looking for a suitable location for the balloon part of the callout. When you position a callout, you set the coordinates of the anchor point, the point that the stem or tail of the callout points to. The control initially puts the balloon part at a fixed offset from the anchor point, depending on the shape and size of the tail. However, the balloon part is always snapped inside the bounds of the display, and the callout control also tries to avoid overlapping other callout controls.
When there are multiple monitors attached to a system, you can no longer assume that (0,0) points to the upper left corner of the display and use GetSystemMetrics() to get the working area of the display. Starting with Windows 98, Microsoft added multimonitor support functions to the Windows API. In a typical configuration, the display areas of the monitors are combined into a virtual screen (with a width that is the sum of the widths of the monitor areas). To make it easier to port existing software to become multimonitor aware, Microsoft also provides the file MULTIMON.H. Using MULTIMON.H has as an added bonus that it automatically provides substitute (stub) functions for those versions of Microsoft Windows that do not support multiple monitors.
To add multimonitor support, you need to include MULTIMON.H in your source files. In one of these files, you should declare the macro COMPILE_MULTIMON_STUBS before including the file so that the substitute functions get defined. From that point on, the multimonitor functions like MonitorFromPoint() and GetMonitorInfo() are available. As a side note, the function GetMonitorInfo() returns a "monitor rectangle" and a "work rectangle." The monitor rectangle describes the coordinates of the display area of the monitor in the larger virtual screen; the work rectangle is the monitor rectangle minus any task bar on that monitor.
To avoid overlapping sibling controls, the callout control uses the Windows function FindWindowEx() to walk through all windows of the Callout class. All example code for FindWindowEx() that I have come across uses FindWindowEx() to get a handle on child windows, but the function can also locate any nonchild (popup, overlapped) window. In experimenting with FindWindowEx(), I noticed that the order in which FindWindowEx() returns handles to the callout windows was consistently the inverse of their creation order, but the callout control does not rely on any order.
When the callout control finds that in its preferred situation it overlaps another callout control, it moves itself to the left or the right, depending on which direction gives the shortest displacement. If that move causes another conflict, the callout toggles the vertical alignment (from above to below the anchor point or vice versa) and tries again, perhaps after another horizontal movement. To keep the tail pointing at the anchor point after moving horizontally, the callout first changes the position where the tail joins the balloon. If that is not enough, the callout changes the slant of the tail. It is quite possible that all these attempts to find a good fit fail, especially because of the additional requirement that the callout control must fit inside the display area of a single monitor. So in some (crowded) situations, a callout may overlap another callout, but at least it tries to avoid that.
A final matter is whether the callout is a child window or a popup window. A child window is clipped inside the client area of its parent, a popup window can extend beyond the parent window's rectangle. For the positioning algorithm of the callout control, it therefore becomes important to check whether the control has a parent windowa popup window has an owner window but (usually) not a parent. Here, the Windows API becomes confusing. In spite of its name, the GetParent() function may return the owner window instead of the parent window. According to the oft-cited Knowledge Base article Q84190, there is not a good way to find the real parent of a window in all circumstances. But actually, that article is out of date because Windows 98 introduced the function GetAncestor(), which does not mix up owner and parent windows. I was not prepared to give up Windows 95 yet, so the callout control implements its own fix for GetParent(), called GetChildParent(), that checks for the "child window"-style flags before calling GetParent().
After the size and position of the balloon part of the callout are determined, the control can finally calculate the size and position of the complete window, taking the callout tail into consideration.
To create a nonrectangular window, I use window HASH(0x832da8). Creating such a region is fairly simple because Windows provides both the functions to create basic HASH(0x832da8) and the functions to combine HASH(0x832da8) (union, intersection, and others). When I started implementing the control, I thought that the most difficult part would be to draw the outline of the callout exactly along the edge of the region. Fortunately, it turns out that the Windows function FrameRgn() does exactly that in one simple call.
Painting windows with a window region is simplest if the window does not have any nonclient areas, such as a border or caption. When you create a callout window, the callout control checks for these flags and removes them if set. If you wish to create a callout window without a border, you must set the border width to zero by sending a message, rather than by adjusting the window style.
Callout.c (available electronically; see "Resource Center," page 5) is the implementation of the callout control. Custom messages, wrapper macros, and other constants are in the include file CALLOUT.H. There is a BUILD.BAT batch file in the source archive to compile it into a DLL. The batch file has command lines for Borland and Microsoft C compilers; you may have to edit it to uncomment the lines for your compiler before running it.
The file MULTIMON.H is part of the Win32 Platform SDK, but you can also get it (in a smaller download) from the archives of Microsoft Systems Journal. The June 1997 issue carries a detailed article on multimonitor support, which you can still read at http://www.microsoft.com/ msj/0697/monitor/monitor.aspx.
The two Haiku error messages at the bottom in Figure 1 are by David Carlson and David Dixon, who wrote them for the Haiku error messages contest for Salon .com (see http://archive.salon.com/21st/ chal/1998/02/10chal2.html).
Window HASH(0x832da8) are not the only method that allows for nonrectangular windows. Windows 2000 introduced "layered windows" with a transparent color key. Layered windows are easier to use than window HASH(0x832da8), but they require Windows 2000 or XP. Apart from constructing a region from primitive shapes, a tool like RegionCreator (found at http://www.flipcode .com/) makes a region from a bitmap with a specific color set as transparent.
Despite the degree of customization that the callout control already provides, I foresee that most future improvements will focus on adding more visual styles and increasing the flexibility of the control. I am confident that the current implementation is useful as it is, and that it will be easily extendible if it needs to.