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.
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);
DDX_Control(pDX, IDC_COUNT, c_Count);
}
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.
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.
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.
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;
}
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 on dialog control managment. 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.
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 article.
Copyright � 1999 CompanyLongName All Rights Reserved.
www.flounder.com/mvp_tips.htm