65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (85投票s)

2003年5月20日

17分钟阅读

viewsIcon

700927

downloadIcon

6528

关于在WTL对话框中托管ActiveX控件的教程。

目录

引言

在第六部分,我将介绍ATL对在对话框中托管ActiveX控件的支持。由于ActiveX控件是ATL的专长,所以不需要额外的WTL类。然而,ATL的托管方式与MFC的方式有很大的不同,因此这是一个需要介绍的重要主题。我将介绍如何托管控件和接收事件,并开发一个与使用ClassWizard编写的MFC应用程序功能上没有区别的应用程序。当然,你也可以在你编写的WTL应用程序中使用ATL的托管支持。

本文的示例项目演示了如何托管IE WebBrowser控件。我选择浏览器控件有两个很好的理由:

  1. 每个人都在他们的电脑上安装了它,并且
  2. 它有很多方法和事件,因此它是一个很好的演示控件。

我当然无法与那些花费大量时间围绕WebBrowser控件编写自定义浏览器的开发者竞争。然而,读完本文后,你将有足够多的知识来开始开发你自己的自定义浏览器。

从AppWizard开始

创建项目

WTL AppWizard可以为我们创建一个已准备好托管ActiveX控件的应用程序。我们将从一个新项目开始,这个项目将被命名为IEHost。我们将使用一个无模式对话框,就像上一篇文章一样,只是这次要勾选Enable ActiveX Control Hosting复选框,如下图所示:

 [AppWizard - 22K]

 [AppWizard - 25K]

勾选该框后,我们的主对话框将派生自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.hatlhost.h是最重要的。它们包含了与COM相关的类(如智能指针CComPtr)以及用于托管控件的窗口类的定义。

接下来,查看maindlg.hCMainDlg的声明:

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

 [Insert menu - 8K]

VC将显示你系统中安装的控件列表。向下滚动到Microsoft Web Browser并点击OK将控件插入对话框。查看新控件的属性并将其ID设置为IDC_IE。对话框现在应该如下图所示,控件在编辑器中可见:

 [ IE control in editor - 6K]

如果现在编译并运行应用程序,你将在对话框中看到WebBrowser控件。但是,它将是空白的,因为我们还没有告诉它去哪里导航。

在下一节中,我将介绍创建和托管ActiveX控件所涉及的ATL类,然后我们将学习如何使用这些类与浏览器进行通信。

用于控件托管的ATL类

在对话框中托管ActiveX控件时,有两个类协同工作:CAxDialogImplCAxWindow。它们处理控件容器必须实现的所有接口,并提供一些实用函数来执行常见操作,例如查询控件是否支持特定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控件的容器窗口。有一个特殊的窗口接口类CAxWindowAtlAxWin一起使用。当一个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提供了封装连接点和事件接收的类,这样我们就可以接收浏览器触发的事件。要使用此支持,我们需要做四件事:

  1. IDispEventImpl添加到CMainDlg的继承列表中。
  2. 编写一个*事件接收器映射*,指示我们要处理哪些事件。
  3. 为这些事件编写处理程序。
  4. 将控件连接到接收器映射(一个称为*建议*的过程)。

VC IDE在此过程中提供了极大的帮助——它会为你对CMainDlg进行更改,并查询ActiveX控件的类型库来显示控件触发的事件列表。由于VC 6和VC 7中添加处理程序的UI不同,我将分别介绍。

在VC 6中添加处理程序

有两种方法可以访问添加处理程序的UI:

  1. 在ClassView窗格中,右键单击CMainDlg并选择菜单上的Add Windows Message Handler
  2. 查看CMainDlg代码或资源编辑器中的相关对话框时,单击WizardBar操作按钮上的下拉箭头,然后在菜单上选择Add Windows Message Handler

选择该命令后,VC将显示一个包含控件列表的对话框,标签为Class or object to handle。在此列表中选择IDC_IE,VC将用WebBrowser控件触发的事件填充New Windows messages/events列表。

 [Event list in VC6 - 21K]

我们将为DownloadBegin事件添加一个处理程序,所以选择该事件并单击Add and Edit按钮。VC将提示你输入方法名。

 [Setting method name - 8K]

第一次添加事件处理程序时,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_MAPEND_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。这将打开一个对话框,你可以在其中选择事件名称并设置处理程序的名称。

 [Event list in VC6 - 24K]

单击Add and Edit按钮将添加处理程序,对CMainDlg进行必要的更改,并打开maindlg.cpp文件,突出显示新处理程序。

另一种方法是查看CMainDlg的属性页,展开Controls节点,然后展开IDC_IE节点。在IDC_IE下,你会找到该控件触发的事件。

 [Adding event handler thru properties list - 12K]

你可以单击事件名称旁边的箭头,然后选择<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_INITDIALOGWM_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相比,最大的区别在于CAxDialogImplWM_INITDIALOGWM_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。该程序处理以下事件:

  • BeforeNavigate2NavigateComplete2:这些事件允许应用程序在WebBrowser导航到URL时进行观察。你可以根据需要取消导航,以响应BeforeNavigate2
  • DownloadBeginDownloadComplete:应用程序使用这些事件来控制“等待”消息,该消息指示浏览器正在工作。一个更精美的应用程序可以使用动画,类似于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时的样子:

 [Sample app - 33K]

IEHost演示了早期文章中涵盖的许多其他WTL功能:CBitmapButton(用于浏览器控件按钮)、CListViewCtrl(用于事件日志记录)、DDX(用于跟踪复选框状态)和CDialogResize

在运行时创建ActiveX控件

也可以在运行时创建ActiveX控件,而不是在资源编辑器中。关于对话框演示了这种技术。对话框资源包含一个占位符分组框,用于标记WebBrowser控件将放置的位置:

 [About box in editor - 5K]

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 );

结果如下:

 [About box browser ctrl - 12K]

示例项目包含上述三种技术的代码。查看CAboutDlg::OnInitDialog()并注释/取消注释代码以查看每种技术的运行情况。

键盘处理

最后一个但非常重要的一点是键盘消息。ActiveX控件的键盘处理相当复杂,因为主机和控件必须协同工作,以确保控件能看到它感兴趣的消息。例如,WebBrowser控件允许你使用TAB键在链接之间移动。MFC自己处理所有这些,所以你可能从未意识到要使键盘支持完美无缺需要多少工作。

不幸的是,AppWizard没有为基于对话框的应用程序生成键盘处理代码。但是,如果你创建一个使用表单视图作为视图窗口的SDI应用程序,你将在PreTranslateMessage()中看到必要的代码。当从消息队列读取鼠标或键盘消息时,代码会获取具有焦点的控件,并通过特殊的ATL消息WM_FORWARDMSG将其转发给控件。通常,当一个窗口收到WM_FORWARDMSG时,它不会做任何事情,因为它不知道该消息。但是,当ActiveX控件具有焦点时,WM_FORWARDMSG最终会被发送到托管该控件的AtlAxWinAtlAxWin识别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)

© . All rights reserved.