Implement ICustomTaskPaneConsumer in C++

The new extensibility interfaces introduced in Office 2007 are implementable via add-ins. Add-ins can be managed or unmanaged (native). If you're using VSTO, development is greatly simplified. However, if you want to build a native add-in to implement one of these interfaces, life is a little more complex – and there doesn't seem to be any documentation on how to do this. What follows here is an exploration of how to build a native (C++ ATL) "shared" COM add-in that implements a custom task pane. You'll see that it's pretty straightforward.

Note: I used VS 2008 to build this example, but the same approach will work with VS 2005.

To start, create a new Shared Add-in project, and select Visual C++/ATL as the target language. Specify a suitable Office host(s) for your add-in (I used Excel) – the remaining wizard options are self-explanatory. The interesting work comes after you've finished the wizard and you have a skeleton add-in project. The tasks required are:

· Use the Implement Interface Wizard to implement the ICustomTaskPaneConsumer interface.

· Clean up the wizard-generated implementation slightly.

· Add an ActiveX control to your project, for use in the task pane.

· Implement ICustomTaskPaneConsumer::CTPFactoryAvailable to create the task pane, using the specified ActiveX control.

The first thing to do is to implement the ICustomTaskPaneConsumer interface. To do this, in Class View, right-click on the CConnect class, and select Add | Implement Interface. In the Implement Interface Wizard, select the Microsoft Office 12.0 Object Library <2.4> from the list of available type libraries. If this typelib doesn't show up in the list, you can choose to implement interfaces from a specific file instead – and specify the path to the Office 2007 version of MSO.DLL as the location:

From the list of interfaces exposed in the Office typelib, select ICustomTaskPaneConsumer (double-click it, or select it and click the ">" arrow to add it to the list). This will update your CConnect class to implement ICustomTaskPaneConsumer, as shown below:

class ATL_NO_VTABLE CConnect :

public CComObjectRootEx<CComSingleThreadModel>,

public CComCoClass<CConnect, &CLSID_Connect>,

public IDispatchImpl<AddInDesignerObjects::_IDTExtensibility2, &AddInDesignerObjects::IID__IDTExtensibility2, &AddInDesignerObjects::LIBID_AddInDesignerObjects, 1, 0>,

public IDispatchImpl<ICustomTaskPaneConsumer, &__uuidof(ICustomTaskPaneConsumer), &LIBID_Office, 2, 4>

{

public:

CConnect()

{

}

DECLARE_REGISTRY_RESOURCEID(IDR_ADDIN)

DECLARE_NOT_AGGREGATABLE(CConnect)

BEGIN_COM_MAP(CConnect)

COM_INTERFACE_ENTRY2(IDispatch, ICustomTaskPaneConsumer)

COM_INTERFACE_ENTRY(AddInDesignerObjects::IDTExtensibility2)

COM_INTERFACE_ENTRY(ICustomTaskPaneConsumer)

END_COM_MAP()

DECLARE_PROTECT_FINAL_CONSTRUCT()

HRESULT FinalConstruct()

{

return S_OK;

}

void FinalRelease()

{

}

public:

//IDTExtensibility2 implementation:

STDMETHOD(OnConnection)(IDispatch * Application, AddInDesignerObjects::ext_ConnectMode ConnectMode, IDispatch *AddInInst, SAFEARRAY **custom);

STDMETHOD(OnDisconnection)(AddInDesignerObjects::ext_DisconnectMode RemoveMode, SAFEARRAY **custom );

STDMETHOD(OnAddInsUpdate)(SAFEARRAY **custom );

STDMETHOD(OnStartupComplete)(SAFEARRAY **custom );

STDMETHOD(OnBeginShutdown)(SAFEARRAY **custom );

CComPtr<IDispatch> m_pApplication;

CComPtr<IDispatch> m_pAddInInstance;

// ICustomTaskPaneConsumer Methods

public:

STDMETHOD(CTPFactoryAvailable)(ICTPFactory * CTPFactoryInst)

{

return E_NOTIMPL;

}

};

Depending on whether you imported the typelib based on its registration or its path, the Wizard will also add one of the following two lines to your stdafx.h (ignore the text wrapping here – each #import statement is all on one line without linebreaks):

// Import the Office 12 type library based on its path.

#import "C:\Program Files (x86)\Common Files\microsoft shared\OFFICE12\MSO.DLL" raw_interfaces_only, raw_native_types, no_namespace, named_guids, auto_search

// Import the Office 12 type library based on its registration.

#import "libid:2DF8D04C-5BFA-101B-BDE5-00AA0044DE52" version("2.4") lcid("0") raw_interfaces_only raw_native_types, no_namespace, named_guids, auto_search

For consistency, change the wizard-generated full function implementation in the .h file, from this:

STDMETHOD(CTPFactoryAvailable)(ICTPFactory * CTPFactoryInst)

{

return E_NOTIMPL;

}

… to a simple declaration, like this:

 

STDMETHOD(CTPFactoryAvailable)(ICTPFactory * CTPFactoryInst);

... and add the skeleton implementation to the .cpp file instead:

STDMETHODIMP CConnect::CTPFactoryAvailable (ICTPFactory * CTPFactoryInst)

{

return E_NOTIMPL;

}

Also, you might need to modify the #import statement to avoid some name clashes, as shown below. Note: because the list of clashes can be quite long, I've put line breaks in the #import statement in this example:

#import "libid:2DF8D04C-5BFA-101B-BDE5-00AA0044DE52" version("2.4") lcid("0") \

raw_interfaces_only raw_native_types, no_namespace, named_guids, auto_search \

rename("DocumentProperties", "msoDocumentProperties") \

rename("RGB", "msoRGB") \

rename("IAccessible", "msoIAccessible")

Before you can instantiate a custom task pane, you need a control to put in it. You can use any registered ActiveX control. For simplicity, you can define a new one in your add-in project. To do this, in Class View, right-click on the add-in project, and select Add | Class. From the Add Class dialog, select ATL Control. In the ATL Control Wizard, type in a suitable name (eg, SimpleControl):

 

When you click Finish, the wizard will generate all the necessary code for a simple ActiveX control. Note the OnDraw implementation, which draws a rectangle and displays a simple string:

HRESULT OnDraw(ATL_DRAWINFO& di)

{

LPCTSTR pszText = _T("ATL 8.0 : SimpleControl");

TextOut(di.hdcDraw,

(rc.left + rc.right) / 2, (rc.top + rc.bottom) / 2,

pszText, lstrlen(pszText));

}

With a suitable ActiveX control, you can now implement the CTPFactoryAvailable function to create a new custom task pane using the control. The first parameter to CreateCTP is the ProgId or CLSID of the ActiveX control. The second parameter is the title to use for the task pane. The third parameter is the IDispatch interface pointer of the parent window. The parent window is only useful in the case of Outlook, where it is the window for the Inspector or Explorer where you want the task pane to appear. In the simple case, you can 'omit' the parent window argument. To 'omit' the arg, you can't just pass NULL. Instead, you should set up a VARIANTARG of type VT_ERROR, with its value set to DISP_E_PARAMNOTFOUND. The fourth parameter is the address of the _CustomTaskPane interface pointer to be filled in by the call:

STDMETHODIMP CConnect::CTPFactoryAvailable (ICTPFactory * CTPFactoryInst)

{

_CustomTaskPane* pTaskPane = NULL;

HRESULT hr = S_OK;

VARIANTARG vargParentWindow;

vargParentWindow.vt = VT_ERROR;

vargParentWindow.scode = DISP_E_PARAMNOTFOUND;

hr = CTPFactoryInst->CreateCTP(

CComBSTR(L"NativeTaskpaneAddIn.SimpleControl"),

CComBSTR(L"Contoso"), vargParentWindow, &pTaskPane);

if (SUCCEEDED(hr))

{

hr = pTaskPane->put_Visible(TRUE);

}

return hr;

}

Set the debug properties of the add-in to specify the path to the Office host application to use as the debug command, and press F5 to build and run:

 

With the increasing coverage of VSTO for add-ins of all types and for most Office applications, the need to build Shared add-ins is lessening. However, if you need to build an unmanaged add-in, this is still your only option.