WTL for MFC 程序员,第四部分 - 对话框和控件






4.97/5 (104投票s)
2003年4月27日
22分钟阅读

608248

5715
在 WTL 中使用对话框和控件。
目录
第四部分介绍
对话框和控件是 MFC 真正为您节省时间和精力的一个领域。如果没有 MFC 的控件类,您将不得不填充结构并编写大量的 `SendMessage` 调用来管理控件。MFC 还提供了对话框数据交换 (DDX),用于在控件和变量之间传输数据。WTL 也支持所有这些功能,并在其通用控件包装器中添加了一些改进。在本文中,我们将研究一个基于对话框的应用程序,它演示了您习惯使用的 MFC 功能,以及一些 WTL 消息处理增强功能。WTL 中的高级 UI 功能和新控件将在第五部分中介绍。
ATL 对话框回顾
回顾第一部分,ATL 有两个对话框类,`CDialogImpl` 和 `CAxDialogImpl`。`CAxDialogImpl` 用于承载 ActiveX 控件的对话框。本文不涉及 ActiveX 控件,因此示例代码使用 `CDialogImpl`。
要创建新的对话框类,您需要做三件事:
- 创建对话框资源
- 编写一个派生自 `CDialogImpl` 的新类
- 创建一个名为 `IDD` 的公共成员变量,并将其设置为对话框的资源 ID。
然后,您可以像在框架窗口中一样添加消息处理程序。WTL 不会改变这个过程,但它确实添加了您可以在对话框中使用的其他功能。
控件包装器类
WTL 有许多控件包装器,您应该很熟悉,因为 WTL 类的名称通常与它们的 MFC 对应类相同(或几乎相同)。方法通常也同名,因此您可以在使用 WTL 包装器时参考 MFC 文档。如果不行,当您需要跳转到某个类的定义时,F12 键会派上用场。
以下是内置控件的包装器类:
- 用户控件:`CStatic`, `CButton`, `CListBox`, `CComboBox`, `CEdit`, `CScrollBar`, `CDragListBox`
- 通用控件:`CImageList`, `CListViewCtrl` (MFC 中的 `CListCtrl`), `CTreeViewCtrl` (MFC 中的 `CTreeCtrl`), `CHeaderCtrl`, `CToolBarCtrl`, `CStatusBarCtrl`, `CTabCtrl`, `CToolTipCtrl`, `CTrackBarCtrl` (MFC 中的 `CSliderCtrl`), `CUpDownCtrl` (MFC 中的 `CSpinButtonCtrl`), `CProgressBarCtrl`, `CHotKeyCtrl`, `CAnimateCtrl`, `CRichEditCtrl`, `CReBarCtrl`, `CComboBoxEx`, `CDateTimePickerCtrl`, `CMonthCalendarCtrl`, `CIPAddressCtrl`
- MFC 中没有的通用控件包装器:`CPagerCtrl`, `CFlatScrollBar`, `CLinkCtrl`(可点击超链接,XP 及更高版本可用)
还有一些 WTL 特有的类:`CBitmapButton`, `CCheckListViewCtrl`(带复选框的列表视图控件),`CTreeViewCtrlEx` 和 `CTreeItem`(一起使用,`CTreeItem` 包装 `HTREEITEM`),`CHyperLink`(可点击超链接,所有操作系统可用)
需要注意的是,大多数包装器类都是窗口接口类,例如 `CWindow`。它们包装一个 `HWND` 并提供消息包装(例如,`CListBox::GetCurSel()` 包装 `LB_GETCURSEL`)。因此,像 `CWindow` 一样,创建临时控件包装器并将其附加到现有控件是廉价的。同样,像 `CWindow` 一样,当控件包装器被销毁时,控件不会被销毁。例外情况是 `CBitmapButton`、`CCheckListViewCtrl` 和 `CHyperLink`。
由于这些文章是针对有经验的 MFC 程序员的,所以我不会花太多时间介绍那些与 MFC 对应类相似的包装器类的细节。但是,我将介绍 WTL 中的新类;`CBitmapButton` 与同名的 MFC 类大相径庭,而 `CHyperLink` 则完全是新的。
使用 AppWizard 创建基于对话框的应用程序
启动 VC 并启动 WTL AppWizard。我相信您和我一样对时钟应用程序感到厌倦,所以我们把下一个应用程序命名为 _ControlMania1_。在 AppWizard 的第一页,点击 _Dialog Based_。我们还可以选择创建模式对话框或无模式对话框。它们之间的区别很重要,我将在第五部分介绍,但现在我们先选择更简单的模式对话框。勾选 _Modal Dialog_ 和 _Generate .CPP Files_,如下图所示:
第二页上的所有选项(对于 VC6 向导)或“用户界面功能”选项卡(在 VC7 中)仅在主窗口是框架窗口时才有效,因此它们都被禁用。单击“完成”以完成向导。
正如您所料,对于基于对话框的应用程序,AppWizard 生成的代码要简单得多。_ControlMania1.cpp_ 包含 `_tWinMain()` 函数,以下是其重要部分:
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; }
代码首先初始化 COM 并创建一个单线程套间。对于承载 ActiveX 控件的对话框来说,这是必要的;如果您的应用程序没有使用 COM,您可以安全地删除 `CoInitialize()` 和 `CoUninitialize()` 调用。接下来,代码调用 WTL 实用函数 `AtlInitCommonControls()`,它是 `InitCommonControlsEx()` 的一个包装器。全局 `_Module` 被初始化,并显示主对话框。(请注意,使用 `DoModal()` 创建的 ATL 对话框实际上是模态的,这与 MFC 不同,在 MFC 中所有对话框都是无模态的,MFC 通过手动禁用对话框的父级来模拟模态。)最后,`_Module` 和 COM 被反初始化,并且 `DoModal()` 返回的值用作应用程序的退出代码。
围绕 `CMainDlg` 变量的代码块很重要,因为 `CMainDlg` 可能包含使用 ATL 和 WTL 功能的成员。这些成员还可能在其析构函数中使用 ATL/WTL 功能。如果不存在该代码块,则 `CMainDlg` 析构函数(以及成员的析构函数)将在调用 `_Module.Term()`(它反初始化 ATL/WTL)之后运行,并尝试使用 ATL/WTL 功能,这可能导致难以诊断的崩溃。(作为历史记录,WTL 3 AppWizard 生成的代码中没有该代码块,我的一些应用程序确实崩溃了。)
您可以立即构建并运行应用程序,尽管对话框非常简陋
CMainDlg 中的代码处理 WM_INITDIALOG、WM_CLOSE 和所有三个按钮。如果您愿意,现在可以快速浏览一下代码;您应该能够理解 CMainDlg 的声明、其消息映射和其消息处理程序。
这个示例项目将演示如何将变量连接到控件。这是添加了几个控件的应用程序;您可以参考此图进行以下讨论。
由于应用程序使用了列表视图控件,因此需要更改 `AtlInitCommonControls()` 的调用。将其更改为:
AtlInitCommonControls ( ICC_WIN95_CLASSES );
这注册了比必要更多的类,但它省去了我们当向对话框添加不同类型的控件时必须记住添加 `ICC_*` 常量的麻烦。
使用控件包装器类
有几种方法可以将成员变量与控件关联起来。有些使用普通的 `CWindow`(或另一个窗口接口类,如 `CListViewCtrl`),而另一些则使用 `CWindowImpl` 派生类。如果您只需要一个临时变量,使用 `CWindow` 是可以的,而如果您需要对控件进行子类化并处理发送给它的消息,则需要 `CWindowImpl`。
ATL 方法 1 - 附加 CWindow
最简单的方法是声明一个 `CWindow` 或其他窗口接口类,并调用其 `Attach()` 方法。您也可以使用 `CWindow` 构造函数或赋值运算符将变量与控件的 `HWND` 关联起来。
此代码演示了将变量与列表控件关联的所有三种方法
HWND hwndList = GetDlgItem(IDC_LIST); CListViewCtrl wndList1 (hwndList); // use constructor CListViewCtrl wndList2, wndList3; wndList2.Attach ( hwndList ); // use Attach method wndList3 = hwndList; // use assignment operator
请记住,`CWindow` 析构函数不会销毁窗口,因此无需在变量超出作用域之前分离它们。如果您愿意,也可以将此技术用于成员变量——您可以在 `OnInitDialog()` 处理程序中附加变量。
ATL 方法 2 - CContainedWindow
`CContainedWindow` 介于使用 `CWindow` 和 `CWindowImpl` 之间。它允许您对控件进行子类化,然后在 _控件的父窗口中_ 处理该控件的消息。这使您可以将所有消息处理程序放在对话框类中,并且无需为每个控件编写单独的 `CWindowImpl` 类。请注意,您不能使用 `CContainedWindow` 来处理 `WM_COMMAND`、`WM_NOTIFY` 或其他通知消息,因为这些消息始终发送到控件的父级。
实际的类 `CContainedWindowT` 是一个模板类,它将窗口接口类名作为其模板参数。有一个特化 `CContainedWindowT<CWindow>`,它像普通的 `CWindow` 一样工作;这被 typedef 为较短的名称 `CContainedWindow`。要使用不同的窗口接口类,请将其名称指定为模板参数,例如 `CContainedWindowT<CListViewCtrl>`。
要连接 `CContainedWindow`,您需要做四件事:
- 在对话框中创建一个 `CContainedWindowT` 成员变量。
- 在对话框消息映射的 `ALT_MSG_MAP` 部分中放置处理程序。
- 在对话框的构造函数中,调用 `CContainedWindowT` 构造函数并告诉它应该将消息路由到哪个 `ALT_MSG_MAP` 部分。
- 在 `OnInitDialog()` 中,调用 `CContainedWindowT::SubclassWindow()` 方法将变量与控件关联起来。
在 ControlMania1 中,我们将为“确定”和“退出”按钮使用 `CContainedWindow`。对话框将处理发送给每个按钮的 `WM_SETCURSOR` 消息,并更改光标。
我们来逐步完成。首先,我们在 `CMainDlg` 中添加 `CContainedWindow` 成员。
class CMainDlg : public CDialogImpl<CMainDlg> { // ... protected: CContainedWindow m_wndOKBtn, m_wndExitBtn; };
其次,我们添加 `ALT_MSG_MAP` 部分。“确定”按钮将使用第 1 部分,而“退出”按钮将使用第 2 部分。这意味着发送到“确定”按钮的所有消息都将路由到 `ALT_MSG_MAP(1)` 部分,发送到“退出”按钮的所有消息都将路由到 `ALT_MSG_MAP(2)` 部分。
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); };
第三,我们为每个成员调用 `CContainedWindow` 构造函数,并告诉它使用哪个 `ALT_MSG_MAP` 部分。
CMainDlg::CMainDlg() : m_wndOKBtn(this, 1), m_wndExitBtn(this, 2) { }
构造函数参数是 `CMessageMap*` 和 `ALT_MSG_MAP` 节号。第一个参数通常是 `this`,表示将使用对话框自己的消息映射,第二个参数告诉对象它应该将消息发送到哪个 `ALT_MSG_MAP` 节。
重要提示:如果您使用的是 VC 7.0 或 7.1 以及 WTL 7.0 或 7.1,并且 `CWindowImpl`- 或 `CDialogImpl`- 派生类同时执行以下所有操作,则会遇到断言失败:
- 消息映射使用 `BEGIN_MSG_MAP` 而不是 `BEGIN_MSG_MAP_EX`。
- 该映射包含一个 `ALT_MSG_MAP` 部分。
- 一个 `CContainedWindowT` 变量将消息路由到该 `ALT_MSG_MAP` 部分。
- 该 `ALT_MSG_MAP` 部分使用新的 WTL 消息处理程序宏。
有关更多详细信息,请参阅本文讨论论坛中的此帖子。解决方案是使用 `BEGIN_MSG_MAP_EX` 而不是 `BEGIN_MSG_MAP`。
最后,我们将每个 `CContainedWindow` 与一个控件关联起来。
LRESULT CMainDlg::OnInitDialog(...) { // ... // Attach CContainedWindows to OK and Exit buttons m_wndOKBtn.SubclassWindow ( GetDlgItem(IDOK) ); m_wndExitBtn.SubclassWindow ( GetDlgItem(IDCANCEL) ); return TRUE; }
这是新的 `WM_SETCURSOR` 处理程序:
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; } }
如果您想使用 `CButton` 功能,您可以像这样声明变量:
CContainedWindowT<CButton> m_wndOKBtn;
然后就可以使用 `CButton` 方法了。
当您将光标移动到按钮上方时,您可以看到 `WM_SETCURSOR` 处理程序正在运行:
ATL 方法 3 - 子类化
方法 3 涉及创建一个 `CWindowImpl` 派生类并用它来子类化一个控件。这与方法 2 类似,但消息处理程序位于 `CWindowImpl` 类中,而不是对话框类中。
ControlMania1 使用此方法子类化主对话框中的“关于”按钮。这是 `CButtonImpl` 类,它派生自 `CWindowImpl` 并处理 `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; } } };
然后,在主对话框中,我们声明一个 `CButtonImpl` 成员变量:
class CMainDlg : public CDialogImpl<CMainDlg> { // ... protected: CContainedWindow m_wndOKBtn, m_wndExitBtn; CButtonImpl m_wndAboutBtn; };
最后,在 `OnInitDialog()` 中,我们对按钮进行子类化。
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 方法 1 - DDX_CONTROL
WTL 的 DDX(对话框数据交换)支持与 MFC 的工作方式非常相似,并且可以相当轻松地将变量连接到控件。首先,您需要一个 `CWindowImpl` 派生类,如前面的示例所示。这次我们将使用一个新类 `CEditImpl`,因为本示例将对编辑控件进行子类化。您还需要在 stdafx.h 中 `#include atlddx.h` 以引入 DDX 代码。
要向 `CMainDlg` 添加 DDX 支持,请将 `CWinDataExchange` 添加到继承列表中。
class CMainDlg : public CDialogImpl<CMainDlg>, public CWinDataExchange<CMainDlg> { //... };
接下来,您在类中创建 DDX 映射,这类似于 ClassWizard 在 MFC 应用程序中生成的 `DoDataExchange()` 函数。有几种 `DDX_*` 宏用于不同类型的数据;我们这里将使用 `DDX_CONTROL` 将变量与控件连接起来。这次,我们将使用处理 `WM_CONTEXTMENU` 的 `CEditImpl`,以便在您右键单击控件时执行某些操作。
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; };
最后,在 `OnInitDialog()` 中,我们调用从 `CWinDataExchange` 继承的 `DoDataExchange()` 函数。第一次调用 `DoDataExchange()` 时,它会根据需要对控件进行子类化。因此,在此示例中,`DoDataExchange()` 将对 ID 为 `IDC_EDIT` 的控件进行子类化,并将其连接到变量 `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; }
`DoDataExchange()` 的参数与 MFC 的 `UpdateData()` 函数的参数含义相同。我们将在下一节中更详细地介绍。
如果您运行 ControlMania1 项目,您可以看到所有这些子类化正在运行。右键单击编辑框将弹出消息框,并且光标将在按钮上方更改形状,如前所示。
WTL 方法 2 - DDX_CONTROL_HANDLE
WTL 7.1 中添加的一个新功能是 `DDX_CONTROL_HANDLE` 宏。在 WTL 7.0 中,如果您想使用 DDX 连接一个普通的窗口接口类(例如 `CWindow`、`CListViewCtrl` 等),您不能使用 `DDX_CONTROL`,因为 `DDX_CONTROL` 仅适用于 `CWindowImpl` 派生类。除了基类要求不同之外,`DDX_CONTROL_HANDLE` 的工作方式与 `DDX_CONTROL` 相同。
如果您仍在使用 WTL 7.0,您可以使用此宏来定义与 `DDX_CONTROL` 兼容的 `CWindowImpl` 派生类:
#define DDX_CONTROL_IMPL(x) \ class x##_ddx : public CWindowImpl<x##_ddx, x> \ { public: DECLARE_EMPTY_MSG_MAP() };
然后你可以写:
DDX_CONTROL_IMPL(CListViewCtrl)
您将拥有一个名为 `CListViewCtrl_ddx` 的类,它像 `CListViewCtrl` 一样工作,但会被 `DDX_CONTROL` 接受。
更多关于 DDX
DDX 当然也可以进行数据交换。WTL 支持在编辑框和字符串变量之间交换字符串数据。它还可以将字符串解析为数字,并在整数或浮点变量之间传输数据。它还支持将复选框或一组单选按钮的状态传输到/从 `int`。
DDX 宏
每个 DDX 宏都会扩展为一个 `CWinDataExchange` 方法调用,该调用完成工作。所有宏都具有通用形式:`DDX_FOO(controlID, variable)`。每个宏接受不同类型的变量,有些宏(如 `DDX_TEXT`)被重载以接受多种类型。
DDX_TEXT
- 将文本数据传输到/从编辑框。变量可以是 `CString`、`BSTR`、`CComBSTR` 或静态分配的字符数组。使用 `new` 分配的数组将不起作用。
DDX_INT
- 在编辑框和 `int` 之间传输数值数据。
DDX_UINT
- 在编辑框和 `unsigned int` 之间传输数值数据。
DDX_FLOAT
- 在编辑框和 `float` 或 `double` 之间传输数值数据。
DDX_CHECK
- 将复选框的状态传输到/从 `int` 或 `bool`。
DDX_RADIO
- 将一组单选按钮的状态传输到/从 `int`。
`DDX_CHECK` 可以接受 `int` 或 `bool` 变量。`int` 版本接受/返回 0、1 和 2(或等效地,`BST_UNCHECKED`、`BST_CHECKED` 和 `BST_INDETERMINATE`)。`bool` 版本(在 WTL 7.1 中添加)可用于复选框永远不会处于不确定状态的情况;如果复选框被选中,此版本接受/返回 `true`,否则返回 `false`。如果复选框碰巧处于不确定状态,`DDX_CHECK` 返回 `false`。
WTL 7.1 中还添加了一个额外的浮点宏:
DDX_FLOAT_P(controlID, variable, precision)
- 类似于 `DDX_FLOAT`,但在编辑框中设置文本时,数字将被格式化为最多显示 `precision` 位有效数字。
关于使用 `DDX_FLOAT` 和 `DDX_FLOAT_P` 的说明:当您在应用程序中使用它们时,需要在 stdafx.h 中添加一个 #define,在包含任何 WTL 头文件之前:
#define _ATL_USE_DDX_FLOAT
这是必要的,因为默认情况下,浮点支持为了大小优化而被禁用。
更多关于 DoDataExchange()
您调用 `DoDataExchange()` 方法,就像在 MFC 中调用 `UpdateData()` 一样。`DoDataExchange()` 的原型是:
BOOL DoDataExchange ( BOOL bSaveAndValidate = FALSE,
UINT nCtlID = (UINT)-1 );
参数如下:
bSaveAndValidate
- 标志,指示数据传输方向。传递 `TRUE` 将数据从控件传输到变量。传递 `FALSE` 将数据从变量传输到控件。请注意,此参数的默认值为 `FALSE`,而 MFC 的 `UpdateData()` 的默认值为 `TRUE`。如果您觉得更容易记住,也可以使用符号 `DDX_SAVE` 和 `DDX_LOAD`(分别定义为 `TRUE` 和 `FALSE`)作为参数。
nCtlID
- 传递 -1 以更新所有控件。否则,如果您只想对一个控件使用 DDX,请传递该控件的 ID。
`DoDataExchange()` 如果成功更新控件则返回 `TRUE`,否则返回 `FALSE`。您可以在对话框中重写两个函数来处理错误。第一个是 `OnDataExchangeError()`,如果在任何原因下数据交换失败,它都会被调用。`CWinDataExchange` 中的默认实现会发出蜂鸣声并设置焦点到导致错误的控件。另一个函数是 `OnDataValidateError()`,但我们将在第五部分讨论 DDV 时再介绍它。
使用 DDX
让我们向 `CMainDlg` 添加几个变量,以便与 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; };
在“确定”按钮处理程序中,我们首先调用 `DoDataExchange()` 将数据从编辑控件传输到我们刚刚添加的两个变量。然后我们在列表控件中显示结果。
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_INT` 将失败并调用 `OnDataExchangeError()`。`CMainDlg` 重写 `OnDataExchangeError()` 以显示一个消息框:
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 示例,让我们添加一个复选框来演示 `DDX_CHECK` 的用法。
此复选框永远不会处于不确定状态,因此我们可以将 `bool` 变量与 `DDX_CHECK` 一起使用。以下是将复选框连接到 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; };
在 `OnOK()` 的末尾,我们测试 `m_bShowMsg` 以查看复选框是否被选中。
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 ); }
示例项目还包含使用其他 `DDX_*` 宏的示例。
处理来自控件的通知
在 WTL 中处理通知类似于 API 级编程。控件以 `WM_COMMAND` 或 `WM_NOTIFY` 消息的形式向其父级发送通知,父级有责任处理它。其他一些消息也可以被视为通知,例如 `WM_DRAWITEM`,它在所有者绘制控件需要绘制时发送。父窗口可以自行处理消息,也可以将消息_反射_回控件。反射的工作方式与 MFC 中相同——控件能够自行处理通知,使代码独立并更易于移植到其他项目。
在父级中处理通知
作为 `WM_NOTIFY` 和 `WM_COMMAND` 发送的通知包含各种信息。`WM_COMMAND` 消息中的参数包含发送消息的控件的 ID、控件的 `HWND` 和通知代码。`WM_NOTIFY` 消息除了这些之外,还包含一个指向 `NMHDR` 数据结构的指针。ATL 和 WTL 有各种消息映射宏用于处理这些通知。我在这里只介绍 WTL 宏,毕竟这是一篇 WTL 文章。请注意,对于所有这些宏,您需要在消息映射中使用 `BEGIN_MSG_MAP_EX`,并在 stdafx.h 中 `#include atlcrack.h`。
消息映射宏
要处理 `WM_COMMAND` 通知,请使用以下 `COMMAND_HANDLER_EX` 宏之一:
COMMAND_HANDLER_EX(id, code, func)
- 处理来自特定控件的具有特定代码的通知。
COMMAND_CODE_HANDLER_EX(id, func)
- 处理具有特定代码的所有通知,无论哪个控件发送它们。
COMMAND_ID_HANDLER_EX(code, func)
- 处理来自特定控件的所有通知,无论代码是什么。
COMMAND_RANGE_HANDLER_EX(idFirst, idLast, func)
- 处理来自 ID 范围在 idFirst 到 idLast(包含)之间的所有控件的所有通知,无论代码是什么。
COMMAND_RANGE_CODE_HANDLER_EX(idFirst, idLast, code, func)
- 处理来自 ID 范围在 idFirst 到 idLast(包含)之间的所有控件的具有特定代码的所有通知。
示例
- `COMMAND_HANDLER_EX(IDC_USERNAME, EN_CHANGE, OnUsernameChange)`:处理从 ID 为 IDC_USERNAME 的编辑框发送的 `EN_CHANGE`。
- `COMMAND_ID_HANDLER_EX(IDOK, OnOK)`:处理从 ID 为 IDOK 的控件发送的所有通知。
- `COMMAND_RANGE_CODE_HANDLER_EX(IDC_MONDAY, IDC_FRIDAY, BN_CLICKED, OnDayClicked)`:处理从 ID 范围在 IDC_MONDAY 到 IDC_FRIDAY 之间的控件发送的所有 `BN_CLICKED` 通知。
还有用于处理 `WM_NOTIFY` 消息的宏。它们的工作方式与上面的宏相同,但它们的名称以“`NOTIFY_`”开头而不是“`COMMAND_`”。
`WM_COMMAND` 处理程序的原型是:
void func ( UINT uCode, int nCtrlID, HWND hwndCtrl );
`WM_COMMAND` 通知不使用返回值,因此处理程序返回 `void`。`WM_NOTIFY` 处理程序的原型是:
LRESULT func ( NMHDR* phdr );
处理程序的返回值用作消息结果。这与 MFC 不同,在 MFC 中,处理程序接收 `LRESULT*` 并通过该变量设置消息结果。通知代码和发送通知的控件的 `HWND` 在 `NMHDR` 结构中可用,作为 `code` 和 `hwndFrom` 成员。就像在 MFC 中一样,如果通知发送的结构不是普通的 `NMHDR`,您的处理程序应该将 `phdr` 参数转换为正确的类型。
我们将向 `CMainDlg` 添加一个通知处理程序,该处理程序处理从列表控件发送的 `LVN_ITEMCHANGED`,并在对话框中显示当前选定的项。我们首先添加消息映射宏和消息处理程序:
class CMainDlg : public ... { BEGIN_MSG_MAP_EX(CMainDlg) NOTIFY_HANDLER_EX(IDC_LIST, LVN_ITEMCHANGED, OnListItemchanged) END_MSG_MAP() LRESULT OnListItemchanged(NMHDR* phdr); //... };
这是消息处理程序:
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 }
此处理程序不使用 `phdr` 参数,但我包含了转换为 `NMLISTVIEW*` 的演示。
反射通知
如果您有一个 `CWindowImpl` 派生类,它实现了控件,比如我们之前的 `CEditImpl`,您可以在该类中处理通知,而不是在父对话框中。这称为_反射_通知,其工作方式类似于 MFC 的消息反射。不同之处在于,父级和控件都参与反射,而在 MFC 中只有控件参与。
当您想将通知反射回控件类时,只需在对话框的消息映射中添加一个宏,`REFLECT_NOTIFICATIONS()`:
class CMainDlg : public ... { public: BEGIN_MSG_MAP_EX(CMainDlg) //... NOTIFY_HANDLER_EX(IDC_LIST, LVN_ITEMCHANGED, OnListItemchanged) REFLECT_NOTIFICATIONS() END_MSG_MAP() };
该宏将一些代码添加到消息映射中,用于处理任何未被早期宏处理的通知消息。该代码检查消息的 `HWND` 并将消息发送到该窗口。但是,消息的值会发生变化,变为 OLE 控件使用的值,这些控件具有类似的消息反射系统。新值称为 `OCM_xxx` 而不是 `WM_xxx`,但消息的处理方式与非反射消息相同。
有 18 条消息被反射:
- 控件通知:`WM_COMMAND`,`WM_NOTIFY`,`WM_PARENTNOTIFY`
- 所有者绘制:`WM_DRAWITEM`,`WM_MEASUREITEM`,`WM_COMPAREITEM`,`WM_DELETEITEM`
- 列表框键盘消息:`WM_VKEYTOITEM`,`WM_CHARTOITEM`
- 其他:`WM_HSCROLL`,`WM_VSCROLL`,`WM_CTLCOLOR*`
在控件类中,为您感兴趣的反射消息添加处理程序,然后在末尾添加 `DEFAULT_REFLECTION_HANDLER()`。`DEFAULT_REFLECTION_HANDLER()` 确保未处理的消息正确路由到 `DefWindowProc()`。这是一个简单的自绘按钮类,它处理反射的 `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 处理反射消息的宏
我们刚刚看到了一个 WTL 反射消息宏,`MSG_OCM_DRAWITEM`。还有 `MSG_OCM_*` 宏用于其他 17 条也可以反射的消息。由于 `WM_NOTIFY` 和 `WM_COMMAND` 具有需要解包的参数,WTL 除了 `MSG_OCM_COMMAND` 和 `MSG_OCM_NOTIFY` 之外还提供了特殊的宏。这些宏的工作方式类似于 `COMMAND_HANDLER_EX` 和 `NOTIFY_HANDLER_EX`,但前面添加了“`REFLECTED_`”。例如,一个树形控件类可以有这样的消息映射:
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 ); };
如果您查看示例代码中的 ControlMania1 对话框,有一个树形控件,它按上面所示处理 `TVN_ITEMEXPANDING`。`CMainDlg` 成员 `m_wndTree` 使用 DDX 连接到树形控件,并且 `CMainDlg` 反射通知。树的 `OnItemExpanding()` 处理程序如下所示:
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 }
如果您运行 ControlMania1 并单击树中的 +/- 按钮,您将看到此处理程序正在运行——一旦展开节点,它将不再折叠。
杂项
对话框字体
如果您像我一样对 UI 很挑剔,并且您正在使用 Win 2000 或 XP,您可能想知道为什么对话框使用的是 MS Sans Serif 而不是 Tahoma。由于 VC 6 太旧,它生成的资源文件对于 NT 4 来说没问题,但对于 NT 的更高版本则不行。您可以修复这个问题,但这需要手动编辑资源文件。
您需要在资源文件中每个对话框条目中更改三件事:
- 对话框类型:将 `DIALOG` 更改为 `DIALOGEX`。
- 窗口样式:添加 `DS_SHELLFONT`
- 对话框字体:将 _MS Sans Serif_ 更改为 _MS Shell Dlg_
不幸的是,前两项更改在您修改和保存资源时会丢失,因此您需要重复进行这些更改。这是一张对话框的“之前”图片:
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
以及“之后”的图片:
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
进行这些更改后,对话框将在较新的操作系统上使用 Tahoma,但在较旧的操作系统上仍将在必要时使用 MS Sans Serif。
在 VC 7 中,您只需在对话框编辑器中更改一个设置即可使用正确的字体。
当您将 _Use System Font_ 更改为 _True_ 时,编辑器将为您将字体更改为 _MS Shell Dlg_。
_ATL_MIN_CRT
如此 VC 论坛常见问题解答中所述,ATL 包含一个优化,允许您创建不链接到 C 运行时库 (CRT) 的应用程序。此优化通过将 `_ATL_MIN_CRT` 符号添加到预处理器设置中来启用。AppWizard 生成的应用程序在 Release 配置中包含此符号。由于我从未编写过不需要 CRT 中_任何东西_的非平凡应用程序,所以我总是删除该符号。无论如何,如果您在 `CString` 或 DDX 中使用浮点功能,则必须删除它。
接下来
在第五部分中,我们将介绍对话框数据验证 (DDV)、WTL 中的新控件以及一些高级 UI 功能,例如自绘和自定义绘制。
版权和许可
本文是受版权保护的材料,版权归 Michael Dunn 所有 (c)2003-2005。我意识到这并不能阻止人们在网络上复制它,但我还是要说。如果您有兴趣翻译本文,请给我发电子邮件告知。我预计不会拒绝任何人翻译的许可,我只是想知道翻译的存在,这样我就可以在此处发布一个链接。
本文随附的演示代码已发布到公共领域。我以这种方式发布它,以便代码能造福所有人。(我没有将文章本身发布到公共领域,因为仅在 CodeProject 上提供文章有助于提高我个人的知名度和 CodeProject 网站。)如果您在自己的应用程序中使用演示代码,如果您能发电子邮件告知我(只是为了满足我的好奇心,想知道人们是否从我的代码中受益),我将不胜感激,但这不是必需的。在您自己的源代码中注明出处也值得赞赏,但不是必需的。
修订历史
- 2003 年 4 月 27 日:文章首次发表。
- 2005 年 12 月 20 日:更新以涵盖 WTL 7.1 中的更改。
系列导航:« 第三部分(工具栏和状态栏) | » 第五部分(高级对话框 UI 类)