Graphics


Adaptable Dialog Boxes for Cross-Platform Programming

Christopher Kempke

Finally, key technologies converge to make for more portable user interfaces.


In this article, I present a framework for building dialog boxes that adapt to the look and feel of their platform. This method also helps with a few related problems: specifying cross-platform resources and handling dialog size changes due to localization. I’ll use a combination of XML, automatic layout, and run-time dialog creation to give you most of the benefits of platform-specific resources, without the associated pain. Source code with an implementation of the layout engine for Mac OS 9.1 (“Carbon”), Mac OS X, and Microsoft Windows can be downloaded from the CUJ website at <www.cuj.com/code>. You can use this code as is, or as a starting point for your own more complete implementation.

Look and Feel

Given any two GUI-based computer systems, it’s likely that dialogs and windows displayed on them have a different look and feel. Dialogs vary in the size, type, and spacing of controls, as well as the position of those controls within the dialog. It is often important to make your program feel “native” on the platform in question. The Apple Macintosh, in particular, has a user community that is particularly sensitive to UI compliance — enough that violating the expected appearance can affect a program’s commercial success.

Platforms are continuing to diverge in their layout styles; each vendor wants to differentiate its interface. This is most visible on the Macintosh, where Mac OS X and the “Classic” Mac OS have dramatically different layout styles. It’s also visible in Windows XP’s new thematic interfaces versus the older Windows models (and the still older Windows 3.1 guidelines). For this article, I will call the Mac OS X interface by its proper name, “Aqua.” The older Macintosh interface is called “Platinum.”

Figures 1, 2, and 3 show the same dialog on Windows XP, Platinum, and Aqua, respectively. Note the subtle (background patterning, button roundness, and 3-D effects) and not-so-subtle (size, spacing, color) differences between them. You can’t see it on paper, but the blue Aqua button slowly pulses blue to clear to indicate that it’s the default.

Resources: Pro and Con

The customary way to define a dialog box’s layout is to define a resource, usually using a visual tool. Resources have a number of advantages: they’re simpler than C/C++ code, there are tools to generate them, and they can be created separately from the application code. On the Mac, they are easily modified after linking the executable. This is important; the person who designs the dialogs may not be a programmer. In the case of localization (which usually requires dialog layout to change), the localizer is almost never the original system programmer.

The problem with platform-specific resources is typically just that: they’re specific to a platform. The Mac, Windows, and X Window System resource formats are all different, despite superficial similarities. Windows uses a different base metric for dialog layout (the DLU, or Dialog Layout Unit, whose size is based on the current system font) than the others (which use pixels). But most problematically, resources specify the exact location and size of each dialog element or control.

This size and spacing information, which I’ll call “metrics,” is a significant impediment to cross-platform use in several ways: platforms differ in the size of standard controls, their location or alignment with respect to the dialog, even the spacing between them. Dialogs tend to be laid out for English, which is a fairly compact language. When localizing for other languages (particularly German and Scandinavian languages), controls often must be enlarged to prevent clipping of the text, even for the original platform.

Solutions: Theory and Background

There are three basic systems to implement: specification (specifying the contents of the dialog to the application), metrics (determining the size of controls and the spaces between them), and layout (turning the specification plus metrics into an actual dialog.)

Specification isn’t too hard. All of the platforms I’ve mentioned allow you to create a dialog dynamically at run time (i.e., using code instead of resources). The method I will use involves creating my own (cross-platform) resource format, which is then implemented in terms of the dynamic routines (see sidebar). This allows me to maintain most of the benefits of resources.

Specification, therefore, becomes a problem of just picking a data format and parsing it. Historically, this has been done dozens of times, resulting in such things as UIL (User Interface Language) and the text resource formats for Mac and Windows. Today, there is a standard “universal data format” in the form of XML. The code provided with this article doesn’t require an XML parser, but you’ll probably want to locate one if you’re going to use this technique; a little time writing a parser for XML-based dialog descriptions will save a significant amount of dialog-specific code (reducing several dozen lines of code per dialog to just three). There are a number of commercial, open-source, and freeware parsers available.

Most interfaces, including Platinum, Aqua, and the Microsoft Windows family, publish layout guidelines, which specify the minimum dimensions and spacing for controls, windows, and dialogs on that platform. These will provide a starting point for solving the metrics problem. Note that some hand tuning will be necessary. On most platforms, the “real world” has standardized on metrics somewhat different from the documented ones. I have created a few metrics member functions (part of the Layout class described below), which can be queried for sizes and relationships between controls. These member functions will embody the layout guidelines — either from the documentation, experience, or personal preference. You may like your controls a little looser or tighter.

Layout is the real challenge. Since the sizes and positions of controls will need to vary between platforms, I can’t specify exact pixel or DLU locations for controls. What I want to specify is the “spirit” of the dialog: which controls should be grouped together and overall visual flow (e.g., which control groups should be above or below each other).

HTML does this. The author specifies the content and general flow, but the actual appearance of the document depends on the browser, window size, user-specified fonts, and a myriad of other factors. The document-formatting language TeX takes this even further; it allows you to specify the layout of text and graphics to an extremely detailed level. TeX offers two concepts that I’ll steal for my implementation: boxes and fills.

You can describe most common layouts as a hierarchy of nested boxes. Boxes come in two sorts, the vertical box (whose children are stacked vertically), and the horizontal box (whose children are horizontal). As TeX does, I’ll shorten vertical box to vbox and horizontal box to hbox. Figure 4 shows a relatively simple dialog broken down into boxes, blue for vboxes, red for hboxes.

By requiring that my layouts be constructed of nested boxes, I give up a little bit of flexibility. But I’ve rarely encountered dialogs that aren’t easily described this way; the exceptions don’t often occur in real layout design.

The second concept I’ll steal from TeX is the idea of a fill. Fills are pseudo controls that expand to consume empty space in a box. Fills expand in the direction of their parent box. That is, vbox fills expand vertically; hbox fills expand horizontally. If there are multiple fills in a box, they split the empty space equally.

Fills are used for alignment. For example, placing a fill on the left side of a control forces that control to the right. A fill on each side of a control will center that control. Horizontal fills are equivalent to “quad leaders” in high-end typesetting (gaps or lines that expand to evenly space portions of a line of text). In Figure 4, the fill in the bottom hbox forces the buttons to their proper Windows position, on the right.

In Figure 5, I show the same dialog laid out for Mac OS X’s Aqua. (The centered buttons are a giveaway.) The fill labeled “Fill (Aqua)” does the centering. You will often want to “tag” a control or fill with a platform in your XML resources — the parser can then ignore non-current platform items. This is easy to add as an attribute on the fill in XML.

The last thing needed for simple layout is the ability to leave a hard (non-expanding, non-contracting) space. This leaves an empty area of predictable size in the layout. You might do this for aesthetic purposes, or because your application will draw into the empty region at a later time.

Solutions: Implementation

To implement this, you pick a convenient, cross-platform format (such as XML), and define your dialog layout in that. For example, consider the shirt sizes dialog in Figures 1, 2, and 3. Listing 1 shows a possible XML layout for this dialog.

I create this XML as an external file, but I don’t want to ship it that way. Both Macintosh and Windows allow you to create a “custom resource” from an external file with a syntax similar to an #include directive. Even though the sample code doesn’t do XML parsing, I’ve included some of these layout definitions and built them into resources as an example in the project provided at <www.cuj.com/code>. See the .rc file for Windows, and the .r file for the Mac code.

For the provided code, the metric functions are built into the Layout object, as the two member functions GetControlTypeMetrics (for sizes) and GetDialogSpacing (for inter-control spacing information). Both are just large switch statements, implemented in LayoutMac.cpp or LayoutWin.cpp as appropriate for the platform. Depending on your needs, this could be made much more elaborate (see “Closing Notes” below). GetDialogSpacing returns a horizontal and vertical measurement for the specified type of gap, but GetControlTypeMetrics is more interesting. It returns an AOControlMetrics structure.

Listing 2 shows struct AOControlMetrics. A few of these members require explanation. usesLabel indicates whether or not the text applied to the control (if any) affects its size. This is true for most controls with labels. minWidth is a minimum width, regardless of the label size, primarily for aesthetics. widthExtra indicates the native size of the control with a zero-length label; for checkboxes and radio groups, it’s the size of the square or circle on the control, plus any margin. Finally, spacingHeight is the amount of height added for each additional text line in a control whose text wraps to multiple lines. You can modify these values to achieve a look you like.

The next step is to build an internal format for the dialog control information. This information is in two parts. The actual controls are in LayoutControlList. PortDialogInfo contains layout information as a tree of nodes that correspond to the XML tags.

LayoutControlList is a vector of the dialog’s controls. For layout purposes, the relevant fields are here. (The i stands for “instance variable,” which dates me to the ancient object Pascal days.)

Listing 3 shows struct LayoutControl. The type is the type of the control. (There’s a set of constants in Layout.h). The command ID is the code that maps the particular dialog item to its corresponding entry in the layout. The title and or text value are used to determine the size of the control, and iFixedWidth and iFixedHeight are booleans, which indicate whether the control can be resized by the layout process. You would set iFixedWidth for controls such as edit text, where the size of the control itself is independent of its contents or title. For fixed width or height controls, the iRect structure contains the initial size; it’s ignored for non-fixed size controls. The layout process will change the iRect to reflect the control’s actual coordinates. The other fields in this object are relevant to dialog control, not layout.

PortDialogInfo is simpler, as shown in Listing 4. This determines the title, height, and width of a dialog as a whole. iLayoutInfo contains a tree of layout nodes, and iWidth and iHeight will be set by the dialog after layout of the controls.

Finally, let’s look at LayoutNode itself. Listing 5 shows struct LayoutNode. The control member here either contains a command ID that maps back to the command ID in the control list or else contains one of a number of kLayout<xxx> constants defined in Layout.h. x and y are the position of the element; width and height are its dimensions. They’re stored separately here, rather than in a rectangle, because they’re accessed independently by the layout code. framed and label apply only to group boxes (those thin lines that group various controls in a dialog together). Finally, children is a list of the node’s children. The last three are only applicable if the node is one of the box types (kLayoutHBox or kLayoutVBox).

The demo application provides some helper functions to build layout nodes and control lists by hand; but in a real application, you would build these directly from the XML resources discussed above. I’ve built them by hand for the demo. Don’t be dismayed by the amount of code in the BuildDialogX functions. With a resource-based loader, the code for loading and displaying a simple dialog reduces to three lines.

Listing 6 shows the code fragment necessary to display a simple dialog.

The Actual Layout Process

After all this setup, the layout process itself is relatively simple. The top node of a layout is always an hbox or a vbox, because they’re the only nodes with children.

The layout code itself is in Layout::LayoutControls in LayoutAll.cpp. This happens in two steps.

First, MeasureAllControls measures all the fixed controls. This effectively sets the height and width fields of the control, but not the position. For each control, you call the metrics object to determine the size of the control, taking into account platform measurements and the text of the control, if any.

The only notable issue is long static text controls. It’s not desirable for the dialog to expand horizontally to fit very long text. Instead, it’s better to specify the control as a fixed width and grow vertically by adding additional lines. Layout::GetControlTypeSize does this; it’s implemented separately for each platform because of the messy platform-specific device contexts needed for text measurements.

The second step is more interesting. Layout now has correct sizes for all of the fixed controls in the dialog. Dialogs minimize space; the code will push the controls as tightly together as the platform metrics allow, wrap boxes around them as tightly as possible, and so on, to get the smallest dialog that follows the layout specified.

The traversal itself (in Layout::LayoutBox) happens in three parts. First, it recursively calls LayoutBox on all child controls. From the resulting list of children’s heights and widths, LayoutBox can determine the height and width of this box. For vertical boxes, you add up all the heights of your children, plus the spacing around and between them, to determine the total height of the box. The width is just the largest child’s width, plus any spacing around that control. Horizontal boxes use the same algorithm in the opposite dimensions.

At this point, LayoutBox knows the box’s height and width. Since it’s likely that some of the box’s children are smaller than the box itself, it calls ExpandFills on all of its children. ExpandFills just computes the amount of empty space in a given box and divides it amongst the fills.

All this is done assuming that all controls and boxes are at the origin. The last step is to move each child into its proper position. MoveChild is recursive, so previously laid out child boxes get moved as a unit.

Overall, the flow looks like this:

for all children
   recursively lay out boxes;
compute height and width to contain 
   children;
let immediate children expand their 
   fills, if any;
move children to their new relative 
   locations

Windows

The source code is designed for laying out dialog boxes, but I believe that with some additional work it could be used for the layout of resizable elements such as windows, as well. Remember that for the dialog box, the layout code compresses everything together as much as possible, before building the next outer box.

Contrast this approach with laying out a user-resizable window. There, the space has been determined by the user’s manipulation of the window, and the metrics are specifying minimum distances rather than actual ones. Implementation-wise, this would happen at the ExpandFills step of Layout::LayoutBox. Instead of passing in the tight-packed height and width of the controls, you’d instead pass in the height and width of the window itself. Then, the fills would expand to take up the extra space. This would likely require a second recursive traversal of the tree so that extra space was divided among fills at different levels of the layout, a situation which does not arise for dialogs.

Closing Notes

I’ve been using a variation of this code for about a year and a half. Aside from the various influences I’ve talked about, I developed the code in a vacuum — I write a cross-platform framework, and maintaining resources on all platforms was becoming tedious. In the last few months, I’ve encountered several others who have independently developed roughly the same technique, in roughly the same time frame. I find it interesting that the conjunction of XML as a standard data formatting language and an emphasis on cross-platform/multilingual application development has caused the same solution to be reinvented multiple times. The similarity of these solutions lends me a certain confidence in the model.

It’s unusual to consider dialog layout in a vacuum, separate from its behavior and implementation — and I didn’t. The source code on the CUJ website (<www.cuj.com/code>) is derived from a larger application framework. It gives you a little extra support for displaying and controlling the created dialogs, instead of just laying them out. This extra support code may or may not be useful in your own applications.

Christopher Kempke has been working with C++ for ten years. He holds a Master’s degree in Computer Science from Oregon State University. For the last seven years, he’s been developing publishing and page-layout software in C++ for Multi-Ad Services, Inc. He also publishes an independent, C++ cross-platform application framework called CroPL, which supports single-source Windows, “Classic” Mac OS, and Mac OS X software development. He can be reached for comment at ckempke@mac.com.