WTL for MFC Programmers, Part IV - Dialogs and Controls

WTL for MFC Programmers, Part IV - Dialogs and Controls

Contents

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 CAxDialogImplCAxDialogImpl 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:

  1. Create the dialog resource
  2. Write a new class derived from CDialogImpl
  3. 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: CStaticCButtonCListBoxCComboBoxCEditCScrollBarCDragListBox
  • Common controls: CImageListCListViewCtrl (CListCtrl in MFC), CTreeViewCtrl (CTreeCtrl in MFC), CHeaderCtrlCToolBarCtrlCStatusBarCtrlCTabCtrlCToolTipCtrlCTrackBarCtrl (CSliderCtrl in MFC), CUpDownCtrl (CSpinButtonCtrl in MFC), CProgressBarCtrlCHotKeyCtrlCAnimateCtrlCRichEditCtrlCReBarCtrlCComboBoxExCDateTimePickerCtrlCMonthCalendarCtrlCIPAddressCtrl
  • Common control wrappers not in MFC: CPagerCtrlCFlatScrollBarCLinkCtrl (clickable hyperlink, available in XP and later)

There are also a few WTL-specific classes: CBitmapButtonCCheckListViewCtrl (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 CBitmapButtonCCheckListViewCtrl, 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:

 [AppWizard page 1 - 21K]

 [AppWizard page 1 - 25K]

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:

 [Bare dialog - 9K]

The code in CMainDlg handles WM_INITDIALOGWM_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.

 [Add'l controls - 12K]

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 CWindows (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_COMMANDWM_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:

  1. Create a CContainedWindowT member variable in the dialog.
  2. Put handlers in an ALT_MSG_MAP section of the dialog's message map
  3. In the dialog's constructor, call the CContainedWindowT constructor and tell it which ALT_MSG_MAP section it should route messages to.
  4. In OnInitDialog(), call the CContainedWindowT::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- or CDialogImpl-derived class does all of these things together:

  • The message map uses BEGIN_MSG_MAP instead of BEGIN_MSG_MAP_EX.
  • The map contains an ALT_MSG_MAP section.
  • CContainedWindowT variable routes messages to that ALT_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 of BEGIN_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:

 [OK button cursor - 10K]   [Exit button cursor - 10K]

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 CWindowCListViewCtrl, 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 CStringBSTRCComBSTR, or statically-allocated character array. Using an array allocated with new 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 or double.
DDX_CHECK
Transfers the state of a check box to/from an int or bool.
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_UNCHECKEDBST_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 most precision 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. Passing FALSE transfers from the variables to the controls. Note that the default for this parameter is FALSE, while the default for MFC's UpdateData() is TRUE. You can also use the symbols DDX_SAVE and DDX_LOAD (defined as TRUE and FALSE 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 );
}

 [DDX results - 11K]

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) );
}

 [DDX error msg - 18K]

As our last DDX example, let's add a check box to show the usage of DDX_CHECK:

 [Msg checkbox - 12K]

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): Handles EN_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 all BN_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_COMMANDWM_NOTIFYWM_PARENTNOTIFY
  • Owner drawing: WM_DRAWITEMWM_MEASUREITEMWM_COMPAREITEMWM_DELETEITEM
  • List box keyboard messages: WM_VKEYTOITEMWM_CHARTOITEM
  • Others: WM_HSCROLLWM_VSCROLLWM_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

  1. The dialog type: Change DIALOG to DIALOGEX.
  2. The window styles: Add DS_SHELLFONT
  3. 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:

 [VC7 dlg editor setting - 8K]

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.

posted @ 2022-12-13 14:18  小风风的博客  阅读(36)  评论(0编辑  收藏  举报