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

如何通过 IDocHostUIHandler 接口自定义 WebBrowser 控件的上下文菜单。

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (43投票s)

2003 年 8 月 13 日

CPOL

24分钟阅读

viewsIcon

578901

downloadIcon

12415

本文介绍了如何通过实现 IDocHostUIHandler 接口来定制 WebBrowser 控件的上下文菜单。

重要提示

不幸的是,我最初在本文中使用的定制方法不适用于 MFC 应用程序(详情请参阅“重要通知”消息线程),您应该忽略我文章的第 4 节和第 6 节。相反,第 5 节仍然 100% 有效,并且在定制 WebBrowser 控件上下文菜单方面仍然非常有用。修订后的示例项目正在使用一种**新**的、更好的定制方法,将在本文的下一次更新中进行全面讨论,希望能在几周内准备好。我发布这份半文档化且未经完全测试的代码,是因为有迹象表明一些开发人员可能需要在我的下一次更新之前尽快获得这份代码。每个修订后的示例也都有一个 Readme.htm 文件,简要描述了该示例的工作原理。

目录

1. 为什么我们需要定制 WebBrowser 控件 (WBC) 的上下文菜单?

WebBrowser 控件 (WBC) 是一个功能强大的 ActiveX 控件,拥有许多有用的功能,例如 HTML、XML 和文本数据查看、网页浏览、下载和文档查看(它可以显示 PDF、Word、Excel、PowerPoint 和其他文档)。其功能和用途的描述已在 MSDN 网站 [1,2,3,4] 中存在,不属于本文的范围。本文将只讨论 WebBrowser 控件在显示 HTML、XML 或文本数据时提供的上下文菜单,我们将特别讨论这些上下文菜单的定制。

WebBrowser 控件显示 HTML、XML 或文本数据时,它默认提供了一组强大的上下文菜单,可用于操作其内容。因此,最终用户自动获得对其内容的显著控制。例如,最终用户可以随时选择前进和后退导航、更改当前编码、打印/导出/重新加载内容,并且还可以查看源数据。默认情况下,WebBrowser 控件执行这些操作不需要应用程序的任何协助或批准。事实上,应用程序甚至可能不知道这些操作曾经发生过!显然,最终用户对我们的 WebBrowser 控件内容拥有如此多的控制权并非总是可取的,并且可能在我们的应用程序设计中引入严重的困难。如果是这种情况,那么我们必须定制 WebBrowser 控件的默认上下文菜单,并使其适应我们应用程序的特定需求。

2. 可以使用的基本定制技术。

我了解两种可以用来定制 WebBrowser 控件上下文菜单的基本方法。我们可以通过重写 CWinApp::PreTranslateMessage() 方法,或者通过实现 IDocHostUIHandler 接口来实现这种定制。然而,由于我们将在接下来的两段中解释的原因,这两种技术都只适用于 WebBrowser 控件显示 HTML、XML 或文本数据的情况。

2.1. 重写 CWinApp::PreTranslateMessage()。

通过重写 CWinApp::PreTranslateMessage(),我们的应用程序将处理 WebBrowser 控件窗口的右键点击事件,而不是由 WebBrowser 控件本身处理。这种方法已经在 Santhosh Cheeran 撰写的另一篇 CodeProject 文章中讨论过。[22] 使用这种方法我们可以实现以下目标:

  • 完全抑制所有默认上下文菜单。
  • 实现我们自己的自定义弹出菜单,它将替换所有默认上下文菜单。

限制

如果我们把 PDF 或 Word 文档拖放到 WebBrowser 控件区域,文档会正常显示,但我们的自定义代码将停止工作!这种行为的原因可以通过 Spy++ 工具轻松找到。Spy++ 清楚地显示,实际显示这些文档的窗口属于一个独立的外部进程。因此,在该窗口上的右键单击事件永远不会出现在我们应用程序的事件队列中,并且我们的 PreTranslateMessage() 方法永远不会被调用来处理它们!

要求

WebBrowser 控件仅适用于 Windows NT 4.0 或更高版本(已安装 Internet Explorer 4.0 或更高版本)的程序。

2.2. 实现 IDocHostUIHandler 接口。

IDocHostUIHandler 接口的实现不是一种技巧或仅仅起作用的东西,这种技术的用处远超定制 WebBrowser 控件的上下文菜单。IDocHostUIHandlerIDocHostUIHandler2IDocHostShowUI 接口由 Microsoft 设计,旨在构成 WebBrowser 控件 UI 定制的核心。[8] 然而,在本文中,我们只需要讨论如何使用 IDocHostUIHandler 接口来定制 WebBrowser 控件的上下文菜单。使用此方法我们可以实现以下功能:

  • 完全抑制所有默认上下文菜单。
  • 实现我们自己的自定义弹出菜单,它将替换所有默认上下文菜单。
  • 抑制部分默认上下文菜单。
  • 实现我们自己的自定义弹出菜单,它将只替换部分默认上下文菜单。
  • 修改/删除默认上下文菜单中的某些菜单项,或向其中添加一些新项。

限制

WebBrowser 控件显示 HTML、XML 或文本数据时,它实际上托管了处理数据解析和渲染的 MSHTML 活动文档。[2,5,6,7] 另一方面,WebBrowser 控件可以显示 MSHTML 无法处理的许多其他数据类型(例如 PDF、Word 或 Excel 数据)。在这些情况下,WebBrowser 控件必须托管专门处理这些特定数据类型的其他活动文档。[2] 然而,IDocHostUIHandler 接口并非旨在定制 WebBrowser 控件本身,实际上它只能定制 MSHTML 活动文档,并且只有 MSHTML 才能调用其方法。[14] 不幸的是,这意味着当 WebBrowser 控件显示必须由 MSHTML 以外的活动文档(例如 PDF、Word 或 Excel 数据)处理的数据时,IDocHostUIHandler 接口将不被使用,我们的定制代码将失效。

要求

WebBrowser 控件可用于在 Windows NT 4.0 或更高版本上运行的程序,并且已安装 Internet Explorer 4.0 或更高版本,但其许多自定义功能要求存在 IE 5 或 IE 5.5。此外,使用此自定义技术,我们无法修改默认 WebBrowser 控件上下文菜单的菜单项,除非存在 Shdoclc.dll 系统文件。通常,此技术预计在 Windows Me/2000/XP 上表现与描述完全一致,并且很可能(尽管未经测试)在安装了 IE 5 或更高版本的 Windows 98 上也能很好地工作。

3. 本文的目标。

在本文中,我们将讨论通过 IDocHostUIHandler 接口定制 WebBrowser 控件上下文菜单。当我尝试自己实现这种定制技术时,我找到了足够的文档 [8],其中讨论了 IDocHostUIHandler 接口的性质和用法,我还找到了一个虽小但有用的代码示例 [10]。然而,在本文中,我提供了三项我无法在互联网上找到的重要便利,我相信它们的缺失让我付出了宝贵的时间:

  1. 分步指南:本文旨在提供一份分步指南,详细解释通过 IDocHostUIHandler 接口定制 WebBrowser 控件上下文菜单的方法。本文还将提供所有必要的参考资料,供任何想要深入研究的人使用。
  2. 一个 VC++ 示例项目:本文附带一个 VC++ 项目,可以构建一个简单的示例应用程序,演示通过 IDocHostUIHandler 接口定制 WebBrowser 控件上下文菜单。通过修改这个示例应用程序,或者用调试器对其进行研究,开发人员可以快速找到本文未讨论的特定技术问题的答案。
  3. 一些可重用代码:此外,所提供的一些代码可以作为开发人员的基础。开发人员无需自己编写和测试所有定制代码,只需修改 WebCtrlInterFace.cppWebCtrlInterFace.h 文件中提供的经过充分测试的 CWebCtrlInterFace 接口实现即可。

我相信,提供上述便利将使本文成为首次尝试定制 WebBrowser 控件上下文菜单的任何人的重要省时工具。事实上,这也是我撰写本文的最初目标:帮助开发人员节省宝贵时间。

4.初步内容的实现。

按照 MSDN 网站 [9] 上的说明,我们将实现 IDocHostUIHandlerIOleClientSite 接口。额外的 IOleClientSite 接口将通过 IOleObject::SetClientSite() 方法传递给 WebBrowser 控件,然后充当一个中间体。也就是说,WebBrowser 控件将使用此 IOleClientSite 实例的 QueryInterface() 方法来获取我们提供的 IDocHostUIHandler 实例。在本节中,我们将讨论与 WebBrowser 控件上下文菜单定制不直接相关的实现问题。我们定制的核心是 IDocHostUIHandler::ShowContextMenu() 的实现,这将在下一节中讨论。

4.1. “CWebCtrlInterFace”类声明。

我们编写 CWebCtrlInterFace C++ 类的声明,它将控制 WebBrowser 控件的定制,并从 IOleClientSiteIDocHostUIHandler 类派生。这样,我们将只需要维护一个对象,并且我们的接口将共享 IUnknown 基接口的一个通用实现。其余的都是相当琐碎的任务。我们只需要阅读文档 [12,13,14] 并相应地编写 CWebCtrlInterFace 类方法的声明。完整的 CWebCtrlInterFace 声明可以在 WebCtrlInterFace.h 头文件中找到。

4.2.“IUnknown”接口方法的实现。

我们将实现 IUnknown 基接口类的三个方法。幸运的是,MSDN 网站上有一个全面的示例 [11],使得这个实现变得相当简单。IUnknown::AddRef()IUnknown::Release() 方法只是管理其对象的存在,它们的实现不依赖于对象的类型。因此,对于这两个方法,我们使用了与示例中完全相同的实现。

另一方面,IUnknown::QueryInterface() 必须符合我们的特定需求,并且必须返回我们的 IDocHostUIHandlerIOleClientSite 接口实例。然而,它的实现也非常简单。我们只需要检查 riid 输入参数,然后通过 ppv 输出参数返回适当的接口指针。实际上,我们的 IUnknown::QueryInterface() 实现需要返回的所有接口指针都是指向我们 CWebCtrlInterFace 类的祖先类的指针。因此,我们可以通过简单地相应地向上转型 this 指针来轻松获取它们。完整的 IUnknown 实现可以在 WebCtrlInterFace.cpp 源文件和 Listing#1 中找到。

4.3. 在 IOleClientSite 和 IDocHostUIHandler 方法中实现“中立”行为。

由于我们只需要将 IOleClientSite 接口用作中间件,因此我们必须以其存在不会影响 WebBrowser 控件行为的方式来实现它。此外,除了影响 WebBrowser 控件上下文菜单行为的 ShowContextMenu() 方法之外,我们的 IDocHostUIHandler 接口的所有其他方法都必须表现得好像它们不存在一样。因此,我们需要在大多数 CWebCtrlInterFace 方法中实现“中立”行为,我们通过以下技术实现这一点:

  1. CWebCtrlInterFace 类配备了 SetDefaultClientSite() 公共方法,该方法接收默认的 IOleClientSite 接口实例,并随后使用它来获取默认的 IDocHostUIHandler 实例。这两个实例存储在 CWebCtrlInterFace 类中,并将用于委托任何我们不想定制的接口调用。清单#2 演示了 SetDefaultClientSite() 方法的实现。
  2. 如果默认接口未提供给我们的 CWebCtrlInterFace 实例,则通过在接口方法中返回适当的返回代码来实现“中立”行为。每种方法中使用的确切返回代码是在阅读相关文档 [13,14] 后确定的,通常是 E_NOTIMPS_FALSEE_FAIL。此外,我们通常有义务确保在不打算改变 WebBrowser 控件默认行为的接口方法中,输出参数指针设置为 NULL

完整的 IOleClientSiteIDocHostUIHandler 实现可以在 WebCtrlInterFace.cpp 源文件中找到。

5. “IDocHostUIHandler::ShowContextMenu()”方法的实现。

IDocHostUIHandler 接口正确实现并安装后,WebBrowser 控件(实际上是 MSHTML)会调用其 ShowContextMenu() 接口方法,而不是自行显示上下文菜单。因此,此接口方法的实现构成了 WebBrowser 控件上下文菜单定制的核心。在 MSDN 网站上,我们可以找到解释 ShowContextMenu() 接口方法的语法及其参数含义的文档 [15]。此外,还有一篇 MSDN 文章 [10] 讨论了 ShowContextMenu() 接口方法的实现,并提供了一个有用的代码示例。在以下段落中,我将尝试更全面地介绍此接口方法的实现,同时频繁引用 MSDN 网站文档以避免不必要的重复。

5.1. 关于 WebBrowser 控件上下文菜单的重要技术信息。

首先我应该澄清,我们想要定制的上下文菜单实际上是由 WebBrowser 控件托管的活动文档处理的,而不是由 WebBrowser 控件本身处理。正如我们之前提到的,[p2.2] 我们只处理 WebBrowser 控件托管 MSHTML 活动文档的情况,它可以处理 HTML、XML 或文本数据。可以处理 PDF、Word 或 Excel 文件的活动文档提供它们自己的、完全不同的上下文菜单,并且它们不识别或使用我们的 IDocHostUIHandler 接口。

MSHTML 提供专门用于图像、表格、文本选择的上下文菜单,以及一个默认上下文菜单。大多数情况下,在定制 WebBrowser 控件上下文菜单时,我们并不真的想放弃所有这些强大的功能;我们只是想进行一些特定的修改。因此,IDocHostUIHandler 接口相对于其他定制技术最重要的技术优势之一是,它允许我们选择要定制哪些菜单以及哪些菜单保持不变。ShowContextMenu() 接口方法的第一个参数表示 MSHTMLIDocHostUIHandler 不干预的情况下将显示的菜单的快捷菜单值。此外,在 mshtmhst.h 头文件中,我们可以找到与每个菜单值对应的常量(例如 CONTEXT_MENU_DEFAULTCONTEXT_MENU_TEXTSELECT 等),并且可以与 ShowContextMenu() 方法的第一个参数进行比较。通过这种方式,我们可以很容易地选择性地定制 WebBrowser 控件上下文菜单,就像我们稍后将在本文中演示的那样。[p5.4]另一方面,如果我们选择通过重写 CWinApp 类的 PreTranslateMessage() 方法来定制 WebBrowser 控件快捷菜单,[22] 我们将永远不知道每次显示自定义菜单时替换的是哪个上下文菜单。

有时,修改特定的 MSHTML 上下文菜单(移除、添加、替换或修改其菜单项)而不是用自定义菜单替换它似乎更合适。还有一些情况需要我们在运行时检查上下文菜单项。在这些情况下,我们可以利用所有 MSHTML 上下文菜单都作为子菜单存储在 Shdoclc.dll 系统文件的 #24641 菜单资源中的事实。[10] 此外,mshtmhst.h 头文件中的快捷菜单值表示相应快捷菜单在此菜单资源中的零基相对位置。因此,加载菜单资源,检索特定快捷菜单的句柄,然后相应地操作它是一项相当琐碎的任务。在本文后面 [p5.6],我们将演示如何使用 C++ 和一些 Win32 SDK 调用来完成所有这些操作。

如果我们要操作 MSHTML 快捷菜单中的特定项,使用 MSHTML 命令 ID(例如 IDM_VIEWSOURCEIDM_SELECTALL 等)会很方便,这些命令 ID 在 mshtmcid.h 头文件中定义,并通过其命令 ID 引用菜单项。许多用于操作菜单项的 Win32 SDK 调用可以接受命令 ID 作为参数(通常在使用 MF_BYCOMMAND 标志时)。这些命令 ID 在我们提供从 MSHTML 快捷菜单借用项的自定义菜单时也至关重要。我们可以将这些项绑定到适当的命令 ID,当菜单选择完成后,我们只需发送一个 WM_COMMAND 消息,将相应的命令 ID 作为第一个消息参数 (wParam) 传递。本文后面 [p.5.5, p.5.6] 将演示如何完成所有这些操作。

最后,在深入实现 ShowContextMenu() 接口方法之前,我必须指出文档中的一个重要错误。ShowContextMenu()dwID 参数并不像文档 [15] 所述那样代表将值 0x1 按快捷菜单值进行位移。实际上,dwID 代表的是快捷菜单值本身。

5.2. 引入 CWebCtrlInterFace 定制模式。

IDocHostUIHandler 接口的 ShowContextMenu() 方法可以通过多种不同的方式实现,分别实现不同类型的定制。[p.2.2] 为了提高 CWebCtrlInterFace 类的有用性和可重用性,[p.3, 3] 我在其 ShowContextMenu() 方法中实现了各种“定制模式”。因此,CWebCtrlInterFace 类包含 m_contextMenuMode 字段,该字段保存当前的上下文菜单定制模式,并确定 ShowContextMenu() 每次应如何工作。当前的定制模式可以通过 CWebCtrlInterFace 类的 GetContextMenuMode()SetContextMenuMode() 方法在运行时控制。每种定制模式实际上都演示了一个如何定制 MSHTML 快捷菜单的独立示例,并且可以具有以下值之一:

kDefaultMenuSupport MSHTML 照常显示其上下文菜单。
kNoContextMenu 所有 MSHTML 上下文菜单都被抑制。
kTextSelectionOnly 除文本选择 MSHTML 上下文菜单外,所有菜单均被抑制。
kAllowAllButViewSource 默认 MSHTML 上下文菜单的“查看源代码”菜单项被移除。
kCustomMenuSupport 我们自己的自定义弹出菜单替换了默认的 MSHTML 上下文菜单。

5.3. “kDefaultMenuSupport”和“kNoContextMenu”模式的实现。

kDefaultMenuSupportkNoContextMenu 模式最容易实现。通过在 ShowContextMenu() 接口方法中返回 S_OK,应用程序表示上下文菜单的请求已由 IDocHostUIHandler 接口实例成功处理,MSHTML 不应再对其执行任何操作。因此,kNoContextMenu 模式通过返回 S_OK 实现,而 kDefaultMenuSupport 模式通过回退到默认 MSHTML 行为实现(有关详细信息,请参阅 [p4.3])。ShowContextMenu() 接口方法的实现可以在 清单#3WebCtrlInterFace.cpp 源文件中找到。

5.4. kTextSelectionOnly 模式的实现。

kTextSelectionOnly 模式的实现可以被视为 kDefaultMenuSupportkNoContextMenu 实现的组合。如果 ShowContextMenu() 接口方法的 dwID 输入参数等于 CONTEXT_MENU_TEXTSELECT 常量,则代码回退到默认 MSHTML 行为(有关详细信息,请参阅 [p4.3]),并显示 MSHTML 文本选择上下文菜单。否则,返回值设置为 S_OK,不显示任何上下文菜单。实现 ShowContextMenu() 接口方法的代码可以在 清单#3WebCtrlInterFace.cpp 源文件中找到。

5.5. kCustomMenuSupport 模式的实现。

在此定制模式下,默认的 MSHTML 快捷菜单被自定义菜单替换,而所有其他快捷菜单保持不变。也就是说,如果 ShowContextMenu() 接口方法的 dwID 输入参数等于 CONTEXT_MENU_DEFAULT 常量,则调用 CustomContextMenu() 函数并显示我们的自定义菜单。否则,代码将回退到默认的 MSHTML 行为(有关详细信息,请参阅 [p4.3])。

CustomContextMenu() 函数加载自定义菜单,然后使用 TrackPopupMenu() [17] Win32 SDK 调用来显示它,其方式与我们传统上显示快捷菜单的方式相同。[16] 我的自定义菜单有意借用了 MSHTML 快捷菜单中的项,并且菜单项标识符的值等于在 mshtmcid.h 头文件中定义的相应 MSHTML 命令 ID。调用 TrackPopupMenu() 函数时设置了 TPM_RETURNCMD 标志,以便在菜单跟踪完成后返回选定菜单项的命令 ID。然后发送一个 WM_COMMAND 消息,将此命令 ID 作为第一个消息参数 (wParam) 传递,并执行该命令。

TrackPopupMenu() 函数将所有者窗口的句柄和快捷菜单在屏幕坐标中的位置作为参数。为了发送执行所选命令的 WM_COMMAND 消息,还需要窗口句柄。因此,CustomContextMenu() 函数必须将 ShowContextMenu() 接口方法的 pptpcmdtReserved 输入参数作为输入参数。[15] ppt 参数提供菜单的屏幕坐标,而 pcmdtReserved 参数可用于获取 IOleWindow 接口指针,该指针可以通过其 GetWindow() 接口方法提供所需的窗口句柄。实现 CustomContextMenu() 函数的代码可以在 清单#4WebCtrlInterFace.cpp 源文件中找到。

5.6. kAllowAllButViewSource 模式的实现。

kAllowAllButViewSource 定制模式与 kCustomMenuSupport 定制模式非常相似。主要区别在于,我修改了默认的 MSHTML 快捷菜单,而不是显示自定义弹出菜单,然后像任何其他快捷菜单一样显示它。[16] 同样,如果 ShowContextMenu()dwID 输入参数等于 CONTEXT_MENU_DEFAULT 常量,则调用 ModifyContextMenu() 函数,否则代码将回退到默认的 MSHTML 行为(有关详细信息,请参阅 [p4.3])。

ModifyContextMenu() 函数源自“WebBrowser 定制”MSDN 文章 [10] 中的 IDocHostUIHandler::ShowContextMenu() 接口方法,与原始代码只有细微差别。它的任务是从默认的 MSHTML 快捷菜单中移除“查看源代码”菜单项,然后适当地显示菜单。除了一些处理语言子菜单和快捷菜单扩展的代码(这些不属于本文的范围)之外,ModifyContextMenu() 实际上实现了我们已经在 [p5.1] 中讨论过的内容。

ModifyContextMenu() 接受 dwIDpptpcmdtReserved 作为 ShowContextMenu() 接口方法的输入参数。[15] pcmdtReserved 输入参数用于获取 IOleWindow 接口指针,然后从中获取快捷菜单所有者窗口的句柄。另一方面,dwID 参数表示 MSHTML 快捷菜单在 Shdoclc.dll 系统文件的 #24641 菜单资源中的零基相对位置。因此,dwID 的值用于通过 GetSubMenu() [18] Win32 SDK 函数获取默认 MSHTML 快捷菜单的菜单句柄。之后,菜单句柄与 IDM_VIEWSOURCE 命令 ID 一起传递给 DeleteMenu() [19] Win32 SDK 函数,该函数移除快捷菜单的“查看源”菜单项。菜单句柄、所有者窗口的句柄以及 ppt 输入参数的屏幕坐标都传递给 TrackPopupMenu() [17] Win32 SDK 函数,该函数显示快捷菜单。

ModifyContextMenu() 的最后任务是确保 WebBrowser 控件在菜单跟踪完成后通过向所有者窗口发送适当的 WM_COMMAND 消息来正确响应快捷菜单选择。具体来说,TrackPopupMenu() 被调用时设置了 TPM_RETURNCMD 标志,以便返回所选菜单项的命令 ID。如果菜单跟踪成功结束,此命令 ID 将作为 ModifyContextMenu() 发送的 WM_COMMAND 消息的第一个消息参数 (wParam)。

最后一个细节是,在 ModifyContextMenu() 函数的实现中 清单#5,我还删除了一个神秘的 IDM_EXTRA_ITEM 菜单项。这只是一种快速(而且粗糙?)的方法,用于消除在删除“查看源代码”菜单项时出现的额外菜单分隔符。我对这种特殊的 hack 并不感到自豪,我正在寻找更好的方法,但到目前为止我还没有成功(有什么想法吗?)。实现 ModifyContextMenu() 函数的代码可以在 清单#5WebCtrlInterFace.cpp 源文件中找到。

6. 示例应用程序。

本文还附带了一个可随时构建的 VC++ 示例项目。该示例项目构建了一个简单的应用程序,演示了通过 IDocHostUIHandler 接口定制 WebBrowser 控件上下文菜单。在示例应用程序中,您可以通过“模式”菜单在不同类型的定制之间进行选择,从而在运行时更改上下文菜单行为。(请参阅文章顶部的屏幕截图。)当然,您也可以修改该应用程序,或使用调试器对其进行研究,以找到本文未讨论的技术问题的答案。

在这个示例应用程序中,我更喜欢使用 CHtmlView MFC 类,[21] 而不是直接托管 WebBrowser 控件。CHtmlView 在 MFC 的文档/视图架构上下文中提供了 WebBrowser 控件的功能,并且通常比 WebBrowser 控件本身更方便使用。下面我将逐步描述这个示例应用程序是如何创建的,以及 CWebCtrlInterFace 实例应该如何配置和安装:

  • 步骤 1:创建一个典型的 Web 浏览器样式应用程序。

    使用 MFC 应用程序向导创建一个典型的“Web 浏览器样式 MFC 应用程序”。[20] 为简单起见,最好创建一个 SDI 应用程序。

  • 步骤 2:创建和释放 CWebCtrlInterFace 实例。

    CWebBrowserView 类的构造函数创建一个 CWebCtrlInterFace 实例,该实例随后存储在 m_iClientSite 成员变量中。当 CWebBrowserView 窗口收到 WM_NCDESTROY 消息时,此 m_iClientSite 实例将被释放。构造函数还会检查 Shdoclc.dll 系统文件是否存在,并相应地更新 m_SHDOCLC_DLL_Found 成员变量。清单#6

  • 步骤 3:安装新的 IOleClientSiteIDocHostUIHandler 实现接口。

    CWebBrowserView 类的 OnInitialUpdate() 方法获取 WebBrowser 控件当前的 IOleClientSite 接口,并使其成为 m_iClientSite 实例的默认 IOleClientSite 接口。[p.4.3] 然后 OnInitialUpdate() 通过将 m_iClientSite 实例设置为 WebBrowser 控件的当前 IOleClientSite 接口 [p.4] 来安装新接口。清单#7

  • 步骤 4:创建“模式”菜单。

    “模式”菜单通过菜单编辑器添加到应用程序菜单中。“模式”菜单项对应于 CWebCtrlInterFace 定制模式 [p.5.2],可以在文章顶部的屏幕截图中看到。相应的命令处理程序,包括 ON_COMMANDON_UPDATE_COMMAND_UI 处理程序类型,也已添加到 CWebBrowserView 类中。(使用旧的 ClassWizard 或新的 Event Handler Wizard。)

  • 步骤 5:实现“模式”菜单行为。

    控制“模式”菜单行为的命令处理程序的实现非常简单。ON_COMMAND 处理程序的实现简单地调用 m_iClientSite 实例的 SetContextMenuMode() 方法来设置适当的 CWebCtrlInterFace 定制模式。另一方面,ON_UPDATE_COMMAND_UI 处理程序的实现通过 CWebCtrlInterFace 类的 GetContextMenuMode() 方法获取当前的定制模式,然后将适当的菜单项标记为已选中。此实现的示例在 清单#8 中演示。

7. 代码清单。

7.1. 清单 #1。

HRESULT STDMETHODCALLTYPE CWebCtrlInterFace::QueryInterface(REFIID riid, 
                                                              LPVOID *ppv) 
{
HRESULT result = S_OK;

    // Always set out parameter to NULL, validating it first 
    if (IsBadWritePtr(ppv, sizeof(LPVOID))) 
        result = E_INVALIDARG;  

    if (result == S_OK)
    {
        *ppv = NULL; 
 
        if ( IsEqualIID( riid, IID_IUnknown ) )
            *ppv = this;
        else if ( IsEqualIID( riid, IID_IOleClientSite ) )
            *ppv = (IOleClientSite *) this;
        else if ( IsEqualIID( riid, IID_IDocHostUIHandler ) )
            *ppv = (IDocHostUIHandler *) this;
        else
            result = E_NOINTERFACE;
    }

    if (result == S_OK)
        this->AddRef(); 
 
    return result; 
}

ULONG STDMETHODCALLTYPE CWebCtrlInterFace::AddRef() 
{    
    InterlockedIncrement(&m_cRef); 
    return m_cRef; 
} 
 
ULONG STDMETHODCALLTYPE CWebCtrlInterFace::Release() 
{ 
    // Decrement the object's internal counter 
    ULONG ulRefCount = InterlockedDecrement(&m_cRef); 
 
    if (0 == m_cRef) 
    {
        delete this; 
    }
 
    return ulRefCount; 
}

7.2. 清单 #2。

VOID CWebCtrlInterFace::SetDefaultClientSite(IOleClientSite *pClientSite)
{
    if (pClientSite != NULL)
    {
        pClientSite->AddRef();

        m_defaultClientSite = pClientSite;
        m_defaultClientSite->QueryInterface(IID_IDocHostUIHandler, 
                                       (VOID **)&m_defaultDocHostUIHandler);
    }
    else
    {
        if (m_defaultClientSite != NULL)
        {
            m_defaultClientSite->Release();
            m_defaultClientSite = NULL;
        }

        if (m_defaultDocHostUIHandler != NULL)
        {
            m_defaultDocHostUIHandler->Release();
            m_defaultDocHostUIHandler = NULL;
        }
    }
}

7.3. 清单 #3。

HRESULT STDMETHODCALLTYPE CWebCtrlInterFace::ShowContextMenu(DWORD dwID, 
             POINT *ppt, IUnknown *pcmdtReserved, IDispatch *pdispReserved)
{
HRESULT result    = S_FALSE; //Dont Interfere
BOOL    handled   = FALSE;

    switch ( m_contextMenuMode )
    {
        case kDefaultMenuSupport:
            break;

        case kNoContextMenu:
            result    = S_OK;
            handled   = TRUE;
            break;

        case kTextSelectionOnly:
            if (dwID != CONTEXT_MENU_TEXTSELECT)
            {
                result    = S_OK;
                handled   = TRUE;
            }
            break;

        case kAllowAllButViewSource:
            if (dwID == CONTEXT_MENU_DEFAULT)
            {
                result    = ModifyContextMenu(dwID, ppt, pcmdtReserved);
                handled   = TRUE;
            }
            break;

        case kCustomMenuSupport:
            if (dwID == CONTEXT_MENU_DEFAULT)
            {
                result    = CustomContextMenu(ppt, pcmdtReserved);
                handled   = TRUE;
            }
            break;
    }

    if (! handled)
    {
        if (m_defaultDocHostUIHandler != NULL)
            result = m_defaultDocHostUIHandler->ShowContextMenu(dwID, ppt, 
                    pcmdtReserved, pdispReserved);
        else
            result = S_FALSE;
    }

    return result;
}

7.4. 清单 #4。

HRESULT CustomContextMenu(POINT *ppt, IUnknown *pcmdtReserved) 
{
    IOleWindow *oleWnd      = NULL;
    HWND       hwnd         = NULL;
    HMENU      hMainMenu    = NULL;
    HMENU      hPopupMenu   = NULL;
    HRESULT    hr           = 0;
    INT        iSelection   = 0;

    if ((ppt == NULL) || (pcmdtReserved == NULL))
        goto error;

    hr = pcmdtReserved->QueryInterface(IID_IOleWindow, (void**)&oleWnd);
    if ( (hr != S_OK) || (oleWnd == NULL))
        goto error;

    hr = oleWnd->GetWindow(&hwnd);
    if ( (hr != S_OK) || (hwnd == NULL))
        goto error;

    hMainMenu = LoadMenu(AfxGetInstanceHandle(),
                     MAKEINTRESOURCE(IDR_CUSTOM_POPUP));
    if (hMainMenu == NULL)
        goto error;

    hPopupMenu = GetSubMenu(hMainMenu, 0);
    if (hPopupMenu == NULL)
        goto error;

    // Show shortcut menu
    iSelection = ::TrackPopupMenu(hPopupMenu,
                        TPM_LEFTALIGN | TPM_RIGHTBUTTON | TPM_RETURNCMD,
                        ppt->x,
                        ppt->y,
                        0,
                        hwnd,
                        (RECT*)NULL);

    // Send selected shortcut menu item command to shell
    if (iSelection != 0)
        (void) ::SendMessage(hwnd, WM_COMMAND, iSelection, NULL);

error:

    if (hMainMenu != NULL)
        ::DestroyMenu(hMainMenu);

    return S_OK;
}

7.5. 清单 #5。

HRESULT ModifyContextMenu(DWORD dwID, POINT *ppt, IUnknown *pcmdtReserved) 
{
    //#define IDR_BROWSE_CONTEXT_MENU  24641
    //#define IDR_FORM_CONTEXT_MENU    24640
    #define SHDVID_GETMIMECSETMENU   27
    #define SHDVID_ADDMENUEXTENSIONS 53

    HRESULT hr;
    HINSTANCE hinstSHDOCLC;
    HWND hwnd;
    HMENU hMenu;
    IOleCommandTarget *spCT;
    IOleWindow *spWnd;
    MENUITEMINFO mii = {0};
    VARIANT var, var1, var2;

    hr = pcmdtReserved->QueryInterface(IID_IOleCommandTarget, 
                                                (void**)&spCT);
    hr = pcmdtReserved->QueryInterface(IID_IOleWindow, (void**)&spWnd);
    hr = spWnd->GetWindow(&hwnd);

    hinstSHDOCLC = LoadLibrary(TEXT("SHDOCLC.DLL"));

    hMenu = LoadMenu(hinstSHDOCLC,
                     MAKEINTRESOURCE(IDR_BROWSE_CONTEXT_MENU));

    hMenu = GetSubMenu(hMenu, dwID);

    // Get the language submenu
    hr = spCT->Exec(&CGID_ShellDocView, 
           SHDVID_GETMIMECSETMENU, 0, NULL, &var);

    mii.cbSize = sizeof(mii);
    mii.fMask  = MIIM_SUBMENU;
    mii.hSubMenu = (HMENU) var.byref;

    // Add language submenu to Encoding context item
    SetMenuItemInfo(hMenu, IDM_LANGUAGE, FALSE, &mii);

    // Insert Shortcut Menu Extensions from registry
    V_VT(&var1) = VT_PTR;
    V_BYREF(&var1) = hMenu;

    V_VT(&var2) = VT_I4;
    V_I4(&var2) = dwID;

    hr = spCT->Exec(&CGID_ShellDocView, 
           SHDVID_ADDMENUEXTENSIONS, 0, &var1, &var2);

    // Remove View Source
    ::DeleteMenu(hMenu, IDM_VIEWSOURCE, MF_BYCOMMAND);
    // Remove the item that produces the exta separator
    ::DeleteMenu(hMenu, IDM_EXTRA_ITEM, MF_BYCOMMAND);

    // Show shortcut menu
    int iSelection = ::TrackPopupMenu(hMenu,
                          TPM_LEFTALIGN | TPM_RIGHTBUTTON | TPM_RETURNCMD,
                          ppt->x,
                          ppt->y,
                          0,
                          hwnd,
                          (RECT*)NULL);

    // Send selected shortcut menu item command to shell
    if (iSelection != 0)
        LRESULT lr = ::SendMessage(hwnd, WM_COMMAND, iSelection, NULL);

    FreeLibrary(hinstSHDOCLC);
    return S_OK;
}

7.6. 清单 #6。

CWebBrowserView::CWebBrowserView()
{
    m_iClientSite = new CWebCtrlInterFace;
    
    if (m_iClientSite != NULL)
        m_iClientSite->AddRef();

    {
        HINSTANCE tstInstance = LoadLibrary(TEXT("SHDOCLC.DLL"));

        m_SHDOCLC_DLL_Found = (tstInstance != NULL);

        if (! m_SHDOCLC_DLL_Found)
        {
            ::AfxMessageBox(_T("Cannot Load Library SHDOCLC.DLL.\n"
                "The \"No View Source Choice\" mode will not work."));
        }

        FreeLibrary(tstInstance);
    }
}

void CWebBrowserView::OnNcDestroy()
{
    if (m_iClientSite != NULL)
    {
        m_iClientSite->Release();
        m_iClientSite = NULL;
    }

    CHtmlView::OnNcDestroy();
}

7.7. 清单 #7。

void CWebBrowserView::OnInitialUpdate()
{
IOleObject *pIOleObj = NULL;

    CHtmlView::OnInitialUpdate();

    if (m_iClientSite != NULL)
    {
        if (m_pBrowserApp != NULL)
            m_pBrowserApp->QueryInterface(IID_IOleObject, 
                                      (void**)&pIOleObj);

        if (pIOleObj != NULL)
        {
        IOleClientSite *oldClientSite = NULL;

            if (pIOleObj->GetClientSite(&oldClientSite) == S_OK)
            {
                m_iClientSite->SetDefaultClientSite(oldClientSite);
                oldClientSite->Release();
            }

            pIOleObj->SetClientSite(m_iClientSite);
        }
    }

    Navigate2(_T("https://codeproject.org.cn/") ,NULL,NULL);
}

7.8. 清单 #8。

void CWebBrowserView::On_CMM_NoContextMenu() 
{
    if (m_iClientSite != NULL)
        m_iClientSite->SetContextMenuMode(kNoContextMenu);
}

void CWebBrowserView::OnUpdate_CMM_NoContextMenu(CCmdUI* pCmdUI) 
{
    if (pCmdUI != NULL)
    {
        if (m_iClientSite != NULL)
        {
            pCmdUI->Enable(TRUE);
            pCmdUI->SetCheck(m_iClientSite->GetContextMenuMode() 
                                                      == kNoContextMenu);
        }
        else
        {
            pCmdUI->Enable(FALSE);
            pCmdUI->SetCheck(FALSE);
        }
    }
}

8. 参考文献。

  1. WebBrowser 控件概述和教程 (MSDN)
  2. 重用 WebBrowser 控件 (MSDN)
  3. WebBrowser 对象 (MSDN)
  4. WebBrowser 定制 (MSDN)
  5. 关于 MSHTML (MSDN)
  6. 重用 MSHTML (MSDN)
  7. MSHTML 参考 (MSDN)
  8. WebBrowser 定制,段落:WebBrowser 定制架构 (MSDN)
  9. WebBrowser 定制,段落:工作原理 (MSDN)
  10. WebBrowser 定制,段落:IDocHostUIHandler::ShowContextMenu (MSDN)
  11. 在 C++ 中实现 IUnknown (MSDN)
  12. IUnknown 接口 (MSDN)
  13. IOleClientSite 接口 (MSDN)
  14. IDocHostUIHandler 接口 (MSDN)
  15. IDocHostUIHandler::ShowContextMenu (MSDN)
  16. 使用菜单,段落:显示快捷菜单 (MSDN)
  17. TrackPopupMenu 函数 (MSDN)
  18. GetSubMenu 函数 (MSDN)
  19. DeleteMenu 函数 (MSDN)
  20. 创建 Web 浏览器样式 MFC 应用程序 (MSDN)
  21. CHtmlView 类 (MSDN)
  22. 如何修改/删除 IE WebBrowser 控件显示的上下文菜单 (Code Project)
© . All rights reserved.