浏览器集成教学 在你的应用程序中集成WebBrowser控件

类别:VC语言 点击:0 评论:0 推荐:

    我把这篇译文作为献给我母亲的生日礼物——虽然我不愿意提醒她又老了一岁。她在我走向程序员的过程有起着极其重要的影响。蒋晟 2003年6月11日

MSDN 主页>  Microsoft Internet Developer > 1998年7月 > 在你的应用程序中集成WebBrowser控件


本文假定你熟悉Internet Explorer 4.0、 C++和 COM。
下载代码 (154KB)
在你的应用程序中集成WebBrowser控件
Michael Heydt

Microsoft Internet Explorer 4.0 实际上是一个作为WebBrowser 控件的宿主外壳应用程序。你的程序可以用一点COM技术做到同样的事。

    偶尔我被问起我是如何在开发的程序中使用HTML的。显然,要是有个可以产生丰富的显示内容的HTML解释器的话,我的C++应用程序会很简洁。不幸的是,我太忙于编写的东西在各种方面的结果和作为基于一个组件的浏览器一样。

   好吧,Microsoft® Internet Explorer 4.0正是我所要的。4.0版本增加了一些扩展,允许应用程序作为Internet Explorer WebBrowser控件的主体,和底层的DHTML交互,甚至扩展DHTML对象模型。呜呼!

    大约在去年,我在写的几种程序中用了WebBrowser控件显示HTML。为这些应用程序加入这个控件比较简单,而且提供了一个很好的特性。但是我不能够设计我自己的数据输入表单,并且接受用户的数据输入。对于所有的意图和用途,WebBrowser控件只能用于显示。

    在我的总公司,我喜欢用Outlook™ Express作为我的Internet邮件客户端。它是轻量级的,并且有一个很好的用户界面。有趣的是,这个应用程序的界面看起来像是用WebBrowser控件中 集成HTML来实现的(参见图1)。我发现Outlook Express对于解释和显示email信息和新闻组投递的HTML部分是很有用的 ,这是当时其他产品不具备的一个特性。

    Microsoft Outlook 98 最近可以下载到了。我已经听说Outlook 98 和Outlook Express一样,可以编辑和解释HTML格式的邮件。所以我好像花了好几天用我的28.8 Kbps modem连接来下载它的Beta版本,并且我没有失望。Outlook 98客户端支持HTML格式的邮件,还具备Outlook Express引入的信纸功能。在我看来,Outlook 98好像 使用HTML来表示输入和显示消息的面板。 图2显示Microsoft Outlook 98的发行版,显示了一些看起来令人怀疑是HTML的表单。如果这里真用的是HTML的话,应用程序显然可以接受用户输入的数据。


华生医生,来吧!

(译者注:在破案方面,华生总是比不上福尔摩斯,但要说到治病,华生应该是个不错的医生,要不Microsoft 怎么从Windows 3.1 起就请他来照顾Windows。Dr. Watson 是系统错误时的诊断工具,它可截获软件错误,识别发生错误的软件并提供详细的原因说明。我最欣赏它的地方是,Dr. Watson 能诊断出问题并提出一些建议给你去解决错误,有时候觉得它还真象个大夫。)

    幸运的是,有一个判断应用程序中正在做什么的相对简单的方法spyxx.exe(也被称作Spy++)。以我的观点,Spy++工具是有史以来Windows®平台上创建的最有用的工具之一。它提供给你的偷看应用程序的实现方法的信息是无价之宝。我启动 了Spy++,并且用它监视几个应用程序。

    我监视的第一个程序是Outlook Express。图3显示Outlook Express中看起来是HTML主体的窗口正在被Spy++ 窗口查找工具突出显示。这个操作的结果参见图4

    图4的表单暴露了窗口的注册名:Internet Explorer_Server。啊哈!可以肯定Internet Explorer在Outlook Express中被重用了。要归功于勤奋,它引导我研究Outlook 98、Windows 资源管理器(安装了Internet Explorer 4.0 外壳集成),和Internet Explorer 4.0 应用程序本身。567都显示被Spy++侦查的程序,并且都产生同样的结果。在每一个程序中,Spy++都检查到一个矩形窗口,其窗口类名称是Internet Explorer_Server。

    这个结果正是我期望的。Internet Explorer WebBrowser控件在所有这些程序中被用于提供丰富的内容显示。。但是我还是不太确定它确实是WebBrowser控件,所以我整理了一个快速的和脏的应用程序, 集成一个Microsoft® WebBrowser控件 (CLSID_WebBrowser),并且用Spy++侦查这个程序。你瞧,窗口的类名真是Internet Explorer_Server!我得到了我需要的确认。

    能够重用WebBrowser控件来显示HTML只是美好和华丽,但是它还是没有告诉我这些程序如何和HTML,以及用户可能输入的数据交互。从Internet Explorer 3.0开始,你可以连接到一个WebBrowser控件的事件接受器对象,和它产生的一些事件交互(例如BeforeNavigate、 DownloadComplete、和StatusTextChange)。但是控件的3.0版本没有一个方法来访问HTML本身。

    在很多研究之后,我发现这就正好是Internet Explorer 4.0添加的功能的范围,这是通过WebBrowser控件支持的一个新的COM接口IWebBrowser2。这个新的接口允许作为WebBrowser控件主体的应用程序访问底层的DHTML文档对象模型,并且扩展它。

Internet Explorer和DHTML对象模型
    要有效的理解IWebBrowser2接口提供给你的应用程序的特性,你首先需要对Internet Explorer和DHTML两者的对象模型有一个基本的了解。当然,完全解释这些模型可能会占据大量篇幅,因为DHTML文件中每种独立的元素类型(<B>、 <HTML>、 <BODY>等等)都具有代表自己的COM对象类或者接口。

    获得对这些对象模型的彻底的理解的最好方法是看一些书和获得一个工具。Inside Dynamic HTML (Microsoft 出版社,1997)是DHTML对象模型的最好的参考书之一。关于对象模型的更多技术信息可以通过研究Internet Client SDK的DHTML相关头文件(特别是mshtml.h、 expdispid.h、 mshtmhst.h和mshtmdid.h)。这些文件指定各种类或者对象支持的接口。不幸的是,可用的Internet Client SDK中的帮助文档只有中等程度的辅助,他们解释了如何使用IWebBrowser2接口的细节,但是没有提供太多其他相关COM接口——像ICustomDoc和IDocHostUIHandler——的信息。

基本接口用法
    在我讨论如何在你的应用程序中使用Internet Explorer 4.0对象之前,让我们复习一下几个接口。

    CLSID_WebBrowser是所有事物的起点——它是WebBrowser ActiveX® 控件的CLSID。Internet Explorer 4.0实际上只是一个叫做Internet Explorer的这个控件的宿主程序。

    IWebBrowser2接口被WebBrowser控件实现,并且表示的了和这个控件交互的主要方法 。 作为一个遗传下来的接口IWebBrowser的扩展,这个新的接口通过get_Document方法,提供了一种访问底层DHTML文档的手段。

    你对WebBrowser控件中当前显示的DHTML文档的访问和操作目前是通过IHTMLDocument2接口实现。它允许你设置和获得文档的元素,设置各种事件的处理脚本,以及获得显示文档的窗口和框架的接口。

(译者注:很多时候,很可能是为了向后的兼容性,你必须获得一个分发接口,例如通过CHtmlView的GetHtmlDocument方法,或者通过加入到工程的浏览器控件封装类的DOM方法,然后查询IHTMLDocument2接口。MFC7的源代码中直接把分发接口指针强制转换为IHTMLDocument2指针,意味着至少在目前,分发接口指针和IHTMLDocument2指针指向同一个对象,但是微软没有保证说永远会这样,所以比较安全的办法还是调用IUnknown的QueryInterface方法获得你需要的版本的IHtmlDocument*指针,这个方法也同样适用于自己通过WINAPI用CLSID_WebBrowser创建的浏览器窗口。)

    IHTMLWindow2是显示HTML文档的窗口的接口。这个接口实际上代表一个框架的集合,当前的窗口可能是它的一部分。和HTML文档对象类似,它也允许你操作窗口,设置各种事件的处理脚本,以及获得一个事件对象,代表用户和文档和窗口交互的细节。

    IHTMLEventObj是一个接口,代表一个事件对象,包含事件的信息,例如光标位置,按下的键,以及操作选择的任何HTML元素。

    IHTMLElement是一个接口,代表一个HTML文档中的元素。HTML元素大概相当于文档中独立的标签。。它的方法允许你做一些事,比如设置各种事件的处理脚本,以及操作元素的innerHTML和outerHTML属性来更改真正的显示HTML。

    到这里为止,上述所有接口存在于Internet Explorer DHTML对象模型中各种对象上。但是为了让你的应用程序处理这些对象产生的事件,你需要理解一些分发接口。下一步我要讲述的接口都派生自IDispatch,并且是应用程序必须实现的事件处理器,用于从一个对象接受事件。要使用这些处理器对象,你需要对对象请求IConnectionPointContainer接口,在这些中的一个位置注册。只要你有了连接点容器,你就可以在对象和你的事件处理器之间建立连接。当事件发生的时候,对象将向提供的处理器发送事件。你至少要了解下列三个分发接口:

    DIID_DWebBrowser事件2接口允许你监控WebBrowser控件产生的事件。一个特别重要的事件是DISPID_NAVIGATECOMPLETE,通知它的宿主文档完全被WebBrowser控件建立。只要这个事件被触发,IWebBrowser2::get_Document方法就会返回一个指向合法的DHTML文档的接口指针。

    有了DIID_HTMLDocument事件接口,一个应用程序可以接收DHTML文档对象触发的事件,例如onmouseover、 onmousemove、或者onclick。

    第三个接口是DIID_HTMLWindow事件。这个DIID_HTMLWindow事件提供的事件被DHTML对象模型中的窗口触发。 这些事件的例子包含OnLoad、 OnUnload、 OnFocus和OnBlur。

    其他两个重要的接口ICustomDoc和IDocHostUIHandler允许你自定义Internet Explorer用户界面的装饰,以及扩展DHTML对象模型。

    ICustomDoc接口被HTML文档对象实现,允许WebBrowser控件宿主设置它的IDocHostUIHandler接口。通常Internet Explorer将调用宿主的Client Site的QueryInterface方法获得IDocHostUIHandler接口。但是,如果宿主不支持IOleClientSite接口或者为了保存这个指针以避免重复调用QueryInterface,可以通过ICustomDoc显式地设置HTML对象的IDocHostUIHandler接口。

    IDocHostUIHandler 接口可选地被WebBrowser控件的宿主程序实现。通过实现这个接口,宿主程序可以替换Internet Explorer 4.0使用的菜单、工具栏和上下文菜单。当WebBrowser需要访问用户界面(比如窗口大小),或者当它需要解析DHTML窗口对象的External属性的时候,WebBrowser控件将查询宿主程序的IOleClientSite接口的这个接口(除非它已经被显式地设置)。

    控件宿主也可以通过给脚本引擎提供一个代表宿主程序的外部的自动化对象来扩展DHTML对象模型。脚本中对这个外部对象的引用被脚本引擎解析,发动一个对WebBrowser宿主应用程序的IDocHostUIHandler接口的查询。然后脚本引擎调用这个接口的get_External方法,宿主应用程序用这个调用返回一个自动化对象的分发接口。下一步,脚本引擎将通过通常的COM自动化分发方法触发这个对象的方法。
 

(译者注:MFC7中对CHtmlView的增强主要是把这两个接口的实现改成虚函数,并且修复了MFC6中CHtmlView的内存泄漏问题,而且产生了比如Unicode支持之类的新的问题,以及隐藏了大量细节而可能造成的误解。虽然我很赞同这个对应用程序的简洁性大有好处的改进,但是如果把CDHtmlDialog里面大堆的DDX函数也包含在这个类里面会更好些)



重用WebBrowser控件以集成DHTML

    在你的程序中完全实现Internet Explorer集成的万里长征第一步是包含WebBrowser控件。WebBrowser控件是一个ActiveX控件,所以你只需要让你的应用程序能够集成ActiveX组件。关于提供ActiveX控件的容器支持的完整讨论超出了本文的范围,但是你应该很容易找到大量合适的文档。在示例程序中,ActiveX控件容器接口的支持由MFC处理。

(译者注:不管你喜不喜欢MFC,它的源代码都值得一看。查看MFC的源代码有助于了解细节性问题)

    本文的示例实现了一个基于表单的应用程序,提供给用户两个屏幕。第一个表单允许用户输入属性和值,提供用户一个 选择,按一个按钮浏览到一个显示前面输入的属性的表单。这个报表表单有一个功能是点击一个按钮,返回数据输入表单。

    输入表单的基本功能是通过DHTML和一些内嵌VBScript实现的。应用程序本身提供DHTML对象模型的一个扩展,表示两个DHTML表单。这个扩展允许表单中的脚本从应用程序中保存和获取属性/值的配对。这是应用程序中一个比较有趣的部分,因为脚本扩展使得一个基于Web的应用程序用这个客户端环境来在服务器 端保存的网页之间保持状态。

    示例应用程序演示了如何从底层DHTML文档对象截获事件,跟踪用户和DHTML表单上的元素的交互。用户在文档上移动鼠标的时候,应用程序截获onmouseover和onmouseout事件来显示当前鼠标下面HTML元素的的标签名和值。

    示例还应用程序演示了如何截获Internet Explorer的默认行为来在用户右击表单时显示一个自定义的上下文菜单。应用程序捕获了这个事件,并且“短路”了菜单的显示,有效 地对用户隐藏了Internet Explorer正被用于提供显示的事实。

     一个最后的架构问题包含在示例的解释中。和Microsoft Outlook 98一样,本示例应用程序显示HTML,但是不真正从一个Web服务器获得内容。实际上,示例应用程序(而且,我假定Outlook 98)从资源获得它的表单。WebBrowser控件能够通过一个简单的目标URL改变,从Win32映像文件提取资源。作为的URL的以http://开始的替代品,一个示例应用程序请求的URL以res://开始。 这通知WebBrowser控件在一个可执行文件内查找资源数据。示例应用程序使用的URL格式是http://<exepath>/<资源名>(译者注:原文有误,应为res://<exepath>/<资源名>,更多信息可以在Internet Development SDK中的Predefined Protocols部分找到),这里<exepath>是应用程序通过GetModuleFileName在初始化的时候获得的。HTML表单是简单的ASCII文件,作为自定义资源包含在资源中。资源可以用任何方式命名,不必和实际上的文件名字一致。


添加和删除属性
  图8显示了示例应用程序运行中的主屏幕。注意系统提供的优雅的DHTML图形(我从Outlook 98的一个模板借用了笔记本主题)。表单允许用户通过在编辑区域输入数据,然后按下添加/删除按钮来输入属性/值的配对。按钮在表单中执行图9所示的脚本。在提供的示例中,用户成功的输入了一个"a,1"的属性/值的配对,并且一个确认消息框被显示。

    图10显示了用户输入一些属性/值的配对之后,按下列表属性按钮的结果。当按钮被按下时,主表单的脚本捕获了按钮的onclick事件,并且调用window.navigate方法移动WebBrowser控件到属性报告表单。

    属性报告页面由图11所示的HTML产生的。这个表单包含一个加载时执行的内嵌脚本。脚本在示例中调用,并且基于用户输入的数据,动态产生一个表格。

在DHTML对象模型中截获事件
    只要你有使得控件工作和显示HTML的代码,你就可以开始监控各种对象的事件。示例应用程序通过建立以下三个对象的事件处理器来完成这个工作,WebBrowser、IHTMLDocument2、和IHTMLWindow2。

    在这些接口上建立事件处理器遵循标准的建立连接点的COM过程。例如,示例程序通过IHTMLDocument2 建立了一个到HTML文档对象的连接。图12显示了本示例的代码片断,连接事件处理器到HTML文档对象(IHTMLDocument2),可以处理来自HTML文档对象的事件。这个代码 在WebBrowser 组件触发NavigateComplete事件时执行,意味着一个新的HTML文档对象可用。

    对com_util::establish_connection_point的调用做了建立连接点的工作。这个函数尝试用4个参数——一个需要连接到的对象的接口指针、想建立的连接的类型,实际上的处理器的指针,以及一个用于设置表示这个连接的Cookie的地方——来建立连接。

    一旦连接点被建立,事件将从文档对象流动到指定的处理器。图13显示了html_document的处理器的Invoke方法,以及它如何映射DISPID到C++ html_document对象的方法调用。html_document基类的所有事件处理方法最初都返回E_NOTIMPL。你必须重载这个类的成员函数来提供需要的功能。

(译者注:MFC中CHtmlView的虚函数成员基本上就是干这个的,但是有时候可能吃力不讨好,比如DownloadComplete事件的虚函数方式缺少事件的一个参数,以至于我经常自己捕获这个事件,而覆盖掉CHtmlView的事件处理)

    这正好就是my_document类作的事情。my_document重载了来自于底层的DHTML文档对象的onmouseover和onmouseout事件。my_document::onmouseover方法(参见图14)最重要,因为它显示了如何获得显示文档的窗口。它也显示了如何从窗口获得描述onmouseover事件的事件对象。从这个事件对象,可以获得表示文档中鼠标移动到的真正的html_element对象。从html_element对象,示例应用程序获得了元素的标签名字和ID,而且在状态栏显示它。


扩展DHTML对象模型
    扩展DHTML对象模型的功能可以被证明是加入到WebBrowser控件的功能之中最重要的特性。可以用脚本访问的DHTML对象模型暴露了各种对象给代码,无论代码是什么脚本语言。其中一个对象,名字是window,表示显示当前文档的窗口。

    window对象具有一个名字是external的属性。在VBScript中,external属性表示一个集成HTML文档的应用程序。当一个脚本访问这个对象的时候,脚本引擎对WebBrowser组件的宿主应用程序的IOleClientSite接口进行一个QueryInterface调用,向它查询IDocHostUIHandler接口(在这里的示例代码中,它是被显式地设置的)如果一个组件宿主支持这个external对象的解析,它返回一个这个接口的指针。脚本引擎然后调用这个接口的get_External method方法,通过get_External,宿主应用程序可以返回一个IDispatch接口指针,指向代表脚本引擎中的external对象的一个自动化对象的指针。

    如果宿主从get_External方法返回了一个自动化对象的接口指针,脚本引擎用这个信息来解析脚本对这个对象的调用。引擎将调用GetIDsOfNames,然后对自动化对象进行合适的Invoke调用(参见图15)。
    你可以为了几乎任何用途来使用这个特性。示例应用程序用它来提供一个客户端的数据存储,在一个基于Web的应用程序中维护状态。你可以使用它来保存对象备以后获取,或者让框架预先下载对象。通过像这样预先下载对象,你可以在页面需要时是他们就绪,使系统有更多的响应时间。


用户界面问题
    前面我提到,如果你在你的应用程序中提供了IDocHostUIHandler的实现,Internet Explorer将调用你的容器,告诉他如何处理将要发生的用户界面活动。一个关于这个的杰出的示例是当用户在右击网页的时候。通常,Internet Explorer显示一个弹出菜单,具有一个“查看源文件”选择。如果用户选择了这个菜单项,Internet Explorer启动一个Notepad之类的程序来显示包含这个网页的HTML。

    这个特性对于学习网页的构成是很不错的。但是,它也产生一个显著的问题。假定你开发了一个基于Web的应用程序,并且客户端程序是用DHTML组成的页面实现的。一个用户浏览到应用程序的一个页面,然后选择“查看源文件”。突然,你的应用程序的代码被暴露给用户,以及世界上的其他人。对于你的代码的私有性不好!
    示例演示了如何防止这种情况的发生。解决方案实际上相当简单。IDocHostUIHandler::ShowContextMenu方法的实现是在C++ browser_control类的一个容器类中处理的。这个方法的实现简单地转发调用到一个虚函数,browser_control中的showcontextmenu。这个方法返回E_NOTIMPL,使得Internet Explorer着手它通常的工作。但是示例中的browser_control类实际上被my_browser_control子类化了,它重载了showcontextmenu方法。这个方法的实现返回S_OK,告诉Internet Explorer不要显示它自己的上下文菜单。Voilà!(译者注:法语感叹词)你的代码不能被访问了。

重新发布Internet Explorer 4.0

    你可能想知道如果用户没有在他们的计算机上安装Internet Explorer 4.0的时候会发生什么。你的应用程序还可以工作吗?简短的回答是:不能。长的回答是,如果Internet Explorer没有在系统上安装,你并没有失去所有希望。通过从微软获得一个许可,你可以和你的应用程序一起再发布Internet Explorer。

    重新发布Internet Explorer的前提是你的应用程序的安装程序要判断适当版本的Internet Explorer是否已经安装在系统上。如果4.0版本被判断为不存在,那么你的安装程序可以利用Internet Explorer 4.0简洁安装包。这个包可以安装Internet Explorer 4.0主浏览器、DirectShow™、 和Microsoft Win32 Java虚拟机。需要更多关于和你的应用程序一起再发布Internet Explorer 4.0的信息,参阅Internet Client SDK,Internet工具和技术,许可和发布。

结论
    我喜欢Web提供的用于开发应用程序的模型:一个客户端的浏览器,以及一个可以在任何地方的服务器。在所有这些噪音中,我想有些人可能没有看到这个模型并不适合所有程序。它们有窗口管理和状态的问题。没有任何东西可以阻止一个用户在工作中间离开去网络冲浪,而服务器毫不知情。而且,不要再 提“查看源文件”了。
    我看到在客户端重用WebBrowser控件——连同要么从服务器传递要么 嵌入在客户应用程序中的HTML表单——提供了一些困难的应用程序开发问题的解决方案。我留给你下面的关于这个结构如何帮助解决一些问题的思考。


在网络应用程序中控制浏览
    在基于Web的应用程序中控制用户浏览有两个分歧。第一个,通过开发一个自定义的浏览器,用WebBrowser控件替代发布Internet Explorer,应用程序可以跟踪和审核用户的浏览。
    第二个,自定义的浏览器可能默认连接到一个特定的URL,并且可能不允许用户显式输入其他的URL来闲逛。这使得浏览器以一个特定的URL为根(像在Windows资源管理器的命名空间一样)。这允许服务器端的代码更加有效率的设计,因为服务器可以不必处理用户不经心地退出的情况。
    开发一个基于Web的应用程序的主要问题之一是数据不能在客户端的页面之间保持。肯定有绕过的方法,但是它们都有缺点。一些像ASP这样优雅的还是用了黑客的方法绕过这个问题。通过一个包含本文技术的应用程序架构在客户端保存数据 ,你可以更加容易的开发和传递启用Web的应用程序。你不必担心强制在服务器和客户端来回传递数据来保持状态的问题。


对用户隐藏HTML源代码
    隐藏HTML源代码的问题已经在本文中被解决了。但是没有价值的隐藏代码有一些影响。首先,你的用户不能看到你的页面的构成和脚本如何工作。在保护了你的应用程序代码和逻辑上的私有性之外,隐藏代码也妨碍了用户查看有价值的信息,这些信息可能有助于它们对操作进行反向工程以及在黑你的Web站点的尝试中进行规划。

    隐藏HTML资源也隐藏了浏览器被重用的事实。这有助于在你的应用程序中完全无缝的集成浏览器的外观。记住,组件软件的一个思想是用户不需要知道组件是如何被使用的,甚至组件是被使用的。

    开发基于Web的应用程序的另一个主要问题是你基本上被限制在一个基于表单的模型内。有多少次你只是需要弹出一个位于HTML表单顶端的对话框?如果你使用本文描述的模型,你可以在这里得到一个 答案,提供DHTML的一个扩展,让宿主应用程序为你做这件事情。

    让我们翻转硬币,从另一方面来看。到目前为止我几乎都是在谈这个框架可以如何帮助你开发基于Web的应用程序。你可以从另一个角度来看:增加Web应用程序功能到基于Win32的应用程序。你的应用程序对于浏览器控件的使用允许你建立丰富内容的用户界面,不用绑定他们到你的可执行程序 。你的程序可以从一个Web服务器动态地载入它的用户界面。

(译者注:这也有助于应用程序的本地化和自定义界面)

    最后,Web提供给你一个内容丰富的用户界面。开发基于Win32的应用程序的最大的挑战之一是为你的应用程序编写一个真正美好的用户界面。要提供一个很好的接口,你需要操作设备环境、调色板、 图形库文件和种种其他问题。如果你重用了WebBrowser控件,Internet Explorer为你 照管了这个问题。

来自于1998年7月的Microsoft Interactive Developer杂志。从你的本地杂志摊获得,或者更好的是订阅。

© 1998 微软公司保留所有权力。使用条款。

本文地址:http://com.8s8s.com/it/it1724.htm