译:MFC 程序员的 WTL 教程(六)

引言:好几年前就读过这个系列了,也曾经有过翻译的念头,都因种种原因作罢。前些日子在网上看到了一位网友对此系列的翻译,虽然看起来要比看原文省劲,但却发现许多处不忠实原文的地方,而且还有一些翻译上的错误,所以就生出了重新翻译的念头。这是第六章,敬请大家指正。

特别注 1:由于本页内容栏宽度不够,会导致部分内容看不见,请点击这里以获得最佳浏览效果。
特别注 2:本文为第一版,要浏览第二版请点击这里

链接:上一部分下一部分

第六部分 – 掌控 ActiveX 控件

内容

  • 简介
  • 以 AppWizard 开始
    • 创建工程
    • 生成的代码
  • 使用资源编辑器添加控件
  • 用于掌控控件的 ATL 类
    • CAxDialogImpl
    • AtlAxWin 和 CAxWindow
  • 调用控件的方法
  • 接收控件激发的事件
    • 改动 CMainDlg
    • 编写接收映射
    • 编写事件处理器
  • 示例工程概述
  • 运行时创建 ActiveX 控件
  • 键盘处理
  • 下一步
  • 修订历史

简介

在这第六部分里,我将介绍 ATL 对在对话框中掌控(hosting)ActiveX 控件的支持。由于 ActiveX 控件是 ATL 的专项,所以这儿并没有相关的 WTL 类。不过,因为 ATL 掌控控件的方式与 MFC 迥异,所以这是我们要介绍的一个重要主题。我会介绍如何掌控控件以及接收(sink)事件,并开发一个相比用 MFC 的 ClassWizard 写就的应用毫无功能损失的应用程序。当然,你可以在你写的 WTL 应用中使用 ATL 对控件掌控的支持。

本文的示例工程演示了如何掌控 IE 的 Web 浏览器控件。我选择浏览器控件是基于以下两个不错的理由:

  1. 每个人的机器上都有它,而且
  2. 它有很多方法并会激发(fire)很多事件,因此用于演示目的,它是确是个很好的控件。

我肯定比不上那些花了很多时间使用 IE 的 Web 浏览器控件编写定制浏览器的人们。但是,通读本文之后,你就会有足够的知识开始编写你自己的定制浏览器了!

以 AppWizard 开始

创建工程

WTL 的 AppWizard 可以为我们创建马上就能掌控 ActiveX 控件的应用。下面我们要创建一个称为 IEHoster 的新工程。像上一章一样,我们要使用一个非模态对话框,只不过这次要把 Enable ActiveX Control Hosting 复选框选中,就象这样:

 [AppWizard - 22K]

选中这个复选框会使得我们的主对话框从 CAxDialogImpl 中派生,因此能够掌控 ActiveX 控件。在第二页上还有另外一个复选框,其文字为 Host ActiveX Controls,但是选中它对结果代码没有任何影响,所以在第一页里就可以点击 Finish 按钮完成了。

生成的代码

在这一节里,我会先介绍一些原来没有见过的由 AppWizard 生成的代码片断;下一节里,我再详细介绍 ActiveX 掌控类。

第一个需要检视的文件是 stdafx.h,其中的包含文件有:

atlcom.h 和 atlhost.h 相对重要。它们包括了一些 COM 相关的类(比如智能指针 CComPtr),以及实际用来掌控控件的窗口类。

接下来,再看 maindlg.h 中 CMainDlg 的声明:

CMainDlg 现在是派生于 CAxDialogImpl,后者是使对话框能够掌控 ActiveX 控件的第一步。

最后,是 WinMain() 中的一行新代码:

AtlAxWinInit() 注册了一个名为 AtlAxWin 的窗口类。该类在 ATL 为 ActiveX 控件创建宿主窗口时使用。

使用资源编辑器添加控件

ATL 允许你象在 MFC 应用中一样使用资源编辑器向对话框上添加 ActiveX。首先,在对话框编辑器中右击,选择 Insert ActiveX control

 [Insert menu - 8K]

VC 会显示一个你的系统上所安装的控件的列表。向下滚动到 Microsoft Web Browser 并点击 OK,可以将该控件插入到对话框中。查看一下新控件的属性并将其 ID 设置为 IDC_IE。对话框看起来就象下面这样,在编辑器中控件也是可见的:

 [ IE control in editor - 6K]

如果你现在就编译并运行这个应用,你就可以在对话框中看到 Web 浏览器控件。由于我们还没有告诉它应该导航到何处,所以它显示的是一个空白页。

在下一节里,我将介绍有关创建和掌控 ActiveX 控件的 ATL 类,然后我们再看如果使用这些类来和浏览器进行通讯。

用于掌控控件的 ATL 类

在对话框中掌控一个 ActiveX 控件的时候,会有两个类协同工作: CAxDialogImplCAxWindow。它们处理控件容器必须实现的所有接口,并为常见的操作(比如对 COM 控件查询一个特定的接口)提供一些辅助函数。

CAxDialogImpl

第一个要介绍的就是 CAxDialogImpl。在你写对话框类的时候,你应该从 CAxDialogImpl 而不是 CDialogImpl 派生,这样才能掌控控件。 CAxDialogImpl 覆盖了 Create()DoModal(),这两个函数由全局函数 AtlAxCreateDialog()AtlAxDialogBox() 分别调用。因为 IEHoster 对话框是由 Create() 创建的,所以我们应该仔细打量一下 AtlAxCreateDialog()

AtlAxCreateDialog() 先加载对话框资源,并使用辅助类 _DialogSplitHelper 遍历所有的控件,以寻找那些由资源编辑器生成的并标明是一个需要创建的 ActiveX 控件的项。例如,下面是 IEHoster.rc 文件中为 Web 浏览器生成的项:

第一个参数是窗口标题(一个空串),第二个是控件 ID,第三个是窗口类名。 _DialogSplitHelper::SplitDialogTemplate() 一看到窗口类是由 '{' 开头就知道这是一个 ActiveX 控件项,它会在内存中创建一个新的对话框模板,在新模板里,那些特殊的 CONTROL 项由创建 AtlAxWin 窗口的项所替代。内存中的新项相当于如下定义:

其结果是一个 AtlAxWin 窗口会使用相同的 ID 被创建出来,而且其窗口标题就是 ActiveX 控件的 GUID。因此,如果你调用 GetDlgItem(IDC_IE),则返回的 HWND 值是 AtlAxWin 窗口的,而不是 ActiveX 控件自己的。

一旦 SplitDialogTemplate() 返回, AtlAxCreateDialog() 再调用 CreateDialogIndirectParam() 以使用修改过的模板来创建对话框。

AtlAxWin 和 CAxWindow

正如上述指出的, AtlAxWin 是用来作为一个 ActiveX 控件的宿主窗口的。随 AtlAxWin 使用的还有一个特殊的窗口接口类: CAxWindow。当从一个对话框模板中创建 AtlAxWin 时, AtlAxWin 的窗口过程,即 AtlAxWindowProc(),会处理 WM_CREATE 并在消息的响应中创建 ActiveX 控件。如果不在对话框模板中,那也可以在运行时创建 ActiveX 控件,不过我们在后面才会介绍。

WM_CREATE 处理器会调用全局的 AtlAxCreateControl(),将 AtlAxWin 的窗口标题传递给它,我们知道,窗口标题已经被设置成了 Web 浏览器的 GUID。 AtlAxCreateControl() 再调用更多的函数,但最后代码会到达 CreateNormalizedObject() 处,它会把窗口标题转换为 GUID 并最终调用 CoCreateInstance() 来创建 ActiveX 控件。

因为 ActiveX 控件是 AtlAxWin 的一个子窗口,所以对话框就不能直接访问控件了。但是, CAxWindow 具有与控件通讯的方法。最常用的方法之一是 QueryControl(),它又会调用到控件的 QueryInterface()。比方说,你可以使用 QueryControl() 来从 Web 浏览器控件中得到一个 IWebBrowser2 接口,并使用该接口把浏览器导航到某个 URL。

调用控件的方法

现在,我们的对话框里就有一个 Web 浏览器了,我们可以使用它的 COM 接口来和它交互。我们要做的第一件事情是使用它的 IWebBrowser2 接口导航到一个新的 URL。在 OnInitDialog() 处理器里,我们可以把掌控着浏览器的 AtlAxWin 附着到一个 CAxWindow 变量上。

接下来,我们声明一个 IWebBrowser2 接口指针并使用 CAxWindow::QueryControl() 向浏览器控件查询该接口:

QueryControl() 调用 Web 浏览器的 QueryInterface(),如果成功的话, IWebBrowser2 就会返回给我们。然后我们可以调用 Navigate()

接收控件激发的事件

从 Web 浏览器获取一个接口是相当简单的,而且这还可以允许我们从一个方向 – 即控件进行通讯,还有很多的通讯,是以事件的形式控件而来。ATL 中具有封装了连接点和事件接收的类,使得我们可以接收到由浏览器激发的事件。要使用这一支持,我们要做四件事:

  1. 使 CMainDlg 成为一个 COM 对象
  2. IDispEventSimpleImpl 添加到 CMainDlg 的继承列表
  3. 写一个事件接收映射以表明我们要处理哪些事件
  4. 为这些事件编写处理器

改动 CMainDlg

我们之所以把 CMainDlg  改造成一个 COM 对象,是 因为事件接收是基于 IDispatch 的。为了使 CMainDlg  能够暴露 COM 接口,它就必须成为一个 COM 对象。 IDispEventSimpleImpl 提供了 IDispatch 的一个实现,而且还处理了那些设置连接点所需的调用。 IDispEventSimpleImpl 还会在我们要处理的事件到达时调用我们的处理器。

下面是要添加到 CMainDlg 继承列表中的类,以及列出了 CMainDlg 暴露的接口的 COM_MAP

CComObjectRootEx 和 CComCoClass 一起使得 CMainDlg 成为了一个 COM 对象。 IDispEventSimpleImpl 的模板参数是:一个事件接收器 ID,我们的类名,以及连接点接口的 IID。ID 可以是任意 unsigned int(译者注:原文中有 positive 作定语,但无符号整数本无正负之分,故而略去)。连接点接口是 DIID_DWebBrowserEvents2,你可以在浏览器控件的相关文档中找到它,或者到 exdisp.h 中查看。

编写接收映射

下一步是为 CMainDlg 添加一个事件接收映射。此映射中列出了我们感兴趣的那些事件,以及我们的事件处理器的名字。我们要的第一个事件是 DownloadBegin。此事件在浏览器开始下载一个页面的时候激发,我们可以在响应代码中显示一条“请等待”消息,于是用户就知道浏览器正处于忙的状态。在 MSDN 中查找 DWebBrowserEvents2::DownloadBegin,我们可以找到此事件的原型为:

这表明该事件既没有参数发送也不需要返回值。为了将此原型转化为接收映射所需的形式,我们需要写一个 _ATL_FUNC_INFO 结构,其中包含了返回值、参数个数以及参数的类型。由于事件是基于 IDispatch 的,所以所有相关的类型都是可以被置入到 VARIANT 中的。整个类型列表相当长,下面是最常用的一些:

VT_EMPTYvoid
VT_BSTRBSTR 格式的字符串
VT_I4:4 字节有符号整数,用于 long</span> 参数
VT_DISPATCHIDispatch*
VT_VARIANTVARIANT
VT_BOOLVARIANT_BOOL(允许的值为 VARIANT_TRUE  或 VARIANT_FALSE

另外,可以把 VT_BYREF 标志加到一个类型上以将其转换为一个指针。例如, VT_VARIANT|VT_BYREF 对应于 VARIANT* 类型。下面是 _ATL_FUNC_INFO 的定义:

其成员有:

cc
我们处理器的调用约定(calling convention)。必须为 CC_STDCALL,也即 __stdcall 约定。
vtReturn
处理器返回值的类型
nParams
处理器接收的参数个数
pVarTypes
所有参数的类型,顺序为从左到右。

知道了这些,就可以为我们的 DownloadBegin 处理器写一个 _ATL_FUNC_INFO 结构了:

现在回到接收器映射。我们为每个要处理的事件把一个 SINK_ENTRY_INFO 宏放到映射中。下面是对于 DownloadBegin 处理器的宏:

宏的参数为接收器 ID(37,与我们将 IDispEventSimpleImpl 添加到继承列表中时使用的数字相同),事件接口的 IID,事件的派发(dispatch)ID,(你可以在 MSDN 或者 exdispid.h 头文件中找到),我们处理器的名字,以及一个描述该处理器的 _ATL_FUNC_INFO 结构的指针。

编写事件处理器

喔,现在终于可以写我们的事件处理器了:

现在我们来看一个更复杂的事件,比方说 BeforeNavigate2。此事件的原型是:

此方法有 7 个 VARIANT 参数,你需要参考 MSDN 来看实际传递的数据类型。我们感兴趣的是 URL,它是 BSTR 格式的字符串。

我们 BeforeNavigate2 处理器的 _ATL_FUNC_INFO 描述就是这样:

与前类似,返回类型是 VT_EMPTY,表示不返回任何值。 nParams 成员为 7,表示有 7 个参数。接下来是一组参数类型。这些类型前文已经介绍过,比如说 VT_DISPATCH 对应于 IDispatch*

接收器映射项与上一个极其相似:

处理器:

我敢打赌,你现在更加感激 ClassWizard 了!当你向一个 MFC 对话框中插入 ActiveX 控件时 ClassWizard 会自动做这些工作。

CMainDlg 改造为 COM 对象还有几个需要注意的地方。首先,全局 Run() 函数必须改动。现在 CMainDlg 是一个 COM 对象了,我们必须使用 CComObject 来创建 CMainDlg

另一个可行的方法是使用 CComObjectStack 类取代 CComObject,并去除 dlgMain.AddRef() 行。 CComObjectStackIUnknown 的三个方法进行了简单的实现 —— 只是立即返回,因为根本就不需要它们 —— 由于是创建在栈上,这种 COM 对象的存在是不在乎引用计数的。

不过,这并不是一个完美的解决方案。 CComObjectStack 仅用于生命期很短的对象,而且很不幸的是,调用任何 IUnknown 的方法都会引发断言。因为 CMainDlg 在其开始侦听事件时就会被 AddRef,因此 CComObjectStack 不适用于此情况。

最终的方法应该是要么使用 CComObject,要么写一个继承于 CComObjectStackCComObjectStack2 类,并允许 IUnknown 方法的调用。存在于 CComObject 中的那个不必要的引用计数微不足道,没有谁会注意到的。不过如果你非要节省这些 CPU 时钟周期,你可以使用示例工程中的 CComObjectStack2 类。

示例工程概述

我们已经知道了事件接收是怎么工作的,现在我们来看看整个 IEHoster 工程。正像我们所讨论的,它掌控了 Web 浏览器控件,并处理了六个事件。它还显示了一个事件的列表,这样你就可以知道定制浏览器是怎样使用这些事件来在 UI 上提供进度的。应用处理的事件有:

  • BeforeNavigate2NavigateComplete2:这两个事件可以使应用监测到 URL 导航。如果你愿意,你可以在 BeforeNavigate2 的响应里取消导航。
  • DownloadBeginDownloadComplete:应用程序使用这两个事件来控制表示浏览器正在工作的“等待”信息。更精致的程序还会像 IE 一样使用个动画。
  • CommandStateChange:此事件告诉应用什么时候可以使用“后退”和“前进”导航命令。应用会据此相应地启用或者禁止后退和前进按钮。
  • StatusTextChange:好几种情况下都会激发此事件,例如当鼠标光标移动到超链接上时。此事件会发送一个字符串,应用会响应此事件,将字符串显示到浏览器窗口下面的一个静态控件里。

应用里还有四个控制浏览器的按钮:后退、前进、停止以及重新加载。这些按钮会调用到相应的 IWebBrowser2 方法。

事件以及伴随事件的数据都被记录到了列表控件里,所以事件一激发你就能看到。你可以关闭任一事件的日志,这样你就可以只观测其中的一两个。为了演示一些实质性的事件处理,在 BeforeNavigate2 处理器中会检查 URL,如果其中包含了“doubleclick.net”,则本次导航会被取消。作为 IE 的插件而不是 HTTP 代理运行的广告和弹出窗口拦截器使用的正是这个方法。下面是作此检查的代码。

下面是我们的应用在浏览论坛时的样子:

 [Sample app - 13K]

IEHoster 还演示了另外好几个在前文中介绍过的 WTL 特性: CBitmapButton(用于浏览器控制按钮), CListViewCtrl(用于事件记录),DDX(用于跟踪复选框的状态),以及 CDialogResize

运行时创建 ActiveX 控件

在运行时而不是在资源编辑器中创建 ActiveX 控件也是可以的。About 对话框演示了这一技术。对话框资源包含了一个占位用的分组框,表明了浏览器控件该在什么位置:

 [About box in editor - 5K]

OnInitDialog() 中,我们使用 CAxWindow 来创建一个新的 AtlAxWin,它会与占位控件使用相同的 RECT,而占位控件随即被销毁:

接下来,我们使用 CAxWindow 的一个方法来创建 ActiveX 控件。可供我们选择的两个方法是 CreateControl() 和 CreateControlEx()CreateControlEx() 有一个附加的参数可以返回接口指针,这样你就不必再另行调用 QueryControl()。我们感兴趣的两个参数,其一是第一个参数,它是 Web 浏览器控件的 GUID 的字符串版本,其二是第四个参数,它是指向 IUnknown* 的一个指针。此指针将会被填充为 ActiveX 控件的 IUnknown。控件创建之后,我们再查询 IWebBrowser2 接口,与前文类似,再将控件导航到一个 URL。

对于具有 ProgID 的 ActiveX 控件,你还可以将其 ProgID 传递给 CreateControlEx() 以代替 GUID。例如,我们可以使用以下调用来创建浏览器控件:

CreateControl() 和 CreateControlEx() 还有专门用于 Web 浏览器的重载函数。如果你的应用把网页作为 HTML 类型的资源包含进来,你就可以将其资源 ID 作为第一个参数,ATL 会创建一个 Web 浏览器控件并导航至该资源。IEHoster 包含了一个 ID 为 IDR_ABOUTPAGE 的页面,因而我们可以用以下代码在 About 对话框中显示它:

下面是成果:

 [About box browser ctrl - 6K]

示例工程中包括了上述三种技术的所有代码,查点 CAboutDlg::OnInitDialog() 并注释或者取消注释其中的代码,可以看每种方法的运作。

键盘处理

最后,却也非常重要的一个细节是键盘消息。ActiveX 控件的键盘处理相当复杂,因为宿主和控件必须一起合作来保证控件能够看到它感兴趣的消息。例如,浏览器控件可以让你使用 TAB 键在链接之间移动。MFC 自己会处理所有的这些,所以你可能从未意识到要使键盘支持能够正常工作所需的工作量。

不幸的是,AppWizard 不会为基于对话框的应用生成键盘处理的代码。不过,如果你创建一个 SDI 应用并使用窗体视图(form view)作为视图窗口,那么你就可以在 PreTranslateMessage() 中看到所需的代码。每当从消息队列里读出一个鼠标或者键盘消息,前述代码就会获取拥有焦点的控件并使用 ATL 中的 WM_FORWARDMSG 消息将读出的消息转发给控件。通常情况下这些代码什么也不做,但是当一个 ActiveX 控件拥有焦点时, WM_FORWARDMSG 消息最终会被送到掌控控件的 AtlAxWin 处。 AtlAxWin  会识别出 WM_FORWARDMSG 并采取必要的步骤来看控件自己是否想处理该消息。

如果拥有焦点的窗口不认识 WM_FORWARDMSG ,则 PreTranslateMessage() 会调用 IsDialogMessage() 以使诸如 TAB 这样的标准对话框导航键可以正常工作。

示例工程中包含了 PreTranslateMessage() 中的必要 的代码。因为 PreTranslateMessage() 仅工作于非模态对话框,所以如果你想有正确的键盘处理的话就必须使用一个非模态对话框。

下一步

在下一篇文章里,我们要返回到框架窗口,介绍关于使用分割条窗口的话题。

修订历史

2003 年 5 月 20 日:首次发布

链接:上一部分下一部分

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注