前段时间因为项目需要,所以翻译了这篇,希望对某些人有用。由于时间仓促,错误之处在所难免,请不吝指出。
另:browser lib为我改写的一个静态库,test为测试程序。

使用纯C在自己的窗口中嵌入HTML控件
原文链接:http://www.codeproject.com/KB/COM/cwebpage.aspx
翻译:xiep
简介
必须被创建的COM对象
可以选择创建的COM对象
获取浏览器对象
显示一个网页
显示一个HTML格式的缓冲区
显示一个CHM页面
调整浏览器的显示区域
后退,返回,和其他动作
释放浏览器对象
cwebpage.dll
事件
  
 

简介
将Internet Explorer当作OLE/COM对象中嵌入自己的窗口的例子很多。但这些例子通常使用MFC,.NET,C#,至少使用了Windows模板库(WTL),因为这些框架都被已经将Internet Explorer预先包装为一个易用的HTML控件。如果你想使用纯C,而不是MFC,WTL,.NET,C#,甚至是根本不使用任何C++代码,那么像使用IE的IWebBrowser2控件的例子就很少了。本文就是关于使用纯C在自己的窗口中嵌入IE浏览器的详细描述,并且附带可以运行的例子。
事实上,我已经使用C代码将IWebBrowser2包装成一个动态链接库,因此你可以在你所创建的窗口中简单的调用其中的某个函数显示一个网页或者HTML文本,而不需要了解关于COM的任何内容,除非你想修改该动态链接库的源码。
在继续阅读本文之前,你应当阅读我关于《纯C中的COM》系列文章。其中第一部分是使用COM对象的基础知识;第四部分是关于处理含有多接口的COM对象的;第二部分关于自动数据类型的使用;以及第五部分关于处理事件(回调函数)的内容。

必须被创建的COM对象
只要阅读了以上提到的文章,你就已经具备了在纯C中编写COM对象的基础知识。下面我们看看使用浏览器对象都需要做些什么。你可以边读本文边查看Simple.c(在Simple目录)。
首先浏览器对象希望我们提供至少3个COM对象,即一个IOleInPlaceFrame对象,一个IOleClientSite对象,以及一个IOleInPlaceSite对象。这些对象以及它们的虚函数表和GUID都已经在SDK的头文件中被定义。因此它们都有自己的预定义的虚函数集合。
我们来看看IOleClientSite对象吧。它的虚函数表被定义为IOleClientSiteVtbl结构。IOleClientSiteVtbl实际上是一个由9个函数指针组成的数组,这些函数必须在程序中被提供。当然前3个函数是QueryInterface,AddRef和Release。为了避免和其他COM对象的这些函数产生冲突,在Simple.c中我将它们分别命名为Site_QueryInterface,Site_AddRef和Site_Release。事实上我将其他的6个函数也以Site_前缀命名,如Site_SaveObject,Site_ShowObject等。IOleClientSite对象被浏览器对象调用以便和我们的窗口交互。每个函数的具体目的依据传递给它们的参数而定,你可以参考MSDN关于IOleClientSite对象的描述。
为IOleClientSite对象创建虚表,最简单的方式就是将虚表定义为一个全局的静态变量,并且使用我们定义的9个函数的指针初始化该虚表。使用的C代码如下:
static  IOleClientSiteVtbl  MyIOleClientSiteTable = { Site_QueryInterface,
                             Site_AddRef,
                             Site_Release,
                             Site_SaveObject,
                             Site_GetMoniker,
                             Site_GetContainer,
                             Site_ShowObject,
                             Site_OnShowWindow,
                             Site_RequestNewObjectLayout
                         };
现在这个名叫MyIOleClientSiteTable的全局变量就是一个被正确初始化的虚表。
在Simple.c中,你也可以看到我为其他对象定义的虚表。但是我们并没有将对象本身定义为全局的。我们将为这些对象增加一些另外的域以便存放私有数据。例如,我们定义_IOleInPlaceFrameEx结构,它包含内嵌的IOleInPlaceFrame和一个用来存放我们的窗口句柄的HWND。应当注意的是这里的HWND成员被放置在结构的最后面,也就是在IOleInPlaceFrame之后,这点非常重要。IOleInPlaceFrame(带有虚表)必须被放在最前面,并且这里的额外数据是和窗口相关的,也就是说每一个窗口都嵌有自己的浏览器对象,因此我们在创建窗口的时候动态为IOleInPlaceFrame分配内存,而不是将它们定义为全局变量。
浏览器对象将IOleInPlaceFrame和IOleInPlaceSite对象当做IOleClientSite的子对象。因此当浏览器需要它们中一个的指针,它必须通过调用IOleClientSite对象虚函数获取,通常这个函数是QueryInterface(但是对于某些对象,如IOleInPlaceFrame,浏览器对象通过调用IOleClientSite的另一个函数获取。)。言外之意是,IOleClientSite的虚函数需要存取IOleInPlaceFrame和IOleInPlaceSite对象,从而向浏览器返回它们的指针。因此我们定义了另一个对象_IOleClientSiteEx,它包含了IOleClientSite,IOleInPlaceFrame和IOleInPlaceSite3个对象。这样使得从一个对象定位另一个对象非常容易。唯一的要求是IOleClientSite必须放在最前面,这样就可以将整个对象当做一个IOleClientSite对象。
您可以查阅MSDN文档,以了解IOleInPlaceFrame,IOleClientSite和IOleInPlaceSite的各虚函数的功能和参数。在Simple.c中,我仅仅实现了在窗口中显示一个网页。

可以选择创建的COM对象
上面提到浏览器对象期望我们至少提供3个对象。但是还存在其他对象,我们可以选择性的创建,从而可以与浏览器对象进行更丰富的交互。IDocHostUIHandler是一个特别有用的对象。它可以让我们控制某些用户界面特性,如替换或禁用右键弹出式菜单,或者确定是否有滚动条或边界以及其他此类事件,或者防止嵌入式脚本运行,用户点击链接在新窗口中打开等。这个对象如此有用,因此我们在源码中还定义了一个IDocHostUIHandler结构,包含18个IDocHostUIHandler函数,以及一个包含这些函数的虚表,同样的我也将该结构放在IOleClientSiteEx结构内部。

获取浏览器对象
在我们获得微软的浏览器对象之前,我们必须调用OleInitialize一次,以确保与OLE/COM系统在我们的进程中初始化。通常,创建COM对象使用CoInitialize函数。但这里的浏览器对象需要做一些额外的OLE初始化工作(CoInitialize不会初始化这些数据),因此我们调用OleInitialize,该函数在内部会自动调用CoInitialize函数。
现在我们准备取得浏览器对象。函数EmbedBrowserObject取得浏览器对象,并嵌入到特定的窗口。我们必须且只调用该函数一次,所以我们在窗口创建时调用该函数(并将窗口句柄作为参数传入)。
首先,因为每个嵌入浏览器的窗口都需要一个单独的IOleInPlaceFrame,IOleClientSite,IOleInPlaceSite和IDocHostUIHandler对象,因此EmbedBrowserObject函数用来为这四个对象分配内存。而我们将这4个对象都包含于_IOleClientSiteEx结构内部,因此只需要调用GlobalAlloc函数分配一个_IOleClientSiteEx结构就可以了。为这4个对象分配内存后,我们需要初始化它们(它们的虚表)。此外,我们还应当将_IOleClientSiteEx的内存地址保存在窗口的用户数据域(USERDATA),这样窗口例程和其他函数可以通过窗口句柄很容易的获取这些对象。
现在我们可以通过调用API CoCreateInstance获得微软的IWebBrowser2对象(也就是Internet Explorer对象),这需要将IWebBrowser2对象的GUID(在SDK的头文件被定义为IID_IWebBrowser2)作为参数,我们还需要传递提供该浏览器对象的动态链接库的GUID(在SDK的头文件被定义为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发送OLEIVERB_SHOW命令并不显示任何网页(这需要在调用EmbedBrowserObject之后,调用另一个函数)。它只是嵌入浏览器对象到我们的窗口,准备现实一个网页,并显示在我们的窗口。
在EmbedBrowserObject的最后,我们调用IWebBrowser2的Release函数,我们不再需要这个对象(如果该对象被释放,我们可以调用IOleObject的QueryInterface函数,子对象的QueryInterface用于定位其母对象)。但是我们并不释放该子对象,我们仍然需要调用它的函数,来显示网页和做其他事情。直到UnEmbedBrowserObject中,我们不再使用浏览器对象时才释放该子对象。

显示一个网页
我们可以通过DisplayHTMLPage来显示一个URL或者磁盘上的HTML文件。在DisplayHTMLPage中所做的事情和UnEmbedBrowserObject非常类似。我们使用浏览器对象的QueryInterface函数来得到其他对象的指针,并调用它们的虚函数以显示URL或磁盘上的HTML文件。你可以再次查看MSDN,了解这些对象和它们的函数。
我们最终调用IWebBrowser2的Navigate2来显示一个网页,传递一个URL作为参数(如http://www.microsot.com),类型为BSTR,或者是磁盘上的一个HTML文件(如C:/myfile.htm)。该BSTR参数必须被塞进一个VARIANT结构,也就是说最终传递的是VARIANT结构指针。
Navigate2将撷取网页内容,并显示在浏览器对象被嵌入的窗口。

显示一个HTML格式的缓冲区
如果我们拥有的一个缓冲区包含需要显示的HTML页面,怎么来显示这个页面呢?仍然可以使用浏览器对象来显示,但是需要一些多余的步骤。
首先,我们需要创建一个空页面,也就是传递URL about:blank参数到Navigate2,IE浏览器引擎将这个URL作为空页面看待。
接下来,我们获取浏览器的IHTMLDocument2对象,并调用它的write函数让浏览器显示我们缓冲区的内容到这个空页面。我们必须将这个缓冲区格式化为BSTR,并且包装到一个叫做“safe array”标准的COM结构。COM提供了一些函数用于分配safe array,当不再使用时应当将它释放掉。
函数DisplayHTMLStr完成这个功能。

显示一个CHM页面
浏览器对象也可以从.CHM中显示一个页面,这需要通过its:协议,代码如下:
//显示MyChmFile.chm文件中的mywebpage.htm页面
DispHTMLPage(hwnd, “its:MyChmFile.chm::mywebpage.htm”);

调整浏览器的显示区域
如果包含浏览器对象的窗口尺寸被改变,浏览器对象将不会自动更新显示区域。我们需要调用浏览器对象的put_Width和put_Height函数来缩放显示区域。
函数ResizeBrowser完成这个功能,它将在处理WM_SIZE窗口消息时被调用。

后退,返回,和其他动作
事实上,如果需要你可以创建若干个浏览器对象,例如创建多个窗口,每个窗口都嵌入了自己的浏览器对象来显示自己的网页。Simple.c创建了两个窗口,并各自嵌入了一个浏览器对象。(因此我们为每个窗口都调用了EmbedBrowserObject各一次)。我们在其中的一个窗口显示微软的主页,而另一个页面显示一个HTML文件。
当浏览器对象被嵌入后,可以使用DisplayHTMLPage或DisplayHTMLStr重复改变现实的页面。
浏览器自动保存了已经被显示的页面URL历史。我们可以使用浏览器对象的GoBack使浏览器回到显示的上一个页面,这将类似于点击IE浏览器的“返回”按钮,实际上还存在其他的动作对应于IE浏览器的按钮,如刷新,转寄,搜索等,我们可以援引。函数DoPageAction用于实现这些功能,虽然Simple.c没有利用这一点,但是你可以增加返回,前进,刷新,搜索等,按钮的例子代码,并利用DoPageAction ) 。

释放浏览器对象
最终我们将需要调用Release函数来释放浏览器对象使用的所有资源,我们在UnEmbedBrowserObject函数中实现这个功能。该函数应当而且只在窗口销毁时被调用一次。同时我们需要在退出程序之前调用OleUninitialize。

cwebpage.dll
Simple目录包含一个完整的C示例代码。可以通过学习这段源码熟悉如何在窗口中使用浏览器对象。它演示了如何显示网页或者磁盘上的HTML文件,或内存中的一个HTML字符串,并创建两个窗口来分别显示。
Browser目录也包含有一个完整的C示例代码。它演示了如何增加“返回”,“前进”,“主页”,和“停止”按钮。它是通过创建子窗口嵌入浏览器对象。
Events目录也包含有一个完整的C示例代码。它演示了如何实现自己的特殊链接,显示链接到其他的HTML字符串(在内存中)的网页 。您可以使用此技术来定义其他特殊类型的“链接”, 当用户点击的链接可以将消息发送到你的窗口。
DLL目录包含一个动态链接库,它包含EmbedBrowserObject,UnEmbedBrowserObject, DisplayHTMLPage,DisplayHTMLStr和DoPageAction函数。DLL文件还包含所有的IStorage , IOleInPlaceFrame,IOleClientSite,IOleInPlaceSite和IDocHostUIHandler 虚表和虚函数。DLL文件自动为你调用OleInitialize和OleUninitialize函数,因此使用该DLL你将不在需要调用添加OLE/COM代码到C程序。这些代码都被放在DLL文件中了。目录下还有一个example.c用于测试该动态链接库,它没有包含任何OLE/COM代码,仅仅调用了DLL。动态链接库稍被修改以便支持Unicode和ANSI,我使用函数IsWIndowUnicode来确定窗口是否使用了Unicode。
该动态链接库也包含了一些支持事件的函数,下面详细讨论这个。

事件
HTML页面通常是由许多元素,如各种标签如字体标记,链接,表格等组成的。每个元素可能有与此相关的不同的动作或事件。例如,链接生成一个事件当用户移动鼠标指针到该链接上。而当用户移动鼠标指针离开它时产生另一个事件。还有其他的活动有可能产生。
当某个元素产生特定的事件时,应用程序可能要求浏览器给与反馈。为了得到某个元素的反馈,HTML页面必须给与该元素一个ID(即一个字符串名字)。例如,网页上有一个字体元素,ID为testfont,HTML源码类似于下面的样子:
<FONT id=testfont color=red>This is some red text.</FONT>
每个事件都有一个唯一的字符串名。例如,当鼠标移到上面字体元素(即,将鼠标指针移动到红色文字上),发生的是一个鼠标悬停事件。当鼠标离开字体元素时,事件发生的是一个鼠标离开(mouseout)事件。
对于页面上的每个元素,都对应浏览器的一个IHMTLElement对象。为了获的某个元素的反馈必须获得它对应的IHMTLElement对象。DLL目录中的Dll.c中有一个名为GetWebElement的函数,用户获取特定元素的IHTMLElement对象。GetWebElement是通过传递的包含浏览器对象窗口的句柄,和所需元素的编号(名称)。要获得IHTMLElement ,我们要经过几个其他浏览器对象,我们必须先获得浏览器的IHTMLDocument2对象,和所需元素的IHTMLElementCollection对象,在获取元素的IDispatch接口,并最终从IHTMLElement对象的IDispatch获得元素的IHTMLElement对象。噢!
一旦我们有一个元素的IHTMLElement,我们就可以附加到该元素从而得到反馈。如上所述,我们需要给浏览器提供一个IDispatch对象。事件发生时,浏览器将调用IDispatch的Invoke函数。我们必须获取浏览器的IHTMLWindow3对象,然后IDispatch为参数调用它的attachEvent函数,为浏览器提供IDispatch。
为了告诉浏览器字体元素的“鼠标悬停”事件发生时,调用我们的IDispatch的Invoke函数,我们需要调用该字体元素IHTMLElement对象的put_onmouseover函数,并传递我们的IDispatch对象指针(实际上我们需要将该指针包装为一个VARIANT变量)。同样的对于“鼠标离开”事件,调用put_onmouseout函数。
  不同类型的元素可能会有不同的事件,因此一些元素,如表格,有更多的子对象我们可以通过其IHTMLElement的QueryInterface函数获得。例如,如果我们有一个FORM元素的IHTMLElement,我们可以调用它的QueryInterface获得它的IHTMLFormElement。然后,我们调用IHTMLFormElement的put_onsubmit函数附加到其提交的事件(例如当用户提交表单数据时)。查看的MSDN以确定哪些网页元素含有子对象(即哪些元素产生哪些事件)。
当然,我们希望所有的关于COM的东西都被封装在cwebpage.dll中,我们需要做的是提供一个创建代表应用程序本身的IDispatch的函数。该函数就是CreateWebEvtHandler。IDispatch的函数位于cwebpage.dll内部 ,因此应用程序不需要创建任何COM对象。应用程序为为需要反馈的元素制定一个ID。例如,应用程序可能决定指派字体元素的编号为1。然后,当DLL IDispatch Invoke获取字体元素的鼠标悬停事件,例如,我们给应用程序窗口发送一个自定义消息。自定义的信息将包括元素的ID和时间的字符串名称(即“mouseover”)。
目录HTMLEvents包含一个例子程序和一个示例网页。该网页上有几个元素,包括一个表格,和一个字体元素。应用程序能够收到这两个元素某些事件的反馈。
应用程序也能获得页面本身产生的事件(如用户双击的空白区域的页面),或浏览器的滚动条等。例子也能收到一些非页面元素产生的事件。
应用程序还能得到更多事件反馈。请查看MSDN并实验。

上传的附件 browser lib.rar
cwebpage_src.zip
Test.rar