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

用于 VB 的托管和自定义多个 WebBrowser 控件实例的 ATL 控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.77/5 (21投票s)

2005年5月12日

13分钟阅读

viewsIcon

1107998

downloadIcon

11102

关于WebBrowser托管和自定义的文章。

目录

引言

本项目旨在用一个WebBrowser包装控件替换现有的控件,该控件允许开发人员创建和使用一个WebBrowser控件,并能完全控制GUI、上下文菜单、快捷键、下载、安全等等,而无需使用任何子类化、注册表或Hack。其结果是一个ATL控件

  • 它与任何WebBrowser包装控件一样易于使用。注册vbMHWB.dll,然后像其他ActiveX控件一样使用它。
  • 允许查看所有请求头(HTML、图像、CSS等),并可以选择添加额外的头(HTTP + HTTPS)。
  • 允许查看所有响应头(HTTP + HTTPS)。
  • 允许使用DOC_HOST_UI_FLAGS按WebBrowser控件实例或全局地自定义GUI。NO3DBORDER,等等。
  • 允许使用DOC_DOWNLOAD_CONTROL_FLAGS按WebBrowser控件实例或全局地自定义行为。DLIMAGESDLVIDEOS,等等。
  • 禁用上下文菜单,或为每个激活的上下文菜单触发OnContextMenu事件。
  • 禁用快捷键,或为每个激活的快捷键触发OnAcceletorKeys事件。
  • 默认情况下,配置为使用FileDownloadExOnFileDLxxxx事件接管用户下载。
  • 可以使用DownloadUrlAsync方法和OnFileDLxxx事件作为简单的下载管理器。
  • 通过SecurityManagerProcessUrlAction事件进行按URL的精细安全调整。
  • 通过OnHTTPSecurityProblem事件拦截和覆盖HTTP安全问题。
  • 通过OnAuthentication事件拦截和覆盖基本身份验证请求。
  • 通过OnGetOptionKeyPathOnGetOverrideKeyPath事件替换或增强注册表设置。
  • 允许通过GETPOST方法发布数据,并通过OnPostxxxx事件进行通知。
  • 通过OnWBDragxxxOnWBDropx事件处理单个或多个拖放。
  • 除了几乎所有WebBrowser包装控件的属性、方法和事件之外,它还增加了一系列新的属性、方法和事件。

背景

该控件是用VC++ 6.0和ATL 3.0编写的。它编译时依赖性最小(无MFC、std::CString等)。它被设计用于在一个ATL创建的窗口中托管多个WebBrowser控件。这与MSDN的建议相悖,MSDN建议每个托管控件使用一个ATL窗口。选择此方法的原因是为了将WebBrowser控件的管理负担从托管客户端应用程序转移到控件本身。通常,开发人员将控件的一个实例放在窗体/对话框上,然后如果需要,客户端应用程序会创建并维护该控件的数组。我的方法使开发人员能够将此控件的一个实例插入到窗体/对话框中,然后使用CvbWB::AddBrowserCvbWB::RemoveBrowser方法来添加和删除WebBrowser控件。每个新创建的控件(通过IWB类的实例)都会被分配一个唯一的ID(wbUID)。这个唯一的ID使得客户端应用程序可以通过其属性、方法与该特定的WebBrowser控件实例进行通信,并找出哪个WebBrowser控件触发了事件。

尽管这个控件是为了VB使用的,但由于它是一个完全符合标准的ActiveX控件,它也可以从MFC中使用。

目录

实现挑战

有许多文章涵盖了创建、托管和接收WebBrowser控件事件的基础知识。因此,与其深入研究CoCreateInstanceIOleObject::SetClientSite等,我决定解释一些主要的实现挑战,在这些挑战方面,您会发现很少甚至没有相关信息。

这是在开发此控件期间遇到并解决的主要挑战列表。我既不声称这些解决方案是独一无二的,也不是最好的。只是它们似乎有效。

事件

  1. 我遇到的第一个问题是缺乏关于如何将byref参数或对象传递给VB等客户端应用程序的文档。尝试处理这些向导生成的事件会导致GPF。这是从CProxy_IvbWBEvents类中摘取的NewWindow3事件的一个非工作代码示例。
    VOID Fire_NewWindow3(SHORT wbUID, IDispatch * * ppDisp,
                            VARIANT_BOOL * Cancel, LONG lFlags, 
                            BSTR sURLContext, BSTR sURL)
    {
        T* pT = static_cast<T*>(this);
        int nConnectionIndex;
        CComVariant* pvars = new CComVariant[6];
        int nConnections = m_vec.GetSize();
    
        for (nConnectionIndex = 0; nConnectionIndex < nConnections;
                                                 nConnectionIndex++)
        {
            pT->Lock();
            CComPtr<IUnknown> sp = m_vec.GetAt(nConnectionIndex);
            pT->Unlock();
    
            IDispatch* pDispatch = reinterpret_cast<IDispatch*>(sp.p);
            if (pDispatch != NULL)
            {
                pvars[5] = wbUID;
    
                /////////////////////////////////////////////////////
                //Next two params need to be passed by ref or they 
                //cause GPF
                //
                pvars[4] = ppDisp;
                pvars[3] = Cancel;
                /////////////////////////////////////////////////////
    
                pvars[2] = lFlags;
                pvars[1] = sURLContext;
                pvars[0] = sURL;
    
                DISPPARAMS disp = { pvars, NULL, 6, 0 };
                pDispatch->Invoke(0x2d, IID_NULL, LOCALE_USER_DEFAULT,
                             DISPATCH_METHOD, &disp, NULL, NULL, NULL);
            }
        }
    }

    这是更正后的代码。

                pvars[4].vt = VT_BYREF|VT_DISPATCH;
                pvars[4].byref = ppDisp;
    
                pvars[3].vt = VT_BOOL|VT_BYREF;
                pvars[3].byref = Cancel;
  2. 我遇到的第二个问题仍然与事件有关。当我实现协议处理程序时,出现了这个问题。显然,IConnectionPointImpl不会在COM组件之间触发事件。因此,在研究了一段时间后,我遇到了KB文章280512,ATLCPImplMT封装了COM组件之间的ATL事件触发。使用包含的类IConnectionPointImplMT(来自MS)解决了这个问题。这是Newwindow3事件的完整代码。
    VOID Fire_NewWindow3(SHORT wbUID, IDispatch * * ppDisp,
                          VARIANT_BOOL * Cancel, LONG lFlags, 
                          BSTR sURLContext, BSTR sURL)
    {
        T* pT = static_cast<T*>(this);
        int nConnectionIndex;
        CComVariant* pvars = new CComVariant[6];
        int nConnections = m_vec.GetSize();
    
        for (nConnectionIndex = 0; nConnectionIndex < nConnections;
                                                  nConnectionIndex++)
        {
            /////////////////////////////////////////////////////////
            //Next three lines need to be replaced
            //pT->Lock();
            //CComPtr<IUnknown> sp = m_vec.GetAt(nConnectionIndex);
            //pT->Unlock();
            /////////////////////
    
            /////////////////////////////////////////////////////////
            //Replaced the previous three lines
            //of code with the next two lines
            CComPtr<IUnknown> sp;
            sp.Attach (GetInterfaceAt(nConnectionIndex));
            /////////////////////
    
            IDispatch* pDispatch = reinterpret_cast<IDispatch*>(sp.p);
            if (pDispatch != NULL)
            {
                pvars[5] = wbUID;
    
                pvars[4].vt = VT_BYREF|VT_DISPATCH;
                pvars[4].byref = ppDisp;
    
                pvars[3].vt = VT_BOOL|VT_BYREF;
                pvars[3].byref = Cancel;
    
                pvars[2] = lFlags;
                pvars[1] = sURLContext;
                pvars[0] = sURL;
    
                DISPPARAMS disp = { pvars, NULL, 6, 0 };
                pDispatch->Invoke(0x2d, IID_NULL, LOCALE_USER_DEFAULT,
                             DISPATCH_METHOD, &disp, NULL, NULL, NULL);
            }
        }
    }
  3. 第三个问题是在实现协议处理程序后出现的。我需要从WBPassthruSink(协议处理程序Sink)类触发事件,以便为特定的WebBrowser控件通知客户端应用程序,通过ProtocolHandlerOnBeginTransactionProtocolHandlerOnResponse事件。由于WBPassthruSink类的实例是由URLMon根据PassthroughAPP的需要创建的,因此我必须找到一种方法来确定涉及的是哪个WebBrowser控件实例,以便为该特定控件触发事件。我的解决方案是在WBPassthruSink::OnStart的实现中使用从我们的协议处理程序获得的IWindowForBindingUI接口来查找Internet Explorer服务器HWND
        //Using IWindowForBindingUI interface
        CComPtr<IWindowForBindingUI> objWindowForBindingUI;
        //This is a macro for QueryService
        HRESULT hret = QueryServiceFromClient(&objWindowForBindingUI);
        if( (SUCCEEDED(hret)) && (objWindowForBindingUI) )
        {
            HWND hwndIEServer = NULL;
            //Should return InternetExplorerServer HWND
            objWindowForBindingUI->GetWindow(IID_IHttpSecurity,
                                                &hwndIEServer);
            //From here we can find the ATL window
            //hosting this instance of our control
            //and have it fire an event for the form/dlg hosting
            //this instance of our control
            if(hwndIEServer)

目录

_ATL_MIN_CRT

这个控件的一个设计目标是最小化依赖性。标准的_ATL_MIN_CRT支持可以很好地消除CRT开销。但不幸的是,它不支持使用全局C++构造,例如以下代码:

class CTest {
public:
  CTest() {
    MessageBox(NULL, _T("Hello, I'm intitialized"),
             _T("Static object"), MB_SETFOREGROUND | MB_OK);
  }
  ~CTest() {
    MessageBox(NULL, _T("Bye, I'm done"), _T("Static object"),
                                      MB_SETFOREGROUND | MB_OK);
  }
};

static CTest g_test;
extern CTest *gp_Test;

上面的代码会导致链接冲突,因为CRT代码中的构造函数/析构函数调用会被引用。为了克服这个问题,我使用了AuxCrt.cpp自定义_ATL_MIN_CRT实现以及AtlImpl.cpp类的替换。这个类来自Andrew Nosenko(andien@geocities.com)。有了这个类,我就可以使用CSimpleArray作为全局变量来跟踪WebBrowser控件的实例。注意,为了方便起见,我将AuxCrt.cpp的一个副本放在了ATL\Include\目录中。

//Taken from StdAfx.h

//gCtrlInstances keeps track of each instance of our control
//This global is needed due to the fact that a client may place
//this control on more than one form/dlg
//or have multiple instances of BW
//hosting in one control. In this case, using one
//global ptr to our control will cause the events to be routed to the
//first control, not the one we want. The control instances (this) is
//added to this array in Constructor and
//removed in Destructor of CvbWB class.

extern CSimpleArray<void*> gCtrlInstances;

//Flag to track registering and unregistering
//of HTTP/HTTPS protocols
//Can only be done once per DLL load.
//Effects all instances of Webbrowser control.
extern BOOL gb_IsHttpRegistered;
extern BOOL gb_IsHttpsRegistered;
//Protocol handling registration
extern CComPtr<IClassFactory> m_spCFHTTP;
extern CComPtr<IClassFactory> m_spCFHTTPS;
....

目录

异步可插入协议

我的一个主要设计目标是能够充当WebBrowser控件和URLMon之间的通道,通过使用异步可插入协议来拦截所有请求和响应。起初,这项任务似乎相当直接,实现了IInternetProtocolIInternetProtocolInfoIInternetPriorityIInternetProtocolSinkIInternetBindInfoIClassFactory接口。在IServiceProvider::QueryService的实现中,创建一个IInternetProtocolImpl的实例并将其传递给URLMon。覆盖必要的方法并处理请求。不幸的是,这种方法有一个很大的缺陷。我的IInternetProtocol实现只被调用来处理主文档,而不是页面中的其他请求,如图像、CSS等。

经过一番搜索,我发现了一个由Igor Tandetnik开发的优秀软件包,名为PassthroughAPP。在Google群组中,在microsoft.public.inetsdk.programming.xxx下,只需搜索PassthroughAPP。该软件包包含五个文件。

PassthroughObject.h 一个简单的COM对象IPassthroughObject
ProtocolCF.h 一个自定义的COM类工厂,实现了CComClassFactory
ProtocolCF.inl 类工厂实现。
ProtocolImpl.h

协议处理程序头文件。已实现的接口

  • IPassthroughObject
  • IInternetProtocol
  • IInternetProtocolInfo
  • IInternetPriority
  • IInternetThreadSwitch
  • IWinInetHttpInfo
  • IInternetProtocolSink
  • IServiceProvider
  • IInternetBindInfo
ProtocolImpl.inl 协议处理程序实现。

除了我在WBPassthruSink类中重写的五个方法来拦截所有请求和响应之外,我将无法回答有关PassthroughAPP的任何问题。请将您的问题直接发送给作者,因为我对该软件包的设计和实现的理解有限。

目录

RegisterBindStatusCallback 和 content-disposition header

这个控件的一个设计目标是完全控制文件下载。起初这很容易实现,而且似乎一切正常,没有出现任何问题。但正如预期的那样,报告了一个奇怪的问题。如果用户试图从Hotmail、Yahoo!等下载附件,默认的下载对话框就会出现,绕过了我的自定义下载管理器。我将问题追溯到RegisterBindStatusCallback方法,该方法在IDownloadManager::Download方法中被调用,它返回了E_FAIL。当服务器在文件下载请求的响应中发送content-disposition头时,似乎会发生此失败。当然,MSDN甚至没有提到E_FAIL返回值,或者RegisterBindStatusCallback为什么会失败。在搜索了一段时间但徒劳无功之后,我决定尝试实现一个变通方法。

第一步是设法强制RegisterBindStatusCallback成功。

  • 调用RegisterBindStatusCallback,传递一个IBindStatusCallback指针来检索前一个IBindStatusCallback
  • 如果返回值是E_FAIL,则调用RevokeObjectParam来取消注册前一个IBindStatusCallback
  • 如果RevokeObjectParam的返回值表示成功,则尝试第二次调用RegisterBindStatusCallback,这次应该会成功。
//Taken from WBDownLoadManager::Download
//filedl is an instance of my IBindStatusCallback implementation
//pbc is a BindCtx pointer passed to Download method

IBindStatusCallback *pPrevBSCB = NULL;
hr = RegisterBindStatusCallback(pbc,
     reinterpret_cast<IBindStatusCallback*>(filedl), &pPrevBSCB, 0L);

if( (FAILED(hr)) && (pPrevBSCB) )
{
    //RevokeObjectParam for current BSCB, so we can register our BSCB
    //_BSCB_Holder_ is the key used to register
    //a callback with a specific BindCtX
    LPOLESTR oParam = L"_BSCB_Holder_";
    hr = pbc->RevokeObjectParam(oParam);
    if(SUCCEEDED(hr))
    {
        //Attempt register again, should succeed now
        hr = RegisterBindStatusCallback(pbc,
                reinterpret_cast<IBindStatusCallback*>(filedl), 0, 0L);
        if(SUCCEEDED(hr))
        {
            //Need to pass a pointer for BindCtx
            //and previous BSCB to our implementation
            filedl->m_pPrevBSCB = pPrevBSCB;
            filedl->AddRef();
            pPrevBSCB->AddRef();
            filedl->m_pBindCtx = pbc;
            pbc->AddRef();
        }
//....

第二步是从我们的实现中转接一些调用到前一个IBindStatusCallback。否则,将不会进行下载。可能会出现内存泄漏和崩溃。这部分是基于试错法。

  • ::OnStartBinding
    if(m_pPrevBSCB)
    {
        m_pPrevBSCB->OnStopBinding(HTTP_STATUS_OK, NULL);
    }
  • ::OnProgress
    if(m_pPrevBSCB)
    {
        //Need to do this otherwise a 
        //filedownload dlg will be displayed
        //as we are downloading the file.
        if(ulStatusCode == BINDSTATUS_CONTENTDISPOSITIONATTACH)
            return S_OK;
        m_pPrevBSCB->OnProgress(ulProgress, ulProgressMax,
                                   ulStatusCode, szStatusText);
    }
  • ::OnStopBinding
    if( (m_pPrevBSCB) && (m_pBindCtx) )
    {
        //Register PrevBSCB and release our pointers
        LPOLESTR oParam = L"_BSCB_Holder_";
        m_pBindCtx->RegisterObjectParam(oParam,
                    reinterpret_cast(m_pPrevBSCB));
        m_pPrevBSCB->Release();
        m_pPrevBSCB = NULL;
        m_pBindCtx->Release();
        m_pBindCtx = NULL;
        //Decrease our ref count, so when release is called
        //we delete this object
        --m_cRef;
    }

目录

类简要概述

请确保您拥有与VC++ 6.0兼容的最新SDK,2003年2月SDK,以及IE6头文件和库

类名 实现 描述
CvbWB
  • CComObjectRootEx
  • IDispatchImpl
  • CComControl
  • IPersistStreamInitImpl
  • IOleControlImpl
  • IOleObjectImpl
  • IOleInPlaceActiveObjectImpl*
  • IViewObjectExImpl
  • IOleInPlaceObjectWindowlessImpl*
  • ISupportErrorInfo
  • IConnectionPointContainerImpl*
  • IPersistStorageImpl
  • ISpecifyPropertyPagesImpl*
  • IQuickActivateImpl
  • IDataObjectImpl
  • IProvideClassInfo2Impl
  • IPropertyNotifySinkCP
  • CComCoClass
  • CProxy_IvbWBEvents

这个类是使用向导创建的完整ATL控件。它负责在客户端应用程序(VB、C++)中托管控件,触发事件,并允许客户端访问所有WebBrowser控件的属性和方法。这项任务是通过使用一个简单的IWB指针数组来实现的。指针通过调用AddBrowserRemoveBrowser方法添加到数组中并从中移除。每个新的IWB实例都被赋予一个唯一的IDwbUID。客户端应用程序使用此ID来访问WebBrowser控件实例的属性和方法。此外,所有事件都带有一个额外的参数wbUID,用于标识触发事件的WebBrowser实例。此外,此类包含许多有用的方法和属性,如get_ActiveDocumentObjget_ActiveElementObj(返回活动文档或元素;考虑框架)、DownloadUrlAsync(启动文件下载,允许客户端应用程序通过OnFileDL_xxx事件监视状态)、ucInternetCrackUrl(将给定URL分解为各个部分,包括文件名和扩展名,并通过Get/Set ucXXX属性访问这些部分),...

IWB IUnknown 此类负责创建和维护WebBrowser控件的单个实例以及所有必要的类,如WBClientSiteIOleClientSite实现)。此外,所有来自所有类的QI都会通过创建它们的同一个IWB实例进行路由。此外,它包含许多有用的方法,如IsFramesetFramesCountDocHighlightFindText等。
WBClientSite IOleClientSite 作为WebBrowser控件托管接口的一部分必需。所有方法都返回E_NOTIMPL
WBInPlaceSite IOleInplaceSite 作为WebBrowser控件托管接口的一部分必需。所有方法都返回E_NOTIMPL
WBEventDispatch IDispatch 此类是WebBrowser控件事件的Sink。当事件从WebBrowser控件到达时,它会触发事件通知客户端应用程序。
WBDocHostShowUI IDocHostShowUI

我只处理::ShowMessage方法,该方法负责拦截HTTP消息并通知客户端通过Fire_ShowMessage事件来决定如何处理该消息。典型的消息可能如下所示:

您当前的安全性设置禁止在此页面上运行ActiveX控件。因此,页面可能无法正确显示。.

WBOleCommandTarget IOleCommandTarget 此类旨在通过::Exec方法拦截脚本错误,并根据m_lScriptError标志允许或禁止它们。
WBAuthenticate IAuthenticate 此类拦截服务器的基本身份验证请求,并使用OnAuthentication事件通知客户端以获取用户名和密码。对于希望自动执行使用基本身份验证方案的登录过程的客户端很有用。
WBDocHostUIHandler IDocHostUIHandler 此类旨在拦截上下文菜单(::ShowContextMenu)、快捷键(::TranslateAccelerator)并设置UI标志(::GetHostInfo)。
WBHttpSecurity IHttpSecurity 此类通过OnSecurityProblem方法拦截HTTP相关的安全问题,如ERROR_HTTP_REDIRECT_NEEDS_CONFIRMATION*、ERROR_INTERNET_SEC_CERT_CN_INVALID*。请注意,错误使用::OnSecurityProblem方法或OnHTTPSecurityProblem事件可能会危及应用程序的安全性,并可能使您的应用程序用户暴露于不必要的信息泄露。
WBSecurityManager IInternetSecurityManager 此类仅实现::ProcessUrlAction方法。对于其他方法,它返回INET_DEFAULT_ACTION请注意,错误使用::ProcessUrlAction方法或SecurityManagerProcessUrlAction*事件可能导致URL操作处理不正确,并可能使用户容易受到特权提升攻击。
WBServiceProvider IServiceProvider 它负责响应我们IUknown(IWB)::QueryService方法中的QueryService调用。
WBWindowForBindingUI IWindowForBindingUI 它通过::GetWindow方法返回一个窗口句柄,MSHTML在必要时使用该句柄在客户端用户界面中显示信息。目前,此方法返回Internet Explorer服务器窗口的句柄。
WBBSCBFileDL IBindStatusCallback
IHttpNegotiate
此类的实例被创建并用于接收回调,并通过OnFileDLxxxx事件通知客户端应用程序所有文件下载的进度,无论是用户单击下载链接还是通过代码使用::DownloadUrlAsync方法。
WBDownLoadManager IDownloadManager 它实现了::Download方法,该方法进而创建一个我们的IBindStatusCallback实现(WBBSCBFileDL)的实例,注册我们的BSCB以进行回调,并通过OnFileDLxxxx事件通知客户端下载进度。每个BSCB都获得一个唯一的ID,并且其指针存储在CvbWB类实例的一个简单数组中。客户端应用程序可以使用此ID通过调用CancelFileDl并传入ID来取消下载。
CTmpBuffer   一个简单的字符串缓冲区类,因为CString不可用,原因是为了最小化依赖性。
CUrlParts   一个简单的类,它使用WinInetInternetCrackUrl方法将给定的URL分解成各个部分。这包括文件名和扩展名(如果可用)。
WBPassThruSink CInternetProtocolSinkWithSP
IHttpNegotiate
此类是协议处理程序的Sink。
WBDropTarget IDropTarget 用于处理自定义拖放。
WBDocHostUIHandler IDocHostUIHandler2 用于处理GetOverrideKeyPath方法。
WBStream IStream 用于带进度的上传处理。

*忽略为避免页面滚动而添加的空格

目录

设置控件

  • Binaries子文件夹中的vbMHWB.dll复制到您的系统目录。
  • 使用regsvr32.exe注册vbMHWB.dll
  • 打开VBDemo或MFCDemo项目。

如何注册,示例

假设系统目录路径为'C:\windows\system32\'

regsvr32.exe C:\windows\system32\vbMHWB.dll.

关于演示应用程序

两个演示项目在GUI和控件的使用方面几乎相同。VBDemo使用Visual Basic 6.0构建,除了此控件外没有其他依赖项。MFCDemo项目使用Visual C++ 6.0构建,同样没有其他依赖项。对于MFCDemo,您需要将BOOL视为VARIANT_BOOL(向导会将VARIANT_BOOL翻译为BOOL),并确保在为入/出BSTR参数赋新值之前释放其值(提供了ClearBSTRPtr方法作为示例)。此步骤对于避免内存泄漏是必需的。

两个项目的构建版本都已包含在Binaries子文件夹中。

目录

控件信息

vbMHWB.htm文件中包含了一个新的或修改的属性、方法(90个)和事件(40个)列表,以及一个更改日志。

相关文档

历史

有关更改列表,请参阅vbMHWB.htm

  • 2006年3月15日 - 当前版本(1.2.1.3)。
  • 2005年5月13日 - 首次发布版本(1.0.0.1)。

目录

© . All rights reserved.