分类: C/C++
2008-08-07 17:41:16
我想用 MFC 和 C 创建一个基于对话框的程序(主窗口本身是个对话框)。我不想使用资源(.rc)文件,而是想在内存中动态创建对话框。我在
MSDN 中找到一些线索,但没有发现代码例子。我了解到 DLGTEMPLATE 和 DLGITEMTEMPLATE 结构以及 InitModalIndirect
函数或许可以用来创建模式对话框,但我不知道从何入手。请问如何不依赖资源文件动态创建对话框?
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 2 所示,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 后跟特定的原子码之一,这些编码如 Figure 4 所示,它们都用于标准的预定义系统控件。例如,下面的代码示范了如何创建一个静态的文本控件:
// 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 确认程序,正巧你写了一个(参见 2005 四月刊)。想知道为什么你要包装 .NET 库,这样无端地添加了许多依赖性(包装库累赘),为什么不用 Visual Studio .NET 里现成而简洁的正则表达式库,你只要包含一个头文件便可以在你的 MFC 程序中使用它,atlrx.h?虽然它不是百分百标准的语法,但我宁愿用它而不愿添加对 .NET 框架的依赖。
// in atlrx.h #ifndef _UNICODE typedef CAtlRECharTraitsA CAtlRECharTraits; #else typedef CAtlRECharTraitsW CAtlRECharTraits; #endif templateclass 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 5 所示,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 获得每个分组匹配。具体细节如
Figure 7 所示。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,以便你能用正则表达式作为分隔符将某个字符串剥离到一个子串数组中。例如:
vectorstrs = CRegex::Split("one,two,three",",");
它返回有三个子串的向量数组:“one”,“two”,“three”。Split
的实现主要是字符串处理功夫,不过一旦你写就了代码,它就是你的并且永远受用。具体细节去参考源代码吧。
如果你决定使用 ATL 正则表达式或我封装的 CRegex,请谨慎:ATL
版本的正则比表达式是一个简化的,非标准的实现。我已经提到过它使用花括弧而不是圆括弧。ATL 还使用非标准的元字符(例如,用 \b 而不是 \s
表示空格),并且你无法在表示范围时([\a\d]不工作)使用元字符。特殊字符清单参见
Figure 8。请仔细阅读文档,提防
bug。复杂的正则表达式并不是总能凑效,你需要 #pragma
来抑制两个四级编译器警告(具体细节参见代码)。但如果你只需要一些基本的模式匹配来对用户输入进行有效性检查,ATL 是可以胜任的。
结束本文之前,我想告诉 C fans 们正则表达式不日将肯定成为标准模板库的一部分。其实现很可能是基于 boost 的实现,你可以从
boost.org 得到它。
祝编程愉快!
您的提问和评论可发送到 Paul 的信箱:cppqa@microsoft.com.