Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++

IWebBrowser2 Wrapper Class

4.67/5 (6 votes)
5 Mar 2014CPOL6 min read 57.8K   2K  
A class that wraps the IWebBrowser2 interface of Microsoft Internet Explorer to enable you to browse, print and save web content programatically

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:

C++
//Make this a global instance
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;

//Add to global table
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.

C++
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){
        //Make sure this browser is not locked by another process then lock it
        while(locked) Sleep(500);
        locked = true;

        //Preserve original browser pointer
        pBrowserOrg = pBrowser;

        //Obtain browser pointer from cookie
        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:

  // Get the right interface
hr = pHTMLDocument2->QueryInterface(IID_IPersistStreamInit, (void**) &spPersist);
if(spPersist==NULL) throw 1001;

// Create the stream object
hr = ::CreateStreamOnHGlobal(NULL, TRUE, &spStream);
if(hr!=S_OK) throw 1002;

// Save the HTML
hr = spPersist->Save(spStream, FALSE);
if(hr!=S_OK) throw 1003;

// Get the size of the stream
hr = spStream->Stat(&stgstats, STATFLAG_NONAME);
if(hr!=S_OK) throw 1004;

// Get the global memory
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.

C++
 //Organize Tags into Types
varIndex.vt = VT_UINT;
for(i=0; i<pDocument->nElements; i++ ){
    //Release previous dispatch (if applicable)
    if(pDisp) pDisp->Release();
    pDisp=NULL;

    //Get dispatch to item i
    varIndex.lVal = i;
    VariantInit(&var);
    if(pDocument->pAll->item(varIndex, var, &pDisp)!=S_OK) throw 1110;

    //Get item i
    if(pDisp->QueryInterface(IID_IHTMLElement, (void **)&pDocument->pElement[i])!=S_OK) throw 1120;

    //Get tag name
    pDocument->pElement[i]->get_tagName(&bstr);
    tag = bstr;
    SysFreeString(bstr);
    bstr=NULL;

    //HTML Tag
    if(!pDocument->pHTML){
        if(tag.CompareNoCase("HTML")==0) pDocument->pHTML = pDocument->pElement[i];
        continue; //Skip all tags before the HTML tag is found
    }

    //Body
    if(!pDocument->pBody){
        if(pDisp->QueryInterface(IID_IHTMLBodyElement, (void **)&pDocument->pBody)==S_OK){
            continue;
        }
    }

    //Table
    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;
            }
        }

        //Prepare row and cell arrays
        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;
    }

    //Row
    if(tag.CompareNoCase("TR")==0){
        //Should only need this when no cells exist in the last row of a table
        while(nRows[nTable]>=pTable[nTable].nRows) nTable--;
        if(nTable<0) throw 1008;

        pTable[nTable].pRow[nRows[nTable]] = pDocument->pElement[i];
        nRows[nTable]++;
        continue;
    }

    //Cell
    if(tag.CompareNoCase("TD")==0 || tag.CompareNoCase("TH")==0){
        //Last cell has been assigned go back to closest non-full table
        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;
    }

    //Hyperlinks
    if(pDisp->QueryInterface(IID_IHTMLAnchorElement, (void **)&pAnchorElem)==S_OK){
        pHyperlink[pDocument->nHyperlinks] = pDocument->pElement[i];
        pDocument->nHyperlinks+=1;
        continue;
    }

    //Forms
    if(pDisp->QueryInterface(IID_IHTMLFormElement, (void **)&pForm[pDocument->nForms])==S_OK){
        pDocument->nForms+=1;
        continue;
    }

    //Buttons
    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;
    }

    //Select Drop Down Menus
    if(pDisp->QueryInterface(IID_IHTMLSelectElement, (void **)&pDropDownMenu[pDocument->nDropDownMenus])==S_OK){
        pDocument->nDropDownMenus+=1;
        continue;
    }

    //Image Inputs
    if(pDisp->QueryInterface(IID_IHTMLInputImage, (void **)&pInputImageElem)==S_OK){
        pInputImage[pDocument->nInputImages] = pDocument->pElement[i];
        pDocument->nInputImages+=1;
        continue;
    }

    //Option Buttons
    if(pDisp->QueryInterface(IID_IHTMLOptionButtonElement, (void **)&pOptionButton[pDocument->nOptionButtons])==S_OK){
        pDocument->nOptionButtons+=1;
        continue;
    }

    //Text Areas
    if(pDisp->QueryInterface(IID_IHTMLTextAreaElement, (void **)&pTextArea[pDocument->nTextAreas])==S_OK){
        pDocument->nTextAreas+=1;
        continue;
    }

    //Text Boxes
    if(pDisp->QueryInterface(IID_IHTMLInputTextElement, (void **)&pTextBox[pDocument->nTextBoxes])==S_OK){
        pDocument->nTextBoxes+=1;
        continue;
    }

    //File Boxes
    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.

C++
int IEDocument::ButtonByName(const char *name)
{
    int i;


    for(i=0; i<nButtons; i++){
        if(ButtonName(i)==name) return i;
    }
    
    return -1; //Not found
} 

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

C++
#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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)