Sharing classes leads to less clutter
Adrian is a founder of InPhase Technologies, a Lucent Technologies/Bell Labs spinoff that is developing holographic data storage products. You can contact him at adrianhill inphase-tech.com, with subject "DDJ."
Dialogs are generally defined for GUI-based applications using a visual tool to design the appearance of the dialog, and an automatic code generator to generate most of the source code associated with the dialog. Apple's Macintosh and Windows-based PCs both use this approach.
For MFC-based Windows applications, you can use Visual Studio's resource editor to position the dialog elements, then use ClassWizard to generate the corresponding C++ class code. The resource editor saves the layout information for all the application's dialogs in a single resource (.rc) file, and saves the corresponding resource ID symbol definitions in the resource.h file. ClassWizard generally writes the C++ class code for each dialog to a separate .cpp and .h file.
While this process is fairly easy to learn and use, the resulting code has several limitations, including:
- The class encapsulating the dialog cannot be reused in other applications by simply including the dialog's .cpp and .h files in the new application's build. The dialog resources (in the application's .rc and resource.h files) must also be copied, either manually or using the resource editor, from one application to the other. This process is tedious, particularly if the class is to be reused in several applications. If the dialog is subsequently changed and the changes are required in all applications, the modifications have to be made to each application in turn. Overall, this procedure is more like the bad old days of C programming than an object-oriented method.
- The process of copying and pasting resources from one application to another can lead to an insidious bug. Since the resource IDs are effectively at global scope (they are all
#defined
in resource.h), clashes are possible if resource IDs copied from one application already exist in the destination application. The application with the resource ID conflict may crash, with no obvious reason. (This has only bitten me once, but it hurt.) - Only one developer can edit an application's resource (.rc) file at a time. This limitation exists because the resource editor updates several
#define
s in resource.h (for example, _APS_NEXT_RESOURCE_VALUE and _APS_NEXT_CONTROL_VALUE) each time new resources are added. If two developers try to check in updated .rc and resource.h files to a source-code version-control system (such as CVS), the process fails because of conflicts in the values of these symbols. By contrast, multiple developers can work on different parts of a .cpp file concurrently, without clashing. - MFC's ClassWizard will generate a new class for each dialog, no matter how simple the dialog is, or how similar the dialog is to other dialogs in the application. This produces "class clutter" that, though not serious, serves to muddy the class view in a large project and adds to code bloat.
In this article I provide a class for defining MFC-based dialogs, which overcomes these limitations by not using resources in the application's .rc and resource.h files. (The complete source code and related files for the class are available electronically; see "Resource Center," page 5.) Instead, dialog box templates are built in memory at runtime. The details are encapsulated in class Dynamic_dialog
, together with its helper class, Dialog_item
.
You define a dialog in C++ source code, either using an instance of Dynamic_dialog
directly or using a class derived from it when the dialog requires additional message map functions. To leverage as much existing MFC functionality as possible, Dynamic_dialog
is derived from MFC's CDialog
class. In particular, it was simple to make all the familiar data exchange and data validation (DDX/DDV) functionality available with very little additional code.
Example 1 is the code for a simple dialog with two edit controls, and the OK and Cancel buttons (Figure 1). Even without seeing the class declaration, the intent of the code is clear.
Inside the Dynamic_dialog Class
In general, an MFC-based dialog is constructed by creating a dialog box template that describes the dialog and its controls, such as edit boxes and pushbuttons. The dialog box template is then used to construct an instance of a CDialog-
derived class (MFC provides class CDialog
to encapsulate the basic functionality of a dialog box).
When you use the resource editor to define a dialog, the dialog box template is compiled from the contents of the .rc file as part of the build process. In the Dynamic_dialog
class, rather than building the templates from the resource file at compile time, I generate the dialog box template in memory at runtime.
This approach is mentioned, but not described, in Jeff Prosise's Programming Windows with MFC,
Second Edition (Microsoft Press, 1999) and Michael Blaszczak's Professional MFC with Visual C++ 6,
Fourth Edition (Wrox Press, 1999). Blaszczak notes that this method is "fraught with pointer arithmetic and alignment tomfoolery." However, by encapsulating the template details in the Dynamic_dialog
and Dialog_item
classes, the apparent complexity is addressed once and presents no further disadvantages.
Dialog Templates in Memory
A dialog box template in memory consists of a template header followed by control definitions for each control in the dialog. A standard template header consists of an instance of the Windows SDK DLGTEMPLATE struct, typically followed by a Unicode string for the dialog box title, the font size, and another Unicode string for the dialog box font name. Each control definition typically consists of an instance of the DLGITEMTEMPLATE struct followed by a Unicode string specifying the control's initial text (see "Templates in Memory," Platform SDK: Windows User Interface, MSDN Library CD, July 2001).
The dialog box template has to be constructed with particular word alignment in order to work correctly, which is probably why this approach is not widely used. There is an example of building dialog templates in memory in "DLGTEMPL: Creating Dialog Templates Dynamically" (MSDN Library CD
, July 2001), but it is incomplete and somewhat unconvincing because it implements most of its dialogs using standard (.rc) resources.
The Dynamic_dialog
class has a CArray
data member that contains one Dialog_item
instance for each control in the dialog. The final dialog template is assembled by the Dynamic_dialog::Build_template_buffer
and Dialog_item::Write_to_template_buffer
functions (in Dyn_dialog.cpp and Dialog_item.cpp).
Using the Dynamic_dialog Class
The Dynamic_dialog
class supports the five most common dialog controls: buttons, combo boxes, list boxes, edit controls, and static text. The DDlgTest application in the code accompanying this article has examples illustrating the use of all these controls. Look for the Dynamic_dialog_test
namespace functions in DynDlgTest.cpp.
Listing One (available electronically) shows the interface to Dynamic_dialog
. You build a dialog by constructing an instance of Dynamic_dialog
, then adding controls to it. The tab order of controls is determined by the order in which controls are added to the dialog. Call DoModal
to display the dialog, just as you would with a class generated by ClassWizard.
The overall dialog appearance can be set using the functions with names having the "Set_dialog_" prefix.
Functions to add controls to the dialog have names starting with "Add_." Most of these functions have a pointer argument value
to associate a variable with the control. All the "Add_" functions have parameters specifying the size and position of the controls.
Edit controls, list boxes, and combo boxes often have associated static strings to tell the user what the control's contents are for. For convenience, the "Add_" functions for these controls take a pointer to a string to be displayed to the left of the control, and a second pointer to another string to be displayed to the right of the control (see Figure 1 and Example 1). If a pointer is 0, then no string is displayed in the corresponding position.
Static text strings can also be added to a dialog using the Add_static_text
function, but supplying these strings in the same call as their edit control (or list box or combo box) is convenient and makes the intent of the code line self documenting.
For numerical values, the Add_edit_control
function is implemented as a template function. If you require range checking (data validation) on the variable, then supply the value limits as the parameters min
and max
. If not, set these two parameters to the same value.
Rather obviously, checkboxes are added with the Add_checkbox
function. The state of the checkbox is reflected in the value of the associated bool value passed to this function. As far as MFC is concerned, checkboxes are simply another form of button.
You add a group of radio buttons with a call to Add_first_radio_button
for the first button in the group, followed by a call to Add_radio_button
for each additional button in the group. Call Add_group_box
to complete the set of radio buttons. The variable associated with a group of radio buttons is passed as a parameter to Add_first_radio_button
.
Buttons and Message Maps
MFC uses a message map to associate a call to a dialog member function with a dialog event, such as clicking on a button or updating an edit box. For example, for a dialog produced with the resource editor, ClassWizard would generate a message map like this (with comments removed):
BEGIN_MESSAGE_MAP(File_dlg, CDialog)
ON_BN_CLICKED(IDC_INPUT_BROWSE, OnInputBrowse)
END_MESSAGE_MAP()
to associate function OnInputBrowse
with a button click on the button with resource ID IDC_INPUT_BROWSE. The first line of the message map indicates that this message map is part of class File_dlg
, which is derived from CDialog
. BEGIN_MESSAGE_MAP, ON_BN_CLICKED, and END_MESSAGE_MAP are all MFC-defined macros.
There is no need to generate message map entries for the nearly ubiquitous OK and Cancel buttons because the CDialog
base class already provides the necessary mapping. These buttons can be added to a Dynamic_dialog
instance with the functions Add_OK_button
and Add_Cancel_button
. Typically, I make an Add_OK_button
call immediately after constructing the dialog (see Example 1) so that the OK button has focus when the dialog is displayed. If users immediately press Enter, the dialog acts as if the OK button had been clicked.
To add other buttons to a dynamic dialog, you derive a class from Dynamic_dialog
and add the button with a call to Dynamic_dialog
's Add_pushbutton
function. You must also provide a message map for your derived class, with an entry for each button. The resource ID specified in the message map must match the value passed as a parameter to Add_pushbutton
. As with resource-based dialogs, you provide the function associated with the button in your derived class.
An example of adding a pushbutton in a Dynamic_dialog-
derived class is in the Pushbutton_dialog
class (Push_dlg.cpp and Push_dlg.h) in the DDlgTest application.
Browse buttons that let users select a file or directory by popping up a second, standard dialog for file or directory selection are common in many applications' dialogs. When users click OK in the second dialog, the file or directory name is copied back to an edit control in the primary dialog. To avoid the need to derive a class from Dynamic_dialog
and add a message map every time I wanted this functionality, I decided to implement it directly in the Dynamic_dialog
class. The message map and associated functions in Dynamic_dialog
are shown in Example 2. I implemented four message map entries; the number can easily be extended if you anticipate needing more than four Browse buttons in a single dialog.
To add a Browse button to a dialog and associate the button with the filename contained in an edit control, call function Add_Browse_button
immediately after the Add_edit_control
call controlling the filename. The first parameter to Add_Browse_button
determines whether the secondary dialog is a standard CFileDialog
, or a dialog to allow a directory to be selected. An example of a dialog with three CString edit controls with associated Browse buttons is in function Dynamic_dialog_test::Browse_test
in the DDlgTest application.
Message map notifications are not limited to pushbuttons. The sample application also contains an example of a dialog with two combo boxes where the contents of the right-hand combo box (in this case, a choice of cities) depends on the selection made by the user in the left combo box (in this example, a choice of states). The dialog code is in class Derived_dialog
(Derived.cpp); the dialog is in Figure 2. The message map for this class is:
BEGIN_MESSAGE_MAP
(Derived_dialog, Dynamic_dialog)
ON_CBN_SELCHANGE
(Derived_dialog::e_box_id,
OnSelchangeState)
END_MESSAGE_MAP()
Conclusion
My primary aim in writing the Dynamic_dialog
class was to let classes that use dialogs be shared among various projects, without the need to cut-and-paste resources from project to project.
To implement simple dialogs, create an instance of Dynamic_dialog
and add the required controls with calls to the member functions. Call DoModal
, as you would for a dialog generated with the resource editor and ClassWizard, to display the dialog. If users click OK (or presses Enter) to close the dialog, then the variables associated with the dialog controls are updated.
When a message map is needed for a dialog, derive a class from Dynamic_dialog
and add the controls for the dialog in the constructor of the derived class. As before, call DoModal
to display the dialog.
I chose the order of parameters passed to several of the Dynamic_dialog
functions to allow common default values, and also to be close to the order of the corresponding values in the definition of dialog controls in a resource file. This makes it fairly simple to write a converter to generate C++ code from .rc dialog resources that already exist in a project.
Another advantage of building dialogs at runtime is that the current state of the program can be used to decide what controls to display. This can result in dialogs that are less cluttered than resource-based dialogs by avoiding the need to gray out or hide dialog items, for example, which are not relevant to the current state of the application.
An obvious extension of the Dynamic_dialog
approach is a class to automatically size a dialog and position the controls in it, given a list of controls to be displayed. This, in turn, offers the prospect of dialogs that resize themselves depending on the differences in lengths of strings in different languages such as German and Spanish.
Acknowledgements
Thanks to Charles Stanhope and Martin Pane, both at InPhase Technologies, for their insightful comments.