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

Usable Automation Shell for Internet Explorer

0.00/5 (No votes)
22 Oct 2011 1  
A correctly functioning shell upon which to further develop IE Automation applications.

Introduction

The below code has a few basic objectives:

  • To begin an instance of Internet Explorer automatically.
  • To detect the basic automation dispatch event messages that Internet Explorer sends to windows when it loads and exits.
  • To set up our automation program correctly, so that we can add any or all available Internet Explorer dispatch events using the MFC ClassWizard.
  • To notify us that the top Internet Explorer page has loaded, for multi-frame web pages that fire more than one DocumentComplete event.
  • To accomplish the surprisingly difficult task of being able to exit from Internet Explorer, or from the code, without leaving instances of either one running or crashing windows.

A Word on Crashes

If you do not use the automation option as described below when beginning a new project, you will get a windows error box each time you build due to not having a permanent GUID.

You cannot automate Internet Explorer in one direction, controlling it without detecting incoming dispatch events, because if someone closes it and you exit your code without a proper AfxConnectionUnadvise, you will get a windows crash.

You have to detect events and exit correctly as shown below, or else the MFC generated code may leave a hidden instance of the code running on your computer, preventing any future builds and making you use the ctrl-alt-del task manager to find it and force close it.

Background

The code is Microsoft Visual C++ 6.0 (updated to service pack 6) specific.

If you have already upgraded, you can dust off your old copy and install it again. You cannot do anything else to an automated web page until you have mastered all of the above. Therefore, you have to learn all of this, before you can actually do things to an automated web page.

There are many different ways you can control an instance of Internet Explorer using C++. We will use the IWebBrowser2 control to start it, and use DWebBrowserEvents2 to detect Dispatch Map messages. It is much, much easier to use IWebBrowser2 to control IE than it is to use DWebBrowserEvents2 to detect IE events it turns out.

Since we will be automating IE, we need a basic understanding of the mysterious MFC commands that are needed for automation:

AfxOleInit(); Application Top Level
AfxEnableControlContainer(); Application Top Level
EnableAutomation(); AutoProxy Class
AfxOleLockApp(); AutoProxy Class
AutoProxy->GetIDispatch(); Dialog Class
CoCreateInstance(CLSID_InternetExplorer); Dialog Class
AfxConnectionAdvise(); Dialog Class
AfxConnectionUnadvise(); Dialog Class Exit
AfxOleUnlockApp(); AutoProxy Destructor

STEP ONE

Start a new MFC AppWizard(EXE) project from the “File” – “New” menu, Projects tab, and enter a project name: of IEC2 and hit the OK button.

MFC AppWizard – Step 1: choose the “Dialog based” button and then click Next.

MFC AppWizard – Step 2: check the “Automation” option and then click Next.

If you don’t select the Automation option now, it is still possible to proceed as others have done, but you will no doubt get a constantly annoying windows box to click each time it compiles a new temporary GUID for your project.

Don’t change MFC AppWizard – Steps 3 and 4. Go to the end and click Finish. Click OK to accept the automatically generated files.

The MFC generated dialog box will appear. Delete the “TODO: Place dialog controls here.” static text box, and resize the dialog box to make it smaller so that it is just bigger than the OK and Cancel buttons.

STEP TWO

Go to your StdAfx.h header file and add the following includes:

#include <atlbase.h> //needed for CComQIPtr
#include <afxctl.h> //needed for AfxConnectionAdvise
// stdafx.h : include file for standard system include files,
#include <afxwin.h> // MFC core and standard components
#include <afxext.h> // MFC extensions
#include <afxdisp.h> // MFC Automation classes
#include <afxdtctl.h> // MFC support for Internet Explorer 4 Common Controls
#ifndef _AFX_NO_AFXCMN_SUPPORT
#include <afxcmn.h> // MFC support for Windows Common Controls
#endif // _AFX_NO_AFXCMN_SUPPORT
#include <atlbase.h>
#include <afxctl.h>

STEP THREE

The AutoProxy destructor in the DlgProxy.h header is in the wrong place. Move it from a protected function to a public function. Add protected variables CComBSTR bstrURL and CComBSTR bstrName to the CIEC2DlgAutoProxy class.

// DlgProxy.h : header file class CIEC2DlgAutoProxy : public CCmdTarget
{
    DECLARE_DYNCREATE(CIEC2DlgAutoProxy)
    CIEC2DlgAutoProxy(); // protected constructor used by dynamic creation

// Attributes
public:
    CIEC2Dlg* m_pDialog;
    virtual ~CIEC2DlgAutoProxy();

// Operations
public:

// Overrides

// ClassWizard generated virtual function overrides
//{{AFX_VIRTUAL(CIEC2DlgAutoProxy)

public:
    virtual void OnFinalRelease();
//}}AFX_VIRTUAL
// Implementation

protected:
    CComBSTR bstrURL;
    CComBSTR bstrName;

STEP FOUR

Add the following bold public and protected variables to the CIEC2Dlg class:

// IEC2Dlg.h : header file
class CIEC2Dlg : public CDialog
{
    DECLARE_DYNAMIC(CIEC2Dlg);
    friend class CIEC2DlgAutoProxy;

// Construction
public:
    BOOL m_bLoaded;
    CString m_csMyStr;
    CComQIPtr<IWebBrowser2>m_pIE;
    DWORD m_dwConnAdvise;
    BOOL m_bQuit;

    CIEC2Dlg(CWnd* pParent = NULL); // standard constructor
    virtual ~CIEC2Dlg();

    // Dialog Data
    //{{AFX_DATA(CIEC2Dlg)
    enum { IDD = IDD_IEC2_DIALOG };

    // NOTE: the ClassWizard will add data members here
    //}}AFX_DATA
    // ClassWizard generated virtual function overrides
    //{{AFX_VIRTUAL(CIEC2Dlg)

protected:
    virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support

//}}AFX_VIRTUAL
// Implementation

protected:
    IUnknown* m_pUnk;
    CIEC2DlgAutoProxy* m_pAutoProxy;
    HICON m_hIcon;
    BOOL CanExit();

STEP FIVE

Add the bolded text to the CIEC2Dlg::OnInitDialog() Function.

BOOL CIEC2Dlg::OnInitDialog()
{
    CDialog::OnInitDialog();

    // Add "About..." menu item to system menu.
    // IDM_ABOUTBOX must be in the system command range.
    ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
    ASSERT(IDM_ABOUTBOX < 0xF000);

    CMenu* pSysMenu = GetSystemMenu(FALSE);
    if (pSysMenu != NULL)
    {
        CString strAboutMenu;
        strAboutMenu.LoadString(IDS_ABOUTBOX);
        if (!strAboutMenu.IsEmpty())
        {
            pSysMenu->AppendMenu(MF_SEPARATOR);
            pSysMenu->AppendMenu(MF_STRING, 
            IDM_ABOUTBOX, strAboutMenu);
        }
    }

    // Set the icon for this dialog. The framework does this automatically
    // when the application's main window is not a dialog
    SetIcon(m_hIcon, TRUE); // Set big icon
    SetIcon(m_hIcon, FALSE); // Set small icon

    // TODO: Add extra initialization here
    m_pAutoProxy=new CIEC2DlgAutoProxy;
    m_pUnk=m_pAutoProxy->GetIDispatch(FALSE);

    m_pIE.CoCreateInstance(CLSID_InternetExplorer);
    m_pIE->put_AddressBar(TRUE);
    m_pIE->put_ToolBar(TRUE);
    m_pIE->put_MenuBar(TRUE);
    m_pIE->put_Visible(TRUE);
    m_csMyStr=_T(<a href="http://www.msdn.com/">"www.msdn.com");</a />

    COleVariant vaURL((LPCTSTR)m_csMyStr);
    m_pIE->Navigate2(&vaURL,COleVariant((long)0,VT_I4),

    COleVariant((LPCTSTR)NULL,VT_BSTR),COleSafeArray(),
    COleVariant((LPCTSTR)NULL,VT_BSTR));
    m_pIE->put_Top(600);
    m_pIE->put_Left(700);

    BOOL bResult=AfxConnectionAdvise((LPUNKNOWN)m_pIE,
    DIID_DWebBrowserEvents2,m_pUnk,FALSE,&m_dwConnAdvise);
    if(bResult==FALSE)
        AfxMessageBox("Failed to AfxConnectionAdvise.");

    //maximize IE automation window
    HDC hdc = ::GetDC(NULL);  // Screen DC used to get current display settings
    if (hdc == NULL)
        return FALSE;
    DWORD m_dwWidth=GetDeviceCaps(hdc, HORZRES);
    DWORD m_dwHeight=GetDeviceCaps(hdc, VERTRES);
    m_pIE->put_Top(0);
    m_pIE->put_Left(0);
    m_pIE->put_Height(m_dwHeight-48);
    m_pIE->put_Width(m_dwWidth);

    return TRUE; // return TRUE unless you set the focus to a control
}

Note here that the MFC wizard neglected to create the above listed m_pAutoProxy=new CIEC2DlgAutoProxy instance for you, although their code has no other purpose. This wasted at least 40 hours of my time. Any other instance of the AutoProxy class you might attempt to create, crashes your computer instantly due to this class’ complicated constructor.

STEP SIX

It is important to do this step exactly right to avoid windows crashes. Change the following four functions so that they look like the below, delete the extra CanExit() function calls and add the bold text:

void CIEC2Dlg::OnClose()
{
    CDialog::OnClose();
}

void CIEC2Dlg::OnOK()
{
    if(m_bLoaded==TRUE)
    {
        AfxMessageBox("The page is loaded.");
    }
    else
        AfxMessageBox("The page is not loaded yet.");
}

void CIEC2Dlg::OnCancel()
{
    if(m_bLoaded==FALSE)
        ::Sleep(100);
    if (CanExit())
        CDialog::OnCancel();
}

BOOL CIEC2Dlg::CanExit()
{
    // If the proxy object is still around, then the automation
    // controller is still holding on to this application. Leave
    // the dialog around, but hide its UI.
    if(m_bQuit==FALSE)
    {
        BOOL bResult = AfxConnectionUnadvise((LPUNKNOWN)m_pIE,
        DIID_DWebBrowserEvents2, m_pUnk, FALSE, m_dwConnAdvise);
        if(bResult==FALSE)
            AfxMessageBox("Failed to AfxConnectionUnadvise.");
        m_pIE->Quit();
        delete m_pAutoProxy;
    }

    if (m_pAutoProxy != NULL)
    {
        ShowWindow(SW_HIDE);
        return FALSE;
    }

    return TRUE;
}

STEP SEVEN

Set m_bQuit and m_bLoaded to FALSE inside the CIEC2Dlg constructor.

CIEC2Dlg::CIEC2Dlg(CWnd* pParent /*=NULL*/)
    : CDialog(CIEC2Dlg::IDD, pParent)
{
    //{{AFX_DATA_INIT(CIEC2Dlg)
    // NOTE: the ClassWizard will add member initialization here
    //}}AFX_DATA_INIT

    // Note that LoadIcon does not require a subsequent DestroyIcon in Win32
    m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
    m_pAutoProxy = NULL;
    m_bQuit=FALSE;
    m_bLoaded=FALSE;
}

STEP EIGHT

We can finally begin using the class wizard to add DISP_ID events!

As Microsoft informs us at MSDN the DWebBrowserEvents2::DocumentComplete Event must use the following format:

void DocumentComplete(IDispatch *pDisp, Variant *URL);

We quickly realize we must try to use the class wizard to somehow enter the exact above format.

So, we bring up the class wizard by hitting ctrl-w and selecting the Automation tab. In the class name selection box, we must highlight the CIEC2DlgAutoProxy automation class. Once that is done, we will now be able to click the Add Method button.

We then enter DocumentComplete into the External name box and enter void into the Return type: box and accept the Internal name box as is.

Beneath the phrase “Parameter list”, we click under the word “Name” in the completely blank area where it looks like there is nothing to click on. An entry box materializes, enter pDisp as the name and then referring to our Microsoft definition, click on the next completely blank area and attempt to enter IDispatch* as the Type from the selection list that materialized. You will see that IDispatch* is not one of the options, but LPDispatch* is. Again, we quickly figure out that LPDispatch is for a long pointer for Dispatch and that this will work even though it has no asterisk. And that LPDispatch* will NOT work even though it has an asterisk, because that LP in front of it makes it a ** type.

Underneath the pDisp, LPDispatch parameter we just entered, click on more blank areas and enter URL as the name and Variant* as the Type. Thankfully, Variant* with an asterisk is already on the provided list. Click OK, this one is done.

Now we must enter the second Dispatch event we want which is for OnQuit. OnQuit must use the following format.

void OnQuit();

Click OK and go back to the Add Method and enter OnQuit as External name and void as a Return type. We don’t need to enter any Parameter lists this time since there aren’t any. Click OK again and then click OK once more at the bottom of the MFC ClassWizard box or all your changes will be lost.

You will see that the wizard added new functions and filled in the Dispatch Map of DlgProxy.cpp.

UPDATE FOR LATER VISUAL STUDIOS

It is easy to add the entire dispatch library for controlling other applications; one need only go to Project - Add Class and chose the option to add a file from a type library. This easy task is not why this article was written. This article is focused on the much more difficult task of receiving the "events" that the application sends back. You will not find these in a class that you can import from a type library. The Visual Studio interface for this whole thing is only half written really, it always helps you with outgoing commands, but never fully supports the incoming event messages. So where are these events and how do you find out what is really available?

You will find them virtually nowhere in cyber space or on MSDN.com. Kind of frustrating you will find. They are available as follows. For a Microsoft Office application, load up your "OleView.exe" program that came with your Visual Studio application. Go to File - View Type Library, then navigate to the Program Files folder for Microsoft Office, use Excel.exe for example, and open it. Somewhere within the 1,062 page idl Library you will find this:

"dispinterface AppEvents" uuid(00024413-0000-0000-C000-000000000046) 

Look through this segment and you will finally see all the dispatch member event ids that will get sent back to your program from Excel, and for what reason they were sent.

For Internet Explorer, there is no Type Library for events or commands. Instead the whole idl has been built into your Visual Studio include library, and is not attached to the program EXE or in its program directory, like all other lesser programs. Instead you will find the entire type library for both events and commands within the file called ExDisp.idl.

As yet another annoyance, the Visual Studio wizard for MDI projects adds the dispatch interface to the document class, however, I believe that since the document class was not derived from a CCmdTarget or IDispatch class, it is not ever able to receive any events, although it can send commands. Copy the AutoProxy class from the Visual Studio wizard for Dialog projects with automation to the MDI project, and use that, or else derive your own CCmdTarget class. Again the wizard has been half written, since automation clients that can send, but cannot receive events, are useless.

For Visual Studio 2005 and later, to utilize the dispatch map event wizard described above, you need to go to the Class View, look for the little symbol, all the way down at the very bottom, that looks like a little magnifying glass. This is the "Interface" symbol. Right click the interface symbol so that you can select Add - Method to add your dispatch interface method to the map. There is no Class Wizard to add dispatch interfaces to Visual Studio 2005 and later, apparently.

END UPDATE

STEP NINE

I think it really goes without saying that the Dispatch Maps we just added exactly right still don’t work and every time we run our program it crashes windows over and over again since our code still cannot detect IE OnQuit events.

Perhaps due to IE upgrades, the MFC ClassWizard generated DISP_FUNCTION maps will not work with IE. By looking at other people’s code, however, we figure out that we should add #include “EXDISPID.h” to DlgProxy.cpp’s include list.

// DlgProxy.cpp : implementation file
#include "stdafx.h"
#include "IEC2.h"
#include "DlgProxy.h"
#include "IEC2Dlg.h"
#include "EXDISPID.h"

Then change DISP_FUNCTION to DISP_FUNCTION_ID and add an extra DISPID_DOCUMENTCOMPLETE between “DocumentComplete” and DocumentComplete. We do the same thing to OnQuit.

BEGIN_DISPATCH_MAP(CIEC2DlgAutoProxy, CCmdTarget)
//{{AFX_DISPATCH_MAP(CIEC2DlgAutoProxy)
DISP_FUNCTION_ID(CIEC2DlgAutoProxy, "DocumentComplete",
DISPID_DOCUMENTCOMPLETE, DocumentComplete, VT_EMPTY,
VTS_DISPATCH VTS_PVARIANT)

DISP_FUNCTION_ID(CIEC2DlgAutoProxy, "OnQuit", DISPID_ONQUIT,
OnQuit, VT_EMPTY, VTS_NONE)
//}}AFX_DISPATCH_MAP
END_DISPATCH_MAP()

We take it that by using the above rules, you can convert any class wizard generated Dispatch Map to one that works with IE 6.0.

STEP TEN

Populate the DocumentComplete() and OnQuit() functions as follows:

void CIEC2DlgAutoProxy::DocumentComplete(LPDISPATCH pDisp, VARIANT FAR* URL)
{
    // TODO: Add your dispatch handler code here
    CComQIPtr<IUnknown,&IID_IUnknown> pIEUnk((IUnknown*)(m_pDialog->m_pIE));
    CComQIPtr<IUnknown,&IID_IUnknown> pLoadedUnk(pDisp);
    m_pDialog->m_pIE->get_LocationName(&bstrName);
    m_pDialog->m_pIE->get_LocationURL(&bstrURL);
    if(pIEUnk==pLoadedUnk)
    {
        m_pDialog->m_bLoaded=TRUE;
        AfxMessageBox("COMPLETELY LOADED: 
        "+(CString)bstrName+" "+(CString)bstrURL);
    }
    else
    {
        AfxMessageBox("Inside Frame loaded: "+(CString)URL->bstrVal);
    }
}

void CIEC2DlgAutoProxy::OnQuit()
{
    // TODO: Add your dispatch handler code here
    m_pDialog->m_bQuit=TRUE;
    m_pDialog->m_pIE->put_Visible(FALSE);
    AfxMessageBox("I know you quit, good-bye.");
    m_pDialog->CDialog::OnCancel();
}

We are now done and you can run the software, www.msdn.com is used as an example in the above code and you will see that it will fire DocumentComplete three separate times before it is finally done loading. This is due to it having two interior frames I believe. It will show you the three different URL names of the three frames, warn you if you click OK before the last frame is loaded, let you know that it knows if you quit IE and will clean up all the windows no matter which way you quit.

After working with externally generated dispatch events for a while, it begins to become clear that it is absolutely vital to allow easy access between the AutoProxy class and the Dialog subroutine. MFC has set it up to do this very easily, by using the m_pDialog-> pointer it created in the AutoProxy class constructor; any other way is extremely hard.

You will next want to peruse msdn.com in order to learn how to manage the cumbersome IHTMLDocument2 element collections to read web pages into your code, export the pieces you want to Excel, and partly because I can think of no other reason to automate IE instead of just using it, how to use IHTMLDocument2 to fill in the forms and click on the links of burdensome web pages.

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