Introduction
A lot of applications in this age of the internet can benefit from being able to browse through sites, print html content and utilize other capabilities of a web browser. Yet building those capabilities natively into your project take a lot of effort. For that matter, no sense re-inventing the wheel. Microsoft has given us the ability to tap into those capabilities which are already a part of Internet Explorer using the IWebBrowser2
interface. However, as any programmer that has dealt with COM interfaces knows, even using this interface is a bit of a task. That is why I developed the IEInstance
class. It wraps the IWebBrowser2
class and allows you to do a lot of things that are possible with the IE interfaces with simple calls to class functions. All this can be done with or without the browser being visible. So you could, for example, add the ability to navigate through webpages and collect data from them, fill out forms and submit them or perform other functions that interact with the web.
How the code works
IEInstance
is the main classes, but it works together with a few other classes to thoroughly wrap the IWebBrowser2
. It offers the basic functions of the browser, such as Navigate, Back, Forward, Refresh and Stop. These are fairly straight forward to implement once you have the IWebBrowser2 object. Other functionality the class adds are less straight forward.
IEInstance objects can be accessed and used across different threads (in a serialized manner) using its Initialize()
function. There are two parts of implementing this capability. First, when the Open()
function is called, which is the first step to using the object, the class creates a global interface table and adds the IWebBrowser2
interface to it:
if(CoCreateInstance(CLSID_StdGlobalInterfaceTable, NULL, CLSCTX_INPROC_SERVER,
IID_IGlobalInterfaceTable, (void **) &pGlobalTable)!=S_OK) throw 1010;
if(CLSIDFromProgID(OLESTR("InternetExplorer.Application"), &clsid)!=S_OK) throw 1020;
if(CoCreateInstance(clsid, NULL, CLSCTX_SERVER, IID_IUnknown, (LPVOID *) &pUnknown)!=S_OK) throw 1030;
if(pUnknown->QueryInterface(IID_IWebBrowser2, (LPVOID *) &pBrowser)!=S_OK) throw 1040;
pUnknown->Release();
if(!pBrowser) throw 1050;
if(pGlobalTable->RegisterInterfaceInGlobal(pBrowser, IID_IWebBrowser2, &cookie)!=S_OK) throw 1060;
Then, when the Initialize()
function is called, it checks the calling thread against the thread that currently "owns" the object. If it is different, the function obtains the interface from the global table and replaces the pBrowser
pointer with this pointer that is applicable in the calling thread. The original pointer is stored in the pBrowserOrg
member variable so that it can be restored when the Uninitialize()
function is called.
bool IEInstance::Initialize()
{
HRESULT hr;
CWinThread *pThread=NULL;
message.Empty();
pThread = AfxGetThread();
hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
if(hr!=S_OK && hr!=S_FALSE) return false;
if(pCurrentThread && pThread==pCurrentThread) return true;
if(pHostThread!=pThread){
while(locked) Sleep(500);
locked = true;
pBrowserOrg = pBrowser;
if(pGlobalTable->GetInterfaceFromGlobal(cookie, IID_IWebBrowser2,(void**) &pBrowser)!=S_OK) return false;
}
pCurrentThread = pThread;
return true;
}
IEInstance
seeks to make certain functions possible, which are typically only possible with user intervention. For example, saving the current document. The Save()
function will do the same thing as when you choose to save a file from a standard IE window, which pops up a Save As... dialog. However, the SaveAs()
function allows you to progamatically specify where to save the file. In IE9 and later, this is pretty straight forward since you can simply query the IWebBrowser2
for its IHTMLDocument2
interface, then query the IHTMLDocument2
for its IPersistStreamInit
interface, save it to an IStream
and then grab its data and save it to a file:
hr = pHTMLDocument2->QueryInterface(IID_IPersistStreamInit, (void**) &spPersist);
if(spPersist==NULL) throw 1001;
hr = ::CreateStreamOnHGlobal(NULL, TRUE, &spStream);
if(hr!=S_OK) throw 1002;
hr = spPersist->Save(spStream, FALSE);
if(hr!=S_OK) throw 1003;
hr = spStream->Stat(&stgstats, STATFLAG_NONAME);
if(hr!=S_OK) throw 1004;
hr = ::GetHGlobalFromStream(spStream, &hGlobal);
if(hr!=S_OK) throw 1005;
lpRawData = ::GlobalLock(hGlobal);
if(lpRawData==NULL) throw 1006;
dwSize = stgstats.cbSize.LowPart + 1;
pFile = fopen(filename, "w");
if(pFile==NULL) throw 1007;
fwrite(lpRawData, dwSize, sizeof(char), pFile);
fclose(pFile);
::GlobalUnlock(hGlobal);
However, older versions of IE don't have this interface. Instead they have an IPersistFile
when the document in the browser is HTML. However, when it is XML, this interface doesn't exist either. In this case, you step through a series of interface queries to finally get the browser's IXMLDOMDocument
object and then you can save that. Since this is somewhat obsolete at this point, I'll leave that for those interested to find that in the source code.
Similarly, printing is also something that IE usually only does with user intervention. IEInstance
implements printing in a way that allows you to specify whether to print to the default printer, bring up the print dialog or bring up a print preview window. There are actually a couple of methods that can be used to issue a print command programatically. IEInstance does this by querying the IWebBrowser2
for its IHTMLDocument2
interface, then query the IHTMLDocument2
for its IOleCommandTarget
object, then issuing that interface's Exec()
function. This approach is used instead of simply calling IWebBrowser2::ExecWB()
because the Print() function also allows you to send a path to a custom print template and this functionality was not working well with ExecWB()
when I originally developed it. That may no longer be the case.
The trickiest part about printing is that when the Exec()
function is executed, it issues the command to the browser, but doesn't wait for the command to be completed. This means that you have to do something to serialize the print command with other operations of your application. In the worst case, you just want to print something then delete the IEInstance
object, which means the function may never be completed before the object is released. IEInstance
offers a way to do this. It does that by using its companion IEEventSink
class. This class wraps the IWebBrowserEvents2
interface of Internet Explorer and is used to catch events. Among other events, it catches the OnPrintTemplateTeardown
event, which IE fires when the print is complete or the print preview window has been closed. You can just call the CloseAfterPrint()
function rather than deleting the IEInstance
object and it will delete itself after the print completes.
Working With Documents
The IEDocument
class is used for storing pointers to the MSHTML objects of the active page. After telling your IEInstance
object to navigate to a page, you call the GetDocument()
function, which waits for navigation to complete and then populates the pointers in the IEDocument
class so they are ready to query and use. This is done by querying the IWebBrowser2
for its IHTMLDocument2
interface, then calling the IHTMLDocument2::get_all()
function and then iterating through each element underneath it. Each element is queried against all the MSHTML objects that the IEDocument
class tracks to see if it finds a match. If so, a pointer is stored so that object can be easily accessed. In the midst of this, tables are organized into IETable objects, which store elements for each cell, organized by rows so you can easily access a particular row and column of a table.
varIndex.vt = VT_UINT;
for(i=0; i<pDocument->nElements; i++ ){
if(pDisp) pDisp->Release();
pDisp=NULL;
varIndex.lVal = i;
VariantInit(&var);
if(pDocument->pAll->item(varIndex, var, &pDisp)!=S_OK) throw 1110;
if(pDisp->QueryInterface(IID_IHTMLElement, (void **)&pDocument->pElement[i])!=S_OK) throw 1120;
pDocument->pElement[i]->get_tagName(&bstr);
tag = bstr;
SysFreeString(bstr);
bstr=NULL;
if(!pDocument->pHTML){
if(tag.CompareNoCase("HTML")==0) pDocument->pHTML = pDocument->pElement[i];
continue; }
if(!pDocument->pBody){
if(pDisp->QueryInterface(IID_IHTMLBodyElement, (void **)&pDocument->pBody)==S_OK){
continue;
}
}
if(tag.CompareNoCase("TABLE")==0){
nTable = pDocument->nTables;
pDocument->nTables++;
pTable[nTable].pElement = pDocument->pElement[i];
nRows[nTable] = 0;
nCells[nTable] = 0;
pDocument->pElement[i]->get_innerHTML(&bstr);
tableHTML = bstr;
SysFreeString(bstr);
bstr=NULL;
nLevels=0;
find=0;
while(find>=0){
find = tableHTML.Find('<', find);
if(find<0) break;
if(find>=tableHTML.GetLength()-1) break;
find++;
if(tableHTML.Mid(find, 5).CompareNoCase("TABLE")==0){
nLevels++;
continue;
}
if(tableHTML.Mid(find, 6).CompareNoCase("/TABLE")==0){
nLevels--;
continue;
}
if(nLevels>0) continue;
if(tableHTML.GetAt(find+1)=='/') continue;
if(tableHTML.Mid(find, 2).CompareNoCase("TR")==0){
pTable[nTable].nRows++;
continue;
}
if(tableHTML.Mid(find, 2).CompareNoCase("TD")==0 || tableHTML.Mid(find, 2).CompareNoCase("TH")==0){
pTable[nTable].nCells++;
continue;
}
}
if(pTable[nTable].nRows>0){
pTable[nTable].pRow = new IHTMLElement*[pTable[nTable].nRows];
if(!pTable[nTable].pRow) throw 1130;
}
if(pTable[nTable].nCells>0){
pTable[nTable].pCell = new IHTMLElement*[pTable[nTable].nCells];
pTable[nTable].cellRow = new int[pTable[nTable].nCells];
if(!pTable[nTable].pCell || !pTable[nTable].cellRow) throw 1140;
}
continue;
}
if(tag.CompareNoCase("TR")==0){
while(nRows[nTable]>=pTable[nTable].nRows) nTable--;
if(nTable<0) throw 1008;
pTable[nTable].pRow[nRows[nTable]] = pDocument->pElement[i];
nRows[nTable]++;
continue;
}
if(tag.CompareNoCase("TD")==0 || tag.CompareNoCase("TH")==0){
while(nCells[nTable]>=pTable[nTable].nCells) nTable--;
if(nTable<0) throw 1150;
pTable[nTable].pCell[nCells[nTable]] = pDocument->pElement[i];
pTable[nTable].cellRow[nCells[nTable]] = nRows[nTable];
nCells[nTable]++;
continue;
}
if(pDisp->QueryInterface(IID_IHTMLAnchorElement, (void **)&pAnchorElem)==S_OK){
pHyperlink[pDocument->nHyperlinks] = pDocument->pElement[i];
pDocument->nHyperlinks+=1;
continue;
}
if(pDisp->QueryInterface(IID_IHTMLFormElement, (void **)&pForm[pDocument->nForms])==S_OK){
pDocument->nForms+=1;
continue;
}
if(pDisp->QueryInterface(IID_IHTMLButtonElement, (void **)&pButtonElem)==S_OK){
pButton[pDocument->nButtons] = pDocument->pElement[i];
pDocument->nButtons+=1;
continue;
}
if(pDisp->QueryInterface(IID_IHTMLInputButtonElement, (void **)&pInputButtonElem)==S_OK){
pButton[pDocument->nButtons] = pDocument->pElement[i];
pDocument->nButtons+=1;
continue;
}
if(pDisp->QueryInterface(IID_IHTMLSelectElement, (void **)&pDropDownMenu[pDocument->nDropDownMenus])==S_OK){
pDocument->nDropDownMenus+=1;
continue;
}
if(pDisp->QueryInterface(IID_IHTMLInputImage, (void **)&pInputImageElem)==S_OK){
pInputImage[pDocument->nInputImages] = pDocument->pElement[i];
pDocument->nInputImages+=1;
continue;
}
if(pDisp->QueryInterface(IID_IHTMLOptionButtonElement, (void **)&pOptionButton[pDocument->nOptionButtons])==S_OK){
pDocument->nOptionButtons+=1;
continue;
}
if(pDisp->QueryInterface(IID_IHTMLTextAreaElement, (void **)&pTextArea[pDocument->nTextAreas])==S_OK){
pDocument->nTextAreas+=1;
continue;
}
if(pDisp->QueryInterface(IID_IHTMLInputTextElement, (void **)&pTextBox[pDocument->nTextBoxes])==S_OK){
pDocument->nTextBoxes+=1;
continue;
}
if(pDisp->QueryInterface(IID_IHTMLInputFileElement, (void **)&pFileBox[pDocument->nFileBoxes])==S_OK){
pDocument->nFileBoxes+=1;
continue;
}
}
With these object pointers now stored for easy access, the IEDocument
class contains a lot of functions that can be used to click hyperlinks or buttons or fill text boxes or areas. For example, find a particular button by name using the ButtonByName()
function. It will return an index to the button you want, then call the ButtonClick()
function and pass this index in to click the button.
int IEDocument::ButtonByName(const char *name)
{
int i;
for(i=0; i<nButtons; i++){
if(ButtonName(i)==name) return i;
}
return -1; }
As you can imagine, you can use these functions to navigate to a web form, fill out the form, then submit it.
Using the code
These three classes and other supporting classes are compiled into a static library, which you can link to your project by simply adding
#include "IEInstance.h"
to your source code or header file.
You can then create an IEInstance
object and call the Open()
function to launch Internet Explorer. You can choose to set its only argument, visible
, to true or false to make the window visible or hidden.
You can then proceed to work with the browser, calling the Navigate()
function to go to a url, then pragmatically clicking links, collecting text or other functions as you need to.
After calling a function that causes the browser to navigate, you'll need to call the GetDocument()
function. You'll need to do this regardless of whether you want the IEInstance
to collect the document objects into its IEDocument
object since this function is also the means of waiting for the navigation to complete before proceeding. It is also recommended to check for a navigation error using the IsErrorPage()
function.
When you are finished with the browser, you can either call the Release()
function to disconnect the browser window from the object before deleting it or Close()
to close the browser window.
History
Version 1.0: Initial Release