Dajbych.net


How to Create Add-ons for Internet Explorer, Part 1

, 9 minutes to read

ie9 logo

Internet Explorer can be extended in many ways. You can add items to menus, create custom panels, separate buttons on the command bar, or create an Accelerator. Because add-ons, except for accelerators, are programmed in C++, we will show you how to simplify your work using .NET libraries. We will use the C++ CLI extension, which can make the entire .NET accessible. Therefore, part of the add-in can be written in C#.

Creating an Internet Explorer add-on is definitely not an intuitive matter. When I needed to create an add-on for IE a year ago, I was looking in vain for a tutorial similar to this. Add-ons for Internet Explorer for its development team aren't a priority right now, so building them isn't as easy as it could be. Still, they can be very useful.

Let’s take a look at how to create a simple add-on – a command panel button that prints the title and address of the page. On this simple example, we will explain the principle of creating an add-on, registering it and creating an installer.

Project

First, you need to create an ATL project. You can find it in the Visual C++ category. If you want the source code samples to work directly without renaming classes and interfaces, call it MyIeAddon*.

In the wizard, you need to check Allow merging of proxy/stub code to generate only one project.

The first step is to add a class that represents the button. From the project pop-up menu, choose Add and then Class. Then select ATL Simple Object.

Name the class appropriately. I chose the name MyButton so that it would be immediately clear from the source code samples which class it is.

Leave preview or search support disabled in the wizard. Set option Aggregation to No and check IObjectWithSite.

So that was the easy part. Now you need to add a few lines of code to the header of the button class. This defines the interface for displaying it in Internet Explorer.

// MyButton.h : Declaration of the CMyButton

#pragma once
#include "resource.h"       // main symbols

#include <shlguid.h>        // IID_IWebBrowser2, DIID_DWebBrowserEvents2, etc.
#include <exdispid.h>       // DISPID_DOCUMENTCOMPLETE, etc.
#include <mshtml.h>         // DOM interfaces

#include "MyIeButton_i.h"

#if defined(_WIN32_WCE) && !defined(_CE_DCOM) && !defined(_CE_ALLOW_SINGLE_THREADED_OBJECTS_IN_MTA)
#error "Single-threaded COM objects are not properly supported on Windows CE platform, such as the Windows
Mobile platforms that do not include full DCOM support. Define _CE_ALLOW_SINGLE_THREADED_OBJECTS_IN_MTA to
force ATL to support creating single-thread COM object's and allow use of it's single-threaded COM object
implementations. The threading model in your rgs file was set to 'Free' as that is the only threading model
supported in non DCOM Windows CE platforms."
#endif

using namespace ATL;

// CMyButton

class ATL_NO_VTABLE CMyButton :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CMyButton, &CLSID_MyButton>,
    public IObjectWithSiteImpl<CMyButton>,
    public IDispatchImpl<IMyButton, &IID_IMyButton, &LIBID_MyIeButtonLib, 1, 0>,
    public IDispEventImpl<1, CMyButton, &DIID_DWebBrowserEvents2, &LIBID_SHDocVw, 1, 1>,
    public IDispEventImpl<0, CMyButton, &DIID_HTMLDocumentEvents2, &LIBID_MSHTML, 4, 0>,
    public IOleCommandTarget
{
public:
    CMyButton()
    {
    }

DECLARE_REGISTRY_RESOURCEID(IDR_MYBUTTON)

DECLARE_NOT_AGGREGATABLE(CMyButton)

BEGIN_SINK_MAP(CMyButton)
    SINK_ENTRY_EX(1, DIID_DWebBrowserEvents2, DISPID_DOCUMENTCOMPLETE, OnDocumentComplete)
END_SINK_MAP()

BEGIN_COM_MAP(CMyButton)
    COM_INTERFACE_ENTRY(IMyButton)
    COM_INTERFACE_ENTRY(IDispatch)
    COM_INTERFACE_ENTRY(IObjectWithSite)
    COM_INTERFACE_ENTRY(IOleCommandTarget)
END_COM_MAP()

    DECLARE_PROTECT_FINAL_CONSTRUCT()

    HRESULT FinalConstruct()
    {
        return S_OK;
    }

    void FinalRelease()
    {
    }

public:

    // IObjectWithSite
    STDMETHOD(SetSite)(IUnknown *pUnkSite);

    // IOleCommandTarget
    STDMETHOD(Exec)(const GUID *pguidCmdGroup, DWORD nCmdID, DWORD nCmdExecOpt, VARIANTARG *pvaIn, VARIANTARG *pvaOut);
    STDMETHOD(QueryStatus)(const GUID *pguidCmdGroup, ULONG cCmds, OLECMD *prgCmds, OLECMDTEXT *pCmdText);

    // DWebBrowserEvents2
    void STDMETHODCALLTYPE OnDocumentComplete(IDispatch *pDisp, VARIANT *pvarURL); 

private:

    CComPtr<IWebBrowser2>  m_spWebBrowser;
    BOOL m_fAdvised; 
    CComQIPtr<IOleCommandTarget,  &IID_IOleCommandTarget> m_spTarget;
};

OBJECT_ENTRY_AUTO(__uuidof(MyButton), CMyButton)
Samotná implementace metod může vypadat například takto:
// MyButton.cpp : Implementation of CMyButton

#include "stdafx.h"
#include "MyButton.h"

// CMyButton

void STDMETHODCALLTYPE CMyButton::OnDocumentComplete(IDispatch *pDisp, VARIANT *pvarURL) {
    
}

STDMETHODIMP CMyButton::SetSite(IUnknown *pUnkSite) {
    if (pUnkSite != NULL) {
        // Cache the pointer to IWebBrowser2
        CComQIPtr<IServiceProvider> sp = pUnkSite;
        HRESULT hr = sp->QueryService(IID_IWebBrowserApp,  IID_IWebBrowser2, (void**)&m_spWebBrowser);
        hr = sp->QueryInterface(IID_IOleCommandTarget, (void**)&m_spTarget);
        if (SUCCEEDED(hr)) {
            
            // Register to sink events from DWebBrowserEvents2.
            hr = IDispEventImpl::DispEventAdvise(m_spWebBrowser);
            if (SUCCEEDED(hr)) {
                m_fAdvised = TRUE;
            }
        }
    } else {
        // Unregister event sink.
        if (m_fAdvised) {
            IDispEventImpl::DispEventUnadvise(m_spWebBrowser);
            m_fAdvised = FALSE;
        }

        // Release pointer
        m_spWebBrowser.Release();
        m_spTarget.Release();
    }

    // Return base implementation
    return IObjectWithSiteImpl<CMyButton>::SetSite(pUnkSite);
}

STDMETHODIMP CMyButton::Exec(const GUID *pguidCmdGroup, DWORD nCmdID, DWORD nCmdExecOpt, VARIANTARG *pvaIn, VARIANTARG *pvaOut) {
    if (m_spWebBrowser != NULL) {

        BSTR url;    
        BSTR title;     
        m_spWebBrowser->get_LocationURL(&url);    
        m_spWebBrowser->get_LocationName(&title);     
        
        MessageBox(NULL, url, title, 0);

        ::SysFreeString(url);    
        ::SysFreeString(title);

        return S_OK;

    } else {
        MessageBox(NULL, _T("No Web browser pointer"), _T("Oops"), 0);
        return E_ABORT;
    }
}

STDMETHODIMP CMyButton::QueryStatus(const GUID *pguidCmdGroup, ULONG cCmds, OLECMD prgCmds[], OLECMDTEXT *pCmdText) {
    HRESULT hr = OLECMDERR_E_UNKNOWNGROUP;
    if (pguidCmdGroup && IsEqualGUID(*pguidCmdGroup, CLSID_ToolbarExtButtons)) {
        for (ULONG i = 0; i < cCmds; i++) {
            if (m_spWebBrowser) {
                // By default, we'll support all commands
                prgCmds[i].cmdf = OLECMDF_ENABLED | OLECMDF_SUPPORTED;
            } else {
                // If we wanted to latch the button down, we could do this:
                prgCmds[i].cmdf |= OLECMDF_LATCHED;
            }
        }
        hr = S_OK;
    }

    return hr;

}

The Exec method is in charge of operating the press of a button. The QueryStatus method determines whether the button is enabled or disabled. The OnDocumentComplete method is always called after the page is loaded.

Registration

The library must be registered, which will actually install it in Internet Explorer. To do this, you first need to find out the appropriate GUIDs of the objects in the library. These can be found in MyIeAddon.idl:

interface IMyButton : IDispatch{
};
[
    uuid(78F4AA07-8BAD-4D7E-AA30-D3726A96C3FD),
    version(1.0),
]
library MyIeButtonLib
{
    importlib("stdole2.tlb");
    [
        uuid(9FCA1565-739D-4741-8957-D5A7957AB6F4)		
    ]
    coclass MyButton
    {
        [default] interface IMyButton;
    };
};

Paste this code into the MyIeAddon.rgs file and replace the GUIDs used in it with yours. This will register the browser extension.

HKLM {
  NoRemove SOFTWARE {
    NoRemove Microsoft {
      NoRemove 'Internet Explorer' {
        NoRemove Extensions {
          ForceRemove '{A3278C3B-DA28-4E8E-924D-E1676D7BF4BE}' = s 'MyButton' {
            val 'Default Visible' = s 'yes'
            val 'ButtonText' = s 'My IE Button'
            val 'CLSID' = s '{1FBA04EE-3024-11d2-8F1F-0000F87ABD16}'
            val 'ClsidExtension' = s '{A3278C3B-DA28-4E8E-924D-E1676D7BF4BE}'
            val 'Icon' = s '%%ProgramFiles(x86)%%\MyIeAddon\icon.ico'
            val 'HotIcon' = s '%%ProgramFiles(x86)%%\MyIeAddon\icon.ico'
          }
        }
      }
    }
  }
}

The button has only one icon measuring 16 × 16 px and cannot be changed dynamically. The *Icon and HotIcon items define its location. You can specify a system variable in the path. It’s not documented anywhere on MSDN, but it works. You can use this to install the x86 add-on on x64 systems. I will show you how to add the variable during installation on older systems where it is not defined.

There are other types of accessories. In addition to the browser extension, there is also a browser helper object. While the browser extension is loaded into memory only when the user clicks the button (which is why the icon cannot be changed), the browser helper is loaded when it is launched. Therefore, it is better to register a class for handling events such as page load as a browser helper object:

HKLM {
  NoRemove SOFTWARE {
    NoRemove Microsoft {
      NoRemove Windows {
        NoRemove CurrentVersion {
          NoRemove Explorer {
            NoRemove 'Browser Helper Objects' {
              ForceRemove '{ 9FCA1565-739D-4741-8957-D5A7957AB6F4}' = s ' MyIeAddon' {
                val 'NoExplorer' = d '1'
              }
            }
          }
        }
      }
    }
  }
}

Each type of add-on must implement a slightly different layout. However, I have prepared a sample of the source code in such a way that it implements both interfaces. The type of add-on therefore only determines how it is registered. You can register a single class as both a browser extension and a browser helper object.

In some cases, registration after a build fails. Fortunately, the error description directly describes how to solve it. Just enable Per-user Redirection in the project properties.

Function

The functionality itself can be written in C++/CLI, which allows the use of .NET libraries. This will slightly increase the time it takes to start Internet Explorer, but it is not necessary to learn MFC. Introduction to the C++/CLI world was written by Jakub Čermák.

The CLI extension introduces language constructs necessary for programming in .NET, such as managed classes. Their instance is created by command gcnew. It points to them with special pointers and a garbage collector takes care of releasing them from memory.

What can be done with the page will be the subject of the next parts. To give you an idea, I can just say that you can, for example, edit DOM pages or read browser cookies.

Installer

InstallShield LE, which can be installed in Visual Studio, is the most suitable for creating an installer. It will take care of copying the files to the correct directory and other important procedures. It is able to download and install the .NET Framework if needed.

Creating an installer is not entirely straightforward. The problem is caused by two Program Files components on x64 systems. In the files to be installed, it is necessary to set the self-registration in the properties of the installed library. During installation via the regsvr32 program, it registers the library in the same way as Visual Studio after build.

If you are using C++/CLI, check in the Redistributables Microsoft .NET Framework Full (Web Download). This will install the .NET Framework 4 if it is not already on the system. Next, in item Cumstom Actions, add a new VBScript to event After Register Product.

A system variable ProgramFiles(x86) can be created in it, if it is not already defined in the system. This is done by a simple script in Visual Basic:

Set wshShell = CreateObject( "WScript.Shell" )

If wshShell.ExpandEnvironmentStrings( "%ProgramFiles(x86)%" ) = "%ProgramFiles(x86)%" Then

    Set wshSystemEnv = wshShell.Environment( "SYSTEM" )
    wshSystemEnv( "ProgramFiles(x86)" ) = "C:\Program Files"    

End If

Updating the add-on is done by reinstalling the newer version. Internet Explorer must not be running during this period.

In the next part, we will show you how to read browser cookies and create add-ons that can somehow handle the user’s profile. This way, the user will not have to log in separately on the website and separately in the browser add-on.

Source code of the example.

The article was written for Zdroják.