sean's happy programming place

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

Sean Baxter

 

  Control Functionality

With the core interfaces IOleObject, IViewObject2, and IPersistStreamInit (plus the interface IPersistPropertyBag for web happiness) out of the way, we can finally get to Message Display's primary interface and some interesting control-related functionality.  Here's the complete IDL for our control's primary interface.

from captcom.idl

import "ocidl.idl";

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

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

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

    const int DISPID_SHAPE = 1;    

    [
       
uuid(F1C4329F-71C7-435b-ADE3-43A5A454EE7D),
        object,
        dual,
        oleautomation,
       
helpstring("IMessageDisplay interface")
    ]
    interface IMessageDisplay : IDispatch
    {
        [id(DISPID_FILLCOLOR), propput] HRESULT FillColor([in] OLE_COLOR color);
        [
id(DISPID_FILLCOLOR), propget]
            HRESULT FillColor([out,
retval] OLE_COLOR* color);

        [
id(DISPID_BORDERCOLOR), propput] HRESULT BorderColor([in] OLE_COLOR color);
        [
id(DISPID_BORDERCOLOR), propget]
            HRESULT BorderColor([
out, retval] OLE_COLOR* color);

        [
id(DISPID_BORDERWIDTH), propput] HRESULT BorderWidth([in] long width);
        [
id(DISPID_BORDERWIDTH), propget]
            HRESULT BorderWidth([
out, retval] long* width);

        [
id(DISPID_FORECOLOR), propput] HRESULT ForeColor([in] OLE_COLOR color);
        [
id(DISPID_FORECOLOR), propget]
            HRESULT ForeColor([
out, retval] OLE_COLOR* color);

        [
id(DISPID_FILLSTYLE), propput] HRESULT FillStyle([in] fillType fill);
        [
id(DISPID_FILLSTYLE), propget]
            HRESULT FillStyle([
out, retval] fillType* fill);

        [
id(DISPID_TEXT), propput] HRESULT Text([in] BSTR bstr);
        [
id(DISPID_TEXT), propget] HRESULT Text([out, retval] BSTR* bstr);

        [
id(DISPID_SHAPE), propput] HRESULT Shape([in] shapeType shape);
        [
id(DISPID_SHAPE), propget] HRESULT Shape([out, retval] shapeType* shape);

        [
id(DISPID_FONT), propput] HRESULT Font([in] IFontDisp* font);
        [
id(DISPID_FONT), propget] HRESULT Font([out, retval] IFontDisp** font);
   
        [
id(DISPID_ABOUTBOX)] HRESULT AboutBox();
    }
   
    [
       
uuid(A4752AC3-6003-4c38-86EC-8A5CCDFC671B),
       
version(1.0),
        control,
       
helpstring("Message Display coclass")
    ]
    coclass MessageDisplay
    {
        [default] interface IMessageDisplay;
    }
}

The first interesting thing here is the inclusion of the "olectl.h" header.  Defined within are dozens of standard DISPIDs relevant to OLE Controls.  The IMessageDisplay interface defines methods over the DISPID_FILLCOLOR, DISPID_BORDERCOLOR, DISPID_BORDERWIDTH, DISPID_FORECOLOR, DISPID_FILLSTYLE, DISPID_TEXT, DISPID_FONT, and DISPID_ABOUTBOX standard DISPIDs.   Using the standard DISPIDs is in no way mandatory, but certain Automation controllers may better support properties and methods defined over them.

The Font property takes an IFontDisp interface pointer as a parameter.  This dispinterface exposes essentially the same functionality as the easier-to-use vtable interface IFont.  Both interfaces are implemented by the standard and extremely popular OLE font object, so you can almost always successfully query an IFontDisp for an IFont.

IMessageDisplay defines only one method (actually they're all methods, but VB programmers and others acting in a high-level delusion would claim the other methods to be "properties"): AboutBox.  When called, this method simply DialogBoxs the appropriate resource template to present a modal About Box dialog to the user.  This method will also be called when the About Box verb is invoked, as we'll see later on.

aboutbox.gif (19338 bytes)

Figure 1: The About Box

The mutator functions of Message Display's primary interface perform bounds checking on the parameter data.  If the parameter is out of the range (for example, isn't a valid enumeration, or a negative border width) the function returns E_FAIL.  Alternatively the function could have set the control's data members to something "close" to what was requested (a negative value might be rounded up to zero).  In addition to the primary interface, IProvideClassInfo is implemented by Message Display.  This allows the client to grab the control's type information to learn the primary interface's definition (the client can also retrieve the type information through IDispatch::GetTypeInfo).

from mdisplay.cpp

class CMessageDisplay : public IMessageDisplay, public IProvideClassInfo, ... {
public:
    // IProvideClassInfo
    HRESULT _stdcall GetClassInfo(ITypeInfo** ppTI) {
        if(!ppTI)
return E_POINTER;
        _pClassInfo.CopyTo(ppTI);
        return S_OK;
    }

    // IMessageDisplay methods
    HRESULT _stdcall put_BorderColor(OLE_COLOR color) {
        _isDirty =
true;
        _specialBorderColor =
true;
        _oleColorBorder = color;
        OleTranslateColor(color, 0, &_rgbColorBorder);
        SendViewChange();
       
return S_OK;
    }
    HRESULT
_stdcall get_BorderColor(OLE_COLOR* color) {
       
if(!color) return E_POINTER;
       
return *color = _oleColorBorder, S_OK;
    }
   
    HRESULT
_stdcall put_FillColor(OLE_COLOR color) {
        _isDirty =
true;
        _specialFillColor =
true;
        _oleColorFill = color;
        OleTranslateColor(color, 0, &_rgbColorFill);
        SendViewChange();
       
return S_OK;
    }
    HRESULT
_stdcall get_FillColor(OLE_COLOR* color) {
       
if(!color) return E_POINTER;
       
return *color = _oleColorFill, S_OK;
    }

    HRESULT
_stdcall put_ForeColor(OLE_COLOR color) {
        _isDirty = true;
        _specialForeColor =
true;
        _oleColorFore = color;
        OleTranslateColor(color, 0, &_rgbColorFore);
        SendViewChange();
       
return S_OK;
    }
    HRESULT
_stdcall get_ForeColor(OLE_COLOR* color) {
       
if(!color) return E_POINTER;
       
return *color = _oleColorFore, S_OK;
    }

    HRESULT
_stdcall put_BorderWidth(long width) {
       
if(width > 10) return E_FAIL;
       
if(width < 0) return E_FAIL;
        _isDirty =
true;
        _borderWidth = width;
        SendViewChange();
       
return S_OK;
    }
    HRESULT
_stdcall get_BorderWidth(long* width) {
       
if(!width) return E_POINTER;
       
return *width = _borderWidth, S_OK;
    }

    HRESULT
_stdcall put_FillStyle(fillType fill) {
       
if(fill < fillSolid) return E_FAIL;
       
if(fill > fillVertical) return E_FAIL;
        _isDirty =
true;
        _fillType = fill;
        SendViewChange();
       
return S_OK;
    }
    HRESULT
_stdcall get_FillStyle(fillType* fill) {
       
if(!fill) return E_POINTER;
       
return *fill = _fillType, S_OK;
    }

    HRESULT
_stdcall put_Shape(shapeType shape) {
       
if(shape < shapeEllipse) return E_FAIL;
       
if(shape > shapeDiamond) return E_FAIL;
        _isDirty =
true;
        _shapeType = shape;
        SendViewChange();
       
return S_OK;
    }
    HRESULT
_stdcall get_Shape(shapeType* shape) {
       
if(!shape) return E_POINTER;
       
return *shape = _shapeType, S_OK;
    }

    HRESULT
_stdcall put_Text(BSTR bstr) {
       
if(SysStringLen(bstr) > 253) {
            _bstrText.Empty();
            _bstrText.m_str = SysAllocStringLen(bstr, 253);
        }
        else _bstrText = bstr;
        _isDirty =
true;
        SendViewChange();
       
return S_OK;
    }
    HRESULT
_stdcall get_Text(BSTR* bstr) {
       
if(!bstr) return E_POINTER;
       
return *bstr = _bstrText.Copy(), S_OK;
    }

    HRESULT
_stdcall put_Font(IFontDisp* pFontDisp) {
        CComPtr<IFont> pFont;
        HRESULT hr = pFontDisp->QueryInterface(&pFont);
       
if(hr) return E_FAIL;
        FONTDESC fd;
        fd.cbSizeofstruct = sizeof(fd);
        pFont->get_Weight(&fd.sWeight);
        pFont->get_Size(&fd.cySize);
        pFont->get_Underline(&fd.fUnderline);
        pFont->get_Strikethrough(&fd.fStrikethrough);
        pFont->get_Italic(&fd.fItalic);
        pFont->get_Charset(&fd.sCharset);
        CComBSTR fontName;
        pFont->get_Name(&fontName);
        fd.lpstrName = reinterpret_cast<OLECHAR*>(
            CoTaskMemAlloc(2 * (fontName.Length() + 1)));
        lstrcpyW(fd.lpstrName, fontName);
        if(_fontDesc.lpstrName) CoTaskMemFree(_fontDesc.lpstrName);
        _fontDesc = fd;
        _isDirty = true;

        SendViewChange();
       
return S_OK;
    }
    HRESULT
_stdcall get_Font(IFontDisp** pFontDisp) {
       
if(!pFontDisp) return E_POINTER;
       
return OleCreateFontIndirect(&_fontDesc, IID_IFontDisp,
            reinterpret_cast<void**>(pFontDisp));
    }

    HRESULT
_stdcall AboutBox() {
        HWND hwnd(0);
        if(_pClientSite) {
            CComPtr<IOleContainer> pContainer;
            CComPtr<IOleWindow> pOleWindow;
            HRESULT hr = _pClientSite->GetContainer(&pContainer);
           
if(!hr) pContainer->QueryInterface(&pOleWindow);
           
if(!hr) pOleWindow->GetWindow(&hwnd);
           
if(hr) hwnd = 0;
        }
        DialogBox(hInstance, MAKEINTRESOURCE(IDD_MDISPLAYABOUTBOX), hwnd, aboutBoxProc);
       
return S_OK;        
    }
};

int
_stdcall CMessageDisplay::aboutBoxProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM) {
    if((message == WM_COMMAND) && (LOWORD(wparam) == IDOK))
        return EndDialog(hwnd, 0), 1;
   
return 0;
}

Notice that AboutBox attempts to retrieve the container's HWND before invoking DialogBox.  It does this by acquiring the container interface from the site (the container contains sites, and each site interracts with a control) and querying it for IOleWindow.   This interface plays a key role in inplace activation, as it serves as a base for a number of other interfaces.

Each of the mutators above (i.e. the put_ methods) adheres to the same strategy: check to make sure the parameter is valid; save the parameter to the data member; set the dirty bit (so that the change will be serialized); and redraw the control with SendViewChange.

ctrlvb2.gif (26700 bytes)

Figure 2: The Message Display Property Page

The property page of Figure 2 is provided by our control, but is in fact, a complete COM object on its own.  Our control declares that it wants the Message Display "General" property page, the stock font property page (shown in Figure 3), and the stock color property page (of Figure 4) shown in the property page frame.  Introduced with Windows 95 were three stock property pages which prove very useful (actually, these three pages existed prior to Windows 95, but in extremely ugly incarnations).  The stock picture property page of Figure 5, in addition to the two already mentioned, is handed an IUnknown pointer through its IPropertyPage::SetObjects method (implemeted below).   From this interface is queried IDispatch, and GetTypeInfo is called to retrieve the primary interface's type information (alternatively, IProvideClassInfo::GetClassInfo could be used; the actual implementation of the stock property pages is hidden from the programmer).  The stock picture property page iterates through the method declarations and searches for IPicture* and IPictureDisp* parameters.  It lists the hits in the box labeled "properties" and provides the user with a mechanism for choosing pictures to send to the control.  The stock font property page works the same way, but it searches for IDispFont* or IFont* parameters, and the stock color property page seeks OLE_COLOR arguments.

ctrlvb4.gif (14805 bytes) ctrlvb5.gif (11764 bytes)

Figure 3: The Stock Font Property Page

Figure 4: The Stock Color Property Page

 

ctrlvb6.gif (45188 bytes)

Figure 5: The Stock Picture Property Page

The source below defines our property page COM object.   The property page implements the single interface IPropertyPage and is instantiated by the property frame using the module's DllGetClassObject export function.  Message Display's property page doesn't support aggregation, so it is instantiated using the CClassFactoryNoAgg factory.  This class is defined in "helper.h," but omitted in the source below for brevity.  The control implements ISpecifyPropertyPages's single method GetPages, which returns the class identifiers of the three property pages requested.  The CLSIDs of the stock pages can be found in the header "msstkppg.h." 

from mdisplay.cpp

#include <msstkppg.h>

class CMessageDisplay : public IMessageDisplay,
public ISpecifyPropertyPages, ... {
public:
    HRESULT _stdcall GetPages(CAUUID* pPages) {
        if(!pPages) return E_POINTER;
        pPages->pElems = reinterpret_cast<GUID*>(
            CoTaskMemAlloc(sizeof(GUID) * 3));
        pPages->pElems[0] = CLSID_MDisplayPropertyPage;
        pPages->pElems[1] = CLSID_StockFontPage;
        pPages->pElems[2] = CLSID_StockColorPage;
        pPages->cElems = 3;
        return S_OK;
    }
};

from exports.cpp

// {BD55A7EE-F883-4a51-B771-21F049C0B326}
static const GUID CLSID_MDisplayPropertyPage =
{ 0xbd55a7ee, 0xf883, 0x4a51, { 0xb7, 0x71, 0x21, 0xf0, 0x49, 0xc0, 0xb3, 0x26 } };

extern HRESULT GetClassObject_MDisplayPropPage(REFIID, void**);

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

from mdprop.cpp

#include <windows.h>
#include <atlbase.h>
#include <vector>
#include <commctrl.h>
#include "helpers.h"
#include "captcom_i.h"
#include "resource.h"

// {BD55A7EE-F883-4a51-B771-21F049C0B326}
static const GUID CLSID_MDisplayPropertyPage =
{ 0xbd55a7ee, 0xf883, 0x4a51, { 0xb7, 0x71, 0x21, 0xf0, 0x49, 0xc0, 0xb3, 0x26 } };

class CMDisplayPropertyPage : public IPropertyPage {
    ULONG _refCount;
    HWND _hwnd;
    CComPtr<IPropertyPageSite> _pPageSite;
    std::vector<CComPtr<IMessageDisplay> > _displayObjects;
    bool _isDirty;
   
bool _canGoDirty;
    int _currentShape, _currentFill;
    int _borderWidth;
    char _messageText[256];
    void Dirty() {
        if(_canGoDirty && (!_isDirty) && _pPageSite) {
            _isDirty = true;
            _pPageSite->OnStatusChange(PROPPAGESTATUS_DIRTY);
        }
    }
    static
int _stdcall dlgProc(HWND, UINT, WPARAM, LPARAM);
public:
    CMDisplayPropertyPage() : _refCount(0) {
        InterlockedIncrement(&moduleCount);
        _hwnd = 0;
    }
    ~CMDisplayPropertyPage() {
        InterlockedDecrement(&moduleCount);
    }

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

    // IPropertyPage methods
    HRESULT
_stdcall SetPageSite(IPropertyPageSite* pPageSite) {
       
return _pPageSite = pPageSite, S_OK;
    }

    HRESULT
_stdcall Deactivate() {
       
if(_hwnd) {
            DestroyWindow(_hwnd);
            _hwnd = 0;
           
return S_OK;
        }
       
return E_UNEXPECTED;
    }
   
    HRESULT
_stdcall Activate(HWND hwndParent, LPCRECT pRect, BOOL modal) {
        Deactivate();
        _canGoDirty =
false;
        _isDirty = false;
        _hwnd = CreateDialogParam(hInstance, MAKEINTRESOURCE(IDD_MDISPLAYPROP),
            hwndParent, dlgProc, reinterpret_cast<long>(this));
        MoveWindow(_hwnd, pRect->left, pRect->top, pRect->right - pRect->left,
            pRect->bottom - pRect->top, false);
        _canGoDirty = true;
       
return S_OK;
    }

    HRESULT
_stdcall GetPageInfo(PROPPAGEINFO* pPageInfo) {
        if(!pPageInfo) return E_POINTER;
        pPageInfo->pszTitle = reinterpret_cast<USHORT*>(CoTaskMemAlloc(2 * 8));
        lstrcpyW(pPageInfo->pszTitle, L"General");
        pPageInfo->size.cx = 259;
        pPageInfo->size.cy = 141;
        const wchar_t docString[] = L"Choose the Message Display properties.";
        pPageInfo->pszDocString = reinterpret_cast<USHORT*>(
            CoTaskMemAlloc(2 * (lstrlenW(docString) + 1)));
        lstrcpyW(pPageInfo->pszDocString, docString);
        pPageInfo->pszHelpFile = 0;
       
return S_OK;
    }

    HRESULT
_stdcall SetObjects(ULONG cObjects, IUnknown** ppUnk) {
        if(!ppUnk)
return E_POINTER;
        std::vector<CComPtr<IMessageDisplay> > displayObjects(cObjects);
        for(int i(0); i < cObjects; ++i) {
            CComPtr<IMessageDisplay> pMessageDisplay;
            if(!ppUnk[i]) return E_POINTER;
            HRESULT hr = ppUnk[i]->QueryInterface(IID_IMessageDisplay,
                reinterpret_cast<void**>(&pMessageDisplay));
            if(hr) return E_NOINTERFACE;
            displayObjects[i] = pMessageDisplay;
        }
        _displayObjects = displayObjects;
       
return S_OK;
    }

    HRESULT
_stdcall Show(UINT nCmdShow) {
       
if(!_hwnd) return E_UNEXPECTED;
       
return ShowWindow(_hwnd, nCmdShow), S_OK;
    }
   
    HRESULT
_stdcall Move(LPCRECT pRect) {
       
if(!_hwnd) return E_UNEXPECTED;
       
return MoveWindow(_hwnd, pRect->left, pRect->top,
            pRect->right - pRect->left, pRect->bottom - pRect->top, true), S_OK;
    }

    HRESULT
_stdcall IsPageDirty() {
       
return _isDirty ? S_OK : S_FALSE;
    }
   
    HRESULT
_stdcall Apply() {
        shapeType shape;
        fillType fill;
        switch(_currentShape) {
        case IDC_ELLIPSE: shape = shapeEllipse; break;
       
case IDC_RECTANGLE: shape = shapeRectangle; break;
       
case IDC_HOURGLASS: shape = shapeHourglass; break;
       
case IDC_DIAMOND: shape = shapeDiamond; break;
        }
       
switch(_currentFill) {
       
case IDC_SOLID: fill = fillSolid; break;
       
case IDC_FORWARDDIAG: fill = fillForwardDiagonal; break;
       
case IDC_BACKWARDDIAG: fill = fillBackwardDiagonal; break;
       
case IDC_CROSSHATCH: fill = fillCrosshatch; break;
       
case IDC_DIAGONALHATCH: fill = fillDiagonalCrosshatch; break;
       
case IDC_VERTICAL: fill = fillVertical; break;
       
case IDC_HORIZONTAL: fill = fillHorizontal; break;
        }
        char messageText[256];
        GetDlgItemText(_hwnd, IDC_MDISPLAYTEXT, messageText, 256);
        CComBSTR bstrText(messageText);
        for(int i(0); i < _displayObjects.size(); ++i) {
            _displayObjects[i]->put_Shape(shape);
            _displayObjects[i]->put_FillStyle(fill);
            _displayObjects[i]->put_BorderWidth(_borderWidth);
            _displayObjects[i]->put_Text(bstrText);
        }
        _isDirty = false;
       
return S_OK;
    }

    HRESULT
_stdcall Help(LPCOLESTR) { return E_NOTIMPL; }

    HRESULT
_stdcall TranslateAccelerator(MSG* pMsg) {
       
return IsDialogMessage(_hwnd, pMsg) ? S_OK : S_FALSE;
    }
};

int
_stdcall CMDisplayPropertyPage::dlgProc(HWND hwnd,
    UINT message, WPARAM wparam, LPARAM lparam) {
    CMDisplayPropertyPage* pPage;
    switch(message) {
   
case WM_STYLECHANGING:
        if(wparam == GWL_EXSTYLE) {
            STYLESTRUCT* pSS = reinterpret_cast<STYLESTRUCT*>(lparam);
            pSS->styleNew |= WS_EX_CONTROLPARENT;
            return 1;
        }
        return 0;
   
case WM_INITDIALOG: {
        pPage =
reinterpret_cast<CMDisplayPropertyPage*>(lparam);
        SetWindowLong(hwnd, GWL_USERDATA, lparam);
       
if(!pPage->_displayObjects.size()) return 1;
        shapeType shape;
        fillType fill;
        long borderWidth;
        CComBSTR bstrText;
        pPage->_displayObjects[0]->get_Shape(&shape);
        pPage->_displayObjects[0]->get_FillStyle(&fill);
        pPage->_displayObjects[0]->get_BorderWidth(&borderWidth);
        pPage->_displayObjects[0]->get_Text(&bstrText);

        switch(shape) {
       
case shapeEllipse:
            CheckDlgButton(hwnd, IDC_ELLIPSE, BST_CHECKED);
            pPage->_currentShape = IDC_ELLIPSE;
           
break;
       
case shapeRectangle:
            CheckDlgButton(hwnd, IDC_RECTANGLE, BST_CHECKED);
            pPage->_currentShape = IDC_RECTANGLE;
           
break;
       
case shapeHourglass:
            CheckDlgButton(hwnd, IDC_HOURGLASS, BST_CHECKED);
            pPage->_currentShape = IDC_HOURGLASS;
           
break;
       
case shapeDiamond:
            CheckDlgButton(hwnd, IDC_DIAMOND, BST_CHECKED);
            pPage->_currentShape = IDC_DIAMOND;
           
break;
        }

       
switch(fill) {
       
case fillSolid:
            CheckDlgButton(hwnd, IDC_SOLID, BST_CHECKED);
            pPage->_currentFill = IDC_SOLID;
           
break;
       
case fillBackwardDiagonal:
            CheckDlgButton(hwnd, IDC_BACKWARDDIAG, BST_CHECKED);
            pPage->_currentFill = IDC_BACKWARDDIAG;
           
break;
       
case fillForwardDiagonal:
            CheckDlgButton(hwnd, IDC_FORWARDDIAG, BST_CHECKED);
            pPage->_currentFill = IDC_FORWARDDIAG;
           
break;
       
case fillCrosshatch:
            CheckDlgButton(hwnd, IDC_CROSSHATCH, BST_CHECKED);
            pPage->_currentShape = IDC_CROSSHATCH;
           
break;
       
case fillDiagonalCrosshatch:
            CheckDlgButton(hwnd, IDC_DIAGONALHATCH, BST_CHECKED);
            pPage->_currentShape = IDC_DIAGONALHATCH;
           
break;
       
case fillHorizontal:
            CheckDlgButton(hwnd, IDC_HORIZONTAL, BST_CHECKED);
            pPage->_currentShape = IDC_HORIZONTAL;
           
break;
       
case fillVertical:
            CheckDlgButton(hwnd, IDC_VERTICAL, BST_CHECKED);
            pPage->_currentShape = IDC_VERTICAL;
           
break;
        }
       
        SendMessage(GetDlgItem(hwnd, IDC_BORDERWIDTHSPINNER), UDM_SETRANGE,
            0, MAKELONG(10, 0));
        SendMessage(GetDlgItem(hwnd, IDC_BORDERWIDTHSPINNER), UDM_SETPOS,
            0, MAKELONG(borderWidth, 0));
        char messageText[256];
        WideCharToMultiByte(CP_ACP, 0, bstrText, bstrText.Length() + 1,
            messageText, 256, 0, 0);
        SendMessage(GetDlgItem(hwnd, IDC_MDISPLAYTEXT), EM_LIMITTEXT, 253, 0);
        lstrcpy(pPage->_messageText, messageText);
        SetDlgItemText(hwnd, IDC_MDISPLAYTEXT, messageText);
        }
        return 1;
   
case WM_COMMAND:
        pPage = reinterpret_cast<CMDisplayPropertyPage*>(
            GetWindowLong(hwnd, GWL_USERDATA));
        if(!pPage) return 1;
        switch(LOWORD(wparam)) {
       
case IDC_ELLIPSE:
       
case IDC_RECTANGLE:
       
case IDC_HOURGLASS:
       
case IDC_DIAMOND:
            if(pPage->_currentShape == LOWORD(wparam)) return 1;
            pPage->_currentShape = LOWORD(wparam);
            pPage->Dirty();
            return 1;
       
case IDC_SOLID:
       
case IDC_FORWARDDIAG:
       
case IDC_BACKWARDDIAG:
       
case IDC_CROSSHATCH:
       
case IDC_DIAGONALHATCH:
       
case IDC_VERTICAL:
       
case IDC_HORIZONTAL:
           
if(pPage->_currentFill == LOWORD(wparam)) return 1;
            pPage->_currentFill = LOWORD(wparam);
            pPage->Dirty();
           
return 1;
        }
       
if(HIWORD(wparam) == EN_CHANGE) {
           
if(LOWORD(wparam) == IDC_MDISPLAYTEXT) {
               
char messageText[256];
                GetDlgItemText(hwnd, IDC_MDISPLAYTEXT, messageText, 256);
               
if(lstrcmp(messageText, pPage->_messageText)) {
                    lstrcpy(pPage->_messageText, messageText);
                    pPage->Dirty();
                }
               
return 1;
            }
           
if(LOWORD(wparam) == IDC_MDISPLAYBORDER) {
                int borderWidth = GetDlgItemInt(hwnd, IDC_MDISPLAYBORDER, 0, false);
                if(pPage->_borderWidth != borderWidth) {
                    pPage->_borderWidth = borderWidth;
                    pPage->Dirty();
                }
                return 1;       
            }        
        }
       
return 0;
    }
   
return 0;
}

HRESULT GetClassObject_MDisplayPropPage(REFIID riid, void** ppv) {
   
if(!ppv) return E_POINTER;
    CClassFactoryNoAgg<CMDisplayPropertyPage>* pFactory =
        new CClassFactoryNoAgg<CMDisplayPropertyPage>;
    pFactory->AddRef();
    HRESULT hr = pFactory->QueryInterface(riid, ppv);
    pFactory->Release();
   
return hr;
}

The first method invoked on our property page by its frame is likely GetPageInfo.  The method's parameter, a PROPPAGEINFO structure, is filled with information about the page, including its size in dialog units and the title that will appear on the page's tab.  MSDN Library comments on the size of property page: "If a control does implement property pages, then those pages should conform to one of the standard sizes: 250x62 or 250x110 dialog units (DLUs)."   While this may have been true at one point in time, it's certainly false today.   Our property page is 259x141 units, and fits in the standard frame just fine.   However, a page can't get much bigger than that.  I suggest that you size your page to whatever works in the standard frame.  If you have a lot of controls to display, there's no problem in supplying several custom property pages for your control.

Prior to requesting page activation, the property frame invokes IPropertyPage::SetObjects, and passes an array of IUnknown pointers.  Our page queries each pointer for our control's primary interface, and returns E_NOINTERFACE on failure.   The function caches the IMessageDisplay pointers in a vector before returning.  IPropertyPage::SetPageSite is also invoked early on.  This method is the propery page equivalent of IOleObject::SetClientSite.   Activate is the function called next.  Here the property page window is created.  Two flags of interest are cleared here: the dirty flag represents the state of the apply button; the _canGoDirty flag prevents the apply button from being enabled before the user has a chance to make any changes.  On WM_INITDIALOG, which executes entirely within the scope of IPropertyPage::Activate, controls are set to reflect the state of the control in question.  Each of these control changes sends a WM_COMMAND message, and the procedure interprets the message as a changed state and attempts to set the dirty bit and enable the apply button.  It's prevented, however, because _canGoDirty is cleared.  Only after WM_INITDIALOG completes it course and the CreateDialog returns can the apply button be enabled.  The window procedure attempts to enable the apply button by calling the class method Dirty, which delegates to the page site's OnStatusChange method.

The host is given complete control over hiding and displaying the page (in reaction to selecting a tab in the frame) by accessing the IPropertyPage::Show method.  Implementation of this method is easy: we just call the familiar API ShowWindow.   Move is just as simple, delegating to MoveWindow.   When the frame's apply button is clicked, and our property page is currently selected, Apply is invoked.  This method simply iterates through each control pointer and sets their states to match that of the property page.   Afterwards, the dirty flag is cleared.

The WM_STYLECHANGING case in our dialog's procedure warrants comment.  Naturally, we'd like to be able to tab through the controls on our property page as well as the controls on the form.  This requires that we set the WS_EX_CONTROLPARENT style on our dialog.  For some silly reason, the standard property frame will turn off this very property, and the tab key will cease to visit our controls!  On WM_STYLECHANGING we prevent the frame from mucking with us this way; the WS_EX_CONTROLPARENT style is turned back on.  Another keyboard-related issue is addressed with the IPropertyPage::TranslateAccelerator method, which calls IsDialogMessage.  See the MSDN Library docs on this API for more details.

ctrlvb3.gif (41734 bytes)

Figure 6: Verbs Displayed in a Popup Menu

This installment of The Journals of Captain COM is nearly complete, but we still haven't completed the first OLE Control interface we encountered: IOleObject!   Our control supports two verbs, both are listed in the container's popup menu, as shown in Figure 6.  In order for the client to retrieve the names of our verbs, we need to provide a verb enumerator, which is in fact, its own COM object (sans class factory).  ATL does not provide a verb enumerator.   Instead, it delegates EnumVerbs to OleRegEnumVerbs.   Therefore, if you are using ATL and want to let the client know of the verbs you support, you must add the verbs to the registry under your CLSID key.  This is a task that many ATL programmers fail to complete, and their controls suffer because of it.

Below is the definition of our verb enumerator.  It implements the single interface IEnumOLEVERB.  See the MSDN Library for details on this interface.

from helpers.h

class CEnumOLEVERB : public IEnumOLEVERB {
    ULONG _refCount;
   
int _nextVerb;
    const OLEVERB* _verbs;
    int _size;
public:
    CEnumOLEVERB(
const OLEVERB* verbs, int size) : _refCount(0),
        _nextVerb(0), _size(size), _verbs(verbs) {
        InterlockedIncrement(&moduleCount);
    }
    ~CEnumOLEVERB() { InterlockedDecrement(&moduleCount); }
    ULONG
_stdcall AddRef() { return ++_refCount; }
    ULONG
_stdcall Release() {
        ULONG ret(_refCount); if(!ret) delete this;
return ret;
    }
    HRESULT _stdcall QueryInterface(REFIID riid, void** ppv) {
        if(!ppv)
return E_POINTER;
       
if(riid == IID_IUnknown) *ppv = this;
        else
if(riid == IID_IEnumOLEVERB) *ppv = this;
       
else return *ppv = 0, E_NOINTERFACE;
        return AddRef(), S_OK;
    }

    static HRESULT CreateInstance(const OLEVERB* verbs, int size,
        IUnknown* pOuter, REFIID riid, void** ppv) {
        if(!ppv) return E_POINTER;
       
if(pOuter) return *ppv = 0, CLASS_E_NOAGGREGATION;
        CEnumOLEVERB* pEnum = new CEnumOLEVERB(verbs, size);
        pEnum->AddRef();
        HRESULT hr = pEnum->QueryInterface(riid, ppv);
        pEnum->Release();
       
return S_OK;
    }

   
static HRESULT CreateInstance(const OLEVERB* verbs, int size,
        IEnumOLEVERB** ppEnumOLEVERB) {
       
return CreateInstance(verbs, size, 0, IID_IEnumOLEVERB,
            reinterpret_cast<void**>(ppEnumOLEVERB));
    }

    HRESULT
_stdcall Next(ULONG celt, OLEVERB* rgelt, ULONG* pceltFetched) {
       
if(!rgelt) return E_POINTER;
        ULONG curPos(0);
        for(int nextVerb(_nextVerb); (nextVerb < _size) && (curPos < celt);
            ++nextVerb, ++curPos) {
            rgelt[curPos] = _verbs[nextVerb];
            rgelt[curPos].lpszVerbName = static_cast<wchar_t*>(
                CoTaskMemAlloc(lstrlenW(_verbs[nextVerb].lpszVerbName) * 2 + 2));
            lstrcpyW(rgelt[curPos].lpszVerbName, _verbs[nextVerb].lpszVerbName);
        }
        _nextVerb += curPos;
       
if(pceltFetched) *pceltFetched = curPos;
       
return (curPos == celt) ? S_OK : S_FALSE;
    }

    HRESULT
_stdcall Clone(IEnumOLEVERB** ppenum) {
        HRESULT hr = CreateInstance(_verbs, _size, 0, IID_IEnumOLEVERB,
            reinterpret_cast<void**>(ppenum));
       
if(hr) return hr;
        (*ppenum)->Skip(_nextVerb);
       
return S_OK;
    }

    HRESULT
_stdcall Reset() {
       
return _nextVerb = 0, S_OK;
    }

    HRESULT
_stdcall Skip(ULONG celt) {
        _nextVerb = min(_size, _nextVerb + static_cast<int>(celt));
       
return S_OK;
    }
};

from mdisplay.cpp

class CMessageDisplay : public IMessageDisplay, public IOleObject, ... {
public:
    // IOleObject implementation (abridged)
    HRESULT _stdcall DoVerb(long, MSG*, IOleClientSite*,
long, HWND, LPCRECT);
    HRESULT
_stdcall EnumVerbs(IEnumOLEVERB**);
};

OLEVERB verbs[] = {
    { OLEIVERB_PROPERTIES, L"Prope&rties", 0, OLEVERBATTRIB_ONCONTAINERMENU },
    { 1, L"&About Box", 0, OLEVERBATTRIB_NEVERDIRTIES | OLEVERBATTRIB_ONCONTAINERMENU }
};

HRESULT
_stdcall CMessageDisplay::EnumVerbs(IEnumOLEVERB** ppEnumVerbs) {
   
return CEnumOLEVERB::CreateInstance(verbs, 2, ppEnumVerbs);
}

HRESULT
_stdcall CMessageDisplay::DoVerb(long verb, MSG* pMsg, IOleClientSite*,
                                       
long, HWND hwnd, LPCRECT pRect) {
    switch(verb) {
    case OLEIVERB_PROPERTIES:
        {
            CComPtr<IOleControlSite> pControlSite;
            HRESULT hr = _pClientSite->QueryInterface(&pControlSite);
            if(hr)
return E_FAIL;
           
return pControlSite->ShowPropertyFrame();
        }
   
case 1:                 // requested the AboutBox
        AboutBox();
       
return S_OK;
    }
   
return E_NOTIMPL;
}

The two element OLEVERB array contains flags for our verbs.  OLEVERBATTRIB_ONCONTAINERMENU is self-explanatory.   OLEVERBATTRIB_NEVERDIRTIES, which is set for the About Box verb, indicates that executing the verb will not alter the state of the control and result in a need to save.   The DoVerb method switches over the verb identifier.  On OLEIVERB_PROPERTIES, a standard verb identifier (from "olectl.h"), we query the client site for IOleControlSite and invoke ShowPropertyFrame on the resulting pointer.  This method tells the site to create a property frame (COM provides a property frame.  See OleCreatePropertyFrame in MSDN Library) and invoke ISpecifyPropertyPages::GetPages to retrieve the list of property pages to instantiate.

Well, that wraps up our discussion of simple OLE Controls.  As you can tell, there's no magic involved here, just lots of code.  ActiveX isn't some fabulous new communication software recovered from alien spacecraft... it's just Microsoft's latest attempt to re-market COM.  So this I say: go forth and program controls.  But do so with skepticism of magic mechanisms.  By cutting through the hype and getting down to the nitty-gritty low level code, we can really master this misunderstood technology.

 

- Captain COM