Building a C++ Add-in for Outlook 2010

Summary: This article shows how to create an unmanaged add-in in C++ that customizes the Microsoft Outlook 2010 user interface. Learn how to customize the ribbon component of the Microsoft Office Fluent user interface, and to add custom form regions to Outlook 2010. (31 printed pages)

Applies to: Office 2010 | Outlook 2010

In this article
Introduction to Building Add-ins in Outlook
Creating a C++ Add-In
Adding a Ribbon and a Ribbon Button
Form Regions
Adding the Bing Search Capabilities
Conclusion
Additional Resources

Published:  January 2010 | Updated:  April 2010

Provided by:  Jon Flanders, MCW Technologies

Click to grab code   Outlook 2010 Sample: Building a C++ Add-In

Contents

  • Introduction to Building Add-ins in Outlook

  • Creating a C++ Add-In

  • Adding a Ribbon and a Ribbon Button

  • Form Regions

  • Adding the Bing Search Capabilities

  • Conclusion

  • Additional Resources

Introduction to Building Add-ins in Outlook

Microsoft Outlook has a rich model for building add-ins that can greatly enhance a user's day-to-day experience with Outlook. To build Outlook add-ins, you can use Microsoft Office development tools in Microsoft Visual Studio 2010 (or the Microsoft Visual Studio Tools for Office for earlier versions of Visual Studio), which is a set of managed libraries built on top of the Outlook and Microsoft Office object models. Alternatively, you can use unmanaged code (that is, C++).

Note

Microsoft Visual C++ supports development of projects in unmanaged C++ as well as managed C++ by using the Common Language Runtime (CLR). In the rest of this article, C++ refers to the unmanaged version of the language, and managed C++ refers to Visual C++ used on top of the CLR.

The following list shows some of the reasons why you might choose to use C++ over C# or Visual Basic (the two language options available with Office development tools in Visual Studio) to build your add-ins.

  • Performance

    An unmanaged add-in generally performs better than one written in a managed language. The code may execute faster, depending on the code and algorithms used. In addition, an unmanaged add-in will load faster because it does not need to load the CLR to do its work. If your add-in moves a lot of data back and forth to Outlook, an unmanaged version also performs better because the data does not need to cross the native/managed boundary on each call (known as marshaling).

  • Distribution package size and fewer dependencies

    With an unmanaged add-in, users do not have to install prerequisites such as the CLR.

    Note

    If your add-in supports both the 32-bit and 64-bit versions of Outlook, you will need to produce a separate build for each version.

  • Messaging API (MAPI) support

    If your add-in needs to use MAPI directly, you must use C++. Calling MAPI directly from managed code is not supported.

  • Versioning

    Although the .NET Framework allows multiple versions of an assembly, the Visual Studio Tools for Office runtime limits you to one assembly per version of Visual Studio Tools for Office or Office development tools in Visual Studio 2010. With unmanaged code you can have more flexibility with versioning.

Note

This article is intended for intermediate and advanced developers who have experience developing applications in C++. The article does not necessarily enumerate in detail common procedures in Visual Studio.

The article uses the Bing API only as an example to provide functionality to a custom user interface. You can connect the custom button in the Outlook inspector ribbon with an API such as the Outlook object model. If you use the Bing API in your application, be sure that your code conforms to the currently supported version of Bing. For more information, see the Bing Developer Center.

Creating a C++ Add-In

Once you have decided to build an Outlook add-in in C++, the first step is to create a C++ library application by using the following procedures.

Note

To download the code sample that accompanies this article, see Outlook 2010 Sample: Building a C++ Add-In.

Step 1: Create a new ATL project

  1. Open Microsoft Visual Studio 2010.

  2. On the File menu, point to New, and then click Project.

  3. In the New Project dialog box, select the ATL Project template from the Visual C++ node.

  4. Name this project NativeAddIn. Using ATL gives you a good infrastructure for using the Outlook add-in model, which is based on COM.

  5. Click OK to bring up the ATL Project Wizard.

  6. Click Finish to use the default settings for this project.

Step 2: Add a new object

  1. Right-click the project node in Solution Explorer, point to Add, and then click Class.

  2. In the Add Class dialog box, select the ATL Simple Object template from the list, and then click Add.

  3. In the ATL Simple Object Wizard, type Connect in the Short Name text box. This is the name of the class that your add-in will implement. Using "Connect" is a well-known convention when building add-in classes.

  4. Type NativeAddin.Connect in the ProgID text box, and let the wizard fill in all the additional information.

  5. Click Finish.

Step 3: Implement interfaces

Next, use the ATL wizards to implement the necessary interfaces for the add-in.

  1. Now that you have your class created, go to the Class View tab. (If the tab is not already visible, click Class View on the View menu, or press CTRL+SHIFT+C).

  2. Expand the NativeAddIn node in the Class View window.

  3. Right-click the CConnect node, which represents the class you just created, point to Add, and then click Implement Interface.

    Note

    You may have to close and open Visual Studio for the CConnect node to appear. This is a known issue in the Beta 2 release of Visual Studio 2010.

  4. In the Implement Interface Wizard dialog box, select Microsoft Add-In Designer <1.0> in the Available type libraries box.

  5. Select _IDTExtensbility2 in the Interfaces list box, and click the > button to add the interface to the Implement Interfaces list.

  6. Click Finish.

_IDTExtensibility2 is the interface that all Office add-ins must implement. It defines the interaction between the add-in and the host, which in this case will be Outlook. The main interaction comes when the add-in is first loaded: Outlook calls the OnConnection method. This method is where you get access to the Outlook object model, store references to those interface pointers, and add your customizations.

Step 4: Set up loading the add-in in Outlook

  1. Open the file Connect.h.

  2. Change all the methods to return S_OK instead of E_NOTIMPL. Outlook calls all these methods, so it is important to return a successful result.

  3. Add code to the OnConnection method so you can verify whether Outlook loads your add-in. Add a call to MessageBoxW, as in the following code.

    STDMETHOD(OnConnection)(LPDISPATCH Application, 
        ext_ConnectMode ConnectMode, 
        LPDISPATCH AddInInst, 
        SAFEARRAY * * custom)
    {
        MessageBoxW(NULL,L"OnConnection",L"Native Addin",MB_OK);
        return S_OK;
    }
    

This is enough to get you started. However, before Outlook can actually load your add-in, it needs a little bit more metadata information about your add-in to know that it exists, and to know how to load it. You will need to put this information into the Windows Registry. Because the ATL project already has a registry script embedded (to put the general COM metadata into the registry), you can just add to that script to add your Outlook-specific metadata to the registry, as well.

Step 5: Insert Outlook-specific metadata for the add-in in the Windows Registry

  1. Open Solution Explorer.

  2. Expand the Resource Files folder, and open the Connect.rgs file.

  3. Add the following registry script to the file.

    HKCU
    {
        NoRemove Software
        {
            NoRemove Microsoft
            {
                NoRemove Office
                {
                    NoRemove Outlook
                    {
                        NoRemove Addins
                        {
                            NativeAddin.Connect
                            {
                                val Description = s 'Sample Addin'
                                val FriendlyName = s 'Sample Addin'
                                val LoadBehavior = d 3
                            }
                        }
                    }
                }
            }
        }
    }
    

This registry entry informs Outlook that this add-in should be loaded, and the LoadBehavior key tells it to load it automatically. At this point, if you build your project, the correct registry keys will be put into the registry and your add-in is almost ready for loading.

Step 6: Determine the appropriate build for the add-in

Now you need to determine whether you are running the 32-bit or 64-bit version of Outlook. Your Office installation is in the drive:\Program Files\Microsoft Office directory if you are running 32-bit Windows, or the drive:\Program Files (x86)\Microsoft Office directory if you are running 32-bit Outlook in a 64-bit Windows. If you are running the 32-bit version of Outlook, you are done; you can build your add-in and then open Outlook.

If you are running the 64-bit version of Outlook, you need to change the project to build your add-in by using the 64-bit compiler. The 64-bit version of Outlook does not load 32-bit add-ins.

Follow these steps to build your add-in for 64-bit Outlook:

  1. Open the Configuration Manager by right-clicking the solution node in Solution Explorer and then clicking Configuration Manager.

  2. In the Configuration Manager, select New from the Active solution platform box to create a new platform setting for the NativeAddin project.

  3. In the New Solution Platform dialog box, select x64 as the new platform, and then click OK.

  4. Click Close in the Configuration Manager dialog box.

  5. Build your solution again before trying to open Outlook.

When you open Outlook, you should see the message box generated by the following line of code in the OnConnection method.

MessageBoxW(NULL, L"OnConnection", L"Native_Addin", MB_OK);
    return S_OK;

The message box is shown in Figure 1.

Figure 1. NativeAddIn showing a message box at startup

NativeAddIn showing a message box at startup

For ease of debugging, you should change the project properties to load Outlook when you debug your project.

Step 7 (optional): Change the project properties to load Outlook for ease of debugging

  1. Right-click your project in Solution Explorer, and then click Properties.

  2. Expand Configuration Properties in the tree view in the left pane, and then select the Debugging node.

  3. For the Command property in the right pane, select Browse in the combo box that appears when you click to the right of the property.

  4. Browse to the Outlook.exe file on your computer (again the path can be different depending on your operating system and whether you are running the 32-bit or 64-bit version of Outlook).

Before going any further, you will probably want to remove (or comment out) the MessageBoxW call in the OnConnection method in your CConnect implementation. As you build and debug, having to dismiss the dialog box on every run will become quite tedious. Commenting out the call is a good idea, because if you have trouble with loading or debugging later, you can always remove the comment for testing purposes to be sure your add-in is loading (or find out that it is not).

Adding a Ribbon and a Ribbon Button

Now that you have built the add-in and it is ready for loading, you probably want to add some additional functionality. One common customization is to add a new tab to the Outlook ribbon, and to add tab groups and buttons to the new tab.

To accomplish this, you will need to implement another interface: IRibbonExtensibility. IRibbonExtensibility is a pretty simple interface because it has only one method, GetCustomUI. GetCustomUI has a BSTR as an output parameter, which should contain XML formatted by using the Microsoft Office Fluent user interface schema. The XML is then used to add additional ribbon elements inside of the host (in this case, Outlook).

IRibbonExtensibility is inside of a different type library than the one you added earlier, so you will need to bring that type library into your project. In fact, now is a good time to clean up the StdAfx.h header file to make the project a little bit more robust for different platforms and computers.

If you open the StdAfx.h file (see Figure 2), you will notice that the ATL Implement Interface Wizard puts an absolute path in the #import statement (which causes the type library to be converted into a C++/COM header file when the project is compiled). This is fine for your computer, but if you want to share the project with another developer, their path may be different. Also, the path may be different if you move between a 32-bit and 64-bit system.

Figure 2. StdAfx.h file with the wizard #import statement

StdAfx.h file with the wizard #import statement

You can make the #import statement more robust by referencing the type library's LIBID (library identifier, or GUID) instead of the path. You will also want to include a second import statement for the Office type library (which contains the IRibbonExtensibility interface). This will make the #import statement much more robust, and adding rename_namespace works around problems that can arise when there are name collisions once the type libraries are imported.

The following procedures will show you how to add a ribbon and a ribbon button.

Step 1: Clean up the StdAfx.h file

  1. Open the StdAfx.h file.

  2. Remove the #import statement and replace it with the following code.

    #import "libid:AC0714F2-3D04-11D1-AE7D-00A0C90F26F4"\
    auto_rename auto_search raw_interfaces_only rename_namespace("AddinDesign")
    
    // Office type library (i.e., mso.dll).
    #import "libid:2DF8D04C-5BFA-101B-BDE5-00AA0044DE52"\
        auto_rename auto_search raw_interfaces_only rename_namespace("Office")
    using namespace AddinDesign;
    using namespace Office;
    

You are now going to make some manual changes to the CConnect class to support interacting with the ribbon.

Step 2: Change the CConnect class to support interaction with the ribbon

  1. Open the Connect.h file.

    You will now add some typedef declarations for the different interfaces you will use to simplify later tasks.

  2. Find the IDispatchImpl declaration on the CConnect class for _IDTExtensibility2 (see Figure 3) and make it a typedef named IDTImpl.

    Figure 3. Wizard-generated IDispatchImpl

    Wizard-generated IDispatchImpl

  3. Add a similar typedef named RibbonImpl for IRibbonExtensibility, and then add that to the list of base classes for CConnect. After you are done, the class should look like the following code.

    typedef IDispatchImpl<_IDTExtensibility2, &__uuidof(_IDTExtensibility2), &__uuidof(__AddInDesignerObjects), /* wMajor = */ 1>
    IDTImpl;
    
    typedef IDispatchImpl<IRibbonExtensibility, &__uuidof(IRibbonExtensibility), &__uuidof(__Office), /* wMajor = */ 2, /* wMinor = */ 5>
    RibbonImpl;
    
    class ATL_NO_VTABLE CConnect :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CConnect, &CLSID_Connect>,
    public IDispatchImpl<IConnect, &IID_IConnect, &LIBID_NativeAddinLib, /*wMajor =*/ 1, /*wMinor =*/ 0>,
    public IDTImpl,
    public RibbonImpl
    
  4. Add the following interface to the ATL COM MAP.

    COM_INTERFACE_ENTRY(IRibbonExtensibility)
    
  5. Change all the methods, including the ones for which you are not yet adding real functionality, to return S_OK instead of E_NOT_IMPL.

Step 3: Implement IRibbonExtensibility

Add the implementation for IRibbonExtensibility. It should look something like the following code.

public:
STDMETHOD(GetCustomUI)(BSTR RibbonID, BSTR * RibbonXml)
{
    if(!RibbonXml)
        return E_POINTER;
    *RibbonXml = CComBSTR("XML GOES HERE");  
    return S_OK;
}

Now you need to decide where you want to store your ribbon XML. One choice would be to embed the XML right into the code, but a more manageable solution is to add the XML as a resource to your project.

Step 4: Set up the ribbon XML as a resource to your project

  1. In Solution Explorer, right-click the NativeAddIn project, point to Add, and then click New Item.

  2. Select the XML File template from the Add New Item dialog box, and name it RibbonManifest.xml.

  3. Click Add to add it to your project.

The ribbon XML is fairly straightforward. It contains a ribbon element and a tab that can contain multiple tab elements. In a ribbon, a tab can have a group, and each group can have buttons. Figure 4 shows how this translates into the Outlook ribbon user interface (UI). Groups are noted by red rectangles, and buttons by blue ovals.

Figure 4. Tab, groups, and buttons on the Outlook ribbon

Tab, groups, and buttons on the Outlook ribbon

You now need to create a new tab, group, and button.

Step 5: Specify XML for a new tab, group, and button

Open your RibbonManifest.xml file and add the following XML.

<customUI xmlns="https://schemas.microsoft.com/office/2006/01/customui">
    <ribbon>
        <tabs>
            <tab id="Bing" label="Bing">
                <group id="BingGroup" label="Bing Group">
                    <button id="NewCustomButton" 
                        imageMso="WebPagePreview" 
                        size="large" 
                        label="Search Bing" 
                        onAction="ButtonClicked"/>
                </group>
            </tab>
        </tabs>
    </ribbon>
</customUI>

As set up in step 4, your RibbonManifest.xml file is stored as a resource so you can return the XML from the IRibbonExtensibility::GetCustomUI method.

Step 6: Implement IRibbonExtensibility::GetCustomUI to return XML

  1. On the View menu, point to Other Windows, and then click Resource View to go to the Resource View for this project.

  2. Expand the NativeAddIn node, right-click the NativeAddIn.rc node, and then click Add Resource.

  3. In the Add Resource dialog box, click the Import button.

  4. Change the file type at the lower right of the dialog box to All Files, select the RibbonManifest.xml file in the file list, and then click Open.

  5. In the Custom Resource Type dialog box, name the resource type "XML", and then click OK.

  6. Now you need some code that can actually process the XML resource. You can implement this yourself, or add the following code to the CConnect class.

    HRESULT HrGetResource(int nId, 
        LPCTSTR lpType, 
        LPVOID* ppvResourceData,       
        DWORD* pdwSizeInBytes)
    {
        HMODULE hModule = _AtlBaseModule.GetModuleInstance();
        if (!hModule)
            return E_UNEXPECTED;
        HRSRC hRsrc = FindResource(hModule, MAKEINTRESOURCE(nId), lpType);
        if (!hRsrc)
            return HRESULT_FROM_WIN32(GetLastError());
        HGLOBAL hGlobal = LoadResource(hModule, hRsrc);
        if (!hGlobal)
            return HRESULT_FROM_WIN32(GetLastError());
        *pdwSizeInBytes = SizeofResource(hModule, hRsrc);
        *ppvResourceData = LockResource(hGlobal);
        return S_OK;
    }
    
    BSTR GetXMLResource(int nId)
    {
        LPVOID pResourceData = NULL;
        DWORD dwSizeInBytes = 0;
        HRESULT hr = HrGetResource(nId, TEXT("XML"), 
            &pResourceData, &dwSizeInBytes);
        if (FAILED(hr))
            return NULL;
        // Assumes that the data is not stored in Unicode.
        CComBSTR cbstr(dwSizeInBytes, reinterpret_cast<LPCSTR>(pResourceData));
        return cbstr.Detach();
    }
    
    SAFEARRAY* GetOFSResource(int nId)
    {
        LPVOID pResourceData = NULL;
        DWORD dwSizeInBytes = 0;
        if (FAILED(HrGetResource(nId, TEXT("OFS"), 
            &pResourceData, &dwSizeInBytes)))
            return NULL;
        SAFEARRAY* psa;
        SAFEARRAYBOUND dim = {dwSizeInBytes, 0};
        psa = SafeArrayCreate(VT_UI1, 1, &dim);
        if (psa == NULL)
            return NULL;
        BYTE* pSafeArrayData;
        SafeArrayAccessData(psa, (void**)&pSafeArrayData);
        memcpy((void*)pSafeArrayData, pResourceData, dwSizeInBytes);
        SafeArrayUnaccessData(psa);
        return psa;
    }
    
  7. Insert the following code to complete the implementation of IRibbonExtensibility::GetCustomUI to return actual XML.

    STDMETHOD(GetCustomUI)(BSTR RibbonID, BSTR * RibbonXml)
    {
        if(!RibbonXml)
            return E_POINTER;
        *RibbonXml = GetXMLResource(IDR_XML1);
        return S_OK;
    }
    

If you build the add-in and run Outlook, you should now see your added tab, group, and button as shown in Figure 5.

Figure 5. Added tab, group, and button on the Outlook ribbon

Added tab, group, and button on the Outlook ribbon

Now you need to add code that responds when the user clicks the Search Bing button. (You will implement the actual Bing search API code later.)

Unlike many other callback environments, Office, including Outlook, uses IDispatch as the way to call back into your IRibbonExtensibility implementation, as opposed to a custom interface that uses connection points. Your object needs to return a DISPID from GetIDsofName when the onAction attribute value (in this case, ButtonClicked) from your ribbon XML is passed in, and to have the IDispatch::Invoke method respond appropriately to the DISPID. The method also needs to have an input parameter that is an IDispatch pointer.

The code now gets a little tricky because it goes down to a lower level in ATL than you normally need to implement a simple COM add-in.

At this point, you first need to create a COM interface with a ButtonClicked method on it.

Step 7: Declare a COM interface with a ButtonClicked method

  1. Open the IDL file, NativeAddIn.idl, for the project.

  2. Add the interface. The interface needs to be a dual interface, and you should pick a unique DISPID value for the id attribute. This will hopefully avoid DISPID collisions later on. The declaration in your NativeAddIn.idl file should look like the following code.

    [
        object,
        uuid(CE895442-9981-4315-AA85-4B9A5C7739D8),
        dual,
        nonextensible,
        helpstring("IRibbonCallback Interface"),
        pointer_default(unique)
    ]
    interface IRibbonCallback : IDispatch{
        [id(42),helpstring("Button Callback")]
        HRESULT ButtonClicked([in]IDispatch* ribbonControl);
    };
    
  3. Make IRibbonCallback the default interface for your class, replacing IConnect.

    coclass Connect
    {
        [default] interface IRibbonCallback;
    };
    

Now that the interface is declared and the default interface is set, you can make your CConnect class implement the interface.

Step 8: Implement the interface

  1. Add the following declaration at the top of your Connect.h file.

    typedef IDispatchImpl<IRibbonCallback, &__uuidof(IRibbonCallback)> 
    CallbackImpl;
    

    Adding the typedef is useful to implement the IDispatch::Invoke method later on.

  2. Add the interface to the CConnect class's base hierarchy. The class should now look like the following code.

    class ATL_NO_VTABLE CConnect :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CConnect, &CLSID_Connect>,
    public IDispatchImpl<IConnect, &IID_IConnect, &LIBID_NativeAddInLib, /*wMajor =*/ 1, /*wMinor =*/ 0>,
    public IDTImpl,
    public RibbonImpl,
    public CallbackImpl
    
  3. Add the interface to the ATL COM MAP, and replace the _IDTExtensibility2 interface in the COM_INTERFACE_ENTRY2 entry. Your COM MAP should look like the following.

    BEGIN_COM_MAP(CConnect)
        COM_INTERFACE_ENTRY2(IDispatch, IRibbonCallback)
        COM_INTERFACE_ENTRY(IConnect)
        COM_INTERFACE_ENTRY(_IDTExtensibility2)
        COM_INTERFACE_ENTRY(IRibbonExtensibility)
        COM_INTERFACE_ENTRY(IRibbonCallback)
    END_COM_MAP()
    
  4. Implement the IRibbonCallback::ButtonClicked method. Add the following code to open a message box so you can confirm that your callback is working correctly.

    STDMETHOD(ButtonClicked)(IDispatch* ribbon)
    {
        MessageBoxW(NULL,L"Button Clicked!",L"NativeAddin",MB_OK);
        return S_OK;
    }
    

There is still one more step before this callback will work: you must override the implementation of IDispatch::Invoke. There are a few ways you could do this, but the following procedure will accomplish the goal. Note that this is not a general purpose way to expose more than one custom interface through IDispatch.

Step 9: Override the implementation of IDispatch::Invoke

  1. Add the following code to the CConnect class.

    STDMETHOD(Invoke)(DISPID dispidMember, 
        const IID &riid, 
        LCID lcid, 
        WORD wFlags, 
        DISPPARAMS *pdispparams, 
        VARIANT *pvarResult, 
        EXCEPINFO *pexceptinfo, 
        UINT *puArgErr)
    {
        HRESULT hr=DISP_E_MEMBERNOTFOUND;
        if(dispidMember==42)
        {
            hr  = CallbackImpl::Invoke(dispidMember, 
                riid, 
                lcid, 
                wFlags,       
                pdispparams, 
                pvarResult, 
                pexceptinfo, 
                puArgErr);
        }
        if (DISP_E_MEMBERNOTFOUND == hr)
            hr = IDTImpl::Invoke(dispidMember, 
                riid, 
                lcid, 
                wFlags, 
                pdispparams, 
                pvarResult, 
                pexceptinfo, 
                puArgErr);
        return hr;
    }
    

    This code determines whether the dispidMember parameter is the unique one for the callback (in this case, 42). If it is, the code calls the CallbackImpl::Invoke method. Otherwise, it calls the IDTImpl::Invoke method, because that is the only other IDispatch interface you need at the moment.

At this point you can build your add-in. When you click the custom button on your new ribbon, a message box should appear.

Form Regions

Another way of adding UI to Outlook is by adding custom form regions. Custom form regions enable you to put a panel of controls into an Outlook inspector. You can put these form regions in various places in the inspector, including replacing the whole dialog box.

To complete this add-in, you will add a form region to the form used to display mail items. The form region will have a Web browser control. Building a form region into your add-in uses similar steps to adding the ribbon UI. Add custom form regions by using the following procedures.

First you need to bring in the necessary type libraries. In this case, you will bring in three type libraries: one for Microsoft Word (because Outlook uses Word to edit e-mail messages), one for Outlook (to enable you to program against the Outlook object model), and one for form regions.

Step 1: Import the necessary type libraries

Add the following #import statements into your StdAfx.h file.

#import "libid:00020905-0000-0000-C000-000000000046"\
auto_rename auto_search raw_interfaces_only rename_namespace("Word")

// Outlook type library (i.e., msoutl.olb).
#import "libid:00062FFF-0000-0000-C000-000000000046"\
auto_rename auto_search raw_interfaces_only rename_namespace("Outlook")

// Forms type library (i.e., fm20.dll).
#import "libid:0D452EE1-E08F-101A-852E-02608C4D0BB4"\
auto_rename auto_search raw_interfaces_only  rename_namespace("Forms")
using namespace Word;
using namespace Outlook;

using namespace Forms;

Now you will implement the _FormRegionStartup interface.

Step 2: Implement the _FormRegionStartup interface

  1. Add the following typedef for the IDispatchImpl to the top of your Connect.h file.

    typedef IDispatchImpl<_FormRegionStartup, &__uuidof(_FormRegionStartup), &__uuidof(__Outlook), /* wMajor = */ 9, /* wMinor = */ 4>
    FormImpl;
    
  2. Add the FormImpl definition to the CConnect class. Your final CConnect declaration should look like the following code.

    class ATL_NO_VTABLE CConnect :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CConnect, &CLSID_Connect>,
    public IDispatchImpl<IConnect, &IID_IConnect, &LIBID_NativeAddinLib, /*wMajor =*/ 1, /*wMinor =*/ 0>,
    public IDTImpl,
    public RibbonImpl,
    public CallbackImpl,
    public FormImpl
    
  3. Because you overrode IDispatch::Invoke in the last section, you will need to add the following code to that method to deal with the new interface.

    STDMETHOD(Invoke)(DISPID dispidMember, 
        const IID &riid, 
        LCID lcid, 
        WORD wFlags, 
        DISPPARAMS *pdispparams, 
        VARIANT *pvarResult,  
        EXCEPINFO *pexceptinfo, 
        UINT *puArgErr)
    {
        HRESULT hr=DISP_E_MEMBERNOTFOUND;
        if(dispidMember==42)
        {
            hr  = CallbackImpl::Invoke(dispidMember, 
                riid, 
                lcid, 
                wFlags, 
                pdispparams, 
                pvarResult, 
                pexceptinfo, 
                puArgErr);
        }
        if (DISP_E_MEMBERNOTFOUND == hr)
            hr = IDTImpl::Invoke(dispidMember, 
                riid, 
                lcid, 
                wFlags, 
                pdispparams, 
                pvarResult, 
                pexceptinfo, 
                puArgErr);
        if (DISP_E_MEMBERNOTFOUND == hr)
            hr = FormImpl::Invoke(dispidMember, 
                riid, 
                lcid, 
                wFlags, 
                pdispparams, 
                pvarResult, 
                pexceptinfo, 
                puArgErr);
        return hr;
    }
    
  4. Add the _FormRegionStartup to the ATL COM interface map.

    BEGIN_COM_MAP(CConnect)
        COM_INTERFACE_ENTRY2(IDispatch, IRibbonCallback)
        COM_INTERFACE_ENTRY(IConnect)
        COM_INTERFACE_ENTRY(_IDTExtensibility2)
        COM_INTERFACE_ENTRY(IRibbonExtensibility)
        COM_INTERFACE_ENTRY(IRibbonCallback)
        COM_INTERFACE_ENTRY(_FormRegionStartup)
    END_COM_MAP()
    
  5. Add the implementation of _FormRegionStartup.

    public:
    STDMETHOD(GetFormRegionStorage)(BSTR FormRegionName, 
        LPDISPATCH Item, 
        long LCID, 
        OlFormRegionMode FormRegionMode, 
        OlFormRegionSize FormRegionSize, 
        VARIANT * Storage)
    {
        V_VT(Storage) = VT_ARRAY | VT_UI1;
        V_ARRAY(Storage) = GetOFSResource(IDR_OFS1);
        return S_OK;
    }
    
    STDMETHOD(BeforeFormRegionShow)(_FormRegion * FormRegion)
    {
        if(m_pFormRegionWrapper)
            delete m_pFormRegionWrapper;
        m_pFormRegionWrapper = new FormRegionWrapper();
        if (!m_pFormRegionWrapper)
            return E_OUTOFMEMORY;
        return m_pFormRegionWrapper->HrInit(FormRegion);
    }
    
    STDMETHOD(GetFormRegionManifest)(BSTR FormRegionName, 
        long LCID, 
        VARIANT * Manifest)
    {
        V_VT(Manifest) = VT_BSTR;
        BSTR bstrManifest = GetXMLResource(IDR_XML2);
        V_BSTR(Manifest) = bstrManifest;
        return S_OK;
    }
    
    STDMETHOD(GetFormRegionIcon)(BSTR FormRegionName, 
        long LCID, 
        OlFormRegionIcon Icon, 
        VARIANT * Result)
    {
        return S_OK;
    }
    
    private:
    FormRegionWrapper* m_pFormRegionWrapper;
    
  6. Modify FinalConstruct to initialize m_pFormRegionWrapper.

    HRESULT FinalConstruct()
    {
        m_pFormRegionWrapper = NULL;
        return S_OK;
    }
    

Notice that the implementation includes a method, _FormRegionStartup::GetFormRegionManifest, that is similar to IRibbonExtensibility::GetCustomUI. The GetFormRegionManifest method also needs to return some Outlook-specific XML to tell Outlook about the form region you want to add.

Step 3: Specify the display of the form region in XML

  1. Add another XML file to your project. Name it FormManifest.xml.

  2. Add the following XML to that file.

    <FormRegion xmlns="https://schemas.microsoft.com/office/outlook/12/formregion.xsd">
        <name>Native</name>
        <formRegionType>adjoining</formRegionType>
        <formRegionName>Search Bing</formRegionName>
        <hidden>true</hidden>
        <ribbonAccelerator>I</ribbonAccelerator>
        <showInspectorCompose>true</showInspectorCompose>
        <showInspectorRead>true</showInspectorRead>
        <showReadingPane>false</showReadingPane>
        <addin>NativeAddin.Connect</addin>
    </FormRegion>
    
  3. Go to the Resource View for this project (on the View menu, point to Other Windows, and then click Resource View).

  4. Expand the NativeAddIn node, right-click the NativeAddIn.rc node, and then click Add Resource.

  5. In the Add Resource dialog box, click the Import button.

  6. Change the file type at the lower right of the dialog box to All Files, select the FormManifest.xml file in the file list, and then click Open.

  7. In the Custom Resource Type dialog box, name the resource type "XML", and then click OK.

To complete this implementation, you need to create the helper class, FormRegionWrapper, that the _FormRegionStartup interface uses to manage the form region (and call the Bing API).

Step 4: Create a helper class to manage the form region and call the Bing API

  1. Add the following #include statements to the StdAfx.h header file.

    #include <msxml2.h>//msxml for parsing the Bing results
    #include <Mshtml.h>//mshtml for controlling the web browser control
    #include <Winhttp.h>//for making the HTTP calls to Bing
    #include <atlstr.h>//for CString
    #include <atlsafe.h>//for the ATL Safearray helper classes
    
  2. Add Winhttp.lib and Msxml2.lib to your linker dependencies. To do this, right-click your project, and then click Properties. In the Properties dialog box, expand the Linker node, and then select Input. Click the drop-down box next to Additional Dependencies, and then click Edit. In the Additional Dependencies dialog box, type winhttp.lib and msxml2.lib on separate lines, and then click OK.

  3. Create a new header file named FormRegionWrapper.h and add the following code to it.

    #pragma once
    #include "stdafx.h"
    #include <vector>
    #include <algorithm>
    
    /*!-----------------------------------------------------------------------
    FormRegionWrapper - Used to help track all the form regions and to 
    listen to some basic events such as the send button click and close.
    -----------------------------------------------------------------------!*/
    class FormRegionWrapper;
    typedef 
    IDispEventSimpleImpl<2, FormRegionWrapper, &__uuidof(FormRegionEvents)>
    FormRegionEventSink;
    const DWORD dispidEventOnClose = 0xF004;
    class FormRegionWrapper : public FormRegionEventSink
    {
        public:
            HRESULT HrInit(_FormRegion* pFormRegion);
            void Show();
            void SearchSelection();
            void Search(BSTR term);
        private:
            static _ATL_FUNC_INFO VoidFuncInfo; 
        public:
            BEGIN_SINK_MAP(FormRegionWrapper)
            SINK_ENTRY_INFO(2, __uuidof(FormRegionEvents), 
                dispidEventOnClose,    
                OnFormRegionClose, 
                &VoidFuncInfo)
            END_SINK_MAP()
            void __stdcall OnFormRegionClose();
        private:
            CComPtr<_FormRegion> m_spFormRegion;
            CComPtr<_MailItem> m_spMailItem;
            CComPtr<IWebBrowser> m_spWebBrowser;
    };
    
  4. Create a C++ (.cpp) file named FormRegionWrapper.cpp and add the following code to it.

    #include "stdafx.h"
    #include "FormRegionWrapper.h"
    using namespace ATL;
    
    #define ReturnOnFailureHr(h) { hr = (h); ATLASSERT(SUCCEEDED((hr))); if (FAILED(hr)) return hr; }
    
    // Macro that calls a COM method that returns an HRESULT value.
    #define CHK_HR(stmt)        do { hr=(stmt); if (FAILED(hr)) ; } while(0)
    // Macro to verify memory allocation.
    #define CHK_ALLOC(p)        do { if (!(p)) { hr = E_OUTOFMEMORY; ; } } while(0)
    // Macro that releases a COM object if not NULL.
    #define SAFE_RELEASE(p)     do { if ((p)) { (p)->Release(); (p) = NULL; } } while(0)
    /*!-----------------------------------------------------------------------
    FormRegionWrapper implementation
    -----------------------------------------------------------------------!*/
    _ATL_FUNC_INFO FormRegionWrapper::VoidFuncInfo = {CC_STDCALL, VT_EMPTY, 0, 0}; 
    
    HRESULT FormRegionWrapper::HrInit(_FormRegion* pFormRegion)
    {
        HRESULT hr = S_OK;
        m_spFormRegion = pFormRegion;
        FormRegionEventSink::DispEventAdvise(m_spFormRegion);
        CComPtr<IDispatch> spDispatch;
        ReturnOnFailureHr(pFormRegion->get_Form(&spDispatch));
        CComPtr<Forms::_UserForm> spForm;
        ReturnOnFailureHr(spDispatch->QueryInterface(&spForm));
        CComPtr<Forms::Controls> spControls;
        ReturnOnFailureHr(spForm->get_Controls(&spControls));
        CComPtr<Forms::IControl> spControl;
        CComBSTR bstrWBName(L"_webBrowser");
        spControls->_GetItemByName(bstrWBName.Detach(),&spControl);
        ReturnOnFailureHr(spControl->QueryInterface<IWebBrowser>
            (&m_spWebBrowser));
        spControl.Release();
        CComPtr<IDispatch> spDispItem;
        ReturnOnFailureHr(pFormRegion->get_Item(&spDispItem));
        ReturnOnFailureHr(spDispItem->QueryInterface(&m_spMailItem));
        return hr;
    }
    
    void FormRegionWrapper::Show()
    {
        if(m_spFormRegion)
        {
            m_spFormRegion->Select();
        }
    }
    
    void FormRegionWrapper::SearchSelection()
    {
        if (m_spMailItem)
        {
            CComPtr<_Inspector> pInspector;
            m_spMailItem->get_GetInspector(&pInspector);
            CComPtr<IDispatch> pWordDispatch;
            pInspector->get_WordEditor(&pWordDispatch);
            CComQIPtr<Word::_Document> pWordDoc(pWordDispatch);
            CComPtr<Word::_Application> pWordApp;
            pWordDoc->get_Application(&pWordApp);
            CComPtr<Word::Selection> pSelection;
            pWordApp->get_Selection(&pSelection);
            if(pSelection)
            {
                CComBSTR text;
                pSelection->get_Text(&text);
                Search(text);
            }
        }
    }
    
    void FormRegionWrapper::Search(BSTR term)
    {
        if(m_spWebBrowser)
        {
            CComBSTR html("<html><body><H1>You searched for:");
            html.AppendBSTR(term);
            html.Append("</H1></body></html>");
            CComPtr<IDispatch> docDispatch;
            m_spWebBrowser->get_Document(&docDispatch);
            if(docDispatch==NULL)
            {
                VARIANT vDummy;
                vDummy.vt=VT_EMPTY;
                m_spWebBrowser->Navigate
                    (L"about:blank",&vDummy,&vDummy,&vDummy,&vDummy);
                m_spWebBrowser->get_Document(&docDispatch);
            }
            if(docDispatch!=NULL)
            {
                CComPtr<IHTMLDocument2> doc;
                HRESULT hr = 
                    docDispatch.QueryInterface<IHTMLDocument2>(&doc);
                if(hr==S_OK)
                {
                    SAFEARRAY *psaStrings = 
                        SafeArrayCreateVector(VT_VARIANT, 0, 1);
                    VARIANT *param;
                    HRESULT hr = 
                        SafeArrayAccessData(psaStrings, (LPVOID*)&param);
                    param->vt = VT_BSTR;
                    param->bstrVal = html.Detach();
                    hr = SafeArrayUnaccessData(psaStrings);
                    doc->write(psaStrings);
                    SafeArrayDestroy(psaStrings);
                }
            }
        }
    }
    
    void FormRegionWrapper::OnFormRegionClose()
    {
        if (m_spMailItem)
        {
            m_spMailItem.Release();
        }
        if (m_spFormRegion)
        {
            FormRegionEventSink::DispEventUnadvise(m_spFormRegion);
            m_spFormRegion.Release();
        }
    }
    

    Note

    For simplicity, multiple classes are included in this file; however, in a real application, you would put each class in its own file.

  5. Add an #include statement for FormRegionWrapper.h to your Connect.h header file.

  6. Modify the ButtonClicked callback method to call the SearchSelection method of the FormRegion member variable (m_pFormRegion) by adding the following code.

    STDMETHOD(ButtonClicked)(IDispatch* ribbon)
    {
        if(m_pFormRegionWrapper)
        {
            m_pFormRegionWrapper->SearchSelection();
        }
        return S_OK;
    }
    

You have created the XML for Outlook to display the form region, and the code to run the form region. Now you will create the form region itself.

Step 5: Create the form region and add a control in Outlook

  1. In Outlook, enable the Developer tab on the ribbon. Click the File tab, click Options, and then click Customize Ribbon. Under Customize the Ribbon, select the Developer check box to enable the Developer tab, and then click OK.

  2. Once you have enabled the Developer tab, open it and click the Design a Form button.

  3. Select the Message form, and then click Open.

    Which form you pick in the Design Form dialog box does not matter in this case, because you are going to end up exporting the form region from this dialog box.

  4. Once the forms designer appears, click the New Form Region button. This creates a new form region, and brings it to the foreground of the forms designer.

  5. Click the Control Toolbox button so you can see the controls you can add to the form.

  6. Now add a custom control. In the Toolbox, right-click anywhere in the Controls tab, and then click Custom Controls. Under Available Controls, select Microsoft Web Browser, and then click OK.

    The WebBrowser control is a nice control to use in this case because it is fairly easy to display a list of Bing search results in it. You can also insert hyperlinks in the control; an Outlook end user can click a link in the control and be brought to that page in their browser.

  7. Add the WebBrowser control to the form by dragging it from the Toolbox to the form region. Size the control so that it takes up all the space in the form region.

  8. Change the name of the newly added control to _webBrowser. Do this by right-clicking the control and then clicking Properties. Naming the control precisely is important because the code in the FormRegionWrapper class retrieves the controls by name (which is just a string).

  9. For aesthetics, go to the Layout tab of the WebBrowser control's properties, and set the Horizontal and Vertical properties to Grow/shrink with form. Your form should look something like Figure 6.

    Figure 6. Form region with added control

    Form region with added control

  10. Save the form region as an .ofs file. (The .ofs file format is a binary file that Outlook uses for form regions.) To do this, click the Save Region button, and then click Save Form Region As. Specify the file name WebBrowser.ofs and save it in your project directory for NativeAddIn.

Step 6: Connect the form region with NativeAddIn

  1. Go back to Visual Studio 2010 and expand the NativeAddIn node in Resource View. Right-click the NativeAddIn.rc node, and then click Add Resource.

  2. In the Add Resource dialog box, click the Import button.

  3. Change the file type at the lower right of the dialog box to All Files, select the WebBrowser.ofs file in the file list, and then click Open.

  4. Name the resource type "OFS", and then click OK.

When the _FormRegionStartup::GetFormRegionStorage method in your Connect.h file is called, it returns the data in the .ofs file to Outlook. Before building and trying your add-in, you will need to add some additional entries to the Connect.rgs file. You must also register the form region in the Windows registry.

Step 7: Add entries to the registry

Insert the following code into your Connect.rgs file.

HKCU
{
    NoRemove Software
    {
        NoRemove Microsoft
        {
            NoRemove Office
            {
                NoRemove Outlook
                {
                    NoRemove Addins
                    {
                        NativeAddin.Connect
                        {
                            val Description = s 'Sample Addin'
                            val FriendlyName = s 'Sample Addin'
                            val LoadBehavior = d 3
                        }
                    }
                    NoRemove FormRegions
                    {
                        IPM.Note
                        {
                            val TestNativeCOMAddin = s '=NativeAddin.Connect'
                        }
                        IPM.Note.NativeAddin
                        {
                            val TestNativeCOMAddin = s '=NativeAddin.Connect'
                        }
                    }
                }
            }
        }
    }
}

Step 8: Build and test the add-in

  1. Build your project.

  2. Open Outlook and create a new mail message.

  3. At the bottom of the mail message form, you should see the form region you created. Highlight text in the message, and then click the Search Bing button. Your code should modify the WebBrowser control and show the search term (see Figure 7).

    Figure 7. Custom ribbon and form region displayed

    Custom ribbon and form region displayed

Adding the Bing Search Capabilities

By this point, you have seen how to build an unmanaged Outlook add-in in C++. Once you have the infrastructure in place, you will be able to build specific functionality on top of this infrastructure.

You now have a working Outlook add-in, but it might be nice to add some additional functionality. This is not essential to this article in terms of learning about building unmanaged add-ins for Outlook, but it nicely demonstrates cross-product functionality. If you want, you can use the following procedure to include a Bing search based on terms selected in the e-mail message.

Note

You will have to sign up for an AppID on the Bing Developer Center to add this functionality.

To add Bing search capabilities

  1. Replace the code in the FormsRegionWrapper.cpp file with the following code:

    #include "stdafx.h"
    #include "FormRegionWrapper.h"
    using namespace  ATL;
    
    #define ReturnOnFailureHr(h) { hr = (h); ATLASSERT(SUCCEEDED((hr))); if (FAILED(hr)) return hr; }
    
    // Macro that calls a COM method that returns an HRESULT value.
    #define CHK_HR(stmt)        do { hr=(stmt); if (FAILED(hr)) ; } while(0)
    
    // Macro to verify memory allocation.
    #define CHK_ALLOC(p)        do { if (!(p)) { hr = E_OUTOFMEMORY; ; } } while(0)
    
    // Macro that releases a COM object if not NULL.
    #define SAFE_RELEASE(p)     do { if ((p)) { (p)->Release(); (p) = NULL; } } while(0)
    class AutoVariant : public VARIANT
    {
    public:
    
        AutoVariant()
        {
            VariantInit(this);
        }
    
        ~AutoVariant()
        {
            VariantClear(this);
        }
    
        HRESULT SetBSTRValue(LPCWSTR sourceString)
        {
            VariantClear(this);
            V_VT(this) = VT_BSTR;
            V_BSTR(this) = SysAllocString(sourceString);
            if (!V_BSTR(this))
            {
                return E_OUTOFMEMORY;
            }        
            return S_OK;
        }
    
        void SetObjectValue(IUnknown *sourceObject)
        {
            VariantClear(this);
            V_VT(this) = VT_UNKNOWN;
            V_UNKNOWN(this) = sourceObject;
            if (V_UNKNOWN(this))
            {
                V_UNKNOWN(this)->AddRef();
            }
        }
    };
    
    class BingHttpRequest
    {
    public:
    
        BingHttpRequest(){}
    
        void Complete()
        {  
            HRESULT hr = CoInitializeEx(NULL,COINIT_APARTMENTTHREADED); 
            m_buffer.Append(m_tempBuffer);
            loadDOM(m_buffer);
            CoUninitialize();
        }
    
        void DoRequestSync(LPWSTR request,IWebBrowser* pWebBrowser)
        {
            m_WebBrowser = pWebBrowser;
            DWORD err =0;
            DWORD dwSize = 0;
            DWORD dwDownloaded = 0;
            LPSTR pszOutBuffer;
            HINTERNET hSession = ::WinHttpOpen(0,
                WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
                WINHTTP_NO_PROXY_NAME,
                WINHTTP_NO_PROXY_BYPASS,
                0);
            HINTERNET hConnection  = ::WinHttpConnect(hSession,               
                L"api.bing.net",
                INTERNET_DEFAULT_PORT,
                0);
            CString lpstrRequest =  L"xml.aspx?Sources=
                web&AppID=70FA6D77B407BA830359D291DD4531800EA4DA38";
            lpstrRequest += L"&query=";
            lpstrRequest += request;
            HINTERNET hRequest = ::WinHttpOpenRequest(hConnection,
                L"GET",
                (lpstrRequest.GetString()),
                0, // use HTTP version 1.1
                WINHTTP_NO_REFERER,
                WINHTTP_DEFAULT_ACCEPT_TYPES,
                0); // flags
            if (!::WinHttpSendRequest(hRequest,
                WINHTTP_NO_ADDITIONAL_HEADERS,
                0, // headers length
                WINHTTP_NO_REQUEST_DATA,
                0, // request data length
                0, // total length
                (DWORD_PTR)this)) // context
            {
                err = GetLastError();
            }
            else
            { 
                bool result = WinHttpReceiveResponse( hRequest, NULL);
                do 
                {
                    // Check for available data.
                    dwSize = 0;
                    if (!WinHttpQueryDataAvailable( hRequest, &dwSize))
                        printf( "Error %u in WinHttpQueryDataAvailable.\n",
                            GetLastError());
                        // Allocate space for the buffer.
                        pszOutBuffer = new char[dwSize+1];
                    if (!pszOutBuffer)
                    {
                        printf("Out of memory\n");
                        dwSize=0;
                    }
                    else
                    {
                        // Read the Data.
                        ZeroMemory(pszOutBuffer, dwSize+1);
                        if (!WinHttpReadData( hRequest, (LPVOID)pszOutBuffer, 
                            dwSize, &dwDownloaded))
                            printf("Error %u in WinHttpReadData.\n", 
                                GetLastError());
                        else
                        {
                            m_buffer.Append(pszOutBuffer);
                            printf("%s", pszOutBuffer);
                        }          
                        // Free the memory allocated to the buffer.
                        delete [] pszOutBuffer;
                    }
                } while (dwSize > 0);
                OutputDebugStringW(m_buffer);
                loadDOM(m_buffer);
            }
            WinHttpCloseHandle(hSession);
        }
    
        CComPtr<IWebBrowser> m_WebBrowser;
    
        HRESULT VariantFromString(PCWSTR wszValue, VARIANT &Variant)
        {
            HRESULT hr = S_OK;
            BSTR bstr = SysAllocString(wszValue);
            CHK_ALLOC(bstr);    
            V_VT(&Variant) = VT_BSTR;
            V_BSTR(&Variant) = bstr;
            return hr;
        }
    
        // Helper function to create a DOM instance. 
        HRESULT CreateAndInitDOM(IXMLDOMDocument2 **ppDoc)
        {
            HRESULT hr = CoCreateInstance(CLSID_DOMDocument, 
                NULL,     
                CLSCTX_INPROC_SERVER,  
                IID_IXMLDOMDocument2,
                (void**)ppDoc);
    
            return hr;
        }
    
        void loadDOM(BSTR xml)
        {
            OutputDebugStringW(xml);
            HRESULT hr = S_OK;
            IXMLDOMDocument2 *pXMLDom=NULL;
            IXMLDOMParseError *pXMLErr = NULL;
            BSTR bstrXML = NULL;
            BSTR bstrErr = NULL;
            VARIANT_BOOL varStatus;
            WCHAR ns[]= L"'xmlns:web=
                'https://schemas.microsoft.com/LiveSearch/2008/04/XML/web' " 
                L"'xmlns:search=
                'https://schemas.microsoft.com/LiveSearch/2008/04/XML/element' ";
            AutoVariant v;
            v.SetBSTRValue(L"xmlns:search=\
                "https://schemas.microsoft.com/LiveSearch/2008/04/XML/element\"
                ");
            CHK_HR(CreateAndInitDOM(&pXMLDom));    
            CHK_HR(pXMLDom->loadXML(xml, &varStatus));
            if (varStatus == VARIANT_TRUE)
            {
                CHK_HR(pXMLDom->get_xml(&bstrXML));
            }
            else
            {
                CHK_HR(pXMLDom->get_parseError(&pXMLErr));
                CHK_HR(pXMLErr->get_reason(&bstrErr));
                printf("Failed to load DOM from BING. %S\n", bstrErr);
            }
            CComPtr<IXMLDOMNodeList> resultNodes;
            pXMLDom->setProperty (L"SelectionNamespaces", 
            CComVariant(L"xmlns:search=\
                "https://schemas.microsoft.com/LiveSearch/2008/04/XML/element\"
                xmlns:web=\
                "https://schemas.microsoft.com/LiveSearch/2008/04/XML/web\""));
            pXMLDom->setProperty(L"SelectionLanguage", CComVariant("XPath"));
            CComPtr<IXMLDOMElement> doc;
            CHK_HR(pXMLDom->get_documentElement(&doc));
            hr =  doc->selectNodes(CComBSTR(L
                "//search:SearchResponse/web:Web/web:Results/web:WebResult"),
                &resultNodes);
            long length;
            if(resultNodes!=NULL)
            {
                resultNodes->get_length(&length);
                printf("Results node count %d",length);
                if(m_WebBrowser)
                {
                    CComBSTR html("<html><body><ul>");
                    CComPtr<IXMLDOMNode> pNode;
                    CComPtr<IXMLDOMNode> pTitleNode;
                    CComPtr<IXMLDOMNode> pUrlNode;
                    CComBSTR title;
                    CComBSTR url;
                    for(int i=0;i<length;i++)
                    {
                        resultNodes->get_item(i,&pNode);
                        pNode->get_firstChild(&pTitleNode);
                        pNode->selectSingleNode(CComBSTR(L"web:Url"),&pUrlNode);
                        if(pTitleNode!=NULL&&pUrlNode!=NULL)
                        {
                            html.Append("<li><a target=\"_blank\" href=\"");
                            pUrlNode->get_text(&url);
                            html.AppendBSTR(url);
                            html.Append("\">");
                            pTitleNode->get_text(&title);
                            html.AppendBSTR(title);
                            html.Append("</a></li>");
                        }
                        pNode.Release();
                        pUrlNode.Release();
                        pTitleNode.Release();
                    }
                    CComPtr<IDispatch> docDispatch;
                    m_WebBrowser->get_Document(&docDispatch);
                    if(docDispatch==NULL)
                    {
                        VARIANT vDummy;
                        vDummy.vt=VT_EMPTY;
                        m_WebBrowser->Navigate(L"about:blank",
                            &vDummy,&vDummy,&vDummy,&vDummy);
                        m_WebBrowser->get_Document(&docDispatch);
                        if(docDispatch!=NULL)
                        {
                            CComPtr<IHTMLDocument2> doc;
                            hr = docDispatch.QueryInterface
                                <IHTMLDocument2>(&doc);
                            if(hr==S_OK)
                            {
                                // Creates a new one-dimensional array.
                                SAFEARRAY *psaStrings =
                                    SafeArrayCreateVector(VT_VARIANT, 0, 1);
                                VARIANT *param;
                                HRESULT hr = SafeArrayAccessData(psaStrings,
                                    (LPVOID*)&param);
                                param->vt = VT_BSTR;
                                param->bstrVal = html.Detach();
                                hr = SafeArrayUnaccessData(psaStrings);
                                doc->write(psaStrings);
                                SafeArrayDestroy(psaStrings);
                            }
                            else
                            {
                                OutputDebugString(L
                                    "QI for IHtmlDocument2 failed");
                            }
                        }
                        else
                            OutputDebugString(L"DOC IS STILL NULL!!!!");
                    }
                }
                else
                {
                    printf("No nodes found");
                }
            }
        }
    
        LPSTR m_tempBuffer;
        CComBSTR m_buffer;
    };
    
    /*!-----------------------------------------------------------------------
        FormRegionWrapper implementation
    -----------------------------------------------------------------------!*/
    
    _ATL_FUNC_INFO FormRegionWrapper::VoidFuncInfo = {CC_STDCALL, VT_EMPTY, 0, 0}; 
    
    HRESULT FormRegionWrapper::HrInit(_FormRegion* pFormRegion)
    {
        HRESULT hr = S_OK;
        m_spFormRegion = pFormRegion;
        FormRegionEventSink::DispEventAdvise(m_spFormRegion);
        CComPtr<IDispatch> spDispatch;
        ReturnOnFailureHr(pFormRegion->get_Form(&spDispatch));
        CComPtr<Forms::_UserForm> spForm;
        ReturnOnFailureHr(spDispatch->QueryInterface(&spForm));
        CComPtr<Forms::Controls> spControls;
        ReturnOnFailureHr(spForm->get_Controls(&spControls));
        CComPtr<Forms::IControl> spControl;
        CComBSTR bstrWBName(L"_webBrowser");
        spControls->_GetItemByName(bstrWBName.Detach(),&spControl);
        ReturnOnFailureHr(spControl->QueryInterface
            <IWebBrowser>(&m_spWebBrowser));
        spControl.Release();
        CComPtr<IDispatch> spDispItem;
        ReturnOnFailureHr(pFormRegion->get_Item(&spDispItem));
        ReturnOnFailureHr(spDispItem->QueryInterface(&m_spMailItem));
        return hr;
    }
    
    void FormRegionWrapper::Show()
    {
        if(m_spFormRegion)
        {
            m_spFormRegion->Select();
        }
    }
    
    void FormRegionWrapper::SearchSelection()
    {
        if (m_spMailItem)
        {
            CComPtr<_Inspector> pInspector;
            m_spMailItem->get_GetInspector(&pInspector);
            CComPtr<IDispatch> pWordDispatch;
            pInspector->get_WordEditor(&pWordDispatch);
            CComQIPtr<_Document> pWordDoc(pWordDispatch);
            CComPtr<Word::_Application> pWordApp;
            pWordDoc->get_Application(&pWordApp);
            CComPtr<Word::Selection> pSelection;
            pWordApp->get_Selection(&pSelection);
            if(pSelection)
            {
                CComBSTR text;
                pSelection->get_Text(&text);
                Search(text);
            }
        }
    }
    
    void FormRegionWrapper::Search(BSTR term)
    {
        BingHttpRequest* r = new BingHttpRequest();
        r->DoRequestSync(term,m_spWebBrowser);
    }
    
    void FormRegionWrapper::OnFormRegionClose()
    {
        if (m_spMailItem)
        {
            m_spMailItem.Release();
        }
        if (m_spFormRegion)
        {
            FormRegionEventSink::DispEventUnadvise(m_spFormRegion);
            m_spFormRegion.Release();
        }
    }
    
  2. You can now debug or start Outlook. Open a new mail message. Type in some text, select the text, and then click the Search Bing button. You should see something like Figure 8.

    Figure 8. Add-in with Bing search integrated

    Add-in with Bing search integrated

Conclusion

You have seen how to build an Outlook add-in in C++ that implements the _IDTExtensibility2 interface. _IDTExtensibility2, along with the Outlook-specific registry entries, are the key to loading your add-in into the Outlook environment.

You also gave your add-in some functionality to customize the Outlook UI, by using two common extensibility mechanisms: a custom ribbon and a form region. With a custom ribbon, you can add UI, such as a button, to the ribbon and allow your add-in to respond to context-based UI events. With a custom form region, you can add UI elements, such as a WebBrowser control, to Outlook forms.

At this point, you have the infrastructure of an add-in that customizes the Outlook UI. With this infrastructure, you can further extend your add-in (for example, by using the Bing search capabilities) to suit your purposes.

Additional Resources

For more information about Outlook form regions, see the following resources:

For more information about customizing the Office Fluent ribbon, see the following resources: