本章内容
- README.TXT
- 本系列介绍
- 第一部分介绍
- ATL 背景知识
- ATL 窗口类
- 定义窗口实现
- 高级消息映射链和嵌入(Mix-in)类
- ATL EXE 的结构
- ATL 中的对话框
- 就要到 WTL 了,我保证!
- 修订历史
README.TXT
在继续或者在本文的讨论板块中发布帖子之前,我希望你能先阅读以下内容。
你需要有 Platform SDK。没有它你将不能使用 WTL。你可以使用在线的 SDK 升级站点或者下载 CAB 文件并在本地运行安装程序。请使用工具把 SDK 的 include 以及 lib 目录加入到 VC 的搜索路径中,该工具可以在 Platform SDK 程序组中的 Visual Studio Registration 文件夹下找到。
你需要有 WTL。可以从微软下载版本 7。在文章 "Introduction to WTL - Part 1" 以及 "Easy installation of WTL" 中有一些关于安装的提示。这些文章已经很老了,不过还是有一些不错的信息。在这些文章中没有提到的一件事情是如何使 VC 搜索 WTL 的 include 目录。在 VC 6 里,点击 Tools|Options 并切换到 Directories 标签页,在 Show directories for 组合框中,选中 Include files,然后添加一个新项,使其指向你放置 WTL 头文件的目录。
你应该了解 MFC,并且要了解到你知道消息映射宏的实质是什么,而且能够编辑那些被标记为“DO NOT EDIT”的代码而不出问题。
你需要了解 Win32 API 编程,而且是很好地了解。如果你是直接通过 MFC 学习 Windows 编程而没有学习在 API 级消息是如何工作的,那很不幸,你会在使用 WTL 时遇到麻烦。如果你不知道一个消息的 WPARAM
和 LPARAM
是什么意思,你应该阅读其他的文章(CodeProject 上就有很多)以使你能够了解。
你需要了解 C++ 模板的语法,在 VC Forum FAQ 上有 C++ FAQ 和模板 FAQ 的链接。
我将只涉及 VC 6 的特性,不过据我所知在 VC 7 上一切都很正常。因为我不使用 VC 7,所以不能对 VC 7 上的问题提供帮助。但可以随便提问 VC 7 的问题,因为其他读者可能能够提供帮助。
本系列介绍
WTL 确实震动了所有人。它具有许多 MFC GUI 类的强大功能,但是可以生成相当小的可执行代码。如果你和我一样,用 MFC 学习 GUI 编程,对 MFC 所提供的控件封装感到相当舒服,并且对 MFC 内建的灵活的消息处理也有同感;如果你和我一样,不喜欢好几百 K 的 MFC 框架附着到自己的程序上,WTL 正适合你。 不过,还是有一些我们必须跨越的障碍:
- ATL 风格的模板乍看起来很怪异。
- 没有 ClassWizard 支持,所以写消息映射成了手工劳动。
- 在 MSDN 里没有文档,需要到其他地方去找,甚至需要去看 WTL 源程序。
- 没有能买到并放到书架上的参考书。
- 它具有“不被微软官方支持”的污名
- ATL/WTL 窗口非常不同于 MFC 窗口,并非你所有的知识都能够对应过来
另一方面,WTL 的好处有:
- 不需学习或者使用复杂的文档/视图框架
- 具有源于 MFC 的一些基本 UI 特性,例如 DDX/DDV 和“更新命令 UI”功能
- 增强了的一些 MFC 特性(例如,更灵活的分割条窗口)
- 与静态链接 MFC 的应用相比,可执行代码非常小
- 你自己可以改正 WTL 的错误而不影响现存的应用(相比之下,一个应用替换掉 MFC/CRT 的 DLL 来改正错误将引起其他应用崩溃)
- 如果仍然需要 MFC,MFC 和 ATL/WTL 窗口可以和平共处(在我工作的一个原型中,我创建了一个包含有 WTL
CSplitterWindow
的 MFC CFrameWnd
,而前者中又包含有 MFC CDialog
。-- 并不是我卖弄,只不过是修改了 MFC 代码而使用了更好的 WTL 分割条)
在本系列中,我将先介绍 ATL 窗口类。毕竟 WTL 是一组 ATL 的附加类,所以对 ATL 窗口有很好的理解相当重要。介绍完 ATL 之后我将介绍 WTL 的特性并展示它如何使界面编程变得轻而易举。
第一部分介绍
WTL 令人震惊。不过在知道为什么之前,我们首先需要了解 ATL。WTL 是一组 ATL 的附加类,如果过去你是一名仅使用 MFC 的程序员,你可能从来没有遇到过 ATL 的 GUI 类。所以请原谅我没有立即涉及 WTL,到 ATL 那儿绕些弯路是有必要的。
在第一部分里,我将给出一些 ATL 的背景知识,包括在写 ATL 代码之前需要知道的一些要点,迅速的解释那些令人胆寒的 ATL 模板,并涵盖了基本的 ATL 窗口类。
ATL 背景知识
ATL 和 WTL 的历史
活动(Active)模板库是一个古怪的名字,不是吗?年长点的可能会记得它原来的名字是 ActiveX 模板库,这是一个更准确的名字,因为 ATL 的目标就是要让 COM 对象和 ActiveX 控件写起来更轻松。(ATL 是在微软将新产品命名为“ActiveX-什么什么”的时候开发的,就像现在微软的新产品被称作“什么什么 .NET”一样)因为 ATL 只是用来写 COM 对象的,所以它只有 GUI 类中最基本的部分,即 MFC 中 CWnd
和 CDialog
的等价物。幸运的是,这些 GUI 类很灵活,可以让像 WTL 这样的东西构筑于其上。
当前是 WTL 的第二个修订版。第一个是版本 3.1,第二个是版本 7。(选定的版本号是为了与 ATL 的版本号匹配,所以不是 1 和 2。)版本 3.1 工作于 VC 6 和 VC 7,不过你需要为 VC 7 定义一组预处理符号。WTL 版本 7 是版本 3.1 的向下兼容替代品,不用修改就可以在 VC 7 上工作,所以对新的开发工作来讲确实没有理由还使用 3.1。
ATL风格的模板
即使你可以毫不头痛的阅读 C++ 模板,但一开始 ATL 还是会有两件事可能成为拦路虎。比如说:
CWindowImpl
{
...
};
这样做是合法的,因为 C++ 规范中声称紧随 class CMyWnd
部分之后,名字 CMyWnd
即被定义;并且可以被用在继承列表中。之所以把类名作为模板参数是因为要让 ATL 能做第二件技巧性的工作 - 编译期虚函数调用。
如果要看一下实际运作,可以看一下这几个类:
}
这儿的 static_cast(this)
是一个技巧。它把 B1*
类型的 this
,通过被调用的特化转型为 D1*
或者 D2*
。因为模板代码在编译时生成,所以保证了此转型是安全的,只要正确的书写了继承列表。 (如果你写成
B1
你会遇到麻烦。)转型是安全的是因为 this
对象只能是类型 D1*
或者 D2*
(相应的),而不是其他。注意,这和正常的 C++ 多态几乎一样,只不过 SayHi()
方法不是虚拟的。
要解释这是如何工作的,我们来看一下 SayHi()
的每个调用。在第一个调用中,特化 B1
被采用,所以 SayHi()
代码展开为:
);
pT->PrintClassName();
}
由于 D1
没有覆盖 PrintClassName()
,所以会搜索 D1
的基类。B1
有 PrintClassName()
方法,所以就是被调到的那个。
现在,看 SayHi()
的第二次调用。这一次使用了特化 B1
,于是 SayHi()
展开为:
);
pT->PrintClassName();
}
这次 D2
确实包含了一个 PrintClassName()
方法,所以它是被调用到的那个。
这种技术的好处是:
- 不需要使用指向对象的指针
- 由于不需要 vtbl 而节省内存
- 不会因为未初始化的 vtbl 而在运行时通过空指针调用虚函数
- 所有函数调用在编译时被解析,所以可以被优化
虽然在这个例子里 vtbl 的节约看起来并不明显(只不过 4 个字节),不过可以考虑在有 15 个基类,有的类有 20 个方法的情况下,累计的节省有多少。
ATL 窗口类
很好,背景知识足够了!是钻研 ATL 的时候了。ATL 采用严格的接口/实现相分离来设计,这在窗口类中也很明显。这和 COM 类似,接口的定义与实现完全分离(或者可能有多个实现)。
ATL 有一个类定义了窗口的“接口”,也就是对一个窗口可以做什么。这个类就是 CWindow
。它只是对 HWND
的一个封装,提供了几乎所有的以 HWND
作为第一个参数的 User API,如 SetWindowText()
和 DestroyWindow()
。 CWindow
有一个当你需要原始 HWND
时可以访问的公用成员 m_hWnd
。CWindow
也有一个 operator HWND
方法,于是你可以传递 CWindow
对象到接受 HWND
的函数中。没有 CWnd::GetSafeHwnd()
的等价物。
CWindow
和 MFC 的 CWnd
很不同。创建 CWindow
对象不需多少代价,因为它只有一个数据成员,而且也没有 MFC 内部用以保存 HWND 到 CWnd
对象的对应关系的对象映射表。还与 CWnd
不一样的是,当一个 CWindow
对象离开作用域,关联的窗口不会被销毁。这意味着你不须总是记住要把你创建的临时 CWindow
对象与关联的窗口脱离。
ATL 中窗口的实现类是 CWindowImpl
。CWindowImpl
包含了做这些事情的代码,比如窗口类注册,窗口子类化,消息映射,以及一个基本的 WindowProc()
。再次不同于 MFC,在MFC 中,所有这些在同一个类里:CWnd
。
还有两个独立的类,包含了对对话框的实现,CDialogImpl
和 CAxDialogImpl
。CDialogImpl
用于普通的对话框,而 CAxDialogImpl
用于要包容 ActiveX 控件的对话框。
定义窗口实现
任何要创建的非对话框窗口应该从 CWindowImpl
派生。新类里需要包含三样:
- 窗口类定义
- 消息映射
- 窗口使用的默认风格,称作窗口修饰
窗口类的定义使用 DECLARE_WND_CLASS
或者 DECLARE_WND_CLASS_EX
宏来完成。它们都定义了一个封装了 WNDCLASSEX 结构的 ATL 结构 CWndClassInfo
。 DECLARE_WND_CLASS
允许指定新窗口类的名字,其余成员使用缺省值;而 DECLARE_WND_CLASS_EX
还允许指定类风格和窗口背景色。类名也可以用 NULL
,ATL 将生成一个。
我们从一个新的类的定义开始,随后的章节里我会逐步向其中增加内容。
))
};
接下来是消息映射。ATL 的消息映射比 MFC 的映射简单的多。一个 ATL 映射被展开为一个大的 switch 语句。switch 查找合适的处理器并调用相应的函数。消息映射的宏是 BEGIN_MSG_MAP
和 END_MSG_MAP
。向窗口中添加一个空的映射。
))
BEGIN_MSG_MAP(CMyWindow)
END_MSG_MAP()
};
我会在下一节中讲解如何向映射中添加处理器。最后,我们要为我们的类定义窗口修饰 。窗口修饰是窗口风格和窗口扩展风格的组合,这些风格在创建窗口时会被用到。这些风格作为模板参数被指定,所以调用者在创建窗口时可以不被如何得到正确的风格而烦恼。这是一个示例的修饰定义,使用了 ATL 类 CWinTraits
:
))
BEGIN_MSG_MAP(CMyWindow)
END_MSG_MAP()
};
调用者可以在 CMyWindowTraits
定义中覆盖这些风格,不过通常没有必要。ATL 还有一些专用的预定义 CWinTraits
,其中适用于像我们这样的顶级窗口的一个是 CFrameWinTraits
:
CWinTraits WS_CLIPSIBLINGS,
WS_EX_APPWINDOW | WS_EX_WINDOWEDGE>
CFrameWinTraits;
填充消息映射
ATL 的消息映射对开发人员来讲是一个缺乏友好性的地方,同时也是 WTL 极大的增强了的地方。ClassView 提供了添加消息处理器的功能,但 ATL 没有类似于 MFC 的特定消息相关的宏和自动化参数解析。在 ATL 里,只有三种类型的消息处理器,一个针对 WM_NOTIFY
,一个针对 WM_COMMAND
,一个针对其他所有的消息。我们从向窗口添加 WM_CLOSE
和 WM_DESTROY
的消息处理器开始。
;
}
};
你会注意到处理器接收原始的 WPARAM
和 LPARAM
值,当消息使用这些参数时你需要自己去解析它们。还有第四个参数,bHandled
。这个参数在处理期调用之前被 ATL 设为 TRUE
。如果你希望 ATL 的缺省 WindowProc()
在你的处理器返回之后也能处理消息,你可以将 bHandled
设为 FALSE
。这和 MFC 不一样,MFC 中必须显式调用消息处理器的基类实现。
我们再添加 WM_COMMAND
的处理器。假定我们窗口的菜单有一个 ID 为 IDC_ABOUT
的 About 项:
;
}
};
注意,COMMAND_HANDLER
宏为你做了解析消息参数的工作。NOTIFY_HANDLER
宏类似地解析 WM_NOTIFY
的消息参数。
高级消息映射和嵌入(Mix-in)类
ATL 中一个最大的不同是任何 C++ 类都可以处理消息,不像 MFC 里消息处理的任务在 CWnd
和 CCmdTarget
间分割开来,再加上几个有 PreTranslateMessage()
方法的类。这一能力允许我们写通常被称作嵌入类的东西,以便通过向继承列表中添加类就可以向我们的窗口中增加特性。
带有消息映射的基类通常是一个以派生类名作为模板参数的模板,这样就能访问派生类中像 m_hWnd(CWindow 中的 HWND 成员
)这样的成员。我们来看一下这个嵌入类,它通过处理 WM_ERASEBKGND
来绘制窗口背景。
:
HBRUSH m_hbrBkgnd;
};
我们来审视一下这个新类。首先,CPaintBkgnd
有两个模板参数:派生类的名字:CPaintBkgnd
,和背景的颜色(t_
前缀通常用于值类型的模板参数)CPaintBkgnd
也从 ATL 类 CMessageMap
派生,这不是严格地必需的,因为 BEGIN_MSG_MAP
宏对于要处理消息的类就已经足够了;因此当你再看其它示例代码时可能看不到把该类作为基类。
构造函数和析构函数相当简单,它们创建/销毁了一个 t_crBrushColor
颜色的画刷。接下来的消息映射处理了 WM_ERASEBKGND
。最后,OnEraseBkgnd()
处理器使用构造函数中创建的画刷来填充窗口。OnEraseBkgnd()
中有两件事值得注意。首先,它使用了派生类的窗口函数(名为 GetClientRect()
)。我们怎么知道派生类里恰好有一个 GetClientRect()
函数?但如果没有的话,代码根本不能编译!编译器确保派生类 T
派生于 CWindow
。其次,OnEraseBkgnd()
从 wParam
处得到设备上下文(WTL 将关照此事,我们最终会到那儿的,我保证!)
在我们的窗口中使用这一嵌入类,要做两件事。首先,我们把它加到继承列表中:
然后,我们要使 CMyWindow
传递消息到 CPaintBkgnd
。这叫做串 联消息映射。在 CMyWindow
的消息映射里,加入 CHAIN_MSG_MAP
宏:
END_MSG_MAP()
...
};
任何到达 CMyWindow
映射而没有处理的消息将被传递到 CPaintBkgnd
的映射中。注意,WM_CLOSE
,WM_DESTROY
,和 IDC_ABOUT
不会被 串联,因为只要它们一被处理,对消息映射的搜索就会终止。typedef 是必要的,因为 CHAIN_MSG_MAP
是个接收单个参数的预处理宏;如果我们以 CPaintBkgnd0,0,255)>
作为参数,其中的逗号会使预处理器认为我们以不止一个参数来调用该宏。
你可以在继承列表中放心的使用多个嵌入类,对每一个类使用 CHAIN_MSG_MAP
宏以使消息能够传入。这不同于 MFC,每个 CWnd
派生类只能有一个基类,而且 MFC 自动向基类传递未处理的消息。
ATL EXE 的结构
现在我们有了一个完整的(虽然不怎么有用)主窗口,我们来看看如何在程序中使用它。ATL 的可执行程序包含一个全局的 CComModule
变量,名为 _Module
。这与 MFC 程序中名为 theApp
的全局 CWinApp
变量类似;唯一的不同是,在 ATL 里,这个变量必须命名为 _Module
。
我们的 stdafx.h 以此开始:
atlbase.h 会包含基本的 Windows 头文件,所以不必再包含 windows.h,tchar.h 等。在 CPP 文件里,声明(译者注:应该为定义)_Module
变量:
CComModule _Module;
CComModule
中有我们需要在 WinMain()
中调用的显式初始化/退出函数,所以我们以此为始:
nCmdShow)
{
_Module.Init(NULL, hInst);
_Module.Term();
}
传给 Init()
的第一个参数仅在 COM 服务器中使用。而我们的 EXE 不包括 COM 对象,所以我们只需要传入 NULL
。ATL 并不像 MFC 那样提供自己的 WinMain()
或者消息泵,所以要使我们的程序运行起来,需要创建 CMyWindow
对象并添加消息泵。
msg.wParam;
}
以上代码中唯一与众不同的是 CWindow::rcDefault
,它是 CWindow 的
一个 RECT
成员。将它作为窗口的初始 RECT
就像在 CreateWindow()
API 里用 CW_USEDEFAULT
表示宽和高。
ATL 在底下使用了一些汇编语言的把戏来把主窗口的句柄和与之相应的 CMyWindow
对象联系起来。在此之上就是我们可以把 CWindow
对象在线程间传来传去而不出问题,而如果在 MFC 里对 CWnd
这样干会死得很惨。
我们的窗口看起来像是这样:
我得承认,没什么特别激动人心的事情。为了能增添些情趣,我们会加一个能显示对话框的 About 菜单项。
ATL中的对话框
正如已经提到的,ATL 有两个对话框类。我们将为我们的对话框使用 CDialogImpl
。创建一个新的对话框类与创建一个新的框架窗口类大致相当,只不过有两处不同:
- 基类是
CDialogImpl
而不是 CWindowImpl
- 需要定义一个名为
IDD
的公用成员,其中包含有对话框的资源 ID
这是新的 about 对话框类的初始定义:
{ IDD = IDD_ABOUT };
BEGIN_MSG_MAP(CAboutDlg)
END_MSG_MAP()
};
ATL 没有针对 OK 和 Cancel 按钮的内建处理器,所以我们需要自己写代码,顺便还有 WM_CLOSE
的处理器,当用户点击标题条上的关闭按钮时此处理器会被调用。我们还需要处理 WM_INITDIALOG
以使对话框出现时能正确的设置键盘焦点。这是带有消息处理器的完整的类定义。
;
}
};
我们为 OK 和 Cancel 使用同一个处理器以演示 wID
参数,该参数的值被设为 IDOK
或 IDCANCEL
,这取决于哪个按钮被点击。
先是对话框和 MFC 相似,创建新类的一个对象并调用 DoModal()
。回到我们的主窗口并添加一个拥有 About 菜单项的菜单,该项将显示我们的 about 对话框。我们需要添加两个消息处理器,一个是为 WM_CREATE
而另一个是为新的菜单项 IDC_ABOUT
。
};
模态对话框的一个小小的不同是在什么地方指定对话框的父窗口。MFC 里是向 CDialog
的构造函数传递父窗口,而在 ATL 中应该把父窗口作为 DoModal()
的第一个参数传递。如果像上面的代码一样不指定,ATL 使用 GetActiveWindow()
(将会是我们的框架窗口 )的返回结果作为父窗口。
LoadMenu()
调用也演示了 CComModule
的一个方法:GetResourceInstance()
。他返回我们的 EXE 的 HINSTANCE
,就像 AfxGetResourceHandle()
一样(还有一个 CComModule::GetModuleInstance()
,此函数与 AfxGetInstanceHandle()
相仿 )。
这是我们修改后的主窗口和 about 对话框:
就要到 WTL 了,我保证!
不过会是在第二部分里。我是在为 MFC 开发人员写这些文章,所以我认为在进入 WTL 之前,最好对 ATL 先做个介绍。如果这是你对 ATL 的第一次亲密接触,那现在也许是你自己写一些简单应用的好机会,从而可以熟悉消息映射以及嵌入类的使用。你也可以实践一下 ClassView 对 ATL 消息映射的支持,它可以为你添加消息处理器。想要开始,请右击 CMyWindow 项并选择关联菜单中的 Add Windows Message Handler。
在第二部分里,我将讲解基本的 WTL 窗口类、WTL AppWizard,以及更好的消息映射宏。