引言:好几年前就读过这个系列了,也曾经有过翻译的念头,都因种种原因作罢。前些日子在网上看到了一位网友对此系列的翻译,虽然看起来要比看原文省劲,但却发现许多处不忠实原文的地方,而且还有一些翻译上的错误,所以就生出了重新翻译的念头。这是第四章,敬请大家指正。
特别注 1:由于本页内容栏宽度不够,会导致部分内容看不见,请点击这里以获得最佳浏览效果。
特别注 2:本文为第一版,要浏览第二版请点击这里。
内容
- 简介
- 重温 ATL 对话框
- 通用控件封装类
- 使用 AppWizard 创建基于对话框的应用
- 使用控件封装类
- ATL 方法 1 – 附着到 CWindow
- ATL 方法 2 – CContainedWindow
- ATL 方法 3 – 子类化
- WTL 方式的 DDX
- 更多 DDX 的内容
- DDX 宏
- 关于 DoDataExchange() 的更多信息
- 使用 DDX
- 处理来自控件的通知
- 在父窗口中处理通知
- 反射通知
- 用于处理反射消息的 WTL 宏
- 拾零
- 对话框字体
- _ATL_MIN_CRT
- 修订历史
第四部分简介
对话框和控件是 MFC 确确实实节省你时间和精力的一个地方。如果没有 MFC 的控件类,你就会被淹没在填充结构以及写下成吨的 SendMessage 调用以管理控件的琐事中。而且 MFC 还提供了对话框数据交换(DDX),可以在控件和变量之间传递数据。WTL 也支持所有这些特性,并且在其通用控件的封装类里还加入了一些改进。在本文中,我们致力于一个基于对话框的应用,它演示了你所使用过的 MFC 特性,以及一些 WTL 在消息处理上的增强。高级 UI 特性以及 WTL 中的新控件将在第五部分里介绍。
重温 ATL 对话框
回忆一下第一部分,ATL 有两个对话框类, CDialogImpl 和 CAxDialogImpl。 CAxDialogImpl 用于掌控 ActiveX 控件的对话框。我们在本文中不包括 ActiveX 控件的内容,因而示例代码使用的是 CDialogImpl。
创建一个新的对话框类,要做三件事:
- 创建对话框资源
- 写一个派生于 CDialogImpl 的新类
- 创建一个名为 IDD 的 public 成员变量并将其设置为对话框的资源 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:
第二页所有选项仅当主窗口是框架窗口时才有意义,所以它们全部都被禁止掉了。点击 Finish,然后是 OK,完成整个向导。
正如你所希望的,AppWizard 对于对话框应用生成的代码是相当简单的。ControlMania1.cpp 有一个 _tWinMain() 函数,下面是其主要部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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 并创建一个单线程单元(single-threaded apartment)。这对于要掌控 ActiveX 控件的对话框的是必需的。接下来,调用了 WTL 辅助函数 AtlInitCommonControls(),这只是 InitCommonControlsEx() 的一个封装。全局 _Module 初始化完毕后,显示主对话框(注意,用 DoModal() 创建的 ATL 对话框的确是模态的,不像 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 的声明,它的消息映射和它的消息处理器了。
此示例工程将演示如何把变量挂接到控件上。下面是应用的样子,又多了几个控件。你在后续的讨论中可以回来参看此图。
由于应用使用了一个列表视图(list view)控件,所以需要改动一下对 AtlInitCommonControls() 的调用。将之改为:
1 |
AtlInitCommonControls ( ICC_WIN95_CLASSES ); |
这会比需要的多注册几个类,但它使我们在添加不同类型的控件时省却了记忆 ICC_* 常量之苦。
使用控件封装类
把一个成员变量联系到控件有好几种方法。有的使用了简单的 CWindow 之属(或者其他的窗口接口类,如 CListViewCtrl),另外的使用了 CWindowImpl 的派生类。如果你只是需要一个临时变量,那么使用 CWindow 就相当不错,而如果你需要子类化一个控件并处理发给它的消息,那就会需要 CWindowImpl 了。
ATL 方法 1 – 附着一个 CWindow
最简单的方法是声明一个 CWindow 或者其他的窗口接口类,然后调用其 Attach() 方法。你也可以使用 CWindow 的构造函数或者赋值操作符把变量关联到控件的 HWND。
下面的代码演示了把变量关联到列表控件的全部三种方法:
1 2 3 4 5 6 |
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 成员。
1 2 3 4 5 6 |
class CMainDlg : public CDialogImpl<CMainDlg> { // ... protected: CContainedWindow m_wndOKBtn, m_wndExitBtn; }; |
其次,添加 ALT_MSG_MAP 节。OK 按钮将使用第一节,Exit 按钮使用第二节。这意味着发送到 OK 按钮的所有消息都会被路由到 ALT_MSG_MAP(1) 节而发送到 Exit 按钮的所有消息会被路由到 ALT_MSG_MAP(2) 节。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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) <b>ALT_MSG_MAP(1) MSG_WM_SETCURSOR(OnSetCursor_OK) ALT_MSG_MAP(2) MSG_WM_SETCURSOR(OnSetCursor_Exit)</b> END_MSG_MAP() <b>LRESULT OnSetCursor_OK(HWND hwndCtrl, UINT uHitTest, UINT uMouseMsg); LRESULT OnSetCursor_Exit(HWND hwndCtrl, UINT uHitTest, UINT uMouseMsg);</b> }; |
第三,为每个成员调用 CContainedWindow 构造函数并把要使用的 ALT_MSG_MAP 节告诉它。
1 2 3 4 |
CMainDlg::CMainDlg() : m_wndOKBtn(this, 1), m_wndExitBtn(this, 2) { } |
构造函数的参数是一个 CMessageMap* 和一个 ALT_MSG_MAP 节号。第一个参数通常是 this,表示将使用对话框自己的消息映射,第二个参数告诉对象应该把它的消息发送到哪一个 ALT_MSG_MAP 节。
最后,为每个 CContainedWindow 关联控件。
1 2 3 4 5 6 7 8 9 |
LRESULT CMainDlg::OnInitDialog(...) { // ... // Attach CContainedWindows to OK and Exit buttons m_wndOKBtn.SubclassWindow ( GetDlgItem(IDOK) ); m_wndExitBtn.SubclassWindow ( GetDlgItem(IDCANCEL) ); return TRUE; } |
下面是新的 WM_SETCURSOR 处理器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
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 的特性,你可以把变量声明为:
1 |
CContainedWindowT<CButton> m_wndOKBtn; |
然后 CButton 的方法就可用了。
当你把光标移动到按钮上的时候,你就可以看到 WM_SETCURSOR 处理器在起作用:
ATL 方法 3 – 子类化
方法 3 致力于创建一个 CWindowImpl 派生类并使用它来子类化控件。这与方法 2 相似,但是消息处理器在 CWindowImpl 类中而不是在对话框类中。
ControlMania1 使用此方法来子类化主对话框中的 About 按钮。下面是 CButtonImpl 类,派生于 CWindowImpl 并处理了 WM_SETCURSOR:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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 成员变量:
1 2 3 4 5 6 7 |
class CMainDlg : public CDialogImpl<CMainDlg> { // ... protected: CContainedWindow m_wndOKBtn, m_wndExitBtn; <b>CButtonImpl m_wndAboutBtn;</b> }; |
最后在 OnInitDialog() 里子类化按钮。
1 2 3 4 5 6 7 8 9 10 11 12 |
LRESULT CMainDlg::OnInitDialog(...) { // ... // Attach CContainedWindows to OK and Exit buttons m_wndOKBtn.SubclassWindow ( GetDlgItem(IDOK) ); m_wndExitBtn.SubclassWindow ( GetDlgItem(IDCANCEL) ); <b>// CButtonImpl: subclass the About button m_wndAboutBtn.SubclassWindow ( GetDlgItem(ID_APP_ABOUT) );</b> return TRUE; } |
WTL 方式的 DDX
WTL DDX(对话框数据交换)工作起来很像 MFC,而且可以相当轻松地把变量连接到控件上。首先,你需要像在前一个例子中一样,有一个 CWindowImpl 的派生类。这次我们要使用一个新类, CEditImpl,这是因为这个例子里我们要子类化编辑框控件。你还需要在 stdafx.h 中 #include atlddx.h 以得到 DDX 支持代码。
为了向 CMainDlg 添加 DDX 支持,把 CWinDataExchange 加入到继承列表中:
1 2 3 4 5 |
class CMainDlg : public CDialogImpl<CMainDlg>, <b>public CWinDataExchange<CMainDlg></b> { //... }; |
接下来在类中创建一个 DDX 映射,它与 MFC 应用中 ClassWizard 生成的 DoDataExchange() 函数相似。针对不同类型的数据,存在着好多个 DDX_* 宏,我们在这儿要使用的是 DDX_CONTROL,以把变量连接到控件上。这一次,当你在控件上右击时,我们用 CEditImpl 处理 WM_CONTEXTMENU 消息来做一些事情。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
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> { //... <b>BEGIN_DDX_MAP(CMainDlg) DDX_CONTROL(IDC_EDIT, m_wndEdit) END_DDX_MAP()</b> protected: CContainedWindow m_wndOKBtn, m_wndExitBtn; CButtonImpl m_wndAboutBtn; <b>CEditImpl m_wndEdit;</b> }; |
最后,在 OnInitDialog() 中,我们调用继承于 CWinDataExchange 的 DoDataExchange() 函数。在 DoDataExchange() 被第一次调用的时候,它会按需子类化控件。在本例中, DoDataExchange() 会子类化 ID 为 IDC_EDIT 的控件,并把它连接到变量 m_wndEdit 上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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) ); <b>// First DDX call, hooks up variables to controls.</b> <b>DoDataExchange(false);</b> return TRUE; } |
DoDataExchange() 的参数和 MFC UpdateData() 函数的参数具有相同的含义。我们在下一节中讨论其更多细节。
如果你运行 ControlMania1 工程,你会看到所有的这些子类化都在起作用。在编辑框上右击会弹出消息框,在按钮上的光标像在前面显示的一样会改变其形状。
更多 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 中传输复选框的状态。
- DDX_RADIO
- 向/从 int 中传输单选按钮组的状态。
注意,如果在你的应用中使用了 DDX_FLOAT,你需要在 stdafx.h 中添加如下的一个 #define,并且需要位于所包含的所有 WTL 头文件之前:
1 |
#define _ATL_USE_DDX_FLOAT |
这是因为出于代码大小的优化,缺省对于浮点的支持是被禁止掉了的。
关于 DoDataExchange() 的更多信息
你应该象在 MFC 中调用 UpdateData() 一样调用 DoDataExchange() 方法。 DoDataExchange() 的原型为:
1 2 |
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
为了使用 DDX,我们向 CMainDlg 中添加两个变量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class CMainDlg : public ... { //... BEGIN_DDX_MAP(CMainDlg) DDX_CONTROL(IDC_EDIT, m_wndEdit) <b>DDX_TEXT(IDC_EDIT, m_sEditContents) DDX_INT(IDC_EDIT, m_nEditNumber)</b> END_DDX_MAP() protected: // DDX variables CString m_sEditContents; int m_nEditNumber; }; |
在 OK 按钮的处理器中,我们首先调用 DoDataExchange() 将数据从编辑框中传输到我们刚刚添加的两个变量中,然后再把结果显示到列表控件中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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() 以显示一个消息框:
1 2 3 4 5 6 7 8 9 |
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 的用法:
DDX_CHECK 所使用的变量是一个 int,其值可以是 0、1 或者 2,对应于未选中、选中以及半选中状态。你也可以使用常量 BST_UNCHECKED、 BST_CHECKED 以及 BST_INDETERMINATE 代替 0-2。对于仅能为选中或者未选中状态的复选框,你可以将变量视为一个布尔值。
下面是为复选框使用 DDX 而作的改动:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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) <b>DDX_CHECK(IDC_SHOW_MSG, m_nShowMsg)</b> END_DDX_MAP() protected: // DDX variables CString m_sEditContents; int m_nEditNumber; <b>int m_nShowMsg;</b> }; |
在 OnOK() 的末尾,我们检查 m_nShowMsg 来看复选框是否被选中。
1 2 3 4 5 6 7 8 9 10 |
void CMainDlg::OnOK ( UINT uCode, int nID, HWND hWndCtl ) { // Transfer data from the controls to member variables. if ( !DoDataExchange(true) ) return; //... if ( m_nShowMsg ) 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 宏,并 #include atlcrack.h。
消息映射宏
要处理 WM_COMMAND 通知,可以使用 COMMAND_HANDLER_EX 若干宏之一:
- COMMAND_HANDLER_EX(id, code, func)
- 处理来自于一个特定控件的特定代码的通知
- COMMAND_ID_HANDLER_EX(id, func)
- 处理特定代码的所有通知,不管是哪个控件发出的
- COMMAND_CODE_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 处理器的原型:
1 |
void func ( UINT uCode, int nCtrlID, HWND hwndCtrl ); |
WM_COMMAND 通知不使用返回值,所以处理器也不用。 WM_NOTIFY 处理器的原型:
1 |
LRESULT func ( NMHDR* phdr ); |
处理器的返回值用作消息的结果。这与 MFC 不同,MFC 中处理器接受一个 LRESULT* 参数并通过该变量来设置消息的结果。通知代码和发送通知的控件的 HWND 在 NMHDR 结构中,分别为 code 和 hwndFrom 成员。和在 MFC 中一样,如果随通知发送的结构不是一个简单的 NMHDR,你的处理器应该把 phdr 参数转型为正确的类型。
我们要为 CMainDlg 添加一个通知处理器来处理由列表控件发送的 LVN_ITEMCHANGED,并在对话框中显示当前选中的条目。我们从添加消息映射宏以及消息处理器开始:
1 2 3 4 5 6 7 8 9 |
class CMainDlg : public ... { BEGIN_MSG_MAP_EX(CMainDlg) NOTIFY_HANDLER_EX(IDC_LIST, LVN_ITEMCHANGED, OnListItemchanged) END_MSG_MAP() LRESULT OnListItemchanged(NMHDR* phdr); //... }; |
下面是消息处理器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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():
1 2 3 4 5 6 7 8 9 |
class CMainDlg : public ... { public: BEGIN_MSG_MAP_EX(CMainDlg) //... NOTIFY_HANDLER_EX(IDC_LIST, LVN_ITEMCHANGED, OnListItemchanged) <b>REFLECT_NOTIFICATIONS()</b> END_MSG_MAP() }; |
这一宏添加了一些代码到消息映射中,可以处理任何先前的宏都没有处理的通知消息。代码将检查消息的 HWND 并将消息发送到该窗口,但是消息的值会被改变为 OLE 控件所使用的值,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。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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。对于其他的 17 个消息也有对应的 MSG_OCM_* 反射宏。由于 WM_NOTIFY 和 WM_COMMAND 具有需要拆解的参数,所以 WTL 除 MSG_OCM_COMMAND 和 MSG_OCM_NOTIFY 之外还为它们提供了特殊的宏。这些宏像 COMMAND_HANDLER_EX 和 NOTIFY_HANDLER_EX 一样地工作,但是具有 “ REFLECTED_” 前缀。例如,树控件可以有这样的消息映射:
1 2 3 4 5 6 7 8 9 10 |
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() 处理器看起来是这样的:
1 2 3 4 5 6 7 8 9 |
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 并点击树中的 +/- 按钮,你就可以看到处理器在工作 – 一旦你展开了一个节点,就再也不能折叠回去了。
拾零
对话框字体
如果你和我一样对用户界面吹毛求疵而又正好在使用 Win 2000 或者 XP,你就可能会对对话框为什么使用的是 MS Sans Serif 字体而不是 Tahoma 字体而感到奇怪。其实是因为 VC 6 实在是太古老了,它生成的资源文件在 NT 4 上可以很好地工作,但对于 NT 的后续版本却不能。你可以修正这一问题,不过需要手动编辑资源文件。就我所知,VC 7 没有这一问题。
你需要对资源文件中存在的每个对话框做三样改动:
- 对话框类型:把 DIALOG 改为 DIALOGEX
- 窗口风格:添加 DS_SHELLFONT
- 对话框字体:把 MS Sans Serif 改为 MS Shell Dlg
不幸的是,如果你又改动并保存(译者注:在集成环境的资源编辑器里)了资源,前两项改动会丢失,你需要再次修改。下面是对话框改动之前的一个示例:
1 2 3 4 5 6 7 |
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 |
下面是之后的样子:
1 2 3 4 5 6 7 |
IDD_ABOUTBOX <b>DIALOGEX</b> DISCARDABLE 0, 0, 187, 102 STYLE <b>DS_SHELLFONT | </b>DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU CAPTION "About" FONT 8, "<b>MS Shell Dlg</b>" BEGIN ... END |
做完这些改动之后,对话框在新的操作系统上会使用 Tahoma 字体,在旧的操作系统上(在需要时)仍然会使用 MS Sans Serif 字体。
_ATL_MIN_CRT
正如在 VC Forum FAQ 中讲到的,ATL 具有一个优化功能,可以使你创建无需链接 C 运行时库(CRT)的应用程序。这一优化通过在预处理选项中添加 _ATL_MIN_CRT 符号来启用。AppWizard 生成的应用在 Release 配置中包含了这一符号。由于我从来没有写过任何一个有价值而又不需要使用 CRT 中任何东西的应用程序,所以我总是去掉这一符号。而且在任何情况下,如果你在 CString 或者 DDX 中使用了浮点特性,你都需要去掉它。
下一步
在第五部分里,会涵盖以下知识,对话框数据验证(DDV)、WTL 中的新控件以及诸如属主绘制(owner draw)和定制绘制(custom draw)这样的高级用户界面特性。
修订历史
2003 年 4 月 27 日:首次发布