Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

WTL for MFC Programmers, Part V - Advanced Dialog UI Classes

0.00/5 (No votes)
31 Dec 2005 2  
Using the new WTL classes that implement advanced dialog UI elements

Contents

Introduction to Part V

In the last article, we saw some of WTL's features relating to dialogs and controls that worked like the corresponding MFC classes. In this one, we'll cover some new WTL classes that implement some more advanced UI features: Owner draw and custom draw, new WTL controls, UI updating, and dialog data validation (DDV).

Specialized Owner Draw and Custom Draw Classes

Since owner drawn and custom drawn controls are so common in GUI work, WTL provides mix-in classes that handle some of the grunt work. We'll cover each of them next, as we start on the sequel to the last sample project, ControlMania2. If you are following along by creating a project with the AppWizard, be sure to make this dialog modeless. It has to be modeless in order for UI updating to work properly. I'll give more details on this in the section on UI updating.

COwnerDraw

Owner drawing involves handling up to four messages: WM_MEASUREITEM, WM_DRAWITEM, WM_COMPAREITEM, and WM_DELETEITEM. The COwnerDraw class, defined in atlframe.h, simplifies your code since you don't need messages handlers for all those messages. Instead, you chain messages to COwnerDraw, and it calls overridable functions that you implement in your class.

How you chain messages depends on whether you are reflecting messages to the control or not. Here is the COwnerDraw message map, which will make the distinction clear:

template <class T> class COwnerDraw
{
public:
  BEGIN_MSG_MAP(COwnerDraw<T>)
    MESSAGE_HANDLER(WM_DRAWITEM, OnDrawItem)
    MESSAGE_HANDLER(WM_MEASUREITEM, OnMeasureItem)
    MESSAGE_HANDLER(WM_COMPAREITEM, OnCompareItem)
    MESSAGE_HANDLER(WM_DELETEITEM, OnDeleteItem)
  ALT_MSG_MAP(1)
    MESSAGE_HANDLER(OCM_DRAWITEM, OnDrawItem)
    MESSAGE_HANDLER(OCM_MEASUREITEM, OnMeasureItem)
    MESSAGE_HANDLER(OCM_COMPAREITEM, OnCompareItem)
    MESSAGE_HANDLER(OCM_DELETEITEM, OnDeleteItem)
  END_MSG_MAP()
};

Notice that the main section of the map handles the WM_* messages, while the ALT_MSG_MAP(1) section handles the reflected versions, OCM_*. The owner draw notifications are like WM_NOTIFY, in that you can handle them in the control's parent, or reflect them back to the control. If you choose the former, you chain messages directly to COwnerDraw:

// C++ class for a dialog that contains owner-drawn controls

class CSomeDlg : public CDialogImpl<CSomeDlg>,
                 public COwnerDraw<CSomeDlg>, ...
{
  BEGIN_MSG_MAP(CSomeDlg)
    //...

    CHAIN_MSG_MAP(COwnerDraw<CSomeDlg>)
  END_MSG_MAP()
 
  void DrawItem ( LPDRAWITEMSTRUCT lpdis );
};

However, if you want the control to handle the messages, you need to chain to the ALT_MSG_MAP(1) section, using the CHAIN_MSG_MAP_ALT macro:

// C++ class that implements an owner-drawn button

class CMyButton : public CWindowImpl<CMyButton, CButton>,
                  public COwnerDraw<CMyButton>, ...
{
  BEGIN_MSG_MAP(CMyButton)
    //...

    CHAIN_MSG_MAP_ALT(COwnerDraw<CMyButton>, 1)
    DEFAULT_REFLECTION_HANDLER()
  END_MSG_MAP()
 
  void DrawItem ( LPDRAWITEMSTRUCT lpdis );
};

COwnerDraw unpacks the parameters sent with the message, then calls an implementation function in your class. In the example above, the classes implement DrawItem(), which is called when WM_DRAWITEM or OCM_DRAWITEM is chained to COwnerDraw. The methods you can override are:

void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct);
void MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct);
int  CompareItem(LPCOMPAREITEMSTRUCT lpCompareItemStruct);
void DeleteItem(LPDELETEITEMSTRUCT lpDeleteItemStruct);

If for some reason you don't want to handle a message in an override, you can call SetMsgHandled(false) and the message will be passed along to any other handlers that might be later in the message map.

For ControlMania2, we'll start with the tree control from ControlMania1, and add an owner-drawn button and handle the reflected WM_DRAWITEM in the button class. Here's the new button in the resource editor:

 [Owner-drawn button 1 - 7K]

Now we need a class that implements this button:

class CODButtonImpl : public CWindowImpl<CODButtonImpl, CButton>,
                      public COwnerDraw<CODButtonImpl>
{
public:
    BEGIN_MSG_MAP_EX(CODButtonImpl)
        CHAIN_MSG_MAP_ALT(COwnerDraw<CODButtonImpl>, 1)
        DEFAULT_REFLECTION_HANDLER()
    END_MSG_MAP()
 
    void DrawItem ( LPDRAWITEMSTRUCT lpdis );
};

DrawItem() uses GDI calls like BitBlt() to draw a picture on the button face. This code should be easy to follow, since once again the WTL class names and methods are similar to MFC.

void CODButtonImpl::DrawItem ( LPDRAWITEMSTRUCT lpdis )
{
// NOTE: m_bmp is a CBitmap init'ed in the constructor.

CDCHandle dc = lpdis->hDC;
CDC dcMem;
 
    dcMem.CreateCompatibleDC ( dc );
    dc.SaveDC();
    dcMem.SaveDC();
 
    // Draw the button's background, red if it has the focus, blue if not.

    if ( lpdis->itemState & ODS_FOCUS ) 
        dc.FillSolidRect ( &lpdis->rcItem, RGB(255,0,0) );
    else
        dc.FillSolidRect ( &lpdis->rcItem, RGB(0,0,255) );
 
    // Draw the bitmap in the top-left, or offset by 1 pixel if the button

    // is clicked.

    dcMem.SelectBitmap ( m_bmp );
 
    if ( lpdis->itemState & ODS_SELECTED ) 
        dc.BitBlt ( 1, 1, 80, 80, dcMem, 0, 0, SRCCOPY );
    else
        dc.BitBlt ( 0, 0, 80, 80, dcMem, 0, 0, SRCCOPY );
 
    dcMem.RestoreDC(-1);
    dc.RestoreDC(-1);
}

Here's what the button looks like:

 [Owner-drawn button - 19K]

CCustomDraw

CCustomDraw works similarly to COwnerDraw in the way you handle NM_CUSTOMDRAW messages and chain them. CCustomDraw has an overridable method for each of the custom draw stages:

DWORD OnPrePaint(int idCtrl, LPNMCUSTOMDRAW lpNMCD);
DWORD OnPostPaint(int idCtrl, LPNMCUSTOMDRAW lpNMCD);
DWORD OnPreErase(int idCtrl, LPNMCUSTOMDRAW lpNMCD);
DWORD OnPostErase(int idCtrl, LPNMCUSTOMDRAW lpNMCD);
 
DWORD OnItemPrePaint(int idCtrl, LPNMCUSTOMDRAW lpNMCD);
DWORD OnItemPostPaint(int idCtrl, LPNMCUSTOMDRAW lpNMCD);
DWORD OnItemPreErase(int idCtrl, LPNMCUSTOMDRAW lpNMCD);
DWORD OnItemPostEraset(int idCtrl, LPNMCUSTOMDRAW lpNMCD);
 
DWORD OnSubItemPrePaint(int idCtrl, LPNMCUSTOMDRAW lpNMCD);

The default handlers all return CDRF_DODEFAULT, so you only need to override a method if you need to do your own drawing or return a different value.

You might have noticed that in the last screen shot, "Dawn" was shown in green. That is achieved by using a new class (CBuffyTreeCtrl in the source) derived from CTreeCtrl, which chains messages to CCustomDraw and overrides OnPrePaint() and OnItemPrePaint(). When the tree is filled, that item data for the "Dawn" node is set to 1, and OnItemPrePaint() checks for that value and changes the text color if it's found.

DWORD CBuffyTreeCtrl::OnPrePaint(
    int idCtrl, LPNMCUSTOMDRAW lpNMCD)
{
    return CDRF_NOTIFYITEMDRAW;
}
 
DWORD CBuffyTreeCtrl::OnItemPrePaint(
    int idCtrl, LPNMCUSTOMDRAW lpNMCD)
{
    if ( 1 == lpNMCD->lItemlParam )
        pnmtv->clrText = RGB(0,128,0);
 
    return CDRF_DODEFAULT;
}

Just as with COwnerDraw, you can call SetMsgHandled(false) in a custom draw message handler to have the message passed on to any other handlers in the message map.

New WTL Controls

WTL has a few new controls of its own that either improve on other wrappers (like CTreeViewCtrlEx), or provide new functionality that isn't in the built-in controls (like CHyperLink).

CBitmapButton

WTL's CBitmapButton, declared in atlctrlx.h, is rather easier to use than the MFC version. The WTL class uses an image list instead of four separate bitmap resources, which means you can keep multiple button images in one bitmap and reduce your GDI usage a bit. This is especially nice if you have a lot of graphics and your app runs on Windows 9x, because using lots of separate graphics can quickly exhaust GDI resources and bring down the system.

CBitmapButton is a CWindowImpl-derived class that has many features including: automatic sizing of the control, automatic generation of a 3D border, hot-tracking support, and several images per button for the various states the control can be in.

In ControlMania2, we'll use a CBitmapButton along side the owner-draw button we created earlier. We start by adding a CBitmapButton member called m_wndBmpBtn to CMainDlg. We then connect it to the new button in the usual way, either by calling SubclassWindow() or using DDX. We load a bitmap into an image list, then tell the button to use that image list. We also tell the button which image in the image list corresponds to which control state. Here's the section from OnInitDialog() that sets up the button:

    // Set up the bitmap button

CImageList iml;
 
    iml.CreateFromImage ( IDB_ALYSON_IMGLIST, 81, 1, CLR_NONE,
                          IMAGE_BITMAP, LR_CREATEDIBSECTION );
 
    m_wndBmpBtn.SubclassWindow ( GetDlgItem(IDC_ALYSON_BMPBTN) );
    m_wndBmpBtn.SetToolTipText ( _T("Alyson") );
    m_wndBmpBtn.SetImageList ( iml );
    m_wndBmpBtn.SetImages ( 0, 1, 2, 3 );

By default, the button assumes ownership of the image list, so OnInitDialog() must not delete the image list it creates. Here is the new button in its default state. Notice how the control is resized to exactly fit the size of the image.

 [WTL bitmap button - 19K]

Since CBitmapButton is a very useful class, I'll cover its public methods here.

CBitmapButton methods

The class CBitmapButtonImpl contains all the code to implement a button, but unless you need to override a method or message handler, you can use CBitmapButton for your controls.

CBitmapButtonImpl(DWORD dwExtendedStyle = BMPBTN_AUTOSIZE,
                  HIMAGELIST hImageList = NULL)

The constructor sets up the button extended style (not to be confused with its window styles) and can assign an image list. Usually the defaults are sufficient, since you can set both attributes with other methods.

BOOL SubclassWindow(HWND hWnd)

SubclassWindow() is overridden to perform the subclassing and initialize internal data that the class keeps.

DWORD GetBitmapButtonExtendedStyle()
DWORD SetBitmapButtonExtendedStyle(DWORD dwExtendedStyle,
                                   DWORD dwMask = 0)

CBitmapButton supports some extended styles that affect the appearance or operation of the button:

BMPBTN_HOVER
Enables hot-tracking. When the cursor is over the button, it will be drawn in the focused state.
BMPBTN_AUTO3D_SINGLE, BMPBTN_AUTO3D_DOUBLE
Automatically generates a 3D border around the image, as well as a focus rectangle when the button has the focus. In addition, if you do not provide an image for the pressed state, one is generated for you. BMPBTN_AUTO3D_DOUBLE produces a slightly thicker border.
BMPBTN_AUTOSIZE
Makes the button resize itself to match the size of the image. This style is the default.
BMPBTN_SHAREIMAGELISTS
If set, the button object does not destroy the image list used to hold the button images. If not set, the image list is destroyed by the CBitmapButton destructor.
BMPBTN_AUTOFIRE
If set, clicking the button and holding down the mouse button generates repeated WM_COMMAND messages.

When calling SetBitmapButtonExtendedStyle(), the dwMask parameter controls which styles are affected. Use the default of 0 to have the new styles completely replace the old ones.

HIMAGELIST GetImageList()
HIMAGELIST SetImageList(HIMAGELIST hImageList)

Use GetImageList() and SetImageList() to associate an image list with the button, or get the image list currently associated with the button.

int  GetToolTipTextLength()
bool GetToolTipText(LPTSTR lpstrText, int nLength)
bool SetToolTipText(LPCTSTR lpstrText)

CBitmapButton supports showing a tooltip when the mouse hovers over the button. Call GetToolTipText() and SetToolTipText() to get or set the text to show in the tooltip.

void SetImages(int nNormal, int nPushed = -1,
               int nFocusOrHover = -1, int nDisabled = -1)

Call SetImages() to tell the button which image in the image list to use for which button state. The parameters are all 0-based indexes into the image list. nNormal is required, but the others are optional. Passing -1 indicates that there is no image for the corresponding state.

CCheckListViewCtrl

CCheckListViewCtrl, defined in atlctrlx.h, is a CWindowImpl-derived class that implements a list view control containing check boxes. This is different from MFC's CCheckListBox, which uses a list box, not a list view. CCheckListViewCtrl is quite simple, since the class adds minimal functionality on its own. However, it does introduce a new helper class, CCheckListViewCtrlImplTraits, that is like CWinTraits but with a third template parameter that is the extended list view styles to use for the control. If you don't define your own set of CCheckListViewCtrlImplTraits, the class uses these styles by default: LVS_EX_CHECKBOXES | LVS_EX_FULLROWSELECT.

Here is a sample traits definition that uses different extended list view styles, plus a new class that uses those traits. (Note that you must include LVS_EX_CHECKBOXES in the extended list view styles, or else you will get an assert failed message.)

typedef CCheckListViewCtrlImplTraits<
    WS_CHILD | WS_VISIBLE | LVS_REPORT, 
    WS_EX_CLIENTEDGE,
    LVS_EX_CHECKBOXES | LVS_EX_GRIDLINES | LVS_EX_UNDERLINEHOT |
      LVS_EX_ONECLICKACTIVATE> CMyCheckListTraits;
 
class CMyCheckListCtrl :
    public CCheckListViewCtrlImpl<CMyCheckListCtrl, CListViewCtrl, 
                                  CMyCheckListTraits>
{
private:
    typedef CCheckListViewCtrlImpl<CMyCheckListCtrl, CListViewCtrl, 
                                   CMyCheckListTraits> baseClass;
public:
    BEGIN_MSG_MAP(CMyCheckListCtrl)
        CHAIN_MSG_MAP(baseClass)
    END_MSG_MAP()
};

CCheckListViewCtrl methods

BOOL SubclassWindow(HWND hWnd)

When you subclass an existing list view control, SubclassWindow() looks at the extended list view styles in the associated CCheckListViewCtrlImplTraits class and applies them to the control. The first two template parameters of the traits class (windows styles and extended window styles) are not used.

BOOL GetCheckState(int nIndex)
BOOL SetCheckState(int nItem, BOOL bCheck)

These methods are actually in CListViewCtrl. SetCheckState() takes an item index and a boolean indicating whether to check or uncheck that item. GetCheckState() takes just an index and returns the current checked state of that item.

void CheckSelectedItems(int nCurrItem)

This method takes an item index. It toggles the check state of that item, which must be selected, and changes the check state of all other selected items to match. You probably won't use this method yourself, since CCheckListViewCtrl handles checking items when the check box is clicked or the user presses the space bar.

Here's how a CCheckListViewCtrl looks in ControlMania2:

 [Check list ctrl - 22K]

CTreeViewCtrlEx and CTreeItem

These two classes make it easier to use tree control features by wrapping an HTREEITEM. A CTreeItem object keeps an HTREEITEM and a pointer to the tree control that contains the item. You can then perform operations on that item using just CTreeItem; you don't have to refer to the tree control in every call. CTreeViewCtrlEx is like CTreeViewCtrl, however its methods deal with CTreeItems instead of HTREEITEMs. So for example, when you call InsertItem(), it returns a CTreeItem instead of an HTREEITEM. You can then operate on the newly-inserted item using the CTreeItem. Here's an example:

// Using plain HTREEITEMs:

HTREEITEM hti, hti2;
 
    hti = m_wndTree.InsertItem ( "foo", TVI_ROOT, TVI_LAST );
    hti2 = m_wndTree.InsertItem ( "bar", hti, TVI_LAST );
    m_wndTree.SetItemData ( hti2, 37 );
 
// Using CTreeItems:

CTreeItem ti, ti2;
 
    ti = m_wndTreeEx.InsertItem ( "baz", TVI_ROOT, TVI_LAST );
    ti2 = ti.AddTail ( "yen", 0 );
    ti2.SetData ( 42 );

CTreeItem has a method corresponding to every CTreeViewCtrl method that takes an HTREEITEM, just like CWindow contains methods corresponding to APIs that take an HWND. Check out the ControlMania2 code, which demonstrates more methods of CTreeViewCtrlEx and CTreeItem.

CHyperLink

CHyperLink is a CWindowImpl-derived class that can subclass a static text control and make it into a clickable hyperlink. CHyperLink automatically handles drawing the link (following the user's IE color preferences) and also supports keyboard navigation. The class CHyperLinkImpl is the base class for CHyperLink and contains all the code to implement a link, but unless you need to override a method or message handler, you can stick with CHyperLink for your controls.

The default behavior of a CHyperLink control is to launch a URL in your default browser when the link is clicked. If the subclassed static control has the WS_TABSTOP style, you can also tab to the control and press the spacebar or Enter key to click the link. CHyperLink will also show a tooltip when the cursor hovers over the link. By default, CHyperLink uses the static control's text as the default for both the URL and the tooltip text, but you can change those properties using method calls as explained below.

In WTL 7.1, many features were added to CHyperLink; these new features are enabled using extended styles. The styles and their usage is explained after the method list.

CHyperLink methods

These are the CHyperLink methods that you'll commonly use. There are others for calculating the control size, parsing the link text, and so on; you can check out the class in atlctrlx.h to see the full list.

CHyperLinkImpl ( DWORD dwExtendedStyle = HLINK_UNDERLINED )
CHyperLink()

The CHyperLinkImpl constructor takes the extended styles to apply to the control. CHyperLink is missing a matching constructor, but you can use SetHyperLinkExtendedStyle() to set those styles.

BOOL SubclassWindow(HWND hWnd)

SubclassWindow() is overridden to perform the subclassing, then initialize internal data that the class keeps. This is called for you automatically if you associate a hyperlink variable with a static control via DDX_CONTROL, or you can call it yourself to subclass a control manually.

DWORD GetHyperLinkExtendedStyle()
DWORD SetHyperLinkExtendedStyle(DWORD dwExtendedStyle, DWORD dwMask = 0)

Gets or sets the extended styles for the control. You must set the extended style before calling SubclassWindow() or Create() so the control knows how to paint the text.

bool GetLabel(LPTSTR lpstrBuffer, int nLength)
bool SetLabel(LPCTSTR lpstrLabel)

Gets or sets the text to use in the control. If you do not set the label text, the label is set to the static control's window text.

bool GetHyperLink(LPTSTR lpstrBuffer, int nLength)
bool SetHyperLink(LPCTSTR lpstrLink)

Gets or sets the URL associated with the control. If you do not set the hyperlink, the hyperlink is set to the static control's window text.

bool GetToolTipText(LPTSTR lpstrBuffer, int nLength)
bool SetToolTipText(LPCTSTR lpstrToolTipText)

Gets or sets the text displayed in a tooltip when the cursor hovers over the link. However, these methods can only be used with links that have the HLINK_COMMANDBUTTON or HLINK_NOTIFYBUTTON extended styles. See below for more information on the tooltip.

Here is how a "plain" hyperlink control looks in the ControlMania2 dialog:

 [WTL hyperlink - 21K]

The URL is set with this call in OnInitDialog():

    m_wndLink.SetHyperLink ( _T("http://www.codeproject.com/") );

CHyperLink extended styles

The new WTL 7.1 features are enabled by setting the appropriate extended style bits. The styles are:

HLINK_UNDERLINED
The link text will be underlined. This is the default behavior.
HLINK_NOTUNDERLINED
The link text will never be underlined.
HLINK_UNDERLINEHOVER
The link text will be underlined only when the cursor is over the link.
HLINK_COMMANDBUTTON
When the link is clicked, the control sends a WM_COMMAND message (with the notification code set to BN_CLICKED) to the control's parent window.
HLINK_NOTIFYBUTTON
When the link is clicked, the control sends a WM_NOTIFY message (with the notification code set to NM_CLICK) to the control's parent window.
HLINK_USETAGS
The control considers only text inside an <a> tag to be the link, other text is drawn normally.
HLINK_USETAGSBOLD
Same as HLINK_USETAGS, but the text inside the <a> tag is drawn bold. When this style is set, the underlining extended styles are ignored, and the link text is never underlined.
HLINK_NOTOOLTIP
The control will not display a tooltip.

If neither the HLINK_COMMANDBUTTON nor HLINK_NOTIFYBUTTON style is set, then the CHyperLink object calls its Navigate() method when clicked. Navigate() calls ShellExecuteEx() to launch a URL in the default browser. If you want to perform some other action when the link is clicked, set HLINK_COMMANDBUTTON or HLINK_NOTIFYBUTTON and then handle the notification message that the control sends.

Other CHyperLink details

You can set the SS_CENTER or SS_RIGHT style on a static control to have the hyperlink text center- or right-aligned. However, if the control has the HLINK_USETAGS or HLINK_USETAGSBOLD style, those bits are ignored and the text is always left-aligned.

If you're using a CHyperLink to open a URL (that is, you haven't set HLINK_COMMANDBUTTON or HLINK_NOTIFYBUTTON), you cannot change the tooltip text with SetToolTipText(). However, you can access the tooltip control directly through the CHyperLink member m_tip and set the text using AddTool():

  m_wndLink.m_tip.AddTool ( m_wndLink, _T("Clickety!"), &m_wndLink.m_rcLink, 1 );

Note that there is a breaking change here from WTL 7.0: CHyperLink uses a tool ID of 1 in WTL 7.1. In WTL 7.0, the ID was the same as the window handle, and you could change the text using m_tip.UpdateTipText(). I didn't have any luck using UpdateTipText() in WTL 7.1; the above code duplicates what CHyperLink::Init() does to set up the tooltip initially.

Due to some painting problems, the HLINK_USETAGS and HLINK_USETAGSBOLD styles are best used when the link text is always going to be on one line. The painting code finds the text within <a> tags and splits the text into three parts: before the tag, within the tag, after the tag. However, if the text for one part requires word-breaking, it will wrap incorrectly. I have illustrated this in ControlMania2 in a separate dialog:

 [Link drawing problems - 13K]

You should also make sure that HLINK_UNDERLINEHOVER is not set along with HLINK_USETAGSBOLD, since that will cause some empty space to appear after the link text, as shown in the first hyperlink above.

UI Updating Dialog Controls

UI updating controls in a dialog is much easier than in MFC. In MFC, you have to know about the undocumented WM_KICKIDLE message and how to handle it and trigger control updating. In WTL, there are no such tricks, although there is a bug in the AppWizard that requires you to add one line of code.

The first thing to remember is that the dialog must be modeless. This is necessary because for CUpdateUI to do its job, your app needs to be in control of the message loop. If you make the dialog modal, the system handles the message loop, so idle handlers won't get called. Since CUpdateUI does its work at idle time, no idle processing means no UI updating.

ControlMania2's dialog is modeless, and the first part of the class definition resembles a frame window class:

class CMainDlg : public CDialogImpl<CMainDlg>, public CUpdateUI<CMainDlg>,
                 public CMessageFilter, public CIdleHandler
{
public:
    enum { IDD = IDD_MAINDLG };
 
    BOOL PreTranslateMessage(MSG* pMsg);
    BOOL OnIdle();
 
    BEGIN_MSG_MAP_EX(CMainDlg)
        MSG_WM_INITDIALOG(OnInitDialog)
        COMMAND_ID_HANDLER_EX(IDOK, OnOK)
        COMMAND_ID_HANDLER_EX(IDCANCEL, OnCancel)
        COMMAND_ID_HANDLER_EX(IDC_ALYSON_BTN, OnAlysonODBtn)
    END_MSG_MAP()
 
    BEGIN_UPDATE_UI_MAP(CMainDlg)
    END_UPDATE_UI_MAP()
//...

};

Notice that CMainDlg derives from CUpdateUI and has an update UI map. OnInitDialog() has this code, which should be familiar from the earlier frame window examples:

    // register object for message filtering and idle updates

    CMessageLoop* pLoop = _Module.GetMessageLoop();
    ATLASSERT(pLoop != NULL);
    pLoop->AddMessageFilter(this);
    pLoop->AddIdleHandler(this);
 
    UIAddChildWindowContainer(m_hWnd);

This time, instead of UIAddToolbar() or UIAddStatusBar(), we call UIAddChildWindowContainer(). This tells CUpdateUI that our dialog contains child windows that will need updating. If you look at OnIdle(), you might suspect that something is missing:

BOOL CMainDlg::OnIdle()
{
    return FALSE;
}

You might expect there to be another CUpdateUI method call here to do the actual updating, and you're right, there should be; the AppWizard left out a line of code. You need to add this line to OnIdle():

BOOL CMainDlg::OnIdle()
{
    UIUpdateChildWindows();
    return FALSE;
}

To demonstrate UI updating, when you click the left-hand bitmap button, the right-hand button is enabled or disabled. So first, we add an entry to the update UI map, using the flag UPDUI_CHILDWINDOW to indicate that the entry is for a child window:

    BEGIN_UPDATE_UI_MAP(CMainDlg)
        UPDATE_ELEMENT(IDC_ALYSON_BMPBTN, UPDUI_CHILDWINDOW)
    END_UPDATE_UI_MAP()

Then in the handler for the left button, we call UIEnable() to toggle the enabled state of the other button:

void CMainDlg::OnAlysonODBtn ( UINT uCode, int nID, HWND hwndCtrl )
{
    UIEnable ( IDC_ALYSON_BMPBTN, !m_wndBmpBtn.IsWindowEnabled() );
}

DDV

WTL's dialog data validation (DDV) support is a bit simpler than MFC's. In MFC, you need create separate macros for DDX (to transfer the data to variables) and DDV (to validate the data). In WTL, one macro does both at the same time. WTL contains basic DDV support using the following macros in the DDX map:

DDX_TEXT_LEN
Does DDX like DDX_TEXT, and verifies that the string's length (not counting the null terminator) is less than or equal to a specified limit.
DDX_INT_RANGE and DDX_UINT_RANGE
These do DDX like DDX_INT and DDX_UINT, plus they verify that the number is between a given minimum and maximum.
DDX_FLOAT_RANGE
Does DDX like DDX_FLOAT and verifies that the number is between a given minimum and maximum.
DDX_FLOAT_P_RANGE (new in WTL 7.1)
Does DDX like DDX_FLOAT_P and verifies that the number is between a given minimum and maximum.

The parameters for these macros are like the corresponding non-validating macros, with an additional one or two parameters indicating the acceptable range. DDX_TEXT_LEN takes one parameter, the maximum allowed length. The others take two additional parameters, indicating the minimum and maximum allowable values.

ControlMania2 has an edit box with ID IDC_FAV_SEASON that is tied to the member variable m_nSeason.

 [Season selector edit box - 26K]

There were seven seasons of Buffy, so the legal values for the season are 1 to 7, and the DDV macro looks like:

    BEGIN_DDX_MAP(CMainDlg)
    //...

        DDX_INT_RANGE(IDC_FAV_SEASON, m_nSeason, 1, 7)
    END_DDX_MAP()

OnOK() calls DoDataExchange() to validate the season number. m_nSeason is filled in as part of the work done in DoDataExchange().

Handling DDV failures

If a control's data fails validation, CWinDataExchange calls the overridable function OnDataValidateError() and DoDataExchange() returns false. The default implementation of OnDataValidateError() just beeps the speaker, so you'll probably want to provide a friendlier indication of the error. The prototype of OnDataValidateError() is:

void OnDataValidateError ( UINT nCtrlID, BOOL bSave, _XData& data );

_XData is a struct that CWinDataExchange fills in with details about the data that was entered and the allowable range. Here's the definition of the struct:

struct _XData
{
    _XDataType nDataType;
    union
    {
        _XTextData textData;
        _XIntData intData;
        _XFloatData floatData;
    };
};

nDataType indicates which of the three members of the union is meaningful. Its possible values are:

enum _XDataType
{
    ddxDataNull = 0,
    ddxDataText = 1,
    ddxDataInt = 2,
    ddxDataFloat = 3,
    ddxDataDouble = 4
};

In our case, nDataType will be ddxDataInt, which means that the _XIntData member in _XData is filled in. _XIntData is a simple struct:

struct _XIntData
{
    long nVal;
    long nMin;
    long nMax;
};

Our OnDataValidateError() override shows an error message telling the user what the allowable range is:

void CMainDlg::OnDataValidateError ( UINT nCtrlID, BOOL bSave, _XData& data )
{
CString sMsg;
 
    sMsg.Format ( _T("Enter a number between %d and %d"),
                  data.intData.nMin, data.intData.nMax );
 
    MessageBox ( sMsg, _T("ControlMania2"), MB_ICONEXCLAMATION );
 
    GotoDlgCtrl ( GetDlgItem(nCtrlID) );
}

Check out atlddx.h to see the other types of data in an _XData struct - _XTextData and _XFloatData.

Resizing Dialogs

One of the first things about WTL that got my attention was its built-in support for resizable dialogs. Some time ago, I wrote an article on this subject, so please refer to that article for more details. To summarize, you add the CDialogResize class to the dialog's inheritance list, call DlgResize_Init() in OnInitDialog(), then chain messages to CDialogResize.

Up Next

In the next article, we'll look at hosting ActiveX controls in dialogs, and how to handle events fired by the controls.

References

Using WTL's Built-in Dialog Resizing Class - Michael Dunn

Using DDX and DDV with WTL - Less Wright

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 benefitting from my code) but is not required. Attribution in your own source code is also appreciated but not required.

Revision History

April 28, 2003: Article first published.
December 31, 2005: Updated to cover changes in WTL 7.1.

Series Navigation: � Part IV (Dialogs and Controls) | � Part VI (Hosting ActiveX Controls)

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here