March 1999
Write ActiveX Controls Using Custom Interfaces Provided by ATL 3.0, Part II |
Code for this article: ATLPart2.exe (182KB)
Many containers use the connection point protocol to hand the container's sink interface pointer to the event source. To support connection point events, a control must implement the IConnectionPointContainer interface as well as one IConnectionPoint interface for each outgoing interface. |
This article assumes you're familiar with C++, ATL, and COM |
This article is adapted from the forthcoming book, ATL Internals,
by Brent Rector and Chris Sells (Addison-Wesley, 1999).
Brent is the founder of Wise Owl Consulting Inc., a COM consulting firm. Reach Brent at http://www.wiseowl.com/brent.htm. Chris is an independent consultant specializing in designing
and building distributed systems using COM. Chris can be
reached at http://www.sellsbrothers.com.
|
Last month, we described the ATL-based BullsEye ActiveX® control and its requirements (Write ActiveX Controls Using Custom Interfaces Provided by ATL 3.0). We also showed you how to generate the initial code for the control, then modify that source to support the control's stock and custom properties and methods. This month we'll enhance the control significantly by adding support for connection points so the control can fire events. In addition, by the end of this article the BullsEye control will support property change notifications, drawing (both windowed and windowless), various persistence interfaces, use of ambient properties, quick activation, and component categories. Finally, the BullsEye control will integrate tightly with the Visual Basic design-time environment by categorizing its properties for the Visual Basic Property View, and will also provide rich per-property browsing support. Adding Connection Point Support
Many containers use the connection point protocol to hand the container's sink interface pointer to the event source (the 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 will support two source interfaces: the control's default source dispatch interface (_IBullsEyeEvents for the BullsEye control) and the property change notification source interface (IPropertyNotifySink). 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'll need to add this base class explicitly should 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'll also need one connection point for each source interface supported by your control. ATL provides the IConnectionPointImpl class as an implementation of the IConnectionPoint interface. Typically, you will not use this class directly, but will instead derive a new class from IConnectionPointImpl, customizing the class by adding various event-firing methods. Your control will inherit from this derived class. Typically, a control will support two connection points: one for property change notifications and one for the control's custom events.
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 a typedef _ATL_PROP_ NOTIFY_EVENT_CLASS (note the single leading underscore) as equivalent to 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. You'll have to add it otherwise.
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 CComControlBase class as shown in Figure 1. Therefore, the call to FireOnChanged in a property put method of a CComControl-derived class is actually a call to FireOnChanged in 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.
typedef CFirePropNotifyEvent
_ATL_PROP_NOTIFY_EVENT_CLASS;
For some unknown reason, it's the property map in your control class that equates the two types. The BEGIN_PROP_ MAP macro defines the type __ATL_PROP_NOTIFY_ EVENT_CLASS as equivalent to the type _ATL_PROP_ NOTIFY_EVENT_CLASS.
#define BEGIN_PROP_MAP(theClass) \
typedef _ATL_PROP_NOTIFY_EVENT_CLASS \
__ATL_PROP_NOTIFY_EVENT_CLASS; \
•
•
•
In the BullsEye control, this means that when your property put method calls FireOnChanged, it 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 to _ATL_PROP_
NOTIFY_EVENT_CLASS. IPropertyNotifySinkCP aliases _ATL_PROP_NOTIFY_SINK_CLASS to CFirePropNotifyEvent. Therefore, you actually call the CBullsEye::CFirePropNotifyEvent::FireOnChanged function. The CFirePropNotifyEvent class contains two static methods, FireOnRequestEdit and FireOnChanged (see Figure 2), 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. This means that you must derive your control class from IPropertyNotifySinkCP in order 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. As long as your control class contains a property map, the code compiles without error and the method calls do nothing at runtime.
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 IPropertyNotifySink-
CP, 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 shown previously. 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 occurs 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, which calls CBullsEye::__ATL_PROP_NOTIFY_
EVENT_CLASS::FireOnChanged. The property map aliases __ATL_PROP_NOTIFY_EVENT_CLASS to _ATL_PROP_
NOTIFY_EVENT_CLASS. The global typedef aliases _ATL_PROP_NOTIFY_SINK_CLASS to CFakeFirePropNotifyEvent. Therefore, you actually call the CBullsEye::
CFakeFirePropNotifyEvent::FireOnChanged function, which simply returns S_OK.
Supporting the Event Connection Point
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 only hook up to 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 right-click on the BullsEye class in the Class View and select Implement Connection Point.
Figure 3 shows the specialized connection point class, CProxy_IBullsEyeEvents, generated by the wizard for the _IBullsEyeEvents dispatch interface. You use this class by adding it to the base class list of the control. 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>,
•
•
•
Finally, the IConnectionPointContainerImpl class needs a table that associates source interface IDs 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_CONNECTION_POINT_MAP macros. 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()
Many containers, such as Visual Basic and Microsoft® 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 the control doesn't source events and they 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'll 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
A control must be able to render its image when requested by its container. There are basically three different situations where a control receives a rendering request:
While 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 (see Figure 4). You need to use the information in this structure when drawing your control. Unfortunately, the structure definition itself is presently all the documentation available about ATL_
DRAWINFO. However, most of the fields are simply copies of similar parameters to the IViewObject::Draw method. ATL provides a default implementation of the OnDrawAdvanced method in CComControlBase (see Figure 5).
CComControlBase::OnDrawAdvanced prepares a normalized device context for drawing, then calls your control class's OnDraw method. The normalized device context is aptly named 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. If you don't want these default values, you should override OnDrawAdvanced (rather than 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, the control can use drawing optimizations. As a result, the control doesn't have to restore certain settings of the device context after changing the setting.
When IDataObject_GetData calls OnDrawAdvanced to retrieve a rendering of the control in a metafile, IDataObject_
GetData saves the device context state, calls OnDrawAdvanced, 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 if and only if the container supports optimized drawing. Therefore, 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 non-metafile device context, 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. Therefore, in ATL's current implementation, when you override OnDraw the bOptimize flag is always TRUE. This doesn't mean you shouldn't check the flag; it means that you should always try to support optimized drawing when overriding OnDraw because such support will always be used.
Figure 6 shows the drawing code for the BullsEye control. There are three features worth noting. First, 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 will show through. Second, BullsEye draws differently into a metafile device context versus another device context. There are some operations you cannot do when drawing to a metafile. Therefore, BullsEye sets up the device context slightly differently in these two cases. Third, BullsEye supports optimized drawing.
Property Persistence
A control typically needs to save its state upon request by its container. Various containers prefer differing 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/VARIANT 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 into a structured storage.
ATL provides three persistence interface implementations that you can use:
Most controls should derive from all three persistence implementation classes so 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. Figure 7 shows the CBullsEye class's property 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 persisting properties are described via a PROP_DATA_ENTRY macro, ATL accesses the member variable in the control directly.
You must explicitly add entries for any additional properties 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'd use the PROP_
ENTRY_EX macro when your control supports multiple dispatch interfaces with various properties accessible via different dispatch interfaces. This is, generally speaking, not a good thing to do.
One word of caution: don't add a PROP_ENTRY macro that has a property name containing 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. This is a bug in the current version of Visual Basic. Other containers allow space-separated property names.
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.
There is presently a bug in the ATL implementation of property bag persistence for properties described using the PROP_DATA_ENTRY macro. The problem is in the AtlIPersistPropertyBag_Load function. Here's a code fragment from that function:
CComVariant var;
if (pMap[i].dwSizeData != 0) {
void* pData =
(void*) (pMap[i].dwOffsetData + (DWORD)pThis);
var.vt = pMap[i].vt; // BUG FIX line added
HRESULT hr = pPropBag->Read(pMap[i].szDesc,
&var, pErrorLog);
if (SUCCEEDED(hr)) {
switch (pMap[i].vt) {
case VT_UI4:
*((long*)pData) = var.lVal;
break;
}
}
The CComVariant constructor initializes var to VT_EMPTY. An empty input variant permits the IPropertyBag::Read method to coerce the value read to any appropriate type. Note, however, that the code copies the variant's value into the member variable of the control based on the type specified in the property map entry, regardless of the type contained in the variant. When the _cx and _cy extents are small enough, the Read method initializes the variant to contain a VT_I2 (short) value. However, the property map entry specifies that the member variable is VT_UI4 type. In this case, the code sets the high-order 16 bits of the control's extents to bogus values. Initializing the variant type to the type contained in the property map, as shown in the "BUG FIX line added" statement in the code fragment, fixes the problem.
Let's look at how the persistence implementations work. Since property bags have a bug we would like to fix, we'll use it as the example. However, all persistence implementations are similar.
IPersistPropertyBagImpl<T>::Load calls T::IPersistPropertyBag_Load to do most of the work. Normally, your control (class T) doesn't provide an IPersistPropertyBag_
Load method, so this call vectors to the base class IPersistPropertyBagImpl<T>::IPersistPropertyBag_Load method. The IPersistPropertyBagImpl<T>::IPersistPropertyBag_
Load method calls the global function AtlIPersistPropertyBag, which has the bug.
We could play some linker tricks and provide a custom AtlIPersistPropertyBag function that gets linked in preference to the one ATL provides. However, it's easier (and less fragile) to have the BullsEye control provide an IPersistPropertyBag_Load method that simply calls a fixed version of AtlIPersistPropertyBag.
As it turns out, we need to provide an IPersistPropertyBag_Load method anyway. The BullsEye control has one additional property: the RingValues indexed (array) property. 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 and write the indexed property. Figure 8 is an example from the BullsEye control. It calls a fixed version of AtlIPersistPropertyBag_
Load, then saves the indexed property.
The IQuickActivate Interface
Some control containers ask a control for its IQuickActivate interface and use the interface to exchange quickly 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 Full, Composite, and HTML controls use by default. However, a control container also provides a control with a few ambient properties during this quick activation process that the ATL implementation doesn't save. Should your control need these ambient propertiesBackColor, ForeColor, and Appearanceit's more efficient to save them during the quick activation process than 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. 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 so:
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 already obtained the ambient properties and 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) {
hr = GetAmbientBackColor(m_clrBackColor);
hr = GetAmbientForeColor(m_clrForeColor);
hr = 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 that the control may be initialized and accessed by scripts on an HTML page 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 may prefer to restrict its functionality at runtime when it needs to be safe for initialization or scripting, but 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. You specify, as a template parameter, the safety options supported by the control, and a container can use the SetInterfaceSafetyOptions 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 does 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()
•
•
•
};
The ICategorizeProperties Interface
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 group them by arbitrary categories. Figure 9 shows the categorized list of the BullsEye control's properties when it's contained on a Visual Basic form.
Figure 9 Property View |
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 or header file, and ATL provides no implementation class for the interface. So here's what you need to do to support it.
Figure 10 lists the IDL definition for the interface. We keep this IDL in a separate file (CategorizeProperties.idl) and import the file into the BullsEye.idl file. This way, when Microsoft finally adds the interface to a system IDL file, we can simply remove the import from the BullsEye.idl file.
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, shown in Figure 11, returns the appropriate property category value for the specified property. The GetCategoryName method, also shown in Figure 11, simply returns a BSTR containing the category name. You only need to support your custom category values since Visual Basic knows the names of the standard property category values.
BullsEye supports one custom category called Scoring and associates its RingValue property with the category. Unfortunately, RingValue is an indexed property and Visual Basic doesn't presently support indexed properties. As a result, the RingValue property doesn't appear in the Visual Basic property view alphabetic list or 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 may 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 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, will provide default formatting, if possible. Of course, the container can always simply display the actual property value.
Note in Figure 9 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. Figure 12 shows the GetDisplayString method for the CBullsEye class.
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 will provide 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 only fails when the property doesn't exist, it exists but cannot be converted into a BSTR, or the BSTR memory allocation fails. This 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 of 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. The default ATL implementation of GetDisplayString returns this value when it cannot provide a display string for a property, and 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 black. When you let Visual Basic provide the display string for a VARIANT_BOOL property, Visual Basic displays "True" and "False". ATL's default implementation displays "-1" and "0", respectively.
Also notice in Figure 9 that when you click on a property in the Visual Basic property view, a dropdown arrow appears to the right side of the property value. Clicking on this arrow produces a dropdown list containing strings representing the valid selections for the property. You provide this support via the IPerPropertyBrowsing interface, too. a container will call the interface's GetPredefinedStrings method to retrieve the strings the container displays in the dropdown list. For each string, the method also provides a DWORD value (a cookie). When a user selects one of the strings from the dropdown 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 Figure 13.
Some containers will let you edit a control's property using the appropriate property page for the property. When you click on such a property in the Visual Basic property view, Visual Basic displays a … button to the right of the property value. Clicking on this button displays the control's property page.
A container uses a control's IPerPropertyBrowsing::
MapPropertyToPage method to find the property page. Unfortunately, the ATL 3.0 IPerPropertyBrowsingImpl class has a small bug in its implementation. When it finds a property listed in the property map, it returns the CLSID contained in the map. The property map serves two functions: it maps properties to property pages, and it also lists properties supported by the persistence interfaces.
For a property that you want persisted, but for which you have no property page editing support, you add an entry such as the following to the property map:
PROP_ENTRY("SomeProperty", DISPID_SOMEPROPERTY,
CLSID_NULL)
IPerPropertyBrowsingImpl finds this entry in the property map and returns CLSID_NULL for the property page class. This causes Visual Basic to display the button. When you click on it, you receive an error message stating that CLSID_
NULL is not registered. To correct the problem, override MapPropertyToPage in your control. Invoke IPerPropertyBrowsingImpl::MapPropertyToPage and, when it finds the requested property, check for CLSID_NULL as the output. In this case, return the proper error status: PERPROP_E_
NOPAGEAVAILABLE.
STDMETHODIMP
CBullsEye::MapPropertyToPage (DISPID dispid,
CLSID *pClsid)
{
HRESULT hr =
IPerPropertyBrowsingImpl<CBullsEye>::
MapPropertyToPage(dispid, pClsid);
if (SUCCEEDED(hr) && CLSID_NULL == *pClsid)
hr = PERPROP_E_NOPAGEAVAILABLE;
return hr;
}
Various Other Features
The BullsEye control has numerous other features that we don't have the space to discuss in depth. It supports drag and drop. It supports transparent areas in the control, and therefore supports two-pass drawing. It supports windowed and windowless activation. BullsEye processes window messages, primarily the left mouse click, and fires the appropriate events when you click on one of the rings. It plays the sound of an arrow hitting a target when you click on a ring (when the Beep property is TRUE). The control displays an About dialog box when a container invokes the stock AboutBox method.
You can download the source code for the entire control from the link at the top of this article. Also included is a simple Visual Basic-based project that hosts the control on a form.
From the March 1999 issue of Microsoft Systems Journal.
For related information see: Creating ActiveX Components in C++ at http://msdn.microsoft.com/library/techart/msdn_creaactx.htm. Also check http://msdn.microsoft.com for daily updates on developer programs, resources and events. |
|