解决 Windows Webbrowser 控件焦点窃取 Bug
此长期存在问题的完整解决方案,已在 Windows 7 - 10 上测试
引言
如果您是一名 MFC 程序员,您几乎肯定会希望在您的应用程序中使用 Webbrowser ActiveX 控件来显示 HTML。它封装了 Internet Explorer 的所有功能,并且由于它已在所有 Windows 安装(甚至包括 Windows 10)中预装,因此无需安装任何其他库或浏览器。
然而,在多文档界面 (MDI) 应用程序中使用时,它有一个致命的缺陷——它会不断窃取其他窗口的焦点。不幸的是,这确实使得它在许多情况下几乎无法使用——其他任何东西都无法正常工作。如果我们能阻止这种情况发生,我们就拥有了一个有用且可扩展的控件——如果不能,那将是一个艰难的选择,只能接受令人不快的替代方案。
背景
作为我的日常工作,我维护一个庞大的 C++ MFC .NET 之前的程序,拥有大量用户,已经十多年了。虽然这都是“遗留”代码,但它运行良好,并且对其进行大量重写在商业上是不可行的。我认为肯定有很多人处于类似的情况:如果这个解决方案在网上随处可见,它会为我节省 3 天的工作量,如果它能帮助其他处于相同情况的人,那将是非常棒的。
我将主要在此处展示我的解决方案,因为这几乎肯定是您阅读本文的原因:在解决问题的过程中,我学到了一些 Windows 鲜为人知的方面,但这本质上是一个 Bug 修复——该控件不应该像它那样表现,我们需要解决这个问题。
但是有一个问题。与此问题相关的许多文档都非常老(大多数提供任何相关信息的搜索都找到 2003 年或更早的文章),而且最近微软似乎已经淘汰了许多旧代码示例等,这些可能有所帮助。我们能在任何地方找到足够的线索来解决这个问题吗?
顺便说一下,我正在 Visual C++ 2010 上使用 MFC,但该解决方案有效,并且已在 Windows 7 和 Windows 10 上进行了测试,从 VC6 开始应该都没问题,所以请继续阅读...
解决方案
Active X 控件具有一个与其宿主程序交互的接口,在 MFC 中由一个名为 COleControlSite
的类表示。该类又包含 IUnknown
接口的实现,这是一个通用接口,可以使用 API 中定义的机制进行查询,以了解它支持哪些服务。在此上下文中,“服务”意味着控件可以做的任何事情:对于网页浏览器,它是调整窗口大小、缩放、浏览到网址、上一页等。由于服务由 GUID 标识,因此系统几乎可以无限扩展,因此可以添加任何类型的控件,提供任意数量的服务。
在网上苦苦搜索之后,我设法拼凑出以下信息:焦点问题的解决方案存在于这种机制中。我们需要支持一个名为 IProtectFocus
的接口,它派生自 IUnknown
。这至少为我们打开了一扇门。
自 MFC7 以来,Webbrowser 控件包装器(MFC 的 CHTMLDialog
和 CHtmlView
类)有一个虚函数 CreateControlSite
,它允许派生类重写默认的控件站点并用我们自己定义的站点替换它。
后续步骤
正如我们所知,MFC 试图通过将 Win32 小部件包装在 C++ 类中来简化 Windows 编程,旨在使其更易于使用它们来构建有用的程序。然而,人们常说,使用 MFC 编程主要是为了让框架完成它并非设计用于完成的事情。然而,使这成为可能的原因,以及我们能够使用这些东西不仅仅是为了制作玩具系统的唯一原因,就是我们可以访问 MFC 类的源代码。
在这里,这种访问很有帮助,因为我们只需在我们的一个源文件中输入“IProtectFocus
”,然后在上下文菜单中点击“转到定义”即可找出它包含什么。结合这一点和非常粗略的 MSDN 文档,我们发现它(像 C++ 中所有此类“接口”一样)是一个抽象
类,它只有一个自己的纯虚方法 AllowFocusChange
,该方法接受一个指向 BOOL 的指针(MFC 术语,用于表示用作布尔值的 int)——该函数只需将它指向的值设置为 FALSE,我们的控件就不会神奇地窃取焦点。
到目前为止一切顺利,但不幸的是,事情并非如此简单。我们必须让宿主程序知道 Webbrowser 支持 IUnknown
的 IProtectFocus
子类,事实证明,这需要实现另一个接口。这次是 IServiceProvider
:它有一个成员函数 QueryService
可以实现我们想要的功能。
正当我们觉得有所进展时,我们却发现 IServiceProvider
通过返回一个指向所请求接口的指针来响应查询,而不是直接以我们想要通信的布尔值来响应请求。但是,这个我们必须返回的指针到底是什么呢?关于这个主题的信息非常少;它是一个指向接口的指针,但这到底意味着什么?然而,既然已经走到这一步,我们还不会放弃。
在单个 IUnknown 实例上实现多个接口
我在网上任何地方都找不到这样的例子。我们唯一的帮助来自微软技术笔记 TN038: MFC/OLE IUnknown 实现。希望您阅读本文时它仍然存在,即使不存在,解决方案也仍然有效。
多部分接口是使用宏构造的,这些宏定义了接口各个部分在整体中的地址。这些宏创建内部类,这些内部类又继承自您正在实现的特定接口。它们还提供了大量支持 IUnknown
机制的样板代码,但它们使代码看起来“不正确”,因为它们声明了许多对读者不可见的东西——您实际上必须仔细查看宏的定义,才能对代码的工作原理有所了解。这是一个很好的例子,说明了 MFC 如何“帮助”;除非您深入研究表面之下,否则您必须遵循规则。
长话短说,我最终将所有这些都拼凑起来,但随后却遭遇了致命打击。它确实解决了焦点问题,但在承载 Webbrowser
的视图关闭时却崩溃了。这次崩溃发生在框架的深处,但我一直怀疑可能会发生类似的事情;IProtectFocus
接口返回一个指向堆的指针,当它被释放时,可能会导致致命的情况。框架似乎依赖于引用计数的方法来尝试避免这种情况,但要么我没有正确实现它,要么它没有按预期工作。
幸运的是,最后的尝试奏效了:我没有返回指向堆上的接口的指针,而是创建了一个静态存根接口并返回指向它的指针。这个指针显然始终有效,尽管不传统,但这个想法似乎有效。任务完成!
Using the Code
大部分代码来自 MFC 的 CHtmlView
(MFC 目录中的 viewhtml.cpp)。该文件包含默认接口的声明和定义,但对于非内联使用,需要将其拆分为 .h 和 .cpp 文件。
我建议您完全按照所示使用这些内容:我花了几个小时才弄明白为什么以前的版本无法链接,结果发现是与一个我甚至不知道存在的库类发生了命名冲突,所以我建议您保留这些名称。
重要提示:我的代码中的派生类名为 CHtmlCtrl
,因此文件中所有对 CHtmlCtrl 的引用都需要更改为您派生类的名称。在此示例中,我使用了 CHtmlDerived
。
将 HtmlControlSiteEx.cpp 和 HtmlControlSiteEx.h 添加到您的项目中。
然后,在您派生自 CHtmlView
的类中,重写虚函数 CreateControlSite
示例 .h 文件
#pragma once
//CHtmlDerived - subclass of CHtmlView fixing the focus-stealing bug
class CHtmlDerived : public class CHtmlView
{
public:
CHtmlDerived(/*params*/);
//...
//declarations
//...
virtual BOOL CreateControlSite(COleControlContainer* pContainer,
COleControlSite** ppSite, UINT nID, REFCLSID clsid);
//...
//declarations
//...
//ALSO ADD THE FOLLOWING which declare functions defined in HtmlControlSiteEx.cpp
//...simply copied from CHtmlView's class declaration
// DocHostUIHandler overrideables
virtual HRESULT OnShowContextMenu(DWORD dwID, LPPOINT ppt,
LPUNKNOWN pcmdtReserved, LPDISPATCH pdispReserved);
virtual HRESULT OnGetExternal(LPDISPATCH *lppDispatch);
virtual HRESULT OnGetHostInfo(DOCHOSTUIINFO *pInfo);
virtual HRESULT OnShowUI(DWORD dwID,
LPOLEINPLACEACTIVEOBJECT pActiveObject,
LPOLECOMMANDTARGET pCommandTarget, LPOLEINPLACEFRAME pFrame,
LPOLEINPLACEUIWINDOW pDoc);
virtual HRESULT OnHideUI();
virtual HRESULT OnUpdateUI();
virtual HRESULT OnEnableModeless(BOOL fEnable);
virtual HRESULT OnDocWindowActivate(BOOL fActivate);
virtual HRESULT OnFrameWindowActivate(BOOL fActivate);
virtual HRESULT OnResizeBorder(LPCRECT prcBorder,
LPOLEINPLACEUIWINDOW pUIWindow, BOOL fFrameWindow);
virtual HRESULT OnTranslateAccelerator(LPMSG lpMsg,
const GUID* pguidCmdGroup, DWORD nCmdID);
virtual HRESULT OnGetOptionKeyPath(LPOLESTR* pchKey, DWORD dwReserved);
virtual HRESULT OnFilterDataObject(LPDATAOBJECT pDataObject,
LPDATAOBJECT* ppDataObject);
virtual HRESULT OnTranslateUrl(DWORD dwTranslate,
OLECHAR* pchURLIn, OLECHAR** ppchURLOut);
virtual HRESULT OnGetDropTarget(LPDROPTARGET pDropTarget,
LPDROPTARGET* ppDropTarget);
};
示例 .cpp 文件
//CHtmlDerived - subclass of CHtmlView fixing the focus-stealing bug
#include "stdafx.h"
#include "htmlctrl.h"
#include "htmlcontrolsite.h"
//etc
CHtmlDerived::CHtmlDerived(/*params*/) : CHtmlView()
{
//do stuff
}
//...
//definitions
//...
BOOL CHtmlDerived::CreateControlSite(COleControlContainer* pContainer,
COleControlSite** ppSite, UINT nID, REFCLSID clsid)
{
//uncomment below to use default site:
//return CHtmlView::CreateControlSite(pContainer, ppSite, nID, clsid);
ASSERT(ppSite != NULL);
*ppSite = new CHtmlControlSiteEx(pContainer);
return TRUE;
}
构建并享受吧!
结论
CodeProject 上已经有很多关于 Webbrowser
控件各个方面的有用文章,但我在线上找到的任何内容似乎都没有提供关于如何解决这个特定问题的精确说明。实现此处描述的修复相对简单,一旦您开始使用这个控件(提示:尝试使用 PreTranslateMessage
),您会发现各种其他有用的功能。
网上其他有助于解决此问题的大部分资料已成为一片断裂链接的海洋。也许我错了,几乎没有人再使用老派的 MFC 了,但在我看来,它对于遗留应用程序仍然有生命力(尽管如果您正在做新事情,有许多更好的替代方案),微软似乎执意移除那些对我们仍然有用的旧代码示例等,这很可惜。