Developing the
BullsEye Control Step by Step
Stock Properties
and Methods
Updating Stock
Properties and Methods in the IDL
Your IDL file describing the control's default
dispatch interface must contain an entry for each stock property
accessor method and all stock methods you support. The Add ATL
Control Wizard generates the method definitions for those stock
properties added using the wizard dialog boxes. Listing 11.6 shows the method
definitions for the BullsEye properties.
The Add ATL Control Wizard has no support for
stock methods. You must add any stock methods explicitly to the
default dispatch interface definition in your IDL file. Only three
stock methods (AboutBox, DoClick, and
Refresh) are defined, and BullsEye supports them
all. I've added the following lines to the IBullsEye
interface definition:
interface IBullsEye : IDispatch {
...
[id(DISPID_ABOUTBOX)] HRESULT AboutBox();
[id(DISPID_DOCLICK)] HRESULT DoClick();
[id(DISPID_REFRESH)] HRESULT Refresh();
};
Listing 11.1, shown earlier in
this chapter, contains the complete definition of the
IBullsEye interface with all these changes.
Implementing Stock
Properties and Methods Using CStockPropImpl
The CStockPropImpl class contains an
implementation of every stock property you can choose from the
Stock Properties page in the ATL Object Wizard. A control derives
from CStockPropImpl when it wants an implementation of any
of the stock properties. The declaration of the template class
looks like this:
template < class T, class InterfaceName,
const IID* piid = &_ATL_IIDOF(InterfaceName),
const GUID* plibid = &CAtlModule::m_libid,
WORD wMajor = 1, WORD wMinor = 0,
class tihclass = CComTypeInfoHolder>
class ATL_NO_VTABLE CStockPropImpl :
public IDispatchImpl< InterfaceName, piid, plibid,
wMajor, wMinor, tihclass >
The class T parameter is the name of
your control class. The InterfaceName parameter is the
name of the dual interface defining the stock property
propget and propput methods. The
CStockPropImpl class implements these accessor methods.
The piid parameter is a pointer to the IID for
the InterfaceName interface. The plibid parameter
is a pointer to the GUID of the type library that contains
a description of the InterfaceName interface.
The CBullsEye class implements its
stock properties using CStockPropImpl like this:
class ATL_NO_VTABLE CBullsEye :
public CStockPropImpl<CBullsEye, IBullsEye, &IID_IBullsEye,
&LIBID_ATLINTERNALSLib>,
...
The CStockPropImpl class contains an
implementation of the property accessor (get and put) methods for
all stock properties. These methods notify and synchronize with the
control's container when any stock property changes.
Most controls don't need support for all the
possible stock properties. However, the CStockPropImpl
base class contains supporting code for all stock properties. This
code needs a data member for each property. ATL expects your
deriving class, the control class, to provide the data members for
only the stock properties that your control supports. You must name
these data members the same variable name as used by the
CStockPropImpl class. Table 11.6 lists the appropriate name for each
supported stock property.
Table 11.6. Stock Properties Supported by
CStockPropImpl
Stock Property
|
Data Member
|
APPEARANCE
|
m_nAppearance
|
AUTOSIZE
|
m_bAutoSize
|
BACKCOLOR
|
m_clrBackColor
|
BACKSTYLE
|
m_nBackStyle
|
BORDERCOLOR
|
m_clrBorderColor
|
BORDERSTYLE
|
m_nBorderStyle
|
BORDERVISIBLE
|
m_bBorderVisible
|
BORDERWIDTH
|
m_nBorderWidth
|
CAPTION
|
m_bstrCaption
|
DRAWMODE
|
m_nDrawMode
|
DRAWSTYLE
|
m_nDrawStyle
|
DRAWWIDTH
|
m_nDrawWidth
|
ENABLED
|
m_bEnabled
|
FILLCOLOR
|
m_clrFillColor
|
FILLSTYLE
|
m_nFillStyle
|
FONT
|
m_pFont
|
FORECOLOR
|
m_clrForeColor
|
HWND
|
m_hWnd
|
MOUSEICON
|
m_pMouseIcon
|
MOUSEPOINTER
|
m_nMousePointer
|
PICTURE
|
m_pPicture
|
READYSTATE
|
m_nReadyState
|
TABSTOP
|
m_bTabStop
|
TEXT
|
m_bstrText
|
VALID
|
m_bValid
|
The CStockPropImpl class contains
property-accessor methods for all these
properties. In theory, that means you must provide member
variables for every single one of them in your class, to avoid
getting an "Undefined variable" compile error. Luckily for us, the
ATL authors took advantage of one of the new extensions to C++:
class CStockPropImpl {
...
HRESULT STDMETHODCALLTYPE get_Picture(IPictureDisp** ppPicture) {
__if_exists(T::m_pPicture) {
ATLTRACE(atlTraceControls,2,
_T("CStockPropImpl::get_Picture\n"));
ATLASSERT(ppPicture != NULL);
if (ppPicture == NULL)
return E_POINTER;
T* pT = (T*) this;
*ppPicture = pT->m_pPicture;
if (*ppPicture != NULL)
(*ppPicture)->AddRef();
}
return S_OK;
}
...
};
Notice the __if_exists block. You saw
this back in Chapter
4, "Objects in ATL"; here it's used to make sure the property
accessor logic is compiled in only if the underlying variable has
been declared in your control. This way, you don't have to worry
about size bloat to store stock properties you're not using. The
downside of this space optimization is this: When you add a member
variable to your control class to hold a stock property and you
misspell the member variable name, you receive no compilation
errors. The code in CStockPropImpl does nothing and
returns S_OK.
The
Add ATL Control Wizard generates the proper member variables in
your control's class when you add a stock property initially. For
example, here are the member variables generated for the stock
properties in the CBullsEye class:
class CBullsEye : ... {
...
OLE_COLOR m_clrBackColor;
LONG m_nBackStyle;
BOOL m_bEnabled;
OLE_COLOR m_clrForeColor;
};
CStockPropImpl implements explicit put
and get methods for the stock properties that are interface
pointers, including FONT, MOUSEICON, and
PICTURE. It also implements a get method for the
HWND stock property. For each other stock property,
CStockPropImpl invokes one of three macros that expand to
a standard put and get method for the property: These macros are
IMPLEMENT_STOCKPROP, IMPLEMENT_BOOL_STOCKPROP,
and IMPLEMENT_BSTR_STOCKPROP.
The IMPLEMENT_STOCKPROP (type, frame, pname,
dispid) macro's parameters are as follows:
-
type.
The data type for the stock property you want an implementation
for.
-
fname. The function name for the get
and put methods. The get method will be named get_fname,
and the put method will be named put_fname. For example,
when fname is Enabled, the method names are
put_Enabled and get_Enabled.
-
pname. Specifies the name of the
member variable that will hold the state of the stock property. For
example, if pname is bEnabled, the macro-created
get and put methods will reference m_bEnabled.
-
dispid. The dispatch ID for the
stock property.
The IMPLEMENT_BOOL_STOCKPROP(fname, pname,
dispid) macro implements a stock Boolean property's accessor
methods. It has the same attributes as listed for the
IMPLEMENT_STOCKPROP macro, except that the get method
tests the value of the data member containing the property and
returns VARIANT_TRUE or VARIANT_FALSE instead of
returning the value.
The IMPLEMENT_BSTR_STOCKPROP(fname, pname,
dispid) macro implements a stock text property's accessor
methods using a BSTR.
Let's look at the implementation of the
IMPLEMENT_STOCKPROP macro. The ATL code illustrates a
couple other issues that are worth noting and that apply to all
stock properties.
#define IMPLEMENT_STOCKPROP(type, fname, pname, dispid) \
HRESULT STDMETHODCALLTYPE put_##fname(type pname) { \
__if_exists(T::m_##pname) { \
ATLTRACE(ATL::atlTraceControls,2, \
_T("CStockPropImpl::put_%s\n"), #fname); \
T* pT = (T*) this; \
if (pT->m_nFreezeEvents == 0 && \
pT->FireOnRequestEdit(dispid) == S_FALSE) \
return S_FALSE; \
pT->m_##pname = pname; \
pT->m_bRequiresSave = TRUE; \
if (pT->m_nFreezeEvents == 0) \
pT->FireOnChanged(dispid); \
__if_exists(T::On##fname##Changed) { \
pT->On##fname##Changed(); \
} \
pT->FireViewChange(); \
pT->SendOnDataChange(NULL); \
} \
return S_OK; \
} \
HRESULT STDMETHODCALLTYPE get_##fname(type* p##pname) { \
__if_exists(T::m_##pname) { \
ATLTRACE(ATL::atlTraceControls,2, \
_T("CStockPropImpl::get_%s\n"), #fname); \
ATLASSERT(p##pname != NULL); \
if (p##pname == NULL) \
return E_POINTER; \
T* pT = (T*) this; \
*p##pname = pT->m_##pname; \
} \
return S_OK; \
}
First, notice that the put method fires an
OnRequestEdit and an OnChanged event notification
to the control's container before and after, respectively, changing
the value of a stock property. Second, the put method fires the
OnRequestEdit and OnChanged events after checking
a control's freeze event. When a control's freeze event count
(maintained in CComControlBase in the
m_nFreezeEvents member variable) is nonzero, a control
should hold off firing events or discard them completely. If this
rule isn't followed, some containers will break.
For example, the Test Container application
shipped with Visual C++ 6.0 crashes when a control fires change
notifications in its FinalConstruct method. A
control should be capable of calling FreezeEvents(TRUE) in
FinalConstruct to disable change notifications, initialize
its properties using the put methods, and then call
FreezeEvents (FALSE) to enable change notifications if
they were previously enabled
Occasionally, you'll decide to support
additional stock properties after creating the initial source code.
The wizard lacks support for adding features to your class after
the initial code generation, so you'll have to make the previously
described changes to your code manually.
Finally, often you'll want to do some work
beyond what the stock property put functions perform. For example,
the CBullsEye class needs to know whenever the background
color changes so that it can delete the old background brush and
schedule the rendering logic to create a new background brush. In
the middle of the put method generated by the
IMPLEMENT_STOCKPROP macro, there's this code:
#define IMPLEMENT_STOCKPROP(type, fname, pname, dispid) \
...
__if_exists(T::On##fname##Changed) { \
pT->On##fname##Changed(); \
} \
When you added the stock properties to the
control, the Add ATL Control Wizard also added methods called
On<propname>Changed. If there is an appropriately
named method in your class (OnBackColorChanged for the
BackColor property, for example), the stock property put
method calls that method when the property value changes. This is
useful when you need to do things beyond just storing the value.
For example, the CBullsEye control needs to know when the
background color changes, so it can delete the old background brush
and schedule the rendering logic to create a new background
brush.
Custom Properties
and Methods
Adding Custom
Properties and Methods to the IDL
In addition to stock properties, your control's
default dispatch interface must contain an entry for the property
get and put methods for each custom control property, as well as
all the stock and custom methods you support. The Add ATL Control
Wizard doesn't currently support stock methods, so you add them to
your class as if they were custom methods (which, in fact, they
are, except that you don't get to choose the function signatures or
the DISPID).
To add a custom property, you must edit the IDL
file and then add the corresponding methods to your C++ class. You
can use Class view to add the properties. Right-click the interface
and select Add, Add Property. This adds the appropriate definitions
to the IDL file. Unfortunately, unlike in previous versions of
Visual Studio, this step only updates the IDL file; you have to
manually add the property get/put methods to your C++ class. At
least the compiler gives you the helpful "Cannot instantiate
abstract class" error message if you forget or get the signature
wrong.
The BullsEye control supports the stock
methods and custom properties listed in Tables 11.2 and 11.3,
respectively. Listing 11.1 contains the complete
definition for the IBullsEye interface, but here's an
excerpt showing the definition of the CenterColor custom
property and the AboutBox stock method.
interface IBullsEye : IDispatch {
...
[propput, bindable, requestedit, id(DISPID_CENTERCOLOR)]
HRESULT CenterColor([in] OLE_COLOR newVal);
[propget, bindable, requestedit, id(DISPID_CENTERCOLOR)]
HRESULT CenterColor([out, retval] OLE_COLOR *pVal);
[id(DISPID_ABOUTBOX)] HRESULT AboutBox();
...
};
Note that Class view does not let you define a
symbol for the DISPID; it takes only integers in the dialog box.
It's usually a good idea to go back into the IDL afterward and
define a symbol, as we've already done in the IDL file for the
BullsEye control.
Implementing
Custom Properties and Stock and Custom Methods
You need to add a function prototype to your
control class for each method added to the IDL in the previous
step. For the previous custom property and stock method, I added
the following function prototypes to the CBullsEye
class:
class CBullsEye : ... {
...
STDMETHODIMP get_CenterColor(/*[out, retval]*/OLE_COLOR *pVal);
STDMETHODIMP put_CenterColor(/*[in]*/ OLE_COLOR newVal);
STDMETHODIMP AboutBox();
...
};
Declaring the
Function Prototypes
Note that you must declare each interface member
function as using the STDMETHODIMP calling convention. A
system header file defines this macro to be the appropriate calling
convention for COM interface methods on a given operating system. This calling
convention does change among various operating systems. Because you
are using the macro instead of explicitly writing its expansion,
your code is more portable across operating systems. On the Win32
operating systems, the macro expands to hrESULT
__stdcall.
The code that the ATL wizards generate
incorrectly uses the STDMETHOD macro. On Win32 operating
systems, this macro expands as virtual HRESULT __stdcall,
which just happens to work. It doesn't necessarily work on other
operating systems that support COM.
Basically, STDMETHOD should be used
only in the original definition of an interface; this is typically
the MIDL-generated header. (However, MIDL doesn't use the macro; it
simply generates its expansion instead.) When implementing an
inter-face method in a class (which we are doing in
CBullsEye), you should use the STDMETHODIMP
macro.
You must manually make a couple changes to the
method definitions. First, most properties should have the
bindable and requestedit attributes. This is
because the property put methods fire change notifications to a
container before and after changing a property. Therefore, you need
to change each method as shown in the following section.
Defining the
Functions
The function implementations are all quite
straightforward. The get_CenterColor method validates its
argument and returns the value of the CenterColor
property:
STDMETHODIMP CBullsEye::get_CenterColor(OLE_COLOR *pVal) {
if (NULL == pVal) return E_POINTER;
*pVal = m_clrCenterColor;
return S_OK;
}
The put_CenterColor method, like all
property change functions, is a bit more complicated:
STDMETHODIMP CBullsEye::put_CenterColor(OLE_COLOR newVal) {
if (m_clrCenterColor == newVal) return S_OK;
if (!m_nFreezeEvents)
if (FireOnRequestEdit(DISPID_CENTERCOLOR) == S_FALSE)
return S_FALSE;
m_clrCenterColor = newVal; // Save new color
::DeleteObject (m_centerBrush); // Clear old brush color
m_centerBrush = NULL;
m_bRequiresSave = TRUE; // Set dirty flag
if (!m_nFreezeEvents)
FireOnChanged(DISPID_CENTERCOLOR); // Notify container
FireViewChange(); // Request redraw
SendOnDataChange(NULL); // Notify advise sinks
return S_OK;
}
First, the method checks to see if the new value
is the same as the current value of the CenterColor
property. If so, the value isn't changing, so we exit quickly.
Then, as in the stock property code, it properly checks to see if
the container presently doesn't want to receive eventsthat is, if
the freeze events count is nonzero.
When the container has not frozen events, the
put_CenterColor method fires the OnRequestEdit
event to ask the container for permission to change the
CenterColor property. When the container refuses the
change, put_CenterColor returns S_FALSE.
When the container grants permission,
put_CenterColor updates the member variable in the control
that contains the color. It also changes some values that cause the
control's rendering code to use the new color the next time the
control redraws.
After the method changes the property, it sets
the control's dirty flag (m_b-RequiresSave) to remember
that the state of the control now needs to be saved. The various
persistence implementations check this flag when executing their
IsDirty method.
Next, the method fires the OnChanged
event to notify the container of the property change, assuming that
events are not frozen.
The CenterColor property affects the
visual rendering (view) of the control. When a control changes such
properties, the control should notify its container that the
control's appearance has changed by calling the
FireViewChange function. In response, eventually, the
container asks the control to redraw itself. After that, the method
notifies all advise sinks (which typically means the container)
that the state (data) of the control has changed by calling
SendOnDataChange.
Note that the state of a control changes
independently of the control's view. Some control property changes,
such as changes to CBullsEye's Beep property, have no
effect on the appearance of the control, so the put_Beep
method doesn't call FireViewChange.
At this point, the astute reader might have
noticed the similarity between the code implementing the
CenterColor property and the code generated by the
IMPLEMENT_STOCKPROP macro. If your property logic is
almost identical to that for the stock properties, you can save
yourself a lot of typing by using the macro instead:
class CBullsEye : ... {
...
void OnCenterColorChanged() {
::DeleteObject(m_centerBrush);
m_centerBrush = 0;
}
IMPLEMENT_STOCKPROP(OLE_COLOR, CenterColor,
clrCenterColor, DISPID_CENTERCOLOR);
...
private:
// Storage for the property value
OLE_COLOR m_clrCenterColor;
...
};
If you're using IMPLEMENT_STOCKPROP this
way, you can simply leave out the declarations for the get and put
methods; the macro adds them for you. To do any custom logic you
need in the property setter, you can use the OnXXXChanged
method (as I did in the OnCenterColorChanged method).
There's a fair bit of irony in this. In ATL 3,
IMPLEMENT_STOCKPROP was the documented way to implement
properties, and it didn't work. To get a fully functional property,
you had to write code like the implementation of
put_CenterColor shown previously. In ATL 8,
IMPLEMENT_STOCKPROP is now undocumented but works just
fine for defining custom properties.
Finally, our final custom method, the stock
AboutBox method, simply displays the About dialog box.
STDMETHODIMP CBullsEye::AboutBox() {
CAboutDlg dlg;
dlg.DoModal();
return S_OK;
}
Stock and Custom
Events
Adding Stock and
Custom Events to the IDL
Your IDL file describing the control's default
source interface must contain an entry for each stock and custom
event method you support. As described previously, for maximum
compatibility with all control containers, you should implement the
default source interface as a dispatch interface. No current
support exists in the IDE for adding event methods to a
dispinterface, despite what it might look like. For
example, in my current Class View window, there's an entry for
_IBullsEye-Events, as shown in Figure 11.10.
Look closely at that
window. Notice that it doesn't have the spoon interface icon;
instead, it's the normal class icon. If you right-click there, it
will allow you to add a method, but it's actually editing the
generated header file that you get when you run MIDL on your
.idl file. Any change you make will be overwritten the
next time you run MIDL. In previous versions of Visual Studio,
Class View was fairly smart about such things. In VS 2005, not so
much.
The BullsEye control needs to support
the two custom events described in Table 11.4. Here's the updated
IDL describing the event dispatch interface. All dispatch
interfaces must be defined within the library block of an IDL
file.
[ uuid(58D6D8CB-765D-4C59-A41F-BBA8C40F7A14),
helpstring("_IBullsEyeEvents Interface")
]
dispinterface _IBullsEyeEvents {
properties:
methods:
[id(DISPID_ONRINGHIT)]
void OnRingHit(short ringNum);
[id(DISPID_ONSCORECHANGED)]
void OnScoreChanged(long ringValue);
};
You'll also want to ensure that the IDL
correctly describes the BullsEye class itself. The
BullsEye coclass definition in the library block of the
IDL file should define the IBullsEye interface as the
default interface for the control and the _IBullsEyeEvents
dispatch interface as the default source interface.
[ uuid(E9312AF5-1C11-4BA4-A0C6-CB660E949B78),
control, helpstring("BullsEye Class")
]
coclass BullsEye {
[default] interface IBullsEye;
[default, source] dispinterface _IBullsEyeEvents;
};
This is what the wizard generates when you
choose to implement connection points, but it never hurts to
double-check.
Adding Connection
Point Support for the Events
Many containers use the connection points
protocol to hand the container's sink interface pointer to the
event source (the control). Chapter 9, "Connection Points," discusses
connection points in detail, so here I just summarize the steps
needed for a control.
To support connection point events, a control
must implement the IConnectionPointContainer interface as
well as one IConnectionPoint interface for each outgoing
(source) interface. Typically, most controls support two source
interfaces: the control's default source dispatch interface
(_IBullsEyeEvents for the BullsEye control) and the
property change notification source interface
(IPropertyNotifySink).
Implementing the
IConnectionPointContainer Interface
When you initially create the source code for a
control and select the Support Connection Points option, the ATL
Object Wizard adds the IConnectionPointContainerImpl base
class to your control class declaration. This is ATL's
implementation of the IConnectionPointContainer interface.
You need to add this base class explicitly if you decide to support
connection points after creating the initial source code.
class ATL_NO_VTABLE CBullsEye :
...
// Connection point container support
public IConnectionPointContainerImpl<CBullsEye>,
...
You also need one connection point for each
source interface that your control supports. ATL provides the
IConnectionPointImpl class, which is described in depth in Chapter 9, as an implementation of the
IConnectionPoint interface. Typically, you do not directly
use this class; instead, you derive a new class from
IConnectionPointImpl and customize the class by adding
various event-firing methods. Your control will inherit from this
derived class.
Supporting
Property Change Notifications
ATL provides a specialization of
IConnectionPointImpl, called
IPropertyNotifySinkCP, that implements a connection point
for the IPropertyNotifySink interface. The
IPropertyNotifySinkCP class also defines the type
definition __ATL_PROP_NOTIFY_EVENT_CLASS (note the double
leading underscore) as an alias for the
CFirePropNotifyEvent class.
template <class T, class CDV = CComDynamicUnkArray >
class ATL_NO_VTABLE IPropertyNotifySinkCP :
public IConnectionPointImpl<T, &IID_IPropertyNotifySink, CDV> {
public:
typedef CFirePropNotifyEvent __ATL_PROP_NOTIFY_EVENT_CLASS;
};
When you use the ATL Object Wizard to create a
full control that supports connection points, the wizard adds the
IPropertyNotifySinkCP base class to your control:
class ATL_NO_VTABLE CBullsEye :
...
public IPropertyNotifySinkCP<CBullsEye>,
...
Recall that a control's property put methods,
for both custom and stock properties, call the
FireOnRequestEdit and FireOnChanged functions to
send property-change notifications. These methods are defined in
the CComControl class like this:
template <class T, class WinBase = CWindowImpl< T > >
class ATL_NO_VTABLE CComControl :
public CComControlBase, public WinBase {
public:
HRESULT FireOnRequestEdit(DISPID dispID) {
T* pT = static_cast<T*>(this);
return T::__ATL_PROP_NOTIFY_EVENT_CLASS::FireOnRequestEdit
(pT->GetUnknown(), dispID);
}
HRESULT FireOnChanged(DISPID dispID) {
T* pT = static_cast<T*>(this);
return T::__ATL_PROP_NOTIFY_EVENT_CLASS::FireOnChanged
(pT->GetUnknown(), dispID);
}
...
};
Therefore, the call to
FireOnChanged in a property put method of a
CComControl-derived class actually is a call to the
FireOnChanged of the class
__ATL_PROP_NOTIFY_EVENT_CLASS (note the double leading
underscore) within your actual control class. When you derive your
control class from IPropertyNotifySinkCP, your control
class inherits a typedef for
_ATL_PROP_NOTIFY_EVENT_CLASS (note the single leading
underscore).
typedef CFirePropNotifyEvent _ATL_PROP_NOTIFY_EVENT_CLASS;
The two types come together in the property map
in your control class. The BEGIN_PROP_MAP macro defines
the type __ATL_PROP_NOTIFY_EVENT_CLASS (note the double
leading underscore) as equivalent to the type
_ATL_PROP_NOTIFY_EVENT_CLASS (note the single leading
underscore).
#define BEGIN_PROP_MAP(theClass) \
__if_not_exists(__ATL_PROP_NOTIFY_EVENT_CLASS) { \
typedef ATL::_ATL_PROP_NOTIFY_EVENT_CLASS
__ATL_PROP_NOTIFY_EVENT_CLASS; \
} \
...
The __if_not_exists block in the
BEGIN_PROP_MAP definition does the typedef only if the
__ATL_PROP_NOTIFY_EVENT_CLASS isn't defined. This gives
you the chance to override the event class by defining the typedef
yourself, if you want.
In the BullsEye control, this means
that when your property put method calls FireOnChanged,
this is actually a call to your CComControl::FireOnChanged
base class method.
-
FireOnChanged calls
CBullsEye::__ATL_PROP_NOTIFY_EVENT_CLASS::FireOnChanged.
-
The property map aliases
__ATL_PROP_NOTIFY_EVENT_CLASS (two leading underscores) to
_ATL_PROP_NOTIFY_EVENT_CLASS (one leading underscore).
-
IPropertyNotifySinkCP aliases
_ATL_PROP_NOTIFY_SINK_CLASS to
CFireProp-NotifyEvent.
-
Therefore, you actually
call the CBullsEye::CFirePropNotifyEvent::FireOnChanged
function.
The CFirePropNotifyEvent class contains
two static methods, FireOnRequest-Edit and
FireOnChanged, that use your control's own connection
point support to enumerate through all connections for the
IPropertyNotifySink interface and call the
OnRequestEdit and OnChanged methods,
respectively, of each connection.
class CFirePropNotifyEvent {
public:
// Ask any objects sinking the IPropertyNotifySink
// notification if it is ok to edit a specified property
static HRESULT FireOnRequestEdit(IUnknown* pUnk, DISPID dispID) {
CComQIPtr<IConnectionPointContainer,
&__uuidof(IConnectionPointContainer)> pCPC(pUnk);
if (!pCPC) return S_OK;
CComPtr<IConnectionPoint> pCP;
pCPC->FindConnectionPoint(__uuidof(IPropertyNotifySink),
&pCP);
if (!pCP) return S_OK;
CComPtr<IEnumConnections> pEnum;
if (FAILED(pCP->EnumConnections(&pEnum)))
return S_OK;
CONNECTDATA cd;
while (pEnum->Next(1, &cd, NULL) == S_OK) {
if (cd.pUnk) {
HRESULT hr = S_OK;
CComQIPtr<IPropertyNotifySink,
&__uuidof(IPropertyNotifySink)> pSink(cd.pUnk);
if (pSink != NULL)
hr = pSink->OnRequestEdit(dispID);
cd.pUnk->Release();
if (hr == S_FALSE) return S_FALSE;
}
}
return S_OK;
}
// Notify any objects sinking the IPropertyNotifySink
// notification that a property has changed
static HRESULT FireOnChanged(IUnknown* pUnk, DISPID dispID) {
CComQIPtr<IConnectionPointContainer,
&__uuidof(IConnectionPointContainer)> pCPC(pUnk);
if (!pCPC) return S_OK;
CComPtr<IConnectionPoint> pCP;
pCPC->FindConnectionPoint(__uuidof(IPropertyNotifySink),
&pCP);
if (!pCP) return S_OK;
CComPtr<IEnumConnections> pEnum;
if (FAILED(pCP->EnumConnections(&pEnum)))
return S_OK;
CONNECTDATA cd;
while (pEnum->Next(1, &cd, NULL) == S_OK) {
if (cd.pUnk) {
CComQIPtr<IPropertyNotifySink,
&__uuidof(IPropertyNotifySink)> pSink(cd.pUnk);
if (pSink != NULL)
pSink->OnChanged(dispID);
cd.pUnk->Release();
}
}
return S_OK;
}
};
This means that you must derive your control
class from IPropertyNotifySinkCP to get the
typedef that maps the FireOnRequestEdit and
FireOnChanged methods in CComControl to the
actual firing functions in CFirePropNotifyEvent.
When you don't derive from
IPropertyNotifySinkCP, you can still call the
FireOnRequestEdit and FireOnChanged methods in
CComControl. This is because ATL defines a
typedef for the symbol
_ATL_PROP_NOTIFY_EVENT_CLASS at global scope:
typedef CFakeFirePropNotifyEvent _ATL_PROP_NOTIFY_EVENT_CLASS;
When your control derives from
IPropertyNotifySinkCP, you inherit a definition for
_ATL_PROP_NOTIFY_EVENT_CLASS that hides the global
definition. When you don't derive from IPropertyNotifySinkCP,
the compiler uses the global definition just given. The
CFakeFirePropNotifyEvent class looks like this:
class CFakeFirePropNotifyEvent {
public:
static HRESULT FireOnRequestEdit(IUnknown* /*pUnk*/,
DISPID /*dispID*/)
{ return S_OK; }
static HRESULT FireOnChanged(IUnknown* /*pUnk*/,
DISPID /*dispID*/)
{ return S_OK; }
};
In the BullsEye control, this means
that when you don't derive from IPropertyNotifySinkCP and
your property put method calls FireOnChanged:
-
This is actually a call to your
CComControl::FireOnChanged base class method.
-
FireOnChanged calls
CBullsEye::__ATL_PROP_NOTIFY_EVENT_CLASS::FireOnChanged.
-
The property map aliases
__ATL_PROP_NOTIFY_EVENT_CLASS (two leading underscores) to
_ATL_PROP_NOTIFY_EVENT_CLASS (one leading underscore).
-
The global typedef aliases
_ATL_PROP_NOTOFY_SINK_CLASS to
CFakeFireProp-NotifyEvent.
-
Therefore, you actually call the
CBullsEye::CFakeFirePropNotifyEvent::FireOnChanged
function, which simply returns S_OK.
Supporting the
Control's Event Connection Point
A connection point (or more than one) is
essential to most ActiveX controls. Without an outgoing interface,
the host for your control has no way of knowing when it needs to
react to a change in the control. Chapter 9 discussed the details on
implementing connection points, so we won't repeat the details
here.
You'll want to use a specialization of
IConnectionPointImpl for each of your control's event
interfaces. Typically, a control implements only one event
interface because Visual Basic and scripting languages can hook up
to only the default event interface. This is the interface you
describe in your object's coclass definition with the [default,
source] attributes. However, a custom C++ client to your
control can connect to any of its source interfaces.
The specialized class derives from
IConnectionPointImpl and adds the appropriate event-firing
helper methods for your events. The easiest way to create a
specialized connection point class is to use the Implement
Connection Point menu item, described in Chapter 9.
Here's the specialized connection point class,
CProxy_IBullsEyeEvents, generated by the wizard for the
_IBullsEyeEvents dispatch interface:
#pragma once
template<class T>
class CProxy_IBullsEyeEvents :
public IConnectionPointImpl<T, &__uuidof(_IBullsEyeEvents)> {
public:
HRESULT Fire_OnRingHit(short ringNum) {
HRESULT hr = S_OK;
T * pThis = static_cast<T *>(this);
int cConnections = m_vec.GetSize();
for (int iConnection = 0;
iConnection < cConnections; iConnection++) {
pThis->Lock();
CComPtr<IUnknown> punkConnection =
m_vec.GetAt(iConnection);
pThis->Unlock();
IDispatch * pConnection =
static_cast<IDispatch *>(punkConnection.p);
if (pConnection) {
CComVariant avarParams[1];
avarParams[0] = ringNum;
avarParams[0].vt = VT_I2;
DISPPARAMS params = { avarParams, NULL, 1, 0 };
hr = pConnection->Invoke(DISPID_ONRINGHIT, IID_NULL,
LOCALE_USER_DEFAULT, DISPATCH_METHOD, ¶ms,
NULL, NULL, NULL);
}
}
return hr;
}
HRESULT Fire_OnScoreChanged(long ringValue)
{
// Code similar to above deleted for clarity
}
};
You use this class by
adding it to the base class list of the control. Therefore,
BullsEye now has two connection points in its base class
list:
class ATL_NO_VTABLE CBullsEye :
// events and property change notifications
public CProxy_IBullsEyeEvents<CBullsEye>,
public IPropertyNotifySinkCP<CBullsEye>,
...
Updating the
Connection Map
Finally, the
IConnectionPointContainerImpl class needs a table that
associates source interface IIDs with the base class
IConnectionPointImpl specialization that implements the
connection point. You define this table in your control class using
the BEGIN_CONNECTION_POINT_MAP,
CONNECTION_POINT_ENTRY, and
END_CON-NECTION_POINT_MAP macros, described in Chapter 9.
Here's the table for the CBullsEye
class:
BEGIN_CONNECTION_POINT_MAP(CBullsEye)
CONNECTION_POINT_ENTRY(DIID__IBullsEyeEvents)
CONNECTION_POINT_ENTRY(IID_IPropertyNotifySink)
END_CONNECTION_POINT_MAP()
Supporting
IProvideClassInfo2
Many containers, such as Visual Basic and
Internet Explorer, use a control's IProvideClassInfo2
interface to determine the control's event interface. When a
control doesn't support IProvideClassInfo2, these
containers assume that the control doesn't source events and never
establish a connection point to your control. Other containers,
such as test container, don't use a control's
IProvideClassInfo2 interface and browse a control's type
information to determine the default source interface.
ATL provides an implementation of this interface
in IProvideClassInfo2Impl. To use it, derive your control
class from IProvideClassInfo2Impl. The
IProvideClassInfo2 interface itself derives from the
IProvideClassInfo interface, so when you update your
control's interface map, you need to provide entries for both
interfaces.
class ATL_NO_VTABLE CBullsEye :
public IProvideClassInfo2Impl<&CLSID_BullsEye,
&DIID__IBullsEyeEvents,
&LIBID_ATLInternalsLib>,
...
{
...
BEGIN_COM_MAP(CBullsEye)
...
// Support for connection points
COM_INTERFACE_ENTRY(IConnectionPointContainer)
COM_INTERFACE_ENTRY(IProvideClassInfo2)
COM_INTERFACE_ENTRY(IProvideClassInfo)
END_COM_MAP()
...
};
On-Demand
Rendering of Your Control's View
The BullsEye
control now has stock properties and custom properties, correctly
responds to QueryInterface calls, and implements an
outgoing connection point. There's just one thing: Aren't controls
supposed to be visual? It's time to talk about how ActiveX controls
actually draw their UI.
A control must be capable of rendering its image
when requested by its container. A control receives a rendering
request in basically three different situations:
-
The control has a window that receives a
WM_PAINT message. A control handles this request in
CComControlBase::OnPaint.
-
The control is windowless, and the container's
window receives a WM_PAINT message encompassing the area
that the control occupies. A control handles this request in
CComControlBase::IViewObject_Draw.
-
The container requests that the control render
its image into a metafile. A control handles this request in
CComControlBase::IDataObject_GetData.
Although all three types of rendering requests
arrive at the control via different mechanisms, the ATL control
implementation classes eventually forward the requests to a
control's OnDrawAdvanced method.
virtual HRESULT OnDrawAdvanced( ATL_DRAWINFO& di );
ATL bundles all parameters to the rendering
requests into an ATL_DRAWINFO structure. You need to use
the information in this structure when drawing your control. Most
of the fields are simply copies of similar parameters to the
IView-Object::Draw method:
struct ATL_DRAWINFO {
UINT cbSize; // Set to sizeof(ATL_DRAWINFO)
DWORD dwDrawAspect; // Drawing aspect typically
// DVASPECT_CONTENT
LONG lindex; // Commonly -1, which specifies
// all of the data
DVTARGETDEVICE* ptd; // Render to this target device
HDC hicTargetDev; // Information context for target device
HDC hdcDraw; // Draw on this device context
LPCRECTL prcBounds; // Rectangle in which to draw
LPCRECTL prcWBounds; // WindowOrg and Ext if metafile
BOOL bOptimize; // Can control use drawing optimizations?
BOOL bZoomed; // Object extent differs from
// drawing rectangle?
BOOL bRectInHimetric;// Rectangle in HiMetric?
SIZEL ZoomNum; // Rectangle size: ZoomX =
// ZoomNum.cx/ZoomDen.cx
SIZEL ZoomDen; // Extent size: ZoomY =
// ZoomNum.cy/ZoomDen.cy
};
ATL provides the following default
implementation of the OnDrawAdvanced method in
CComControl:
inline HRESULT
CComControlBase::OnDrawAdvanced(ATL_DRAWINFO& di) {
BOOL bDeleteDC = FALSE;
if (di.hicTargetDev == NULL) {
di.hicTargetDev = AtlCreateTargetDC(di.hdcDraw, di.ptd);
bDeleteDC = (di.hicTargetDev != di.hdcDraw);
}
RECTL rectBoundsDP = *di.prcBounds;
BOOL bMetafile =
GetDeviceCaps(di.hdcDraw, TECHNOLOGY) == DT_METAFILE;
if (!bMetafile) {
::LPtoDP(di.hdcDraw, (LPPOINT)&rectBoundsDP, 2);
SaveDC(di.hdcDraw);
SetMapMode(di.hdcDraw, MM_TEXT);
SetWindowOrgEx(di.hdcDraw, 0, 0, NULL);
SetViewportOrgEx(di.hdcDraw, 0, 0, NULL);
di.bOptimize = TRUE; //since we save the DC we can do this
}
di.prcBounds = &rectBoundsDP;
GetZoomInfo(di);
HRESULT hRes = OnDraw(di);
if (bDeleteDC)
::DeleteDC(di.hicTargetDev);
if (!bMetafile)
RestoreDC(di.hdcDraw, -1);
return hRes;
}
CComControl::OnDrawAdvanced prepares a
normalized device context for drawing and then calls your control
class's OnDraw method. A normalized device context is so
called because the device context has (some of) the normal defaults
for a device contextspecifically, the mapping mode is
MM_TEXT, the window origin is 0,0, and the viewport origin
is 0,0. Override the OnDrawAdvanced method when you want
to use the device context passed by the container as is, without
normalizing it. For example, if you don't want these defaults
values, you should override OnDrawAdvanced instead of
OnDraw, for greater efficiency.
When a container asks a control to draw into a
device context, the container specifies whether the control can use
optimized drawing techniques. When the bOptimize flag in
the ATL_DRAWINFO structure is TRUE, this means
that the DC will have some of its settings
automatically restored after the call to OnDraw returns.
Thus, in your drawing code, you don't have to worry about setting
the DC to its original settings (brushes, pens, and so on). It's
not really much of an optimization, but every little bit helps.
-
When IDataObject_GetData calls
OnDrawAdvanced to retrieve a rendering of the control in a
metafile, IDataObject_GetData saves the device context
state, calls OnDrawAdvanced, and then restores the device
context state. Therefore, IDataObject_GetData sets the
bOptimize flag to trUE.
-
When OnPaint calls
OnDrawAdvanced to have the control render to its window,
the bOptimize flag is set to FALSE.
-
When IViewObject_Draw calls
OnDrawAdvanced to have the control render to the
container's window, the bOptimize flag is set to
TRUE only if the container supports optimized drawing.
When you override OnDrawAdvanced, you
should always check the value of the bOptimize flag and
restore the state of the device context as necessary.
For a nonmetafile device context device,
OnDrawAdvanced saves the state of the entire device
context and restores it after calling your control's
OnDraw method. Because of this, the default
OnDrawAdvanced method sets the bOptimize flag to
TRUE.
When you override OnDraw in ATL's current
implementation, the bOptimize flag is always
TRUE. This doesn't mean that you shouldn't check the flag.
It means that you should always go to the effort of supporting
optimized drawing when overriding OnDraw because such
support is always used.
Listing
11.8 gives excerpts of the drawing code for the
BullsEye control. A few features in this code are
noteworthy:
-
BullsEye supports transparent drawing
via the BackStyle stock property. When BackStyle
is 1 (opaque), the control uses the background color to
fill the area around the bull's eye. When BackStyle is
0 (transparent), the control doesn't draw to the area
outside the circle of the bull's eye. This leaves the area around
the circle transparent, and the underlying window contents show
through.
-
BullsEye draws differently into a
metafile device context than into another device context. You
cannot do some operations when drawing to a metafile. Therefore,
BullsEye sets up the device context slightly differently
in these two cases.
-
BullsEye supports optimized
drawing.
The OnDraw method handles the interface
to ATL: receiving the ATL_DRAWINFO structure, and figuring
out what mapping mode and coordinate system to use, depending on
whether the DC is for a metafile. The DrawBullsEye method,
on the other hand, actually does the drawing.
Listing 11.8.
BullsEye OnDraw and DrawBullsEye Methods
#define ASSERT_SUCCESS( b ) ATLASSERT( ( b ) != 0 )
#define VERIFY_SUCCESS( c ) { BOOL bSuccess = ( c ); \
ATLASSERT( bSuccess != 0 ); }
// Drawing code
static const int LOGWIDTH = 1000;
HRESULT CBullsEye::OnDraw(ATL_DRAWINFO &di) {
CRect rc = *( ( RECT * )di.prcBounds );
HDC hdc = di.hdcDraw;
// Create brushes as needed
...
// First, fill in background color in invalid area when
// BackStyle is Opaque
if (m_nBackStyle == 1 /* Opaque*/ ) {
VERIFY_SUCCESS( ::FillRect( hdc, &rc, m_backBrush ) );
}
int nPrevMapMode;
POINT ptWOOrig, ptVOOrig;
SIZE szWEOrig, szVEOrig;
BOOL bMetafile =
GetDeviceCaps( di.hdcDraw, TECHNOLOGY ) == DT_METAFILE;
if( !bMetafile ) {
// OnDrawAdvanced normalized the device context
// We are now using MM_TEXT and the coordinates
// are in device coordinates.
// Establish convenient coordinate system for on screen
...
} else {
// We will be played back in ANISOTROPIC mapping mode
// Set up bounding rectangle and coordinate system
// for metafile
...
}
// Draw the BullsEye
DrawBullsEye( di );
// Note on optimized drawing:
// Even when using optimized drawing, a control cannot
// leave a changed mapping mode, coordinate transformation
// value, selected bitmap, clip region, or metafile in the
// device context.
if (!bMetafile) {
::SetMapMode (hdc, nPrevMapMode);
::SetViewportOrgEx (hdc, ptVOOrig.x, ptVOOrig.y, NULL);
::SetViewportExtEx (hdc, szVEOrig.cx, szVEOrig.cy, NULL);
}
::SetWindowOrgEx (hdc, ptWOOrig.x, ptWOOrig.y, NULL);
::SetWindowExtEx (hdc, szWEOrig.cx, szWEOrig.cy, NULL);
return S_OK;
}
void CBullsEye::DrawBullsEye(ATL_DRAWINFO &di) {
HDC hdc = di.hdcDraw;
// Create brushes as needed
...
// Compute the width of a ring
short sRingCount;
HRESULT hr = get_RingCount( &sRingCount );
ATLASSERT( SUCCEEDED( hr ) );
int ringWidth = LOGWIDTH / (sRingCount * 2 - 1);
// Draw the border between rings using the border pen
HPEN hOldPen = (HPEN)SelectObject( hdc, m_borderPen );
HBRUSH hOldBrush = 0;
for( short i = sRingCount - 1; i >= 0; --i ) {
// Draw the current ring
...
// Set the correct ring color
HBRUSH hBrush = ( HBRUSH )::SelectObject( hdc, ringBrush );
// First time through, save the original brush
if( hOldBrush == 0 ) {
hOldBrush = hBrush;
}
...
}
// When optimized drawing not in effect,
// restore the original pen and brush
if( !di.bOptimize ) {
::SelectObject( hdc, hOldPen );
::SelectObject( hdc, hOldBrush );
}
}
|
Property
Persistence
A control typically needs to save its state upon
its container's request. Various containers prefer different
persistence techniques. For example, Internet Explorer and Visual
Basic prefer to save a control's state using a property bag, which
is an association (or dictionary) of text name/value pairs. The
dialog editor in Visual C++ prefers to save a control's state in
binary form using a stream. Containers of embedded objects save the
objects in structured storage.
ATL provides three persistence interface
implementations, as discussed in Chapter 7, "Persistence in ATL":
IPersistStreamInitImpl
|
Saves properties in binary form into a
stream
|
IPersistStorageImpl
|
Saves properties in binary form in structured
storage
|
IPersistPropertyBagImpl
|
Saves properties as name/value pairs
|
Most controls should derive from all three
persistence-implementation classes so that they support the widest
variety of containers. The BullsEye control does this:
class ATL_NO_VTABLE CBullsEye :
...
// Persistence
public IPersistStreamInitImpl<CBullsEye>,
public IPersistStorageImpl<CBullsEye>,
public IPersistPropertyBagImpl<CBullsEye>,
};
As always, you need to add entries to the COM
map for each supported interface. The persistence interfaces all
derive from IPersist, so you need to add it to the COM map
as well.
BEGIN_COM_MAP(CBullsEye)
...
// Persistence
COM_INTERFACE_ENTRY(IPersistStreamInit)
COM_INTERFACE_ENTRY2(IPersist, IPersistStreamInit)
COM_INTERFACE_ENTRY(IPersistStorage)
COM_INTERFACE_ENTRY(IPersistPropertyBag)
END_COM_MAP()
All three persistence implementations save the
properties listed in the control's property map. You define the
property map using the BEGIN_PROP_MAP and
END_PROP_MAP macros. Here's the CBullsEye class's
property map:
BEGIN_PROP_MAP(CBullsEye)
PROP_DATA_ENTRY("_cx", m_sizeExtent.cx, VT_UI4)
PROP_DATA_ENTRY("_cy", m_sizeExtent.cy, VT_UI4)
PROP_ENTRY("BackStyle", DISPID_BACKSTYLE,
CLSID_BullsEyePropPage)
PROP_ENTRY("Beep", DISPID_BEEP,
CLSID_BullsEyePropPage)
PROP_ENTRY("Enabled", DISPID_ENABLED,
CLSID_BullsEyePropPage)
PROP_ENTRY("RingCount", DISPID_RINGCOUNT,
CLSID_BullsEyePropPage)
PROP_ENTRY("AlternateColor", DISPID_ALTERNATECOLOR,
CLSID_StockColorPage)
PROP_ENTRY("BackColor", DISPID_BACKCOLOR,
CLSID_StockColorPage)
PROP_ENTRY("CenterColor", DISPID_CENTERCOLOR,
CLSID_StockColorPage)
PROP_ENTRY("ForeColor", DISPID_FORECOLOR,
CLSID_StockColorPage)
END_PROP_MAP()
The ATL Object Wizard adds the first two
PROP_DATA_ENTRY macros to a control's property map when it
generates the initial source code. These entries cause ATL to save
and restore the extent of the control. When you describe a
persistent property using a PROP_DATA_ENTRY macro, ATL
directly accesses the member variable in the control.
You must explicitly add entries for any
additional properties that the control needs to persist. The
BullsEye control lists all but one of its persistent
properties using the PROP_ENTRY macro. This macro causes
ATL to save and restore the specified property by accessing the
property using the default dispatch interface for the control.
Alternatively, you can use the PROP_ENTRY_EX macro to
specify the IID, other than IID_IDispatch, of the dispatch
interface that supports the property. You use the
PROP_ENTRY_EX macro when your control supports multiple
dispatch interfaces with various properties accessible via
different dispatch interfaces. Supporting multiple dispatch
interfaces is generally not a good thing to do.
One caution: Don't add a PROP_ENTRY
macro that has a property name that contains an embedded space
character. Some relatively popular containers, such as Visual
Basic, provide an implementation of IPropertyBag::Write
that cannot handle names with embedded spaces.
For properties described with the
PROP_ENTRY and PROP_ENTRY_EX macros, the various
persistence implementations query for the appropriate interface and
call IDispatch::Invoke, specifying the
DISPID from the property map entry to get and put the
property.
The ATL property map can handle most property
types without custom code, but sometimes you need to customize how
your properties are persisted. For example, the BullsEye
control has one additional propertythe RingValues indexed
(array) propertyand the ATL property map doesn't support indexed
properties. To persist such properties, you must explicitly
implement the IPersistStreamInit_Save,
IPersistStreamInit_Load,
IPersistPropertyBag_Save, and
IPersistPropertyBag_Load methods normally provided by the
ATL persistence-implementation classes and read/write the indexed
property. Here's an example from the BullsEye control that
calls the ATL implementation of IPersistPropertyBag_Load
and then saves the indexed property:
HRESULT CBullsEye::IPersistPropertyBag_Load(
LPPROPERTYBAG pPropBag,
LPERRORLOG pErrorLog,
ATL_PROPMAP_ENTRY* pMap) {
if (NULL == pPropBag) return E_POINTER;
// Load the properties described in the PROP_MAP
HRESULT hr =
IPersistPropertyBagImpl<CBullsEye>::IPersistPropertyBag_Load(
pPropBag, pErrorLog, pMap);
if (SUCCEEDED(hr))
m_bRequiresSave = FALSE;
if (FAILED (hr)) return hr;
// Load the indexed property - RingValues
// Get the number of rings
short sRingCount;
get_RingCount (&sRingCount);
// For each ring, read its value
for (short nIndex = 1; nIndex <= sRingCount; nIndex++) {
// Create the base property name
CComBSTR bstrName = OLESTR("RingValue");
// Create ring number as a string
CComVariant vRingNumber = nIndex;
hr = vRingNumber.ChangeType (VT_BSTR);
ATLASSERT (SUCCEEDED (hr));
// Concatenate the two strings to form property name
bstrName += vRingNumber.bstrVal;
// Read ring value from the property bag
CComVariant vValue = 0L;
hr = pPropBag->Read(bstrName, &vValue, pErrorLog);
ATLASSERT (SUCCEEDED (hr));
ATLASSERT (VT_I4 == vValue.vt);
if (FAILED (hr)) {
hr = E_UNEXPECTED;
break;
}
// Set the ring value
put_RingValue (nIndex, vValue.lVal);
}
if (SUCCEEDED(hr)) m_bRequiresSave = TRUE;
return hr;
}
IQuickActivate
Some control containers ask a control for its
IQuickActivate interface and use it to quickly exchange a
number of interfaces between the container and the control during
the control's activation processthus the interface name.
ATL provides an implementation of this
interface, IQuickActivateImpl, which, by default, full,
composite, and HTML controls use. However, a control container also
gives a control a few ambient properties during this quick
activation process that the ATL implementation doesn't save. If
your control needs these ambient propertiesBackColor,
ForeColor, and Appearanceit's more efficient to
save them during the quick activation process than to incur three
more round-trips to the container to fetch them later.
The tricky aspect is that a container might not
quick-activate your control. Therefore, the control should save the
ambient properties when quick-activated or retrieve the ambient
properties when the container provides the control's client site,
but not both. Luckily, it's easy to add this functionality to your
control.
When a container quick-activates your control,
it calls the control's IQuickActivate::QuickActivate
method, which is present in your control's
IQuickActivateImpl base class. This method delegates the
call to your control class's IQuickActivate_QuickActivate
method. By default, a control class doesn't provide the method, so
the call invokes a default implementation supplied by
CComControlBase. You simply need to provide an
implementation of the IQuickActivate_QuickActivate method
that saves the ambient properties and forwards the call to the
method in CComControlBase, like this:
HRESULT CBullsEye::IQuickActivate_QuickActivate(
QACONTAINER *pQACont,
QACONTROL *pQACtrl) {
m_clrForeColor = pQACont->colorFore;
m_clrBackColor = pQACont->colorBack;
m_nAppearance = (short) pQACont->dwAppearance;
m_bAmbientsFetched = true;
HRESULT hr = CComControlBase::IQuickActivate_QuickActivate(
pQACont, pQACtrl);
return hr;
}
Note that the function also
sets a flag, m_bAmbientsFetched, to remember that it has
already obtained the ambient properties and, therefore, shouldn't
fetch them again when the control receives its client site.
BullsEye initializes the flag to false in its
constructor and checks the flag in its
IOleObject_SetClientSite method like this:
HRESULT CBullsEye::IOleObject_SetClientSite(
IOleClientSite *pClientSite) {
HRESULT hr =
CComControlBase::IOleObject_SetClientSite(pClientSite);
if (!m_bAmbientsFetched) {
GetAmbientBackColor (m_clrBackColor);
GetAmbientForeColor (m_clrForeColor);
GetAmbientAppearance (m_nAppearance);
}
return hr;
}
Component
Categories
Frequently, you'll want your control to belong
to one or more component categories. For example, the
BullsEye control belongs to the ATL Internals Sample
Components category. Additionally, BullsEye is a member of
the Safe for Initialization and Safe for Scripting categories, so
scripts on an HTML page can initialize and access it without
security warnings. Adding the proper entries to the control's
category map registers the class as a member of the specified
component categories. BullsEye uses this category map:
BEGIN_CATEGORY_MAP(CBullsEye)
IMPLEMENTED_CATEGORY(CATID_ATLINTERNALS_SAMPLES)
IMPLEMENTED_CATEGORY(CATID_SafeForScripting)
IMPLEMENTED_CATEGORY(CATID_SafeForInitializing)
END_CATEGORY_MAP()
Registering a control as
a member of the Safe for Initialization or Safe for Scripting
component categories is a static decision. In other words, you're
deciding that the control is or is not always safe. A control might
prefer to restrict its functionality at runtime when it needs to be
safe for initialization or scripting, but then prefer to have its
full, potentially unsafe functionality available at other
times.
Such controls must implement the
IObjectSafety interface. ATL provides a default
implementation of this interface in the IObjectSafetyImpl
class. As a template parameter, you specify the safety options that
the control supports. A container can use a method of this
interface to selectively enable and disable each supported option.
A control can determine its current safety level, and potentially
disable or enable unsafe functionality, by checking the
m_dwCurrentSafety member variable.
You use this implementation class like most of
the others, derive your control class from the appropriate template
class, and add the proper interface entry to the COM interface map.
BullsEye would do it like this:
class ATL_NO_VTABLE CBullsEye :
...
// Object safety support
public IObjectSafetyImpl<CBullsEye,
INTERFACESAFE_FOR_UNTRUSTED_CALLER |
INTERFACESAFE_FOR_UNTRUSTED_DATA>,
...
BEGIN_COM_MAP(CBullsEye)
// Object safety support
COM_INTERFACE_ENTRY(IObjectSafety)
...
END_COM_MAP()
STDMETHODIMP(FormatHardDrive)( ) {
if( m_dwCurrentSafety == 0 ) {
// Container isn't asking for safe behavior, perform mayhem
...
return S_OK; // We just erased a hard drive,
// how could we not be ok?
}
else {
// Container has asked we play nice, so don't
// actually erase the drive
...
return S_FALSE; // Ok, we succeeded, but we're
// being a little grumpy
}
ICategorizeProperties
Visual Basic provides a property view that displays
the properties of a control on a form. The property view can
display the properties on a control alphabetically or grouped by
arbitrary categories. Figure 11.3 shows the categorized
list of the BullsEye control's properties when the control
is contained on a Visual Basic form.
A control must implement the
ICategorizeProperties interface so that Visual Basic can
display the control's properties in the appropriate categories in
its property view. Unfortunately, this interface isn't presently
defined in any system IDL file or any system header file, and ATL
provides no implementation class for the interface. So, here's what
you need to do to support it.
First, here's the IDL for the interface:
[
object, local,
uuid(4D07FC10-F931-11CE-B001-00AA006884E5),
helpstring("ICategorizeProperties Interface"),
pointer_default(unique)
]
interface ICategorizeProperties : IUnknown {
typedef [public] int PROPCAT;
const int PROPCAT_Nil = -1;
const int PROPCAT_Misc = -2;
const int PROPCAT_Font = -3;
const int PROPCAT_Position = -4;
const int PROPCAT_Appearance = -5;
const int PROPCAT_Behavior = -6;
const int PROPCAT_Data = -7;
const int PROPCAT_List = -8;
const int PROPCAT_Text = -9;
const int PROPCAT_Scale = -10;
const int PROPCAT_DDE = -11;
HRESULT MapPropertyToCategory([in] DISPID dispid,
[out] PROPCAT* ppropcat);
HRESULT GetCategoryName([in] PROPCAT propcat, [in] LCID lcid,
[out] BSTR* pbstrName);
}
I keep this IDL in a separate file,
CategorizeProperties.idl, and import the file into the
BullsEye.idl file. At this point, it's highly unlikely
that Microsoft will add this interface to a system IDL file, so
having it in a separate IDL file makes it easier to reuse the
definition in multiple projects.
You implement the interface
like all interfaces in ATL: Derive your control class from
ICategorizeProperties, add the interface entry to the
control's interface map, and implement the two methods,
MapPropertyToCategory and GetCategoryName. Note
that there are 11 predefined property categories with negative
values. You can define your own custom categories, but be sure to
assign them positive values.
The MapPropertyToCategory method
returns the appropriate property category value for the specified
property.
const int PROPCAT_Scoring = 1;
STDMETHODIMP CBullsEye::MapPropertyToCategory(
/*[in]*/ DISPID dispid, /*[out]*/ PROPCAT* ppropcat) {
if (NULL == ppropcat) return E_POINTER;
switch (dispid) {
case DISPID_FORECOLOR:
case DISPID_BACKCOLOR:
case DISPID_CENTERCOLOR:
case DISPID_ALTERNATECOLOR:
case DISPID_RINGCOUNT:
case DISPID_BACKSTYLE:
*ppropcat = PROPCAT_Appearance;
return S_OK;
case DISPID_BEEP:
case DISPID_ENABLED:
*ppropcat = PROPCAT_Behavior;
return S_OK;
case DISPID_RINGVALUE:
*ppropcat = PROPCAT_Scoring;
return S_OK;
default:
return E_FAIL;
}
}
The GetCategoryName method simply
returns a BSTR containing the category name. You need to
support only your custom category values because Visual Basic knows
the names of the standard property category values.
STDMETHODIMP CBullsEye::GetCategoryName(/*[in]*/ PROPCAT propcat,
/*[in]*/ LCID lcid,
/*[out]*/ BSTR* pbstrName)
{
if(PROPCAT_Scoring == propcat) {
*pbstrName = ::SysAllocString(L"Scoring");
return S_OK;
}
return E_FAIL;
}
BullsEye supports
one custom category, Scoring, and associates its
Ring-Value property with the category. Unfortunately, the
RingValue property is an indexed property, and Visual
Basic doesn't support indexed properties. Thus, the
RingValue property doesn't appear in Visual Basic's
property view, either in the alphabetic list or in the categorized
list.
Per-Property
Browsing
When Visual Basic and similar containers display
a control's property in a property view, they can ask the control
for a string that better describes the property's current value
than the actual value of the property. For example, a particular
property might have valid numerical values of 1,
2, and 3, which represent the colors red, blue,
and green, respectively. When Visual Basic asks the control for a
display string for the property when the property has the value
2, the control returns the string "blue".
A container uses the control's
IPerPropertyBrowsing interface to retrieve the display
strings for a control's properties. When the control doesn't
provide a display string for a property, some containers, such as
Visual Basic, provide default formatting, if possible. Of
course, the container can always simply display the actual property
value.
Notice in Figure 11.3 that the Visual Basic
property view displays Yes for the value of the
Beep property (which was set to -1) and
transparent for the BackStyle property (which was
set to 0). To provide custom display strings for a
property's value, your control must implement
IPerPropertyBrowsing and override the
GeTDisplayString method. You return the appropriate string
for the requested property based on the property's current value.
Here's the GetdisplayString method for the
CBullsEye class:
STDMETHODIMP CBullsEye::GetDisplayString(DISPID dispid,
BSTR *pBstr) {
ATLTRACE2(atlTraceControls,2,
_T("CBullsEye::GetDisplayString\n"));
switch (dispid) {
case DISPID_BEEP:
if (VARIANT_TRUE == m_beep)
*pBstr = SysAllocString (OLESTR("Yes"));
else
*pBstr = SysAllocString (OLESTR("No"));
return *pBstr ? S_OK : E_OUTOFMEMORY;
case DISPID_BACKSTYLE:
if (1 == m_nBackStyle)
*pBstr = SysAllocString (OLESTR("Opaque"));
else
*pBstr = SysAllocString (OLESTR("Transparent"));
return *pBstr ? S_OK : E_OUTOFMEMORY;
case DISPID_ALTERNATECOLOR: // Make VB apply default
case DISPID_BACKCOLOR: // formatting for these
case DISPID_CENTERCOLOR: // color properties.
case DISPID_FORECOLOR: // Otherwise it displays color
// values in decimal and doesn't
// draw the color sample
// correctly
return S_FALSE; // This is an undocumented return
// value that works...
}
return
IPerPropertyBrowsingImpl<CBullsEye>::GetDisplayString(
dispid, pBstr);
}
The
IPerPropertyBrowsingImpl<T>::GetDisplayString
implementation fetches the value of the specified property and, if
it's not already a BSTR, converts the value into a
BSTR using VariantChangeType. This produces
relatively uninteresting display strings for anything but simple
numerical value properties.
Visual Basic provides default formatting for
certain property types, such as OLE_COLOR and
VARIANT_BOOL properties, but only if your
GetdisplayString method doesn't provide a string for the
property. The default implementation fails when the property
doesn't exist, when the property exists but cannot be converted
into a BSTR, or when the BSTR memory allocation
fails. This basically means that the default implementation of
GetdisplayString often provides less than useful strings
for many properties.
BullsEye's GetdisplayString
method lets Visual Basic provide default formatting for all its
OLE_COLOR properties by returning S_FALSE when
asked for those properties. This value isn't documented as a valid
return value for GetdisplayString, but there are a couple
of convincing reasons to use it: First, the default ATL
implementation of GetdisplayString returns this value when
it cannot provide a display string for a property. Second, it
works.
When you let Visual Basic provide the display
string for an OLE_COLOR property, it displays the color
value in hexadecimal and displays a color sample. ATL's default
implementation displays the color value in decimal, and the sample
image is typically always black. When you let Visual Basic provide
the display string for a VARIANT_BOOL property, Visual
Basic displays true and False. ATL's default
imple-mentation displays 1 and 0,
respectively.
Also notice in Figure 11.3 that when you click
on a property in Visual Basic's property view to modify the
property, a drop-down arrow appears to the right side of the
property value. Clicking this arrow produces a drop-down list that
contains strings representing the valid selections for the
property. You provide this support via the
IPerPropertyBrowsing interface, too. A container calls the
interface's GetPredefinedStrings method to retrieve the
strings that the container displays in the drop-down list. For each
string, the method also provides a DWORD value (cookie).
When a user selects one of the strings from the drop-down list, the
container calls the interface's GetPredefinedValue method
and provides the cookie. The method returns the property value
associated with the selected string. The container then typically
performs a property put IDispatch call to change the
property to the predefined value.
The BullsEye control supports
predefined strings and values for the Beep and
BackStyle properties, as shown in the following code:
/************************/
/* GetPredefinedStrings */
/************************/
#define DIM(a) (sizeof(a)/sizeof(a[0]))
static const LPCOLESTR rszBeepStrings [] = {
OLESTR("Yes, make noise"), OLESTR("No, be mute") };
static const DWORD rdwBeepCookies [] = { 0, 1 };
static const VARIANT_BOOL rvbBeepValues [] = {
VARIANT_TRUE, VARIANT_FALSE };
static const UINT cBeepStrings = DIM(rszBeepStrings);
static const UINT cBeepCookies = DIM(rdwBeepCookies);
static const UINT cBeepValues = DIM(rvbBeepValues);
static const LPCOLESTR rszBackStyleStrings [] = {
OLESTR("Opaque"), OLESTR("Transparent") };
static const DWORD rdwBackStyleCookies [] = { 0, 1 };
static const long rvbBackStyleValues [] = { 1, 0 };
static const UINT cBackStyleStrings = DIM(rszBackStyleStrings);
static const UINT cBackStyleCookies = DIM(rdwBackStyleCookies);
static const UINT cBackStyleValues = DIM(rvbBackStyleValues);
STDMETHODIMP CBullsEye::GetPredefinedStrings(
/*[in]*/ DISPID dispid,
/*[out]*/ CALPOLESTR *pcaStringsOut,
/*[out]*/ CADWORD *pcaCookiesOut) {
ATLTRACE2(atlTraceControls,2,
_T("CBullsEye::GetPredefinedStrings\n"));
if (NULL == pcaStringsOut || NULL == pcaCookiesOut)
return E_POINTER;
ATLASSERT (cBeepStrings == cBeepCookies);
ATLASSERT (cBeepStrings == cBeepValues);
ATLASSERT (cBackStyleStrings == cBackStyleCookies);
ATLASSERT (cBackStyleStrings == cBackStyleValues);
pcaStringsOut->cElems = 0;
pcaStringsOut->pElems = NULL;
pcaCookiesOut->cElems = 0;
pcaCookiesOut->pElems = NULL;
HRESULT hr = S_OK;
switch (dispid) {
case DISPID_BEEP:
hr = SetStrings (cBeepValues, rszBeepStrings, pcaStringsOut);
if (FAILED (hr)) return hr;
return SetCookies (cBeepValues, rdwBeepCookies,
pcaCookiesOut);
case DISPID_BACKSTYLE:
hr = SetStrings (cBackStyleValues, rszBackStyleStrings,
pcaStringsOut);
if (FAILED (hr)) return hr;
return SetCookies (cBackStyleValues, rdwBackStyleCookies,
pcaCookiesOut);
}
return
IPerPropertyBrowsingImpl<CBullsEye>::GetPredefinedStrings(
dispid, pcaStringsOut, pcaCookiesOut);
}
/**********************/
/* GetPredefinedValue */
/**********************/
STDMETHODIMP CBullsEye::GetPredefinedValue(
DISPID dispid, DWORD dwCookie, VARIANT* pVarOut) {
if (NULL == pVarOut) return E_POINTER;
ULONG i;
switch (dispid) {
case DISPID_BEEP:
// Walk through cookie array looking for matching value
for (i = 0; i < cBeepCookies; i++) {
if (rdwBeepCookies[i] == dwCookie) {
pVarOut->vt = VT_BOOL;
pVarOut->boolVal = rvbBeepValues [i];
return S_OK;
}
}
return E_INVALIDARG;
case DISPID_BACKSTYLE:
// Walk through cookie array looking for matching value
for (i = 0; i < cBackStyleCookies; i++) {
if (rdwBackStyleCookies[i] == dwCookie) {
pVarOut->vt = VT_I4;
pVarOut->lVal = rvbBackStyleValues [i];
return S_OK;
}
}
return E_INVALIDARG;
}
return
IPerPropertyBrowsingImpl<CBullsEye>::GetPredefinedValue(
dispid, dwCookie, pVarOut);
}
Some containers let you
edit a control's property using the appropriate property page for
the property. When you click on such a property in Visual Basic's
property view, Visual Basic displays a small button containing . .
. to the right of the property value. Clicking this button displays
the control's property page for the property.
A container uses a control's
IPerPropertyBrowsing::MapPropertyToPage method to find the
property page for a property. ATL's implementation of this method
uses the property map to determine which property page corresponds
to a particular property. When a property doesn't have a property
page, you specify CLSID_NULL as follows:
PROP_ENTRY("SomeProperty", DISPID_SOMEPROPERTY, CLSID_NULL)
IPerPropertyBrowsingImpl finds this
entry in the property map and returns the error
PERPROP_E_NOPAGEAVAILABLE. This prevents Visual Basic from
displaying the property page ellipses button ("...").
Keyboard Handling
for an ActiveX Control
When an ATL-based ActiveX control has the focus
on a Visual Basic form, it does not give the focus to the default
button on the form (the button with a Default property of
TRue) when you press Enter. ATL provides implementations
of the IOleControl::GetControlInfo and
IOleInPlaceActiveObject::TranslateAccelerator methods. The
GetControlInfo method returns E_NOTIMPL. A
container calls a control's GetControlInfo method to get
the control's keyboard mnemonics and keyboard behavior, and it
calls the control's translateAccelerator method to process
the key presses.
BullsEye overrides the default
implementation of GetControlInfo provided by ATL with the
following code:
STDMETHODIMP CBullsEye::GetControlInfo(CONTROLINFO *pci) {
if(!pci) return E_POINTER;
pci->hAccel = NULL;
pci->cAccel = 0;
pci->dwFlags = 0;
return S_OK;
}
The default implementation of
TRanslateAccelerator looks like this:
STDMETHOD(TranslateAccelerator)(LPMSG pMsg) {
T* pT = static_cast<T*>(this);
HRESULT hRet = S_OK;
MSG msg = *pMsg;
if (pT->PreTranslateAccelerator(&msg, hRet))
return hRet;
CComPtr<IOleControlSite> spCtlSite;
hRet = pT->InternalGetSite(__uuidof(IOleControlSite),
(void**)&spCtlSite);
if (SUCCEEDED(hRet)) {
if (spCtlSite != NULL) {
DWORD dwKeyMod = 0;
if (::GetKeyState(VK_SHIFT) < 0)
dwKeyMod += 1; // KEYMOD_SHIFT
if (::GetKeyState(VK_CONTROL) < 0)
dwKeyMod += 2; // KEYMOD_CONTROL
if (::GetKeyState(VK_MENU) < 0)
dwKeyMod += 4; // KEYMOD_ALT
hRet = spCtlSite->TranslateAccelerator(&msg, dwKeyMod);
}
else
hRet = S_FALSE;
}
return (hRet == S_OK) ? S_OK : S_FALSE;
}
When the BullsEye control has the input
focus, these method implementations pass all Tab and Enter key
presses to the container for processing. This implementation allows
one to tab into and out of the BullsEye control. While the
control has the input focus, pressing the Enter key activates the
default pushbutton on a Visual Basic form, if any.
For the BullsEye control, it doesn't
make much sense to allow a user to tab into the control. You can
use the MiscStatus bits for a control to inform the
control's container that the control doesn't want to be activated
by tabbing. A container asks a control for its MiscStatus
setting by calling the control's
IOle-Object::GetMiscStatus method. ATL provides an
implementation of this method in the IOleControlImpl
class:
STDMETHOD(GetMiscStatus)(DWORD dwAspect, DWORD *pdwStatus) {
ATLTRACE2(atlTraceControls,2,
_T("IOleObjectImpl::GetMiscStatus\n"));
return OleRegGetMiscStatus(T::GetObjectCLSID(),
dwAspect, pdwStatus);
}
This code simply delegates the call to the
OleRegGetMiscStatus function, which reads the value from
the control's Registry entry. A control can have multiple
MiscStatus valuesone for each drawing aspect that the
control supports. Most controls support the drawing aspect
DVASPECT_CONTENT, which has the value of 1. You
specify the drawing aspect as a subkey of MiscStatus. The
value of the subkey is the string of decimal numbers comprising the
sum of the desired OLEMISC enumeration values.
For example, BullsEye uses the
following MiscStatus settings:
OLEMISC_SETCLIENTSITEFIRST
|
131072
|
OLEMISC_NOUIACTIVATE
|
16384
|
OLEMISC_ACTIVATEWHENVISIBLE
|
256
|
OLEMISC_INSIDEOUT
|
128
|
CANTLINKINSIDE
|
16
|
OLEMISC_RECOMPOSEONRESIZE
|
1
|
The sum of these values is 14,7857, so you
specify that as the value of the subkey called 1 of your
class.
ForceRemove {7DC59CC5-36C0-11D2-AC05-00A0C9C8E50D} =
s 'BullsEye Class' {
...
'MiscStatus' = s '0'
{
'1' = s '147857'
}
}
Alternatively, BullsEye can override
the GetMiscStatus method and provide the desired value;
the Registry entry then would not be needed:
STDMETHODIMP CBullsEye::GetMiscStatus (
DWORD dwAspect, DWORD *pdwStatus) {
if (NULL == pdwStatus) return E_POINTER;
*pdwStatus =
OLEMISC_SETCLIENTSITEFIRST |
OLEMISC_ACTIVATEWHENVISIBLE |
OLEMISC_INSIDEOUT |
OLEMISC_CANTLINKINSIDE |
OLEMISC_RECOMPOSEONRESIZE |
OLEMISC_NOUIACTIVATE ;
return S_OK;
}
The
OLEMISC_NOUIACTIVATE setting prevents Visual Basic from
giving the BullsEye control the input focus when a user
attempts to tab into the control.
|