Chapter 11. ActiveX Controls

A Review of ActiveX Controls

A complete review of the COM interfaces and interactions between an ActiveX control and a control container is outside the scope of this book. If you are unfamiliar with the various interfaces and interactions described in this chapter, various other texts specifically address these topics. Inside OLE (Microsoft Press, 1995), by Kraig Brockschmidt, is the original COM text; it devotes hundreds of pages to in-place activation and visual interface components.

An ActiveX control is a superset of an in-place activated object, so you also need to read the OLE Controls Specification from Microsoft, which describes the requirements to be a control. In addition, the OLE Controls 1996 Specification, commonly referred to as the OC96 spec, documents optimizations for control activations (such as windowless controls and windowless control containment), two-pass rendering for nonrectangular windows, hit testing for nonrectangular windows, fast-activation protocols between controls and containers, and numerous other features.

Instead of rewording the material available in these references, I show you how to implement such an object. This chapter describes how to implement a feature-complete ActiveX control using ATL.

ActiveX Control Functionality

A control incorporates much of the functionality you saw in earlier chapters. For example, a control is a COM object. Therefore, an ATL control contains all the standard functionality of an ATL-based COM object. A control is a user-interface (UI) component; therefore, it has thread affinity and should live in a single-threaded apartment. A control thus derives from the CComObjectRootEx<CComSingleThreadModel> base class.

A control must be a createable class so its container can instantiate it. Therefore, the control class also derives from CComCoClass. Many controls use the CComCoClass default class object’s implementation of the IClassFactory interface. Licensed controls override this default by specifying the DECLARE_CLASSFACTORY2 macro, which declares a class object that implements the IClassFactory2 interface.

In addition, most controls support one or more of the following features:

  • Stock properties and methods such as ForeColor and Refresh that a container can access via the control’s IDispatch implementation.

  • Custom properties and methods that a container can access via the control’s IDispatch implementation.

  • Stock and custom event callback methods using the connection points protocol to a container’s dispinterface implementation. This requires the control to implement the IConnectionPointContainer and IProvideClassInfo2 interfaces, as well as a connection point that makes calls to the event dispinterface.

  • Property change notifications to one or more clients using the connection points protocol to the clients’ IPropertyNotifySink interface implementations. Control properties that send such change notifications should be marked in the control’s type library using the bindable or requestedit attributes, as appropriate.

  • On-demand rendering of a view of the object via the IViewObject, IView-Object2, and IViewObjectEx interfaces.

  • Standard OLE control functionality, as provided by the IOleControl interface, and in-place activation using the IOleObject and IOleInPlaceActiveObject interfaces.

  • Persistence support for various containers. At a minimum, a control typically provides support so that a container can save the object into a stream using the IPersistStreamInit interface. Many controls also support persistence to a property bag using IPersistPropertyBag because Visual Basic and Internet Explorer prefer this medium. Some controls additionally support IPersistStorage so that they can be embedded into OLE documents.

  • Fast and efficient windowless activation, as provided by the IOleInPlace-ObjectWindowless interface when the control’s container supports this optimization.

  • Fast and efficient exchange of multiple interfaces during activation between a control and its controls using the IQuickActivate interface.

  • Object safety settings either through component category membership or via IObjectSafety.

  • Drag-and-drop support, as provided by implementations of the IDataObject, IDropSource, and IDropTarget interfaces.

  • A graphical user interface that provides a means to edit the control’s properties. Typically, a control provides one or more COM objects, called property pages, each of which displays a user interface that can modify a logically related subset of the control’s properties. A container requests the CLSIDs of the property page COM objects using the control’s ISpecifyPropertyPages interface implementation.

  • A container’s capability to access information about the properties of a control that supports property pages by using the IPerPropertyBrowsing interface. For example, the container can obtain a text string describing a property, determine which property page contains the user interface to edit the property, and retrieve a list of strings describing the allowed values for the property.

  • Support for arranging the control’s properties by category in Visual Basic’s property view. A control implements the ICategorizeProperties interface to provide the list of categories to Visual Basic and to map each property to a category.

  • Default keyboard handling for an ActiveX control. This is commonly needed for tabbing, default button presses on Enter, arrow keys, and pop-up help.

  • MiscStatus flags settings for a control. Special settings are necessary for some controls to operate properly.

Property Page Functionality

Because a control frequently provides one or more property pages, a complete control implementation also supplies one or more property page objects, which do the following:

  • Implement (at least) the IPropertyPage interface, which provides the main features of a property page object.

  • Optionally implement the IPropertyPage2 interface to support selection of a specific property. Visual Basic uses this support to open the correct property page and set the input focus directly to the specified control when the user wants to edit a property.

  • Receive property change notifications from one or more controls using the connection points protocol to the property page’s IPropertyNotifySink interface implementation.

The BullsEye Control Requirements

_images/bullseye.jpg

This chapter describes the ATL implementation of a feature-rich control called BullsEye. The BullsEye control implements all the previously described features. The BullsEye control draws a bull’s eye. You can configure the number of rings in the bull’s eye (from one to nine) and the color of the center ring, as well as the color of the ring adjacent to the center (called the alternate ring color). BullsEye draws additional rings by alternately using the center and alternate colors.

The area around the bull’s eye can be transparent or opaque. When transparent, the background around the bull’s eye shows through. When opaque, the bull’s eye fills the area around the circle using the background color. By default, BullsEye uses the container’s ambient background color as the background color. BullsEye also uses the foreground color to draw a line separating each ring

You can assign score values to each ring. By default, the center ring is worth 512 points and each other ring is worth half the points of its adjacent inner ring. When a user clicks on a ring, the control fires an OnRingHit event and an OnScoreChanged event. The argument to the OnRingHit event method specifies the ring upon which the user clicked. Rings are numbered from 1 to N, where 1 is the centermost ring. The OnScoreChanged event specifies the point value of the clicked ring. For example, clicking on ring 2 with default scores fires an OnScoreChanged event with an argument of 256 points.

In addition, when you click on one of the bull’s-eye rings, the control can provide feedback by playing a sound. By default, you hear the sound of an arrow striking the bull’s eye. The Boolean Beep property, when set to trUE, indicates that the control should play its sound on a ring hit.

BullsEye supports all standard control functionality. In addition to windowed activation, BullsEye can be activated as a windowless control when its container supports such functionality.

Many containers ask their controls to save their state using the IPersistStreamInit interface and an IStream medium. When embedding a control in an OLE document, a container asks a control to save its state using the IPersistStorage interface and the IStorage medium. A container, such as Internet Explorer and Visual Basic, that prefers to save the state of a control as textual name/value pairs uses the control’s IPersistPropertyBag interface and the IPropertyBag medium. BullsEye supports all three persistence protocols and mediastreams, storages, and property bags.

BullsEye also provides two property pages. One property page is custom to the BullsEye control and enables you to set the Enabled, Beep (sound on ring hit), and BackStyle (TRansparent) properties (see Figure 11.1).

Figure 11.1. BullsEye custom property page

_images/11atl01.jpg

The other property page is the standard color-selection property page (see Figure 11.2). The BullsEye control has four color properties: the center ring color, the alternate ring color, the background color (used to fill the area around the bull’s eye) and the foreground color (used to draw the separator line between rings).

Figure 11.2. ``BullsEye`` color property page

_images/11atl02.jpg

The BullsEye control also categorizes its properties for Visual Basic 6. VB6 has a property-view window in which you can select a view that sorts the properties by standard and control-defined categories (see Figure 11.3).

Figure 11.3. Visual Basic 6 property view window for ``BullsEye``

_images/11atl03.jpg

The BullsEye control lists its color properties and the RingCount property in the standard Appearance category. The control lists its Beep property in the standard Behavior category.

BullsEye also supports per-property browsing, which allows a control to specify a list of strings that a container should display as the available choices for a property’s value. Notice in the example Visual Basic property view window that, in the Behavior category, Visual Basic displays the strings “Yes, make noise” and “No, be mute” as the selections available for the Beep property.

Also notice that the Misc category contains an entry called (About) that represents the AboutBox stock method. BullsEye displays the dialog box shown in Figure 11.4 when the user selects the About entry.

Figure 11.4. BullsEye About box

_images/11atl04.jpg

Requirements: The Properties and Methods

BullsEye supports the four stock properties shown in Table 11.1.

Table 11.1. ``BullsEye`` Stock Properties

Property Name

Type

Stock DISPID

Description

BackColor

OLE_COLOR

DISPID_BACKCOLOR

Background color

BackStyle

Long

DISPID_BACKSTYLE

Background style, transparent or opaque

Enabled

VARIANT_BOOL

DISPID_ENABLED

Enabled status, TRUE or FALSE

ForeColor

OLE_COLOR

DISPID_FORECOLOR

Foreground color

In addition, BullsEye supports all three stock methods, as shown in Table 11.2.

Table 11.2. ``BullsEye`` Stock Methods

Method Name

Stock DISPID

Description

AboutBox

DISPID_ABOUTBOX

Displays the control’s Help About dialog box

DoClick

DISPID_DOCLICK

Simulates a mouse click on the control

Refresh

DISPID_REFRESH

Redraws the control

Finally, BullsEye supports the custom properties listed in Table 11.3.

Table 11.3. ``BullsEye`` Custom Properties

Property Name

Type

Custom DISPID

Description

Application

IDispatch*

DISPID_APPLICATION

Returns the IDispatch* for the hosting application

AlternateColor

OLE_COLOR

DISPID_ALTERNATECOLOR

Gets/sets the color of the alternate (even) rings

Beep

VARIANT_BOOL

DISPID_BEEP

Enables/disables sound effects for the control

CenterColor

OLE_COLOR

DISPID_CENTERCOLOR

Gets/sets the color of the center (odd) rings

Parent

IDispatch*

DISPID_PARENT

Returns the IDispatch* for the control’s parent

RingCount

Short

DISPID_RINGCOUNT

Gets/sets the number of rings

RingValue

Long

DISPID_RINGVALUE

Gets/sets the value of each ring

Declaring the Properties and Methods in IDL

A control container accesses the properties and methods of a control using the control’s IDispatch interface. A control must therefore provide an implementation of IDispatch when it has properties and methods.

ATL-based controls in generaland the BullsEye control, specificallyimplement their properties and methods using a dual interface, not a dispatch interface, even though a dual interface is unnecessary because the vtable portion of the dual interface typically goes unused. A custom C++ control container can access the control’s properties and methods using the vtable, but no other container currently does. Visual Basic 6 accesses properties and methods of a control using the control’s IDispatch interface. Visual Basic uses the vtable portion of a dual interface only for noncontrol objects.

The BullsEye control provides access to its properties and methods on the default IBullsEye dual interface. When you generate a new ATL-based control class, the wizard generates the definition of the default dual interface, but you must populate the definition with the accessor methods for your control’s properties and the control’s methods. Listing 11.1 gives the definition of the IBullsEye interface.

Listing 11.1. The IBullsEye Interface

 1[ object,
 2  uuid(B4FBD008-B03D-4F48-9C5B-4A981EB6A515),
 3  dual, nonextensible, helpstring("IBullsEye Interface"),
 4  pointer_default(unique)
 5]
 6interface IBullsEye : IDispatch {
 7    const int DISPID_ALTERNATECOLOR = 1;
 8    const int DISPID_BEEP = 2;
 9    const int DISPID_CENTERCOLOR = 3;
10    const int DISPID_RINGCOUNT = 4;
11    const int DISPID_RINGVALUE = 5;
12    const int DISPID_APPLICATION = 6;
13    const int DISPID_PARENT = 7;
14
15    // Stock Properties
16    [propput, bindable, requestedit, id(DISPID_BACKCOLOR)]
17    HRESULT BackColor([in]OLE_COLOR clr);
18    [propget, bindable, requestedit, id(DISPID_BACKCOLOR)]
19    HRESULT BackColor([out,retval]OLE_COLOR* pclr);
20    [propput, bindable, requestedit, id(DISPID_BACKSTYLE)]
21    HRESULT BackStyle([in]long style);
22    [propget, bindable, requestedit, id(DISPID_BACKSTYLE)]
23    HRESULT BackStyle([out,retval]long* pstyle);
24    [propput, bindable, requestedit, id(DISPID_FORECOLOR)]
25    HRESULT ForeColor([in]OLE_COLOR clr);
26    [propget, bindable, requestedit, id(DISPID_FORECOLOR)]
27    HRESULT ForeColor([out,retval]OLE_COLOR* pclr);
28    [propput, bindable, requestedit, id(DISPID_ENABLED)]
29    HRESULT Enabled([in]VARIANT_BOOL vbool);
30    [propget, bindable, requestedit, id(DISPID_ENABLED)]
31    HRESULT Enabled([out,retval]VARIANT_BOOL* pbool);
32
33// Stock methods
34    [id(DISPID_ABOUTBOX)] HRESULT AboutBox( );
35    [id(DISPID_DOCLICK)] HRESULT DoClick( );
36    [id(DISPID_REFRESH)] HRESULT Refresh( );
37    // Custom properties
38    [propget, bindable, requestedit, id(DISPID_APPLICATION)]
39    HRESULT Application([out, retval] IDispatch** pVal);
40
41    [propget, bindable, requestedit, id(DISPID_ALTERNATECOLOR)]
42    HRESULT AlternateColor([out, retval] OLE_COLOR* pVal);
43    [propput, bindable, requestedit, id(DISPID_ALTERNATECOLOR)]
44    HRESULT AlternateColor([in] OLE_COLOR newVal);
45
46    [propget, bindable, requestedit, id(DISPID_BEEP)]
47    HRESULT Beep([out, retval] VARIANT_BOOL* pVal);
48    [propput, bindable, requestedit, id(DISPID_BEEP)]
49    HRESULT Beep([in] VARIANT_BOOL newVal);
50
51    [propget, bindable, requestedit, id(DISPID_CENTERCOLOR)]
52    HRESULT CenterColor([out, retval] OLE_COLOR* pVal);
53    [propput, bindable, requestedit, id(DISPID_CENTERCOLOR)]
54    HRESULT CenterColor([in] OLE_COLOR newVal);
55
56    [propget, bindable, requestedit, id(DISPID_PARENT)]
57    HRESULT Parent([out, retval] IDispatch** pVal);
58
59    [propget, bindable, requestedit, id(DISPID_RINGCOUNT)]
60    HRESULT RingCount([out, retval] SHORT* pVal);
61    [propput, bindable, requestedit, id(DISPID_RINGCOUNT)]
62    HRESULT RingCount([in] SHORT newVal);
63
64    [propget, bindable, requestedit, id(DISPID_RINGVALUE)]
65    HRESULT RingValue([in] SHORT sRingNumber,
66        [out, retval] LONG* pVal);
67    [propput, bindable, requestedit, id(DISPID_RINGVALUE)]
68    HRESULT RingValue([in] SHORT sRingNumber, [in] LONG newVal);
69};

Requirements: The Events

BullsEye Custom Events

The BullsEye control doesn’t support any of the stock events. However, it has two custom events, as detailed in Table 11.4.

Table 11.4. ``BullsEye`` Custom Events

Event

Event DISPID

Description

void OnRingHit (short sRingNumber)

DISPID_ONRINGHIT

The user clicked on one of the bull’s-eye rings. The argument specifies the ring that the user clicked. Rings are numbers from 1 to N from the center outward.

void OnScoreChanged (long RingValue)

DISPID_ONSCORECHANGED

This event follows the OnRingHit event when the user clicks on a bull’s-eye ring. The argument specifies the score value of the ring that the user clicked.

An event interface contains only methods and should be a dispatch interface for all containers to receive the event callbacks. Some containers, such as Visual Basic, can receive event callbacks on custom IUnknown-derived interfaces. An event interface should never be a dual interface.

Declaring the Event Dispatch Interface in IDL

Listing 11.2 gives the definition of the _IBullsEyeEvents dispatch interface. For the constants for the DISPIDs to appear in the MIDL-generated C/C++ header file, the definitions of the constants must appear in the IDL file outside of the library block. You must define the dispinterface itself inside the library block.

Listing 11.2. The _IBullsEyeEvents Dispatch Interface

 1const int DISPID_ONRINGHIT      = 1;
 2const int DISPID_ONSCORECHANGED = 2;
 3
 4[ uuid(58D6D8CB-765D-4C59-A41F-BBA8C40F7A14),
 5  helpstring("Event interface for BullsEye Control")
 6]
 7dispinterface _IBullsEyeEvents {
 8    properties:
 9    methods:
10    [id(DISPID_ONRINGHIT)]
11    void OnRingHit(short ringNumber);
12    [id(DISPID_ONSCORECHANGED)]
13    void OnScoreChanged(long ringValue);
14};

Requirements: The BullsEye and Property Page Coclasses

You must also define the BullsEye coclass in the library block of the IDL file (see Listing 11.3). At a minimum, you must specify the default IDispatch interface (IBullsEye) via which a container can access the control’s properties and methods, and the default source interface (_IBullsEyeEvents) tHRough which the BullsEye control fires events to its container.

Listing 11.3. The BullsEye Coclass

1[   uuid(E9312AF5-1C11-4BA4-A0C6-CB660E949B78),
2    control, helpstring("BullsEye Class")
3]
4coclass BullsEye {
5    [default] interface IBullsEye;
6    [default, source] dispinterface _IBullsEyeEvents;
7};

Additionally, you should define all the custom property page classes implemented by your control in the library block of the IDL file. BullsEye has only one custom property page, called BullsEyePropPage (see Listing 11.4).

Listing 11.4. The BullsEyePropPage Coclass

1[   uuid(47446235-8500-43a2-92A7-F0686FDAA69C),
2    helpstring("BullsEye Property Page class")
3]
4coclass BullsEyePropPage {
5    interface IUnknown;
6};

Creating the Initial Control Using the ATL Wizard

Some people prefer to write all the code for a control by hand. They don’t care for the wizard-generated code because they don’t understand what it does. Occasionally, the wizard-generated code is incorrect as well. Even if you generate the initial code base using the wizard, you will change it greatly before the control is complete anyway, so you might as well save some time and effort initially by using the wizard.

Selecting Options for the CBullsEye Implementation Class

Using the requirements for the BullsEye control, I created an ATL project and used the Add Class dialog box to add an ATL control. First, I defined the name of my implementation class, (CBullsEye), its source filenames, the primary interface name (IBullsEye), and various COM object registration information (see Figure 11.5).

Figure 11.5. ``BullsEye`` Names dialog box

[View full size image]

_images/11atl05.jpg

The Options screen (see Figure 11.6) looks much like the Options page you get when adding a simple object. In addition to letting you choose the interface type and threading model, it lets you choose what type of control you want.

Figure 11.6. COM object options for the ``BullsEye`` control

[View full size image]

_images/11atl06.jpg

In this case, I chose Standard Control because it matches the BullsEye requirements the best of the three choices. [1] The Options page in this wizard is slightly different than the Simple Object Wizard’s Options page. Controls need thread affinity (because they are UI components and use window handles, which are associated with a thread). Therefore, the wizard correctly allows you to request only the Single or Apartment threading models for a control.

The Minimal Control check box [2] tells the wizard that you want to support only the minimum interfaces needed to be an ActiveX control. This means that when you get to the Interfaces page in the wizard (see Figure 11.7), the Supported list will be empty by default instead of the standard set you normally get.

Figure 11.7. Interfaces for the ``BullsEye`` control

[View full size image]

_images/11atl07.jpg

Containers access a control’s properties and methods using the control’s IDispatch interface. The easiest way to get an IDispatch implementation in your control is to specify that the primary interface should be a dual interface. If you specify the Custom interface option, you must implement IDispatch separately on the control. Controls can be aggregated or not. I’ve requested that BullsEye support aggregation, even though it increases the size of each instance by 8 bytes.

Next, we get to choose from a set of stock interfaces (see Figure 11.7).

In addition to the standard list the wizard supplies, I added the IPropertyNotify-Sink interface, which is the standard interface for the control to tell its container that a property has changed.

The Appearance page, shown in Figure 11.8, enables you to select various control options that are not available elsewhere.

Figure 11.8. Miscellaneous control options for the ``BullsEye`` control

[View full size image]

_images/11atl08.jpg

The options on the Appearance page specify various optimizations that ActiveX control containers can take advantage of. The Opaque option says that your control is completely opaque: None of the container’s background will show through your control. Containers can use this to avoid having to paint the background under the control. Solid Background (which means anything only when Opaque is also specified) indicates that the background of your control is a solid color instead of a patterned brush. Later, I discuss how to implement the BullsEye rendering code so that it supports transparent areas around the bull’s eye, but let’s start with an opaque control. Your choices for these two options appear in the code as a DECLARE_VIEW_STATUS macro in your class declaration. This macro provides a method that returns the options you chose. The IViewObjectExImpl< > template uses this method to implement the IViewObjectEx interface.

The Normalize DC (device context) option causes your control to override the OnDraw method for its rendering. When not selected, the control overrides the OnDrawAdvanced method. By default. OnDrawAdvanced saves the state of the device context, switches to MM_TEXT mapping mode, calls OnDraw, and then restores the saved device context. Therefore, when you ask for a normalized DC and don’t override OnDrawAdvanced, you introduce a little more overhead. BullsEye uses this support, though.

Windowed Only states that the control does not support windowless activation; the control must have its own window. This is useful for drop targets, for example, that need to receive window messages, but the BullsEye control can handle windowless activation.

Insertable adds a Registry entry under the control’s CLSID key that makes the control show up in the standard Insert Object dialog box used by OLE containers, as in the various Microsoft Office applications. Selecting this option also adds support for the IPersistStorage and IDataObject interfaces.

The Add Control Based On option enables you to create an ActiveX control that superclasses one of the standard Windows controls: Button, ComboBox, Edit, and more. A total of 16 options are available here, but the typical ActiveX control generally will be a new window class, so None is the default.

The Miscellaneous Status bits provide a couple extra pieces of information to the container: They result in Registry changes in the RGS file and the introduction of a DECLARE_OLEMISC_STATUS macro into your header file. Invisible at Runtime means that the control displays only at design time, not at runtime. The VB Timer control is the canonical example of this behavior. Acts Like Button controls can be set as the default OK or Cancel buttons on dialog boxes or on forms. Finally, Acts Like Label is used for a control that doesn’t accept keyboard focus, but that can be an accelerator key to select the next control in the Tab order (surprisingly enough, this is exactly how a label behaves).

Figure 11.8 shows the options chosen for the BullsEye control.

Finally, you can have the wizard generate support for any stock properties that you want the control to support. BullsEye requires four stock properties, which I’ve selected in the dialog box in Figure 11.9. ATL provides the implementation of the property-accessor methods.

Figure 11.9. Stock properties for the ``BullsEye`` control

[View full size image]

_images/11atl09.jpg

No wizard support exists for stock methods, so you have to implement them, as well as the BullsEye custom properties, by hand.

Base Classes Used by Wizard-Generated Classes

The ATL wizard generates the initial source code for a control. The control class derives from a number of different base classes, depending on the type of control you ask the wizard to generate. The wizard also adds support for various features based on selections you make from the ATL Object Wizard Properties dialog pages. Table 11.5 summarizes the base classes used by the different types of wizard-generated controls.

Table 11.5. Base Classes Used by Various Control Types

Base Classes Used

Standard Control

Minimal Standard Control

Composite Control

DHTML Control

Minimal Composite Control

Minimal DHTML Control

CComObjectRootEx<TM>

CComCoClass

CComControl

CComCompositeControl

CStockPropImpl

SP

SP

SP

SP

SP | SP

IConnectionPointContainerImpl

CP

CP

CP

CP

CP

CP

IDataObjectImpl

IDispatchImpl

No SP and dual

No SP and dual

No SP and dual

No SP and dual

IOleControlImpl

IOleInPlaceActiveObjectImpl

IOleInPlaceObjectWindowlessImpl

IOleObjectImpl

IPersistStorageImpl

IPersistStreamInitImpl

IPropertyNotifySinkCP

CP

CP

CP

IProvideClassInfo2Impl

IQuickActivateImpl

ISpecifyPropertyPagesImpl

ISupportErrorInfoImpl

SEI

SEI

SEI

SEI

SEI

SEI

IViewObjectExImpl

Control uses normalized DC

User

User

User

User

Control is windowed only

User

User

SP = Stock properties selected; CP = Connection points enabled; SEI = SupportErrorInfo enabled; User = User chosen option on Appearance wizard page when creating class

The Initial BullsEye Source Files

The Initial CBullsEye Class Declaration

Listing 11.5 shows the initial wizard-generated class declaration for the CBullsEye control class. We have to make a number of changes to the class before it meets all the requirements described at the beginning of the chapter. For example, we need to add a few more base classes to obtain all the required control functionality. Also, presently the control supports no properties except the stock properties I selected via the wizard dialog boxes. Plus, there is quite of bit of implementation code to write to make the control draw and behave as a bull’s eye. I’ll get to all that, but first let’s look at the initial source code.

I’ve reformatted the source code slightly from the original wizard-generated source code to group related functionality and to add a few comments (see Listing 11.5).

Listing 11.5. The Initial Wizard-Generated CBullsEye Class

  1// CBullsEye
  2class ATL_NO_VTABLE CBullsEye :
  3    // COM object support
  4    public CComObjectRootEx<CComSingleThreadModel>,
  5    // Class object support
  6    public CComCoClass<CBullsEye, &CLSID_BullsEye>,
  7    // Default dual interface for control.
  8    // Requests for properties preprocessed by
  9    // stock property base class
 10    public CStockPropImpl<CBullsEye, IBullsEye>,
 11    // Error info support for default dual interface
 12    public ISupportErrorInfo,
 13    // Basic "lite" control implementation
 14    public CComControl<CBullsEye>
 15    public IOleControlImpl<CBullsEye>,
 16    public IOleObjectImpl<CBullsEye>,
 17    public IOleInPlaceActiveObjectImpl<CBullsEye>,
 18    public IOleInPlaceObjectWindowlessImpl<CBullsEye>,
 19    public IViewObjectExImpl<CBullsEye>,
 20    // "Lite" control persistence implementation
 21    public IPersistStreamInitImpl<CBullsEye>,
 22    // Full control additional implementation
 23#ifndef _WIN32_WCE
 24    // Support for OLE Embedding
 25    public IDataObjectImpl<CBullsEye>,
 26#endif
 27    public IPersistStorageImpl<CBullsEye>,
 28    // Support for property pages
 29    public ISpecifyPropertyPagesImpl<CBullsEye>,
 30    // Support for fast activation
 31    public IQuickActivateImpl<CBullsEye>,
 32    // Connection point implementation
 33    public IConnectionPointContainerImpl<CBullsEye>,
 34    public CProxy_IBullsEyeEvents<CBullsEye>,
 35    public IProvideClassInfo2Impl<&CLSID_BullsEye,
 36        &__uuidof(_IBullsEyeEvents), &LIBID_BullsEyeControlLib>,
 37    // Selection of IPropertyNotifySink adds additional
 38    // connection point for property change notifications
 39    public IPropertyNotifySinkCP<CBullsEye>,
 40#ifdef _WIN32_WCE
 41    // IObjectSafety is required on Windows CE for the
 42    // control to be loaded correctly
 43    public IObjectSafetyImpl<CBullsEye,
 44        INTERFACESAFE_FOR_UNTRUSTED_CALLER>,
 45#endif
 46{
 47public:
 48    CBullsEye() { }
 49
 50DECLARE_REGISTRY_RESOURCEID(IDR_BULLSEYE)
 51DECLARE_PROTECT_FINAL_CONSTRUCT()
 52
 53    HRESULT FinalConstruct() { return S_OK; }
 54
 55    void FinalRelease() { }
 56
 57DECLARE_OLEMISC_STATUS(OLEMISC_RECOMPOSEONRESIZE |
 58    OLEMISC_CANTLINKINSIDE |
 59    OLEMISC_INSIDEOUT |
 60    OLEMISC_ACTIVATEWHENVISIBLE |
 61    OLEMISC_SETCLIENTSITEFIRST
 62)
 63
 64BEGIN_COM_MAP(CBullsEye)
 65    // Default dual interface to control
 66    COM_INTERFACE_ENTRY(IBullsEye)
 67    COM_INTERFACE_ENTRY(IDispatch)
 68    // Error info support for default dual interface
 69    COM_INTERFACE_ENTRY(ISupportErrorInfo)
 70    // Basic "lite" control implementation
 71    COM_INTERFACE_ENTRY(IOleControl)
 72    COM_INTERFACE_ENTRY(IOleObject)
 73    COM_INTERFACE_ENTRY(IOleInPlaceActiveObject)
 74    COM_INTERFACE_ENTRY(IOleInPlaceObject)
 75    COM_INTERFACE_ENTRY(IOleInPlaceObjectWindowless)
 76    COM_INTERFACE_ENTRY2(IOleWindow, IOleInPlaceObjectWindowless)
 77    COM_INTERFACE_ENTRY(IViewObjectEx)
 78    COM_INTERFACE_ENTRY(IViewObject2)
 79    COM_INTERFACE_ENTRY(IViewObject)
 80    // "Lite" control persistence implementation
 81    COM_INTERFACE_ENTRY(IPersistStreamInit)
 82    COM_INTERFACE_ENTRY2(IPersist, IPersistStreamInit)
 83    // Full control additional implementation
 84    // Support for OLE Embedding
 85#ifndef _WIN32_WCE
 86    COM_INTERFACE_ENTRY(IDataObject)
 87#endif
 88    COM_INTERFACE_ENTRY(IPersistStorage)
 89    // Support for property pages
 90    COM_INTERFACE_ENTRY(ISpecifyPropertyPages)
 91    // Support for fast activation
 92    COM_INTERFACE_ENTRY(IQuickActivate)
 93    // Support for connection points
 94    COM_INTERFACE_ENTRY(IConnectionPointContainer)
 95    COM_INTERFACE_ENTRY(IProvideClassInfo)
 96    COM_INTERFACE_ENTRY(IProvideClassInfo2)
 97#ifdef _WIN32_WCE
 98    // IObjectSafety is required on Windows CE for the
 99    // control to be loaded correctly
100    COM_INTERFACE_ENTRY_IID(IID_IObjectSafety, IObjectSafety)
101#endif
102END_COM_MAP()
103
104// Initially, the control's stock properties are the only
105// properties supported via persistence and property pages.
106BEGIN_PROP_MAP(CBullsEye)
107    PROP_DATA_ENTRY("_cx", m_sizeExtent.cx, VT_UI4)
108    PROP_DATA_ENTRY("_cy", m_sizeExtent.cy, VT_UI4)
109#ifndef _WIN32_WCE
110    PROP_ENTRY("BackColor", DISPID_BACKCOLOR,
111        CLSID_StockColorPage)
112#endif
113    PROP_ENTRY("BackStyle", DISPID_BACKSTYLE, CLSID_NULL)
114    PROP_ENTRY("Enabled", DISPID_ENABLED, CLSID_NULL)
115#ifndef _WIN32_WCE
116    PROP_ENTRY("ForeColor", DISPID_FORECOLOR,
117        CLSID_StockColorPage)
118#endif
119END_PROP_MAP()
120
121BEGIN_CONNECTION_POINT_MAP(CBullsEye)
122    // Property change notifications
123    CONNECTION_POINT_ENTRY(IID_IPropertyNotifySink)
124    // Our default connection point
125    CONNECTION_POINT_ENTRY(__uuidof(_IBullsEyeEvents))
126END_CONNECTION_POINT_MAP()
127
128// Initially the control passes all Windows messages
129// to the base class
130BEGIN_MSG_MAP(CBullsEye)
131    CHAIN_MSG_MAP(CComControl<CBullsEye>)
132    DEFAULT_REFLECTION_HANDLER()
133END_MSG_MAP()
134
135// ISupportsErrorInfo
136    STDMETHOD(InterfaceSupportsErrorInfo)(REFIID riid) {
137        // Implementation deleted for clarity...
138    }
139    // IViewObjectEx
140    DECLARE_VIEW_STATUS(VIEWSTATUS_SOLIDBKGND |
141        VIEWSTATUS_OPAQUE)
142
143// IBullsEye
144public:
145    HRESULT OnDraw(ATL_DRAWINFO& di) {
146        // Sample drawing code omitted for clarity
147    }
148
149    // Storage for values of stock properties
150    OLE_COLOR m_clrBackColor;
151    LONG m_nBackStyle;
152    BOOL m_bEnabled;
153    OLE_COLOR m_clrForeColor;
154
155    // Stock property change notification functions
156    void OnBackColorChanged() {
157        ATLTRACE(_T("OnBackColorChanged\n"));
158    }
159    void OnBackStyleChanged() {
160        ATLTRACE(_T("OnBackStyleChanged\n"));
161    }
162    void OnEnabledChanged() {
163        ATLTRACE(_T("OnEnabledChanged\n"));
164    }
165    void OnForeColorChanged() {
166        ATLTRACE(_T("OnForeColorChanged\n"));
167    }
168};

The Initial IBullsEye Interface

Listing 11.6 is the initial wizard-generated IDL description for the IBullsEye interface. The wizard generates the interface containing any stock properties you’ve specified. We need to add all the custom properties for the control, as well as any stock and custom methods that the control supports.

Listing 11.6. The Initial Wizard-Generated IDL for the IBullsEye Interface

 1[
 2    object,
 3    uuid(B4FBD008-B03D-4F48-9C5B-4A981EB6A515),
 4    dual,
 5    nonextensible,
 6    helpstring("IBullsEye Interface"),
 7    pointer_default(unique)
 8]
 9interface IBullsEye : IDispatch{
10    [propput, bindable, requestedit, id(DISPID_BACKCOLOR)]
11    HRESULT BackColor([in]OLE_COLOR clr);
12    [propget, bindable, requestedit, id(DISPID_BACKCOLOR)]
13    HRESULT BackColor([out,retval]OLE_COLOR* pclr);
14    [propput, bindable, requestedit, id(DISPID_BACKSTYLE)]
15    HRESULT BackStyle([in]long style);
16    [propget, bindable, requestedit, id(DISPID_BACKSTYLE)]
17    HRESULT BackStyle([out,retval]long* pstyle);
18    [propput, bindable, requestedit, id(DISPID_FORECOLOR)]
19    HRESULT ForeColor([in]OLE_COLOR clr);
20    [propget, bindable, requestedit, id(DISPID_FORECOLOR)]
21    HRESULT ForeColor([out,retval]OLE_COLOR* pclr);
22    [propput, bindable, requestedit, id(DISPID_ENABLED)]
23    HRESULT Enabled([in]VARIANT_BOOL vbool);
24    [propget, bindable, requestedit, id(DISPID_ENABLED)]
25    HRESULT Enabled([out,retval]VARIANT_BOOL* pbool);
26};

The Initial _IBullsEyeEvents Dispatch Interface

The initial _IBullsEyeEvents dispatch interface is empty (see Listing 11.7). We need to add the BullsEye custom events to the dispinterface. When a control supports any of the stock events, you add them to the event interface as well.

Listing 11.7. The Initial Wizard-Generated IDL for the _IBullsEyeEvent Dispatch Interface

1[  uuid(58D6D8CB-765D-4C59-A41F-BBA8C40F7A14),
2   helpstring("_IBullsEyeEvents Interface")
3]
4dispinterface _IBullsEyeEvents {
5   properties:
6   methods:
7};

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:

1interface IBullsEye : IDispatch {
2  ...
3  [id(DISPID_ABOUTBOX)] HRESULT AboutBox();
4  [id(DISPID_DOCLICK)] HRESULT DoClick();
5  [id(DISPID_REFRESH)] HRESULT Refresh();
6};

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:

1template < class T, class InterfaceName,
2    const IID* piid = &_ATL_IIDOF(InterfaceName),
3    const GUID* plibid = &CAtlModule::m_libid,
4    WORD wMajor = 1, WORD wMinor = 0,
5    class tihclass = CComTypeInfoHolder>
6class ATL_NO_VTABLE CStockPropImpl :
7    public IDispatchImpl< InterfaceName, piid, plibid,
8    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:

1class ATL_NO_VTABLE CBullsEye :
2    public CStockPropImpl<CBullsEye, IBullsEye, &IID_IBullsEye,
3                      &LIBID_ATLINTERNALSLib>,
4    ...

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++:

 1class CStockPropImpl {
 2  ...
 3  HRESULT STDMETHODCALLTYPE get_Picture(IPictureDisp** ppPicture) {
 4    __if_exists(T::m_pPicture) {
 5      ATLTRACE(atlTraceControls,2,
 6        _T("CStockPropImpl::get_Picture\n"));
 7      ATLASSERT(ppPicture != NULL);
 8      if (ppPicture == NULL)
 9        return E_POINTER;
10
11
12      T* pT = (T*) this;
13      *ppPicture = pT->m_pPicture;
14      if (*ppPicture != NULL)
15          (*ppPicture)->AddRef();
16    }
17    return S_OK;
18  }
19  ...
20};

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:

1class CBullsEye : ... {
2  ...
3  OLE_COLOR m_clrBackColor;
4  LONG      m_nBackStyle;
5  BOOL      m_bEnabled;
6  OLE_COLOR m_clrForeColor;
7};

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.

 1#define IMPLEMENT_STOCKPROP(type, fname, pname, dispid) \
 2    HRESULT STDMETHODCALLTYPE put_##fname(type pname) { \
 3        __if_exists(T::m_##pname) { \
 4            ATLTRACE(ATL::atlTraceControls,2, \
 5                _T("CStockPropImpl::put_%s\n"), #fname); \
 6            T* pT = (T*) this; \
 7            if (pT->m_nFreezeEvents == 0 && \
 8                pT->FireOnRequestEdit(dispid) == S_FALSE) \
 9                return S_FALSE; \
10            pT->m_##pname = pname; \
11            pT->m_bRequiresSave = TRUE; \
12            if (pT->m_nFreezeEvents == 0) \
13                pT->FireOnChanged(dispid); \
14            __if_exists(T::On##fname##Changed) { \
15                pT->On##fname##Changed(); \
16            } \
17            pT->FireViewChange(); \
18            pT->SendOnDataChange(NULL); \
19        } \
20        return S_OK; \
21    } \
22    HRESULT STDMETHODCALLTYPE get_##fname(type* p##pname) { \
23        __if_exists(T::m_##pname) { \
24            ATLTRACE(ATL::atlTraceControls,2, \
25                _T("CStockPropImpl::get_%s\n"), #fname); \
26            ATLASSERT(p##pname != NULL); \
27            if (p##pname == NULL) \
28                return E_POINTER; \
29            T* pT = (T*) this; \
30            *p##pname = pT->m_##pname; \
31        } \
32        return S_OK; \
33    }

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:

1#define IMPLEMENT_STOCKPROP(type, fname, pname, dispid) \
2  ...
3            __if_exists(T::On##fname##Changed) { \
4                pT->On##fname##Changed(); \
5            } \

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.

 1interface IBullsEye : IDispatch {
 2...
 3    [propput, bindable, requestedit, id(DISPID_CENTERCOLOR)]
 4      HRESULT CenterColor([in] OLE_COLOR newVal);
 5
 6    [propget, bindable, requestedit, id(DISPID_CENTERCOLOR)]
 7      HRESULT CenterColor([out, retval] OLE_COLOR *pVal);
 8
 9    [id(DISPID_ABOUTBOX)] HRESULT AboutBox();
10...
11};

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:

1class CBullsEye : ... {
2  ...
3  STDMETHODIMP get_CenterColor(/*[out, retval]*/OLE_COLOR *pVal);
4  STDMETHODIMP put_CenterColor(/*[in]*/ OLE_COLOR newVal);
5  STDMETHODIMP AboutBox();
6  ...
7};

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:

1STDMETHODIMP CBullsEye::get_CenterColor(OLE_COLOR *pVal) {
2    if (NULL == pVal) return E_POINTER;
3    *pVal = m_clrCenterColor;
4    return S_OK;
5}

The put_CenterColor method, like all property change functions, is a bit more complicated:

 1STDMETHODIMP CBullsEye::put_CenterColor(OLE_COLOR newVal) {
 2  if (m_clrCenterColor == newVal) return S_OK;
 3
 4  if (!m_nFreezeEvents)
 5    if (FireOnRequestEdit(DISPID_CENTERCOLOR) == S_FALSE)
 6      return S_FALSE;
 7
 8  m_clrCenterColor = newVal;           // Save new color
 9  ::DeleteObject (m_centerBrush);      // Clear old brush color
10  m_centerBrush = NULL;
11  m_bRequiresSave = TRUE;              // Set dirty flag
12  if (!m_nFreezeEvents)
13    FireOnChanged(DISPID_CENTERCOLOR); // Notify container
14  FireViewChange();                    // Request redraw
15  SendOnDataChange(NULL);              // Notify advise sinks
16
17  return S_OK;
18}

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 events – that 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:

 1class CBullsEye : ... {
 2    ...
 3    void OnCenterColorChanged() {
 4        ::DeleteObject(m_centerBrush);
 5        m_centerBrush = 0;
 6        }
 7
 8IMPLEMENT_STOCKPROP(OLE_COLOR, CenterColor,
 9    clrCenterColor, DISPID_CENTERCOLOR);
10    ...
11private:
12    // Storage for the property value
13    OLE_COLOR m_clrCenterColor;
14    ...
15};

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.

1STDMETHODIMP CBullsEye::AboutBox() {
2    CAboutDlg dlg;
3    dlg.DoModal();
4    return S_OK;
5}

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.

Figure 11.10. Class View it lies!

_images/11atl10.jpg

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.

 1[ uuid(58D6D8CB-765D-4C59-A41F-BBA8C40F7A14),
 2  helpstring("_IBullsEyeEvents Interface")
 3]
 4dispinterface _IBullsEyeEvents {
 5    properties:
 6    methods:
 7    [id(DISPID_ONRINGHIT)]
 8    void OnRingHit(short ringNum);
 9    [id(DISPID_ONSCORECHANGED)]
10    void OnScoreChanged(long ringValue);
11};

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.

1[ uuid(E9312AF5-1C11-4BA4-A0C6-CB660E949B78),
2  control, helpstring("BullsEye Class")
3]
4coclass BullsEye {
5    [default] interface IBullsEye;
6    [default, source] dispinterface _IBullsEyeEvents;
7};

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.

1class ATL_NO_VTABLE CBullsEye :
2    ...
3    // Connection point container support
4    public IConnectionPointContainerImpl<CBullsEye>,
5    ...

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.

1template <class T, class CDV = CComDynamicUnkArray >
2class ATL_NO_VTABLE IPropertyNotifySinkCP :
3    public IConnectionPointImpl<T, &IID_IPropertyNotifySink, CDV> {
4public:
5    typedef CFirePropNotifyEvent __ATL_PROP_NOTIFY_EVENT_CLASS;
6};

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:

1class ATL_NO_VTABLE CBullsEye :
2    ...
3    public IPropertyNotifySinkCP<CBullsEye>,
4    ...

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:

 1template <class T, class WinBase = CWindowImpl< T > >
 2class ATL_NO_VTABLE CComControl :
 3  public CComControlBase, public WinBase {
 4public:
 5  HRESULT FireOnRequestEdit(DISPID dispID) {
 6    T* pT = static_cast<T*>(this);
 7    return T::__ATL_PROP_NOTIFY_EVENT_CLASS::FireOnRequestEdit
 8     (pT->GetUnknown(), dispID);
 9}
10  HRESULT FireOnChanged(DISPID dispID) {
11    T* pT = static_cast<T*>(this);
12    return T::__ATL_PROP_NOTIFY_EVENT_CLASS::FireOnChanged
13     (pT->GetUnknown(), dispID);
14  }
15  ...
16};

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).

1typedef 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).

1#define BEGIN_PROP_MAP(theClass) \
2    __if_not_exists(__ATL_PROP_NOTIFY_EVENT_CLASS) { \
3        typedef ATL::_ATL_PROP_NOTIFY_EVENT_CLASS
4        __ATL_PROP_NOTIFY_EVENT_CLASS; \
5    } \
6...

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.

 1class CFirePropNotifyEvent {
 2public:
 3  // Ask any objects sinking the IPropertyNotifySink
 4  // notification if it is ok to edit a specified property
 5  static HRESULT FireOnRequestEdit(IUnknown* pUnk, DISPID dispID) {
 6    CComQIPtr<IConnectionPointContainer,
 7      &__uuidof(IConnectionPointContainer)> pCPC(pUnk);
 8    if (!pCPC) return S_OK;
 9
10    CComPtr<IConnectionPoint> pCP;
11    pCPC->FindConnectionPoint(__uuidof(IPropertyNotifySink),
12      &pCP);
13    if (!pCP) return S_OK;
14
15    CComPtr<IEnumConnections> pEnum;
16    if (FAILED(pCP->EnumConnections(&pEnum)))
17      return S_OK;
18
19    CONNECTDATA cd;
20    while (pEnum->Next(1, &cd, NULL) == S_OK) {
21      if (cd.pUnk) {
22        HRESULT hr = S_OK;
23        CComQIPtr<IPropertyNotifySink,
24          &__uuidof(IPropertyNotifySink)> pSink(cd.pUnk);
25
26        if (pSink != NULL)
27          hr = pSink->OnRequestEdit(dispID);
28
29        cd.pUnk->Release();
30        if (hr == S_FALSE) return S_FALSE;
31      }
32    }
33    return S_OK;
34}
35  // Notify any objects sinking the IPropertyNotifySink
36  // notification that a property has changed
37  static HRESULT FireOnChanged(IUnknown* pUnk, DISPID dispID) {
38    CComQIPtr<IConnectionPointContainer,
39      &__uuidof(IConnectionPointContainer)> pCPC(pUnk);
40    if (!pCPC) return S_OK;
41
42    CComPtr<IConnectionPoint> pCP;
43    pCPC->FindConnectionPoint(__uuidof(IPropertyNotifySink),
44      &pCP);
45    if (!pCP) return S_OK;
46    CComPtr<IEnumConnections> pEnum;
47
48    if (FAILED(pCP->EnumConnections(&pEnum)))
49      return S_OK;
50
51    CONNECTDATA cd;
52    while (pEnum->Next(1, &cd, NULL) == S_OK) {
53      if (cd.pUnk) {
54        CComQIPtr<IPropertyNotifySink,
55          &__uuidof(IPropertyNotifySink)> pSink(cd.pUnk);
56
57        if (pSink != NULL)
58          pSink->OnChanged(dispID);
59        cd.pUnk->Release();
60      }
61    }
62    return S_OK;
63  }
64};

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:

1typedef 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:

 1class CFakeFirePropNotifyEvent {
 2public:
 3  static HRESULT FireOnRequestEdit(IUnknown* /*pUnk*/,
 4    DISPID /*dispID*/)
 5  { return S_OK; }
 6
 7  static HRESULT FireOnChanged(IUnknown* /*pUnk*/,
 8    DISPID /*dispID*/)
 9  { return S_OK; }
10};

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:

 1#pragma once
 2
 3template<class T>
 4class CProxy_IBullsEyeEvents :
 5  public IConnectionPointImpl<T, &__uuidof(_IBullsEyeEvents)> {
 6public:
 7  HRESULT Fire_OnRingHit(short ringNum) {
 8    HRESULT hr = S_OK;
 9    T * pThis = static_cast<T *>(this);
10    int cConnections = m_vec.GetSize();
11
12    for (int iConnection = 0;
13      iConnection < cConnections; iConnection++) {
14      pThis->Lock();
15      CComPtr<IUnknown> punkConnection =
16        m_vec.GetAt(iConnection);
17      pThis->Unlock();
18
19      IDispatch * pConnection =
20        static_cast<IDispatch *>(punkConnection.p);
21
22      if (pConnection) {
23        CComVariant avarParams[1];
24        avarParams[0] = ringNum;
25        avarParams[0].vt = VT_I2;
26        DISPPARAMS params = { avarParams, NULL, 1, 0 };
27        hr = pConnection->Invoke(DISPID_ONRINGHIT, IID_NULL,
28          LOCALE_USER_DEFAULT, DISPATCH_METHOD, &params,
29          NULL, NULL, NULL);
30      }
31    }
32    return hr;
33  }
34
35  HRESULT Fire_OnScoreChanged(long ringValue)
36  {
37    // Code similar to above deleted for clarity
38  }
39};

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:

1class ATL_NO_VTABLE CBullsEye :
2    // events and property change notifications
3    public CProxy_IBullsEyeEvents<CBullsEye>,
4    public IPropertyNotifySinkCP<CBullsEye>,
5    ...

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:

1BEGIN_CONNECTION_POINT_MAP(CBullsEye)
2    CONNECTION_POINT_ENTRY(DIID__IBullsEyeEvents)
3    CONNECTION_POINT_ENTRY(IID_IPropertyNotifySink)
4END_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.

 1class ATL_NO_VTABLE CBullsEye :
 2  public IProvideClassInfo2Impl<&CLSID_BullsEye,
 3                                &DIID__IBullsEyeEvents,
 4                                &LIBID_ATLInternalsLib>,
 5  ...
 6{
 7  ...
 8  BEGIN_COM_MAP(CBullsEye)
 9    ...
10    // Support for connection points
11    COM_INTERFACE_ENTRY(IConnectionPointContainer)
12    COM_INTERFACE_ENTRY(IProvideClassInfo2)
13    COM_INTERFACE_ENTRY(IProvideClassInfo)
14  END_COM_MAP()
15  ...
16};

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:

  1. The control has a window that receives a WM_PAINT message. A control handles this request in CComControlBase::OnPaint.

  2. 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.

  3. 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.

1virtual 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:

 1struct ATL_DRAWINFO {
 2  UINT cbSize;         // Set to sizeof(ATL_DRAWINFO)
 3  DWORD dwDrawAspect;  // Drawing aspect  typically
 4                       // DVASPECT_CONTENT
 5  LONG lindex;         // Commonly -1, which specifies
 6                       // all of the data
 7  DVTARGETDEVICE* ptd; // Render to this target device
 8  HDC hicTargetDev;    // Information context for target device
 9  HDC hdcDraw;         // Draw on this device context
10  LPCRECTL prcBounds;  // Rectangle in which to draw
11  LPCRECTL prcWBounds; // WindowOrg and Ext if metafile
12  BOOL bOptimize;      // Can control use drawing optimizations?
13  BOOL bZoomed;        // Object extent differs from
14                       // drawing rectangle?
15  BOOL bRectInHimetric;// Rectangle in HiMetric?
16  SIZEL ZoomNum;       // Rectangle size: ZoomX =
17                       // ZoomNum.cx/ZoomDen.cx
18  SIZEL ZoomDen;       // Extent size: ZoomY =
19                       // ZoomNum.cy/ZoomDen.cy
20};

ATL provides the following default implementation of the OnDrawAdvanced method in CComControl:

 1inline HRESULT
 2CComControlBase::OnDrawAdvanced(ATL_DRAWINFO& di) {
 3  BOOL bDeleteDC = FALSE;
 4  if (di.hicTargetDev == NULL) {
 5    di.hicTargetDev = AtlCreateTargetDC(di.hdcDraw, di.ptd);
 6    bDeleteDC = (di.hicTargetDev != di.hdcDraw);
 7  }
 8  RECTL rectBoundsDP = *di.prcBounds;
 9  BOOL bMetafile =
10    GetDeviceCaps(di.hdcDraw, TECHNOLOGY) == DT_METAFILE;
11  if (!bMetafile) {
12    ::LPtoDP(di.hdcDraw, (LPPOINT)&rectBoundsDP, 2);
13    SaveDC(di.hdcDraw);
14    SetMapMode(di.hdcDraw, MM_TEXT);
15    SetWindowOrgEx(di.hdcDraw, 0, 0, NULL);
16    SetViewportOrgEx(di.hdcDraw, 0, 0, NULL);
17    di.bOptimize = TRUE; //since we save the DC we can do this
18  }
19  di.prcBounds = &rectBoundsDP;
20  GetZoomInfo(di);
21  HRESULT hRes = OnDraw(di);
22  if (bDeleteDC)
23    ::DeleteDC(di.hicTargetDev);
24  if (!bMetafile)
25    RestoreDC(di.hdcDraw, -1);
26  return hRes;
27}

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 [3] 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

  1#define ASSERT_SUCCESS( b ) ATLASSERT( ( b ) != 0 )
  2#define VERIFY_SUCCESS( c ) { BOOL bSuccess = ( c ); \
  3  ATLASSERT( bSuccess != 0 ); }
  4
  5// Drawing code
  6
  7static const int LOGWIDTH = 1000;
  8
  9HRESULT CBullsEye::OnDraw(ATL_DRAWINFO &di) {
 10    CRect rc = *( ( RECT * )di.prcBounds );
 11    HDC hdc = di.hdcDraw;
 12
 13    // Create brushes as needed
 14    ...
 15
 16    // First, fill in background color in invalid area when
 17    // BackStyle is Opaque
 18    if (m_nBackStyle == 1 /* Opaque*/ ) {
 19        VERIFY_SUCCESS( ::FillRect( hdc, &rc, m_backBrush ) );
 20    }
 21
 22    int nPrevMapMode;
 23    POINT ptWOOrig, ptVOOrig;
 24    SIZE szWEOrig, szVEOrig;
 25
 26    BOOL bMetafile =
 27        GetDeviceCaps( di.hdcDraw, TECHNOLOGY ) == DT_METAFILE;
 28    if( !bMetafile ) {
 29        // OnDrawAdvanced normalized the device context
 30        // We are now using MM_TEXT and the coordinates
 31        // are in device coordinates.
 32
 33        // Establish convenient coordinate system for on screen
 34        ...
 35    } else {
 36        // We will be played back in ANISOTROPIC mapping mode
 37        // Set up bounding rectangle and coordinate system
 38        // for metafile
 39        ...
 40    }
 41
 42    // Draw the BullsEye
 43    DrawBullsEye( di );
 44
 45    // Note on optimized drawing:
 46    // Even when using optimized drawing, a control cannot
 47    // leave a changed mapping mode, coordinate transformation
 48    // value, selected bitmap, clip region, or metafile in the
 49    // device context.
 50
 51    if (!bMetafile) {
 52        ::SetMapMode (hdc, nPrevMapMode);
 53
 54        ::SetViewportOrgEx (hdc, ptVOOrig.x,  ptVOOrig.y,  NULL);
 55        ::SetViewportExtEx (hdc, szVEOrig.cx, szVEOrig.cy, NULL);
 56    }
 57
 58    ::SetWindowOrgEx (hdc, ptWOOrig.x,  ptWOOrig.y,  NULL);
 59    ::SetWindowExtEx (hdc, szWEOrig.cx, szWEOrig.cy, NULL);
 60
 61    return S_OK;
 62}
 63void CBullsEye::DrawBullsEye(ATL_DRAWINFO &di) {
 64    HDC hdc = di.hdcDraw;
 65
 66    // Create brushes as needed
 67    ...
 68
 69    // Compute the width of a ring
 70    short sRingCount;
 71    HRESULT hr = get_RingCount( &sRingCount );
 72    ATLASSERT( SUCCEEDED( hr ) );
 73
 74    int ringWidth = LOGWIDTH / (sRingCount * 2 - 1);
 75
 76    // Draw the border between rings using the border pen
 77    HPEN hOldPen = (HPEN)SelectObject( hdc, m_borderPen );
 78
 79    HBRUSH hOldBrush = 0;
 80
 81    for( short i = sRingCount - 1; i >= 0; --i ) {
 82        // Draw the current ring
 83        ...
 84
 85        // Set the correct ring color
 86        HBRUSH hBrush = ( HBRUSH )::SelectObject( hdc, ringBrush );
 87        // First time through, save the original brush
 88        if( hOldBrush == 0 ) {
 89            hOldBrush = hBrush;
 90        }
 91        ...
 92    }
 93
 94    // When optimized drawing not in effect,
 95    // restore the original pen and brush
 96    if( !di.bOptimize ) {
 97        ::SelectObject( hdc, hOldPen );
 98        ::SelectObject( hdc, hOldBrush );
 99    }
100}

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:

1class ATL_NO_VTABLE CBullsEye :
2    ...
3    // Persistence
4    public IPersistStreamInitImpl<CBullsEye>,
5    public IPersistStorageImpl<CBullsEye>,
6    public IPersistPropertyBagImpl<CBullsEye>,
7};

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.

1BEGIN_COM_MAP(CBullsEye)
2    ...
3    // Persistence
4    COM_INTERFACE_ENTRY(IPersistStreamInit)
5    COM_INTERFACE_ENTRY2(IPersist, IPersistStreamInit)
6    COM_INTERFACE_ENTRY(IPersistStorage)
7    COM_INTERFACE_ENTRY(IPersistPropertyBag)
8END_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:

 1BEGIN_PROP_MAP(CBullsEye)
 2  PROP_DATA_ENTRY("_cx", m_sizeExtent.cx, VT_UI4)
 3  PROP_DATA_ENTRY("_cy", m_sizeExtent.cy, VT_UI4)
 4  PROP_ENTRY("BackStyle",      DISPID_BACKSTYLE,
 5    CLSID_BullsEyePropPage)
 6  PROP_ENTRY("Beep",           DISPID_BEEP,
 7    CLSID_BullsEyePropPage)
 8  PROP_ENTRY("Enabled",        DISPID_ENABLED,
 9    CLSID_BullsEyePropPage)
10  PROP_ENTRY("RingCount",      DISPID_RINGCOUNT,
11    CLSID_BullsEyePropPage)
12  PROP_ENTRY("AlternateColor", DISPID_ALTERNATECOLOR,
13    CLSID_StockColorPage)
14  PROP_ENTRY("BackColor",      DISPID_BACKCOLOR,
15    CLSID_StockColorPage)
16  PROP_ENTRY("CenterColor",    DISPID_CENTERCOLOR,
17    CLSID_StockColorPage)
18  PROP_ENTRY("ForeColor",      DISPID_FORECOLOR,
19    CLSID_StockColorPage)
20END_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. [4]

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:

 1HRESULT CBullsEye::IPersistPropertyBag_Load(
 2  LPPROPERTYBAG pPropBag,
 3  LPERRORLOG pErrorLog,
 4  ATL_PROPMAP_ENTRY* pMap) {
 5  if (NULL == pPropBag) return E_POINTER;
 6
 7  // Load the properties described in the PROP_MAP
 8  HRESULT hr =
 9    IPersistPropertyBagImpl<CBullsEye>::IPersistPropertyBag_Load(
10      pPropBag, pErrorLog, pMap);
11  if (SUCCEEDED(hr))
12    m_bRequiresSave = FALSE;
13
14  if (FAILED (hr)) return hr;
15
16  // Load the indexed property - RingValues
17
18  // Get the number of rings
19  short sRingCount;
20  get_RingCount (&sRingCount);
21
22  // For each ring, read its value
23  for (short nIndex = 1; nIndex <= sRingCount; nIndex++) {
24
25    // Create the base property name
26    CComBSTR bstrName = OLESTR("RingValue");
27
28    // Create ring number as a string
29    CComVariant vRingNumber = nIndex;
30    hr = vRingNumber.ChangeType (VT_BSTR);
31    ATLASSERT (SUCCEEDED (hr));
32
33    // Concatenate the two strings to form property name
34    bstrName += vRingNumber.bstrVal;
35
36    // Read ring value from the property bag
37    CComVariant vValue = 0L;
38    hr = pPropBag->Read(bstrName, &vValue, pErrorLog);
39    ATLASSERT (SUCCEEDED (hr));
40    ATLASSERT (VT_I4 == vValue.vt);
41
42    if (FAILED (hr)) {
43      hr = E_UNEXPECTED;
44      break;
45    }
46
47    // Set the ring value
48    put_RingValue (nIndex, vValue.lVal);
49  }
50
51  if (SUCCEEDED(hr)) m_bRequiresSave = TRUE;
52  return hr;
53}

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 properties``BackColor``, ForeColor, and Appearance it’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:

 1HRESULT CBullsEye::IQuickActivate_QuickActivate(
 2    QACONTAINER *pQACont,
 3    QACONTROL *pQACtrl) {
 4    m_clrForeColor = pQACont->colorFore;
 5    m_clrBackColor = pQACont->colorBack;
 6    m_nAppearance = (short) pQACont->dwAppearance;
 7    m_bAmbientsFetched = true;
 8
 9    HRESULT hr = CComControlBase::IQuickActivate_QuickActivate(
10        pQACont, pQACtrl);
11    return hr;
12}

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:

 1HRESULT CBullsEye::IOleObject_SetClientSite(
 2    IOleClientSite *pClientSite) {
 3    HRESULT hr =
 4        CComControlBase::IOleObject_SetClientSite(pClientSite);
 5    if (!m_bAmbientsFetched) {
 6        GetAmbientBackColor (m_clrBackColor);
 7        GetAmbientForeColor (m_clrForeColor);
 8        GetAmbientAppearance (m_nAppearance);
 9    }
10    return hr;
11}

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:

1BEGIN_CATEGORY_MAP(CBullsEye)
2  IMPLEMENTED_CATEGORY(CATID_ATLINTERNALS_SAMPLES)
3  IMPLEMENTED_CATEGORY(CATID_SafeForScripting)
4  IMPLEMENTED_CATEGORY(CATID_SafeForInitializing)
5END_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:

 1class ATL_NO_VTABLE CBullsEye :
 2  ...
 3  // Object safety support
 4  public IObjectSafetyImpl<CBullsEye,
 5    INTERFACESAFE_FOR_UNTRUSTED_CALLER |
 6    INTERFACESAFE_FOR_UNTRUSTED_DATA>,
 7  ...
 8
 9BEGIN_COM_MAP(CBullsEye)
10  // Object safety support
11  COM_INTERFACE_ENTRY(IObjectSafety)
12  ...
13END_COM_MAP()
14
15STDMETHODIMP(FormatHardDrive)( ) {
16  if( m_dwCurrentSafety == 0 ) {
17    // Container isn't asking for safe behavior, perform mayhem
18    ...
19    return S_OK; // We just erased a hard drive,
20                 // how could we not be ok?
21  }
22  else {
23    // Container has asked we play nice, so don't
24    // actually erase the drive
25    ...
26    return S_FALSE; // Ok, we succeeded, but we're
27                    // being a little grumpy
28}

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:

 1[
 2    object, local,
 3    uuid(4D07FC10-F931-11CE-B001-00AA006884E5),
 4    helpstring("ICategorizeProperties Interface"),
 5    pointer_default(unique)
 6]
 7interface ICategorizeProperties : IUnknown {
 8    typedef [public] int PROPCAT;
 9
10    const int PROPCAT_Nil        = -1;
11    const int PROPCAT_Misc       = -2;
12    const int PROPCAT_Font       = -3;
13    const int PROPCAT_Position   = -4;
14    const int PROPCAT_Appearance = -5;
15    const int PROPCAT_Behavior   = -6;
16    const int PROPCAT_Data       = -7;
17    const int PROPCAT_List       = -8;
18    const int PROPCAT_Text       = -9;
19    const int PROPCAT_Scale      = -10;
20    const int PROPCAT_DDE        = -11;
21
22    HRESULT MapPropertyToCategory([in] DISPID dispid,
23        [out] PROPCAT* ppropcat);
24    HRESULT GetCategoryName([in] PROPCAT propcat, [in] LCID lcid,
25        [out] BSTR* pbstrName);
26}

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.

 1const int PROPCAT_Scoring = 1;
 2
 3STDMETHODIMP CBullsEye::MapPropertyToCategory(
 4    /*[in]*/ DISPID dispid, /*[out]*/ PROPCAT* ppropcat) {
 5    if (NULL == ppropcat) return E_POINTER;
 6
 7    switch (dispid) {
 8    case DISPID_FORECOLOR:
 9    case DISPID_BACKCOLOR:
10    case DISPID_CENTERCOLOR:
11    case DISPID_ALTERNATECOLOR:
12    case DISPID_RINGCOUNT:
13    case DISPID_BACKSTYLE:
14        *ppropcat = PROPCAT_Appearance;
15        return S_OK;
16
17    case DISPID_BEEP:
18    case DISPID_ENABLED:
19        *ppropcat = PROPCAT_Behavior;
20        return S_OK;
21
22    case DISPID_RINGVALUE:
23        *ppropcat = PROPCAT_Scoring;
24        return S_OK;
25
26    default:
27        return E_FAIL;
28    }
29}

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.

 1STDMETHODIMP CBullsEye::GetCategoryName(/*[in]*/ PROPCAT propcat,
 2    /*[in]*/ LCID lcid,
 3    /*[out]*/ BSTR* pbstrName)
 4{
 5    if(PROPCAT_Scoring == propcat) {
 6        *pbstrName = ::SysAllocString(L"Scoring");
 7        return S_OK;
 8    }
 9    return E_FAIL;
10}

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. [5] 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:

 1STDMETHODIMP CBullsEye::GetDisplayString(DISPID dispid,
 2    BSTR *pBstr) {
 3    ATLTRACE2(atlTraceControls,2,
 4        _T("CBullsEye::GetDisplayString\n"));
 5    switch (dispid) {
 6    case DISPID_BEEP:
 7        if (VARIANT_TRUE == m_beep)
 8            *pBstr = SysAllocString (OLESTR("Yes"));
 9        else
10            *pBstr = SysAllocString (OLESTR("No"));
11
12        return *pBstr ? S_OK : E_OUTOFMEMORY;
13
14    case DISPID_BACKSTYLE:
15        if (1 == m_nBackStyle)
16            *pBstr = SysAllocString (OLESTR("Opaque"));
17        else
18            *pBstr = SysAllocString (OLESTR("Transparent"));
19
20        return *pBstr ? S_OK : E_OUTOFMEMORY;
21
22    case DISPID_ALTERNATECOLOR: // Make VB apply default
23    case DISPID_BACKCOLOR:      // formatting for these
24    case DISPID_CENTERCOLOR:    // color properties.
25    case DISPID_FORECOLOR:      // Otherwise it displays color
26                                // values in decimal and doesn't
27                                // draw the color sample
28                                // correctly
29        return S_FALSE;  // This is an undocumented return
30                         // value that works...
31    }
32    return
33        IPerPropertyBrowsingImpl<CBullsEye>::GetDisplayString(
34            dispid, pBstr);
35}

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:

  1/************************/
  2/* GetPredefinedStrings */
  3/************************/
  4
  5#define DIM(a) (sizeof(a)/sizeof(a[0]))
  6
  7static const LPCOLESTR    rszBeepStrings [] = {
  8  OLESTR("Yes, make noise"), OLESTR("No, be mute") };
  9static const DWORD        rdwBeepCookies [] = { 0, 1 };
 10static const VARIANT_BOOL  rvbBeepValues [] = {
 11  VARIANT_TRUE, VARIANT_FALSE };
 12static const UINT cBeepStrings = DIM(rszBeepStrings);
 13static const UINT cBeepCookies = DIM(rdwBeepCookies);
 14static const UINT cBeepValues  = DIM(rvbBeepValues);
 15
 16static const LPCOLESTR    rszBackStyleStrings [] = {
 17  OLESTR("Opaque"), OLESTR("Transparent") };
 18static const DWORD        rdwBackStyleCookies [] = { 0, 1 };
 19static const long          rvbBackStyleValues [] = { 1, 0 };
 20
 21static const UINT cBackStyleStrings = DIM(rszBackStyleStrings);
 22static const UINT cBackStyleCookies = DIM(rdwBackStyleCookies);
 23static const UINT cBackStyleValues  = DIM(rvbBackStyleValues);
 24
 25STDMETHODIMP CBullsEye::GetPredefinedStrings(
 26  /*[in]*/ DISPID dispid,
 27  /*[out]*/ CALPOLESTR *pcaStringsOut,
 28  /*[out]*/ CADWORD *pcaCookiesOut) {
 29   ATLTRACE2(atlTraceControls,2,
 30    _T("CBullsEye::GetPredefinedStrings\n"));
 31  if (NULL == pcaStringsOut || NULL == pcaCookiesOut)
 32    return E_POINTER;
 33
 34  ATLASSERT (cBeepStrings == cBeepCookies);
 35  ATLASSERT (cBeepStrings == cBeepValues);
 36
 37  ATLASSERT (cBackStyleStrings == cBackStyleCookies);
 38  ATLASSERT (cBackStyleStrings == cBackStyleValues);
 39
 40  pcaStringsOut->cElems = 0;
 41  pcaStringsOut->pElems = NULL;
 42  pcaCookiesOut->cElems = 0;
 43  pcaCookiesOut->pElems = NULL;
 44
 45  HRESULT hr = S_OK;
 46  switch (dispid) {
 47  case DISPID_BEEP:
 48    hr = SetStrings (cBeepValues, rszBeepStrings, pcaStringsOut);
 49    if (FAILED (hr)) return hr;
 50    return SetCookies (cBeepValues, rdwBeepCookies,
 51      pcaCookiesOut);
 52
 53  case DISPID_BACKSTYLE:
 54    hr = SetStrings (cBackStyleValues, rszBackStyleStrings,
 55      pcaStringsOut);
 56    if (FAILED (hr)) return hr;
 57    return SetCookies (cBackStyleValues, rdwBackStyleCookies,
 58      pcaCookiesOut);
 59  }
 60  return
 61    IPerPropertyBrowsingImpl<CBullsEye>::GetPredefinedStrings(
 62      dispid, pcaStringsOut, pcaCookiesOut);
 63}
 64
 65/**********************/
 66/* GetPredefinedValue */
 67/**********************/
 68
 69STDMETHODIMP CBullsEye::GetPredefinedValue(
 70  DISPID dispid, DWORD dwCookie, VARIANT* pVarOut) {
 71  if (NULL == pVarOut) return E_POINTER;
 72
 73  ULONG i;
 74  switch (dispid) {
 75  case DISPID_BEEP:
 76    // Walk through cookie array looking for matching value
 77    for (i = 0; i < cBeepCookies; i++) {
 78      if (rdwBeepCookies[i] == dwCookie) {
 79        pVarOut->vt = VT_BOOL;
 80        pVarOut->boolVal = rvbBeepValues [i];
 81        return S_OK;
 82      }
 83    }
 84    return E_INVALIDARG;
 85
 86  case DISPID_BACKSTYLE:
 87    // Walk through cookie array looking for matching value
 88    for (i = 0; i < cBackStyleCookies; i++) {
 89      if (rdwBackStyleCookies[i] == dwCookie) {
 90        pVarOut->vt = VT_I4;
 91        pVarOut->lVal = rvbBackStyleValues [i];
 92        return S_OK;
 93      }
 94    }
 95    return E_INVALIDARG;
 96  }
 97
 98  return
 99    IPerPropertyBrowsingImpl<CBullsEye>::GetPredefinedValue(
100      dispid, dwCookie, pVarOut);
101}

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:

1PROP_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:

1STDMETHODIMP CBullsEye::GetControlInfo(CONTROLINFO *pci) {
2    if(!pci) return E_POINTER;
3    pci->hAccel  = NULL;
4    pci->cAccel  = 0;
5    pci->dwFlags = 0;
6    return S_OK;
7}

The default implementation of TRanslateAccelerator looks like this:

 1STDMETHOD(TranslateAccelerator)(LPMSG pMsg) {
 2  T* pT = static_cast<T*>(this);
 3  HRESULT hRet = S_OK;
 4  MSG msg = *pMsg;
 5  if (pT->PreTranslateAccelerator(&msg, hRet))
 6  return hRet;
 7
 8  CComPtr<IOleControlSite> spCtlSite;
 9  hRet = pT->InternalGetSite(__uuidof(IOleControlSite),
10    (void**)&spCtlSite);
11  if (SUCCEEDED(hRet)) {
12    if (spCtlSite != NULL) {
13      DWORD dwKeyMod = 0;
14      if (::GetKeyState(VK_SHIFT) < 0)
15        dwKeyMod += 1; // KEYMOD_SHIFT
16      if (::GetKeyState(VK_CONTROL) < 0)
17        dwKeyMod += 2; // KEYMOD_CONTROL
18      if (::GetKeyState(VK_MENU) < 0)
19        dwKeyMod += 4; // KEYMOD_ALT
20      hRet = spCtlSite->TranslateAccelerator(&msg, dwKeyMod);
21    }
22    else
23      hRet = S_FALSE;
24  }
25  return (hRet == S_OK) ? S_OK : S_FALSE;
26}

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:

1STDMETHOD(GetMiscStatus)(DWORD dwAspect, DWORD *pdwStatus) {
2  ATLTRACE2(atlTraceControls,2,
3    _T("IOleObjectImpl::GetMiscStatus\n"));
4  return OleRegGetMiscStatus(T::GetObjectCLSID(),
5    dwAspect, pdwStatus);
6}

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.

1ForceRemove {7DC59CC5-36C0-11D2-AC05-00A0C9C8E50D} =
2s 'BullsEye Class' {
3    ...
4    'MiscStatus' = s '0'
5    {
6        '1' = s '147857'
7    }
8}

Alternatively, BullsEye can override the GetMiscStatus method and provide the desired value; the Registry entry then would not be needed:

 1STDMETHODIMP CBullsEye::GetMiscStatus (
 2    DWORD dwAspect, DWORD *pdwStatus) {
 3    if (NULL == pdwStatus) return E_POINTER;
 4
 5    *pdwStatus =
 6           OLEMISC_SETCLIENTSITEFIRST |
 7           OLEMISC_ACTIVATEWHENVISIBLE |
 8           OLEMISC_INSIDEOUT |
 9           OLEMISC_CANTLINKINSIDE |
10           OLEMISC_RECOMPOSEONRESIZE |
11           OLEMISC_NOUIACTIVATE ;
12
13    return S_OK;
14}

The OLEMISC_NOUIACTIVATE setting prevents Visual Basic from giving the BullsEye control the input focus when a user attempts to tab into the control.

Summary

ActiveX controls use much of the functionality discussed so far. An ATL-based control typically supports properties and methods using ATL’s IDispatchImpl support. In addition, a control typically fires events; therefore, it often derives from IConnectionPointContainerImpl and uses a connection point proxy-generated class (IconnectionPointImpl derived) for each connection point. A control generally requires persistence support, so it uses one or more of the persistence-implementation classes: IPersistSrteamInitImpl, IPersistPropertyBagImpl, and IPersistStorageImpl.

In addition, many controls should implement numerous other interfaces so that they integrate well with various control containers, such as Visual Basic. In the next chapter, you learn how ATL supports hosting ActiveX controls, and how to write a control container using ATL.