特别注:由于本页内容栏宽度不够,会导致部分内容看不见,请点击这里以获得最佳浏览效果。
第六部分 – 掌控 ActiveX 控件
内容
- 简介
- 以 AppWizard 开始
- 创建工程
- 生成的代码
- 使用资源编辑器添加控件
- 用于掌控控件的 ATL 类
- CAxDialogImpl
- AtlAxWin 和 CAxWindow
- 调用控件的方法
- 接收控件激发的事件
- 在 VC 6 里添加处理器
- 在 VC 7 里添加处理器
- 事件的知会
- VC 6 里的知会
- VC 7 里的知会
- 示例工程概述
- 运行时创建 ActiveX 控件
- 键盘处理
- 下一步
- 修订历史
简介
在这第六部分里,我将介绍 ATL 对在对话框中掌控(hosting)ActiveX 控件的支持。由于 ActiveX 控件是 ATL 的专项,所以这儿并没有相关的 WTL 类。不过,因为 ATL 掌控控件的方式与 MFC 迥异,所以这是我们要介绍的一个重要主题。我会介绍如何掌控控件以及接收(sink)事件,并开发一个相比用 MFC 的 ClassWizard 写就的应用毫无功能损失的应用程序。当然,你可以在你写的 WTL 应用中使用 ATL 对控件掌控的支持。
本文的示例工程演示了如何掌控 IE 的 Web 浏览器控件。我选择浏览器控件是基于以下两个不错的理由:
- 每个人的机器上都有它,而且
- 它有很多方法并会激发(fire)很多事件,因此用于演示目的,它是确是个很好的控件。
我肯定比不上那些花了很多时间使用 IE 的 Web 浏览器控件编写定制浏览器的人们。但是,通读本文之后,你就会有足够的知识开始编写你自己的定制浏览器了!
以 AppWizard 开始
创建工程
WTL 的 AppWizard 可以为我们创建马上就能掌控 ActiveX 控件的应用。下面我们要创建一个称为 IEHoster 的新工程。像上一章一样,我们要使用一个非模态对话框,只不过这次要把 Enable ActiveX Control Hosting 复选框选中,就象这样:
选中这个复选框会使得我们的主对话框从 CAxDialogImpl 中派生,因此能够掌控 ActiveX 控件。在 VC 6 的向导里,在第二页上还有另外一个复选框,其文字为 Host ActiveX Controls,但是选中它对结果代码没有任何影响,所以在第一页里就可以点击 Finish 按钮完成了。
生成的代码
在这一节里,我会先介绍一些原来没有见过的由 AppWizard 生成的代码片断;下一节里,我再详细介绍 ActiveX 掌控类。
第一个需要检视的文件是 stdafx.h,其中的包含文件有:
1 2 3 4 5 6 7 8 9 10 |
#include <atlbase.h> #include <atlapp.h> extern CAppModule _Module; #include <atlcom.h> #include <atlhost.h> #include <atlwin.h> #include <atlctl.h> // .. other WTL headers ... |
atlcom.h 和 atlhost.h 相对重要。它们包括了一些 COM 相关的类(比如智能指针 CComPtr),以及用来掌控控件的窗口类。
接下来,再看 maindlg.h 中 CMainDlg 的声明:
1 2 3 |
class CMainDlg : public CAxDialogImpl<CMainDlg>, public CUpdateUI<CMainDlg>, public CMessageFilter, public CIdleHandler |
CMainDlg 现在是派生于 CAxDialogImpl,后者是使对话框能够掌控 ActiveX 控件的第一步。
最后,是 WinMain() 中的一行新代码:
1 2 3 4 5 6 7 8 9 10 11 12 |
int WINAPI _tWinMain(...) { //... _Module.Init(NULL, hInstance); <b>AtlAxWinInit();</b> int nRet = Run(lpstrCmdLine, nCmdShow); _Module.Term(); return nRet; } |
AtlAxWinInit() 注册了一个名为 AtlAxWin 的窗口类。该类在 ATL 为 ActiveX 控件创建宿主窗口时使用。
由于 ATL 7 的一个改动,你必须给 _Module.Init() 传递一个 LIBID。论坛中的一些人建议在 VC 7 中使用如下代码:
1 |
_Module.Init(NULL, hInstance, &LIBID_ATLLib); |
这个改动在我这儿工作的很好。
使用资源编辑器添加控件
ATL 允许你象在 MFC 应用中一样使用资源编辑器向对话框上添加 ActiveX。首先,在对话框编辑器中右击,选择 Insert ActiveX control:
VC 会显示一个你的系统上所安装的控件的列表。向下滚动到 Microsoft Web Browser 并点击 OK,可以将该控件插入到对话框中。查看一下新控件的属性并将其 ID 设置为 IDC_IE。对话框看起来应该象下面这样,在编辑器中控件也是可见的:
如果你现在就编译并运行这个应用,你就可以在对话框中看到 Web 浏览器控件。由于我们还没有告诉它应该导航到何处,所以它显示的是一个空白页。
在下一节里,我将介绍有关创建和掌控 ActiveX 控件的 ATL 类,然后我们再看如果使用这些类来和浏览器进行通讯。
用于掌控控件的 ATL 类
在对话框中掌控一个 ActiveX 控件的时候,会有两个类协同工作: CAxDialogImpl 和 CAxWindow。它们处理控件容器必须实现的所有接口,并为常见的操作(比如对 COM 控件查询一个特定的接口)提供一些辅助函数。
CAxDialogImpl
第一个要介绍的就是 CAxDialogImpl。在你写对话框类的时候,你应该从 CAxDialogImpl 而不是 CDialogImpl 派生,这样才能掌控控件。 CAxDialogImpl 覆盖了 Create() 和 DoModal(),这两个函数由全局函数 AtlAxCreateDialog() 和 AtlAxDialogBox() 分别调用。因为 IEHost 对话框是由 Create() 创建的,所以我们应该仔细打量一下 AtlAxCreateDialog()。
AtlAxCreateDialog() 先加载对话框资源,并使用辅助类 _DialogSplitHelper 遍历所有的控件,以寻找那些由资源编辑器生成的并标明是一个需要创建的 ActiveX 控件的项。例如,下面是 IEHost.rc 文件中为 Web 浏览器生成的项:
1 2 |
CONTROL "",IDC_IE,"{8856F961-340A-11D0-A96B-00C04FD705A2}", WS_TABSTOP,7,7,116,85 |
第一个参数是窗口标题(一个空串),第二个是控件 ID,第三个是窗口类名。 _DialogSplitHelper::SplitDialogTemplate() 一看到窗口类是由 '{' 开头就知道这是一个 ActiveX 控件项,它会在内存中创建一个新的对话框模板,在新模板里,那些特殊的 CONTROL 项由创建 AtlAxWin 窗口的项所替代。内存中的新项相当于如下定义:
1 2 |
CONTROL "{8856F961-340A-11D0-A96B-00C04FD705A2}", IDC_IE, "AtlAxWin", WS_TABSTOP, 7, 7, 116, 85 |
其结果是一个 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 变量上。
1 |
CAxWindow wndIE = GetDlgItem(IDC_IE); |
接下来,我们声明一个 IWebBrowser2 接口指针并使用 CAxWindow::QueryControl() 向浏览器控件查询该接口:
1 2 3 4 |
CComPtr<IWebBrowser2> pWB2; HRESULT hr; hr = wndIE.QueryControl ( &pWB2 ); |
QueryControl() 调用 Web 浏览器的 QueryInterface(),如果成功的话, IWebBrowser2 就会返回给我们。然后我们可以调用 Navigate():
1 2 3 4 5 6 7 |
if ( pWB2 ) { CComVariant v; // empty variant pWB2->Navigate ( CComBSTR("http://www.codeproject.com/"), &v, &v, &v, &v ); } |
接收控件激发的事件
从 Web 浏览器获取一个接口是相当简单的,而且这还可以允许我们从一个方向 – 即向控件进行通讯,还有很多的通讯,是以事件的形式从控件而来。ATL 中具有封装了连接点和事件接收的类,使得我们可以接收到由浏览器激发的事件。要使用这一支持,我们要做四件事:
- 将 IDispEventSimpleImpl 添加到 CMainDlg 的继承列表
- 写一个事件接收映射以表明我们要处理哪些事件
- 为这些事件编写处理器
- 把控件连接到接收映射上(这一过程称为知会(advising))(译注:对于 advise/advising 在 COM 方面的使用,业界尚没有一个被广泛接受的统一译法,此处译者姑且译为知会,之所以没有译为通知,是因为在文中很难与 notify/notification 区别开来)
VC 的 IDE 在此过程中可以提供极大的帮助 – 它会为你对 CMainDlg 进行改动,还可以查询 ActiveX 控件的类型库,显示控件可以激发的事件的列表。由于 VC 6 和 VC 7 中添加处理器的用户界面不同,下面我分开来介绍。
在 VC 6 里添加处理器
有两种方法可以调出添加处理器的界面:
- 在 ClassView 窗格里,右击 CMainDlg 并选择菜单中的 Add Windows Message Handler。
- 在查看 CMainDlg 的代码时,或者在资源编辑器中查看相关的对话框时,点击 WizardBar 上 Action 按钮的下拉箭头,并选择菜单中的 Add Windows Message Handler。
选择该命令之后,VC 会显示一个对话框,其中有一个题为 class or object to handle 的控件列表。在列表中选中 IDC_IE,则 VC 会把 WebBrowser 控件可以激发的事件填充到 New Windows message/events 列表中。
因为我们要为 DownloadBegin 事件添加处理器,所以要选中该事件并点击 Add and Edit 按钮。VC 就会提示你要求给出方法名:
在你第一次添加事件处理器时,VC 会对 CMainDlg 做一点改动,使其可以成为一个事件接收器。头文件中的改动有点零散,汇总起来就是下面的这些代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<b>#import</b> "C:\WINNT\System32\shdocvw.dll" class CMainDlg : public CAxDialogImpl<CMainDlg>, public CUpdateUI<CMainDlg>, public CMessageFilter, public CIdleHandler, <b>public IDispEventImpl<IDC_IE, CMainDlg></b> { // ... public: <b>BEGIN_SINK_MAP(CMainDlg) SINK_ENTRY(IDC_IE, 0x6a, OnDownloadBegin) END_SINK_MAP()</b> void __stdcall OnDownloadBegin() { // TODO: Add Code for event handler. } }; |
#import 语句是一个编译器指令,用以读取 shdocvw.dll (WebBrowser ActiveX 控件的实现就在此文件中)中的类型库,并为能使用控件中的组件类和接口创建封装类。通常你会把此指令放到 stdafx.h 中,不过在本例中,我们其实根本不需要它,因为 Platform SDK 中已经有了含有 WebBrowser 的接口和方法的头文件了。
继承列表中现在已经有了 IDispEventImpl。它有两个模板参数,第一个是我们指派给 ActiveX 控件的 ID,即 IDC_IE,第二个是派生于 IDispEventImpl 的类的名字。
接收映射由 BEGIN_SINK_MAP 和 END_SINK_MAP 宏隔起来。每一个 SINK_ENTRY 宏列出了一个 CMainDlg 要处理的事件,宏的参数分别为控件 ID(又是 IDC_IE),事件的分派 ID,以及事件到达时要调用的函数的名字。VC 会从 ActiveX 控件的类型库中读取分派 ID,所以不必担心应该指定什么数值(exdispid.h 头文件中列出了 IE 和资源浏览器发送的许多事件的 ID,如果你到其中查看,你可以看到 0x6A 对应着常量 DISP_DOWNLOADBEGIN)。
最后面是一个新方法, OnDownloadBegin()。对那些有参数的事件,VC 会为方法设置正确的原型。所有的事件处理器都是 __stdcall 调用规范,因为它们是 COM 方法。
在 VC 7 里添加处理器
也有两种方法可以添加事件处理器。你可以在对话框编辑器中的 ActiveX 控件上右击,并在菜单上选择 Add Event Handler。你可以在显示出的对话框里选择事件名并设置处理器的名字。
点击 Add and Edit 按钮将添加该处理器,对 CMainDlg 做必要的更改,并打开 maindlg.cpp 文件,高亮显示着新添加的处理器。
另一个方法是查看 CMainDlg 的属性页,展开 Controls 结点,然后是 IDC_IE 结点。在 IDC_IE 结点下,你可以找到控件激发的事件。
你可以点击事件名边上的箭头,选择菜单上的 <Add> [MethodName] 来添加处理器。你还可以稍后修改处理器的名字,当然还是在属性页里改。
VC 7 对 CMainDlg 的修改和 VC 6 几乎一样,一个例外是并不添加 #import 指令。
事件的知会
最后一步是知会到控件, CMainDlg 想要接收由 WebBrowser 控件激发的事件。此过程在 VC 6 和 VC 7 里仍然不一样,所以还需要分别介绍。相同的是,知会都发生在 OnInitDialog() 里,反知会(unadvising)发生在 OnDestroy() 里。
VC 6 中的知会
VC 6 的 ATL 里有一个全局函数 AtlAdviseSinkMap()。该函数接收一个具有接收映射的 C++ 对象的指针(通常为 this 指针),以及一个布尔值。如果布尔值为 true,则对象是希望开始接收事件,如果为 false,则对象希望停止接收事件。 AtlAdviseSinkMap() 知会对话框中所有的控件开始或者停止向 C++ 对象发送事件。
要使用此函数,就要为 WM_INITDIALOG 和 WM_DESTROY 添加处理器,然后再像这样调用 AtlAdviseSinkMap():
1 2 3 4 5 6 7 8 9 10 11 |
BOOL CMainDlg::OnInitDialog(...) { // Begin sinking events AtlAdviseSinkMap ( this, true ); } void CMainDlg::OnDestroy() { // Stop sinking events AtlAdviseSinkMap ( this, false ); } |
AtlAdviseSinkMap() 返回一个 HRESULT 表示知会成功与否。如果 AtlAdviseSinkMap() 在 中失败了,那么你就不能从有的(或者是全部的) ActiveX 控件处得到事件。
VC 7 中的知会
在 VC 7 里, CAxDialogImpl 有一个名为 AdviseSinkMap() 的方法封装了 AtlAdviseSinkMap()。 AdviseSinkMap() 接收一个布尔参数,其意义与 AtlAdviseSinkMap() 的第二个参数相同。 AdviseSinkMap() 检查到类里有一个接收映射,就会调用 AtlAdviseSinkMap()。
相对于 VC 6,最大的不同在于 CAxDialogImpl() 已经有了为你调用 AdviseSinkMap() 的 WM_INITDIALOG 和 WM_DESTROY 的处理器。要想受益于此特性,就要在 CMainDlg 消息映射的开头添加一个 CHAIN_MSG_MAP 宏,就像这样:
1 2 3 4 |
BEGIN_MSG_MAP(CMainDlg) CHAIN_MSG_MAP(CAxDialogImpl<CMainDlg>) // rest of the message map... END_MSG_MAP() |
示例工程概述
我们已经知道了事件接收是怎么工作的,现在我们来看看整个 IEHost 工程。正像我们所讨论的,它掌控了 Web 浏览器控件,并处理了六个事件。它还显示了一个事件的列表,这样你就可以知道定制浏览器是怎样使用这些事件来在 UI 上提供进度的。应用处理的事件有:
- BeforeNavigate2 和 NavigateComplete2:这两个事件可以使应用监测到 URL 导航。如果你愿意,你可以在 BeforeNavigate2 的响应里取消导航。
- DownloadBegin 和 DownloadComplete:应用程序使用这两个事件来控制表示浏览器正在工作的“等待”信息。更精致的程序还会像 IE 一样使用个动画。
- CommandStateChange:此事件告诉应用什么时候可以使用“后退”和“前进”导航命令。应用会据此相应地启用或者禁止后退和前进按钮。
- StatusTextChange:好几种情况下都会激发此事件,例如当鼠标光标移动到超链接上时。此事件会发送一个字符串,应用会响应此事件,将字符串显示到浏览器窗口下面的一个静态控件里。
应用里还有四个控制浏览器的按钮:后退、前进、停止以及重新加载。这些按钮会调用到相应的 IWebBrowser2 方法。
事件以及伴随事件的数据都被记录到了列表控件里,所以事件一激发你就能看到。你可以关闭任一事件的日志,这样你就可以只观测其中的一两个。为了演示一些实质性的事件处理,在 BeforeNavigate2 处理器中会检查 URL,如果其中包含了“doubleclick.net”,则本次导航会被取消。作为 IE 的插件而不是 HTTP 代理运行的广告和弹出窗口拦截器使用的正是这个方法。下面是作此检查的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
void __stdcall CMainDlg::OnBeforeNavigate2 ( IDispatch* pDisp, VARIANT* URL, VARIANT* Flags, VARIANT* TargetFrameName, VARIANT* PostData, VARIANT* Headers, VARIANT_BOOL* Cancel ) { CString sURL = URL->bstrVal; // You can set *Cancel to VARIANT_TRUE to stop the // navigation from happening. For example, to stop // navigates to evil tracking companies like doubleclick.net: if ( sURL.Find ( _T("doubleclick.net") ) > 0 ) *Cancel = VARIANT_TRUE; } |
下面是我们的应用在浏览论坛时的样子:
IEHost 还演示了另外好几个在前文中介绍过的 WTL 特性: CBitmapButton(用于浏览器控制按钮), CListViewCtrl(用于事件记录),DDX(用于跟踪复选框的状态),以及 CDialogResize。
运行时创建 ActiveX 控件
在运行时而不是在资源编辑器中创建 ActiveX 控件也是可以的。About 对话框演示了这一技术。对话框资源包含了一个占位用的分组框,表明了浏览器控件该在什么位置:
在 OnInitDialog() 中,我们使用 CAxWindow 来创建一个新的 AtlAxWin,它会与占位控件使用相同的 RECT,而占位控件随即被销毁:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
LRESULT CAboutDlg::OnInitDialog(...) { CWindow wndPlaceholder = GetDlgItem ( IDC_IE_PLACEHOLDER ); CRect rc; CAxWindow wndIE; // Get the rect of the placeholder group box, then destroy // that window because we don't need it anymore. wndPlaceholder.GetWindowRect ( rc ); ScreenToClient ( rc ); wndPlaceholder.DestroyWindow(); // Create the AX host window. wndIE.Create ( *this, rc, _T(""), WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN ); |
接下来,我们使用 CAxWindow 的一个方法来创建 ActiveX 控件。可供我们选择的两个方法是 CreateControl() 和 CreateControlEx()。 CreateControlEx() 有一个附加的参数可以返回接口指针,这样你就不必再另行调用 QueryControl()。我们感兴趣的两个参数,其一是第一个参数,它是 Web 浏览器控件的 GUID 的字符串版本,其二是第四个参数,它是指向 IUnknown* 的一个指针。此指针将会被填充为 ActiveX 控件的 IUnknown。控件创建之后,我们再查询 IWebBrowser2 接口,与前文类似,再将控件导航到一个 URL。
1 2 3 4 5 6 7 8 9 10 11 12 |
CComPtr<IUnknown> punkCtrl; CComQIPtr<IWebBrowser2> pWB2; CComVariant v; // empty VARIANT // Create the browser control using its GUID. wndIE.CreateControlEx ( L"{8856F961-340A-11D0-A96B-00C04FD705A2}", NULL, NULL, &punkCtrl ); // Get an IWebBrowser2 interface on the control and navigate to a page. pWB2 = punkCtrl; pWB2->Navigate ( CComBSTR("about:mozilla"), &v, &v, &v, &v ); } |
对于具有 ProgID 的 ActiveX 控件,你还可以将其 ProgID 传递给 CreateControlEx() 以代替 GUID。例如,我们可以使用以下调用来创建浏览器控件:
1 2 3 |
// Use the control's ProgID: Shell.Explorer: wndIE.CreateControlEx ( L"Shell.Explorer", NULL, NULL, &punkCtrl ); |
CreateControl() 和 CreateControlEx() 还有专门用于 Web 浏览器的重载函数。如果你的应用把网页作为 HTML 类型的资源包含进来,你就可以将其资源 ID 作为第一个参数,ATL 会创建一个 Web 浏览器控件并导航至该资源。IEHost 包含了一个 ID 为 IDR_ABOUTPAGE 的页面,因而我们可以用以下代码在 About 对话框中显示它:
1 |
wndIE.CreateControl ( IDR_ABOUTPAGE ); |
下面是成果:
示例工程中包括了上述三种技术的所有代码,查点 CAboutDlg::OnInitDialog() 并注释或者取消注释其中的代码,可以看每种方法的运作。
键盘处理
最后,却也非常重要的一个细节是键盘消息。ActiveX 控件的键盘处理相当复杂,因为宿主和控件必须一起合作来保证控件能够看到它感兴趣的消息。例如,浏览器控件可以让你使用 TAB 键在链接之间移动。MFC 自己会处理所有的这些,所以你可能从未意识到要使键盘支持能够正常工作所需的工作量。
不幸的是,AppWizard 不会为基于对话框的应用生成键盘处理的代码。不过,如果你创建一个 SDI 应用并使用窗体视图(form view)作为视图窗口,那么你就可以在 PreTranslateMessage() 中看到所需的代码。每当从消息队列里读出一个鼠标或者键盘消息,前述代码就会获取拥有焦点的控件并使用 ATL 中的 WM_FORWARDMSG 消息将读出的消息转发给控件。通常,一个窗口在接收到 WM_FORWARDMSG 时什么也不做,因为它对该消息一无所知。但是当一个 ActiveX 控件拥有焦点时, WM_FORWARDMSG 消息最终会被送到掌控控件的 AtlAxWin 处。 AtlAxWin 会识别出 WM_FORWARDMSG 并采取必要的步骤来看控件自己是否想处理该消息。
如果拥有焦点的窗口不认识 WM_FORWARDMSG ,则 PreTranslateMessage() 会调用 IsDialogMessage() 以使诸如 TAB 这样的标准对话框导航键可以正常工作。
示例工程中包含了 PreTranslateMessage() 中的必要的代码。因为 PreTranslateMessage() 仅工作于非模态对话框,所以如果你想有正确的键盘处理的话就必须使用一个非模态对话框。
下一步
在下一篇文章里,我们要返回到框架窗口,介绍关于使用分割条窗口的话题。
修订历史
2003 年 5 月 20 日:首次发布
2006 年 1 月 5 日:重写了接收映射、事件处理器以及协商几节。旧代码比真正所需更为复杂。