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






4.77/5 (21投票s)
2005年5月12日
13分钟阅读

1107998

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控件实例或全局地自定义行为。DLIMAGES
,DLVIDEOS
,等等。 - 禁用上下文菜单,或为每个激活的上下文菜单触发
OnContextMenu
事件。 - 禁用快捷键,或为每个激活的快捷键触发
OnAcceletorKeys
事件。 - 默认情况下,配置为使用
FileDownloadEx
和OnFileDLxxxx
事件接管用户下载。 - 可以使用
DownloadUrlAsync
方法和OnFileDLxxx
事件作为简单的下载管理器。 - 通过
SecurityManagerProcessUrlAction
事件进行按URL的精细安全调整。 - 通过
OnHTTPSecurityProblem
事件拦截和覆盖HTTP安全问题。 - 通过
OnAuthentication
事件拦截和覆盖基本身份验证请求。 - 通过
OnGetOptionKeyPath
和OnGetOverrideKeyPath
事件替换或增强注册表设置。 - 允许通过
GET
或POST
方法发布数据,并通过OnPostxxxx
事件进行通知。 - 通过
OnWBDragxxx
和OnWBDropx
事件处理单个或多个拖放。 - 除了几乎所有WebBrowser包装控件的属性、方法和事件之外,它还增加了一系列新的属性、方法和事件。
背景
该控件是用VC++ 6.0和ATL 3.0编写的。它编译时依赖性最小(无MFC、std::
、CString
等)。它被设计用于在一个ATL创建的窗口中托管多个WebBrowser控件。这与MSDN的建议相悖,MSDN建议每个托管控件使用一个ATL窗口。选择此方法的原因是为了将WebBrowser控件的管理负担从托管客户端应用程序转移到控件本身。通常,开发人员将控件的一个实例放在窗体/对话框上,然后如果需要,客户端应用程序会创建并维护该控件的数组。我的方法使开发人员能够将此控件的一个实例插入到窗体/对话框中,然后使用CvbWB::AddBrowser
和CvbWB::RemoveBrowser
方法来添加和删除WebBrowser控件。每个新创建的控件(通过IWB
类的实例)都会被分配一个唯一的ID(wbUID
)。这个唯一的ID使得客户端应用程序可以通过其属性、方法与该特定的WebBrowser控件实例进行通信,并找出哪个WebBrowser控件触发了事件。
尽管这个控件是为了VB使用的,但由于它是一个完全符合标准的ActiveX控件,它也可以从MFC中使用。
实现挑战
有许多文章涵盖了创建、托管和接收WebBrowser控件事件的基础知识。因此,与其深入研究CoCreateInstance
、IOleObject::SetClientSite
等,我决定解释一些主要的实现挑战,在这些挑战方面,您会发现很少甚至没有相关信息。
这是在开发此控件期间遇到并解决的主要挑战列表。我既不声称这些解决方案是独一无二的,也不是最好的。只是它们似乎有效。
事件
- 我遇到的第一个问题是缺乏关于如何将
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;
- 我遇到的第二个问题仍然与事件有关。当我实现协议处理程序时,出现了这个问题。显然,
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); } } }
- 第三个问题是在实现协议处理程序后出现的。我需要从
WBPassthruSink
(协议处理程序Sink)类触发事件,以便为特定的WebBrowser控件通知客户端应用程序,通过ProtocolHandlerOnBeginTransaction
和ProtocolHandlerOnResponse
事件。由于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
之间的通道,通过使用异步可插入协议来拦截所有请求和响应。起初,这项任务似乎相当直接,实现了IInternetProtocol
、IInternetProtocolInfo
、IInternetPriority
、IInternetProtocolSink
、IInternetBindInfo
和IClassFactory
接口。在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 |
协议处理程序头文件。已实现的接口
|
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 |
|
这个类是使用向导创建的完整ATL控件。它负责在客户端应用程序(VB、C++)中托管控件,触发事件,并允许客户端访问所有WebBrowser控件的属性和方法。这项任务是通过使用一个简单的 |
IWB |
IUnknown |
此类负责创建和维护WebBrowser控件的单个实例以及所有必要的类,如WBClientSite (IOleClientSite 实现)。此外,所有来自所有类的QI都会通过创建它们的同一个IWB 实例进行路由。此外,它包含许多有用的方法,如IsFrameset 、FramesCount 、DocHighlightFindText 等。 |
WBClientSite |
IOleClientSite |
作为WebBrowser控件托管接口的一部分必需。所有方法都返回E_NOTIMPL 。 |
WBInPlaceSite |
IOleInplaceSite |
作为WebBrowser控件托管接口的一部分必需。所有方法都返回E_NOTIMPL 。 |
WBEventDispatch |
IDispatch |
此类是WebBrowser控件事件的Sink。当事件从WebBrowser控件到达时,它会触发事件通知客户端应用程序。 |
WBDocHostShowUI |
IDocHostShowUI |
我只处理 您当前的安全性设置禁止在此页面上运行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 |
一个简单的类,它使用WinInet 的InternetCrackUrl 方法将给定的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个)列表,以及一个更改日志。
相关文档
- WebBrowser自定义
- C/C++开发人员的WebBrowser控件参考
- 高级托管参考
- 如何在可插入协议处理程序中获取协议头
- 示例:ATLCPImplMT封装了COM组件之间的ATL事件触发
- 如何作为WebBrowser控件宿主处理脚本错误
- 关于异步可插入协议
- URL Monikers接口
- HTML控件API接口
- 实现自定义下载管理器
- MSHTML参考
- IInternetSecurityManager
- 使用ATL和C运行时代码进行编程
历史
有关更改列表,请参阅vbMHWB.htm。
- 2006年3月15日 - 当前版本(1.2.1.3)。
- 2005年5月13日 - 首次发布版本(1.0.0.1)。