弹出窗口阻止程序






4.98/5 (44投票s)
2003年6月6日
26分钟阅读

1329678

7090
使用 ATL 编写的 BHO 弹出窗口阻止程序的第 3 版。
引言
我的上一篇文章演示了如何使用ATL编写的BHO创建基本的弹出窗口阻止程序。本月,添加了许多新功能,使弹出窗口阻止程序更具竞争力,可与您实际需要付费的商业应用程序相媲美
- 声音和热键现在可由用户选择。
- 当弹出窗口被阻止时,有一个可选的视觉指示器。
- 已添加白名单以允许整个网站或域的弹出窗口。
- 已添加弹出窗口被阻止的网站会话历史记录。
- 有一种简单的方法可以设置相关的IE设置来阻止视频和动画,并禁用脚本调试器。
- 已添加禁用Macromedia Flash动画的方法。
- 点击打开新窗口的链接时,您不再需要按住热键。
将讨论几个新的ATL类、DHTML、DOM和事件模型。此外,弹出窗口阻止程序菜单已添加到IE工具菜单,并使用WTL创建了一个选项卡式选项对话框。内容很多,让我们开始吧。
创建选项对话框
我们首先需要一个选项对话框,这不仅是为了方便用户,也是因为稍后我们将需要从IE工具菜单启动此对话框。我们可以使用ATL对话框类创建此对话框,或者如果您喜欢受虐,甚至可以直接使用Win32,但我真的很喜欢使用WTL,所以我决定走这条路。此外,我还想最终使用一些其他的WTL类,如CHyperLink
以及一些方便的常见对话框控件类。顺便说一下,WTL真的很酷,因为它非常简单、小巧、轻量级,而且您不必拖着任何巨大的DLL。它尚未得到Microsoft的官方支持,但当像ATL一样,如果它损坏了,您可以自己修复它(我们也将演示),谁会在意呢?只要能用,我都会用它而不是MFC。
话虽如此,我们立即遇到了WTL和属性代码的一个小问题,那就是WTL需要一个_Module
变量,该变量是ATL 3的一部分。使用ATL 7和属性代码时,找不到该变量。解决方法是在stdafx.h中定义我们自己的_Module
变量。
#define _Module (*_pModule) #include <atlmisc.h> #include <atlctl.h> ... #undef _Module
如果Microsoft将来正式支持WTL,这些小问题应该会消失。
弹出窗口阻止程序版本1的一个问题是,它只能通过IE上下文菜单访问。这并不总是可接受的,因为可能与其他使用上下文菜单的程序发生冲突,或者具有自己上下文菜单的嵌入对象(如Macromedia Flash)可能占据整个网页。因此,如果上下文菜单被禁用或不可用,显然我们需要另一种方法来控制弹出窗口阻止程序。我注意到其他一些应用程序在托盘上放置了一个shell图标,但我想避免这样做,因为托盘经常被过度使用和过度拥挤。我决定将弹出窗口阻止程序添加到IE工具菜单,因为它既不显眼,又始终可访问——即使没有鼠标。您添加到IE菜单栏的每个菜单项都必须由单独的类处理,因此,为了避免额外的工作,我只想添加一个菜单项,用于调出选项对话框,并在该对话框上提供所有选项。查看我们的原始菜单,选项对话框因此必须处理
- 启用
- 播放声音
- 访问Osborn Technologies
- 关于
这可以使用WTL::CPropertySheet
轻松处理。我们可以为“启用”和“播放声音”等选项创建一个WTL::CPropertyPage
,并为“关于”创建另一个页面。一个最简单的属性页的类定义如下
class CYourPage : public CPropertyPageImpl<CYourPage> { public: enum { IDD = IDD_PROPPAGE_YOUR_RESOURCE_ID }; BEGIN_MSG_MAP(CYourPage) MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog) CHAIN_MSG_MAP(CPropertyPageImpl<CYourPage>) END_MSG_MAP() LRESULT OnInitDialog(UINT, WPARAM, LPARAM, BOOL&) { return TRUE; } };
创建一个或多个属性页并将它们添加到CPropertySheet
,如下所示
void CPub::EditOptions(UINT nStartPage /*= 0*/) { CPropertySheet sht(IDS_CONFIGDLG_TITLE, nStartPage); CConfigOptionsPage pageOptions; CConfigAdvancedPage pageAdvanced; CConfigWhiteListPage pageWhiteList; CConfigHistoryPage pageHistory; CConfigAboutPage pageAbout; // Initialize the variables on your property pages here ... // Initialize the property sheet sht.m_psh.dwFlags |= PSH_NOAPPLYNOW; sht.AddPage(pageOptions); sht.AddPage(pageAdvanced); sht.AddPage(pageWhiteList); sht.AddPage(pageHistory); sht.AddPage(pageAbout); if (sht.DoModal() == IDOK) { // Retrieve the new values from the property pages here. } }
如果您从BHO创建并启动属性表,它将始终出现在浏览器左上角。这看起来有点奇怪。最好至少将其居中于浏览器窗口。为此,您可以从CPropertyPage::OnInitDialog
调用GetPropertySheet().CenterWindow()
。如果您始终使用相同的起始页,这会非常棒,但假设您想在启动时显示不同的页面。您可以从每个页面调用CenterWindow()
,但如果用户移动对话框,然后单击另一个选项卡,对话框将跳回浏览器窗口的中心。我的解决方案是创建我自己的属性表类,该类将起始页保存到一个全局变量中,该变量可从属性页访问。然后,我在OnInitDialog
开头将起始页与活动页进行比较,并且只有当它们相同时才将窗口居中。
class CConfigPropertySheet : public CPropertySheetImpl<CConfigPropertySheet> { public: CConfigPropertySheet(UINT nTitleID = NULL, UINT uStartPage = 0, HWND hWndParent = NULL) : CPropertySheetImpl<CConfigPropertySheet>(nTitleID, uStartPage, hWndParent) { g_nStartPage = uStartPage; } BEGIN_MSG_MAP(CConfigPropertySheet) MESSAGE_HANDLER(WM_COMMAND, CPropertySheetImpl<CConfigPropertySheet>::OnCommand) END_MSG_MAP() }; LRESULT OnInitDialog(UINT, WPARAM, LPARAM, BOOL&) { if (g_nStartPage == GetPropertySheet().GetActiveIndex()) GetPropertySheet().CenterWindow(); ... }
一个方便的WTL类是CWinDataExchange
,它是MFC数据交换类的模拟。将“启用”和“播放声音”的复选框添加到您的选项属性页资源中,然后使属性页类派生自CWinDataExchange
并添加一个DDX映射,如下所示
class CConfigOptionsPage : public CPropertyPageImpl<CCONFIGOPTIONSPAGE>, public CWinDataExchange<CCONFIGOPTIONSPAGE> { public: BEGIN_DDX_MAP(CConfigOptionsPage) DDX_CHECK(IDC_ENABLE, m_nEnable) DDX_CHECK(IDC_PLAY_DEFAULT_SOUND, m_nPlayDefaultSound) ... END_DDX_MAP() }
就像使用MFC一样,您调用DoDataExchange(FALSE)
来初始化控件,调用DoDataExchange(TRUE)
来从控件中检索值。您可以在atlddx.h中找到CWinDataExchange
的定义和所有DDX_*
宏的列表。
另一组有用的WTL类可以在atlctrls.h中找到。它们是标准和通用Windows控件的包装类,例如CEdit
、CComboBox
、CListViewCtrl
、CTreeViewCtrl
等。使用这些类再简单不过了。使用DDX_CONTROL
宏连接到您的对话框控件,或者像我在这里一样简单地分配一个窗口句柄
class CConfigOptionsPage : public CPropertyPageImpl<CConfigOptionsPage>, public CWinDataExchange<CConfigOptionsPage> { // Add a combobox with ID == IDC_HOTKEY to the Options page. // Use the WTL CComboBox class to interact with the control. CComboBox m_cbHotkey; public: LRESULT OnInitDialog(UINT, WPARAM, LPARAM, BOOL&) { if (g_nStartPage == GetPropertySheet().GetActiveIndex()) GetPropertySheet().CenterWindow(); // Attach to the WTL::CComboBox class m_cbHotkey = GetDlgItem(IDC_HOTKEY); ATLASSERT(m_cbHotkey.IsWindow()); CString s; s.LoadString(IDS_CTRL); m_cbHotkey.InsertString(-1, s); // VK_CONTROL s.LoadString(IDS_SHIFT); m_cbHotkey.InsertString(-1, s); // VK_SHIFT s.LoadString(IDS_DISABLED); m_cbHotkey.InsertString(-1, s); if (m_cbHotkey.GetCount() > 0) { if (m_nHotkey == VK_CONTROL) m_cbHotkey.SetCurSel(0); else if (m_nHotkey == VK_SHIFT) m_cbHotkey.SetCurSel(1); else m_cbHotkey.SetCurSel(2); } return 1; // Let the system set the focus } }
我的最终选项页如下所示
对于“关于”页面,我基本上是从原始的“关于”对话框复制粘贴的。但是,我确实利用了WTL::CHyperLink
类来添加指向我的网站和电子邮件的链接,从而使我无需“访问Osborn技术”上下文菜单项。我将用于网页链接和电子邮件链接的静态控件添加到“关于”属性页,然后使用CHyperLink
类对其进行子类化
class CConfigAboutPage : public CPropertyPageImpl<CConfigAboutPage> { CHyperLink m_weblink; CHyperLink m_emaillink; public: ... LRESULT OnInitDialog(UINT, WPARAM, LPARAM, BOOL&) { ... m_weblink.SetLabel(_T("www.osborntech.com")); m_weblink.SetHyperLink(_T("http://www.osborntech.com")); m_weblink.SubclassWindow(GetDlgItem(IDC_WEBLINK)); m_emaillink.SetLabel(_T("info@osborntech.com")); m_emaillink.SetHyperLink(_T("mailto:info@osborntech.com")); m_emaillink.SubclassWindow(GetDlgItem(IDC_EMAILLINK)); return TRUE; } };
这是最终的“关于”页面
我们现在可以通过调整CPub::ShowContextMenu
方法从BHO启动新的选项对话框。我们通过将所需的启动页面传递给EditOptions()
来确定最初显示的属性页面。
... else if (nCmd == ID_CTXMENU_OPTIONS) { EditOptions(0); } else if (nCmd == ID_CTXMENU_WHITELIST) { EditOptions(2); } else if (nCmd == ID_CTXMENU_HISTORY) { EditOptions(3); } else if (nCmd == ID_CTXMENU_ABOUT) { EditOptions(4); }
我们可以通过使用互斥体来防止同时显示多个选项对话框。
void CPub::EditOptions(UINT nStartPage /*= 0*/) { // The mutex prevents simultaneous access by // multiple instances of the BHO. if (WAIT_OBJECT_0 != WaitForSingleObject(m_hMutexOptions, 0)) { AtlMessageBox(NULL, IDS_MSG_OPTIONSDLG, IDS_PROJNAME); return; } ... ReleaseMutex(m_hMutexOptions); }
现在是解释BHO的一般情况的好时机。BHO将自身附加到IE的每个实例。每个IE实例可能在单独的进程中(并且每个进程都必须加载一个DLL),或者IE的几个实例可能在同一个进程中运行。在处理全局变量时,理解这一点非常重要。当几个IE实例碰巧在同一个进程中运行时,它们只加载BHO DLL一次,尽管每个实例仍然创建自己的BHO对象实例。全局变量在同一进程中运行的所有BHO实例之间共享,因此在修改这些变量时必须使用某种同步。如果您想测试这一点,您可以在每次从桌面启动IE时在单独的进程中启动IE,或者通过右键单击链接并选择“在新窗口中打开”来在同一进程中启动IE的另一个实例。
顺便说一下,请注意EditOptions()
中对AtlMessageBox
的调用。这是一个ATL函数,它显示一个普通的MessageBox,但额外的好处是它接受字符串ID而不是带引号的字符串。通过将字符串放在资源文件中,将来(可能在版本3)国际化弹出窗口阻止程序会更容易。
修改IE工具菜单
为了使弹出窗口阻止程序可以通过键盘访问,我决定将其添加到IE工具菜单。MSDN文章“添加菜单项”中非常清楚地解释了此过程。新菜单项可以运行三种类型的东西:脚本、可执行文件和COM对象。对于弹出窗口阻止程序,我们将运行一个COM对象。MSDN文章中描述的一般步骤显示了所需的注册表修改。我已将这些注册表更改封装在PopupBlocker.rgs文件中。
HKLM
{
NoRemove SOFTWARE
{
NoRemove Microsoft
{
NoRemove 'Internet Explorer'
{
NoRemove Extensions
{
ForceRemove {0D555BC6-E331-48b3-A60E-AAC0DF79438A} =
s 'PopupBlocker Menu'
{
val MenuCustomize = s 'Tools'
val CLSID =
s '{1FBA04EE-3024-11D2-8F1F-0000F87ABD16}'
val MenuText = s 'Popup Blocker'
val MenuStatusBar = s 'Show the Popup Blocker menu'
val ClsidExtension =
s '{93F764AC-24D1-484F-92EA-3C84E31CDF72}'
}
}
}
}
}
}
ForceRemove
行上的GUID可以是您使用MSVC中包含的Create GUID实用程序生成的任何新GUID。MenuCustomize
(可选)表示我们正在将菜单项添加到“工具”菜单。“工具”是默认值。您可以选择“工具”或“帮助”,没有其他选项。MenuText
是用户在拉下“工具”菜单时将看到的内容。MenuStatusBar
(可选)是当用户突出显示我们的菜单项时IE状态栏上显示的内容。ClsidExtension
是我们接下来将创建的COM对象的GUID。请注意,这些注册表设置放置在HKLM (HKEY_LOCAL_MACHINE
) 下。根据您的应用程序,您也可以将它们放置在HKCU (HKEY_CURRENT_USER
) 下;但是,我们希望将它们放置在HKLM下,因为BHO是在HKLM下注册的。
COM 对象应使用向导创建为 ATL 简单对象。在向导中,务必选择 IObjectWithSite
选项,因为我们需要访问 IE 的运行实例。创建对象后,我们还需要实现 IOleCommandTarget
。您的类声明应如下所示
// IToolsMenu [ object, uuid("6ED8119A-E0C0-4F30-84B6-5294CFADC3DC"), dual, helpstring("IToolsMenu Interface"), pointer_default(unique) ] __interface IToolsMenu : IDispatch { }; // CToolsMenu [ coclass, threading("apartment"), aggregatable("never"), vi_progid("OsbornTech.PopupBlocker.ToolsMenu"), progid("OsbornTech.PopupBlocker.ToolsMenu.1"), version(1.0), uuid("93F764AC-24D1-484F-92EA-3C84E31CDF72"), helpstring("ToolsMenu Class") ] class ATL_NO_VTABLE CToolsMenu : public IObjectWithSiteImpl<CTOOLSMENU>, public IToolsMenu, public IOleCommandTarget { ... // // IOleObjectWithSite Methods // STDMETHOD(SetSite)(IUnknown *pUnkSite) { return S_OK; } // // IOleCommandTarget // STDMETHOD(QueryStatus)( /*[in]*/ const GUID *pguidCmdGroup, /*[in]*/ ULONG cCmds, /*[in,out][size_is(cCmds)]*/ OLECMD *prgCmds, /*[in,out]*/ OLECMDTEXT *pCmdText) { return S_OK; } STDMETHOD(Exec)( /*[in]*/ const GUID *pguidCmdGroup, /*[in]*/ DWORD nCmdID, /*[in]*/ DWORD nCmdExecOpt, /*[in]*/ VARIANTARG *pvaIn, /*[in,out]*/ VARIANTARG *pvaOut) { return S_OK; } ... }
请注意,CToolsMenu
GUID 是写入注册表中ClsidExtension
下的GUID。此时,如果您编译弹出窗口阻止程序并运行IE,您应该会在IE工具菜单下看到我们新的菜单项。我们所要做的就是添加一些代码,使其实际执行某些操作。
然而,我们首先要解决一个相当大的问题。那就是如何找到一种方法让这个CToolsMenu
对象与BHO进行通信。两者都附加到IE的同一运行实例,但没有明显的方法让它们相互通信。我的想法是使用BHO创建一个DOM扩展,并让CToolsMenu
对象通过window.external
与BHO通信,就像您使用一段脚本一样。
为了实现这一点,我们需要在SetSite
下获取IE的运行实例。这就是MSDN不足之处。MSDN文章描述了如何获取IShellBrowser
,但没有提及如何获取IWebBrowser2
,只说可以做到。嗯,非常感谢。经过一番挖掘,我发现您必须使用IServiceProvider::QueryService()
来获取浏览器的指针(参见Q257717)。一旦我们有了IWebBrowser2
,就可以简单地向下钻取到IHTMLWindow2::get_external
以获取外部调度,我们将其隐藏在成员变量中。
STDMETHOD(SetSite)(IUnknown *pUnkSite) { if (!pUnkSite) { ATLTRACE(_T("SetSite(): pUnkSite is NULL\n")); } else { // Get the web browsers external dispatch interface. // Get the service provider from the site CComQIPtr<IServiceProvider><ISERVICEPROVIDER, &IID_IServiceProvider> spProv(pUnkSite); if (spProv) { CComPtr<IServiceProvider><ISERVICEPROVIDER> spProv2; HRESULT hr = spProv->QueryService(SID_STopLevelBrowser, IID_IServiceProvider, reinterpret_cast<void **>(&spProv2)); if (SUCCEEDED(hr) && spProv2) { CComPtr<IWebBrowser2><IWEBBROWSER2> m_spWebBrowser2; hr = spProv2->QueryService(SID_SWebBrowserApp, IID_IWebBrowser2, reinterpret_cast<void **>(&m_spWebBrowser2)); if (SUCCEEDED(hr) && m_spWebBrowser2) { CComPtr<IDISPATCH> spDisp; hr = m_spWebBrowser2->get_Document(&spDisp); if (SUCCEEDED(hr)) { CComQIPtr<IHTMLDocument2><IHTMLDOCUMENT2, &IID_IHTMLDocument2> spHTML(spDisp); if (spHTML) { CComPtr<IHTMLWindow2><IHTMLWINDOW2> spWindow; spHTML->get_parentWindow(&spWindow); if (spWindow) spWindow->get_external(&m_spExtDisp); } } } } } } return S_OK; }
要实际调用外部调度,我们需要完成IOleCommandTarget
的实现。我们唯一关心的方法是IOleCommandTarget::Exec()
,当我们的菜单项被选中时,它会以nCmd == 0
被调用。我们尚未创建DOM扩展(这是下一步),但是当我们创建时,我们将添加一个名为EditOptions
的命令,它不带参数也不返回值。为了进行调用,我们获取BHO中EditOptions
方法的调度ID,然后调用Invoke。
STDMETHOD(Exec)( /*[in]*/ const GUID *pguidCmdGroup, /*[in]*/ DWORD nCmdID, /*[in]*/ DWORD nCmdExecOpt, /*[in]*/ VARIANTARG *pvaIn, /*[in,out]*/ VARIANTARG *pvaOut) { if (nCmdID == 0) { // User selected our menu item from the IE Tools menu. // Call the DOM extension in the BHO. if (m_spExtDisp) { DISPID dispid; OLECHAR FAR* szMember = L"EditOptions"; HRESULT hr = m_spExtDisp->GetIDsOfNames(IID_NULL, &szMember, 1, LOCALE_SYSTEM_DEFAULT, &dispid); if (SUCCEEDED(hr)) { DISPPARAMS DispParams; VARIANT varResult; EXCEPINFO ExcepInfo; UINT uArgErr; VariantClear(&varResult); memset(&DispParams, 0, sizeof(DISPPARAMS)); memset(&ExcepInfo, 0, sizeof(EXCEPINFO)); m_spExtDisp->Invoke(dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, 0, &DispParams, &varResult, &ExcepInfo, &uArgErr); } } } return S_OK; }
当然,所有这些都不会奏效,直到我们使用EditOptions
方法创建DOM扩展,这在BHO中完成。
创建 DOM 扩展
DOM 扩展使我们能够向 IE 添加新命令,这些命令可以从脚本或任何其他(例如 CToolsMenu
)可以获取文档窗口指针的地方调用。当我们在 CToolsMenu::SetSite()
中调用 IHTMLWindow2::get_external()
时,IE 会调用 BHO 中的 CPub::GetExternal()
方法,以查看我们是否希望提供外部调度接口。在版本 1 中,我们只是从 GetExternal()
返回 S_FALSE
,但在版本 2 中,我们将返回 DOM 扩展的调度接口。通过使用向导添加另一个 ATL 简单对象并向该对象添加名为 EditOptions
的方法来创建 DOM 扩展。
// IPubDomExtender [ object, uuid("F3777260-7308-464A-BAA2-CC492C0CE7D2"), dual, helpstring("IPubDomExtender Interface"), pointer_default(unique) ] __interface IPubDomExtender : IDispatch { [id(1), helpstring("method EditOptions")] HRESULT EditOptions(); }; // CPubDomExtender [ coclass, threading("apartment"), aggregatable("never"), vi_progid("OsbornTech.PubDomExtender"), progid("OsbornTech.PubDomExtender.1"), version(1.0), uuid("83EC9074-6CBA-43E8-B7E0-6A3809C4A958"), helpstring("OsbornTech PubDomExtender Class") ] class ATL_NO_VTABLE CPubDomExtender : public IPubDomExtender { public: CPubDomExtender() : m_pPub(NULL) { } DECLARE_PROTECT_FINAL_CONSTRUCT() HRESULT FinalConstruct() { return S_OK; } void FinalRelease() { } public: STDMETHOD(EditOptions)() { if (m_pPub) m_pPub->EditOptions(); return S_OK; } public: CPub* m_pPub; };
在CPub::Invoke()
中,在DISPID_DOCUMENTCOMPLETE
下添加以下代码
// Create a CPubDomExtender object that will be used by // window.external CComObject<CPUBDOMEXTENDER>* pDomExt; hr = CComObject<CPUBDOMEXTENDER>::CreateInstance (&pDomExt); ATLASSERT(SUCCEEDED(hr)); if (SUCCEEDED(hr)) { CComQIPtrspDisp = pDomExt; m_spExtDispatch = spDisp; pDomExt->m_pPub = this; }
然后更改CPub::GetExternal()
方法以传回此IDispatch
指针。
STDMETHOD(GetExternal)(IDispatch** ppDispatch) { // IE will call this when the user asks for the external dispatch // of the window, either via script (window.external) or via // the Tools menu. if (m_spExtDispatch) return m_spExtDispatch.CopyTo(ppDispatch); if (m_spDefaultDocHostUIHandler) return m_spDefaultDocHostUIHandler->GetExternal(ppDispatch); return S_FALSE; }
一旦您将所有这些编译好,当从IE工具菜单中选择弹出窗口阻止程序时,应该会显示选项对话框。您可以在CToolsMenu::SetSite()
中设置一个断点,并在CPub::GetExternal()
中设置一个断点,以查看选择菜单项时的事件顺序。
会话历史
会话历史的目的是跟踪当前会话期间弹出窗口被阻止的所有站点。这里唯一棘手的部分是BHO的所有实例之间共享站点列表。像所有其他事情一样,有许多方法可以做到这一点。在这种情况下,我选择使用内存映射文件作为在进程之间共享数据的方法。
如果您不知道内存映射文件是什么,可以从MSDN中的“关于文件映射”中快速了解。这里有一个更快的概述:一旦您映射了一个文件,您就会获得一个指向数据的指针,您可以像处理任何其他char*
一样处理它。您可以执行strcpy
、strcmp
等,就像您正在处理一个正常的C字符串一样。共享部分在于您可以为这个映射指定一个名称。然后从另一个进程中,您可以通过提供名称来打开映射,然后*瞧*,您就获得了一个指向相同数据的字符串指针。我遗漏了一些东西,但这就是它的要点。
由于我只保留当前会话的站点列表,所以我不希望保留文件。当最后一个BHO终止时,内存映射文件应该被删除。ATL提供了一个CAtlTemporaryFile
类,它符合要求。CAtlTemporaryFile
在关闭时会自动删除自己。
ATL附带了另一个很棒的类,叫做CAtlFileMapping
,它是Win32文件映射函数的薄包装。这个类隐藏了创建文件映射的一些细节,并提供了一种打开命名文件映射的方法。当然,如果这个类没有一个明显的遗漏,它会更棒:虽然你可以打开一个命名文件映射,但使用这个类无法创建一个。开箱即用,CAtlFileMapping::MapFile()
方法不带名称参数。所以让我们抓住这个机会,创建一个新的“修复”类。我所做的是复制整个atlfile.h并创建atlfile_fixed.h,其中包含相同的类,只是我将命名空间从ATL
更改为PUB
。然后我像这样更改了MapFile
方法的参数列表
// FIXED: Added the pszName parameter HRESULT MapFile( HANDLE hFile, SIZE_T nMappingSize = 0, ULONGLONG nOffset = 0, DWORD dwMappingProtection = PAGE_READONLY, DWORD dwViewDesiredAccess = FILE_MAP_READ, LPCTSTR pszName = NULL) throw()
在MapFile
的深处,我将对CreateFileMapping
的调用从
m_hMapping = ::CreateFileMapping(hFile, NULL, dwMappingProtection,
liFileSize.HighPart, liFileSize.LowPart, 0);
to
m_hMapping = ::CreateFileMapping(hFile, NULL, dwMappingProtection, liFileSize.HighPart, liFileSize.LowPart, pszName);
好了,都修好了。现在,要使用我们的类而不是普通的ATL类,您只需像这样指定PUB
命名空间
PUB::CAtlFileMapping<TCHAR> g_filemapBlockedList;
在DllMain
的DLL_PROCESS_ATTACH
下,我首先尝试打开一个名为_T("PubBlockedSiteList")
的现有文件映射。如果失败,我会在一个临时文件上创建一个新的映射。
// File mapping is another way of sharing data between processes. // Popup Blocker keeps a list of sites where popups have been blocked // during the current session. This list is shared between all // instances of the BHO. HRESULT hr = E_FAIL; try { hr = g_filemapBlockedList.OpenMapping(_T("PubBlockedSiteList"), SHMEMSIZE); if (FAILED(hr)) { CAtlTemporaryFile f; hr = f.Create(NULL, GENERIC_READ | GENERIC_WRITE); if (SUCCEEDED(hr)) { f.SetSize(SHMEMSIZE); hr = g_filemapBlockedList.MapFile(f, SHMEMSIZE, 0, PAGE_READWRITE, FILE_MAP_ALL_ACCESS, _T("PubBlockedSiteList")); } } } catch(...) { hr = E_FAIL; }
通常,临时文件在超出作用域时会被删除,但只要有打开的映射,它就会保持打开状态——即使您明确关闭它!该文件最终会在DllMain
的DLL_PROCESS_DETACH
下解除映射,这将关闭映射。但是,只要另一个BHO仍然映射该文件,临时文件就会保持打开状态。映射文件中的数据只是一个由换行符分隔的URL字符串。我创建了一个UpdateStats
方法,该方法将新的URL附加到文件并增加跟踪被阻止弹出窗口数量的计数器。
void CPub::UpdateStats(BSTR bsUrl) { InterlockedIncrement((LPLONG)&g_dwBlockedSession); InterlockedIncrement((LPLONG)&g_dwBlockedTotal); if (WAIT_OBJECT_0 != WaitForSingleObject(m_hMutex, 1000)) return; try { CRegKey rk; DWORD dwErr = rk.Open(HKEY_CURRENT_USER, g_sCoRegKey + g_sPubRegKey); if (dwErr == ERROR_SUCCESS) rk.SetDWORDValue(_T("BlockedTotal"), g_dwBlockedTotal); if (g_bEnableHistory) { CString sUrl = CW2T(bsUrl); sUrl += _T("\n"); TCHAR* pchData = (TCHAR*)g_filemapBlockedList.GetData(); if (pchData && _tcsstr(pchData, sUrl) == NULL) { // Not already added int nBufLen = (int)g_filemapBlockedList.GetMappingSize(); int nDataLen = lstrlen(pchData); int nUrlLen = sUrl.GetLength(); if (nDataLen + nUrlLen < nBufLen) { nDataLen = lstrlen(pchData); memcpy(pchData + nDataLen, (LPCTSTR)sUrl, nUrlLen); } } } } catch(...) { } ReleaseMutex(m_hMutex); }
请注意,此代码受互斥锁保护,以防止多个BHO同时更改全局数据。我也可以将计数器放在受互斥锁保护的代码中,但我想使用InterlockedIncrement
只是为了展示另一种做法。InterlockedIncrement
通常只适用于同一进程中的线程,但由于这些变量在共享内存中(参见#pragma data_seg(".SHARED")
),在这种情况下它也有效。
为了显示映射文件的内容,我将历史页面添加到了选项对话框中。
在历史属性页的OnInitDialog
中,我解析URL列表并将它们填充到列表框中。
LRESULT OnInitDialog(UINT, WPARAM, LPARAM, BOOL&) { if (g_nStartPage == GetPropertySheet().GetActiveIndex()) GetPropertySheet().CenterWindow(); CString s; s.Format(_T("%ld"), m_dwBlockedSession); SetDlgItemText(IDC_BLOCKED_SESSION, s); s.Format(_T("%ld"), m_dwBlockedTotal); SetDlgItemText(IDC_BLOCKED_TOTAL, s); m_lbHistory = GetDlgItem(IDC_HISTORY); ATLTRY(s = (TCHAR*)g_filemapBlockedList.GetData()); if (!s.IsEmpty()) { int nPos = 0; CString sUrl = s.Tokenize(_T("\n"), nPos); while (sUrl != "") { CUrl url; if (url.CrackUrl(sUrl, ATL_URL_DECODE)) { if (url.GetScheme() == ATL_URL_SCHEME_HTTP || url.GetScheme() == ATL_URL_SCHEME_HTTPS) { // Get the domain without the scheme CString sDomain = url.GetHostName(); sDomain += url.GetUrlPath(); // Strip off the file name int idx = sDomain.ReverseFind(_T('/')); if (idx > 0) sDomain = sDomain.Left(idx); sDomain.MakeLower(); sDomain.Trim(); if (!sDomain.IsEmpty()) m_lbHistory.InsertString(0, sDomain); } } sUrl = s.Tokenize(_T("\n"), nPos); } } if (m_lbHistory.GetCount() > 0) m_lbHistory.SetCurSel(0); return TRUE; }
在这里,您会注意到另一个很酷的ATL类CUrl
的使用,它对于解析URL非常有用。在这种情况下,我用它来检查URL是否格式正确(否则CrackUrl
会失败)以及是否支持方案(HTTP或HTTPS)。然后,我在将其插入组合框之前,剥离了方案和路径。
白名单
白名单是版本2中比较有趣的部分之一。白名单的目的是保留一组应允许弹出窗口的URL。同样,这是必须在BHO的所有实例之间共享的数据,理想情况下内存中应该只有一份副本。虽然这可以通过多种方式完成,但我选择通过简单的文件名符使用IPersistFile
,主要是因为有人在评论版本1时建议了它(那就是你,Heath)。
首先,一点点(非常少)关于命名对象的基础知识。白名单存储在磁盘文件“对象”中。磁盘文件对象有一个显示名称,类似于C:\My Documents\WhiteList.pub。显示名称告诉我们关于文件对象位置的一些信息,但没有告诉我们如何访问其中包含的数据。这就是命名对象发挥作用的地方。您可以将命名对象视为一个智能名称(实际上 moniker 的意思是名称),它封装了访问其所代表对象的智能。因此,命名对象实际上是一个用于命名和访问另一个对象的对象。命名对象可以代表许多不同的事物——不仅仅是文件——但我想让事情简单化,所以在这里我不会深入细节。您可以参考 Kraig Brockschmidt 的《Inside OLE》一书,了解所有您可能想知道的关于命名对象的信息。在我们的例子中,我们只对表示我们的白名单磁盘文件的简单文件命名对象感兴趣。
由于我们要使用文件 Moniker,我们需要创建一个实现 IPersistFile
并且知道如何加载我们的白名单文件的对象。首先,我们在解决方案中添加一个新的 ATL 项目,并向该项目添加一个新的 ATL 简单对象,然后更改对象类,使其实现 IPersistFile
。我们最终得到如下所示的内容
// IWhiteList [ object, uuid("A40E7A73-615A-450B-A302-2A316E9C9892"), dual, helpstring("IWhiteList Interface"), pointer_default(unique) ] __interface IWhiteList : IDispatch { }; // CWhiteList [ coclass, threading("apartment"), support_error_info("IWhiteList"), aggregatable("never"), vi_progid("OsbornTech.PopupBlocker.WhiteList"), progid("OsbornTech.PopupBlocker.WhiteList.1"), version(1.0), uuid("52F0D70A-DBE1-4D79-AA46-1AD947CB6BCC"), helpstring("PopupBlocker WhiteList Class") ] class ATL_NO_VTABLE CWhiteList : public IWhiteList, public IPersistFile { public: CWhiteList() : { } DECLARE_PROTECT_FINAL_CONSTRUCT() HRESULT FinalConstruct() { return S_OK; } void FinalRelease() { } public: // IPersistFile STDMETHOD(GetClassID)(LPCLSID) { return E_NOTIMPL; } STDMETHOD(IsDirty)() { return E_NOTIMPL; } STDMETHOD(Save)(LPCOLESTR, BOOL) { return E_NOTIMPL; } STDMETHOD(SaveCompleted)(LPCOLESTR) { return E_NOTIMPL; } STDMETHOD(GetCurFile)(LPOLESTR*) { return E_NOTIMPL; } STDMETHOD(Load)(LPCOLESTR, DWORD); };
请注意,我们实现的唯一方法是 IPersistFile::Load()
,当 COM 加载我们的对象时,它将使用白名单文件的显示名称调用。现在您应该会问自己,COM 将如何加载我们的对象?COM 是如何将我们的白名单文件与我们刚刚创建的这个对象关联起来的?COM 通过一个称为绑定的过程来完成此操作。当我们在弹出窗口阻止程序中调用 CoGetObject
时,白名单文件名将转换为标识白名单文件的 Moniker,然后绑定到由 Moniker 标识的 CWhiteList
对象。Moniker 识别出白名单是一个文件并调用 GetClassFile
。GetClassFile
尝试将文件中的各种位(在我们的例子中是白名单文件头)与注册表中的模式进行匹配,注册表还包含我们 CWhiteList
对象的 GUID 条目。
为此,我们需要创建一个白名单文件,其中包含带有唯一位的标题。我创建了一个名为PubWhiteList.pwl的文件,其中第一行(标题)仅包含#jpo
。随着白名单URL的添加,它们将以纯文本格式列在后续行上,每行一个URL。然后我们需要添加一个注册表条目,将这些唯一位链接到CWhiteList
对象的GUID。我使用PubWhiteList.rgs来完成此操作。
HKCR { NoRemove FileType { ForceRemove {52F0D70A-DBE1-4D79-AA46-1AD947CB6BCC} = s 'PopupBlocker WhiteList' { 0 = s '0,4, FFFFFFFF, 236A706F' } } }
注册表中的模式标识为
regdb key = offset, cb, mask, value
换句话说,如果文件的前四个字节是0x23 0x6A 0x70 0x6F
(#jpo
),那么该文件应该与GUID {52F0D70A-DBE1-4D79-AA46-1AD947CB6BCC}
(我们的CWhiteList
对象)标识的对象关联。一旦moniker建立了这种关联,它就会使用GUID调用CoCreateInstance
,请求IPersistFile
接口,并调用IPersistFile::Load
,传入白名单文件的名称。然后moniker调用IPersistFile::QueryInterface(IID_IWhiteList)
并将指针作为CoGetObject
的out-parameter传回给弹出窗口阻止程序。完成任务后,moniker就会完全退出。这就是CoGetObject
在弹出窗口阻止程序中的调用方式
::CoGetObject(m_bsWhiteListPath, 0, __uuidof(IWhiteList), (void**)&g_spWhiteList);
m_bsWhiteListPath
是白名单文件的显示名称,IWhiteList
是我们请求的接口,g_spWhiteList
是Moniker返回的对象。
但我们还没有完成白名单对象。我们的要求之一是内存中只有一份数据副本。就目前而言,每个BHO都会实例化一个白名单对象。我们想要的是所有BHO实例都使用的一个白名单对象。为了实现这一点,我们在运行对象表(ROT)中注册白名单对象。
STDMETHODIMP CWhiteList::Load(LPCOLESTR pszFileName, DWORD grfMode) { ATLTRACE(_T("CWhiteList::Load\n")); m_sFileName = pszFileName; // Load the white list file data HRESULT hr = LoadListFromFile(); if (SUCCEEDED(hr)) { // Register ourselves in the ROT to ensure only one instance // of this class per file is running at a time. CComPtr<IRUNNINGOBJECTTABLE> pROT; HRESULT hr = GetRunningObjectTable(0, &pROT); if (SUCCEEDED(hr)) { CComPtr<IMONIKER> pmk; hr = CreateFileMoniker(pszFileName, &pmk); if (SUCCEEDED(hr)) pROT->Register(0, GetUnknown(), pmk, &m_dwCookie); } } return S_OK; }
在这里,我们只是基于白名单文件名创建一个文件 Moniker(这是一种标准 Moniker 类型),并将该 Moniker 注册到 ROT 中。然后,下一次 Popup Blocker 实例调用 CoGetObject
时,在创建新的 CWhiteList
对象之前,COM 会首先检查 ROT 以查看是否已有一个正在运行,如果存在,则返回指向该对象的指针。相当巧妙。您可以使用 MSVC 附带的 IROTVIEW 实用程序来验证白名单文件 Moniker 是否已注册到 ROT 中。
剩下的工作是向CWhiteList
对象添加允许访问数据的方法。最有趣的方法是IWhiteList::Find()
,它接受一个URL并返回TRUE
(如果该URL在白名单中找到)。
STDMETHODIMP CWhiteList::Find(BSTR bsUrl, VARIANT_BOOL* pbFound) { USES_CONVERSION; *pbFound = VARIANT_FALSE; CString sUrl = W2T(bsUrl); sUrl.MakeLower(); ATLTRACE(_T("Find: %s\n"), (LPCTSTR)sUrl); // We are expecting a fully formed URL like http://blah... // Make a half-hearted attempt at fixing the URL if it // doesn't look right. Sometimes you will get something // like javascript:makeWin2('http://blah...'). int i = sUrl.Find(_T("http://")); if (i > 0) { sUrl = sUrl.Mid(i); } else if (i < 0) { i = sUrl.Find(_T("https://")); if (i > 0) sUrl = sUrl.Mid(i); else if (i < 0) sUrl = _T("http://") + sUrl; } CUrl url; if (url.CrackUrl(sUrl, ATL_URL_DECODE)) { CString s = url.GetHostName(); if (lstrcmp(url.GetUrlPath(), _T("/")) != 0) s += url.GetUrlPath(); std::set<CString>::const_iterator itr; while (1) { if ((itr = m_setDomain.find(s)) != m_setDomain.end()) { *pbFound = VARIANT_TRUE; break; } int i = s.ReverseFind(_T('/')); if (i < 0) break; s = s.Left(i); } } return S_OK; }
我将白名单存储在内存中的std::set
中,因为集合针对快速查找进行了优化。查找算法如下:给定一个URL,例如http://some.domain.name/popups/ad1/123456.ext,我们剥离方案以生成some.domain.name/sub1/sub2/123456.ext。然后我们进入一个循环,按顺序执行以下查找
some.domain.name/sub1/sub2/123456.ext
some.domain.name/sub1/sub2
some.domain.name/sub1
some.domain.name
如果在任何时候我们成功查找,我们设置found = TRUE
并退出。搜索顺序很重要,因为您可能不想将整个网站(some.domain.name)列入白名单,而更具体地说是网站中的某个页面(some.domain.name/sub1)。
选项对话框上的“白名单”属性页如下所示
组合框允许用户输入一个网站,如果下拉,则列出IUrlHistoryStg2
枚举的所有方案为HTTP或HTTPS的网站。
BOOL LoadHistory() { m_cbDomain.ResetContent(); // We need this interface for getting the history. // Load the correct Class and request IUrlHistoryStg2 CComPtr<IURLHISTORYSTG2> spHistory; HRESULT hr = CoCreateInstance(CLSID_CUrlHistory, NULL, CLSCTX_INPROC_SERVER, IID_IUrlHistoryStg2, reinterpret_cast<VOID **>(&spHistory)); if (SUCCEEDED(hr)) { CComPtr<IENUMSTATURL> spEnum; hr = spHistory->EnumUrls(&spEnum); if (SUCCEEDED(hr)) { spEnum->Reset(); STATURL stat = {NULL}; stat.cbSize = sizeof(STATURL); ULONG nFetched = 0; do { hr = spEnum->Next(1, &stat, &nFetched); if (SUCCEEDED(hr) && nFetched) { CString sUrl = CW2T(stat.pwcsUrl); sUrl.MakeLower(); // Attempt to fix url int idx = sUrl.Find(_T("http://")); if (idx > 0) sUrl = sUrl.Mid(idx); CUrl url; if (url.CrackUrl(sUrl, ATL_URL_DECODE)) { if (url.GetScheme() == ATL_URL_SCHEME_HTTP || url.GetScheme() == ATL_URL_SCHEME_HTTPS) { // Get the domain without the scheme CString s = url.GetHostName(); s += url.GetUrlPath(); // Strip off the file name int idx = s.ReverseFind(_T('/')); if (idx > 0) s = s.Left(idx); s.MakeLower(); // Don't add the domain to the combobox if it // already exists in the combobox or whitelist. if (m_cbDomain.FindStringExact(-1, s) < 0 && m_lbWhiteList.FindStringExact(-1, s) < 0) { m_cbDomain.AddString(s); } } } // free memory allocated for us CoTaskMemFree(stat.pwcsTitle); CoTaskMemFree(stat.pwcsUrl); stat.pwcsTitle = NULL; stat.pwcsUrl = NULL; } } while (nFetched); } } return TRUE; }
这样做的目的是为了方便用户找到最近访问过的网站,但如果每个人的历史记录都像我一样长,那么它可能太困难/混乱,以至于用处不大。在下一个版本中,使用日期限制并且不将超过三天前的内容插入组合框中可能是有价值的。
事件处理
在版本2中,我还演示了如何处理HTMLWindowEvents2
和HTMLDocumentEvents2
。我主要添加这些事件处理程序是为了演示如何完成(这与版本1中DWebBrowserEvents2
的处理方式几乎完全相同),因此它们目前没有被用于执行太多操作。但是,我确实使用HTMLDocumentEvents2::onclick
来确定用户何时点击链接,然后允许新窗口打开。
case DISPID_HTMLDOCUMENTEVENTS2_ONCLICK: ATLTRACE(_T("HTMLDocumentEvents::onclick fired\n")); { CComPtr<IDISPATCH> spDisp; spDisp = pDispParams->rgvarg[0].pdispVal; if (spDisp) { CComQIPtr<IHTMLEVENTOBJ &IID_IHTMLEventObj,> spEventObj(spDisp); if (spEventObj) { CComPtr<IHTMLELEMENT> spElem; HRESULT hr= spEventObj->get_srcElement(&spElem); if (SUCCEEDED(hr) &&spElem) { CComBSTR bsTagName; while (1) { spElem->get_tagName(&bsTagName); bsTagName.ToUpper(); if (bsTagName == L"BODY") break; // did not click a link if (bsTagName == L"A" || bsTagName == L"AREA" || bsTagName == L"INPUT" || bsTagName == L"IMG") { *m_pbUserClickedLink = TRUE; break; } CComPtr<IHTMLELEMENT> spParentElem; hr = spElem->get_parentElement(&spParentElem); if (FAILED(hr) || !spParentElem) break; spElem = spParentElem; } } } } } break;
当用户点击文档时,我从事件对象中检索源元素并检查它是否是链接(或图像、区域等)。如果是,我设置一个标志并跳出循环。如果不是,我获取源元素的父元素并再次尝试,如此循环直到我检查了所有嵌套元素。回到CPub::Invoke
中,我检查这个标志以确定是否允许打开新窗口。
case DISPID_NEWWINDOW2: ATLTRACE(_T("(%ld) DISPID_NEWWINDOW2\n"), ::GetCurrentThreadId()); { BOOL bSiteWhiteListed = TRUE; CComBSTR bsUrl; HRESULT hr = m_spWebBrowser2->get_LocationURL(&bsUrl); if (SUCCEEDED(hr) && bsUrl.Length() > 0) bSiteWhiteListed = IsSiteWhiteListed(bsUrl); if (m_bBlockNewWindow && !bSiteWhiteListed && !m_bUserClickedLink && IsEnabled()) { ATLTRACE(_T("(%ld) Blocked a popup\n"), ::GetCurrentThreadId()); // Some links open a new window and will be blocked. // Give some indication to the user as to what is // happening; otherwise, the link will appear to be // broken. NotifiyUser(); // Update statistics UpdateStats(bsUrl); // Set the cancel flag to block the popup pDispParams->rgvarg[0].pvarVal->vt = VT_BOOL; pDispParams->rgvarg[0].pvarVal->boolVal = VARIANT_TRUE; } m_bBlockNewWindow = TRUE; // Reset m_bUserClickedLink = FALSE; // Reset } break;
检查鼠标点击对于使弹出窗口阻止程序尽可能不显眼起到了很大的作用。
CPub
还实现了IPropertyNotifySink
,它捕获文档的READYSTATE
。当我获得READYSTATE_COMPLETE
时,整个文档已经下载并处理完毕,此时我隐藏Flash动画。
隐藏 Flash 动画
弹出窗口只是网站设计师争夺我们注意力的一种方式。另一种是无处不在的Flash动画。虽然Flash技术非常酷,而且它在网络上确实占有一席之地,但它可能会被滥用。每个浏览网页的人都见过那些几乎蠕动着Flash动画的网站。所以与其忍受这种干扰,我添加了一个可选方法来隐藏Flash动画。我在弹出窗口阻止程序中实现的方法是等待文档完全加载,然后遍历DOM并隐藏我找到的所有Flash动画。
void DisableFlashMoviesHelper(IHTMLDocument2* pDoc) { CComQIPtr<IHTMLDOCUMENT2, &IID_IHTMLDocument2> spHTML(pDoc); if (spHTML) { CComPtr<IHTMLELEMENTCOLLECTION> spAll; HRESULT hr = spHTML->get_all(&spAll); if (SUCCEEDED(hr) && spAll) { // Find all the OBJECT tags on the (maybe partially loaded) document CComVariant vTagName = L"OBJECT"; vTagName.ChangeType(VT_BSTR); CComPtr<IDISPATCH> spTagsDisp; hr = spAll->tags(vTagName, &spTagsDisp); if (SUCCEEDED(hr) && spTagsDisp) { CComQIPtr<IHTMLELEMENTCOLLECTION, &IID_IHTMLElementCollection> spTags(spTagsDisp); if (spTags) { long nCnt; hr = spTags->get_length(&nCnt); if (SUCCEEDED(hr)) { for (long i = 0; i < nCnt; i++) { CComVariant varIdx; V_VT(&varIdx) = VT_I4; V_I4(&varIdx) = i; CComPtr<IDISPATCH> spTagDisp; hr = spTags->item(varIdx, varIdx, &spTagDisp); if (SUCCEEDED(hr) && spTagDisp) { CComQIPtr<IHTMLOBJECTELEMENT, &IID_IHTMLObjectElement> spObject(spTagDisp); if (spObject) { CComBSTR bsClassID; hr = spObject->get_classid(&bsClassID); if (SUCCEEDED(hr) && bsClassID) { bsClassID.ToUpper(); if (bsClassID == L"CLSID:D27CDB6E-AE6D-11CF-96B8-444553540000") { // This is a flash activex control. Resize // and hide it so we don't have to look at it. CComQIPtr<IHTMLElement> spElem(spTagDisp); if (spElem) { CComBSTR bs = L"hidden"; CComPtr<IHTMLSTYLE> spStyle; hr = spElem->get_style(&spStyle); if (SUCCEEDED(hr) && spStyle) { spStyle->put_visibility(bs); spStyle->put_pixelHeight(0); spStyle->put_pixelWidth(0); } } ATLTRACE(_T("Deleted flash animation\n")); } } } } } } } } // Find all the EMBED tags on the (maybe partially loaded) document vTagName = L"EMBED"; vTagName.ChangeType(VT_BSTR); spTagsDisp.Release(); spTagsDisp; hr = spAll->tags(vTagName, &spTagsDisp); if (SUCCEEDED(hr) && spTagsDisp) { CComQIPtr<IHTMLElementCollection> spTags(spTagsDisp); if (spTags) { long nCnt; hr = spTags->get_length(&nCnt); if (SUCCEEDED(hr)) { for (long i = 0; i < nCnt; i++) { CComVariant varIdx; V_VT(&varIdx) = VT_I4; V_I4(&varIdx) = i; CComPtr<IDISPATCH> spTagDisp; hr = spTags->item(varIdx, varIdx, &spTagDisp); if (SUCCEEDED(hr) && spTagDisp) { CComQIPtr<IHTMLEmbedElement> spObject(spTagDisp); if (spObject) { CComBSTR bsSrc; hr = spObject->get_src(&bsSrc); if (SUCCEEDED(hr) && bsSrc) { CString sSrc = CW2T(bsSrc.m_str); if (sSrc.Right(4).CompareNoCase(_T(".swf")) == 0) { // This is a flash activex control. Resize // and hide it so we don't have to look at it. CComQIPtr<IHTMLElement> spElem(spTagDisp); if (spElem) { CComBSTR bs = L"hidden"; CComPtrspStyle; hr = spElem->get_style(&spStyle); if (SUCCEEDED(hr) && spStyle) { spStyle->put_visibility(bs); spStyle->put_pixelHeight(0); spStyle->put_pixelWidth(0); } } ATLTRACE(_T("Deleted flash animation\n")); } } } } } } } } } } }
Flash动画可以通过OBJECT
标签或EMBED
标签放置在网页上,因此我们必须检查两者。如果使用OBJECT
标签指定ActiveX控件,那么我们可以使用类ID来确定它是否是Flash控件。如果使用EMBED
标签,我检查源文件的扩展名(.swf)。如果找到Flash控件,我使用内联CSS来调整大小并隐藏它。另一种方法,Ferdo建议的,是直接删除它。还有一种方法是获取Flash控件的IDispatch
指针并调用其Stop
和Rewind
方法。这样做的好处是您仍然可以看到Flash控件,但没有Flash,如果您明白我的意思的话。
您会注意到上面列出的DisableFlashMoviesHelper
例程接受一个IHTMLDocument2
作为参数。这是因为这个例程可能在一个网页上多次调用,因为有嵌套的框架,每个框架都有自己的文档。处理嵌套框架最逻辑的方法可能是使用递归。
void DisableFlashMoviesRecursively(IHTMLDocument2* pDoc) { CComQIPtr<IHTMLDOCUMENT2 &IID_IHTMLDocument2,> spHTML(pDoc); if (spHTML) { // Disable flash for the this document DisableFlashMoviesHelper(spHTML); // Disable flash for any embedded documents CComPtr<IHTMLELEMENTCOLLECTION> spAll; HRESULT hr = spHTML->get_all(&spAll); if (SUCCEEDED(hr) && spAll) { long nCnt = 0; HRESULT hr= spAll->get_length(&nCnt); if (SUCCEEDED(hr) && nCnt >0) { for (long i = 0; i < nCnt; i++) { CComVariant varIdx; V_VT(&varIdx) = VT_I4; V_I4(&varIdx) = i; CComPtr<IDISPATCH> spElemDisp; hr = spAll->item(varIdx, varIdx, &spElemDisp); if (SUCCEEDED(hr) && spElemDisp) { CComQIPtr<IHTMLFRAMEBASE2 &IID_IHTMLFrameBase2,> spFrame(spElemDisp); if (spFrame) { // This is a frame or iframe element. // Disable flash in its document. CComPtr<IHTMLWINDOW2> spWin; hr = spFrame->get_contentWindow(&spWin); if (SUCCEEDED(hr) && spWin) { CComPtr<IHTMLDOCUMENT2> spDoc; hr = spWin->get_document(&spDoc); if (SUCCEEDED(hr) && spDoc) { DisableFlashMoviesRecursively(spDoc); } } } } } } } } } void CPub::DisableFlashMovies() { ATLASSERT(m_spWebBrowser2); if (!m_spWebBrowser2) return; CComPtr<IDISPATCH> spDisp; HRESULT hr = m_spWebBrowser2->get_Document(&spDisp); if (SUCCEEDED(hr) && spDisp) { CComQIPtr<IHTMLDOCUMENT2 &IID_IHTMLDocument2,> spHTML(spDisp); if (spHTML) DisableFlashMoviesRecursively(spHTML); } }
在DisableFlashMoviesRecursively
例程中,我遍历文档的所有元素以查找框架。如果找到一个,我向下钻取到其文档并递归调用DisableFlashMoviesRecursively
。这样我们就可以绝对确定已找到页面上的所有Flash控件。
一个令人烦恼的是,Flash 动画在页面加载时播放,我们必须等到文档加载完毕才能遍历整个 DOM。因此,动画将一直播放到最终的 DISPID_DOCUMENTCOMPLETE
或 READYSTATE_COMPLETE
,如果您的连接速度较慢,这一点会非常明显。通过在每个 DISPID_DOWNLOADBEGIN
或中间的 DISPID_DOCUMENTCOMPLETE
调用 DisableFlashMovies
,从而逐帧隐藏动画,可以部分缓解这种情况。有点笨拙但相当有效。我选择不这样做,因为我认为可能有更好、更不密集的方法来解决这个问题,例如通过每个独立的 Flash 控件的 onload 或就绪状态事件进行处理。就我个人而言,当遇到一个非常糟糕的页面时,我只是在上下文菜单上使用“删除 Flash 动画”。也许我会在版本 3 中重新审视这个问题。
IE设置
有一些相关的IE设置可以关闭视频和动画,并禁用脚本调试器。这些只是注册表设置,也可以从IE工具/Internet选项/高级对话框中访问。我只是为了方便,将这些设置放在了选项对话框的“高级”选项卡上。
此页上最重要的设置是“禁用脚本调试器”,默认情况下应选中。如果您不禁用脚本调试器,BHO将无法捕获脚本错误。有关其他设置的帮助,请参阅IE Internet选项。
视觉指示
Kiliman 建议,当弹出窗口被阻止时,除了听觉提示外,还应有视觉提示。任何类型的提示都应尽可能不显眼,因为在浏览体验中增加干扰与我们使用弹出窗口阻止程序的目标背道而驰。在考虑了多种方法后,我决定简单地将默认状态文本从“完成”更改为“此页面包含弹出窗口!”。IE 在没有更好的事情可做时显示默认状态,因此这不会干扰 IE 可能发送到状态栏的其他消息。

// Show message in status bar if (g_bStatusMsg && m_spHTMLWindow) { // IE shows default status when there is nothing better to do. CComBSTR bsPopupsWarning = L"This page contains popup windows!"; m_spHTMLWindow->put_defaultStatus(bsPopupsWarning); }
别管IWebBrowser2::StatusText
属性了,IE会立即用defaultStatus
覆盖你的文本。
杂项
版本1中需要修复的一件事是允许安全站点(HTTPS)上的弹出窗口。这是因为许多安全站点使用弹出窗口来提示用户身份验证。为了实现这一点,我简单地将所有安全站点都列入了白名单。
此程序与Windows 98或98 SE不兼容;但是,应该很容易纠正。由于我不再拥有Win98系统,我必须向社区求助。如果有人愿意为Win98进行修正,我将在此发布。
<shameless_plug>最新版本可从Osborn Technologies网站安装。</shameless_plug>
改进
仍有改进空间
- 寻找更好的方法来清除 Flash 动画,可能是在 Flash 对象加载时通过事件触发。
- 寻找一种方法,让白名单历史组合框不那么令人望而生畏。
修订历史
- 2003年6月6日:在进一步审查之前,删除了在
E_ACCESSDENIED
上删除帧的代码。添加了缺失的安装项目。添加了缺失的二进制文件(setup.msi)。 - 2003年6月7日:更新以在Win98 SE上安装。
- 2003年6月8日:添加了ferdo的代码以阻止脚本调整大小。在高级属性页上更改IE设置后添加了刷新。
- v2.3 (2003年6月10日):Flash现在在所有帧中被删除。热键和白名单现在禁用所有功能。
- v2.4 (2003年6月11日):修复了白名单修复引入的错误0x8001010E。
- v2.5 (2003年6月17日):修复了保存设置错误。
- v3.0 (2003年7月3日):脚本调整大小的阻止代码已变得更加健壮(非常感谢Ferdinand Oeinck的工作!)。添加了一个用户可选的热键用于删除动画图像。现在动画图像可以用热键手动阻止,或者在页面加载后自动阻止,并可选择(再次感谢Ferdinand!)阻止非缓存图像。一些捕获帧内元素鼠标点击的问题已得到修复。
- v3.0(2003年7月8日):添加了miers对IE5.5的修复(谢谢!)。添加了对调整大小代码的修复(再次感谢Ferdinand!)。