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.
 |
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.
 |
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.
 |
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 IPersistStreamInit. IOleObject 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.
 |
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 IPersistPropertyBag. GetSizeMax 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.
 |
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!