WTL for MFC Programmers, Part VI - 托管 ActiveX 控件






4.96/5 (85投票s)
2003年5月20日
17分钟阅读

700927

6528
关于在WTL对话框中托管ActiveX控件的教程。
目录
- 引言
- 从AppWizard开始
- 使用资源编辑器添加控件
- 用于控件托管的ATL类
- 调用控件的方法
- 接收控件触发的事件
- 示例项目概述
- 在运行时创建ActiveX控件
- 键盘处理
- 接下来
- 版权和许可
- 修订历史
引言
在第六部分,我将介绍ATL对在对话框中托管ActiveX控件的支持。由于ActiveX控件是ATL的专长,所以不需要额外的WTL类。然而,ATL的托管方式与MFC的方式有很大的不同,因此这是一个需要介绍的重要主题。我将介绍如何托管控件和接收事件,并开发一个与使用ClassWizard编写的MFC应用程序功能上没有区别的应用程序。当然,你也可以在你编写的WTL应用程序中使用ATL的托管支持。
本文的示例项目演示了如何托管IE WebBrowser控件。我选择浏览器控件有两个很好的理由:
- 每个人都在他们的电脑上安装了它,并且
- 它有很多方法和事件,因此它是一个很好的演示控件。
我当然无法与那些花费大量时间围绕WebBrowser控件编写自定义浏览器的开发者竞争。然而,读完本文后,你将有足够多的知识来开始开发你自己的自定义浏览器。
从AppWizard开始
创建项目
WTL AppWizard可以为我们创建一个已准备好托管ActiveX控件的应用程序。我们将从一个新项目开始,这个项目将被命名为IEHost。我们将使用一个无模式对话框,就像上一篇文章一样,只是这次要勾选Enable ActiveX Control Hosting复选框,如下图所示:
勾选该框后,我们的主对话框将派生自CAxDialogImpl
,以便它可以托管ActiveX控件。在VC 6向导中,第二页上还有一个名为Host ActiveX Controls的复选框,但勾选它似乎对生成的代码没有影响,所以你可以从第一页点击“Finish”。
生成的代码
在本节中,我将介绍一些AppWizard之前没有出现过的代码。在下一节中,我将详细介绍ActiveX托管类。
第一个要查看的文件是stdafx.h,它有以下包含:
#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
的声明:
class CMainDlg : public CAxDialogImpl<CMainDlg>, public CUpdateUI<CMainDlg>, public CMessageFilter, public CIdleHandler
CMainDlg
现在派生自CAxDialogImpl
,这是启用对话框托管ActiveX控件的第一步。
最后,在WinMain()
中有一行新的代码:
int WINAPI _tWinMain(...) { //... _Module.Init(NULL, hInstance); AtlAxWinInit(); int nRet = Run(lpstrCmdLine, nCmdShow); _Module.Term(); return nRet; }
AtlAxWinInit()
注册了一个名为AtlAxWin
的窗口类。ATL在创建ActiveX控件的容器窗口时会使用它。
由于ATL 7的更改,你必须将LIBID传递给_Module.Init()
。论坛上的一些人建议在VC 7中使用以下代码:
_Module.Init(NULL, hInstance, &LIBID_ATLLib);
这个更改对我来说一直很好。
使用资源编辑器添加控件
ATL允许你使用资源编辑器将ActiveX控件添加到对话框中,就像在MFC应用程序中一样。首先,在对话框编辑器中右键单击并选择Insert ActiveX control。
VC将显示你系统中安装的控件列表。向下滚动到Microsoft Web Browser并点击OK将控件插入对话框。查看新控件的属性并将其ID设置为IDC_IE
。对话框现在应该如下图所示,控件在编辑器中可见:
如果现在编译并运行应用程序,你将在对话框中看到WebBrowser控件。但是,它将是空白的,因为我们还没有告诉它去哪里导航。
在下一节中,我将介绍创建和托管ActiveX控件所涉及的ATL类,然后我们将学习如何使用这些类与浏览器进行通信。
用于控件托管的ATL类
在对话框中托管ActiveX控件时,有两个类协同工作:CAxDialogImpl
和CAxWindow
。它们处理控件容器必须实现的所有接口,并提供一些实用函数来执行常见操作,例如查询控件是否支持特定COM接口。
CAxDialogImpl
第一个类是CAxDialogImpl
。当你编写对话框类时,你将其派生自CAxDialogImpl
而不是CDialogImpl
,以启用控件托管。CAxDialogImpl
重写了Create()
和DoModal()
,它们分别调用全局函数AtlAxCreateDialog()
和AtlAxDialogBox()
。由于IEHost对话框是通过Create()
创建的,所以我们将仔细研究AtlAxCreateDialog()
。
AtlAxCreateDialog()
加载对话框资源,并使用辅助类_DialogSplitHelper
遍历控件,查找资源编辑器创建的特殊条目,这些条目表明需要创建一个ActiveX控件。例如,这是IEHost.rc文件中WebBrowser控件的条目:
CONTROL "",IDC_IE,"{8856F961-340A-11D0-A96B-00C04FD705A2}", WS_TABSTOP,7,7,116,85
第一个参数是窗口文本(空字符串),第二个是控件ID,第三个是窗口类名。_DialogSplitHelper::SplitDialogTemplate()
发现窗口类名以'{'
开头,就知道这是一个ActiveX控件条目。它会在内存中创建一个新的对话框模板,将那些特殊的CONTROL
条目替换为创建AtlAxWin
窗口的条目。新条目是内存中的等价物:
CONTROL "{8856F961-340A-11D0-A96B-00C04FD705A2}", IDC_IE, "AtlAxWin", WS_TABSTOP, 7, 7, 116, 85
结果是会创建一个具有相同ID的AtlAxWin
窗口,其窗口文本将是ActiveX控件的GUID。因此,如果你调用GetDlgItem(IDC_IE)
,返回的值是AtlAxWin
窗口的HWND
,而不是ActiveX控件本身的HWND
。
SplitDialogTemplate()
返回后,AtlAxCreateDialog()
调用CreateDialogIndirectParam()
来使用修改后的模板创建对话框。
AtlAxWin 和 CAxWindow
如上所述,AtlAxWin
被用作ActiveX控件的容器窗口。有一个特殊的窗口接口类CAxWindow
与AtlAxWin
一起使用。当一个AtlAxWin
从对话框模板创建时,AtlAxWin
窗口过程AtlAxWindowProc()
处理WM_CREATE
消息,并响应该消息创建ActiveX控件。也可以在运行时创建ActiveX控件,而无需在对话框模板中,但这将在稍后介绍。
WM_CREATE
处理程序调用全局函数AtlAxCreateControl()
,并将AtlAxWin
的窗口文本传递给它。回想一下,这被设置为WebBrowser控件的GUID。AtlAxCreateControl()
会调用几个函数,最终代码会到达CreateNormalizedObject()
,它将窗口文本转换为GUID,最后调用CoCreateInstance()
来创建ActiveX控件。
由于ActiveX控件是AtlAxWin
的子窗口,对话框无法直接访问该控件。但是,CAxWindow
提供了与控件通信的方法。你最常使用的是QueryControl()
,它会对控件调用QueryInterface()
。例如,你可以使用QueryControl()
从WebBrowser控件获取IWebBrowser2
接口,并使用该接口将浏览器导航到一个URL。
调用控件的方法
现在我们的对话框中有了WebBrowser,我们可以使用它的COM接口与其进行交互。我们要做的第一件事是使用它的IWebBrowser2
接口让它导航到一个新的URL。在OnInitDialog()
处理程序中,我们可以将一个CAxWindow
变量附加到正在托管浏览器的AtlAxWin
上。
BOOL CMainDlg::OnInitDialog() { CAxWindow wndIE = GetDlgItem(IDC_IE);
接下来,我们声明一个IWebBrowser2
接口指针,并使用CAxWindow::QueryControl()
查询浏览器控件是否支持该接口。
CComPtr<IWebBrowser2> pWB2; HRESULT hr; hr = wndIE.QueryControl ( &pWB2 );
QueryControl()
调用WebBrowser的QueryInterface()
,如果成功,则返回IWebBrowser2
接口。然后我们可以调用Navigate()
。
if ( pWB2 ) { CComVariant v; // empty variant pWB2->Navigate ( CComBSTR("https://codeproject.org.cn/"), &v, &v, &v, &v ); }
接收控件触发的事件
从WebBrowser获取接口很简单,它允许我们进行单向通信——*到*控件。也有很多*来自*控件的通信,即事件。ATL提供了封装连接点和事件接收的类,这样我们就可以接收浏览器触发的事件。要使用此支持,我们需要做四件事:
- 将
IDispEventImpl
添加到CMainDlg
的继承列表中。 - 编写一个*事件接收器映射*,指示我们要处理哪些事件。
- 为这些事件编写处理程序。
- 将控件连接到接收器映射(一个称为*建议*的过程)。
VC IDE在此过程中提供了极大的帮助——它会为你对CMainDlg
进行更改,并查询ActiveX控件的类型库来显示控件触发的事件列表。由于VC 6和VC 7中添加处理程序的UI不同,我将分别介绍。
在VC 6中添加处理程序
有两种方法可以访问添加处理程序的UI:
- 在ClassView窗格中,右键单击
CMainDlg
并选择菜单上的Add Windows Message Handler。 - 查看
CMainDlg
代码或资源编辑器中的相关对话框时,单击WizardBar操作按钮上的下拉箭头,然后在菜单上选择Add Windows Message Handler。
选择该命令后,VC将显示一个包含控件列表的对话框,标签为Class or object to handle。在此列表中选择IDC_IE,VC将用WebBrowser控件触发的事件填充New Windows messages/events列表。
我们将为DownloadBegin事件添加一个处理程序,所以选择该事件并单击Add and Edit按钮。VC将提示你输入方法名。
第一次添加事件处理程序时,VC会修改CMainDlg
的一些地方,使其能够成为事件接收器。这些更改分散在头文件中;下面是一个摘要片段:
#import "C:\WINNT\System32\shdocvw.dll" class CMainDlg : public CAxDialogImpl<CMainDlg>, public CUpdateUI<CMainDlg>, public CMessageFilter, public CIdleHandler, public IDispEventImpl<IDC_IE, CMainDlg> { // ... public: BEGIN_SINK_MAP(CMainDlg) SINK_ENTRY(IDC_IE, 0x6a, OnDownloadBegin) END_SINK_MAP() void __stdcall OnDownloadBegin() { // TODO : Add Code for event handler. } };
#import
语句是一个编译器指令,用于读取shdocvw.dll(实现WebBrowser ActiveX控件的文件)中的类型库,并为使用该控件中的coclasses和接口创建包装类。你通常会将此指令移到stdafx.h,但在此情况下我们不需要它,因为Platform SDK已经包含了包含WebBrowser接口和方法的头文件。
继承列表现在包含IDispEventImpl
,它有两个模板参数。第一个是我们分配给ActiveX控件的ID,IDC_IE,第二个是派生自IDispEventImpl
的类的名称。
接收器映射由BEGIN_SINK_MAP
和END_SINK_MAP
宏分隔。每个SINK_ENTRY
宏列出了CMainDlg
将处理的一个事件。宏的参数是控件ID(再次是IDC_IE
),事件的dispatch ID,以及接收到事件时要调用的函数的名称。VC从ActiveX控件的类型库中读取dispatch ID,所以你不必担心找出这些数字。(如果你查看exdispid.h头文件,它列出了IE和Explorer发送的各种事件的ID,你会发现0x6A对应于常量DISPID_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()
中,解除建议发生在OnDestroy()
中。
在VC 6中建议
VC 6中的ATL有一个全局函数AtlAdviseSinkMap()
。该函数接受一个指向具有接收器映射的C++对象的指针(通常是this
),以及一个布尔值。如果布尔值为true
,则对象希望开始接收事件。如果为false
,则对象希望停止接收事件。AtlAdviseSinkMap()
建议对话框中的所有控件为C++对象开始或停止发送事件。
要使用此函数,请为WM_INITDIALOG
和WM_DESTROY
添加处理程序,然后像这样调用AtlAdviseSinkMap()
:
BOOL CMainDlg::OnInitDialog(...) { // Begin sinking events AtlAdviseSinkMap ( this, true ); } void CMainDlg::OnDestroy() { // Stop sinking events AtlAdviseSinkMap ( this, false ); }
AtlAdviseSinkMap()
返回一个HRESULT
,指示建议是否成功。如果在OnInitDialog()
中AtlAdviseSinkMap()
失败,那么你将不会收到来自某些(或所有)ActiveX控件的事件。
在VC 7中建议
在VC 7中,CAxDialogImpl
有一个名为AdviseSinkMap()
的方法,它是AtlAdviseSinkMap()
的包装器。AdviseSinkMap()
接受一个布尔参数,其含义与AtlAdviseSinkMap()
的第二个参数相同。AdviseSinkMap()
检查类是否具有接收器映射,然后调用AtlAdviseSinkMap()
。
与VC 6相比,最大的区别在于CAxDialogImpl
有WM_INITDIALOG
和WM_DESTROY
的处理程序,它们会为你调用AdviseSinkMap()
。要利用此功能,请在CMainDlg
消息映射的开头添加一个CHAIN_MSG_MAP
宏,如下所示:
BEGIN_MSG_MAP(CMainDlg)
CHAIN_MSG_MAP(CAxDialogImpl<CMainDlg>)
// rest of the message map...
END_MSG_MAP()
示例项目概述
现在我们已经了解了事件接收是如何工作的,让我们来看一下完整的IEHost项目。它托管了我们一直在讨论的WebBrowser控件,并处理了六个事件。它还显示了事件列表,因此你可以了解自定义浏览器如何使用它们来提供进度UI。该程序处理以下事件:
BeforeNavigate2
和NavigateComplete2
:这些事件允许应用程序在WebBrowser导航到URL时进行观察。你可以根据需要取消导航,以响应BeforeNavigate2
。DownloadBegin
和DownloadComplete
:应用程序使用这些事件来控制“等待”消息,该消息指示浏览器正在工作。一个更精美的应用程序可以使用动画,类似于IE本身使用的动画。CommandStateChange
:此事件告诉应用程序“后退”和“前进”导航命令何时可用。应用程序会相应地启用或禁用后退和前进按钮。StatusTextChange
:此事件在几种情况下触发,例如当鼠标光标悬停在超链接上时。此事件发送一个字符串,应用程序通过在浏览器窗口下方的静态控件中显示该字符串来响应。
该应用程序还有四个控制浏览器的按钮:后退、前进、停止和重新加载。这些按钮会调用相应的IWebBrowser2
方法。
事件及其附带的数据都会记录到一个列表控件中,因此你可以看到事件触发的情况。你也可以关闭任何事件的日志记录,这样你就可以只关注一两个事件。为了演示一些非平凡的事件处理,BeforeNavigate2
处理程序会检查URL,如果它包含“doubleclick.net”,则取消导航。作为IE插件而不是HTTP代理运行的广告和弹出窗口拦截器就是使用此方法。这是执行此检查的代码:
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; }
这是应用程序在查看Lounge时的样子:
IEHost演示了早期文章中涵盖的许多其他WTL功能:CBitmapButton
(用于浏览器控件按钮)、CListViewCtrl
(用于事件日志记录)、DDX(用于跟踪复选框状态)和CDialogResize
。
在运行时创建ActiveX控件
也可以在运行时创建ActiveX控件,而不是在资源编辑器中。关于对话框演示了这种技术。对话框资源包含一个占位符分组框,用于标记WebBrowser控件将放置的位置:
在OnInitDialog()
中,我们使用CAxWindow
在占位符的相同RECT
中创建一个新的AtlAxWin
(然后销毁占位符):
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()
。我们感兴趣的两个参数是第一个,它是WebBrowser控件GUID的字符串版本;第四个是指向IUnknown*
的指针。此指针将被填充为ActiveX控件的IUnknown
。创建控件后,我们像以前一样查询IWebBrowser2
接口,并将控件导航到一个URL。
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。例如,我们可以用以下调用创建WebBrowser控件:
// Use the control's ProgID, Shell.Explorer: wndIE.CreateControlEx ( L"Shell.Explorer", NULL, NULL, &punkCtrl );
CreateControl()
和CreateControlEx()
也有专门为WebBrowser控件设计的重载。如果你的应用程序包含HTML资源作为网页,你可以将它的资源ID作为第一个参数传递。ATL将创建一个WebBrowser控件并将其导航到该资源。IEHost包含一个ID为IDR_ABOUTPAGE
的页面,所以我们可以用这段代码在关于对话框中显示它:
wndIE.CreateControl ( IDR_ABOUTPAGE );
结果如下:
示例项目包含上述三种技术的代码。查看CAboutDlg::OnInitDialog()
并注释/取消注释代码以查看每种技术的运行情况。
键盘处理
最后一个但非常重要的一点是键盘消息。ActiveX控件的键盘处理相当复杂,因为主机和控件必须协同工作,以确保控件能看到它感兴趣的消息。例如,WebBrowser控件允许你使用TAB键在链接之间移动。MFC自己处理所有这些,所以你可能从未意识到要使键盘支持完美无缺需要多少工作。
不幸的是,AppWizard没有为基于对话框的应用程序生成键盘处理代码。但是,如果你创建一个使用表单视图作为视图窗口的SDI应用程序,你将在PreTranslateMessage()
中看到必要的代码。当从消息队列读取鼠标或键盘消息时,代码会获取具有焦点的控件,并通过特殊的ATL消息WM_FORWARDMSG
将其转发给控件。通常,当一个窗口收到WM_FORWARDMSG
时,它不会做任何事情,因为它不知道该消息。但是,当ActiveX控件具有焦点时,WM_FORWARDMSG
最终会被发送到托管该控件的AtlAxWin
。AtlAxWin
识别WM_FORWARDMSG
并采取必要步骤来查看控件是否想自己处理该消息。
如果具有焦点的窗口不识别WM_FORWARDMSG
,则PreTranslateMessage()
会调用IsDialogMessage()
,以便标准的对话框导航键(如TAB)能够正常工作。
示例项目中包含CMainDlg::PreTranslateMessage()
中的必要代码。由于PreTranslateMessage()
仅在无模式对话框中有效,所以如果需要正确的键盘处理,你的基于对话框的应用程序*必须*使用无模式对话框。
接下来
在下一篇文章中,我们将回到框架窗口,并介绍使用分割窗口的主题。
版权和许可
本文为版权材料,(c)2003-2006 Michael Dunn。我知道这不能阻止人们在网上到处复制它,但我还是得说。如果你有兴趣翻译本文,请给我发邮件告知。我不认为我会拒绝任何人翻译的许可,我只是想知道翻译情况,以便在这里发布链接。
本文附带的演示代码已发布到公共领域。我以这种方式发布是为了让代码造福所有人。(我不会将文章本身公开,因为文章仅在CodeProject上提供有助于我的可见性和CodeProject网站。)如果你在自己的应用程序中使用演示代码,发邮件告知我将受到赞赏(只是为了满足我对人们是否从我的代码中受益的好奇心),但不是必需的。在你的源代码中注明出处也受赞赏,但不是必需的。
修订历史
- 2003年5月20日:首次发布文章。
- 2006年1月5日:重写了关于接收器映射、事件处理程序和建议的部分。旧代码比必要时更复杂。
系列导航:« Part V (Advanced Dialog UI Classes) | » Part VII (Splitter Windows)