sean's happy programming place

The Journals of Captain COM:
OLE Controls (Part I)

Sean Baxter

Get the project source here.

  Why OLE Controls?

Perhaps the most-implemented and successful set of COM interfaces are those defined by the OLE Controls standard.  Objects implementing these interfaces are called OLE Controls, or in MS marketing terms, "ActiveX Controls."  Devised during the 16 bit Windows days to facilitate the embedding and linking of document objects into Microsoft Office applications, OLE Documents/OLE Controls was overhauled in 1994 and again revised in 1996 to deliver any type of user interface item, including controls no more complicated than the Windows intrinsics, with minimal overhead.

Visual Basic has long been the biggest consumer of this technology: user interface objects that are selected from the Toolbox and drawn onto the Form are all OLE Controls.  VB uses type information to load the controls' properties into the Properties window; connection points on the object let the VB programmer implement event handling; the IPersist-derived interfaces allow the control to serialize state information to a stream, storage, or property bag; and COM property pages provide a flexible design-time mechanism for modifying aspects of the control.  While these last four technologies are not part of the OLE Controls standard, they are routinely supported by OLE Control objects.

With the advent of Internet Explorer 3, embedding OLE Controls into web pages has become a possibility.  Internet Explorer 4 added support for some OC96 (OLE Controls 1996 standard) mechanisms, and IE5 refined that support.  Unfortunately the realities of the web have prevented HTML-embedded controls from gaining much popularity.  Inferior browsers like Netscape don't support OLE Controls, so many developers have stuck with Java applets or plain forms for compatibility reasons (fortunately Netscape's market share is plummeting and the delinquent browser will soon be no more).  Also, the text-based nature of HTML has prevented controls from using flexible streams and storages for persisting stat.  Since OLE Controls are native Win32 DLLs, complicated ones (especially those linked with the static MFC libraries) may require significant storage.  Unfortunately the pathetic bandwidth of your household modem makes downloading controls a slow ordeal.  Not many users will wait ten minutes while a control is sent over the lines, so developers have to stick with more space-friendly options.  Fortunately the web is changing and controls will ultimately prevail: Internet Explorer is the majority browser and is shipped with all new PCs, high-speed residential connections are becoming available, and developers are getting clever with where they store control initialization data.

A bad misconception of "ActiveX" has spread through the computer world and prevented many developers from even understanding what browser-embedded controls really are.  ActiveX is not some newfangled Internet technology.  It's just COM.  ActiveX "Controls" are not spooky DHTML creations that control webpage menus and play Tic Tac Toe.  They're just COM objects that implement the OLE Controls interfaces.  Even though they may be embedded in a web page, they have no special networking mechanisms: controls are downloaded from the server, installed to the user's registry, and run from a local process.  If a control does indeed communicate with a remote server, it's using a regular Win32 network API like Windows Sockets, WinInet, or a remote COM marshaler, not any ActiveX Controls-provided mechanism.

To drive the point home, let's look at a popular browser-embedded OLE Control:

Really slick price history control

Figure 1: Really slick price history control

Figure 1 shows the popular MSN Investor stock charting control.  This powerful control really delivers "web content."   Or something along those lines.

Really slick cascading news menu control

Figure 2: Really slick cascading News Menu control

Here's another useful control: the MSNBC News Menu control.  Just hover over a topic to see the day's news.  If this doesn't qualify as "ActiveX" nothing does.  So let's investigate the realities of these two controls:

Controls installed from Internet Explorer

Figure 3: Controls installed from Internet Explorer

Figure 3 shows the controls installed from Internet Explorer.  Notice that the MSN Investor stock charting control is well over a meg.  Modems aren't going to cut it if we want to browse a control-enhanced web.

ChrtCtl is packed in an OCX

Figure 4: ChrtCtl is packed in an OCX

In Figure 4 we take a look at the files contained in the stock charting control: inv7.ocx contains the control.  What's an OCX file?  Simply a DLL given an OCX extension.  Look at the text at the top of the dialog box: the stock charting control is called "ChrtCtl Class."  Where does this name come from?  If this really is a COM object, as I assert, it would be likely that ChrtCtl Class is the control's registry-specified name.

ChrtCtl implemented interfaces

Figure 5: ChrtCtl implemented interfaces

In Figure 5 OLEView.exe is launched and ChrtCtl Class is located.  Oh my!  Apparently it is a COM object.  The control's server is inv7.ocx (which was the file shown in the .CAB in the previous figure), the control's ProgID is ChrtCtl.ChrtCtl.1, and the control has a type library named "Money Central Type Library."  Notice the large number of interfaces that ChrtCtl Class implements.  IOleObject, IOleControl, IOleInPlaceObject, and IViewObject are just a few of the OLE Controls interfaces that the stock charter supports, so obviously it's an OLE Control.  Do you see any interfaces like IActiveXSpookyInternetThing?  Of course not - that's because ActiveX Controls is not a separate technology from OLE Controls; they are one in the same.

IChrtCtl custom interface

Figure 6: IChrtCtl custom interface

In Figure 6 we explore the stock charting control's type library, "MoneyCentral Type Library."  This library includes the type information for eleven coclasses, including ChrtCtl.  The stock charting control implements two custom interfaces described in the type library (in addition to the many standard interfaces listed in Figure 5).  IChrtCtl is the default interface - it's dual.  _ChrtEvents is the control's source interface, and like most source interfaces, it's dispatch only.  The methods on IChrtCtl are invoked from the page's HTML to initialize the control for use.  If the control were embedded in a Visual Basic form, VB would call the same methods, as would C++ if that were the client language.

In this and the next few Journals of Captain COM we'll explore the mechanisms of OLE Controls, and the motivations for them in the context of small, fast, and flexible user interface components.  After the discussion of controls is done we'll study the other side of the OLE Controls interfaces, those for control containment.  In this article we'll develop a simple control that does not support inplace activation.  It will display a selected shape with a particular fill color and border, as well as a text string.  So without any further ado...

  Registration and Stuff

When a design-time container (like Visual Basic, FrontPage, or the Visual C++ resource editor) enumerates the CLSID key of the registry, it needs a way to determine if the COM object in question is a control or not.  Correct registration requires that both the control's coclass and type library are registered, and that the appropriate OLE Controls keys are found under the CLSID key.   We'll write the registration code for the control here, using the techniques covered in The Journals of Captain COM: Self Registration.

First, the control's type library is defined with IDL.  Even though we aren't interested in prototyping any methods on the control's default interface yet, we must produce a type library to complete the self-registration requirement.

from captcom.idl

import "ocidl.idl";

[
    uuid(2487F8DD-9748-4123-8A7F-F4C521970F59),
    version(1.0),
    helpstring("Captain COM Controls")
]
library CaptainCOM
{
    importlib("stdole32.tlb");

    [
        uuid(F1C4329F-71C7-435b-ADE3-43A5A454EE7D),
        object,
        dual,
        oleautomation,
        helpstring("IMessageDisplay interface")
    ]
    interface IMessageDisplay : IDispatch
    {
    /*
        methods for IMessageDisplay will be declared here in due time
    */

    }

    [
       
uuid(A4752AC3-6003-4c38-86EC-8A5CCDFC671B),
       
version(1.0),
        control,
       
helpstring("Message Display coclass")
    ]
    coclass MessageDisplay
    {
        [default] interface IMessageDisplay;
    }
}

The only item we haven't seen yet is the MIDL attribute control.   This attribute indicates that a coclass is a visual component (like an OLE Control).  Clients that don't support OLE Controls, upon seeing this attribute, could prevent users from instantiating the component.

from captcom.rgs

HKCR
{
    NoRemove CLSID
    {
        ForceRemove {A4752AC3-6003-4c38-86EC-8A5CCDFC671B} = s 'Message Display Control'
        {
            InprocServer32 = s '%Module%'
            {
                val ThreadingModel = s 'Apartment'
            }
            TypeLib = s '{2487F8DD-9748-4123-8A7F-F4C521970F59}'
            ToolboxBitmap32 = s '%Module%, 2'
            Version = s '1.0'
            ForceRemove 'Control'
           
ForceRemove 'Programmable'
            ProgID = s 'CaptainCOM.MDisplay.1'
            VersionIndependentProgID = s 'CaptainCOM.MDisplay'
            MiscStatus = s '0'
            {
                1 = s '131073'
            }
           
ForceRemove 'Implemented Categories'
            {
               
ForceRemove '{40FC6ED4-2438-11CF-A3DB-080036F12502}'
               
ForceRemove '{40FC6ED5-2438-11CF-A3DB-080036F12502}'
               
ForceRemove '{7DD95801-9882-11CF-9FA9-00AA006C42C4}'
               
ForceRemove '{7DD95802-9882-11CF-9FA9-00AA006C42C4}'
               
ForceRemove '{0DE86A53-2BAA-11CF-A229-00AA003D7352}'
               
ForceRemove '{0DE86A57-2BAA-11CF-A229-00AA003D7352}'
            }
        }
    }

   
ForceRemove CaptainCOM.MDisplay.1 = s 'Message Display Control'
    {
        CLSID = s '{A4752AC3-6003-4c38-86EC-8A5CCDFC671B}'
    }
   
ForceRemove CaptainCOM.MDisplay = s 'Message Display Control'
    {
        CLSID = s '{A4752AC3-6003-4c38-86EC-8A5CCDFC671B}'
        CurVer = s 'SeanControls.MDisplay.1'
    }
}

The above Registrar script is typical for an OLE Control.  The ThreadingModel is specified as single-threaded apartment, as UI components in Windows have thread-affinity.  The TypeLib key holds the GUID of the control's type library.  Without this key, many Automation controllers would be incapable of retrieving your object's type information. 

ToolboxBitmap32 is the first control-specific key we've come across.  Its value is the path of a bitmap resource (in our example, the bitmap resource has ID 2).  This bitmap is available to clients to use to iconically represent a control.  In Visual Basic, for example, the ToolboxBitmap32 image is placed in the Component Toolbox (see Figure 7 below).  While there is no size requirement on the bitmap, 16x16 is recommended for best compatibility with clients.  The color of the lower-left-hand pixel sets the bitmap's transparent color.

The Version tag simply indicates the version of the control.  It should match the version in the ProgID key as well as the version MIDL attribute in the coclass definition (however, even if it doesn't, I can't imagine any problems arising).  While the Version key seems rather unnecessary and redundant, Visual Basic will not enumerate your control if the key is missing.  The Control key is included for compatibility with older controllers.  This key indicates that the COM object in question is an OLE Control.  Newer clients should check for the appropriate component category (we'll get to that later).  The Programmable key tells the container that the object provides type information for its default interface.  Without type information, controls are practically useless.

MiscStatus holds data important to the behavior of your control.  While multiple subkeys are allowed, generally you'll only see a 1 subkey.  This represents the aspect of the control described by the key's value.  DVASPECT_CONTENT has value 1, DVASPECT_THUMBNAIL is 2, DVASPECT_ICON is 4, and DVASPECT_DOCPRINT is 8 (check "wtypes.h").  The latter three aspects are largely ignored (check Brockschmidt for a description of each aspect).  Each aspect key's value is a bitfield (in decimal) of OLEMISC values.  Most containers don't require a control's misc status until after the control is instantiated, and at runtime the misc status can be retrieved with the IOleObject::GetMiscStatus method.  However, many controls (like ATL-generated controls) delegate IOleObject::GetMiscStatus to the COM runtime method OleRegGetMiscStatus, which simply pulls the requested aspect subkey from MiscStatus.  We'll discuss the OLEMISC bits later in this article.

The GUIDs under the Implemented Categories key enumerate the control's supported component categories.  Component categories are simply behaviors and functionality that an object support.  These component categories are defined as subkeys under HKCR\Component Categories and in the Component Categories MSDN documention (documentation path Component Services \ COM \ Controls and Property Pages \ Guide \ ActiveX Control and Control Container Guidelines \ Component Categories).  The implemented categories in our control are Controls, Automation Objects, Safe for Scripting, Safe for Initialization, Persists to StreamInit, and Persists to Property Bag, respectively.   One use of component categories is Internet Explorer scripting.  If a control does not implement the Safe for Scripting category, for example, Internet Explorer will request permission from the user before the object is instantiated, or completely prevent instantiation at all.  This prevents malicious scripts from accessing components with access to system-level resources.

from exports.cpp

#include <windows.h>
#include <atlbase.h>
#include <statreg.h>
#include "captcom_i.h"
#include "resource.h"

HINSTANCE hInstance;
long moduleCount;

int _stdcall DllMain(HINSTANCE hInstance, DWORD reason, void*) {
    if(reason == DLL_PROCESS_ATTACH) {
        ::hInstance = hInstance;
        DisableThreadLibraryCalls(hInstance);
    }
    return 1;
}

extern "C" HRESULT
_stdcall DllRegisterServer() {
    char ansiPath[MAX_PATH];
    GetModuleFileName(hInstance, ansiPath, MAX_PATH);
    wchar_t widePath[MAX_PATH];
    MultiByteToWideChar(CP_ACP, 0, ansiPath, lstrlen(ansiPath) + 1,
        widePath, MAX_PATH);
    CRegObject ro;
    ro.AddReplacement(L"Module", widePath);
    ro.ResourceRegister(widePath, IDR_REGISTRY, L"REGISTRY");

    CComPtr<ITypeLib> pTypeLib;
    HRESULT hr = LoadTypeLib(widePath, &pTypeLib);
   
if(!hr) RegisterTypeLib(pTypeLib, widePath, 0);
    return hr ? SELFREG_E_TYPELIB : S_OK;
}

extern "C" HRESULT
_stdcall DllUnregisterServer() {
   
char ansiPath[MAX_PATH];
    GetModuleFileName(hInstance, ansiPath, MAX_PATH);
   
wchar_t widePath[MAX_PATH];
    MultiByteToWideChar(CP_ACP, 0, ansiPath, lstrlen(ansiPath) + 1,
        widePath, MAX_PATH);
    CRegObject ro;
    ro.AddReplacement(L"Module", widePath);
    ro.ResourceUnregister(widePath, IDR_REGISTRY, L"REGISTRY");
   
return UnRegisterTypeLib(LIBID_CaptainCOM, 1, 0, 0, SYS_WIN32) ?
        SELFREG_E_TYPELIB : S_OK;
}

extern "C" HRESULT
_stdcall DllCanUnloadNow() {
   
return moduleCount ? S_FALSE : S_OK;
}

extern HRESULT GetClassObject_MessageDisplay(REFIID,
void**);

extern "C" HRESULT
_stdcall DllGetClassObject(REFCLSID rclsid, REFIID riid, void** ppv) {
    if(rclsid == CLSID_MessageDisplay)
        return GetClassObject_MessageDisplay(riid, ppv);
    return CLASS_E_CLASSNOTAVAILABLE;
}

The above code listing contains pretty standard implements of the four COM export functions.  Note that we've delegated creation of our control's class factory to a function GetClassObject_MessageDisplay.  For now, this function simply returns CLASS_E_CLASSNOTAVAILABLE.

toolbox.gif (5434 bytes)

Figure 7: VB Component Toolbox

After building and registering our DLL server, the MessageDisplay is enumerated by Visual Basic (and other Automation controllers).  As you see in Figure 7, our control's ToolboxBitmap32 image is used.  The tooltip text is our control's IDL-defiend coclass name.

Before we begin writing code for the control, we need to make sure we have the support classes we'll need.  The first two of the classes are presented here. 

from helpers.h

#pragma once
#include <windows.h>
#include <atlbase.h>

extern long moduleCount;

extern HINSTANCE hInstance;

template<typename T> struct CInnerAggregationObject : IUnknown {
    T* This() { return reinterpret_cast<T*>(
reinterpret_cast<BYTE*>(this) -
        offsetof(T, _innerObject));
    }
    HRESULT _stdcall QueryInterface(REFIID riid, void** ppv) {
       
return This()->InnerQueryInterface(riid, ppv);
    }
    ULONG
_stdcall AddRef() { return This()->InnerAddRef(); }
    ULONG
_stdcall Release() { return This()->InnerRelease(); }
};

template<typename T> class CClassFactory : public IClassFactory {
    ULONG _refCount;
public:
    CClassFactory() : _refCount(0) {
        InterlockedIncrement(&moduleCount);
    }
    ~CClassFactory() { InterlockedDecrement(&moduleCount); }

    // IUnknown method implementations
    HRESULT
_stdcall QueryInterface(REFIID riid, void** ppv) {
        if(!ppv)
return E_POINTER;
       
if(riid == IID_IUnknown) *ppv = this;
        else
if(riid == IID_IClassFactory) *ppv = this;
       
else return *ppv = 0, E_NOINTERFACE;
       
return ++_refCount, S_OK;
    }
    ULONG
_stdcall AddRef() { return ++_refCount; }
    ULONG
_stdcall Release() {
        ULONG ret(--_refCount);
if(!ret) delete this; return ret;
    }

    // IClassFactory method implementations
    HRESULT
_stdcall CreateInstance(IUnknown* pOuter, REFIID riid, void** ppv) {
       
if(!ppv) return E_POINTER;
       
if(pOuter && (riid != IID_IUnknown)) return *ppv = 0, E_INVALIDARG;
        T* t = new T(pOuter);
        t->InnerAddRef();
        HRESULT hr = t->InnerQueryInterface(riid, ppv);
        t->InnerRelease();
       
return hr;
    }
    HRESULT
_stdcall LockServer(BOOL) { return S_OK; }

    // return instance of CClassFactory
    static HRESULT GetClassObject(REFIID riid,
void** ppv) {
        if(!ppv) return E_POINTER;
        CClassFactory<T>* factory = new CClassFactory<T>;
        factory->AddRef();
        HRESULT hr = factory->QueryInterface(riid, ppv);
        factory->Release();
       
return hr;
    }
};

/*
    More goodies to be defined later
*/

Since our control supports aggregation, we need a mechanism to take the place of the outer object and delegate to our control's IUnknown inner methods when aggregation is not actually used.  CInnerAggregationObject will be instantiated as a data member of our control class.  It's IUnknown implementation delegates to the InnerXXXX IUnknown methods in our class.  If you aren't familiar with this mechanism, check out Essential COM by Don Box.  The CClassFactory class is our factory for components that support aggregation.  Notice the static method GetClassObject.  This simplifies the implementation of our GetClassObject_MessageDisplay method; the class factory manages its own instantiation.

from mdisplay.h

#pragma once

#include <windows.h>
#include <atlbase.h>
#include "helpers.h"
#include "captcom_i.h"

class CMessageDisplay :
public IUnknown { // will derive from other interfaces later
    ULONG _refCount;
    IUnknown* _pOuter;
public:
    CInnerAggregationObject<CMessageDisplay> _innerObject;

    CMessageDisplay(IUnknown* pOuter) :

        _pOuter(pOuter ? pOuter : &_innerObject), _refCount(0) {
        InterlockedIncrement(&moduleCount);
    }
    ~CMessageDisplay() { InterlockedDecrement(&moduleCount); }

    // IUnknown delegating methods
    HRESULT _stdcall QueryInterface(REFIID riid,
void** ppv) {
        return _pOuter->QueryInterface(riid, ppv);
    }
    ULONG
_stdcall AddRef() { return _pOuter->AddRef(); }
    ULONG
_stdcall Release() { return _pOuter->Release(); }
   
    // IUnknown implementations
    HRESULT InnerQueryInterface(REFIID, void**);
    ULONG InnerAddRef() {
return ++_refCount; }
    ULONG InnerRelease() {
        ULONG ret(--_refCount); if(!ret) delete this;
return ret;
    }

    // lots more to come!
};

from mdisplay.cpp

#include "mdisplay.h"

HRESULT GetClassObject_MessageDisplay(REFIID riid, void** ppv) {
    return CClassFactory<CMessageDisplay>::GetClassObject(riid, ppv);
}

HRESULT CMessageDisplay::InnerQueryInterface(REFIID riid, void** ppv) {
   
if(!ppv) return E_POINTER;
   
if(riid == IID_IUnknown) *ppv = &_innerObject;
    else return *ppv = 0, E_NOINTERFACE;
    return reinterpret_cast<IUnknown*>(*ppv)->AddRef(), S_OK;
}

This is the start of our control class.  The three IUnknown delegating methods and the three implementation methods have been defined.  Notice that the CMessageDisplay constructor takes a pointer to the controlling aggregation object as its parameter.  This value is passed to the constructor by CClassFactory::CreateInstance.

We'll continue our implementation by exploring the OLE Controls interfaces on page 2.