在 WTL 中使用 DDX 和 DDV





5.00/5 (10投票s)
2000 年 12 月 19 日

218006

1767
本文将通过一个实际示例向您展示如何使用 WTL 的 DDX/DDV 实现。
引言
UI 编码中更繁琐的部分是简单地执行将成员数据传递到各种对话框以供用户交互,然后将其拉回并在重新提交到程序核心部分之前确保数据有效的繁重工作。MFC 意识到了这一点,并长期以来一直拥有 DDX(动态数据交换)和 DDV(动态数据验证)的概念。然而,那些专注于下载大小的人坚持使用 ATL 和 SDK 风格的编码,这意味着必须进行大量死记硬背的代码才能使用户能够修改我们 UI 对话框中的各种属性和设置。但是,通过 WTL,我们现在有机会在不增加体积的情况下获得 MFC 编码的许多便利性。本文将展示如何利用 WTL 的 DDX/DDV 实现,展示我添加到 WTL 实现中的两个自定义扩展以扩展其功能,并提供来自使用 WTL 的属性表实现(CPropertyPageImpl
)的实际示例中的代码。
DDX/DDV 究竟是什么?
如前所述,DDX 的目的是提供一个框架,用于处理应用程序与用户通过对话框、属性表等呈现的 UI 之间的数据传递(加载和检索)。DDV 的目标是能够自动验证用户所做的任何更改,以确保他们输入了有效数据,其中有效性由您的应用程序定义。使用 DDX 的好处是可以在应用程序变量及其在各种对话框中的表示/修改之间实现高效、易于阅读和维护的映射。此外,您会发现用于支持交互式 UI 的基本基础结构编码时间会大大减少。
概述完毕后,让我们开始编码,以开始解释利用 DDX 的详细信息。WTL 通过头文件 atlddx.h 提供 DDX/DDV 处理,其中包含用于创建 DDX 映射(与 ATL 的消息映射概念相同)的宏,以及模板类 CWinDataExchange
。
因此,利用 DDX/DDV 的第一步是添加
#include <ATLddx.h>
到您的 StdAfx.h 或其他主要头文件中。唯一需要注意的是,如果您使用的是 WTL 的 CString
,则必须在 AtlDDx.h 之前包含 AtlMisc.h。
#include <atlmisc.h> //CString support #include <atlddx.h>
在 StdAfx.h 中,紧挨着 atlwin.h 下方,然后就不用管它了。因为本文中的示例代码也将使用属性表来演示 ddx,所以我们必须添加 atldlgs.h 的包含,最终我们的 StdAfx.h 文件将包含此代码段
... #include <atlbase.h> #include <atlapp.h> extern CAppModule _Module; #include <atlwin.h> #include <atlmisc.h> #include <atlddx.h> #include <atldlgs.h> ...
下一步是确保您使用的对话框类继承自 CWinDataExchange
以获得 DDX 支持,如下所示
class CCameraBase : public CPropertyPageImpl<CCameraBase>, public CWinDataExchange<CCameraBase>
在此之后,我们现在可以进入核心部分,即实际将应用程序变量与其各自的 UI 组件连接起来。这通过 DDX 消息映射完成,下面是一个简单的示例
BEGIN_DDX_MAP(<your dialog class>) DDX_TEXT(<dlg resource id>,<string variable>) DDX_TEXT_LEN(<dlg resource id2>, <string variable2>, <max text length>) END_DDX_MAP()
在消息映射中,您指示 WTL 的 DDX 类如何将您的变量连接到对话框的 UI 元素。对于不同的数据类型,可以使用许多宏,但让我们简要看一下上面的两个条目。第一个宏 DDX_TEXT
简单地表示在加载时将 <string 变量> 的值分配给 <dlg 资源 ID> – 通常是一个映射到编辑框的字符串变量。
在对话框关闭时,映射会反向进行 – 将 <dlg 资源 ID> 的当前内容拉出并放回 <string 变量>。非常整洁。上面显示的第二个宏 DDX_TEXT_LEN
具有与 DDX_TEXT
类似的功能,因为它将 <dlg 资源 ID2> 连接到 <string 变量2>,但您可以看到还有一个第三个参数,<最大文本长度>。在此指定一个值,如果用户输入的文本超过该值,DDV 错误处理程序将启动。您可以覆盖默认处理程序,也可以使用默认处理程序,该处理程序会发出哔哔声,并将焦点移回有问题的控件,以提示用户进行更正。(您可以通过覆盖函数 void OnDataValidateError(UINT id, BOOL bSave,_XData& data)
来实现自己的处理程序,这在示例应用程序中有演示)。
消息映射有许多宏,通常有一个用于纯链接(类似于 DDX_TEXT
)的变体,以及一个用于链接和验证(类似于 DDX_TEXT_LEN
)的变体。请参阅本文末尾的摘要。
这样,最后一步就是实际告诉 WTL 何时触发实际的数据交换。这可以通过调用 DoDataExchange(BOOL fParam)
来完成,其中 FALSE
用于加载对话框中的数据值,TRUE
用于从对话框中检索数据。在哪里执行此操作取决于您,但 OnInitDialog
(WM_INITDIALOG
的处理程序)是 DoDataExchange(FALSE)
或加载调用的一个好位置。对于拉回修改后的数据,您可以将 DoDataExchange(TRUE)
调用(检索)放在对话框的 OnOK
中,对于属性页,您可能希望在 OnKillActivate()
中处理它。
DDX 实战
了解了利用 DDX/DDV 的基本原理后,让我们继续研究示例应用程序,在那里我们可以看到 DDX 的实际应用,并检查我发现的一些限制以及如何解决它们。
该示例应用程序基于一个处理多个无线摄像头查看/监视的商业应用程序的代码子集。此应用程序的相关部分是它必须允许用户为多达 16 个摄像头指定单独的设置,并在干净的 UI 中完成。我选择的解决方案是使用 WTL 的属性页实现,为每个摄像头提供一个漂亮的选项卡式对话框,然后使用 DDX/DDV 来简化单个设置的来回传输。示例应用程序只是允许您处理四个设置,以便您可以在调试器中看到设置是如何传输和验证的。我将在此概述一些更有趣的部分,之后,只需在调试器中跟踪代码即可巩固您对 DDX/DDV 的理解。
由于摄像头设置将在整个应用程序中全局使用,因此我创建了一个全局结构,如下所示(来自 stdafx.h)
struct _cameraprops { CString ssFriendlyName; UINT iHouseCode; UINT iUnitCode; CString ssSaveDir; BOOL fIsInSnapshotCycle; BOOL fAddTimestamp; }; extern _cameraprops g_cameraProps[4];
并在主 cpp 文件(propsheetddx.cpp)中分配了存储
#include "maindlg.h" CAppModule _Module; _cameraprops g_cameraProps[4];
设置了中央存储结构后,下一步是创建处理属性表的头文件并创建属性页处理程序的基本类。
class CCameraBase : public CPropertyPageImpl<CameraBas>, public CWinDataExchange<CameraBas>
然后,我在 VC 中创建了一个对话框,外观如下
接下来,我添加了 DDX 消息映射以指定 UI 与全局结构 g_cameraprops 之间的链接
BEGIN_DDX_MAP(CCameraBase) DDX_TEXT_LEN(IDC_edit_CameraTitle, g_cameraProps[m_iIdentity].ssFriendlyName, 35) DDX_COMBO_INDEX(IDC_cmbo_HouseCode, g_cameraProps[m_iIdentity].iHouseCode) DDX_COMBO_INDEX(IDC_cmbo_UnitCode, g_cameraProps[m_iIdentity].iUnitCode) DDX_TEXT(IDC_edit_FileDirectory, g_cameraProps[m_iIdentity].ssSaveDir) DDX_BOOL_RADIO(IDC_radio_AddTimeStamp, g_cameraProps[m_iIdentity].fAddTimestamp, IDC_radio_NoTimeStamp) END_DDX_MAP() enum { IDD = IDD_PROP_PAGE1 };
当然,我们必须调用 DoDataExchange 进行加载……
LRESULT OnInitDialog(...) { ... InitComboBoxes(hwndComboHouse, hwndComboUnit, m_iIdentity); CenterWindow(); DoDataExchange(FALSE); ... }
以及用于验证和拉回修改后的数据
BOOL OnKillActive() { DoDataExchange(TRUE); return true; }由于我们必须扩展到 16 个摄像头(样本中为 4 个),因此我修改了 CcameraBase 类的构造函数以接受一个整数来标识它正在处理的摄像头
CCameraBase(int _index)
{
m_iIdentity = _index;
...
}
完成这些后,我们就拥有了一个属性页的基本布局,一个用于传输数据并在用户修改后将其拉回的框架,以及一个允许我们为所有摄像头重用同一类的索引。现在,为了在我们的 UI 中实际实现这个 4x,我们转向派生的 CpropertySheetImpl 类 CcameraProperties。如下所示,它并没有太多内容
class CCameraProperties : public CPropertySheetImpl<CameraPropertie> { public: CCameraBase m_page1; CCameraBase m_page2; CCameraBase m_page3; CCameraBase m_page4; CCameraProperties():m_page1(1),m_page2(2),m_page3(3),m_page4(4) { m_psh.dwFlags |= PSH_NOAPPLYNOW; AddPage(m_page1); AddPage(m_page2); AddPage(m_page3); AddPage(m_page4); SetActivePage(0); SetTitle(_T("Camera and Video Input Properties")); }
请注意上面粗体显示的成员初始化列表,其中我们实际使用相机来标识每个类实例。之后,调用 AddPage 将类集成到选项卡布局中,并通过 SetActivePage
指定第一个页面。上面省略了一个小型消息映射,但除此之外,您现在就拥有了整个属性表的处理程序。
扩展 WTL 中的 DDX
当然,事情并非一帆风顺——我遇到了两个直接问题,需要向 <ATLDDX.H> 添加新扩展才能使 DDX 框架执行一些额外的处理。第一项是关于组合框——通用的 ddx 实现是指定 DDX_INT
并传入组合框 ID 来处理摄像头单元代码和房间代码与其各自组合框的映射。然而,令人头疼的是,DDX_INT 调用 Get/SetDlgItemInt,它只是放置或检索整数的文本表示。对于使用 X10 无线协议的摄像头,单元代码和房间代码都代表数组值的索引,而不是值本身……例如:房间代码 A 用 0 表示,B 用 1 表示,依此类推。在默认的 DDX 实现中,如果我传入其内在值 0,我将得到一个显示 0 的组合框,这不是我想要的。
由于我认为将组合框表示为数组或枚举的索引而不是尝试表示文本值非常普遍,因此我添加了一个名为 DDX_COMBO_INDEX
的新宏和宏处理程序。它将处理索引值的传递和检索,而不是文本的直接翻译。样本中包含了修改后的代码,但最终如下所示
#define DDX_COMBO_INDEX(nID, var) \ if(nCtlID == (UINT)-1 || nCtlID == nID) \ { \ if(!DDX_Combo_Index(nID, var, TRUE, bSaveAndValidate)) \ return FALSE; \ }
接着
template <class Type> BOOL DDX_Combo_Index(UINT nID, Type& nVal, BOOL bSigned, BOOL bSave, BOOL bValidate = FALSE, Type nMin = 0, Type nMax = 0) { T* pT = static_cast<>(this); BOOL bSuccess = TRUE; if(bSave) { nVal = ::SendMessage((HWND) (Type)pT->GetDlgItem(nID), CB_GETCURSEL, (WPARAM) 0, (LPARAM) 0); bSuccess = (nVal == CB_ERR ? false : true); } else { ATLASSERT(!bValidate || nVal >= nMin && nVal <= nMax); int iRet = ::SendMessage((HWND) (Type)pT->GetDlgItem(nID), CB_SETCURSEL, (WPARAM) nVal, (LPARAM) 0); bSuccess = (iRet == CB_ERR ? false : true); } if(!bSuccess) { pT->OnDataExchangeError(nID, bSave); } else if(bSave && bValidate) // validation { ATLASSERT(nMin != nMax); if(nVal < nMin || nVal > nMax) { _XData data; data.nDataType = ddxDataInt; data.intData.nVal = (long)nVal; data.intData.nMin = (long)nMin; data.intData.nMax = (long)nMax; pT->OnDataValidateError(nID, bSave, data); bSuccess = FALSE; } } return bSuccess; }
这样,我就能成功处理组合框 UI 中的索引了。另一个令人头疼的问题是,我想要的 UI 是两个单选按钮,代表用户选择的真/假(参见选项 #4,“您希望添加时间戳吗”)。起初我认为 DDX_RADIO
是我想要的,但它不起作用,DDX_CHECK
也不起作用(它不处理 UI 的切换以在两个单选按钮之间创建独占选择)。因此,我又添加了一个扩展 DDX_BOOL_RADIO
,它接受两个资源 ID,如下所示
DDX_BOOL_RADIO(<primary radio buttonID>, <BOOL variable>, <Secondary radio buttonID>)
此扩展程序的作用是在加载时确保只选中两个按钮中的一个,具体取决于布尔变量的状态。如果为真,则选中主 ID 单选按钮,次要单选按钮初始化为未选中,反之亦然,如果初始加载值为假。显然,您只需使用一个单选按钮来表示真/假状态,但我希望通过明确地用两个按钮和关联的文本来称呼它,让用户清楚他们在选择什么。您还可以在修改后的 <ATLDDX.H> 文件中找到 DDX_BOOL_BUTTON
的代码。
这样,示例应用程序的基本代码框架就完成了——我们现在拥有一个 UI,可以优雅地处理 WTL 的 DDX/DDV 与 UI 和内部变量之间的数据传输,让用户了解他们通过选项卡式属性对话框所选择的内容,最后,我们拥有一个代码库,可以轻松扩展到任意数量的摄像头,而代码更改最少。
希望通过快速在调试器下运行示例应用程序,可以消除任何剩余问题,您现在可以利用 WTL 的 DDX/DDV 框架,确保您不再需要为未来的 UI 编写大量的死记硬背的 GetDlgItem
/SetDlgItem
风格的代码。
如果您有改进本文的建议,或编写自己的 atlddx.h 扩展,我将不胜感激。您可以通过 less_wright@hotmail.com 给您发送电子邮件。
默认数据处理程序
这是默认 DDX/DDV 处理程序的列表
DDX_TEXT(nID, var) DDX_TEXT_LEN(nID, var, len) DDX_INT(nID, var) DDX_INT_RANGE(nID, var, min, max) DDX_UINT(nID, var) DDX_UINT_RANGE(nID, var, min, max) DDX_FLOAT(nID, var) DDX_FLOAT_RANGE(nID, var, min, max) // NOTE: you must define _ATL_USE_DDX_FLOAT to // exchange float values. DDX_CONTROL(nID, obj) DDX_CHECK(nID, var) DDX_RADIO(nID, var)