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

在 Windows 窗体中托管网页

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (33投票s)

2003 年 5 月 19 日

11分钟阅读

viewsIcon

360519

downloadIcon

7242

使用 Internet Explorer / MSHTML 托管接口来嵌入和操作 WebBrowser 控件。

Sample Image - winformiehost.jpg

引言

本文讨论了我实现将网页运行在 WinForm 客户端内部并向其触发事件的过程。随附的示例应用程序提供了本文的实现。虽然示例应用程序也可以用更简单的方式完成(例如,全部使用 ASP.NET),但目的是提供一个可用于实现更复杂设计的可运行模型。

应用程序

该应用程序由一个 WinForm 应用程序组成,该应用程序分为两部分:

  1. 右侧是 Web 应用程序
  2. 左侧是根据 Web 中单击的内容填充的 WinForm 组件。

该应用程序实现了 IDocHostUIHandler 接口,该接口允许 MSHTML 回调我的代码。然后,它在 IE 容器中显示网页。当单击一个按钮时,将调用一个 JavaScript 组件,并向其传递单击内容的文本描述。JavaScript 将此数据传递给外部方法,该方法将使用数据填充窗体。如果找不到数据,则购买按钮将被禁用。

要使应用程序在您的计算机上正常运行,请修改 BIN\Release 目录中的应用程序配置文件 (SampleEventProgram.exe.config),并更改指向您的计算机上示例 Web 应用程序位置的路径。

研究

由于我不是一个纯粹的 C++ COM 开发人员,甚至对 IE 引擎的内部原理了解不多,所以我开始通过在 Internet 上进行研究来部署。第一天,我搜索了有关在窗体中嵌入 IE 的所有内容。我之所以提到这一点,是因为它引导我找到了一个似乎是博客的网页。我出于好奇阅读了它,想看看为什么它会出现在我的搜索结果中。在该博客中,作者说:

“在我成长的开发生涯中,我曾遇到过几个未得到解答的问题。[...]为什么在 Windows 窗体中实现浏览器并控制它如此困难?”

我可能应该把它当作一个警告,但我还是继续追求我的目标。毕竟,微软已经有了多年的改进这个过程……对吧?在这里 CodeProject,我偶然发现了一个已经过时的讨论串,没有得到任何明确的帮助,还有 Wiley Technology Publishing 和 Nikhil Dabas 的文章。第一篇文章写得很好,但文章中最重要的一部分(实现 IDocHostUIHandlerICustomDoc)已经离线,并且是用 Delphi 实现的!然而,Nikhil 的文章很好地讨论了实现该接口,并在其示例应用程序中为接口部署了一个 DLL!

但是,他的事件挂钩部署要求您了解 Web 窗体上的特定控件,然后才能挂接它们的单击事件。它也不允许 Web 应用程序将任何信息发送回 WinForm 客户端。这对于让单击事件直接进入代码来说非常有用。但我需要 HTML 对象来告诉我有关单击内容的一些信息。因此,虽然我最终实现了 IDocHostUIHandler,但我仍然没有完成最后一步并使其正常工作。我在“对象为空或不是对象”的持续结果中卡了数周。

我得到了一些提示,例如查看 GetExternal,并且我发誓有一个帖子建议在我的 JavaScript 中使用 window.getExternal。显然,这并没有让我走多远,因为后来我了解到这不是一个有效的 JavaScript 调用。我还得到了一些关于实现 IDispatch 的建议。但似乎没有什么能真正完成脚本化我的程序的最后一步。

与 CP 会员 .S.Rod. 进行的长达两天的讨论最终使我对一切有了更好的理解,并得到了极大的帮助,将所有内容联系起来并使其正常工作。所有这些研究中最有趣的是,我与大约四个人进行了交谈,并得到了四种不同的实现方法。我相信在这些方法中的每一种中,讨论中的那个人都有最终适合他们的方法。不幸的是,直到我最后一次讨论,我才得到了一个让我摆脱了空对象问题的解决方案。

所有这些研究的唯一缺点是,我发现我偶尔会通过从几个人那里获取输入,然后将它们组合在一起,并与已完成的工作发生冲突而“伤害”自己。更糟糕的是,在此过程中我换了一台新电脑,花了整整两天时间才把所有东西恢复正常!就在我准备暂时放弃这个项目的时候,.S.Rod. 好心地帮我把所有东西都整合起来了。这是最终结果,以及一个示例应用程序,以帮助指导他人完成控制 IE 的过程。

代码

在这个应用程序中,我将有一个网页,用于显示产品目录的按钮和图形。单击网页上的按钮将使用描述填充窗体并激活购买按钮。单击 WinForm 中的购买按钮将向您家门口的 Lefty Larduchi 收钱。我的第一步只是构建网页(纯 HTML 和 JavaScript),并使其能够显示内容。

创建窗体不成问题。只需启动一个新的 C# Windows 窗体项目,自定义工具栏,然后从 COM 部分添加 Internet Explorer。窗体由一个停靠在左侧的面板、一个滑块、一个停靠在右侧的 IE,以及位于面板内的两个文本框和一个按钮组成。

现在,控制 IE 的第一步之一是实现 IDocHostUIHandler。Nikhil 写了一篇关于如何实现的优秀文章,所以我不会重复他的工作。您可以 在此处 了解第一步。

请务必保留他示例应用程序中的 MSHtmHstInterop.dll 部分。我使用示例应用程序将基本的 IDocHostUIHandler 实现复制并粘贴到我的窗体中。

因此,在实现 IDocHostUIHandler 之后,还需要做什么?嗯,在 Nikhil 的文章中,他的例子会要求您知道将被单击的控件,并且需要有人单击该控件。这是完成此任务的代码:

private void WebBrowser_DocumentComplete(object sender,
    AxSHDocVw.DWebBrowserEvents2_DocumentCompleteEvent e)
{
  IHTMLDocument2 doc = (IHTMLDocument2)this.WebBrowser.Document;
  HTMLButtonElement button = (HTMLButtonElement)
    doc.all.item("theButton", null);
  ((HTMLButtonElementEvents2_Event)button).onclick += new
    HTMLButtonElementEvents2_onclickEventHandler(this.Button_onclick);
}

我不得不面对一个应用程序需求,即我们要显示主要部分,每个部分都是 DHTML,每个部分都必须向我提供有关自身的信息,然后由 WinForm 根据该信息采取行动。我发现,在我阅读的关于此主题的无数文章中,Outlook 部署了这种 WinForm/IE 融合,只是不在 .NET 中!

在这个示例中,我们使用 JavaScript 对象 window.external 与窗体进行交互。因此,当用户单击一个部分时,它将触发脚本区域中的一个方法。该方法通过 window.external 调用 MSHTML 到 IDocHostUIHandler.GetExternal 方法,然后使用 IDispatch 方法获取方法的地址并调用它。下一节引自与 .S.Rod. 的讨论。我无法更好地描述它:

  1. 任何愿意实现自定义菜单或外部方法调用的用户都应注册一个自定义站点处理程序。这就是通过 ICustomDoc.SetUIHandler(object) 方法调用完成的。
  2. 传递的对象引用必须实现 IDocHostUIHandler 接口,这是一个基于 IUnknown 的接口。其中有一个方法用作所有 window.external.mymethod() 调用的入口点,那就是 IDocHostUIHandler.GetExternal(out object ppDispatch)
  3. GetExternal 方法应返回一个实现调度接口的对象引用。如果您不知道,调度接口是一个标准的自动化接口,它提供通过其名称调用方法的功能,这要归功于两个辅助方法:GetIdOfName()Invoke()
  4. 好消息是 .NET Framework 提供了此属性:

    [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]

    它实现了所有底层机制。剩下要做的就是声明和实现实际的方法。

最终,我们有一个示例 HTML 文件,它通过调用 JavaScript 的 window.external.MyMethod() 来响应单击。为了使此工作正常进行,必须声明上述对象并实现 MyMethod() 方法。在示例应用程序中,该方法名称是:

public void PopulateWindow(string selectedPageItem).

此时需要注意的是,任何与 COM 级别交互的方法都应定义为始终返回 void。如果需要返回数据,则通过参数完成,并将返回参数标记为 out。如果需要返回错误,例如,则通过设置 HRESULT 来完成,使用 System.Runtime.InteropServices。在 C# 中设置 HRESULT 的方法是执行一个

throw new ComException("", returnValue)

returnValue 是您类中的某个位置定义的 int 值,并设置为您要引发的值。

在示例应用程序中,通过 IDispatch 公开对象的第一个步骤是创建自定义接口:

[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
interface void ICallUIHandler
{
  void PopulateWindow(string selectedPageItem)
}

然后,我们在类定义中实现该接口:

public class PopulateClass:IPopulateWindow
{
  SampleEventProgram.Form1 myOwner;

  /// <SUMMARY>
  /// Requires a handle to the owning form
  /// </SUMMARY>
  public PopulateClass(SampleEventProgram.Form1 ownerForm)
  {
    myOwner = ownerForm;
  }

  /// <SUMMARY>
  /// Looks up the string passed and populates the form
  /// </SUMMARY>
  public void PopulateWindow(string itemSelected)
  {
    // insert logic here
  }
}

因此,我们在这里所做的是创建了一个暴露 IDispatch 的接口,我们在 PopulateClass 类定义中实现了该接口,并在构造函数逻辑中接受了指向我们窗体的指针。这允许我们访问我们选择的特定字段。我需要该类能够更改两个文本框并启用按钮。因此,我必须进入窗体代码并将这三个项的定义从 private 改为 public。因此,在这些定义中,我做了以下连接:

  1. 我的接口绑定到 IDispatch
  2. 我的类绑定到我的接口。
  3. 我的类绑定到我的窗体,充当 MSHTML 世界和 C# .NET 世界之间的桥梁。

最后,我必须实现将我的 Web 窗体连接到上面定义的类的最后一部分代码。在 IDocHostUIHandler.GetExternal 的实现中,我需要将传递的对象设置为我类的实例。在实现 IDocHostUIHandler 时,您应该从 Nikhil 的示例应用程序中获取实现并将其复制/粘贴到您的程序中。修改必要的实现如下:

void IDocHostUIHandler.GetExternal(out object ppDispatch)
{
  ppDispatch = new PopulateClass(this);
}

这现在将您的类连接到 mshtml 的 window.external 部分,将窗体连接到新的类定义,并使所有内容都准备好进行处理。类实现基本上充当 System.Windows.Forms.FormMicrosoft.MSHTML 世界以及您的 Web 窗体之间的中间人。最后一步——在我编写 PopulateWindow 方法中的代码之前——是选择我希望我的类访问哪些字段,并将它们的定义从 private 更改为 public,或者遵循更好的编码标准——为这些字段添加公共访问器。在此示例中,我公开了通过公共访问器更改的各种元素。

结论

现在我有一个正在工作的应用程序以及一个正在工作的示例应用程序,我不禁要问为什么需要这么长时间才能将所有这些信息汇集在一起。但现在,它就在这里。在示例应用程序中:

  1. WinForm 加载,调用构造函数。
  2. 发生 InitializeComponents。这会加载 WebBrowser 控件。
  3. WebBrowser 加载了 about:blank,这将初始化浏览器的文档对象部分。
  4. 文档初始化后,我现在可以通过 ICustomDoc 接口实现 IDocHostUIHandler
  5. 最后加载 HTML 页面。

当 HTML 按钮被单击时,它会调用 CallHostUI 方法,并将单击的项目名称传递给它。

  1. CallHostUI 脚本调用 window.external.PopulateWindow(),传递每个按钮发送的文本。
  2. window.external 通过 MSHTML 调用 IDocHostUIHandler.GetExternal,并设置为对象的实例。
  3. 设置实例的逻辑还将表单的引用传递给它。接下来,它使用 IDispatch 来发现 PopulateClass 方法及其位置。
  4. 调用该方法,表单的引用使类能够修改字段并启用按钮。

在所有这些工作都完成后,我应该添加一个警告。我发现 Visual Designer 代码不期望您在窗体定义之前有接口和类定义。结果是,如果您添加一个控件或修改一个控件,它在视觉上似乎有效,但您的代码实际上没有发生任何更改,并且在关闭并重新打开项目后,更改会消失。更令人沮丧的是,当您添加事件处理程序时:您会获得委托的绑定,但没有实际的基础方法实现。幸运的是,要解决此问题,只需将您的接口和类移动到源代码的底部即可。

这可以提供非常丰富的客户端演示文稿以及用于处理数据的丰富 WinForm 元素。在我这个特定的例子中,我公开了为我们内部 Web UI 演示引擎开发的网页。当 Web 页面内的每个部分被鼠标悬停时,该部分会以亮黄色边框突出显示。单击该部分会将该信息传递给我的 WinForm,该 WinForm 将该部分显示在属性页中。框架中的各种内置编辑器以及我们编写的自定义编辑器将挂接到该属性页,以允许简单地修改数据。例如,更改单元格元素中的颜色会弹出颜色选择器编辑器,更改字体会弹出字体选择器编辑器。

© . All rights reserved.