65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (104投票s)

2003年4月27日

22分钟阅读

viewsIcon

608248

downloadIcon

5715

在 WTL 中使用对话框和控件。

目录

第四部分介绍

对话框和控件是 MFC 真正为您节省时间和精力的一个领域。如果没有 MFC 的控件类,您将不得不填充结构并编写大量的 `SendMessage` 调用来管理控件。MFC 还提供了对话框数据交换 (DDX),用于在控件和变量之间传输数据。WTL 也支持所有这些功能,并在其通用控件包装器中添加了一些改进。在本文中,我们将研究一个基于对话框的应用程序,它演示了您习惯使用的 MFC 功能,以及一些 WTL 消息处理增强功能。WTL 中的高级 UI 功能和新控件将在第五部分中介绍。

ATL 对话框回顾

回顾第一部分,ATL 有两个对话框类,`CDialogImpl` 和 `CAxDialogImpl`。`CAxDialogImpl` 用于承载 ActiveX 控件的对话框。本文不涉及 ActiveX 控件,因此示例代码使用 `CDialogImpl`。

要创建新的对话框类,您需要做三件事:

  1. 创建对话框资源
  2. 编写一个派生自 `CDialogImpl` 的新类
  3. 创建一个名为 `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_,如下图所示:

 [AppWizard page 1 - 21K]

 [AppWizard page 1 - 25K]

第二页上的所有选项(对于 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 生成的代码中没有该代码块,我的一些应用程序确实崩溃了。)

您可以立即构建并运行应用程序,尽管对话框非常简陋

 [Bare dialog - 9K]

CMainDlg 中的代码处理 WM_INITDIALOG、WM_CLOSE 和所有三个按钮。如果您愿意,现在可以快速浏览一下代码;您应该能够理解 CMainDlg 的声明、其消息映射和其消息处理程序。

这个示例项目将演示如何将变量连接到控件。这是添加了几个控件的应用程序;您可以参考此图进行以下讨论。

 [Add'l controls - 12K]

由于应用程序使用了列表视图控件,因此需要更改 `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`,您需要做四件事:

  1. 在对话框中创建一个 `CContainedWindowT` 成员变量。
  2. 在对话框消息映射的 `ALT_MSG_MAP` 部分中放置处理程序。
  3. 在对话框的构造函数中,调用 `CContainedWindowT` 构造函数并告诉它应该将消息路由到哪个 `ALT_MSG_MAP` 部分。
  4. 在 `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` 处理程序正在运行:

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

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 results - 11K]

如果您在编辑框中输入非数字文本,`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 error msg - 18K]

作为我们最后一个 DDX 示例,让我们添加一个复选框来演示 `DDX_CHECK` 的用法。

 [Msg checkbox - 12K]

此复选框永远不会处于不确定状态,因此我们可以将 `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 的更高版本则不行。您可以修复这个问题,但这需要手动编辑资源文件。

您需要在资源文件中每个对话框条目中更改三件事:

  1. 对话框类型:将 `DIALOG` 更改为 `DIALOGEX`。
  2. 窗口样式:添加 `DS_SHELLFONT`
  3. 对话框字体:将 _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 中,您只需在对话框编辑器中更改一个设置即可使用正确的字体。

 [VC7 dlg editor setting - 8K]

当您将 _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 类)

© . All rights reserved.