Chinaunix首页 | 论坛 | 博客
  • 博客访问: 589014
  • 博文数量: 752
  • 博客积分: 40000
  • 博客等级: 大将
  • 技术积分: 5005
  • 用 户 组: 普通用户
  • 注册时间: 2008-10-13 14:47
文章分类

全部博文(752)

文章存档

2011年(1)

2008年(751)

我的朋友

分类:

2008-10-13 16:46:29

C++ At Work 专栏...
对话框模板,RegexTest

原著:Paul DiLascia
翻译:

下载源代码: (279KB)
原文出处:


我想用 MFC 和 C++ 创建一个基于对话框的程序(主窗口本身是个对话框)。我不想使用资源(.rc)文件,而是想在内存中动态创建对话框。我在 MSDN 中找到一些线索,但没有发现代码例子。我了解到 DLGTEMPLATE 和 DLGITEMTEMPLATE 结构以及 InitModalIndirect 函数或许可以用来创建模式对话框,但我不知道从何入手。请问如何不依赖资源文件动态创建对话框?

Thomas Zeitlberger
从理论上讲,动态创建对话框很简单,但实际上那样做很危险。就是内存中创建正确的结构并调用一系列 Indirect 对话框创建函数之一:用 CDialog::CreateIndirect 创建非模式对话框,或者用 CDialog::InitModalIndirect 创建模式对话框(然后调用 DoModal 运行)。它们分别对应着 Win32 API 函数 ::CreateDialogIndirect 和 ::DialogBoxIndirect。不管用什么方法,你都得在内存中传递一个指向对话框模板的指针。
  从概念上讲,创建对话框模板很简单,就是在内存中建立并初始化相关结构。其具体细节是有讲究的,因为这些结构有点奇奇怪怪,很诡诈,你不得有一点差错,只要有一个字节的偏差,那么你的程序便会莫名其妙地垮掉。控件的位置和大小计算也会出现混乱,原因是对话框不使用像素,而是用对话框单位(units),它依赖对话框的字体。
  要完整地讨论包含所有类型控件的对话框模板不是本专栏所能胜任的。但我可以提供一个简单的例子,它至少包含一个控件。我写了一个类:CStringDialog,它显示一个对话框,请求用户输入一个字符串,如图 Figure 1 所示。


Figure 1 String Dialog

为了使用这个类,你只需实例化然后调用 Init 和 DoModal 即可:
CStringDialog dlg;
dlg.Init(_T("Hi"), _T("Please enter your name:"));
if (dlg.DoModal()==IDOK) {
    CString name = dlg.m_str;
    // do something with it...
}

  CStringDialog 的样子和行为类似于所有基于对话框资源的 CDialog 派生类,所不同的是该对话框用其自身模板在内存中动态生成。
  那么对话框模板到底是个什么东西呢?对话框模板其实就是一个描述对话框的内存结构。这个模板之所以复杂并容易出错,是因为它并非像 CREATESTRUCT 和 WNDCLASS 一样是个定长结构。它是一个变长结构,其中包含有定长结构元素 DLGTEMPLATE 以及 DLGITEMTEMPLATE 结构数组,其每个数组元素对应着一个对话框控件项。DLGTEMPLATE 和 DLGITEMTEMPLATE 两者都包含一些跟在 C 结构后面非常很特别的变长域。这些结构如 所示,Figure 3 是整个结构的布局。

 
Figure 3 对话框模板

  对话框模板有点像汇编语言编程手册中的内容,现在就让我们穿上蹩脚的工作制服,立即从 DLGTEMPLATE 开始吧。
  假设你分配了一块足够大的内存来存放整个对话框模板,首先要做的事情就填写 DLGTEMPLATE 结构域。这一部分不难:

WORD* pTempl = new WORD[1024];
DLGTEMPLATE& dt = *((DLGTEMPLATE*)pTempl);
dt.style = WS_POPUPWINDOW|DS_MODALFRAME|WS_DLGFRAME;
dt.cdit = 3; // # dlg items
dt.x = 100; // in dlg units
// etc.

  DLGTEMPLATE 结构域是自扩展的,对此我不再做进一步说明。紧跟着该结构后面的域是变长域:菜单,对话框类和标题。每一项都不能超过一个 WORD。它可以是一个空结尾的 Unicode 字符串以标示某个 MENU 资源的名字,对话框类名或标题。此外,菜单和类名可以用特殊值 0xFFFF 后跟一个 16位 的 ID——即可以是菜单资源的 ID,也可以是预定义系统窗口类的序数。在大多数情况下,类名都应该使用 0x0000(空串),它告诉 Windows 操作系统使用默认的对话框类(#32770)。多数对话框都没有菜单,所以菜单也是 0x0000(空串)。在代码中是这样写的:

*pTempl++ = 0; // 菜单 (无)
*pTempl++ = 0; // 对话框类 (使用标准的对话框类)

接下来是标题,一个空结尾的 Unicode 字符串:

USES_CONVERSION;
LPCWSTR wszText = T2W(_T("My Dialog"));
wcscpy((WCHAR*)pTempl, wszText);
pTempl += wcslen(wszText)+1;

  这段代码支持 Unicode 或者 ASCII,因为定义了 _UNICODE,T2W是一个串转换宏。不要忘了增加模板指针 pTempl 的增量值,将其指到串后面的下一个 WORD。如果对话框具有 DS_SETFON 式样,在第四个字段:16位的字体大小后跟 Unicode 字体名,例如:“Verdana”。
  最后,我要指出对话框模板有一个扩展版本 DLGTEMPLATEEX,它可以让你指定更多的域,如字体点数和重量、是否用斜体、字符集、字体名。想了解更多信息请参考文档。这里我仅描述一个简单版本,因为通过在 OnInitDialog 处理例程中调用 SetFont 来设置字体是很容易的事情。(对于对话框中的控件项也有一个 DLGITEMTEMPLATEEX 扩展版本)。
  讲了这么多 DLGTEMPLATE。下面该看看控件。对话框中的每个控件项都是通过一个模板来描述的(DLGITEMTEMPLATE),其值不能超过一个 DWORD:

pTempl = AlignDWORD(pTempl);
DLGITEMTEMPLATE& it = *((DLGITEMTEMPLATE*)pTempl);
it.x = 0;
it.y = 0;
// etc.

  与 DLGTEMPLATE 类似,DLGITEMTEMPLATE 结构后面有三个变长域。即类名,文本和“”创建数据(creation data)。类名也是空结尾的 Unicode 字符串指定窗口类名(例如,“SysListView32”或者“MyFancyControl”),或者 0xFFFF 后跟特定的原子码之一,这些编码如 所示,它们都用于标准的预定义系统控件。例如,下面的代码示范了如何创建一个静态的文本控件:

// class immediately after DLGITEMTEMPLATE
*pTempl++ = 0xFFFF; // next WORD is atom:
*pTempl++ = 0x0082; // static control

  类名后面是标题。它既可以是 Unicode 字符串,也可以是特定的 0xFFFF 后跟 16位的资源 ID。你还可以用 0xFFFF + ID 的格式来为某个具备 SS_ICON 或 SS_BITMAP 式样的静态控件指定一个图标或位图。CStringDialog 使用字符串形式来创建其提示:

USES_CONVERSION;
LPCWSTR wszTest = T2W(_T("My Dialog"));
int maxlen = /* don''t overflow! */
wcsncpy((WCHAR*)pTempl, wszText, maxlen); pTempl += wcslen(wszText)+1;

  最后,“creation data”可以是任何你想要的数据。第一个 WORD 是数据长度,如果没有数据,其值可以是零。Windows 用 LPARAM 将一个指向数据的指针传递给 WM_INITDIALOG(模式对话框)或者 WM_CREATE(无模式对话框)。在此我不推荐使用创建数据,因为将任何你想要的数据成员添加到对话框类中,并用对话框的构造函数或 OnInitDialog 处理例程初始化它们的做法要容易得多。但你仍然得提供一个 0 WORD 来告诉 Windows 没有创建数据:

*pTempl++ = 0; // no creation data

  一旦你填充完指定的 DLGITEMTEMPLATE 数据,便可如法炮制下一个对话框控件,直到完成所有的控件。接着确保 DLGTEMPLATE::cdit 指定正确的控件总数。为了简化建立对话框模板的过程(使之尽量少出错)。我写了一个辅助类,CDlgTemplateBuilder。CStringDialog 用这个类可以一步到位建立对话框:

// in CStringDialog::Init
CDlgTemplateBuilder& dtb = m_dtb;
DLGTEMPLATE* pTempl = dtb.Begin(...);
dtb.AddItem(...);
dtb.AddItem(...);
dtb.AddItem(...);
InitModalIndirect(pTempl, ...);

  我已将细节模糊而突出主要思路:调用一次 Begin,然后针对每个控件调用一次 AddItem。CDlgTemplateBuilder::Begin 生成 DLGTEMPLATE 并且每次调用 AddItem 生成另一个 DLGITEMTEMPLATE。CDlgTemplateBuilder 在自己的内存缓冲里生成模板并在每次添加控件时自动增加 DLGTEMPLATE::cdit(控件项数目)。CDlgTemplateBuilder 具备辅助函数 AlignDWORD 和 AddText 以确保数据对齐和实现正确的字符串转换。有关细节自己下载源代码细细琢磨吧。
  我前面说过对话框使用对话框单位,而非像素。DLGTEMPLATE 和 DLGITEMTEMPLATE 两个都包含 x,y,cx h和 cy 成员来指定对话框或控件项的位置和大小。这些值都是对话框单位。每个水平方向的对话框单位是四分之一的基本单位,而每个垂直方向的对话框单位是八分之一的基本单位。一个基本单位是对话框中一个字符宽度和高度的平均值,并且依赖于对话框的字体。是不是很痛苦啊!没错,但凭心而论,这个想法是值得赞誉的:对话框单位使你的对话框外观独立于其字体。所以不管你用大字体也好,小字体也好,所有控件的相对位置是不会变的,一切都显示正常。Windows 有一个特别的函数叫做 MapDialogRect,用来将对话框单位转换为像素;令人惊讶的是却没有反向转换函数,而这正是生成模板所需要的——但你可以用如下公式:

CSize base = ::GetDialogBaseUnits();
xDlg = MulDiv(xPixel, 4, base.cx);
yDlg = MulDiv(yPixel, 8, base.cy);

  对于 CStringDialog 来说,我懒得去做这些事情,而是试验性找到正确的值显示出如图 Figure 1 所示的对话框。更复杂的实现得检查提示串的长度,或允许调用这指定尺寸。如果处理这些对话框单位让你头痛,那么你就创建大小和位置都是 0 的控件得了,然后实现 OnSize  处理例程将控件移到适合的像素位置。你的对话框得从 OnInitDialog 向自身发送 WM_SIZE 消息以确保第一次显示时控件被正确定位。

  最后,我是如何让 CStringDialog 显示如 Figure 1 所示的问号的呢?CStringDialog::Init 让你指定提示图标。默认是 IDI_QUESTION。但 IDI_QUESTION 是一个内建的图标,不是来自应用程序资源文件的图标。如果你指定一个对话框模板中的资源 ID,Windows 期望它在资源文件中。那么我如何让 Windows 改用系统图标呢?
  当然,话虽如此,CStringDialog 检查图标资源 ID,看看值是否大于 IDI_APPLICATION,也就是第一个系统图标的 ID。如果该图标 ID 在系统 ID 范围之内,CStringDialog 通过调用 ::LoadIcon 来加载它,此时 hInstance 置为 NULL(用于系统图标)并在 数据成员 m_hIcon 中保存加载的 HICON。然后 CStringDialog用 0xFFFF + nResID 格式(nResID=0)来构造对话框模板。这导致 Windows 创建一个静态图标,但并非实际的图标,然后,CStringDialog 在 OnInitDialog 中才设置实际图标:

// in CStringDialog::OnInitDialog()
if (m_hIcon) {
    CStatic* pStatic = (CStatic*)GetDlgItem(IDICON);
    pStatic->SetIcon(m_hIcon);
}

  这样一来,你可以将任何 IDI_XXX 形式的图标 IDs 传递给 CStringDialog::Init。你还能用自己的图标,只要其 ID 小于 IDI_APPLICATION = 32512。具体细节请参考源代码。

 

我最近要写一个正则表达式的 DDV 确认程序,正巧你写了一个(参见)。想知道为什么你要包装 .NET 库,这样无端地添加了许多依赖性(包装库累赘),为什么不用 Visual Studio .NET 里现成而简洁的正则表达式库,你只要包含一个头文件便可以在你的 MFC 程序中使用它,atlrx.h?虽然它不是百分百标准的语法,但我宁愿用它而不愿添加对 .NET 框架的依赖。

Gil Rosin
将我一军还要朝我拍砖!我甚至都不知道 ATL 有一个 regex 类。Windows 的东西太多,即使你是一个高手,也不一定就知道的那么全。没错,你说得很对,ATL 确实提供了一个 regex 实现!
  首先,让我来更正一下人们关于 .NET 框架的一些印象。我知道很多人在应用程序中添加这样的依赖性时都非常勉强,因为害怕代码臃肿,我刚开始也是这样。但是使用 .NET 框架也许并不像你想像的那么糟。虽然托管应用在启动时明显感觉性能问题,但不管你信不信,一旦框架被加载之后,微软中间语言(MSIL)代码甚至可以运行得比本机 EXEs 还快。那是因为 JIT (即时)编译器真的能进行许多性能优化。虽然一些面向老版本 Windows 如 Windows 98 或 Windows NT 的应用不一定有现成的框架环境,你得自己安装(参见“”或在 google 上搜索“”),但较新的以及未来的 Windows 版本都会将预装框架环境。随着 .NET 框架越来越普及,其性能也会得到不断的改进,调用框架所产生的额外成本(性能和安装方面)将会降至最小。
  对于我的包装库,其实要说它的“累赘”,只不过是一层使之能编译的薄薄糖衣。顶多添加了一个额外的函数调用,因为每个包装器对象只是一个托管对象句柄。正像我在四月份的文章(“”)中所指出的那样,函数调用对于大多数应用程序来说无关紧要。此外,我之所以选择 regex,只是以此为例;我的主要目的是创建一种通用机制来包装任何框架类。最终,如果你使用的是老版本编译器不支持 /clr,或者出于某种原因想避免使用 /clr,那么你只需要用包装器即可。要不然就撇开包装器,直接通过托管扩展调用框架。
  现在我已经消除了误解,我必须承认,当我得知 ATL 模板库有一个 regex 类后,尽管我的孤陋寡闻使我有些忐忑不安,但我还是十分兴奋的。当我收到你的 e-mail 后做的第一件事情是将测试程序从使用 .NET 库的 ManWrap 移植到 ATL。我想看看是不是很容易做到。我碰到了一些小麻烦,但没有费什么周折就解决了。
  与 .NET 框架相比,ATL 实现的 regex 比较原始,但它在多数情况下表现不错。ATL 使用两个模板类:一个是 CAtlRegExp,用于操作正则表达式;另一个是 CAtlREMatchContext,用于处理匹配。这两个模板由另一个描述字符集特性(例如,ASCII,WCHAR 或多字节)的类参数化。在实际应用中,你可以将此忽略掉,因为 ATL 模板根据你对 _UNICODE 的设置提供默认的字符集特性 CAtlRECharTraits:
// in atlrx.h
#ifndef _UNICODE
typedef CAtlRECharTraitsA CAtlRECharTraits;
#else
typedef CAtlRECharTraitsW CAtlRECharTraits;
#endif
template 
class CAtlRegExp; // forward declaration

从效果上讲,所有的 ATL regex 默认使用 TCHARs。所以在 ATL 中要创建正则表达式可以这样写:

CAtlRegExp<> re;
re.Parse("a+b+");

它将正则表达式解析为内部结构,这样一来你便可以用 CAtlREMatchContext 调用 Match 来得到匹配:

CAtlREMatchContext<> mc;
re.Match("aaabbx", &mc);

  与框架的 regex 类以及其它更成熟的实现相比,CAtlREMatchContext 多少土气一些。它有一个数据成员 m_Match,类型为 MatchGroup 结构,用于保存匹配的开始和结尾:

struct MatchGroup {
const RECHAR *szStart;
const RECHAR *szEnd;
};

  此处 RECHAR 可以是任何在字符集中定义的字符类型;在实际应用中,如果使用的是默认的字符集,则它与 TCHAR 相同。CAtlREMatchContext 还可以在输入字符串中查找匹配的子分组。调用 Match 之后,mc.m_uNumGroups 保存匹配子分组的数目,你可以调用 GetMatch(i,...)来获得第 i 个子分组匹配。ATL 正则表达式的一个不可思议的事情之一是它使用花括弧来表示分组,而不是标准的圆括弧。例如:

CAtlRegExp<> re;
re.Parse("{a+}{b+}");
re.Match("aaabbx", &mc);

  这段代码将找到一个匹配(“aaabb”),两个分组(“aaa”和“bb”)。ATL 会匹配常规的父分组,但你无法找到单独的子匹配,除非你使用花括弧。
  CAtlRegExp 和 CAtlREMatchContext 在使用上显得有些笨拙。例如,为了找到所有匹配,你得用前一次匹配的 szEnd 指针作为下一个输入串的开始重复调用 Match。又不是研究火箭,为什么必须跟踪状态才能快速找到想要的所有匹配?于是我用一个简单的类 CRegex 对细节进行了封装,使编程更容易一些。主要的头文件如 所示,Figure 6 是我四月份文章中关于 ManWrap 的 RegexTest 程序,已经移植到 ATL。最初的程序是输入正则表达式和字符串,RegexTest 显示匹配和分组。它用 CRegex 来遍历匹配:

CRegex re(/* regex */);
re.SetInput(/* input */);
while (re.NextMatch()) {
    int offset=0;
    CString match = re.GetMatch(&offset);
    ...
}

  CRegex 构造函数自动调用 Parse(你应该检查 m_err 看看是否出错),如果你想要单独分组匹配,可以调用 CRegex::GetNumGroups 来获取当前匹配的分组总数,调用 CRegex::GetGroup 获得每个分组匹配。具体细节如 所示。CRegex 在内部有其自己的 CAtlRegExp 和 CAtlREMatchContext 对象,并对其在输入串中位置进行跟踪——不必自己去做!


Figure 6 RegexTest 运行画面

  我还添加了几个有用的特性,ATL 没有 regex Replace 函数,但添加一个并不难。有了框架的 regex 类,CRegex::Replace 得来全不费功夫,函数静态,这样你用不用 CRegex 对象调用都可以。例如,RegexTest 的主对话框使用静态版本来将新行(\n)转换为回车换行 CRLF(\r\n):

static CString LF2CRLF(LPCTSTR lpsz)
{
    return CRegex::Replace(lpsz,_T("\n"),_T("\r\n"),TRUE);
}

  就这么简单,如果没有 CRegex,直接用 CAtlRegExp 要数行代码才能做到。当你在微软的基本构件块上花点时间来做高层次的抽象,你就会出效率。这就是分层设计的本质所在。我甚至添加了一个与 .NET 框架类似的动态 Replace 函数,以便能用回调函数来实现动态替换算法,就像我四月份文章中 WordMess 程序所做的那样。最后,我添加了 Split,以便你能用正则表达式作为分隔符将某个字符串剥离到一个子串数组中。例如:

vector strs = CRegex::Split("one,two,three",",");

  它返回有三个子串的向量数组:“one”,“two”,“three”。Split 的实现主要是字符串处理功夫,不过一旦你写就了代码,它就是你的并且永远受用。具体细节去参考源代码吧。
  如果你决定使用 ATL 正则表达式或我封装的 CRegex,请谨慎:ATL 版本的正则比表达式是一个简化的,非标准的实现。我已经提到过它使用花括弧而不是圆括弧。ATL 还使用非标准的元字符(例如,用 \b 而不是 \s 表示空格),并且你无法在表示范围时([\a\d]不工作)使用元字符。特殊字符清单参见 。请仔细阅读文档,提防 bug。复杂的正则表达式并不是总能凑效,你需要 #pragma 来抑制两个四级编译器警告(具体细节参见代码)。但如果你只需要一些基本的模式匹配来对用户输入进行有效性检查,ATL 是可以胜任的。
  结束本文之前,我想告诉 C++ fans 们正则表达式不日将肯定成为标准模板库的一部分。其实现很可能是基于 boost 的实现,你可以从 boost.org 得到它。

祝编程愉快!

您的提问和评论可发送到 Paul 的信箱:.
 
--------------------next---------------------


阅读(208) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~