Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Embed an HTML control in your own window using plain C

0.00/5 (No votes)
3 Aug 2006 52  
Specifically shows how to embed a browser OLE object in your own window, and more generally demonstrates how to manipulate and create COM/OLE objects, in plain C (ie, no MFC, WTL, ATL, .NET, C#, nor even C++). The latter is applicable to many other uses, such as creating your own script engine.

Sample Image - cwebpage.jpg

Introduction

There are numerous examples that demonstrate how to embed Internet Explorer as an OLE/COM object in your own window. But these examples typically use Microsoft Foundation Classes (MFC), .NET, C#, or at least Windows Template Library (WTL) because those frameworks have pre-fabricated "wrappers" to easily give you an "HTML control" to embed in your window. If you're trying to use plain C, without MFC, WTL, .NET, C#, or even any C++ code at all, then there is a dearth of examples and information how to deal with COM objects such as IE's IWebBrowser2. Here is an article and working example in C to specifically show you what you need to do in order to embed IE in your own window.

In fact, I've even wrapped up the example C code (to embed IE in your own window) into a Dynamic Link Library (DLL) so that you can simply call one function to display a web page or some HTML string in a window you create. You won't even need to get your hands dirty with COM (unless you plan to modify the source of the DLL).

Before proceeding with this article, you should first read my series of articles regarding COM in plain C. Part 1 discusses information you'll need to know in order to use COM objects. We will also have to deal with objects that have multiple interfaces, as discussed in Part 4. We'll be using automation datatypes, as discussed in Part 2. Finally, we'll also be dealing with events (callbacks), discussed in Part 5.

Mandatory COM objects we must create

After reading my above series of COM articles, you've got some needed background on writing COM objects in plain C. Let's now examine what we need to host the browser object. You may wish to peruse the source code file Simple.c (in the Simple directory) as you read the following discussion.

First of all, the browser object expects us to provide (at least) 3 of our own COM objects. We need an IOleInPlaceFrame, IOleClientSite, and an IOleInPlaceSite objects. All of these objects (and their VTables, and GUIDs) are already defined for us in Microsoft's include files (shipped with your C interpreter or in the SDK downloadable from Microsoft's web site). So, they each have their own specific pre-defined set of functions in the VTable.

Let's just examine our IOleClientSite object. It has a VTable which is defined as a IOleClientSiteVtbl struct. Essentially, it's an array of 9 pointers to functions that we must supply in our program. (ie, We have to write 9 specific functions just for our IOleClientSite object). Of course, the first 3 functions will be the QueryInterface, AddRef, and Release functions for our IOleClientSite object. In Simple.c, I've named those three functions Site_QueryInterface, Site_AddRef, and Site_Release (to avoid any name conflicts with the QueryInterface, AddRef, and Release functions of our other COM objects). In fact, I've named the other 6 functions starting with Site_. They have names like Site_SaveObject, Site_ShowObject, etc. Our IOleClientSite functions are called by the browser object to interact with our window that contains the browser object. What the specific purpose of each of those functions is, and what arguments are passed to it, you can check for yourself by looking through the documentation on MSDN about the IOleClientSite object.

To create the VTable for our IOleClientSite object, the easiest thing to do is just declare it as a static global, and initialize it with the pointers to our 9 functions. Here is how we do that in our C source:

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

Now have a global variable named MyIOleClientSiteTable which is a properly initialized VTable for our IOleClientSite object.

In Simple.c, you'll see that we also declare our other objects' VTables as globals. But we do not declare our objects themselves as globals. We are going to add some extra members to these objects for our own private data. For example, instead of a just an ordinary IOleInPlaceFrame, we're going to define our own _IOleInPlaceFrameEx which contains an embedded IOleInPlaceFrame plus an extra HWND where we can store the handle to our own window. Notice that this extra HWND member is added to the end of the struct, after the IOleInPlaceFrame. That is very important. The IOleInPlaceFrame (with its VTable pointer) must come first. And note that our extra data is window-specific. In other words, we'll need a different IOleInPlaceFrame, IOleClientSite, and IOleInPlaceSite struct per each window that has an embedded browser object. For this reason, we'll allocate them when we create a window, instead of declaring them as globals.

The browser object considers our IOleInPlaceFrame and IOleInPlaceSite objects to be sub-objects of our IOleClientSite object. So, when the browser wants/needs a pointer to one of those, it is going to call one of our IOleClientSite functions to ask us to return a pointer to it. Usually the function it calls is our IOleClientSite's QueryInterface function. (But for some objects, such as our IOleInPlaceFrame object, the browser will request the pointer by calling a different IOleClientSite function. That's just the way it is). The implication here is that our IOleClientSite functions will need to have access to our IOleInPlaceFrame and IOleInPlaceSite objects so that our IOleClientSite functions can return pointers to any one (when asked to do that by the browser). For this reason, we're going to define one large object (_IOleClientSiteEx) which has our 3 objects - IOleClientSite, IOleInPlaceFrame and IOleInPlaceSite - all embedded inside this large struct. That makes it easy to locate any one object from another, simply using pointer arithmetic. The only requirement is that our IOleClientSite be the first thing inside of this larger object. That way, this larger object can masquerade as an ordinary IOleClientSite.

You can consult the MSDN documentation to learn what the functions in our IOleInPlaceFrame, IOleClientSite, and IOleInPlaceSite VTables are supposed to do, and what is passed to them. In Simple.c, we employ only as much functionality as is needed to display a web page in a window of our own creation.

Extra COM objects we may choose to create

As mentioned, the browser object expects us to supply at least the 3 above objects. But there are other objects we may optionally implement in our program in order to support additional interaction with the browser object. In particular, a IDocHostUIHandler is very useful. It lets us control certain user interface features, such as being able to replace/disable the pop-up context menu when the user right-clicks on the embedded browser object, or determine whether scroll bars or borders or other such things are rendered, or prevent embedded scripts in the web page from running, or have a new browser window automatically open if the user clicks on any link, etc. Because such an object is so useful, we also implement an IDocHostUIHandler interface in our example C code. (ie, We have a IDocHostUIHandler struct, 18 IDocHostUIHandler functions, and a VTable containing pointers to those 18 functions). We embed this IDocHostUIHandler inside of our large _IOleClientSiteEx object.

Obtain the browser object

Before we obtain Microsoft's browser object, we must call OleInitialize once to make sure that the OLE/COM system is initialized for our process. Normally, when using COM, you call CoInitialize. But the browser needs some extra OLE initialization done (which CoInitialize does not do). So we call OleInitialize, which itself calls CoInitialize for us.

Now we're ready to obtain a browser object. Our function EmbedBrowserObject is where we obtain a browser object and embed it into a particular window. We need do this only once, so we call EmbedBrowserObject right when we create the window (and pass the window handle to EmbedBrowserObject).

First, since we need a separate IOleInPlaceFrame, IOleClientSite, IOleInPlaceSite and IDocHostUIHandler object for each window (that hosts the browser control), EmbedBrowserObject allocates these 4 objects (ie, structs). Actually, since we've placed all 4 of these structs inside of our own, larger struct (ie, that we defined as a _IOleClientSiteEx struct), one call to GlobalAlloc a _IOleClientSiteEx struct does it all. After we allocate those 4 COM objects, we have to initialize them (ie, stuff a pointer to each VTable in its respective lpVtbl member). Furthermore, we'll save a pointer to this allocated struct in our window's USERDATA field. That way, our window procedure (and other functions) can easily retrieve our COM objects from the window handle.

Now, we obtain one of Microsoft's IWebBrowser2 objects (ie, the main object of Internet Explorer) with a call to the operating system function CoCreateInstance, passing the GUID for the IWebBrowser2 object (defined as the symbol IID_IWebBrowser2 in Microsoft's include files). We also pass the GUID of the DLL in which Microsoft's browser control resides (defined as the symbol CLSID_WebBrowser).

If all goes well, CoCreateInstance will return a pointer to a newly created IWebBrowser2 object. The pointer is stored in our variable webBrowser2.

Next, we need to get the IWebBrowser2 object's IOleObject sub-object. We get a sub-object by calling the parent object's QueryInterface function. So we call IWebBrowser2's QueryInterface to get a pointer to its IOleObject sub-object (that we store in our variable browserObject). This sub-object is the one we mostly use to embed IE in our own window, and control the display of web pages. The IOleObject sub-object is not yet embedded. It is merely created. For the remainder of this article, I'll refer to this IOleObject sub-object as simply the browser object.

Next, we need to call the browser object's SetClientSite to give it a pointer to our own IOleClientSite object. The browser object will need to call some of our IOleClientSite functions to get information from us.

We also call its SetHostNames() to pass the browser the name of our application (so it can display that in its own message boxes).

So how do we embed the browser object? We need to call the browser object's DoVerb function to send the browser object a command that tells it to embed itself in our window (OLEIVERB_SHOW). We also pass our window handle to DoVerb. While we're inside of this call to DoVerb, the browser object is going to call some of our IOleClientSite functions. It will have called several of them before DoVerb returns.

Sending a OLEIVERB_SHOW command via DoVerb does not display any web page. (We have another function we can call to do that, after we're finished with EmbedBrowserObject). It merely embeds the browser object in our window so that it is ready to display a web page and have it shown in our window.

At the end of EmbedBrowserObject, we call the IWebBrowser2 object's Release function. We don't need this object any more (and if we did, we could call the IOleObject sub-object's QueryInterface. Remember that a sub-object's QueryInterface can always be used to locate its parent). But we don't Release the sub-object. We still need that in order to call its functions to display web pages, and do other things. We won't release the sub-object until later in UnEmbedBrowserObject, when we're finally done using the browser object.

Display a web page

We can call our function DisplayHTMLPage to display a URL or HTML file on disk. What we do in DisplayHTMLPage is very similiar to what we do in EmbedBrowserObject. We use the browser object's QueryInterface() to grab pointers to other objects associated with it, and use the VTables of those other objects to call their functions in order to display a URL or HTML file on disk. Again, you can consult the MSDN documentation to learn more about the objects we're asking for and their functions we're calling.

Basically, we need to call the IWebBrowser2's Navigate2 function to display a web page, passing the URL of the page we wish to display. Our URL (ie, web address, such as "http://www.microsoft.com" or an HTM filename on disk such as "c:\myfile.htm") must be passed to the IWebBrowser2's Navigate2 function as a BSTR. What's more, our BSTR needs to be stuffed into a VARIANT struct, and that VARIANT struct is then passed to Navigate2.

Navigate2 will fetch the contents of the page (from wherever it resides) and display it in the browser object embedded in our window.

Display an HTML formatted buffer

What if we have a buffer (in memory) that already contains the HTML page we wish to display? In this case, we can get the brower object to display it, but this involves a few extra steps.

First, we need to create an empty page, which we can do by calling Navigate2 and passing it a URL of about:blank. This is a special URL that the IE engine recognizes as a blank page.

Next, we get the browser's IHTMLDocument2 object, and call its write function to tell it to write the contents of our buffer to this empty web page. We must format our buffer as a BSTR, and also wrap it in a standard COM struct known as a "safe array". COM provides some functions we can call to allocate a safe array (and also free it when we're done with it).

Our function DisplayHTMLStr accomplishes this.

Display a page from a CHM file

The browser object can display a page from a compiled help (.CHM) file by using the special URL protocol its:. Just call DisplayHTMLPage like so:

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

Resize the browser display area

If the user resizes our window containing the browser object, the object will not automatically resize its display area. We need to specifically call some browser functions if we wish to enlarge/shrink the display area. We call put_Width and put_Height, passing the desired width and height, respectively.

Our function ResizeBrowser accomplishes this. Normally, this is called when we process the WM_SIZE message for our window.

Forward, back, and other actions

In fact, you can create several browser objects if desired, for example, if you wanted several windows - each hosting its own browser object so that each window could display its own web page. Simple.c creates two windows that each host a browser object. (So we call EmbedBrowserObject once for each window). In one window, we call DisplayHTMLPage to display Microsoft's web page. In another window, we call DisplayHTMLStr to display some HTML string in memory.

Indeed, after we've embedded a browser object, we can call DisplayHTMLPage or DisplayHTMLStr repeatedly to change what is being displayed.

The web browser automatically keeps a "history" of the URLs we have displayed. We can cause the browser to go back to displaying a previously-viewed page by calling the browser's GoBack function. This would be akin to clicking on IE's "Back" button. In fact, there are several actions that correspond to IE buttons, such as Refresh, Forward, Search, etc that we can invoke. Our function DoPageAction serves as a generic interface to several of these browser functions. (Although Simple.c doesn't utilize this, you could add Back, Forward, Refresh, Search, etc, buttons to the example code, and utilize DoPageAction).

Free the browser object

When we're finally done with the browser object, we need to Release it to free any resources it used. We do that in UnEmbedBrowserObject. This needs to be done only once, so we do it right when the window is being destroyed. And we need to call OleUninitialize before our program exits.

The cwebpage.dll

The Simple directory contains a complete C example with everything in one source file. Study this to familiarize yourself with the technique of using the browser object in your own window. It demonstrates how to display either an HTML file on the web or disk, or an HTML string in memory, and creates 2 windows to do such.

The Browser directory also contains a complete C example. It demonstrates how to add "Back", "Forward", "Home", and "Stop" buttons. It creates a child window (inside of the main window) into which the browser object is embedded.

The Events directory also contains a complete C example. It demonstrates how to implement your own special link to display a web page with links to other HTML strings (in memory). You could use this technique to define other specialized types of "links" that can send messages to your window when the user clicks upon the link.

The DLL directory contains a DLL that has the functions EmbedBrowserObject, UnEmbedBrowserObject, DisplayHTMLPage, DisplayHTMLStr, and DoPageAction in it. The DLL also contains all of the IStorage, IOleInPlaceFrame, IOleClientSite, IOleInPlaceSite, and IDocHostUIHandler VTables and their functions. The DLL also calls OleInitialize and OleUninitialize on your behalf. So to use this DLL, you don't need to put any OLE/COM coding in your C program at all. It's all in the DLL instead. And there is a small example called Example.c that uses the DLL. It's just Simple.c with all the OLE/COM stuff ripped out of it and replaced with calls to use the DLL. The DLL functions have been modified slightly to support both UNICODE or ansi. I use the function IsWindowUnicode to discover whether the application window (hosting the browser object) is UNICODE or not.

The DLL also has a few new functions to support events, which will be discussed below.

Events

An HTML page is typically composed of numerous elements, such as various tags like a FONT tag, links, forms, etc. Each element may have various "actions" or "events" associated with it. For example, a link generates an event when the user moves the mouse pointer over it. It generates another event when the user moves the mouse pointer off of it. And there are other events it may generate.

An application may ask the browser to provide feedback when a particular event occurs with a particular element. In order for us to get feedback about an element, the HTML page must be written to give that element an ID (ie, a string name). For example, let's assume that our page has a FONT element on it. Let's arbitrarily give this FONT element an ID of testfont. Here is how the HTML page's FONT element may look:

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

Each event itself has a unique string name. For example, when the mouse is moved over the above FONT element (ie, the mouse pointer is moved over the red text), the event which occurs is a mouseover event. When the mouse is moved off of the FONT element, the event which occurs is a mouseout event.

For every element on the web page, the web browser has an IHTMLElement object for it. To get feedback on an element, we first must get its IHTMLElement object. In the DLL directory's Dll.c is a function called GetWebElement which shows how to get an IHTMLElement object for a particular element. GetWebElement is passed the window containing the browser object, and the ID (name) of the desired element. To get the IHTMLElement, we have to run through several other browser objects. We have to get the browser's IHTMLDocument2, and then get the IHTMLElementCollection (for the desired element) from that, and then get the element's IDispatch, and finally get the element's IHTMLElement object from that IDispatch. Whew!

Once we have an element's IHTMLElement, we can then "attach" to the element to receive feedback on its events. As you've learned from my article about COM events, we need to provide the browser with an IDispatch object we create. The browser will call our IDispatch's Invoke function when an event occurs. We must give our event IDispatch to the browser, which we do by obtaining the browser's IHTMLWindow3 object and calling its attachEvent function, passing our IDispatch.

Then, to tell the browser to call our IDispatch's Invoke whenever a FONT element's "mouseover" event occurs, we call that FONT IHTMLElement's put_onmouseover function, passing a pointer to our IDispatch. (Actually, we need to wrap our IDispatch pointer in a VARIANT). To get feedback on that FONT element's "mouseout" event, we call that FONT IHTMLElement's put_onmouseout function, passing a pointer to our IDispatch.

Different types of elements may have different events, and so some elements, such as a FORM, have additional sub-objects we can get via its IHTMLElement's QueryInterface. For example, if we had a FORM element's IHTMLElement, we could call its QueryInterface to get its IHTMLFormElement. Then, we could call the IHTMLFormElement'sput_onsubmit function to attach to its submit event (ie, when the user submits the FORM data). Consult MSDN docs to determine which web page elements have which sub-objects (ie, which elements generate which events).

Of course, we want to isolate all of the COM stuff in our cwebpage.dll, so what we're going to do is provide a function that will create an IDispatch on behalf of the application. That function is CreateWebEvtHandler. The IDispatch's functions will be inside of cwebpage.dll, so the application will not need to create any of its own COM objects. To abstact this, we'll make the application assign an ID number of its choice to each element it wants feedback upon. For example, the app may decide to assign the ID number 1 to the FONT element. Then when our DLL IDispatch Invoke gets feedback on that FONT element's mouseover event, for example, we will pass a custom message to the app's window. The custom message will include the ID number of the element, and the string name of the event (ie, "mouseover").

In the directory HTMLEvents is an example application, and an example web page. The web page has several elements on it, including a FORM, and a FONT element. The application receives feedback on some events for both elements.

An application can also receive feedback for events associated with the web page itself (such as the user double-clicking on a blank area of the page), or the browser's scroll bars, etc. The example receives feedback on some of those non-element events too.

There are lots more events an app could get feedback upon. Consult MSDN docs, and experiment.

History

  • Initial release upon December 1, 2002.
  • Update upon December 6, 2002. Added IDocHostUIHandler interface, and the function DoPageAction. Applied a fix to UnEmbedBrowserObject() and DisplayHTMLStr(). Revamped the comments in the code to be more explicit and clear. Added the Browser.c and Events.c examples.
  • Update upon May 15, 2006. Added the ability to receive "events" from the web page, such as knowing when the user clicks upon some item on the page, or submits a form, etc. Incorporated both UNICODE and ANSI support into the one DLL.
  • Update upon August 1, 2006. Rewrote the article to reference my series on COM in plain C, while expounding more upon browser specific matters. Also updated the download link on this page to contain the latest code base.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here