WTL for MFC Programmers, Part IV - Dialogs and Controls
WTL for MFC Programmers, Part IV - Dialogs and Controls
Contents
- Introduction
- Refresher on ATL Dialogs
- Common Control Wrappers
- Creating a Dialog-Based App with the AppWizard
- Using the Control Wrapper Classes
- More on DDX
- Handling Notifications from Controls
- Odds & Ends
- Copyright and license
- Revision History
Introduction to Part IV
Dialogs and controls are one area where MFC really saves you time and effort. Without MFC's control classes, you'd be stuck filling in structs and writing tons of SendMessage
calls to manage controls. MFC also provides dialog data exchange (DDX), which transfers data between controls and variables. WTL supports all those features as well, and adds some more improvements in its common control wrappers. In this article we'll look at a dialog-based app that demonstrates the MFC features that you're used to, as well as some more WTL message-handling enhancements. Advanced UI features and new controls in WTL will be covered in Part V.
Refresher on ATL Dialogs
Recall from Part I that ATL has two dialog classes, CDialogImpl
and CAxDialogImpl
. CAxDialogImpl
is used for dialogs that host ActiveX controls. We won't cover ActiveX controls in this article, so the sample code uses CDialogImpl
.
To create a new dialog class, you do three things:
- Create the dialog resource
- Write a new class derived from
CDialogImpl
- Create a public member variable called
IDD
, and set it to the dialog's resource ID.
Then you can add message handlers just like in a frame window. WTL doesn't change that process, but it does add other features that you can use in dialogs.
Control Wrapper Classes
WTL has lots of control wrappers which should be familiar to you because the WTL classes are usually named the same (or almost the same) as their MFC counterparts. The methods are usually named the same as well, so you can use the MFC documentation while using the WTL wrappers. Failing that, the F12 key comes in handy when you need to jump to the definition of one of the classes.
Here are the wrapper classes for built-in controls:
- User controls:
CStatic
,CButton
,CListBox
,CComboBox
,CEdit
,CScrollBar
,CDragListBox
- Common controls:
CImageList
,CListViewCtrl
(CListCtrl
in MFC),CTreeViewCtrl
(CTreeCtrl
in MFC),CHeaderCtrl
,CToolBarCtrl
,CStatusBarCtrl
,CTabCtrl
,CToolTipCtrl
,CTrackBarCtrl
(CSliderCtrl
in MFC),CUpDownCtrl
(CSpinButtonCtrl
in MFC),CProgressBarCtrl
,CHotKeyCtrl
,CAnimateCtrl
,CRichEditCtrl
,CReBarCtrl
,CComboBoxEx
,CDateTimePickerCtrl
,CMonthCalendarCtrl
,CIPAddressCtrl
- Common control wrappers not in MFC:
CPagerCtrl
,CFlatScrollBar
,CLinkCtrl
(clickable hyperlink, available in XP and later)
There are also a few WTL-specific classes: CBitmapButton
, CCheckListViewCtrl
(list view control with check boxes), CTreeViewCtrlEx
and CTreeItem
(used together, CTreeItem
wraps an HTREEITEM
), CHyperLink
(clickable hyperlink, available on all OSes)
One thing to note is that most of the wrapper classes are window interface classes, like CWindow
. They wrap an HWND
and provide wrappers around messages (for instance, CListBox::GetCurSel()
wraps LB_GETCURSEL
). So like CWindow
, it is cheap to create a temporary control wrapper and attach it to an existing control. Also like CWindow
, the control is not destroyed when the control wrapper is destructed. The exceptions are CBitmapButton
, CCheckListViewCtrl
, and CHyperLink
.
Since these articles are aimed at experienced MFC programmers, I won't be spending much time on the details of the wrapper classes that are similar to their MFC counterparts. I will, however, be covering the new classes in WTL; CBitmapButton
is quite different from the MFC class of the same name, and CHyperLink
is completely new.
Creating a Dialog-Based App with the AppWizard
Fire up VC and start the WTL AppWizard. I'm sure you're as tired as I am with clock apps, so let's call our next app ControlMania1. On the first page of the AppWizard, click Dialog Based. We also have a choice between making a modal or modeless dialog. The difference is important and I will cover it in Part V, but for now let's go with the simpler one, modal. Check Modal Dialog and Generate .CPP Files as shown here:
All the options on the second page (for the VC6 wizard) or the User Interface Features tab (in VC7) are only meaningful when the main window is a frame window, so they are all disabled. Click Finish to complete the wizard.
As you might expect, the AppWizard-generated code is much simpler for a dialog-based app. ControlMania1.cpp has the _tWinMain()
function, here are the important parts:
int WINAPI _tWinMain (
HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/,
LPTSTR lpstrCmdLine, int nCmdShow )
{
HRESULT hRes = ::CoInitialize(NULL);
AtlInitCommonControls(ICC_COOL_CLASSES | ICC_BAR_CLASSES);
hRes = _Module.Init(NULL, hInstance);
int nRet = 0;
// BLOCK: Run application
{
CMainDlg dlgMain;
nRet = dlgMain.DoModal();
}
_Module.Term();
::CoUninitialize();
return nRet;
}
The code first initializes COM and creates a single-threaded apartment. This is necessary for dialogs that host ActiveX controls; if your app isn't using COM, you can safely remove the CoInitialize()
and CoUninitialize()
calls. Next, the code calls the WTL utility function AtlInitCommonControls()
, which is a wrapper for InitCommonControlsEx()
. The global _Module
is initialized, and the main dialog is displayed. (Note that ATL dialogs created with DoModal()
actually are modal, unlike MFC where all dialogs are modeless and MFC simulates modality by manually disabling the dialog's parent.) Finally, _Module
and COM are uninitialized, and the value returned by DoModal()
is used as the app's exit code.
The block around the CMainDlg
variable is important because CMainDlg
may have members that use ATL and WTL features. Those members may also use ATL/WTL features in their destructors. If the block were not present, the CMainDlg
destructor (and the destructors of the members) would run after the call to _Module.Term()
(which uninitializes ATL/WTL) and try to use ATL/WTL features, which could cause a hard-to-diagnose crash. (As a historical note, the WTL 3 AppWizard-generated code did not have a block there, and some of my apps did crash.)
You can build and run the app right away, although the dialog is pretty bare:
The code in CMainDlg
handles WM_INITDIALOG
, WM_CLOSE
, and all three buttons. Take a quick glance through the code now if you like; you should be able to follow the CMainDlg
declaration, its message map, and its message handlers.
This sample project will demonstrate how to hook up variables to the controls. Here's the app with a couple more controls; you can refer back to this diagram for the following discussions.
Since the app uses a list view control, the call to AtlInitCommonControls()
will need to be changed. Change it to:
AtlInitCommonControls ( ICC_WIN95_CLASSES );
That registers more classes than necessary, but it saves us having to remember to add ICC_*
constants when we add different types of controls to the dialog.
Using the Control Wrapper Classes
There are several ways to associate a member variable with a control. Some use plain CWindow
s (or another window interface class, like CListViewCtrl
), while others use a CWindowImpl
-derived class. Using a CWindow
is fine if you just need a temporary variable, while a CWindowImpl
is required if you need to subclass a control and handle messages sent to it.
ATL Way 1 - Attaching a CWindow
The simplest method is to declare a CWindow
or other window interface class, and call its Attach()
method. You can also use the CWindow
constructor or assignment operator to associate the variable with a control's HWND
.
This code demonstrates all three methods of associating variables with the list control:
HWND hwndList = GetDlgItem(IDC_LIST);
CListViewCtrl wndList1 (hwndList); // use constructor
CListViewCtrl wndList2, wndList3;
wndList2.Attach ( hwndList ); // use Attach method
wndList3 = hwndList; // use assignment operator
Remember that the CWindow
destructor does not destroy the window, so there is no need to detach the variables before they go out of scope. You can also use this technique with member variables if you wish - you can attach the variables in the OnInitDialog()
handler.
ATL Way 2 - CContainedWindow
CContainedWindow
is sort of halfway between using a CWindow
and a CWindowImpl
. It lets you subclass a control, then handle that control's messages in the control's parent window. This lets you put all the message handlers in the dialog class, and you don't have to write separate CWindowImpl
classes for each control. Note that you don't use CContainedWindow
to handle WM_COMMAND
, WM_NOTIFY
, or other notification messages, because those messages are always sent to the control's parent.
The actual class, CContainedWindowT
, is a template class that takes a window interface class name as its template parameter. There is a specialization CContainedWindowT<CWindow>
that works like a plain CWindow
; this is typedef'ed to the shorter name CContainedWindow
. To use a different window interface class, specify its name as the template parameter, for example CContainedWindowT<CListViewCtrl>
.
To hook up a CContainedWindow
, you do four things:
- Create a
CContainedWindowT
member variable in the dialog. - Put handlers in an
ALT_MSG_MAP
section of the dialog's message map - In the dialog's constructor, call the
CContainedWindowT
constructor and tell it whichALT_MSG_MAP
section it should route messages to. - In
OnInitDialog()
, call theCContainedWindowT::SubclassWindow()
method to associate a variable with a control.
In ControlMania1, we'll use a CContainedWindow
for the OK and Exit buttons. The dialog will handle WM_SETCURSOR
messages sent to each button, and change the cursor.
Let's go through the steps. First, we add CContainedWindow
members in CMainDlg
.
class CMainDlg : public CDialogImpl<CMainDlg>
{
// ...
protected:
CContainedWindow m_wndOKBtn, m_wndExitBtn;
};
Second, we add ALT_MSG_MAP
sections. The OK button will use section 1, while the Exit button will use section 2. This means that all messages sent to the OK button will be routed to the ALT_MSG_MAP(1)
section, and all messages sent to the Exit button will be routed to the ALT_MSG_MAP(2)
section.
class CMainDlg : public CDialogImpl<CMainDlg>
{
public:
BEGIN_MSG_MAP_EX(CMainDlg)
MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)
COMMAND_ID_HANDLER(ID_APP_ABOUT, OnAppAbout)
COMMAND_ID_HANDLER(IDOK, OnOK)
COMMAND_ID_HANDLER(IDCANCEL, OnCancel)
ALT_MSG_MAP(1)
MSG_WM_SETCURSOR(OnSetCursor_OK)
ALT_MSG_MAP(2)
MSG_WM_SETCURSOR(OnSetCursor_Exit)
END_MSG_MAP()
LRESULT OnSetCursor_OK(HWND hwndCtrl, UINT uHitTest, UINT uMouseMsg);
LRESULT OnSetCursor_Exit(HWND hwndCtrl, UINT uHitTest, UINT uMouseMsg);
};
Third, we call the CContainedWindow
constructor for each member and tell it which ALT_MSG_MAP
section to use.
CMainDlg::CMainDlg() : m_wndOKBtn(this, 1),
m_wndExitBtn(this, 2)
{
}
The constructor parameters are a CMessageMap*
and an ALT_MSG_MAP
section number. The first parameter will usually be this
, meaning that the dialog's own message map will be used, and the second parameter tells the object which ALT_MSG_MAP
section it should send its messages to.
Important note: If you are using VC 7.0 or 7.1 and WTL 7.0 or 7.1, you will run into an assert failure if a
CWindowImpl
- orCDialogImpl
-derived class does all of these things together:
- The message map uses
BEGIN_MSG_MAP
instead ofBEGIN_MSG_MAP_EX
. - The map contains an
ALT_MSG_MAP
section. - A
CContainedWindowT
variable routes messages to thatALT_MSG_MAP
section. - That
ALT_MSG_MAP
section uses the new WTL message handler macros.
See this thread in the article's discussion forum for more details. The solution is to use
BEGIN_MSG_MAP_EX
instead ofBEGIN_MSG_MAP
.
Finally, we associate each CContainedWindow
with a control.
LRESULT CMainDlg::OnInitDialog(...)
{
// ...
// Attach CContainedWindows to OK and Exit buttons
m_wndOKBtn.SubclassWindow ( GetDlgItem(IDOK) );
m_wndExitBtn.SubclassWindow ( GetDlgItem(IDCANCEL) );
return TRUE;
}
Here are the new WM_SETCURSOR
handlers:
LRESULT CMainDlg::OnSetCursor_OK (
HWND hwndCtrl, UINT uHitTest, UINT uMouseMsg )
{
static HCURSOR hcur = LoadCursor ( NULL, IDC_HAND );
if ( NULL != hcur )
{
SetCursor ( hcur );
return TRUE;
}
else
{
SetMsgHandled(false);
return FALSE;
}
}
LRESULT CMainDlg::OnSetCursor_Exit (
HWND hwndCtrl, UINT uHitTest, UINT uMouseMsg )
{
static HCURSOR hcur = LoadCursor ( NULL, IDC_NO );
if ( NULL != hcur )
{
SetCursor ( hcur );
return TRUE;
}
else
{
SetMsgHandled(false);
return FALSE;
}
}
If you wanted to use CButton
features, you could declare the variables like this:
CContainedWindowT<CButton> m_wndOKBtn;
and then the CButton
methods would be available.
You can see the WM_SETCURSOR
handlers in action when you move the cursor over the buttons:
ATL Way 3 - Subclassing
Method 3 involves creating a CWindowImpl
-derived class and using it to subclass a control. This is similar to method 2, however the message handlers go in the CWindowImpl
class instead of the dialog class.
ControlMania1 uses this method to subclass the About button in the main dialog. Here is the CButtonImpl
class, which is derived from CWindowImpl
and handles WM_SETCURSOR
:
class CButtonImpl : public CWindowImpl<CButtonImpl, CButton>
{
BEGIN_MSG_MAP_EX(CButtonImpl)
MSG_WM_SETCURSOR(OnSetCursor)
END_MSG_MAP()
LRESULT OnSetCursor(HWND hwndCtrl, UINT uHitTest, UINT uMouseMsg)
{
static HCURSOR hcur = LoadCursor ( NULL, IDC_SIZEALL );
if ( NULL != hcur )
{
SetCursor ( hcur );
return TRUE;
}
else
{
SetMsgHandled(false);
return FALSE;
}
}
};
Then in the main dialog, we declare a CButtonImpl
member variable:
class CMainDlg : public CDialogImpl<CMainDlg>
{
// ...
protected:
CContainedWindow m_wndOKBtn, m_wndExitBtn;
CButtonImpl m_wndAboutBtn;
};
And finally, in OnInitDialog()
, we subclass the button.
LRESULT CMainDlg::OnInitDialog(...)
{
// ...
// Attach CContainedWindows to OK and Exit buttons
m_wndOKBtn.SubclassWindow ( GetDlgItem(IDOK) );
m_wndExitBtn.SubclassWindow ( GetDlgItem(IDCANCEL) );
// CButtonImpl: subclass the About button
m_wndAboutBtn.SubclassWindow ( GetDlgItem(ID_APP_ABOUT) );
return TRUE;
}
WTL Way 1 - DDX_CONTROL
WTL's DDX (dialog data exchange) support works a lot like MFC's, and it can rather painlessly connect a variable to a control. To begin, you need a CWindowImpl
-derived class as in the previous example. We'll be using a new class this time, CEditImpl
, since this example will subclass the edit control. You also need to #include atlddx.h in stdafx.h to bring in the DDX code.
To add DDX support to CMainDlg
, add CWinDataExchange
to the inheritance list:
class CMainDlg : public CDialogImpl<CMainDlg>,
public CWinDataExchange<CMainDlg>
{
//...
};
Next you create a DDX map in the class, which is similar to the DoDataExchange()
function that the ClassWizard generates in MFC apps. There are several DDX_*
macros for varying types of data; the one we'll use here is DDX_CONTROL
to connect a variable with a control. This time, we'll use CEditImpl
that handles WM_CONTEXTMENU
to do something when you right-click in the control.
class CEditImpl : public CWindowImpl<CEditImpl, CEdit>
{
BEGIN_MSG_MAP_EX(CEditImpl)
MSG_WM_CONTEXTMENU(OnContextMenu)
END_MSG_MAP()
void OnContextMenu ( HWND hwndCtrl, CPoint ptClick )
{
MessageBox("Edit control handled WM_CONTEXTMENU");
}
};
class CMainDlg : public CDialogImpl<CMainDlg>,
public CWinDataExchange<CMainDlg>
{
//...
BEGIN_DDX_MAP(CMainDlg)
DDX_CONTROL(IDC_EDIT, m_wndEdit)
END_DDX_MAP()
protected:
CContainedWindow m_wndOKBtn, m_wndExitBtn;
CButtonImpl m_wndAboutBtn; CEditImpl m_wndEdit;
};
Finally, in OnInitDialog()
, we call the DoDataExchange()
function that is inherited from CWinDataExchange
. The first time DoDataExchange()
is called, it subclasses controls as necessary. So in this example, DoDataExchange()
will subclass the control with ID IDC_EDIT
, and connect it to the variable m_wndEdit
.
LRESULT CMainDlg::OnInitDialog(...)
{
// ...
// Attach CContainedWindows to OK and Exit buttons
m_wndOKBtn.SubclassWindow ( GetDlgItem(IDOK) );
m_wndExitBtn.SubclassWindow ( GetDlgItem(IDCANCEL) );
// CButtonImpl: subclass the About button
m_wndAboutBtn.SubclassWindow ( GetDlgItem(ID_APP_ABOUT) );
// First DDX call, hooks up variables to controls.
DoDataExchange(false);
return TRUE;
}
The parameter to DoDataExchange()
has the same meaning as the parameter to MFC's UpdateData()
function. We'll cover that in more detail in the next section.
If you run the ControlMania1 project, you can see all this subclassing in action. Right-clicking in the edit box will pop up the message box, and the cursor will change shape over the buttons as shown earlier.
WTL Way 2 - DDX_CONTROL_HANDLE
A new feature that was added in WTL 7.1 is the DDX_CONTROL_HANDLE
macro. In WTL 7.0, if you wanted to hook up a plain window interface class (such as CWindow
, CListViewCtrl
, etc.) with DDX, you couldn't use DDX_CONTROL
because DDX_CONTROL
only works with CWindowImpl
-derived classes. With the exception of the different base class requirement, DDX_CONTROL_HANDLE
works the same as DDX_CONTROL
.
If you're still using WTL 7.0, you can use this macro to define CWindowImpl
-derived classes that will work with DDX_CONTROL
:
#define DDX_CONTROL_IMPL(x) \
class x##_ddx : public CWindowImpl<x##_ddx, x> \
{ public: DECLARE_EMPTY_MSG_MAP() };
You can then write:
DDX_CONTROL_IMPL(CListViewCtrl)
and you will have a class called CListViewCtrl_ddx
that works like a CListViewCtrl
but will be accepted by DDX_CONTROL
.
More on DDX
DDX can, of course, actually do data exchange too. WTL supports exchanging string data between an edit box and a string variable. It can also parse a string as a number, and transfer that data between an integer or floating-point variable. And it also supports transferring the state of a check box or group of radio buttons to/from an int
.
DDX macros
Each DDX macro expands to a CWinDataExchange
method call that does the work. The macros all have the general form: DDX_FOO(controlID, variable)
. Each macro takes a different type of variable, and some like DDX_TEXT
are overloaded to accept many types.
DDX_TEXT
- Transfers text data to/from an edit box. The variable can be a
CString
,BSTR
,CComBSTR
, or statically-allocated character array. Using an array allocated withnew
will not work. DDX_INT
- Transfers numerical data between an edit box and an
int
. DDX_UINT
- Transfers numerical data between an edit box and an
unsigned int
. DDX_FLOAT
- Transfers numerical data between an edit box and a
float
ordouble
. DDX_CHECK
- Transfers the state of a check box to/from an
int
orbool
. DDX_RADIO
- Transfers the state of a group of radio buttons to/from an
int
.
DDX_CHECK
can take either an int
or bool
variable. The int
version accepts/returns the values 0, 1, and 2 (or equivalently, BST_UNCHECKED
, BST_CHECKED
, and BST_INDETERMINATE
). The bool
version (added in WTL 7.1) can be used when a check box will never be in the indeterminate state; this version accepts/returns true
if the check box is checked, or false
if it's unchecked. If the check box happens to be in the indeterminate state, DDX_CHECK
returns false
.
There is also an additional floating-point macro that was added in WTL 7.1:
DDX_FLOAT_P(controlID, variable, precision)
- Similar to
DDX_FLOAT
, but when setting the text in an edit box, the number is formatted to show at mostprecision
significant digits.
A note about using DDX_FLOAT
and DDX_FLOAT_P
: When you use this in your app, you need to add a #define to stdafx.h, before any WTL headers are included:
#define _ATL_USE_DDX_FLOAT
This is necessary because by default, floating-point support is disabled as a size optimization.
More about DoDataExchange()
You call the DoDataExchange()
method just as you call UpdateData()
in MFC. The prototype for DoDataExchange()
is:
BOOL DoDataExchange ( BOOL bSaveAndValidate = FALSE, UINT nCtlID = (UINT)-1 );
The parameters are:
bSaveAndValidate
- Flag indicating which direction the data will be transferred. Passing
TRUE
transfers from the controls to the variables. PassingFALSE
transfers from the variables to the controls. Note that the default for this parameter isFALSE
, while the default for MFC'sUpdateData()
isTRUE
. You can also use the symbolsDDX_SAVE
andDDX_LOAD
(defined asTRUE
andFALSE
respectively) as the parameter, if you find that easier to remember. nCtlID
- Pass -1 to update all controls. Otherwise, if you want to use DDX on only one control, pass the control's ID.
DoDataExchange()
returns TRUE
if the controls are updated successfully, or FALSE
if not. There are two functions you can override in your dialog to handle errors. The first, OnDataExchangeError()
is called if the data exchange fails for any reason. The default implementation in CWinDataExchange
sounds a beep and sets focus to the control that caused the error. The other function is OnDataValidateError()
, but we'll get to that in Part V when we cover DDV.
Using DDX
Let's add a couple of variables to CMainDlg
for use with DDX.
class CMainDlg : public ...
{
//...
BEGIN_DDX_MAP(CMainDlg)
DDX_CONTROL(IDC_EDIT, m_wndEdit)
DDX_TEXT(IDC_EDIT, m_sEditContents)
DDX_INT(IDC_EDIT, m_nEditNumber)
END_DDX_MAP()
protected:
// DDX variables
CString m_sEditContents;
int m_nEditNumber;
};
In the OK button handler, we first call DoDataExchange()
to transfer the data from the edit control to the two variables we just added. We then show the results in the list control.
LRESULT CMainDlg::OnOK ( UINT uCode, int nID, HWND hWndCtl )
{
CString str;
// Transfer data from the controls to member variables.
if ( !DoDataExchange(true) )
return;
m_wndList.DeleteAllItems();
m_wndList.InsertItem ( 0, _T("DDX_TEXT") );
m_wndList.SetItemText ( 0, 1, m_sEditContents );
str.Format ( _T("%d"), m_nEditNumber );
m_wndList.InsertItem ( 1, _T("DDX_INT") );
m_wndList.SetItemText ( 1, 1, str );
}
If you enter non-numerical text in the edit box, DDX_INT
will fail and call OnDataExchangeError()
. CMainDlg
overrides OnDataExchangeError()
to show a message box:
void CMainDlg::OnDataExchangeError ( UINT nCtrlID, BOOL bSave )
{
CString str;
str.Format ( _T("DDX error during exchange with control: %u"), nCtrlID );
MessageBox ( str, _T("ControlMania1"), MB_ICONWARNING );
::SetFocus ( GetDlgItem(nCtrlID) );
}
As our last DDX example, let's add a check box to show the usage of DDX_CHECK
:
This check box will never be in the indeterminate state, so we can use a bool
variable with DDX_CHECK
. Here are the changes to make hook up the check box to DDX:
class CMainDlg : public ...
{
//...
BEGIN_DDX_MAP(CMainDlg)
DDX_CONTROL(IDC_EDIT, m_wndEdit)
DDX_TEXT(IDC_EDIT, m_sEditContents)
DDX_INT(IDC_EDIT, m_nEditNumber)
DDX_CHECK(IDC_SHOW_MSG, m_bShowMsg)
END_DDX_MAP()
protected:
// DDX variables
CString m_sEditContents;
int m_nEditNumber;
bool m_bShowMsg;
};
At the end of OnOK()
, we test m_bShowMsg
to see if the check box was checked.
void CMainDlg::OnOK ( UINT uCode, int nID, HWND hWndCtl )
{
// Transfer data from the controls to member variables.
if ( !DoDataExchange(true) )
return;
//...
if ( m_bShowMsg )
MessageBox ( _T("DDX complete!"), _T("ControlMania1"),
MB_ICONINFORMATION );
}
The sample project has examples of using the other DDX_*
macros as well.
Handling Notifications from Controls
Handling notifications in WTL is similar to API-level programming. A control sends its parent a notification in the form of a WM_COMMAND
or WM_NOTIFY
message, and it's the parent's responsibility to handle it. A few other messages can be considered notifications, such as WM_DRAWITEM
, which is sent when an owner-drawn control needs to be painted. The parent window can act on the message itself, or it can reflect the message back to the control. Reflection works as in MFC - the control is able to handle notifications itself, making the code self-contained and easier to move to other projects.
Handling notifications in the parent
Notifications sent as WM_NOTIFY
and WM_COMMAND
contain various information. The parameters in a WM_COMMAND
message contain the ID of the control sending the message, the HWND
of the control, and the notification code. WM_NOTIFY
messages have all those as well as a pointer to an NMHDR
data structure. ATL and WTL have various message map macros for handling these notifications. I'll only be covering the WTL macros here, since this is a WTL article after all. Note that for all of these macros, you need to use BEGIN_MSG_MAP_EX
in your message map, and #include atlcrack.h in stdafx.h.
Message map macros
To handle a WM_COMMAND
notification, use one of the COMMAND_HANDLER_EX
macros:
COMMAND_HANDLER_EX(id, code, func)
- Handles a notification from a particular control with a particular code.
COMMAND_CODE_HANDLER_EX(id, func)
- Handles all notifications with a particular code, regardless of which control sends them.
COMMAND_ID_HANDLER_EX(code, func)
- Handles all notifications from a particular control, regardless of the code.
COMMAND_RANGE_HANDLER_EX(idFirst, idLast, func)
- Handles all notifications from controls whose IDs are in the range idFirst to idLast inclusive, regardless of the code.
COMMAND_RANGE_CODE_HANDLER_EX(idFirst, idLast, code, func)
- Handles all notifications from controls whose IDs are in the range idFirst to idLast inclusive, with a particular code.
Examples:
COMMAND_HANDLER_EX(IDC_USERNAME, EN_CHANGE, OnUsernameChange)
: HandlesEN_CHANGE
when sent from the edit box with ID IDC_USERNAME.COMMAND_ID_HANDLER_EX(IDOK, OnOK)
: Handles all notifications sent from the control with ID IDOK.COMMAND_RANGE_CODE_HANDLER_EX(IDC_MONDAY, IDC_FRIDAY, BN_CLICKED, OnDayClicked)
: Handles allBN_CLICKED
notifications from the controls with IDs in the range IDC_MONDAY to IDC_FRIDAY
There are also macros for handling WM_NOTIFY
messages. They work just like the macros above, but their names start with "NOTIFY_
" instead of "COMMAND_
".
The prototype for a WM_COMMAND
handler is:
void func ( UINT uCode, int nCtrlID, HWND hwndCtrl );
WM_COMMAND
notifications do not use a return value, so the handlers return void
. The prototype for a WM_NOTIFY
handler is:
LRESULT func ( NMHDR* phdr );
The return value of the handler is used as the message result. This is different from MFC, where the handler receives an LRESULT*
and sets the message result through that variable. The notification code and the HWND
of the control that sent the notification are available in the NMHDR
struct, as the code
and hwndFrom
members. Just as in MFC, if the notification sends a struct that is not a plain NMHDR
, your handler should cast the phdr
parameter to the correct type.
We'll add a notification handler to CMainDlg
that handles LVN_ITEMCHANGED
sent from the list control, and shows the currently-selected item in the dialog. We start by adding the message map macro and message handler:
class CMainDlg : public ...
{
BEGIN_MSG_MAP_EX(CMainDlg)
NOTIFY_HANDLER_EX(IDC_LIST, LVN_ITEMCHANGED, OnListItemchanged)
END_MSG_MAP()
LRESULT OnListItemchanged(NMHDR* phdr);
//...
};
Here's the message handler:
LRESULT CMainDlg::OnListItemchanged ( NMHDR* phdr )
{
NMLISTVIEW* pnmlv = (NMLISTVIEW*) phdr;
int nSelItem = m_wndList.GetSelectedIndex();
CString sMsg;
// If no item is selected, show "none". Otherwise, show its index.
if ( -1 == nSelItem )
sMsg = _T("(none)");
else
sMsg.Format ( _T("%d"), nSelItem );
SetDlgItemText ( IDC_SEL_ITEM, sMsg );
return 0; // retval ignored
}
This handler doesn't use the phdr
parameter, but I included the cast to NMLISTVIEW*
as a demonstration.
Reflecting Notifications
If you have a CWindowImpl
-derived class that implements a control, like our CEditImpl
from before, you can handle notifications in that class, instead of the parent dialog. This is called reflecting the notifications, and works similarly to MFC's message reflection. The difference is that both the parent and the control participate in the reflection, whereas in MFC only the control does.
When you want to reflect notifications back to the control classes, you just add one macro to the dialog's message map, REFLECT_NOTIFICATIONS()
:
class CMainDlg : public ...
{
public:
BEGIN_MSG_MAP_EX(CMainDlg)
//...
NOTIFY_HANDLER_EX(IDC_LIST, LVN_ITEMCHANGED, OnListItemchanged)
REFLECT_NOTIFICATIONS()
END_MSG_MAP()
};
That macro adds some code to the message map that handles any notification messages that are not handled by any earlier macros. The code examines the HWND
of the message and sends the message to that window. The value of the message is changed, however, to a value used by OLE controls which have a similar message reflection system. The new value is called OCM_xxx
instead of WM_xxx
, but otherwise the message is handled just like non-reflected messages.
There are 18 messages which are reflected:
- Control notifications:
WM_COMMAND
,WM_NOTIFY
,WM_PARENTNOTIFY
- Owner drawing:
WM_DRAWITEM
,WM_MEASUREITEM
,WM_COMPAREITEM
,WM_DELETEITEM
- List box keyboard messages:
WM_VKEYTOITEM
,WM_CHARTOITEM
- Others:
WM_HSCROLL
,WM_VSCROLL
,WM_CTLCOLOR*
In the control class, you add handlers for the reflected messages you are interested in, then at the end, add DEFAULT_REFLECTION_HANDLER()
. DEFAULT_REFLECTION_HANDLER()
ensures that unhanded messages are properly routed on to DefWindowProc()
. Here is a simple owner-drawn button class that handles the reflected WM_DRAWITEM
.
class CODButtonImpl : public CWindowImpl<CODButtonImpl, CButton>
{
public:
BEGIN_MSG_MAP_EX(CODButtonImpl)
MSG_OCM_DRAWITEM(OnDrawItem)
DEFAULT_REFLECTION_HANDLER()
END_MSG_MAP()
void OnDrawItem ( UINT idCtrl, LPDRAWITEMSTRUCT lpdis )
{
// do drawing here...
}
};
WTL macros for handling reflected messages
We just saw one of the WTL macros for reflected messages, MSG_OCM_DRAWITEM
. There are MSG_OCM_*
macros for the other 17 messages that can be reflected as well. Since WM_NOTIFY
and WM_COMMAND
have parameters that need to be unpacked, WTL provides special macros for them in addition to MSG_OCM_COMMAND
and MSG_OCM_NOTIFY
. These macros work like COMMAND_HANDLER_EX
and NOTIFY_HANDLER_EX
, but have "REFLECTED_
" prepended. For example, a tree control class could have this message map:
class CMyTreeCtrl : public CWindowImpl<CMyTreeCtrl, CTreeViewCtrl>
{
public:
BEGIN_MSG_MAP_EX(CMyTreeCtrl)
REFLECTED_NOTIFY_CODE_HANDLER_EX(TVN_ITEMEXPANDING, OnItemExpanding)
DEFAULT_REFLECTION_HANDLER()
END_MSG_MAP()
LRESULT OnItemExpanding ( NMHDR* phdr );
};
If you check out the ControlMania1 dialog in the sample code, there is a tree control that handles TVN_ITEMEXPANDING
as shown above. The CMainDlg
member m_wndTree
is connected to the tree control using DDX, and CMainDlg
reflects notifications. The tree's OnItemExpanding()
handler looks like this:
LRESULT CBuffyTreeCtrl::OnItemExpanding ( NMHDR* phdr )
{
NMTREEVIEW* pnmtv = (NMTREEVIEW*) phdr;
if ( pnmtv->action & TVE_COLLAPSE )
return TRUE; // don't allow it
else
return FALSE; // allow it
}
If you run ControlMania1 and click the +/- buttons in the tree, you'll see this handler in action - once you expand a node, it will not collapse again.
Odds & Ends
Dialog Fonts
If you're picky about UI like me, and you're using Win 2000 or XP, you might be wondering why the dialogs are using MS Sans Serif instead of Tahoma. Since VC 6 is so old, the resource files it generates work fine for NT 4, but not later versions of NT. You can fix this, but it requires hand-editing the resource file.
There are three things you need to change in each dialog's entry in the resource file
- The dialog type: Change
DIALOG
toDIALOGEX
. - The window styles: Add
DS_SHELLFONT
- The dialog font: Change MS Sans Serif to MS Shell Dlg
Unfortunately, the first two changes get lost whenever you modify and save the resources, so you'll need to make the changes repeatedly. Here's a "before" picture of a dialog:
IDD_ABOUTBOX DIALOG DISCARDABLE 0, 0, 187, 102
STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "About"
FONT 8, "MS Sans Serif"
BEGIN
...
END
And the "after" picture:
IDD_ABOUTBOX DIALOGEX DISCARDABLE 0, 0, 187, 102
STYLE DS_SHELLFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "About"
FONT 8, "MS Shell Dlg"
BEGIN
...
END
After making these changes, the dialog will use Tahoma on newer OSes, but still use MS Sans Serif when necessary on older OSes.
In VC 7, you just need to change one setting in the dialog editor to use the right font:
When you change Use System Font to True, the editor will change the font to MS Shell Dlg for you.
_ATL_MIN_CRT
As explained in this VC Forum FAQ, ATL contains an optimization that lets you create an app that does not link to the C runtime library (CRT). This optimization is enabled by adding the _ATL_MIN_CRT
symbol to the preprocessor settings. An AppWizard-generated app contains this symbol in the Release configurations. Since I've never written a non-trivial app that didn't need something from the CRT, I always remove that symbol. You must remove it, in any case, if you use floating-point features in CString
or DDX.
Up Next
In Part V, we'll cover dialog data validation (DDV), the new controls in WTL, and some advanced UI features like owner draw and custom draw.
Copyright and license
This article is copyrighted material, (c)2003-2005 by Michael Dunn. I realize this isn't going to stop people from copying it all around the 'net, but I have to say it anyway. If you are interested in doing a translation of this article, please email me to let me know. I don't foresee denying anyone permission to do a translation, I would just like to be aware of the translation so I can post a link to it here.
The demo code that accompanies this article is released to the public domain. I release it this way so that the code can benefit everyone. (I don't make the article itself public domain because having the article available only on CodeProject helps both my own visibility and the CodeProject site.) If you use the demo code in your own application, an email letting me know would be appreciated (just to satisfy my curiosity about whether folks are benefiting from my code) but is not required. Attribution in your own source code is also appreciated but not required.