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
:
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:
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:
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 )
{
CDCHandle dc = lpdis->hDC;
CDC dcMem;
dcMem.CreateCompatibleDC ( dc );
dc.SaveDC();
dcMem.SaveDC();
if ( lpdis->itemState & ODS_FOCUS )
dc.FillSolidRect ( &lpdis->rcItem, RGB(255,0,0) );
else
dc.FillSolidRect ( &lpdis->rcItem, RGB(0,0,255) );
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:
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:
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.
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:
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 CTreeItem
s instead of HTREEITEM
s. 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:
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 );
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:
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:
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:
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
.
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)