Building MFC Dialogs at Runtime

Adrian presents a class for defining MFC-based dialogs.


September 01, 2004
URL:http://www.drdobbs.com/windows/building-mfc-dialogs-at-runtime/184405811

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:

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.

Download the source code for this article here

int    exposure_time_ms = 8;  // Two different numeric types.
double laser_power_mw = 40.0;

Dynamic_dialog dlg("Camera Image Capture", 155, 100);

dlg.Add_OK_button(20, 70, 50, 14);
dlg.Add_Cancel_button(85, 70, 50, 14);
dlg.Add_edit_control("Exposure time", 15, "ms.", 70, 20,
                     45, 12, &exposure_time_ms, 1, 100);
dlg.Add_edit_control("Laser power",   15, "mW.", 70, 40,
                     45, 12, &laser_power_mw, 0.1, 200.0);
if (dlg.DoModal() == IDOK)
   {
   // ... more code ...

Example 1: Code for a dialog with two edit controls and two buttons.

// ----- Message Map -----
BEGIN_MESSAGE_MAP(Dynamic_dialog, CDialog)
   ON_BN_CLICKED(Dynamic_dialog::e_first_browse_idc,     OnBrowse0)
   ON_BN_CLICKED(Dynamic_dialog::e_first_browse_idc + 1, OnBrowse1)
   ON_BN_CLICKED(Dynamic_dialog::e_first_browse_idc + 2, OnBrowse2)
   ON_BN_CLICKED(Dynamic_dialog::e_first_browse_idc + 3, OnBrowse3)
END_MESSAGE_MAP()

void Browse(int button);

// ----- OnBrowse? functions ----
void Dynamic_dialog::OnBrowse0() { Browse(0); }
void Dynamic_dialog::OnBrowse1() { Browse(1); }
void Dynamic_dialog::OnBrowse2() { Browse(2); }
void Dynamic_dialog::OnBrowse3() { Browse(3); }

Example 2: Message map in Dynamic_dialog for implementing Browse buttons.

Figure 1: Dialog generated by the code in Example 1.

Figure 2: Sample dialog with two combo boxes where selecting a state in the left combo box changes the contents of the right combo box.

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.