Windows Programming


Quick MS-Windows Dialog Design

Adrian Barbu


Adrian Barbu has an M.S. in software engineering and is a project manager at Goto Informatique, a French software company specializing in communication software. He can be reached via e-mail at ab@goto. fr.

Introduction

I've found that, one way or another, dialog design and coding make up some of the most expensive parts of MS-Windows programming. If a programmer wants to do fast development, he will have to marry his program to a heavy application framework, taking in several hundreds of kilobytes of code, and losing lots of design liberty in the bargain. On the other hand, programming without a framework requires repetitive dialog procedure design, with hopelessly boring code to move data back and forth between program variables and dialog controls. I finally got so tired of both approaches that I rolled my sleeves up to see if I could come up with something better. The result is a C++ class library (QUICKDLG), providing access to string, boolean, or enumeration data, and capable of presenting it in an MS-Windows 3.1x dialog box for user interaction.

The library's presentation layer works as a run-time interpreter of a dialog's description, responding to both resource text and embedded resource "code" for control type (captions, styles, positions, sizes, etc.) to produce dialog behaviors (such as initializing controls and data checks). This approach enables features that no resource editor I know of supports directly as of this writing: tabbed dialogs, and relief from text length problems in software internationalization. Also, this solution delivers tiny code size, ease of use, and design freedom for the rest of the surrounding application. For those of you in a hurry to find out whether this story is worth reading, Listing 1 shows the description of a fairly complex dialog box, while Figure 1 displays a one-tab portion of the result, as displayed by QUICKDLG.

Since full source code for QUICKDLG is available on this month's code disk and online sources, this article focuses on concepts and design decisions.

An Associative Memory

Listing 2 shows all you need to know about QUICKDLG to use it in your C++ code. The abstract class ASSOCMEM functions as a data manager, grouping related pieces of information together eventually to be shown on screen as a dialog. These pieces of data are either strings or integer values, which ASSOCMEM stores and retrieves using a unique access key passed as the first parameter of the get and set primitives. For example:

ASSOCMEM *pM;
BOOL bBold = pM->get("f_bold", 0);
//... pM->set("f_face", "Swiss");
Of course, ASSOCMEM is no more than a placeholder class with a pure virtual interface, so if you want to instantiate an object you must instantiate one from ASSOCMEM's descendant class, SHOWDATA:

SHOWDATA FontMemory   ("CFG.INI", "FontInfo");
You might still want to access that object through an ASSOCMEM pointer, however. In the short term use of such a pointer will enable you to use two less header files in compiling the client modules. In the long run, telling one piece of code no more than necessary about another might prevent a maintenance nightmare in a large project.

In addition to providing persistence, SHOWDATA can display data in and bring up a fully operational dialog box. This is the job of member function SHOWDATA::modalDlg, which uses its string arguments to locate the text of the dialog description to be loaded and interpreted. Once the text is interpreted, control will return to application only after the dialog is shut down by the user. During interpretation the textual dialog description creates the links between data and controls in the dialog. For example, the line

CHECK|VAR=f_bold|LABEL=&Bold font
tells SHOWDATA that the dialog should contain a checkbox, linked to an integer variable accessed by the key f_bold, so that gets and sets may be performed on the variable. In a similar manner, a RADIO control with R0 to Rn labels will retrieve and eventually store an integer value between zero and n for use as a C++ enum value.

I want to point out two important aspects of using the SHOWDATA class. The first aspect is crucial to understanding the QUICKDLG approach: Since the dialog description is a piece of plain ASCII text interpreted at run time, the call

FontMemory.modalDlg(hWnd, hInst, "DIALOGS.DLG", "FONTDLG");
will reflect any changes in DIALOGS.DLG immediately after a file save (say, from your text editor). Thus, QUICKDLG is its own prototyping tool, since it can't tell the difference between a real application run and some design-time test mode. In other words, all you need to design a dialog box is a text editor and a test tool featuring a WinMain with a SHOWDATA constructor and a SHOWDATA::modalDlg call like the one above.

However, since you may feel uncomfortable about shipping a dialog description file along with your application, I've provided an alternative: Once dialog design is complete, simply put the line

Dialogs TEXT DIALOGS.DLG
into your resource .RC file, then change the function call to read

FontMemory.modalDlg(hWnd, hInst, "Dialogs", "FONTDLG", "TEXT");
This form of the call, with the optional fifth argument, causes SHOWDATA to obtain all the dialog data directly from the .EXE file. (This assumes you have compiled the .RC file into the .EXE at build time.) Using this call you need distribute only one .EXE file and no one will ever know you have betrayed your old resource editor.

The second aspect of using SHOWDATA concerns the isolation of an application from QUICKDLG's core code. Specifically, SHOWDATA contains a pointer to a class MODALDLG whose internals remain unknown to other parts of the code, including SHOWDATA.HPP. Since MODALDLG's internals are hidden, the QUICKDLG library may be upgraded without recompiling the client modules of an application. This is what James Coplien [1] calls the letter-envelope idiom of C++. Now you know how to handle the envelope. The rest of the story is about the letter inside.

The Dialog Description Interpreter

Several classes cooperate to read the dialog description into QUICKDLG. The main component is the class DESCRIPT (listing 3) , which retrieves parts of the description unit — a string structured as a type followed by zero or more parameters:

<desc_type>['|'<param>=<value>]
DESCRIPT is implemented as an enhancement of STR, a string class provided with concatenation, comparisons, letter case operations, whitespace discarding, and substring search. (Class STR is provided on this month's code disk; for more information also see "A C++ Class Generator," by Adrian Barbu, CUJ, July 1995.)

DESCRIPT pays no attention to the order of parameters in the description unit; QUICKDLG will always ignore unknown elements of a dialog description. However, DESCRIPT may encounter an ordered structure, when it comes to what I call a multiple parameter. For example, the description of a radiobutton group (to speak Windowese) for a marital status might be:

RADIO|VAR=MS|LABEL=Marital status|\
RO=Single|R1=Married|\
R2=Divorced|R3=<Unknown>
To deal with such situations I've created a helper class MULTIPARAM. Its constructor receives both a pointer to the DESCRIPT object and the invariant part of the parameter name. The MULTIPARAM object can then return the number of entries (via member function no) and retrieve a parameter value by index (via member functions value). As DESCRIPT deals with one string defining a single dialog header or a control, it relies upon a class called LINEIN, whose constructor loads the entire resource text input in one big gulp, regardless of its file or user resource origin. After construction, the LINEIN object works like an iterator over the resource text. With each iteration the object yields the next description unit, in search of the desired dialog (most resource texts will contain several dialog descriptions).

Along the way, LINEIN also handles two low-level parsing jobs. The first is reforming long lines split with backslash and newline, exactly as the C/C++ preprocessor does for multi-line macros before expanding them. The second is eliminating comments, that is, lines with a semicolon for first non-white space character, like back in the assembler days. Finally, all description keywords recognized by QUICKDLG are gathered as public constant statics of a codeless class called KW.

(Besides keeping the global name space clean, I think the qualification KW:: gives better readability. Pretty short to type, too.)

Dialog Geometry And Aggregate Controls

Classes MODALDLG (listing 4) and CTL (listing 5) are the backbone of the QUICKDLG library. The first encapsulates all the aspects of dialog-as-usual mechanics. The second serves as an abstract base class for all the controls supported by QUICKDLG, as well as for the ones you might want to add in the future. MODALDLG lives two distinct lives, one before and one after creation of the dialog as an MS-Windows entity on screen. The same is true for class CTL. I call these two phases of existence Before Creation (BC) and On-Screen.

Before Creation

First I present a closer look at MODALDLG and CTL in the BC era. When MS-Windows puts up a dialog it pulls the dialog data out of a block of binary information, whether that block was loaded from compiled resources or created on-the-fly at run time (QUICKDLG will of course do the latter). This block consists of a dialog header followed by as many similar pieces of data to represent dialog items (static texts, edits, buttons, and so on). Incorrectly described as structs in various documents, these aggregates of data do not appear in WINDOWS.H, and they couldn't possibly otherwise than in a comment, since data members inside may be longer, shorter or plainly nonexistent, according to dynamic values of upper members.

To live with this unbelievable mess, I have defined true, fixed-size structs MODALDLG::DLGHEADER and CTL::DLGITEM. I then use API calls after dialog creation to handle the variable parts — e.g. SetDlgItemText for item captions. Even so, a second look at the structures will confirm that yes, they are hopelessly alignment dependant. Therefore you must always compile the QUICKDLG library with byte alignment code generation. This may be frustrating, but it has no adverse effect on the application.

The greatest challenge of the QUICKDLG approach is filling the X and Y position and size fields in these structures, since no such geometric information exists in the initial description. To perform this task, MODALDLG's constructor begins by initializing its LINEIN _input member to browse the text description. Then, as a new control is encountered, MODALDLG::virtualCTLCtor creates a CTL-derived object according to its DESCRIPT type. During its construction, this new object scans its DESCRIPT for all the information relevant to its geometry (namely its various labels), so as to compute the control's overall width and height in dialog units. Upon return, MODALDLG adjusts its own geometry accordingly, then asks the new control to append its DLGITEM data to the BLOCKMEM _template under construction. After encountering the last control, MODALDLG computes a horizontal band to accomodate the OK and CANCEL buttons, centered and sized according to their labels, then appends the two corresponding DLGITEMs to the memory block. MODALDLG's own geometry is now completely determined, so its overall width and height can finally be poked into the beginning of the _template memory block.

The whole process is quite precise because every text label undergoes a dummy write under life-sized font and screen conditions — this determines its exact bound rectangle. The main danger of such automatic item placement is uncontrolled growth of the dialog on both X and Y axes. QUICKDLG has several ways to address this, sharing responsibilities between MODALDLG and CTL-derived controls. To begin with, QUICKDLG gives a more predictable Y-size to the radiobutton group (by far the greatest space waster) by fitting all its choices into a combo box. Even more effective, MODALDLG can add a Z axis to itself, enabling complex dialogs to specify tabbed groups of controls.

Tabbed Dialog Creation

This is as good a time as any for me to digress briefly and talk about MODALDLG's support for tabbed dialogs. MODALDLG keeps a table of planes GRPXY _pGXY[] according to the parameters G0 to Gn specified in its DESCRIPT (if none, there will still be a _pGXY[0] holding all the controls). As it scans the input, MODALDLG signals the width and height of each new control to the corresponding plane, then puts the control and the group it belongs to in the table GCTL _ctab[CTABSIZE]. At display time all controls will be hidden but those belonging to the active plane. At the end of the process, MODALDLG takes the maximum width and height of all planes for its own overall dimensions.

On-Screen Behavior

The on-screen life of MODALDLG and its controls is largely characterized by natural exchanges between grown-up objects, as opposed to the enslaving approach of the native MS-Windows dialog procedure. To make the dialog appear, MODALDLG::run calls the API primitive DialogBoxIndirectParam, where "Indirect" signifies use of the _template memory block rather than loading of a compiled dialog resource.

As for "Param," well, it hints at our chance to make a C++-palatable entity out of something that originally isn't, the dialog procedure itself. Indeed, like any other callback function, the dialog procedure is a piece of code called by the system according to a prototype (a binary stack pattern, actually) buried deeply once and for all, which cannot accomodate a hidden this pointer. As a result, a callback function cannot be a member function of a C++ class. This is why the dialog procedure _DlgProc is a static function of the class MODALDLG (listing 4) . Its code must rely on the first argument, the window handle, to retrieve the actual MODALDLG object concerned.

This little trick is possible because DialogBoxIndirectParam accepts an extra 32-bit value (a pointer to the calling MODALDLG instance) that the system will give back to the static dialog procedure with the first important message, WM_INITDIALOG. QUICKDLG doesn't get a second chance to link this C++ object address to the window handle, so upon receipt of WM_INITDIALOG it stores the pointer as two 16-bit properties of the window (the best we can do). From this point on, until the dialog is shut down, the inline function HWIN2this (listing 4) reassembles the MODALDLG pointer out of the window handled before reacting to the received message.

Receipt of WM_INITDIALOG is also the right time to initialize controls on screen, so the MODALDLG instance calls CTL::initScreen from all its controls to wake them up to their second, on-screen life. Since each control was given a pointer to the correct associative memory location upon creation, a burst of SHOWDATA::get will bring data on screen.

So far so good, but C++ doesn't mean OOP yet. In native Windows, it is the dialog procedure that gets WM_COMMAND messages for anything significant happening to its controls: check state, list selections, edit typing, button clicks, you name it, it all goes to the dialog procedure. If you want a smarter edit control you must add code to the procedure of the dialog it belongs to. To circumvent this requirement, MODALDLG redispatches every such notification message to the corresponding CTL-derived object, using the control identifier as an index in the GCTL _ctab[] table:

CTL* pCTL = _ctab[wId].ctl;
if(pCtl)
   pCtl-wm_command(wId, wNotif);
This technique leaves it up to the CTL object to react or not, according to the event in question and to the object's own degree of specialization (that is, inheritance).

As an example of how this works, clicking the OK button causes two full scans of the GCTL table. The first scan polls each control via CTL::isDataOk to confirm that the control contains valid data. If any control answers no, MODALDLG brings its tabbed plane to foreground (if necessary) and sets input focus for correction. If all controls are okay, the second scan is a CTL::saveData broadcast, which will trigger the proper SHOWDATA::set.

Finally, I have thrown two look-and-feel features into MODALDLG's behavior which I find nice in the long run. The first is to use the dialog's VAR name as a SHOWDATA key to save the last visited tabbed group of controls. Bringing up the same group will help the end user feel familiar with the dialog next time he sees it on the screen. The second feature works inside the function _thinFontJob, to mark a difference between actual data (written in bold face, as in everyday MS-Windows dialogs) and label meta-information on statics and buttons (written in normal face). Take them as an apocryphal appendix to your favorite interface design guide. Or just comment them away.

Cooperative Dialog Design

This QUICKDLG approach leaves a lot of responsibility to the dialog designer, which is only natural, as he is the one who is supposed to know what all those controls mean and how they relate to each other. However, QUICKDLG handles some of the more complicated layout tasks, providing tremendous relief. For example, QUICKDLG makes all decisions concerning X- and Y-axis layout of the controls. A QUICKDLG control generally amounts to more than a MS-Windows dialog item. For example, a QUICKDLG EDIT Control is made of two items, the edit zone and the static text explaining it. If both edit zone and text fit in one "line" of text no longer han half the width of the screen, the EDIT control's constructor will set overall width and height accordingly. If not, the constructor will align the edit zone below the static text. This sort of calculation takes place inside every aggregated control. (MODALDLG only asks for the verdicts using virtuals CTL::duW() and CTL::duH().)

These policies work together for a globally satisfactory result on all the real life dialogs I tried. Obviously, QUICKDLG will never be able to use every square unit of your dialog as you can do yourself with a GUI resource editor. But there are times when such tools are best avoided. Some day your international sales manager may urge you to switch all your dialogs to German or Italian, something like "Verzeichnis for Herunterladen" or "Repertorio di telecaricamento," to fit in an item in place of "Download directory." That day you may find QUICKDLG (or some similar concept) simply invaluable.

References

[1] James Coplien. Advanced C++ Programming Styles And Idioms (Addison-Wesley, 1992).

[2] Paul DiLascia. Windows++, Writing Reusable Windows Code In C++ (Addison-Wesley, 1992).