sean's happy programming place

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

Sean Baxter

 

  The Core Interfaces

This installment of The Journals of Captain COM is intended to prepare you for writing professional quality, windowless, interactive OLE Controls in raw C++.  Why raw C++?  Unfortunately, ATL is sorely lacking in flexibility, and the framework it provides doesn't allow real optimization of rendering, for example.  Additionally, the ATL control host, CAxWindowT<>, is riddled with bugs and design blunders which prevents controls from sizing themselves or activating inplace windowless.  In the next installment I'll present a gradient-slider control (as seen in Photoshop) and narrate its construction.  And in the article following that we'll write our own control host, free of the problems that riddle ATL.

Here I present a non-interactive control (that does not go inplace active) which draws a specified message in a particular shape  filled with any color and brush hatching.  This control can be dropped in to a Visual Basic form and customized to give many different looks.

ctrlvb1.gif (61836 bytes)

Figure 1: Controls in Visual Basic

Additionally, I've created one property page (which is a COM object all by itself) and specified two stock pages to provide a mechanism for very easy programming.

ctrlvb2.gif (26700 bytes)

Figure 2: Property Pages

And yes, by supporting property bag persistence, the control can be embedded inside a webpage and viewed with Internet Explorer.  Unfortunately, the combination of weak webpage programming languages (like VBScript and JScript), the requirement of dispatch interfaces and property bag persistence, security concerns, and long download times have made the web browser a less suitable OLE Control host and application platform.  We'll tackle these issues in a future installment.

ctrlweb1.gif (37598 bytes)

Figure 3: Controls in MSIE

With the registration code completed on the first page, it's time to jump into OLE Control's core interfaces: IOleObject, IViewObject2, and IPersistStreamInitIOleObject is the most crucial (and with 21 methods, the biggest) standard interface your control will implement.  While many of the methods of IOleObject are intended for use in document objects (such as controls that can be embedded in Word and deal with the clipboard), it has many responsibilities that are relevant even in small, lightweight controls like the ones we'll be creating, including size negotiation, setting the site interface, and verb execution (which is important for inplace activation).  IViewObject2 derives IViewObject and adds an additional but redundant method, GetExtent.  The host invokes IViewObject2::Draw on your control to demand a rendering.  Because IViewObject2::Draw passes a non-remotable type (an HDC), IViewObject2 can only be implemented by controls that run in-process (the interface IDataObject, which we won't be covering) provides a mechanism for rendering controls that live in local servers.  This doesn't concern us, however, as all of our controls will be in-process.  Finally, the IPersistStreamInit interface allows the host to present the control a persistence stream with which to save and load itself.  While the IStream interface could wrap nearly any storage entity or network protocol, most hosts pass the implementation that wraps a structured storage file (see StgCreateDocfile for more information).   Even though a control doesn't necessarily need to implement IPersistStreamInit, it is required to support at least one persistence interface for compatibility with most containers, including Visual Basic.

The control's primary interface is a dual interface, that is, it derives IDispatch and its methods only accept VARIANT-compatible types.  This makes the control available to a wider audience of clients (especially scripting languages found in web browsers like VBScript and JScript), but does prove a bit inconvenient for C++ clients (can't pass structs, for example).  I now present the Message Display control's implementation of IOleObject:

from mdisplay.cpp

class CMessageDisplay : public IMessageDisplay, public IOleObject, ... {
    ULONG _refCount;
    IUnknown* _pOuter;

    static CComPtr<ITypeInfo> _pDispatchInfo;
   
static CComPtr<ITypeInfo> _pClassInfo;

    CComPtr<IOleClientSite> _pClientSite;

    CComPtr<IAdviseSink> _pViewAdviseSink;
    CComPtr<IOleAdviseHolder> _pAdviseHolder;

    SIZEL _himetricExtent, _pixelExtent;
    // other data members go here
public:
    CInnerAggregationObject<CMessageDisplay> _innerObject;

    CMessageDisplay(IUnknown*);
    ~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;
    }
   
    // IDispatch methods (delegate to the type info for the tricky stuff)
    HRESULT
_stdcall GetTypeInfoCount(UINT* pctInfo) {
       
if(!pctInfo) return E_POINTER;
       
return *pctInfo = 1, S_OK;
    }
    HRESULT
_stdcall GetTypeInfo(UINT tiInfo, LCID, ITypeInfo** ppTypeInfo) {
       
if(!ppTypeInfo) return E_POINTER;
       
if(tiInfo) return *ppTypeInfo = 0, DISP_E_BADINDEX;
        _pDispatchInfo.CopyTo(ppTypeInfo);
       
return S_OK;
    }
    HRESULT
_stdcall GetIDsOfNames(REFIID, OLECHAR** rgszNames, UINT cNames,
        LCID, DISPID* rgDispId) {
       
return _pDispatchInfo->GetIDsOfNames(rgszNames, cNames, rgDispId);
    }
    HRESULT
_stdcall Invoke(DISPID dispId, REFIID, LCID, WORD wFlags,
        DISPPARAMS* pDispParams, VARIANT* pVarResult, EXCEPINFO* pExcepInfo,
        UINT* puArgErr) {
       
return _pDispatchInfo->Invoke(static_cast<IMessageDisplay*>(this),
            dispId, wFlags, pDispParams, pVarResult, pExcepInfo, puArgErr);
    }

    // IOleObject implementation (yikes)
    HRESULT
_stdcall SetClientSite(IOleClientSite* pClientSite) {
        _pClientSite = pClientSite;
       
return S_OK;
    }
    HRESULT
_stdcall GetClientSite(IOleClientSite** ppClientSite) {
        if(!ppClientSite)
return E_POINTER;
        _pClientSite.CopyTo(ppClientSite);
       
return S_OK;
    }
    HRESULT
_stdcall SetHostNames(LPCOLESTR, LPCOLESTR) { return S_OK; }
    HRESULT
_stdcall Close(DWORD dwSaveOption) {
       
if((dwSaveOption == OLECLOSE_SAVEIFDIRTY) ||
            (dwSaveOption == OLECLOSE_PROMPTSAVE) && _isDirty)
            _pClientSite->SaveObject();
       
if(_pViewAdviseSink) _pViewAdviseSink->OnClose();
        _pAdviseHolder->SendOnClose();
        _pClientSite.Release();
       
return S_OK;
    }
    HRESULT
_stdcall SetMoniker(DWORD, IMoniker*) { return E_NOTIMPL; }
    HRESULT
_stdcall GetMoniker(DWORD, DWORD, IMoniker** ppMoniker) {
        if(ppMoniker) *ppMoniker = 0;
       
return E_NOTIMPL;
    }
    HRESULT
_stdcall InitFromData(IDataObject*, BOOL, DWORD) {
       
return E_NOTIMPL;
    }
    HRESULT
_stdcall IsUpToDate() { return S_OK; }
    HRESULT
_stdcall GetClipboardData(DWORD, IDataObject**) {
       
return E_NOTIMPL;
    }
    HRESULT
_stdcall DoVerb(long, MSG*, IOleClientSite*, long, HWND, LPCRECT);
    HRESULT
_stdcall EnumVerbs(IEnumOLEVERB**);
    HRESULT
_stdcall Update() { return S_OK; }
    HRESULT
_stdcall GetUserClassID(CLSID* pClsid) {
       
if(!pClsid) return E_POINTER;
       
return *pClsid = CLSID_MessageDisplay, S_OK;
    }
    HRESULT
_stdcall GetUserType(DWORD dwFormOfType, LPOLESTR* pszUserType) {
       
return OleRegGetUserType(CLSID_MessageDisplay, dwFormOfType, pszUserType);
    }
    HRESULT
_stdcall SetExtent(DWORD dwAspect, SIZEL* pSizel) {
       
if(!pSizel) return E_POINTER;
       
if(dwAspect != DVASPECT_CONTENT) return E_FAIL;
        _himetricExtent = *pSizel;
        _pixelExtent = HimetricToPixel(_himetricExtent);
        _pixelExtent.cx = max(10, _pixelExtent.cx);
        _pixelExtent.cy = max(10, _pixelExtent.cy);
        _himetricExtent = PixelToHimetric(_pixelExtent);
       
return S_OK;
    }
    HRESULT
_stdcall GetExtent(DWORD dwAspect, SIZEL* pSizel) {
       
if(!pSizel) return E_POINTER;
       
if(dwAspect != DVASPECT_CONTENT) return E_INVALIDARG;
       
return *pSizel = _himetricExtent, S_OK;
    }
    HRESULT
_stdcall Advise(IAdviseSink* pAdvSink, DWORD* pdwConnection) {
       
return _pAdviseHolder->Advise(pAdvSink, pdwConnection);
    }
    HRESULT
_stdcall Unadvise(DWORD dwConnection) {
       
return _pAdviseHolder->Unadvise(dwConnection);
    }
    HRESULT
_stdcall EnumAdvise(IEnumSTATDATA** ppEnumAdvise) {
       
return _pAdviseHolder->EnumAdvise(ppEnumAdvise);
    }
    HRESULT
_stdcall GetMiscStatus(DWORD dwAspect, DWORD* pdwStatus) {
        if(!pdwStatus)
return E_POINTER;
       
if(dwAspect != DVASPECT_CONTENT) return *pdwStatus = 0, E_FAIL;
       
return *pdwStatus = 131073, S_OK;
    }
    HRESULT
_stdcall SetColorScheme(LOGPALETTE*) { return E_NOTIMPL; }

    // more class methods here
};

CComPtr<ITypeInfo> CMessageDisplay::_pDispatchInfo = 0;
CComPtr<ITypeInfo> CMessageDisplay::_pClassInfo = 0;


CMessageDisplay::CMessageDisplay(IUnknown* pOuter) :
    _pOuter(pOuter ? pOuter : &_innerObject), _refCount(0) {
    InterlockedIncrement(&moduleCount);
   
if(!_pDispatchInfo) {        // this is the first instance - load type info
        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);
        CComPtr<ITypeLib> pTypeLib;
        LoadTypeLib(widePath, &pTypeLib);
        pTypeLib->GetTypeInfoOfGuid(IID_IMessageDisplay, &_pDispatchInfo);
        pTypeLib->GetTypeInfoOfGuid(CLSID_MessageDisplay, &_pClassInfo);
    }
    CreateOleAdviseHolder(&_pAdviseHolder);
    // a few other things will go here
}

This is a fair amount of code, but very little logic.  The IDispatch implementation and its associated data members, _pDispatchInfo and _pClassInfo, are boilerplate and were presented in the previous installment of The Journals of Captain COM_pClientSite is a pointer to the host's site interface, on which the control can request a save or query for the container.  The host can also establish advisory connections with the control through the methods IOleObject::Advise and IViewObject2::SetAdvise.  Through these interfaces the control can notify the host of view changes, saves, and closure.  Each of the advisory functions work a bit differently: the IOleObject method supports multiple advise sinks.  It maintains an advise sink holder, _pAdviseHolder, which is provided by the COM runtime CreateOleAdviseHolder.

The _pixelExtent and _himetricExtent data members contain the width and height of the control.  Why do we need to structures to hold this data?  The OLE Controls standard wanted to specify control dimensions in device-independent units (the motivation was real when working document objects even though it's unnecessary for our purposes), so SetExtent works in HIMETRIC units.  A single HIMETRIC unit is 1/100th of a millimeter, and the number of millimeters per pixel can be resolved from GetDeviceCaps and some arithmetic (incidentally, GetDeviceCaps always returns 96 pixels per logical inch when provided a screen DC).  The following code converts between HIMETRIC and pixels:

from helpers.h

const float HIMETRIC_PER_PIXEL(26.4583333333f);

inline SIZEL PixelToHimetric(SIZEL pixelSize) {
    SIZEL ret;
    ret.cx = static_cast<long>(pixelSize.cx * HIMETRIC_PER_PIXEL + 0.5);
    ret.cy = static_cast<long>(pixelSize.cy * HIMETRIC_PER_PIXEL + 0.5);
    return ret;
}

inline SIZEL HimetricToPixel(SIZEL himetricSize) {
    SIZEL ret;
    ret.cx = static_cast<long>(himetricSize.cx / HIMETRIC_PER_PIXEL + 0.5);
    ret.cy = static_cast<long>(himetricSize.cy / HIMETRIC_PER_PIXEL + 0.5);
   
return ret;
}

There are a great many methods of IOleObject that I've left E_NOTIMPL.  Monikers are COM objects used to identify and activate other COM objects, and are dealt with in the methods GetMoniker and SetMoniker.   Because we really have no use for monikers, these methods are unimplemented.  Check MSDN Library for a detailed coverage of monikers if you're curious.  InitFromData and GetClipboardData are both methods that deal with the clipboard.  The latter function will persist the object to the clipboard, and the former will load the object from clipboard data.  Since we don't need that functionality, they too are E_NOTIMPL.  IsUpToDate and Update typically deal with document objects that live in local servers, and won't be dealt with here (check Brockschmidt's Inside OLE for coverage of these methods and all things document-related).  SetColorScheme allows the container to tell the control what colors it prefers over the system colors.  Then, instead of calling GetSysColor to retrieve, say, the dialog color, the control would pluck the appropriate color from the LOGPALETTE provided to it.  I didn't implement this method (and neither did ATL) because of the unnecessary code burden it brings.  Plus, all of the colors of the Message Display control are customizable through methods exposed by the primary interface.

Most of the remaining methods of IOleObject are implemented with no trouble.  SetClientSite and GetClientSite do exactly what their names indicate and no more.  SetHostNames is invoked by the host to inform the control of the name of its container.  Since we don't care, we ignore this information.  GetUserClassID and GetUserType return the CLSID of the control and its name, respectively.  If GetUserType weren't already easy to implement, COM has provided a helper function OleRegGetUserType which dives into the registry and digs up the object's name for us.  We can simply delegate the trio of functions Advise, Unadvise, and EnumAdvise to the advise holder we created in the class's constructor. 

SetExtent has a nominal amount of logic: it sets the control's extent to the size parameter and adjusts both the height and width to a minium of ten pixels.  Isn't this disobeying the orders of the container?  No.   The control actually has final say on its extent with the GetExtent method.  All the host can really do is recommend a size for the control.  When the host calls SetExtent, it will follow it up immediately with a call to GetExtent to see if the control has accepted, rejected, or altered the suggestion.  In addition to the size, these two methods also take an aspect parameter.  The aspect tells the control which of the renderings (like icon, thumbnail, or full content) the caller is refering to.  In non-document controls, we are only interested in full-content renderings, so we reject anything except DVASPECT_CONTENT (don't worry - ATL does the same).

The GetMiscStatus method returns the important set of status bits to the caller.  This is the same status as specified in the registry key MiscStatus.  If you look at the MSDN Library description of the OLEMISC enum, you'll see that 131073 corresponds to the status bits OLEMISC_SETCLIENTSITEFIRST and OLEMISC_RECOMPOSEONRESIZE: we want the container to call SetClientSite prior to calling methods on any of the persistence interfaces, and after a resize we want the container to SetExtent and request a redraw with IViewObject::Draw rather than just stretching the previous rendering.  When we start working with inplace activated controls in the next installment of The Journals of Captain COM you'll see more of the status bits.  Close can potentially be a difficult to implement method, but in our example, it's very simple.  The function requests a save from the client site when deemed appropriate by the parameter.  The function then releases IViewObject2's advise sink, releases IOleObject's advise holder, and finally releases the site.  The control is unusable when Close returns, and the control will be Released to death shortly after.

The two remaining IOleObject methods are DoVerb and EnumVerbs.  Verbs are similar to methods in that they perform an action of some sort, but were originally intended to be more easily accessible to the user.  In a document, for example, a control that plays multimedia files could provide verbs like "play," "stop," "pause," and "rewind."  The container could enumerate these verbs and present them to the user through a popup menu.  Our control supports two verbs: one shows the property frame and one brings up the control's about box.  There are a number of standard verbs (that aren't enumerated by the control) that deal with inplace activated controls.  We'll deal with verbs later on in the article, when we have some functionality coded.

ctrlvb3.gif (41734 bytes)

Figure 4: Verbs Displayed in a Popup Menu

Next we'll cover the IViewObject2 implementation.  This isn't a difficult function to implement (with the exception of Draw, which can be as complicated as required), but the data members that allow for much of the rendering functionality will be introduced here, so the following source is fairly hefty:

from captcom.idl

typedef enum tagFillType {
    fillSolid,
    fillBackwardDiagonal,
    fillForwardDiagonal,
    fillCrosshatch,
    fillDiagonalCrosshatch,
    fillHorizontal,
    fillVertical
} fillType;

typedef
enum tagShapeType {
    shapeEllipse,
    shapeRectangle,
    shapeHourglass,
    shapeDiamond
} shapeType;

from mdisplay.cpp

class CMessageDisplay : public IMessageDisplay, public IViewObject2 {
    CComPtr<IAdviseSink> _pViewAdviseSink;
    SIZEL _himetricExtent, _pixelExtent;
    DWORD _advf;                 // IViewObject advise sink flags
    void SendViewChange();
   
void LoadSpecialColors();

    // drawing properties
    OLE_COLOR _oleColorFill, _oleColorFore, _oleColorBorder;
    COLORREF _rgbColorFill, _rgbColorFore, _rgbColorBorder;
    union {
        DWORD _colorFlags;
        struct {
            DWORD _specialFillColor:1;
            DWORD _specialForeColor:1;
            DWORD _specialBorderColor:1;
        };
    };
    long _borderWidth;
    fillType _fillType;
    shapeType _shapeType;
    CComBSTR _bstrText;
    FONTDESC _fontDesc;

    // other goodies too numerous to display here

public:
    // IViewObject
    HRESULT _stdcall Draw(DWORD, long,
void*, DVTARGETDEVICE*, HDC, HDC,
        LPCRECTL, LPCRECTL, BOOL(_stdcall*)(DWORD), DWORD);
    HRESULT _stdcall SetAdvise(DWORD aspect, DWORD advf, IAdviseSink* pAdvSink) {
        if(aspect != DVASPECT_CONTENT) return DV_E_DVASPECT;
        _advf = advf;
        _pViewAdviseSink = pAdvSink;
        if(_advf & ADVF_PRIMEFIRST) SendViewChange();
        return S_OK;
    }
    HRESULT
_stdcall GetAdvise(DWORD* pAspects, DWORD* pAdvf, IAdviseSink** ppAdvSink) {
       
if(!ppAdvSink) return E_POINTER;
        _pViewAdviseSink.CopyTo(ppAdvSink);
       
if(pAspects) *pAspects = DVASPECT_CONTENT;
       
if(pAdvf) *pAdvf = _advf;
       
return S_OK;
    }
    HRESULT
_stdcall Freeze(DWORD, long, void*, DWORD*) { return E_NOTIMPL; }
    HRESULT
_stdcall Unfreeze(DWORD) { return E_NOTIMPL; }
    HRESULT
_stdcall GetColorSet(DWORD, long, void*, DVTARGETDEVICE*, HDC,
        LOGPALETTE**) {
return E_NOTIMPL; }

    // IViewObject2
    HRESULT
_stdcall GetExtent(DWORD aspect, long, DVTARGETDEVICE*, SIZEL* pSize) {
       
if(!pSize) return E_POINTER;
       
if(aspect != DVASPECT_CONTENT) return DV_E_DVASPECT;
       
return *pSize = _himetricExtent, S_OK;
    }
    // many more methods
};

CMessageDisplay::CMessageDisplay(IUnknown* pOuter) :
    _pOuter(pOuter ? pOuter : &_innerObject), _refCount(0) {
    // see above for the rest of this constructor definition
    _advf = 0;
    ZeroMemory(&_fontDesc, sizeof(_fontDesc));
}

HRESULT
_stdcall CMessageDisplay::Draw(DWORD dwAspect, long, void*,
    DVTARGETDEVICE*, HDC, HDC hdc, LPCRECTL pRectBounds, LPCRECTL pRectWBounds,
    BOOL(_stdcall*)(DWORD), DWORD) {

    if(dwAspect != DVASPECT_CONTENT) return DV_E_DVASPECT;
    HBRUSH brush;
   
if(_fillType == fillSolid) brush = CreateSolidBrush(_rgbColorFill);
    else {
        switch(_fillType) {
        case fillBackwardDiagonal:
            brush = CreateHatchBrush(HS_BDIAGONAL, _rgbColorFill);
            break;
       
case fillForwardDiagonal:
            brush = CreateHatchBrush(HS_FDIAGONAL, _rgbColorFill);
           
break;
       
case fillCrosshatch:
            brush = CreateHatchBrush(HS_CROSS, _rgbColorFill);
           
break;
       
case fillDiagonalCrosshatch:
            brush = CreateHatchBrush(HS_DIAGCROSS, _rgbColorFill);
           
break;
       
case fillHorizontal:
            brush = CreateHatchBrush(HS_HORIZONTAL, _rgbColorFill);
           
break;
       
case fillVertical:
            brush = CreateHatchBrush(HS_VERTICAL, _rgbColorFill);
           
break;
        }
    }

    HPEN pen = _borderWidth ? CreatePen(PS_SOLID, _borderWidth, _rgbColorBorder) :
        static_cast<HPEN>(GetStockObject(NULL_PEN));

    char fontName[512];
    WideCharToMultiByte(CP_ACP, 0, _fontDesc.lpstrName,
        lstrlenW(_fontDesc.lpstrName) + 1, fontName, 512, 0, 0);
    HFONT font = CreateFont(-MulDiv(_fontDesc.cySize.Lo, 96, 72) / 10000,
        0, 0, 0, _fontDesc.sWeight, _fontDesc.fItalic, _fontDesc.fUnderline,
        _fontDesc.fStrikethrough, _fontDesc.sCharset, 0, 0, 0, 0, fontName);

    HGDIOBJ oldBrush = SelectObject(hdc, brush);
    HGDIOBJ oldPen = SelectObject(hdc, pen);
    HGDIOBJ oldFont = SelectObject(hdc, font);
    int oldTextColor = SetTextColor(hdc, _rgbColorFore);
    int oldBkMode = SetBkMode(hdc, TRANSPARENT);

    switch(_shapeType) {
    case shapeEllipse:
        Ellipse(hdc, pRectBounds->left, pRectBounds->top, pRectBounds->right,
            pRectBounds->bottom);
        break;
   
case shapeRectangle:
        Rectangle(hdc, pRectBounds->left, pRectBounds->top, pRectBounds->right,
            pRectBounds->bottom);
       
break;
   
case shapeHourglass: {
        POINT points[3];
        points[0].x = pRectBounds->left;
        points[0].y = pRectBounds->top;
        points[1].x = pRectBounds->right;
        points[1].y = points[0].y;
        points[2].x = (pRectBounds->left + pRectBounds->right) / 2;
        points[2].y = (pRectBounds->top + pRectBounds->bottom) / 2;
        Polygon(hdc, points, 3);
        points[0].y = pRectBounds->bottom;
        points[1].y = points[0].y;
        Polygon(hdc, points, 3);
       
break;
        }
   
case shapeDiamond: {
        POINT points[4];
        points[0].x = pRectBounds->left;
        points[0].y = (pRectBounds->top + pRectBounds->bottom) / 2;
        points[1].x = (pRectBounds->left + pRectBounds->right) / 2;
        points[1].y = pRectBounds->top;
        points[2].x = pRectBounds->right;
        points[2].y = points[0].y;
        points[3].x = points[1].x;
        points[3].y = pRectBounds->bottom;
        Polygon(hdc, points, 4);
       
break;
        }
    }
    char message[512];
    WideCharToMultiByte(CP_ACP, 0, _bstrText, _bstrText.Length() + 1,
        message, _bstrText.Length() + 1, 0, 0);
    RECT rect;
    memcpy(&rect, pRectBounds, sizeof(rect));
    DrawText(hdc, message, _bstrText.Length(), &rect,
        DT_CENTER | DT_VCENTER | DT_SINGLELINE);

    SetBkMode(hdc, oldBkMode);
    SetTextColor(hdc, oldTextColor);
    DeleteObject(SelectObject(hdc, oldBrush));
    DeleteObject(SelectObject(hdc, oldFont));
    DeleteObject(SelectObject(hdc, oldPen));
    return S_OK;
}

void CMessageDisplay::SendViewChange() {
    if(_pViewAdviseSink) _pViewAdviseSink->OnViewChange(DVASPECT_CONTENT, -1);
   
if(_advf & ADVF_ONLYONCE) {
        _pViewAdviseSink = 0;
        _advf = 0;
    }
}

void CMessageDisplay::LoadSpecialColors() {
    _specialFillColor = _specialForeColor = _specialBorderColor = false;
    _oleColorFill = GetSysColor(COLOR_ACTIVECAPTION);
    _rgbColorFill = _oleColorFill;   
    DISPPARAMS dispparams = { 0, 0, 0, 0 };
    CComVariant retVal;
   
    CComPtr<IDispatch> pHost;
    HRESULT hr = _pClientSite->QueryInterface(&pHost);
    if(hr) {
        _oleColorFore = GetSysColor(COLOR_CAPTIONTEXT);
        _rgbColorFore = _oleColorFore;
        _oleColorBorder = GetSysColor(COLOR_WINDOWFRAME);
        _rgbColorBorder = _oleColorBorder;
       
return;
    }

    hr = pHost->Invoke(DISPID_AMBIENT_FORECOLOR, IID_NULL, 0, DISPATCH_PROPERTYGET,
        &dispparams, &retVal, 0, 0);
   
if(!hr) {
        _specialForeColor = true;
        _oleColorFore = retVal.lVal;
        OleTranslateColor(_oleColorFore, 0, &_rgbColorFore);
    } else _oleColorFore = _rgbColorFore = GetSysColor(COLOR_CAPTIONTEXT);

    // BorderColor isn't an ambient property, so use ForeColor
    _rgbColorBorder = _rgbColorFore;
    _specialBorderColor = _specialForeColor;
    _oleColorBorder = _oleColorFore;
}

The data members above hold information relevant to rendering the control.  The FONTDESC structure _fontDesc is like a simplified LOGFONT.  It's used to create an OLE font object with the method OleCreateFontIndirect, which we'll encounter when the primary interface is defined.  _bstrText holds the string that will be rendered in the center of the control.  Its length is restricted to 253 characters by the put_Text function, which we'll get to shortly.  _shapeType and _fillType are instances of enumerations defined in the IDL file.  The former has values correlating to shapes the GDI can draw: a rectangle, an ellipse, an hourglass, and a diamond; the latter holds brush styles for CreateHatchBrush, plus fillSolid for CreateSolidBrush_borderWidth contains the width of the pen used to draw the shape's border.  If this element is zero, a null pen will be used.

The color management involved may seem initially a bit strange.   Notice that for each color there is a COLORREF, an OLE_COLOR, and a flag.  OLE_COLOR is the agreed-upon color type in OLE Controls.  Automation controllers that load type information and use it to present the user with a property window (like VB) will give the user a color palette to select an OLE_COLOR from.  The stock color property page also looks for OLE_COLOR parameters.  An OLE_COLOR is similar to a COLORREF, except it can index into the system palette using the space in the high byte.  It's safe to assign a COLORREF to an OLE_COLOR (as is done in LoadSpecialColors), but assigning an OLE_COLOR to a COLORREF requires the COM runtime function OleTranslateColor.   The COLORREF data members in CMessageDisplay are simply the OLE_COLOR members pushed through this translation function.  The special flags simply indicate whether or not the corresponding color has been explicitly set by the client.   If not, then when the control is loaded from its persistent data, its colors will be reset to the current defaults, not the defaults at the time the control was saved.  The method LoadSpecialColors sets the color members to the defaults and clears the special flags.  Notice that the control queries the site's dispatch interface for DISPID_AMBIENT_FORECOLOR and, if available, sets the foreground color to it.

The MSDN Library documentation of the OLE Controls interfaces is by no means error-free.  For example:

from MSDN Library documentation of IViewObject2::GetExtent

HRESULT GetExtent(
    DWORD dwAspect,             //View object for which the size is being requested
    DWORD lindex,               //Part of the object to draw
    DVTARGETDEVICE ptd,         //Pointer to the target device in a structure
    LPSIZEL lpsizel             //Pointer to size of object
);

My class definition, however, says that GetExtent's second and third parameters are of types long and DVTARGETDEVICE*, respectively.  Mine compiles, MSDN's won't.  If you ever are boggled by a compiler error and suspect the documentation is to blame, check the IDL that's included with the Platform SDK.  Here's another troublesome mistake:

from MSDN Library documentation of IFont::get_Size

Retrieves the point size of the font expressed in a 64-bit CY variable. The upper 32-bits of this value contains the integer point size and the lower 32-bits contains the fractional point size.

Wrong again!  Look at the height parameter of the CreateFont call in CMessageDisplay::Draw.  The low 32-bits of _fontDesc.cySize is transformed into pixels (through the MulDiv.  See the CreateFont docs if you don't understand this) and then divided by 10000.  Contrary to the docs, the size of the font is not the upper 32-bits of the currency structure, but 1/10000th of the lower 32-bits.

from  mdisplay.cpp

class CMessageDisplay : public IMessageDisplay, public IPersistStreamInit,
   
public IPersistPropertyBag, ... {
    bool _isDirty;
    // more stuff here
public:

    // IPersist
    HRESULT _stdcall GetClassID(CLSID* pClassID) {
        return GetUserClassID(pClassID);
    }
    // IPersistStreamInit
    HRESULT _stdcall InitNew();
    HRESULT
_stdcall Load(IStream*);
    HRESULT
_stdcall Save(IStream*, BOOL);
    HRESULT
_stdcall IsDirty() {
        return _isDirty ? S_OK : S_FALSE;
    }
    HRESULT
_stdcall GetSizeMax(ULARGE_INTEGER*);

   // IPersistPropertyBag
    HRESULT _stdcall Save(IPropertyBag*, BOOL, BOOL);
    HRESULT
_stdcall Load(IPropertyBag*, IErrorLog*)
};

HRESULT
_stdcall CMessageDisplay::InitNew() {
    _pixelExtent.cx = 150;
    _pixelExtent.cy = 100;
    _himetricExtent = PixelToHimetric(_pixelExtent);
    LoadSpecialColors();
    _borderWidth = 3;
    _fillType = fillSolid;
    _shapeType = shapeEllipse;
    _bstrText = L"Hello There";
    _fontDesc.cbSizeofstruct = sizeof(_fontDesc);
    _fontDesc.cySize.Lo = 18 * 10000;
    _fontDesc.cySize.Hi = 0;
    _fontDesc.fItalic = 0;
    _fontDesc.fStrikethrough = 0;
    _fontDesc.sCharset = 0;
    _fontDesc.fUnderline = 0;
    _fontDesc.sWeight = FW_NORMAL;
    _fontDesc.lpstrName = reinterpret_cast<OLECHAR*>(
        CoTaskMemAlloc(2 * (5 + 1)));
    lstrcpyW(_fontDesc.lpstrName, L"Arial");
    return S_OK;
}

///////////////////////////////////////////////////////////////////////////////
// persist stream order
//
// fillType
// borderWidth
// shapeType
// oleColorFill
// oleColorBorder
// oleColorFore
// colorFlags
// bstrText
// fontDesc


HRESULT _stdcall CMessageDisplay::Save(IStream* pStream, BOOL clearDirty) {
    if(clearDirty) _isDirty = false;
    ULONG write;
    pStream->Write(&_fillType, sizeof(_fillType), &write);
    pStream->Write(&_borderWidth,
sizeof(_borderWidth), &write);
    pStream->Write(&_shapeType,
sizeof(_shapeType), &write);
    pStream->Write(&_oleColorFill,
sizeof(_oleColorFill), &write);
    pStream->Write(&_oleColorBorder,
sizeof(_oleColorBorder), &write);
    pStream->Write(&_oleColorFore,
sizeof(_oleColorFore), &write);
    pStream->Write(&_colorFlags,
sizeof(_colorFlags), &write);
    _bstrText.WriteToStream(pStream);
    pStream->Write(&_fontDesc,
sizeof(_fontDesc), &write);
        // lpstrName in struct is ignore
    CComBSTR fontName(_fontDesc.lpstrName);
    fontName.WriteToStream(pStream);
    return S_OK;
}

HRESULT
_stdcall CMessageDisplay::Load(IStream* pStream) {
    ULONG read;
    pStream->Read(&_fillType,
sizeof(_fillType), &read);
    pStream->Read(&_borderWidth,
sizeof(_borderWidth), &read);
    pStream->Read(&_shapeType,
sizeof(_shapeType), &read);
    pStream->Read(&_oleColorFill,
sizeof(_oleColorFill), &read);
    pStream->Read(&_oleColorBorder,
sizeof(_oleColorBorder), &read);
    pStream->Read(&_oleColorFore,
sizeof(_oleColorFore), &read);
    pStream->Read(&_colorFlags,
sizeof(_colorFlags), &read);
    _bstrText.ReadFromStream(pStream);
    pStream->Read(&_fontDesc,
sizeof(_fontDesc), &read);
    CComBSTR fontName;
    fontName.ReadFromStream(pStream);
    _fontDesc.lpstrName = reinterpret_cast<OLECHAR*>(
        CoTaskMemAlloc(2 * (fontName.Length() + 1)));
    lstrcpyW(_fontDesc.lpstrName, fontName);

    if(!_specialForeColor) _oleColorFore = GetSysColor(COLOR_CAPTIONTEXT);
   
if(!_specialFillColor) _oleColorFill = GetSysColor(COLOR_ACTIVECAPTION);
   
if(!_specialBorderColor) _oleColorBorder = GetSysColor(COLOR_WINDOWFRAME);
    OleTranslateColor(_oleColorFore, 0, &_rgbColorFore);
    OleTranslateColor(_oleColorFill, 0, &_rgbColorFill);
    OleTranslateColor(_oleColorBorder, 0, &_rgbColorBorder);    
   
return S_OK;
}

HRESULT _stdcall CMessageDisplay::Save(IPropertyBag* pPropertyBag, BOOL clearDirty, BOOL) {
    CComVariant var;
    var.vt = VT_UI4;
    var.lVal = _fillType;   
    pPropertyBag->Write(L"FillStyle", &var);
    var.lVal = _borderWidth;
    pPropertyBag->Write(L"BorderWidth", &var);
    var.lVal = _shapeType;
    pPropertyBag->Write(L"Shape", &var);
    if(_specialFillColor) {
        var.lVal = _oleColorFill;
        pPropertyBag->Write(L"FillColor", &var);
    }
   
if(_specialBorderColor) {
        var.lVal = _oleColorBorder;
        pPropertyBag->Write(L"BorderColor", &var);
    }
   
if(_specialForeColor) {
        var.lVal = _oleColorFore;
        pPropertyBag->Write(L"ForeColor", &var);
    }
    var = _bstrText;
    pPropertyBag->Write(L"Text", &var);
    var.Clear();
    var.vt = VT_UI4;
    var.lVal = _fontDesc.fItalic;
    pPropertyBag->Write(L"Italic", &var);
    var.lVal = _fontDesc.fUnderline;
    pPropertyBag->Write(L"Underline", &var);
    var.lVal = _fontDesc.fStrikethrough;
    pPropertyBag->Write(L"Strikethrough", &var);
    var.lVal = _fontDesc.sCharset;   
    pPropertyBag->Write(L"Charset", &var);
    var.lVal = _fontDesc.sWeight;
    pPropertyBag->Write(L"Weight", &var);
    var.vt = VT_CY;
    var.cyVal = _fontDesc.cySize;
    pPropertyBag->Write(L"FontSize", &var);
    var.vt = VT_BSTR;
    var.bstrVal = SysAllocString(_fontDesc.lpstrName);
    pPropertyBag->Write(L"FontName", &var);
   
if(clearDirty) _isDirty = false;
   
    return S_OK;
}

HRESULT _stdcall CMessageDisplay::Load(IPropertyBag* pPropertyBag, IErrorLog*) {
    _borderWidth = 3;
    _fillType = fillSolid;
    _shapeType = shapeEllipse;
    _bstrText = L"Hello There";
    _fontDesc.cbSizeofstruct = sizeof(_fontDesc);
    _fontDesc.cySize.Lo = 18 * 10000;
    _fontDesc.cySize.Hi = 0;
    _fontDesc.fItalic = 0;
    _fontDesc.fStrikethrough = 0;
    _fontDesc.sCharset = 0;
    _fontDesc.fUnderline = 0;
    _fontDesc.sWeight = FW_NORMAL;
    _fontDesc.lpstrName = reinterpret_cast<OLECHAR*>(
        CoTaskMemAlloc(2 * (5 + 1)));
    lstrcpyW(_fontDesc.lpstrName, L"Arial");
    _colorFlags = 0;
   
    CComVariant var;
    var.vt = VT_UI4;
    HRESULT hr = pPropertyBag->Read(L"FillStyle", &var, 0);
    if(!hr) _fillType = static_cast<fillType>(var.lVal);
    hr = pPropertyBag->Read(L"BorderWidth", &var, 0);
   
if(!hr) _borderWidth = var.lVal;
    hr = pPropertyBag->Read(L"Shape", &var, 0);
   
if(!hr) _shapeType = static_cast<shapeType>(var.lVal);
    hr = pPropertyBag->Read(L"FillColor", &var, 0);
   
if(!hr) _oleColorFill = var.lVal, _specialFillColor = true;
    hr = pPropertyBag->Read(L"BorderColor", &var, 0);
   
if(!hr) _oleColorBorder = var.lVal, _specialBorderColor = true;
    hr = pPropertyBag->Read(L"ForeColor", &var, 0);
   
if(!hr) _oleColorFore = var.lVal, _specialForeColor = true;
    var.vt = VT_BSTR;
    hr = pPropertyBag->Read(L"Text", &var, 0);
   
if(!hr) {
        _bstrText = var.bstrVal;
        var.Clear();
    }
    var.vt = VT_UI4;
    hr = pPropertyBag->Read(L"Italic", &var, 0);
   
if(!hr) _fontDesc.fItalic = var.lVal;
    hr = pPropertyBag->Read(L"Underline", &var, 0);
   
if(!hr) _fontDesc.fUnderline = var.lVal;
    hr = pPropertyBag->Read(L"Strikethrough", &var, 0);
   
if(!hr) _fontDesc.fStrikethrough = var.lVal;
    hr = pPropertyBag->Read(L"Charset", &var, 0);
   
if(!hr) _fontDesc.sCharset = var.lVal;
    hr = pPropertyBag->Read(L"Weight", &var, 0);
   
if(!hr) _fontDesc.sWeight = var.lVal;
    var.vt = VT_CY;
    hr = pPropertyBag->Read(L"FontSize", &var, 0);
   
if(!hr) _fontDesc.cySize = var.cyVal;
    var.vt = VT_BSTR;
    hr = pPropertyBag->Read(L"FontName", &var, 0);
    if(!hr) {
        CoTaskMemFree(_fontDesc.lpstrName);
        _fontDesc.lpstrName = reinterpret_cast<OLECHAR*>(
            CoTaskMemAlloc(2 * (SysStringLen(var.bstrVal) + 1)));
        lstrcpyW(_fontDesc.lpstrName, var.bstrVal);
        var.Clear();
    }
    var.vt = VT_EMPTY;
    if(!_specialForeColor) _oleColorFore = GetSysColor(COLOR_CAPTIONTEXT);
   
if(!_specialFillColor) _oleColorFill = GetSysColor(COLOR_ACTIVECAPTION);
   
if(!_specialBorderColor) _oleColorBorder = GetSysColor(COLOR_WINDOWFRAME);
    OleTranslateColor(_oleColorFore, 0, &_rgbColorFore);
    OleTranslateColor(_oleColorFill, 0, &_rgbColorFill);
    OleTranslateColor(_oleColorBorder, 0, &_rgbColorBorder);

    return S_OK;
}

HRESULT _stdcall CMessageDisplay::GetSizeMax(ULARGE_INTEGER* pSize) {
    if(!pSize) return E_POINTER;
    pSize->QuadPart = 1092;
    return S_OK;
}

Through the IPersistStreamInit and IPersistPropertyBag interfaces, the client can compel the control to save or load its state from a stream or property bag.  The dirty flag data member is true if the control has changed since last being serialized (or created).  The host will call IsDirty before closing the control, or whenever it deems necessary.  If the methods returns S_OK, Save will be invoked on either IPersistStreamInit or IPersistPropertyBagGetSizeMax is a method of IPersistStreamInit.  If the container is working with non-growable streams, or simply wants to ensure that the necessary storage space will be available when needed, it can invoke this method to get a size for preallocation.  Our implementation returns 1092, which is just slighty more than the maximum size of all serializable data members (both strings are limited to 253 characters, which puts their total byte lengths to 512 each, including length prefix and null terminator).

Both persistence interfaces share the same InitNew implementation.  This method is called before the control is displayed, but after the client site pointer is set (because we specified the OLEMISC_SETCLIENTSITEFIRST status flag).  In the InitNew method the default control extent, colors, font, and text message are selected.  The special flags are also zeroed.  When the control is serialized to a stream, the appropriate Save method is called.  IStream::Write is passed the size and address of each of the control's data members.  This function is similar to the API call WriteFile.  We compose a BSTR from _fontDesc.lpstrName and save both it and _bstrText to the stream with the helper methods CComBSTR::WriteToStream.   Notice that the lpstrName element on disk is ignored - the string is serialized after the structure as a BSTR.  Load reads the values back in with the same order, and produces default colors for those colors with a zeroed special flag.  It then calls OleTranslateColor to acquire the corresponding COLORREF values.  Load doesn't need to read in the control's extent, because that's managed by the container.

A property bag is exactly what it sounds like: a collection of property names and values.  While property bags can only hold VARIANT-compatible types, they do have the advantage of being able to be stored nearly anywhere, including text files.  Figure 5 below shows the HTML source needed to embed a Message Display control into a page.  Note the <param> tags - these constitute the property bag data.

ctrlweb2.gif (38666 bytes)

Figure 5: Properties for IPersistPropertyBag in HTML

The IPersistPropertyBag::Save method takes three parameters: a property bag pointer, a clear-dirty flag, and a save-all-properties flag.  Message Control ignores the latter flag, because it's easier to just save every property, not just those that have changed since the last save.  As you can see, the special flags are not saved to the property bag - the mere existence of a color in the property bag connotes that the color was explicitly set.  Also, since we can't save structures to property bags (because structures aren't VARIANT-compatible types), we have to break down and save the FONTDESC by its individual components.  Fortunately, this makes it very easy for an HTML guy to manipulate the presentation of the control.

Now it's on to the primary interface and page 3!