Avoiding GetDlgItem

Home
Back To Tips Page

Introduction and Overview

When you learned to program Windows in C, you learned about GetDlgItem. This is the way to get a handle for a control when you are working with a dialog. In C, it is the only way to get a window handle for the control. In C++/MFC, there is a better way. It makes about as much sense to use GetDlgItem in C++/MFC as it does to program Windows in assembly code. Yes, I know there are masochists who like to program Windows in assembly code. But it is rare that it is actually needed. Perhaps in the inner loop of a DSP algorithm, where you might drop a few of those fancy Pentium III opcodes inline, but the rest of the time it is insanity. The same is true of GetDlgItem. Actually, there is slightly more reason to use a raw GetDlgItem than there is to use assembly code, but the reasons represent rare and very uncommon cases.

My view is, if you are writing more than one GetDlgItem per year, you are probably not using C++/MFC correctly. Fortunately, there is a better, more elegant, and safer way to get access to controls using MFC.

Note that you should almost never use UpdateData in a dialog. If you use it, you should use it only in a modeless dialog. There is, as far as I can tell, absolutely no excuse for using UpdateData in a modal dialog. I discuss this in much more detail in another essay. But the techniques shown here are fundamental to avoiding the use of UpdateData as well. A simple rule: If you're calling UpdateData in a modal dialog, you're not using MFC correctly.

Creating Control Variables

To avoid the use of GetDlgItem, you must create a control variable to represent your control. You can create a control variable for any control that has an ID other than IDC_STATIC. Due to what appears to be terminal insanity, you have to go through an unfortunate set of complex machinations, discussed below, to create control variables for radio buttons other than the first in a group; I cannot figure out why this is viewed as a good idea, but Microsoft seems to have a peculiar idea about how you should use radio buttons. 

To create a control variable, you activate ClassWizard, select the Member Variables tab, highlight the control you want a variable for in the list box, and click the Add Variable button. You will get a window much like the one shown below (which has already been filled in):

In the "Member variable name" window you type the name of your member variable. The window starts out preloaded with "m_". I found immediately that this was confusing. For some controls, you can have both a control variable and a value variable. I use "m_" for the value variable (a feature I rarely use, as it turns out), and use "c_" as a prefix for control variables. Trust me on this one: if you start using m_ to represent control variables, you will quickly come to regret it. Been there, done that. That's why I have a new convention. It works. For other conventions, see the discussion below.

Go to the Category window. For some controls, you can only have a Control variable, and that is the only option. For others, such as an edit control, you can have a Control or a Value variable. The default is Value, so you have to do what I have done in the illustration and select Control as the category. Having selected a Control variable, you may now select the type. If you have a class you have derived directly from a base class, as I have derived CNumericEdit from CEdit, ClassWizard will recognize it and present it as one of the options in the Variable Type window. Since c_Count is an edit control representing a count, I selected my CNumericEdit class as the variable type.

Unfortunately, if you have a class which is derived from one of your classes that is derived from a base class, ClassWizard can't cope (ClassWizard can't cope with a lot of common things you would like to do...). In this case, just select the base type, and you'll have to hand-edit it later.

What this gives you is a variable of the selected type. If you look in your .h file, you will find a declaration has been added

        CNumericEdit c_Count;

It is your responsibility to see that the appropriate header file (NumericEdit.h, in this case) is included before the dialog header file so the user-defined control types are known.

If you have (as I usually do), two or more derivation levels to get to your useful control (for example, I have a fancy ComboBox class, CSmartCombo, and one which displays little icons with the selections, CImageCombo, which is derived from CSmartCombo, which is derived from CComboBox, you can't specify this class. If I want a CImageCombo control, I just tell it CComboBox is the variable type, and then I go in and change the header file by hand. Silly, but ClassWizard is not a wizard you want to leave to carry water if there are brooms nearby...

Having done this, you will notice another feature: in the .cpp file there has been a function all along, which now has the line:

void CMyClass::DoDataExchange(CDataExchange * pDX)
  {
   CDialog::DoDataExchange(pDX);
   //{{AFX_DATA_MAP(CMyClass)
   DDX_Control(pDX, IDC_COUNT, c_Count);
   //}}AFX_DATA_MAP
  }

Like most //{{ //}} style ClassWizard comments, you shouldn't mess with the contents of the delimited block yourself unless you are willing to risk the consequences. What the DDX_Control line does is map the HWND of the control to the variable, in this case IDC_COUNT is mapped to c_Count. And yes, it does it under the floor with GetDlgItem

After your OnInitDialog handler calls CDialog::OnInitDialog, these variables are available for use. Thus, instead of writing

CButton * button = (CButton *)GetDlgItem(IDC_BUTTON);
if(button->GetCheck( ) == BST_CHECKED) ...

you can write

if(c_Button.GetCheck( ) == BST_CHECKED) ...

In my case, I can write

if(c_Count.GetWindowInt() == 0) ...

because GetWindowInt is a method of my CNumericEdit class. I can even write

c_Count.SetWindowText(37);

because I've overloaded SetWindowText in my class to take integer values.

And that represents one of the problems of having those hundreds of GetDlgItem casts: what if you subclass a control? You have to find all uses of the control in GetDlgItem and change the casts. Say, for example, you've overridden AddString in your CListBox-derived class to recompute the horizontal extent and call SetHorizontalExtent, and consequently also overridden ResetContent to set the horizontal extent to 0. If you then change some ordinary CListBox variable to be your new CHorzListBox class, you have to find all the casts to CListBox, and change only those which apply to your new CHorzListBox class. Ugly, isn't it? Whereas if you use control variables, all you do is change the variable type in the declaration, and all the overloading and inheritance work correctly. Much better. This is how C++ is supposed to be used.

Pitfalls and Gotchas

This is not without its problems. Some of the problems were there already when you used GetDlgItem, and some are artifacts of a limited world view of ClassWizard.

We've already mentioned that ClassWizard can't cope with more than one derivation level. Silly, but I've been complaining about this since ClassWizard was introduced in 16-bit MFC and nobody pays attention. 

Radio button variables

Another intrinsic problem, which represents some sort of strange philosophical viewpoint of Microsoft, is that you must not be allowed to create control variables for radio buttons. This makes no sense. They have some weird idea that the only way you will ever manipulate radio buttons is via an index. This is hopelessly inadequate. Therefore, you have to go through some serious contortions to get control variables for your radio buttons.

The first thing you have to do is go back and mark all radio buttons as having the WS_GROUP style. Only radio buttons with a WS_GROUP style can have a control variable. However, if you mark all of them with WS_GROUP, create the control variables, and then remove the WS_GROUP attribute, everything works just fine, thank you. Why we have to go through these extra steps makes no sense whatsoever, but like the derived classes problem, I've been complaining about this for years with no effect. My problem is that I keep forgetting to go back and undo all the WS_GROUP attributes, so the first time I run the program after this I find that all my radio buttons are one-button groups. Whoops. $#%! Fix, and recompile/relink. 

Uninitialized Controls

This problem actually already exists, and you may have been hit by it. But for those of you who haven't been, here's what can happen to you. 

During dialog creation or the DDX_Control initialization, controls can generate messages. These messages often have handlers that want to assume the controls are present. They may not be. You have no real control over the order of DDX_Control initialization. Consider two simple examples:


Example 1: Resizing controls

You can create a dialog with a resize border. Using this resize border you can drag the dialog edges around to resize it. A typical use might be if you have a list box in the dialog, most conveniently placed at the bottom of the dialog. As you stretch the dialog horizontally or vertically, you want to stretch the list box so it fits the entire dialog. Looks simple:

void CMyDialog::OnSize(UINT nType, int cx, int cy)
   {
    CDialog::OnSize(nType, cx, cy);
    CRect r;
    GetClientRect(&r);
    CRect lb;
    c_ListBox.GetWindowRect(&lb);
    ScreenToClient(&lb);
    c_ListBox.SetWindowPos(NULL, 0, 0, r.Width(), r.Height() - lb.top,
                           SWP_NOMOVE | SWP_NOZORDER); 
   }

Unfortunately, the OnSize routine can be called very early; WM_SIZE is one of the first five messages sent when a window is created. This means that at the time the dialog is created, the OnSize handler is called, but none of the controls have been created yet! The result is the c_ListBox.GetWindowRect takes an access fault somewhere deep in Windows. Ugly.


Example 2: A spin control with an autobuddy.

If you have a CSpinCtrl which has its "Autobuddy" attribute set, this means that it will set the value of its buddy control (such as an edit control) whenever its value changes. It also means that it will set that control to 0 when the spin control is created. This generates a WM_CHANGE and WM_UPDATE sequence to the edit control. If you are looking for this event, and want to change the state of some other control based on the change, your attempt to access the other control will cause an access fault if that other control is not already defined.


Both of these problems can be solved by adding an extra variable to the dialog class. Add a protected variable, "BOOL initialized". In the dialog class constructor, set "initialized" to FALSE. (Note that I do not use the m_ prefix for class variables that are not public and set or read by the DoModal caller. I've been programming far too many years to think this convention is useful. Also, you will never see me use Hungarian notation in any variable I define, anywhere, at any time. But that's another discussion). You must set this in the constructor, not in OnInitDialog, because by the time you get to the assignment in OnInitDialog it is far too late. To protect against the possibility of accessing an invalid variable, you just check the initialized variable. For example, 

void CMyDialog::OnSize(UINT nType, UINT cx, UINT cy)
   {
    CDialog::OnSize(nType, cx, cy);
    if(!initialized)
       return;
    // ... whatever else you want to do
   }

Naming Conventions

Microsoft has some weird ideas about naming conventions. Hungarian notation was invented in the days before C had prototypes and thus cross-module checking. It probably had some utility in the days before C resembled a real programming language. It has absolutely no value today, and should be avoided. I find the m_ convention for member variables equally insane. I use it only in limited situations, in particular, only for values associated with controls. I only need value variables for controls whose values are passed into the dialog from outside, or which are passed back to the caller. Since I rarely do this, the occurrence of m_ variables in my code is very, very low.

There are other conventions I use. For example, I prefix the control variables with "c_". This indicates that it is a control variable. It is necessary to use a different convention than "m_", because some controls can have both a control variable and a value variable. In addition, it is often useful to be able to name the caption associated with some controls, such as an edit control. This is useful if you have to enable/disable the controls. Consider the case of an edit control, c_Text, which has a static control which is the caption. I use the prefix "x_" to indicate the caption. This introduces much sanity into the world. If I want to preload the text control by passing the value in, I create a value variable, m_Text. To access the control, I create a control variable, c_Text. To access its caption, I create a control x_Text. I never access the m_ variable within the dialog, unless I've provided a "reset to initial state" button which resets the control to its initial value. There is no need to access the value variable. If accessed, I only read it.

This means that the DDX calls will look something like

DDX_Control(pDX, IDC_TEXT, c_Text);
DDX_Text(pDX, IDC_TEXT, m_Text);
DDX_Control(pDX, IDC_TEXT_CAPTION, x_Text);

To enable or disable the text control and its corresponding caption you can then write something like

void CMyDialog::updateControls( )
    {
     BOOL enable;
     enable = ... 
     c_Text.EnableWindow(enable);
     x_Text.EnableWindow(enable);
    }

Note how clear this is. Easy to write, easy to understand, easy to maintain. What's that updateControls method? That's another essay!

Summary

The GetDlgItem method has limited usefulness in writing MFC code. If you write it, there is an excellent chance you are simply not using MFC correctly. Certainly there are exceptions, and I show one in my essay "Who Owns the GUI". But such instances are rare. A well-written MFC application will have no gratuitous instances of GetDlgItem, anywhere, and no instances of UpdateData in any modal dialog, ever.

[Dividing Line Image]

The views expressed in these essays are those of the author, and in no way represent, nor are they endorsed by, Microsoft.

Send mail to newcomer@flounder.com with questions or comments about this web site.
Copyright © 1999 The Joseph M. Newcomer Co./FlounderCraft Ltd. All Rights Reserved.
Last modified: May 14, 2011