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
andRefresh
that a container can access via the control’sIDispatch
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 theIConnectionPointContainer
andIProvideClassInfo2
interfaces, as well as a connection point that makes calls to the eventdispinterface
.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 thebindable
orrequestedit
attributes, as appropriate.On-demand rendering of a view of the object via the
IViewObject
,IView-Object2
, andIViewObjectEx
interfaces.Standard OLE control functionality, as provided by the
IOleControl
interface, and in-place activation using theIOleObject
andIOleInPlaceActiveObject
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 usingIPersistPropertyBag
because Visual Basic and Internet Explorer prefer this medium. Some controls additionally supportIPersistStorage
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
, andIDropTarget
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’sISpecifyPropertyPages
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

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

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

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``

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

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 |
---|---|---|---|
|
|
|
Background color |
|
|
|
Background style, transparent or opaque |
|
|
|
Enabled status, |
|
|
|
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 |
---|---|---|
|
|
Displays the control’s Help About dialog box |
|
|
Simulates a mouse click on the control |
|
|
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 |
---|---|---|---|
|
|
|
Returns the |
|
|
|
Gets/sets the color of the alternate (even) rings |
|
|
|
Enables/disables sound effects for the control |
|
|
|
Gets/sets the color of the center (odd) rings |
|
|
|
Returns the |
|
|
|
Gets/sets the number of rings |
|
|
|
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 |
---|---|---|
|
|
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. |
|
|
This event follows the |
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

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

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.
ATL 3.0 used the term “Lite control” for what’s now handled by the Minimal Control check box. Some documentation and names in the ATL source still use the “Lite” term.
Figure 11.7. Interfaces for the ``BullsEye`` control

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

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

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 |
---|---|---|---|---|---|---|
|
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
|
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
|
✓ |
✓ |
✓ |
✓ |
||
|
✓ |
✓ |
||||
|
SP |
SP |
SP |
SP |
SP | SP |
|
|
CP |
CP |
CP |
CP |
CP |
CP |
|
✓ |
✓ |
✓ |
|||
|
No SP and dual |
No SP and dual |
No SP and dual |
✓ |
No SP and dual |
✓ |
|
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
|
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
|
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
|
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
|
✓ |
✓ |
✓ |
|||
|
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
|
CP |
CP |
CP |
|||
|
✓ |
✓ |
✓ |
|||
|
✓ |
✓ |
✓ |
|||
|
✓ |
✓ |
✓ |
|||
|
SEI |
SEI |
SEI |
SEI |
SEI |
SEI |
|
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
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 |
|
AUTOSIZE |
|
BACKCOLOR |
|
BACKSTYLE |
|
BORDERCOLOR |
|
BORDERSTYLE |
|
BORDERVISIBLE |
|
BORDERWIDTH |
|
CAPTION |
|
DRAWMODE |
|
DRAWSTYLE |
|
DRAWWIDTH |
|
ENABLED |
|
FILLCOLOR |
|
FILLSTYLE |
|
FONT |
|
FORECOLOR |
|
HWND |
|
MOUSEICON |
|
MOUSEPOINTER |
|
PICTURE |
|
READYSTATE |
|
TABSTOP |
|
TEXT |
|
VALID |
|
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 namedget_fname
, and the put method will be namedput_fname
. For example, whenfname
isEnabled
, the method names areput_Enabled
andget_Enabled
.pname
. Specifies the name of the member variable that will hold the state of the stock property. For example, ifpname
isbEnabled
, the macro-created get and put methods will referencem_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!

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
callsCBullsEye::__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
toCFireProp-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
callsCBullsEye::__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
toCFakeFireProp-NotifyEvent
.Therefore, you actually call the
CBullsEye::CFakeFirePropNotifyEvent::FireOnChanged
function, which simply returnsS_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, ¶ms,
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:
The control has a window that receives a
WM_PAINT
message. A control handles this request inCComControlBase::OnPaint
.The control is windowless, and the container’s window receives a
WM_PAINT
message encompassing the area that the control occupies. A control handles this request inCComControlBase::IViewObject_Draw
.The container requests that the control render its image into a metafile. A control handles this request in
CComControlBase::IDataObject_GetData
.
Although all three types of rendering requests
arrive at the control via different mechanisms, the ATL control
implementation classes eventually forward the requests to a
control’s OnDrawAdvanced
method.
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.
More specifically, those settings that get saved and restored by the SaveDC/RestoreDC GDI calls.
When
IDataObject_GetData
callsOnDrawAdvanced
to retrieve a rendering of the control in a metafile,IDataObject_GetData
saves the device context state, callsOnDrawAdvanced
, and then restores the device context state. Therefore,IDataObject_GetData
sets thebOptimize
flag totrUE
.When
OnPaint
callsOnDrawAdvanced
to have the control render to its window, thebOptimize
flag is set toFALSE
.When
IViewObject_Draw
callsOnDrawAdvanced
to have the control render to the container’s window, thebOptimize
flag is set toTRUE
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 theBackStyle
stock property. WhenBackStyle
is1
(opaque), the control uses the background color to fill the area around the bull’s eye. WhenBackStyle
is0
(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]
For a discussion of why this is a bad thing, see Chapter 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.
Visual
Basic first queries a control for IPerPropertyBrowsing
to
retrieve the display strings for a property. When that query fails,
Visual Basic loads the type library and retrieves the enumerated
values that were specified in the IDL for the property, if
available. Failing that, Visual Basic displays 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.