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

使用纯 C 在自己的窗口中嵌入 HTML 控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (154投票s)

2002年12月15日

CPOL

19分钟阅读

viewsIcon

1307881

downloadIcon

16758

特别展示了如何在自己的窗口中嵌入浏览器 OLE 对象,更广泛地演示了如何在纯 C(即不使用 MFC、WTL、ATL、.NET、C#,甚至 C++)中操作和创建 COM/OLE 对象。后者适用于许多其他用途,例如创建自己的脚本引擎。

Sample Image - cwebpage.jpg

引言

有许多示例演示了如何将 Internet Explorer 作为 OLE/COM 对象嵌入到自己的窗口中。但这些示例通常使用 Microsoft Foundation Classes (MFC)、.NET、C# 或至少是 Windows Template Library (WTL),因为这些框架提供了预制的“包装器”,可以轻松地为您提供一个“HTML 控件”来嵌入到您的窗口中。如果您尝试使用纯 C,不使用 MFC、WTL、.NET、C#,甚至完全不使用 C++ 代码,那么关于如何处理 COM 对象(如 IE 的 IWebBrowser2)的示例和信息将非常稀少。这是一篇带有可运行示例的文章,用 C 语言具体展示了您需要做什么才能将 IE 嵌入到自己的窗口中。

事实上,我甚至已经将示例 C 代码(用于将 IE 嵌入到您的窗口中)打包成了一个动态链接库 (DLL),这样您只需调用一个函数即可在您创建的窗口中显示网页或 HTML 字符串。您甚至不需要深入研究 COM(除非您打算修改 DLL 的源代码)。

在继续阅读本文之前,您应该先阅读我关于纯 C 中 COM 的系列文章。 第一部分 讨论了使用 COM 对象所需的信息。我们还将处理具有多个接口的对象,正如在 第四部分 中讨论的。我们将使用自动化数据类型,如在 第二部分 中讨论的。最后,我们还将处理事件(回调),这将在 第五部分 中讨论。

我们必须创建的强制性 COM 对象

阅读了我上面关于 COM 的系列文章后,您已经具备了用纯 C 编写 COM 对象所需的背景知识。现在让我们来看看托管浏览器对象所需的条件。在阅读以下讨论时,您可能希望仔细查看源代码文件 Simple.c(位于 Simple 目录中)。

首先,浏览器对象要求我们提供(至少)3个我们自己的 COM 对象。我们需要 IOleInPlaceFrameIOleClientSiteIOleInPlaceSite 对象。所有这些对象(及其 VTable 和 GUID)都已在 Microsoft 的包含文件中(随您的 C 解释器提供或从 Microsoft 网站下载的 SDK 中)为我们定义好了。因此,它们每个都有自己预定义的一组 VTable 函数。

让我们来看看我们的 IOleClientSite 对象。它有一个 VTable,定义为一个 IOleClientSiteVtbl 结构。本质上,它是一个包含 9 个函数指针的数组,我们必须在程序中提供这些函数。 (也就是说,我们必须为我们的 IOleClientSite 对象编写 9 个特定的函数)。当然,前 3 个函数将是我们 IOleClientSite 对象的 QueryInterfaceAddRefRelease 函数。在 Simple.c 中,我将这三个函数命名为 Site_QueryInterfaceSite_AddRefSite_Release(以避免与其他 COM 对象的 QueryInterface、AddRef 和 Release 函数发生名称冲突)。事实上,我将其他 6 个函数命名为以 Site_ 开头。它们的名字如 Site_SaveObject, Site_ShowObject, 等。当浏览器对象需要与包含它的窗口进行交互时,它会调用我们的 IOleClientSite 函数。每个函数的确切用途以及传递给它的参数,您可以自己通过查阅 MSDN 上关于 IOleClientSite 对象 的文档来了解。

要创建我们 IOleClientSite 对象的 VTable,最简单的方法就是将其声明为一个静态全局变量,并用我们 9 个函数的指针进行初始化。在我们的 C 源文件中,我们可以这样做:

static IOleClientSiteVtbl MyIOleClientSiteTable = {Site_QueryInterface,
Site_AddRef,
Site_Release,
Site_SaveObject,
Site_GetMoniker,
Site_GetContainer,
Site_ShowObject,
Site_OnShowWindow,
Site_RequestNewObjectLayout};

现在,有一个名为 MyIOleClientSiteTable 的全局变量,它是我们 IOleClientSite 对象的一个已正确初始化的 VTable。

在 Simple.c 中,您会看到我们也声明了其他对象的 VTable 作为全局变量。但我们没有将对象本身声明为全局变量。我们将为这些对象添加一些额外的成员,用于我们自己的私有数据。例如,与其仅仅使用一个普通的 IOleInPlaceFrame,我们定义了自己的 _IOleInPlaceFrameEx,它包含一个嵌入的 IOleInPlaceFrame 和一个额外的 HWND,我们可以在其中存储我们自己窗口的句柄。请注意,这个额外的 HWND 成员被添加到结构体的末尾,位于 IOleInPlaceFrame 之后。这一点非常重要。 IOleInPlaceFrame(及其 VTable 指针)必须放在前面。并且请注意,我们的额外数据是窗口特定的。换句话说,每个托管嵌入式浏览器对象的窗口都需要一个不同的 IOleInPlaceFrameIOleClientSiteIOleInPlaceSite 结构。因此,我们将在创建窗口时分配它们,而不是将它们声明为全局变量。

浏览器对象将我们的 IOleInPlaceFrameIOleInPlaceSite 对象视为我们 IOleClientSite 对象的子对象。因此,当浏览器需要一个指向其中一个对象的指针时,它将调用我们 IOleClientSite 的某个函数,要求我们返回一个指向它的指针。通常它调用的函数是我们 IOleClientSiteQueryInterface 函数。(但对于某些对象,例如我们的 IOleInPlaceFrame 对象,浏览器将通过调用另一个 IOleClientSite 函数来请求指针。情况就是这样)。这意味着我们的 IOleClientSite 函数需要能够访问我们的 IOleInPlaceFrameIOleInPlaceSite 对象,以便我们的 IOleClientSite 函数在被要求时能够返回指向其中任何一个对象的指针。因此,我们将定义一个大型对象(_IOleClientSiteEx),其中包含我们的 3 个对象 - IOleClientSiteIOleInPlaceFrameIOleInPlaceSite - 全部嵌入在此大型结构体中。这样就可以轻松地从一个对象找到另一个对象,只需使用指针算术。唯一的要求是我们 的 IOleClientSite 必须是这个更大对象中的第一个。这样,这个更大的对象就可以伪装成一个普通的 IOleClientSite。

您可以查阅 MSDN 文档,了解我们 IOleInPlaceFrameIOleClientSiteIOleInPlaceSite VTable 中的函数应该做什么,以及传递给它们的参数。在 Simple.c 中,我们只使用了展示网页所需的最少功能。

我们可以选择创建的额外 COM 对象

如前所述,浏览器对象要求我们至少提供上述 3 个对象。但是,我们可以在程序中选择性地实现其他对象,以便支持与浏览器对象的额外交互。特别是,IDocHostUIHandler 非常有用。它允许我们控制某些用户界面特性,例如能够替换/禁用当用户右键单击嵌入式浏览器对象时弹出的上下文菜单,或者确定滚动条或边框等内容是否渲染,或者阻止嵌入的脚本运行,或者在用户单击任何链接时自动打开新的浏览器窗口等。由于此类对象非常有用,我们在示例 C 代码中也实现了一个 IDocHostUIHandler 接口。(也就是说,我们有一个 IDocHostUIHandler 结构体,18 个 IDocHostUIHandler 函数,以及一个包含这 18 个函数指针的 VTable)。我们将此 IDocHostUIHandler 嵌入到我们的大型 _IOleClientSiteEx 对象中。

获取浏览器对象

在获取 Microsoft 的浏览器对象之前,我们必须调用一次 OleInitialize,以确保 OLE/COM 系统已为我们的进程初始化。通常,在使用 COM 时,您会调用 CoInitialize。但是浏览器需要一些额外的 OLE 初始化(CoInitialize 无法完成)。因此,我们调用 OleInitialize,它会为我们调用 CoInitialize

现在我们就可以获取浏览器对象了。我们的函数 EmbedBrowserObject 负责获取浏览器对象并将其嵌入到特定窗口中。我们只需要做一次,所以我们在创建窗口时立即调用 EmbedBrowserObject(并将窗口句柄传递给 EmbedBrowserObject)。

首先,由于每个窗口(托管浏览器控件)都需要一个单独的 IOleInPlaceFrameIOleClientSiteIOleInPlaceSiteIDocHostUIHandler 对象,因此 EmbedBrowserObject 会分配这 4 个对象(即结构体)。实际上,由于我们将这 4 个结构体都放置在我们自己的、更大的结构体中(即我们定义为 _IOleClientSiteEx 结构体),因此一次调用 GlobalAlloc 分配一个 _IOleClientSiteEx 结构体就完成了所有操作。分配完这 4 个 COM 对象后,我们必须初始化它们(即,将指向每个 VTable 的指针放入其各自的 lpVtbl 成员)。此外,我们将在窗口的 USERDATA 字段中保存指向此已分配结构体的指针。这样,我们的窗口过程(和其他函数)就可以轻松地从窗口句柄中检索我们的 COM 对象。

现在,我们通过调用操作系统函数 CoCreateInstance 来获取 Microsoft 的 IWebBrowser2 对象(即 Internet Explorer 的主对象),传递 IWebBrowser2 对象的 GUID(在 Microsoft 的包含文件中定义为符号 IID_IWebBrowser2)。我们还传递 Microsoft 浏览器控件所在的 DLL 的 GUID(定义为符号 CLSID_WebBrowser)。

如果一切顺利,CoCreateInstance 将返回一个指向新创建的 IWebBrowser2 对象的指针。该指针存储在我们的变量 webBrowser2 中。

接下来,我们需要获取 IWebBrowser2 对象的 IOleObject 子对象。我们通过调用父对象的 QueryInterface 函数来获取子对象。因此,我们调用 IWebBrowser2 的 QueryInterface 以获取其 IOleObject 子对象的指针(我们将其存储在我们的变量 browserObject 中)。这个子对象是我们主要用来将 IE 嵌入到自己的窗口中,并控制网页显示的那个。IOleObject 子对象尚未嵌入。它仅仅是被创建了。在本文的其余部分,我将把这个 IOleObject 子对象简称为浏览器对象

接下来,我们需要调用浏览器对象的 SetClientSite,为其提供指向我们自己的 IOleClientSite 对象的指针。浏览器对象将需要调用我们的一些 IOleClientSite 函数来从我们这里获取信息。

我们还调用它的 SetHostNames() 来向浏览器传递我们应用程序的名称(以便它可以在自己的消息框中显示该名称)。

那么如何嵌入浏览器对象呢?我们需要调用浏览器对象的 DoVerb 函数,向浏览器对象发送一个命令,告诉它将自己嵌入到我们的窗口中(OLEIVERB_SHOW)。我们还将我们的窗口句柄传递给 DoVerb。在调用 DoVerb 的过程中,浏览器对象将调用我们的一些 IOleClientSite 函数。在 DoVerb 返回之前,它会调用其中的几个。

通过 DoVerb 发送 OLEIVERB_SHOW 命令不会显示任何网页。(我们有另一个函数可以在完成 EmbedBrowserObject 后调用)。它仅仅是将浏览器对象嵌入到我们的窗口中,使其准备好显示网页并在我们的窗口中显示。

EmbedBrowserObject 的最后,我们调用 IWebBrowser2 对象的 Release 函数。我们不再需要这个对象了(如果需要,我们可以调用 IOleObject 子对象的 QueryInterface。请记住,子对象的 QueryInterface 始终可以用来定位其父对象)。但是我们不释放子对象。我们仍然需要它来调用其函数来显示网页和其他操作。我们不会在 UnEmbedBrowserObject 中释放子对象,直到我们最终完成使用浏览器对象为止。

显示网页

我们可以调用我们的函数 DisplayHTMLPage 来显示 URL 或磁盘上的 HTML 文件。在 DisplayHTMLPage 中,我们的操作与在 EmbedBrowserObject 中非常相似。我们使用浏览器对象的 QueryInterface() 来获取指向其关联的其他对象的指针,并使用这些其他对象的 VTable 来调用它们的函数,以便显示 URL 或磁盘上的 HTML 文件。同样,您可以查阅 MSDN 文档,以了解我们请求的对象及其调用的函数的更多信息。

基本上,我们需要调用 IWebBrowser2Navigate2 函数来显示网页,传递我们想要显示的页面的 URL。我们的 URL(即网址,例如“http://www.microsoft.com”或磁盘上的 HTM 文件名,例如“c:\myfile.htm”)必须作为 BSTR 传递给 IWebBrowser2Navigate2 函数。更重要的是,我们的 BSTR 需要被填充到一个 VARIANT 结构体中,然后该 VARIANT 结构体才被传递给 Navigate2

Navigate2 将从其所在位置获取页面内容,并在我们窗口中嵌入的浏览器对象中显示它。

显示 HTML 格式的缓冲区

如果我们有一个缓冲区(在内存中)已经包含我们想要显示的 HTML 页面该怎么办?在这种情况下,我们可以让浏览器对象显示它,但这涉及一些额外的步骤。

首先,我们需要创建一个空白页面,可以通过调用 Navigate2 并传递 URL about:blank 来实现。这是一个特殊的 URL,IE 引擎会将其识别为空白页面。

接下来,我们获取浏览器的 IHTMLDocument2 对象,并调用其 write 函数,告诉它将我们的缓冲区内容写入这个空白网页。我们必须将我们的缓冲区格式化为 BSTR,并将其包装在一个标准的 COM 结构体中,称为“安全数组”。COM 提供了一些我们可以调用的函数来分配安全数组(并在完成后释放它)。

我们的函数 DisplayHTMLStr 完成了这项工作。

显示 CHM 文件中的页面

浏览器对象可以使用特殊的 URL 协议 its: 来显示编译帮助 (.CHM) 文件中的页面。只需像这样调用 DisplayHTMLPage

// Display the page named "mywebpage.htm" from our .CHM file
// named MyChmFile.chm
DisplayHTMLPage(hwnd, "its:MyChmFile.chm::mywebpage.htm");

调整浏览器显示区域的大小

如果用户调整了包含浏览器对象的窗口大小,该对象不会自动调整其显示区域的大小。如果我们希望放大/缩小显示区域,我们需要专门调用一些浏览器函数。我们调用 put_Widthput_Height,分别传递所需的宽度和高度。

我们的函数 ResizeBrowser 完成了这项工作。通常,这会在我们处理窗口的 WM_SIZE 消息时调用。

前进、后退和其他操作

事实上,您可以根据需要创建多个浏览器对象,例如,如果您想要多个窗口 - 每个窗口托管自己的浏览器对象,以便每个窗口都可以显示自己的网页。Simple.c 创建了两个窗口,每个窗口都托管一个浏览器对象。(所以我们为每个窗口调用一次 EmbedBrowserObject)。在一个窗口中,我们调用 DisplayHTMLPage 来显示 Microsoft 的网页。在另一个窗口中,我们调用 DisplayHTMLStr 来显示内存中的一些 HTML 字符串。

确实,在嵌入浏览器对象之后,我们可以反复调用 DisplayHTMLPageDisplayHTMLStr 来更改显示的内容。

网页浏览器会自动保留我们显示的 URL 的“历史记录”。通过调用浏览器的 GoBack 函数,我们可以让浏览器返回显示以前查看过的页面。这相当于单击 IE 的“后退”按钮。事实上,还有几个操作对应于 IE 按钮,例如刷新、前进、搜索等,我们可以调用它们。我们的函数 DoPageAction 作为这些浏览器函数的通用接口。(尽管 Simple.c 没有使用这个功能,但您可以在示例代码中添加“后退”、“前进”、“主页”、“停止”按钮,并使用 DoPageAction)。

释放浏览器对象

当我们最终完成浏览器对象的使用后,我们需要 Release 它以释放它使用的所有资源。我们在 UnEmbedBrowserObject 中这样做。这只需要做一次,所以我们在窗口销毁时立即进行。并且我们需要在程序退出前调用 OleUninitialize

cwebpage.dll

Simple 目录包含一个完整的 C 示例,所有内容都在一个源文件中。仔细研究它,以便熟悉在自己的窗口中使用浏览器对象的技巧。它演示了如何显示 Web 或磁盘上的 HTML 文件,或内存中的 HTML 字符串,并创建了 2 个窗口来实现这一点。

Browser 目录也包含一个完整的 C 示例。它演示了如何添加“后退”、“前进”、“主页”和“停止”按钮。它创建了一个子窗口(在主窗口内),浏览器对象被嵌入其中。

Events 目录也包含一个完整的 C 示例。它演示了如何实现我们自己的特殊链接,以显示带有指向其他 HTML 字符串(在内存中)的链接的网页。您可以使用此技术来定义其他类型的特殊“链接”,这些链接可以在用户单击链接时向您的窗口发送消息。

DLL 目录包含一个 DLL,其中包含 EmbedBrowserObject, UnEmbedBrowserObject, DisplayHTMLPage, DisplayHTMLStrDoPageAction 函数。DLL 还包含所有的 IStorage, IOleInPlaceFrame, IOleClientSite, IOleInPlaceSiteIDocHostUIHandler VTable 及其函数。DLL 还会代表您调用 OleInitializeOleUninitialize。所以要使用这个 DLL,您不需要在 C 程序中编写任何 OLE/COM 代码。所有这些都在 DLL 中。有一个名为 Example.c 的小型示例使用了该 DLL。它只是 Simple.c,其中所有 OLE/COM 代码都被移除,并替换为调用 DLL 的函数。DLL 函数已稍作修改,以支持 UNICODE 或 ansi。我使用函数 IsWindowUnicode 来检测应用程序窗口(托管浏览器对象)是否为 UNICODE。

DLL 还包含一些新函数来支持事件,这将在下面讨论。

事件

HTML 页面通常由许多元素组成,例如各种标签(如 FONT 标签、链接、表单等)。每个元素可能具有各种“动作”或“事件”。例如,当用户将鼠标指针移到链接上时,链接会触发一个事件。当用户将鼠标指针移开时,它会触发另一个事件。它还可能触发其他事件。

应用程序可以请求浏览器在特定元素发生特定事件时提供反馈。为了让我们获得关于某个元素的反馈,HTML 页面必须被编写成给该元素一个 ID(即字符串名称)。例如,假设我们的页面有一个 FONT 元素。我们 arbitrarily 给这个 FONT 元素一个 ID,名为 testfont。HTML 页面的 FONT 元素可能如下所示:

<FONT id=testfont color=red> This is some red text. </FONT>

每个事件本身都有一个唯一的字符串名称。例如,当鼠标指针移到上面的 FONT 元素上时(即鼠标指针移到红色文本上),发生的事件是 mouseover 事件。当鼠标指针移开 FONT 元素时,发生的事件是 mouseout 事件。

对于网页上的每个元素,网页浏览器都有一个 IHTMLElement 对象。为了获得元素的反馈,我们首先必须获取其 IHTMLElement 对象。在 DLL 目录的 Dll.c 中有一个名为 GetWebElement 的函数,它展示了如何获取特定元素的 IHTMLElement 对象。 GetWebElement 接收包含浏览器对象的窗口和所需元素的 ID(名称)。为了获取 IHTMLElement,我们必须经过几个其他浏览器对象。我们必须获取浏览器的 IHTMLDocument2,然后从该对象中获取(所需元素的)IHTMLElementCollection,然后获取元素的 IDispatch,最后从中获取元素的 IHTMLElement 对象。哇!

一旦我们有了元素的 IHTMLElement,我们就可以“附加”到该元素以接收其事件的反馈。正如您从我关于 COM 事件的文章中学到的,我们需要向浏览器提供一个我们创建的 IDispatch 对象。当事件发生时,浏览器将调用我们 IDispatchInvoke 函数。我们必须将我们的事件 IDispatch 提供给浏览器,通过获取浏览器的 IHTMLWindow3 对象并调用其 attachEvent 函数,传递我们的 IDispatch 来实现。

然后,为了告诉浏览器在 FONT 元素的“mouseover”事件发生时调用我们 IDispatch 的 Invoke,我们调用该 FONT IHTMLElementput_onmouseover 函数,传递指向我们 IDispatch 的指针。(实际上,我们需要将我们的 IDispatch 指针包装在一个 VARIANT 中)。为了获得该 FONT 元素“mouseout”事件的反馈,我们调用该 FONT IHTMLElementput_onmouseout 函数,传递指向我们 IDispatch 的指针。

不同类型的元素可能具有不同的事件,因此某些元素(如 FORM)具有可以通过其 IHTMLElementQueryInterface 获取的其他子对象。例如,如果我们有一个 FORM 元素的 IHTMLElement,我们可以调用其 QueryInterface 来获取其 IHTMLFormElement。然后,我们可以调用 IHTMLFormElementput_onsubmit 函数来附加到其提交事件(即用户提交 FORM 数据时)。查阅 MSDN 文档,以确定哪些网页元素具有哪些子对象(即哪些元素会触发哪些事件)。

当然,我们希望将所有 COM 方面的内容都隔离在我们的 cwebpage.dll 中,所以我们要做的就是提供一个函数来代表应用程序创建一个 IDispatch。该函数是 CreateWebEvtHandlerIDispatch 的函数将位于 cwebpage.dll 中,因此应用程序无需创建自己的 COM 对象。为了抽象这一点,我们将让应用程序为它想要反馈的每个元素分配一个它自己选择的 ID 号。例如,应用程序可能决定为 FONT 元素分配 ID 号 1。那么当我们的 DLL IDispatch Invoke 收到关于该 FONT 元素的 mouseover 事件的反馈时,例如,我们将向应用程序的窗口发送一个自定义消息。自定义消息将包含元素的 ID 号以及事件的字符串名称(即“mouseover”)。

HTMLEvents 目录中有一个示例应用程序和一个示例网页。该网页包含多个元素,包括一个 FORM 和一个 FONT 元素。应用程序接收这两个元素的某些事件的反馈。

应用程序还可以接收与网页本身相关的事件的反馈(例如用户双击页面空白区域),或浏览器的滚动条等。该示例也接收其中一些非元素事件的反馈。

应用程序可以获得的事件还有很多。请查阅 MSDN 文档并进行实验。

历史

  • 首次发布于 2002 年 12 月 1 日。
  • 2002 年 12 月 6 日更新。添加了 IDocHostUIHandler 接口和 DoPageAction 函数。修复了 UnEmbedBrowserObject() 和 DisplayHTMLStr()。改进了代码中的注释,使其更明确清晰。添加了 Browser.c 和 Events.c 示例。
  • 2006 年 5 月 15 日更新。增加了接收网页“事件”的能力,例如知道用户何时单击页面上的某个项目,或提交表单等。将 UNICODE 和 ANSI 支持合并到一个 DLL 中。
  • 2006 年 8 月 1 日更新。重写了文章,引用我关于纯 C 中 COM 的系列文章,并更详细地阐述了浏览器相关的特定事项。还更新了此页面上的下载链接,以包含最新的代码库。
© . All rights reserved.