previous page
next page

Developing the BullsEye Control Step by Step

Stock Properties and Methods

Updating Stock Properties and Methods in the IDL

Your IDL file describing the control's default dispatch interface must contain an entry for each stock property accessor method and all stock methods you support. The Add ATL Control Wizard generates the method definitions for those stock properties added using the wizard dialog boxes. Listing 11.6 shows the method definitions for the BullsEye properties.

The Add ATL Control Wizard has no support for stock methods. You must add any stock methods explicitly to the default dispatch interface definition in your IDL file. Only three stock methods (AboutBox, DoClick, and Refresh) are defined, and BullsEye supports them all. I've added the following lines to the IBullsEye interface definition:

interface IBullsEye : IDispatch {
  ...
  [id(DISPID_ABOUTBOX)] HRESULT AboutBox();
  [id(DISPID_DOCLICK)] HRESULT DoClick();
  [id(DISPID_REFRESH)] HRESULT Refresh();
};

Listing 11.1, shown earlier in this chapter, contains the complete definition of the IBullsEye interface with all these changes.

Implementing Stock Properties and Methods Using CStockPropImpl

The CStockPropImpl class contains an implementation of every stock property you can choose from the Stock Properties page in the ATL Object Wizard. A control derives from CStockPropImpl when it wants an implementation of any of the stock properties. The declaration of the template class looks like this:

template < class T, class InterfaceName,              
    const IID* piid = &_ATL_IIDOF(InterfaceName),     
    const GUID* plibid = &CAtlModule::m_libid,        
    WORD wMajor = 1, WORD wMinor = 0,                 
    class tihclass = CComTypeInfoHolder>              
class ATL_NO_VTABLE CStockPropImpl :                  
    public IDispatchImpl< InterfaceName, piid, plibid,
    wMajor, wMinor, tihclass >                        

The class T parameter is the name of your control class. The InterfaceName parameter is the name of the dual interface defining the stock property propget and propput methods. The CStockPropImpl class implements these accessor methods. The piid parameter is a pointer to the IID for the InterfaceName interface. The plibid parameter is a pointer to the GUID of the type library that contains a description of the InterfaceName interface.

The CBullsEye class implements its stock properties using CStockPropImpl like this:

class ATL_NO_VTABLE CBullsEye :
    public CStockPropImpl<CBullsEye, IBullsEye, &IID_IBullsEye,
                      &LIBID_ATLINTERNALSLib>,
    ...

The CStockPropImpl class contains an implementation of the property accessor (get and put) methods for all stock properties. These methods notify and synchronize with the control's container when any stock property changes.

Most controls don't need support for all the possible stock properties. However, the CStockPropImpl base class contains supporting code for all stock properties. This code needs a data member for each property. ATL expects your deriving class, the control class, to provide the data members for only the stock properties that your control supports. You must name these data members the same variable name as used by the CStockPropImpl class. Table 11.6 lists the appropriate name for each supported stock property.

Table 11.6. Stock Properties Supported by CStockPropImpl

Stock Property

Data Member

APPEARANCE

m_nAppearance

AUTOSIZE

m_bAutoSize

BACKCOLOR

m_clrBackColor

BACKSTYLE

m_nBackStyle

BORDERCOLOR

m_clrBorderColor

BORDERSTYLE

m_nBorderStyle

BORDERVISIBLE

m_bBorderVisible

BORDERWIDTH

m_nBorderWidth

CAPTION

m_bstrCaption

DRAWMODE

m_nDrawMode

DRAWSTYLE

m_nDrawStyle

DRAWWIDTH

m_nDrawWidth

ENABLED

m_bEnabled

FILLCOLOR

m_clrFillColor

FILLSTYLE

m_nFillStyle

FONT

m_pFont

FORECOLOR

m_clrForeColor

HWND

m_hWnd

MOUSEICON

m_pMouseIcon

MOUSEPOINTER

m_nMousePointer

PICTURE

m_pPicture

READYSTATE

m_nReadyState

TABSTOP

m_bTabStop

TEXT

m_bstrText

VALID

m_bValid


The CStockPropImpl class contains property-accessor methods for all these properties. In theory, that means you must provide member variables for every single one of them in your class, to avoid getting an "Undefined variable" compile error. Luckily for us, the ATL authors took advantage of one of the new extensions to C++:

class CStockPropImpl {                                             
  ...                                                              
  HRESULT STDMETHODCALLTYPE get_Picture(IPictureDisp** ppPicture) {
    __if_exists(T::m_pPicture) {                                   
      ATLTRACE(atlTraceControls,2,                                 
        _T("CStockPropImpl::get_Picture\n"));                      
      ATLASSERT(ppPicture != NULL);                                
      if (ppPicture == NULL)                                       
        return E_POINTER;                                          
                                                                   
                                                                   
      T* pT = (T*) this;                                           
      *ppPicture = pT->m_pPicture;                                 
      if (*ppPicture != NULL)                                      
          (*ppPicture)->AddRef();                                  
    }                                                              
    return S_OK;                                                   
  }                                                                
  ...                                                              
};                                                                 

Notice the __if_exists block. You saw this back in Chapter 4, "Objects in ATL"; here it's used to make sure the property accessor logic is compiled in only if the underlying variable has been declared in your control. This way, you don't have to worry about size bloat to store stock properties you're not using. The downside of this space optimization is this: When you add a member variable to your control class to hold a stock property and you misspell the member variable name, you receive no compilation errors. The code in CStockPropImpl does nothing and returns S_OK.

The Add ATL Control Wizard generates the proper member variables in your control's class when you add a stock property initially. For example, here are the member variables generated for the stock properties in the CBullsEye class:

class CBullsEye : ... {
  ...
  OLE_COLOR m_clrBackColor;
  LONG      m_nBackStyle;
  BOOL      m_bEnabled;
  OLE_COLOR m_clrForeColor;
};

CStockPropImpl implements explicit put and get methods for the stock properties that are interface pointers, including FONT, MOUSEICON, and PICTURE. It also implements a get method for the HWND stock property. For each other stock property, CStockPropImpl invokes one of three macros that expand to a standard put and get method for the property: These macros are IMPLEMENT_STOCKPROP, IMPLEMENT_BOOL_STOCKPROP, and IMPLEMENT_BSTR_STOCKPROP.

The IMPLEMENT_STOCKPROP (type, frame, pname, dispid) macro's parameters are as follows:

  • type. The data type for the stock property you want an implementation for.

  • fname. The function name for the get and put methods. The get method will be named get_fname, and the put method will be named put_fname. For example, when fname is Enabled, the method names are put_Enabled and get_Enabled.

  • pname. Specifies the name of the member variable that will hold the state of the stock property. For example, if pname is bEnabled, the macro-created get and put methods will reference m_bEnabled.

  • dispid. The dispatch ID for the stock property.

The IMPLEMENT_BOOL_STOCKPROP(fname, pname, dispid) macro implements a stock Boolean property's accessor methods. It has the same attributes as listed for the IMPLEMENT_STOCKPROP macro, except that the get method tests the value of the data member containing the property and returns VARIANT_TRUE or VARIANT_FALSE instead of returning the value.

The IMPLEMENT_BSTR_STOCKPROP(fname, pname, dispid) macro implements a stock text property's accessor methods using a BSTR.

Let's look at the implementation of the IMPLEMENT_STOCKPROP macro. The ATL code illustrates a couple other issues that are worth noting and that apply to all stock properties.

#define IMPLEMENT_STOCKPROP(type, fname, pname, dispid) \    
    HRESULT STDMETHODCALLTYPE put_##fname(type pname) { \    
        __if_exists(T::m_##pname) { \                        
            ATLTRACE(ATL::atlTraceControls,2, \              
                _T("CStockPropImpl::put_%s\n"), #fname); \   
            T* pT = (T*) this; \                             
            if (pT->m_nFreezeEvents == 0 && \                
                pT->FireOnRequestEdit(dispid) == S_FALSE) \  
                return S_FALSE; \                            
            pT->m_##pname = pname; \                         
            pT->m_bRequiresSave = TRUE; \                    
            if (pT->m_nFreezeEvents == 0) \                  
                pT->FireOnChanged(dispid); \                 
            __if_exists(T::On##fname##Changed) { \           
                pT->On##fname##Changed(); \                  
            } \                                              
            pT->FireViewChange(); \                          
            pT->SendOnDataChange(NULL); \                    
        } \                                                  
        return S_OK; \                                       
    } \                                                      
    HRESULT STDMETHODCALLTYPE get_##fname(type* p##pname) { \
        __if_exists(T::m_##pname) { \                        
            ATLTRACE(ATL::atlTraceControls,2, \              
                _T("CStockPropImpl::get_%s\n"), #fname); \   
            ATLASSERT(p##pname != NULL); \                   
            if (p##pname == NULL) \                          
                return E_POINTER; \                          
            T* pT = (T*) this; \                             
            *p##pname = pT->m_##pname; \                     
        } \                                                  
        return S_OK; \                                       
    }                                                        

First, notice that the put method fires an OnRequestEdit and an OnChanged event notification to the control's container before and after, respectively, changing the value of a stock property. Second, the put method fires the OnRequestEdit and OnChanged events after checking a control's freeze event. When a control's freeze event count (maintained in CComControlBase in the m_nFreezeEvents member variable) is nonzero, a control should hold off firing events or discard them completely. If this rule isn't followed, some containers will break.

For example, the Test Container application shipped with Visual C++ 6.0 crashes when a control fires change notifications in its FinalConstruct method. A control should be capable of calling FreezeEvents(TRUE) in FinalConstruct to disable change notifications, initialize its properties using the put methods, and then call FreezeEvents (FALSE) to enable change notifications if they were previously enabled

Occasionally, you'll decide to support additional stock properties after creating the initial source code. The wizard lacks support for adding features to your class after the initial code generation, so you'll have to make the previously described changes to your code manually.

Finally, often you'll want to do some work beyond what the stock property put functions perform. For example, the CBullsEye class needs to know whenever the background color changes so that it can delete the old background brush and schedule the rendering logic to create a new background brush. In the middle of the put method generated by the IMPLEMENT_STOCKPROP macro, there's this code:

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

When you added the stock properties to the control, the Add ATL Control Wizard also added methods called On<propname>Changed. If there is an appropriately named method in your class (OnBackColorChanged for the BackColor property, for example), the stock property put method calls that method when the property value changes. This is useful when you need to do things beyond just storing the value. For example, the CBullsEye control needs to know when the background color changes, so it can delete the old background brush and schedule the rendering logic to create a new background brush.

Custom Properties and Methods

Adding Custom Properties and Methods to the IDL

In addition to stock properties, your control's default dispatch interface must contain an entry for the property get and put methods for each custom control property, as well as all the stock and custom methods you support. The Add ATL Control Wizard doesn't currently support stock methods, so you add them to your class as if they were custom methods (which, in fact, they are, except that you don't get to choose the function signatures or the DISPID).

To add a custom property, you must edit the IDL file and then add the corresponding methods to your C++ class. You can use Class view to add the properties. Right-click the interface and select Add, Add Property. This adds the appropriate definitions to the IDL file. Unfortunately, unlike in previous versions of Visual Studio, this step only updates the IDL file; you have to manually add the property get/put methods to your C++ class. At least the compiler gives you the helpful "Cannot instantiate abstract class" error message if you forget or get the signature wrong.

The BullsEye control supports the stock methods and custom properties listed in Tables 11.2 and 11.3, respectively. Listing 11.1 contains the complete definition for the IBullsEye interface, but here's an excerpt showing the definition of the CenterColor custom property and the AboutBox stock method.

interface IBullsEye : IDispatch {
...
    [propput, bindable, requestedit, id(DISPID_CENTERCOLOR)]
      HRESULT CenterColor([in] OLE_COLOR newVal);

    [propget, bindable, requestedit, id(DISPID_CENTERCOLOR)]
      HRESULT CenterColor([out, retval] OLE_COLOR *pVal);

    [id(DISPID_ABOUTBOX)] HRESULT AboutBox();
...
};

Note that Class view does not let you define a symbol for the DISPID; it takes only integers in the dialog box. It's usually a good idea to go back into the IDL afterward and define a symbol, as we've already done in the IDL file for the BullsEye control.

Implementing Custom Properties and Stock and Custom Methods

You need to add a function prototype to your control class for each method added to the IDL in the previous step. For the previous custom property and stock method, I added the following function prototypes to the CBullsEye class:

class CBullsEye : ... {
  ...
  STDMETHODIMP get_CenterColor(/*[out, retval]*/OLE_COLOR *pVal);
  STDMETHODIMP put_CenterColor(/*[in]*/ OLE_COLOR newVal);
  STDMETHODIMP AboutBox();
  ...
};

Declaring the Function Prototypes

Note that you must declare each interface member function as using the STDMETHODIMP calling convention. A system header file defines this macro to be the appropriate calling convention for COM interface methods on a given operating system. This calling convention does change among various operating systems. Because you are using the macro instead of explicitly writing its expansion, your code is more portable across operating systems. On the Win32 operating systems, the macro expands to hrESULT __stdcall.

The code that the ATL wizards generate incorrectly uses the STDMETHOD macro. On Win32 operating systems, this macro expands as virtual HRESULT __stdcall, which just happens to work. It doesn't necessarily work on other operating systems that support COM.

Basically, STDMETHOD should be used only in the original definition of an interface; this is typically the MIDL-generated header. (However, MIDL doesn't use the macro; it simply generates its expansion instead.) When implementing an inter-face method in a class (which we are doing in CBullsEye), you should use the STDMETHODIMP macro.

You must manually make a couple changes to the method definitions. First, most properties should have the bindable and requestedit attributes. This is because the property put methods fire change notifications to a container before and after changing a property. Therefore, you need to change each method as shown in the following section.

Defining the Functions

The function implementations are all quite straightforward. The get_CenterColor method validates its argument and returns the value of the CenterColor property:

STDMETHODIMP CBullsEye::get_CenterColor(OLE_COLOR *pVal) {
    if (NULL == pVal) return E_POINTER;
    *pVal = m_clrCenterColor;
    return S_OK;
}

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

STDMETHODIMP CBullsEye::put_CenterColor(OLE_COLOR newVal) {
  if (m_clrCenterColor == newVal) return S_OK;

  if (!m_nFreezeEvents)
    if (FireOnRequestEdit(DISPID_CENTERCOLOR) == S_FALSE)
      return S_FALSE;

  m_clrCenterColor = newVal;           // Save new color
  ::DeleteObject (m_centerBrush);      // Clear old brush color
  m_centerBrush = NULL;
  m_bRequiresSave = TRUE;              // Set dirty flag
  if (!m_nFreezeEvents)
    FireOnChanged(DISPID_CENTERCOLOR); // Notify container
  FireViewChange();                    // Request redraw
  SendOnDataChange(NULL);              // Notify advise sinks

  return S_OK;
}

First, the method checks to see if the new value is the same as the current value of the CenterColor property. If so, the value isn't changing, so we exit quickly. Then, as in the stock property code, it properly checks to see if the container presently doesn't want to receive eventsthat is, if the freeze events count is nonzero.

When the container has not frozen events, the put_CenterColor method fires the OnRequestEdit event to ask the container for permission to change the CenterColor property. When the container refuses the change, put_CenterColor returns S_FALSE.

When the container grants permission, put_CenterColor updates the member variable in the control that contains the color. It also changes some values that cause the control's rendering code to use the new color the next time the control redraws.

After the method changes the property, it sets the control's dirty flag (m_b-RequiresSave) to remember that the state of the control now needs to be saved. The various persistence implementations check this flag when executing their IsDirty method.

Next, the method fires the OnChanged event to notify the container of the property change, assuming that events are not frozen.

The CenterColor property affects the visual rendering (view) of the control. When a control changes such properties, the control should notify its container that the control's appearance has changed by calling the FireViewChange function. In response, eventually, the container asks the control to redraw itself. After that, the method notifies all advise sinks (which typically means the container) that the state (data) of the control has changed by calling SendOnDataChange.

Note that the state of a control changes independently of the control's view. Some control property changes, such as changes to CBullsEye's Beep property, have no effect on the appearance of the control, so the put_Beep method doesn't call FireViewChange.

At this point, the astute reader might have noticed the similarity between the code implementing the CenterColor property and the code generated by the IMPLEMENT_STOCKPROP macro. If your property logic is almost identical to that for the stock properties, you can save yourself a lot of typing by using the macro instead:

class CBullsEye : ... {
    ...
    void OnCenterColorChanged() {
        ::DeleteObject(m_centerBrush);
        m_centerBrush = 0;
        }

IMPLEMENT_STOCKPROP(OLE_COLOR, CenterColor,
    clrCenterColor, DISPID_CENTERCOLOR);
    ...
private:
    // Storage for the property value
    OLE_COLOR m_clrCenterColor;
    ...
};

If you're using IMPLEMENT_STOCKPROP this way, you can simply leave out the declarations for the get and put methods; the macro adds them for you. To do any custom logic you need in the property setter, you can use the OnXXXChanged method (as I did in the OnCenterColorChanged method).

There's a fair bit of irony in this. In ATL 3, IMPLEMENT_STOCKPROP was the documented way to implement properties, and it didn't work. To get a fully functional property, you had to write code like the implementation of put_CenterColor shown previously. In ATL 8, IMPLEMENT_STOCKPROP is now undocumented but works just fine for defining custom properties.

Finally, our final custom method, the stock AboutBox method, simply displays the About dialog box.

STDMETHODIMP CBullsEye::AboutBox() {
    CAboutDlg dlg;
    dlg.DoModal();
    return S_OK;
}

Stock and Custom Events

Adding Stock and Custom Events to the IDL

Your IDL file describing the control's default source interface must contain an entry for each stock and custom event method you support. As described previously, for maximum compatibility with all control containers, you should implement the default source interface as a dispatch interface. No current support exists in the IDE for adding event methods to a dispinterface, despite what it might look like. For example, in my current Class View window, there's an entry for _IBullsEye-Events, as shown in Figure 11.10.

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.

[ uuid(58D6D8CB-765D-4C59-A41F-BBA8C40F7A14),
  helpstring("_IBullsEyeEvents Interface")
]
dispinterface _IBullsEyeEvents {
    properties:
    methods:
    [id(DISPID_ONRINGHIT)]
    void OnRingHit(short ringNum);
    [id(DISPID_ONSCORECHANGED)]
    void OnScoreChanged(long ringValue);
};

You'll also want to ensure that the IDL correctly describes the BullsEye class itself. The BullsEye coclass definition in the library block of the IDL file should define the IBullsEye interface as the default interface for the control and the _IBullsEyeEvents dispatch interface as the default source interface.

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

This is what the wizard generates when you choose to implement connection points, but it never hurts to double-check.

Adding Connection Point Support for the Events

Many containers use the connection points protocol to hand the container's sink interface pointer to the event source (the control). Chapter 9, "Connection Points," discusses connection points in detail, so here I just summarize the steps needed for a control.

To support connection point events, a control must implement the IConnectionPointContainer interface as well as one IConnectionPoint interface for each outgoing (source) interface. Typically, most controls support two source interfaces: the control's default source dispatch interface (_IBullsEyeEvents for the BullsEye control) and the property change notification source interface (IPropertyNotifySink).

Implementing the IConnectionPointContainer Interface

When you initially create the source code for a control and select the Support Connection Points option, the ATL Object Wizard adds the IConnectionPointContainerImpl base class to your control class declaration. This is ATL's implementation of the IConnectionPointContainer interface. You need to add this base class explicitly if you decide to support connection points after creating the initial source code.

class ATL_NO_VTABLE CBullsEye :
    ...
    // Connection point container support
    public IConnectionPointContainerImpl<CBullsEye>,
    ...

You also need one connection point for each source interface that your control supports. ATL provides the IConnectionPointImpl class, which is described in depth in Chapter 9, as an implementation of the IConnectionPoint interface. Typically, you do not directly use this class; instead, you derive a new class from IConnectionPointImpl and customize the class by adding various event-firing methods. Your control will inherit from this derived class.

Supporting Property Change Notifications

ATL provides a specialization of IConnectionPointImpl, called IPropertyNotifySinkCP, that implements a connection point for the IPropertyNotifySink interface. The IPropertyNotifySinkCP class also defines the type definition __ATL_PROP_NOTIFY_EVENT_CLASS (note the double leading underscore) as an alias for the CFirePropNotifyEvent class.

template <class T, class CDV = CComDynamicUnkArray >               
class ATL_NO_VTABLE IPropertyNotifySinkCP :                        
    public IConnectionPointImpl<T, &IID_IPropertyNotifySink, CDV> {
public:                                                            
    typedef CFirePropNotifyEvent __ATL_PROP_NOTIFY_EVENT_CLASS;    
};                                                                 

When you use the ATL Object Wizard to create a full control that supports connection points, the wizard adds the IPropertyNotifySinkCP base class to your control:

class ATL_NO_VTABLE CBullsEye :
    ...
    public IPropertyNotifySinkCP<CBullsEye>,
    ...

Recall that a control's property put methods, for both custom and stock properties, call the FireOnRequestEdit and FireOnChanged functions to send property-change notifications. These methods are defined in the CComControl class like this:

template <class T, class WinBase = CWindowImpl< T > >         
class ATL_NO_VTABLE CComControl :                             
  public CComControlBase, public WinBase {                    
public:                                                       
  HRESULT FireOnRequestEdit(DISPID dispID) {                  
    T* pT = static_cast<T*>(this);                            
    return T::__ATL_PROP_NOTIFY_EVENT_CLASS::FireOnRequestEdit
     (pT->GetUnknown(), dispID);                              
}                                                             
  HRESULT FireOnChanged(DISPID dispID) {                      
    T* pT = static_cast<T*>(this);                            
    return T::__ATL_PROP_NOTIFY_EVENT_CLASS::FireOnChanged    
     (pT->GetUnknown(), dispID);                              
  }                                                           
  ...                                                         
};                                                            

Therefore, the call to FireOnChanged in a property put method of a CComControl-derived class actually is a call to the FireOnChanged of the class __ATL_PROP_NOTIFY_EVENT_CLASS (note the double leading underscore) within your actual control class. When you derive your control class from IPropertyNotifySinkCP, your control class inherits a typedef for _ATL_PROP_NOTIFY_EVENT_CLASS (note the single leading underscore).

typedef CFirePropNotifyEvent _ATL_PROP_NOTIFY_EVENT_CLASS;

The two types come together in the property map in your control class. The BEGIN_PROP_MAP macro defines the type __ATL_PROP_NOTIFY_EVENT_CLASS (note the double leading underscore) as equivalent to the type _ATL_PROP_NOTIFY_EVENT_CLASS (note the single leading underscore).

#define BEGIN_PROP_MAP(theClass) \                    
    __if_not_exists(__ATL_PROP_NOTIFY_EVENT_CLASS) { \
        typedef ATL::_ATL_PROP_NOTIFY_EVENT_CLASS     
        __ATL_PROP_NOTIFY_EVENT_CLASS; \              
    } \                                               
...                                                   

The __if_not_exists block in the BEGIN_PROP_MAP definition does the typedef only if the __ATL_PROP_NOTIFY_EVENT_CLASS isn't defined. This gives you the chance to override the event class by defining the typedef yourself, if you want.

In the BullsEye control, this means that when your property put method calls FireOnChanged, this is actually a call to your CComControl::FireOnChanged base class method.

  • FireOnChanged calls CBullsEye::__ATL_PROP_NOTIFY_EVENT_CLASS::FireOnChanged.

  • The property map aliases __ATL_PROP_NOTIFY_EVENT_CLASS (two leading underscores) to _ATL_PROP_NOTIFY_EVENT_CLASS (one leading underscore).

  • IPropertyNotifySinkCP aliases _ATL_PROP_NOTIFY_SINK_CLASS to CFireProp-NotifyEvent.

  • Therefore, you actually call the CBullsEye::CFirePropNotifyEvent::FireOnChanged function.

The CFirePropNotifyEvent class contains two static methods, FireOnRequest-Edit and FireOnChanged, that use your control's own connection point support to enumerate through all connections for the IPropertyNotifySink interface and call the OnRequestEdit and OnChanged methods, respectively, of each connection.

class CFirePropNotifyEvent {                                       
public:                                                            
  // Ask any objects sinking the IPropertyNotifySink               
  // notification if it is ok to edit a specified property         
  static HRESULT FireOnRequestEdit(IUnknown* pUnk, DISPID dispID) {
    CComQIPtr<IConnectionPointContainer,                           
      &__uuidof(IConnectionPointContainer)> pCPC(pUnk);            
    if (!pCPC) return S_OK;                                        
                                                                   
    CComPtr<IConnectionPoint> pCP;                                 
    pCPC->FindConnectionPoint(__uuidof(IPropertyNotifySink),       
      &pCP);                                                       
    if (!pCP) return S_OK;                                         
                                                                   
    CComPtr<IEnumConnections> pEnum;                               
    if (FAILED(pCP->EnumConnections(&pEnum)))                      
      return S_OK;                                                 
                                                                   
    CONNECTDATA cd;                                                
    while (pEnum->Next(1, &cd, NULL) == S_OK) {                    
      if (cd.pUnk) {                                               
        HRESULT hr = S_OK;                                         
        CComQIPtr<IPropertyNotifySink,                             
          &__uuidof(IPropertyNotifySink)> pSink(cd.pUnk);          
                                                                   
        if (pSink != NULL)                                         
          hr = pSink->OnRequestEdit(dispID);                       
                                                                   
        cd.pUnk->Release();                                        
        if (hr == S_FALSE) return S_FALSE;                         
      }                                                            
    }                                                              
    return S_OK;                                                   
}                                                                  
  // Notify any objects sinking the IPropertyNotifySink            
  // notification that a property has changed                      
  static HRESULT FireOnChanged(IUnknown* pUnk, DISPID dispID) {    
    CComQIPtr<IConnectionPointContainer,                           
      &__uuidof(IConnectionPointContainer)> pCPC(pUnk);            
    if (!pCPC) return S_OK;                                        
                                                                   
    CComPtr<IConnectionPoint> pCP;                                 
    pCPC->FindConnectionPoint(__uuidof(IPropertyNotifySink),       
      &pCP);                                                       
    if (!pCP) return S_OK;                                         
    CComPtr<IEnumConnections> pEnum;                               
                                                                   
    if (FAILED(pCP->EnumConnections(&pEnum)))                      
      return S_OK;                                                 
                                                                   
    CONNECTDATA cd;                                                
    while (pEnum->Next(1, &cd, NULL) == S_OK) {                    
      if (cd.pUnk) {                                               
        CComQIPtr<IPropertyNotifySink,                             
          &__uuidof(IPropertyNotifySink)> pSink(cd.pUnk);          
                                                                   
        if (pSink != NULL)                                         
          pSink->OnChanged(dispID);                                
        cd.pUnk->Release();                                        
      }                                                            
    }                                                              
    return S_OK;                                                   
  }                                                                
};                                                                 

This means that you must derive your control class from IPropertyNotifySinkCP to get the typedef that maps the FireOnRequestEdit and FireOnChanged methods in CComControl to the actual firing functions in CFirePropNotifyEvent.

When you don't derive from IPropertyNotifySinkCP, you can still call the FireOnRequestEdit and FireOnChanged methods in CComControl. This is because ATL defines a typedef for the symbol _ATL_PROP_NOTIFY_EVENT_CLASS at global scope:

typedef CFakeFirePropNotifyEvent _ATL_PROP_NOTIFY_EVENT_CLASS;

When your control derives from IPropertyNotifySinkCP, you inherit a definition for _ATL_PROP_NOTIFY_EVENT_CLASS that hides the global definition. When you don't derive from IPropertyNotifySinkCP, the compiler uses the global definition just given. The CFakeFirePropNotifyEvent class looks like this:

class CFakeFirePropNotifyEvent {                      
public:                                               
  static HRESULT FireOnRequestEdit(IUnknown* /*pUnk*/,
    DISPID /*dispID*/)                                
  { return S_OK; }                                    
                                                      
  static HRESULT FireOnChanged(IUnknown* /*pUnk*/,    
    DISPID /*dispID*/)                                
  { return S_OK; }                                    
};                                                    

In the BullsEye control, this means that when you don't derive from IPropertyNotifySinkCP and your property put method calls FireOnChanged:

  • This is actually a call to your CComControl::FireOnChanged base class method.

  • FireOnChanged calls CBullsEye::__ATL_PROP_NOTIFY_EVENT_CLASS::FireOnChanged.

  • The property map aliases __ATL_PROP_NOTIFY_EVENT_CLASS (two leading underscores) to _ATL_PROP_NOTIFY_EVENT_CLASS (one leading underscore).

  • The global typedef aliases _ATL_PROP_NOTOFY_SINK_CLASS to CFakeFireProp-NotifyEvent.

  • Therefore, you actually call the CBullsEye::CFakeFirePropNotifyEvent::FireOnChanged function, which simply returns S_OK.

Supporting the Control's Event Connection Point

A connection point (or more than one) is essential to most ActiveX controls. Without an outgoing interface, the host for your control has no way of knowing when it needs to react to a change in the control. Chapter 9 discussed the details on implementing connection points, so we won't repeat the details here.

You'll want to use a specialization of IConnectionPointImpl for each of your control's event interfaces. Typically, a control implements only one event interface because Visual Basic and scripting languages can hook up to only the default event interface. This is the interface you describe in your object's coclass definition with the [default, source] attributes. However, a custom C++ client to your control can connect to any of its source interfaces.

The specialized class derives from IConnectionPointImpl and adds the appropriate event-firing helper methods for your events. The easiest way to create a specialized connection point class is to use the Implement Connection Point menu item, described in Chapter 9.

Here's the specialized connection point class, CProxy_IBullsEyeEvents, generated by the wizard for the _IBullsEyeEvents dispatch interface:

#pragma once

template<class T>
class CProxy_IBullsEyeEvents :
  public IConnectionPointImpl<T, &__uuidof(_IBullsEyeEvents)> {
public:
  HRESULT Fire_OnRingHit(short ringNum) {
    HRESULT hr = S_OK;
    T * pThis = static_cast<T *>(this);
    int cConnections = m_vec.GetSize();

    for (int iConnection = 0;
      iConnection < cConnections; iConnection++) {
      pThis->Lock();
      CComPtr<IUnknown> punkConnection =
        m_vec.GetAt(iConnection);
      pThis->Unlock();

      IDispatch * pConnection =
        static_cast<IDispatch *>(punkConnection.p);

      if (pConnection) {
        CComVariant avarParams[1];
        avarParams[0] = ringNum;
        avarParams[0].vt = VT_I2;
        DISPPARAMS params = { avarParams, NULL, 1, 0 };
        hr = pConnection->Invoke(DISPID_ONRINGHIT, IID_NULL,
          LOCALE_USER_DEFAULT, DISPATCH_METHOD, &params,
          NULL, NULL, NULL);
      }
    }
    return hr;
  }

  HRESULT Fire_OnScoreChanged(long ringValue)
  {
    // Code similar to above deleted for clarity
  }
};

You use this class by adding it to the base class list of the control. Therefore, BullsEye now has two connection points in its base class list:

class ATL_NO_VTABLE CBullsEye :
    // events and property change notifications
    public CProxy_IBullsEyeEvents<CBullsEye>,
    public IPropertyNotifySinkCP<CBullsEye>,
    ...

Updating the Connection Map

Finally, the IConnectionPointContainerImpl class needs a table that associates source interface IIDs with the base class IConnectionPointImpl specialization that implements the connection point. You define this table in your control class using the BEGIN_CONNECTION_POINT_MAP, CONNECTION_POINT_ENTRY, and END_CON-NECTION_POINT_MAP macros, described in Chapter 9.

Here's the table for the CBullsEye class:

BEGIN_CONNECTION_POINT_MAP(CBullsEye)
    CONNECTION_POINT_ENTRY(DIID__IBullsEyeEvents)
    CONNECTION_POINT_ENTRY(IID_IPropertyNotifySink)
END_CONNECTION_POINT_MAP()

Supporting IProvideClassInfo2

Many containers, such as Visual Basic and Internet Explorer, use a control's IProvideClassInfo2 interface to determine the control's event interface. When a control doesn't support IProvideClassInfo2, these containers assume that the control doesn't source events and never establish a connection point to your control. Other containers, such as test container, don't use a control's IProvideClassInfo2 interface and browse a control's type information to determine the default source interface.

ATL provides an implementation of this interface in IProvideClassInfo2Impl. To use it, derive your control class from IProvideClassInfo2Impl. The IProvideClassInfo2 interface itself derives from the IProvideClassInfo interface, so when you update your control's interface map, you need to provide entries for both interfaces.

class ATL_NO_VTABLE CBullsEye :
  public IProvideClassInfo2Impl<&CLSID_BullsEye,
                                &DIID__IBullsEyeEvents,
                                &LIBID_ATLInternalsLib>,
  ...
{
  ...
  BEGIN_COM_MAP(CBullsEye)
    ...
    // Support for connection points
    COM_INTERFACE_ENTRY(IConnectionPointContainer)
    COM_INTERFACE_ENTRY(IProvideClassInfo2)
    COM_INTERFACE_ENTRY(IProvideClassInfo)
  END_COM_MAP()
  ...
};

On-Demand Rendering of Your Control's View

The BullsEye control now has stock properties and custom properties, correctly responds to QueryInterface calls, and implements an outgoing connection point. There's just one thing: Aren't controls supposed to be visual? It's time to talk about how ActiveX controls actually draw their UI.

A control must be capable of rendering its image when requested by its container. A control receives a rendering request in basically three different situations:

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

  2. The control is windowless, and the container's window receives a WM_PAINT message encompassing the area that the control occupies. A control handles this request in CComControlBase::IViewObject_Draw.

  3. The container requests that the control render its image into a metafile. A control handles this request in CComControlBase::IDataObject_GetData.

Although all three types of rendering requests arrive at the control via different mechanisms, the ATL control implementation classes eventually forward the requests to a control's OnDrawAdvanced method.

virtual HRESULT OnDrawAdvanced( ATL_DRAWINFO& di );

ATL bundles all parameters to the rendering requests into an ATL_DRAWINFO structure. You need to use the information in this structure when drawing your control. Most of the fields are simply copies of similar parameters to the IView-Object::Draw method:

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

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

inline HRESULT                                                
CComControlBase::OnDrawAdvanced(ATL_DRAWINFO& di) {           
  BOOL bDeleteDC = FALSE;                                     
  if (di.hicTargetDev == NULL) {                              
    di.hicTargetDev = AtlCreateTargetDC(di.hdcDraw, di.ptd);  
    bDeleteDC = (di.hicTargetDev != di.hdcDraw);              
  }                                                           
  RECTL rectBoundsDP = *di.prcBounds;                         
  BOOL bMetafile =                                            
    GetDeviceCaps(di.hdcDraw, TECHNOLOGY) == DT_METAFILE;     
  if (!bMetafile) {                                           
    ::LPtoDP(di.hdcDraw, (LPPOINT)&rectBoundsDP, 2);          
    SaveDC(di.hdcDraw);                                       
    SetMapMode(di.hdcDraw, MM_TEXT);                          
    SetWindowOrgEx(di.hdcDraw, 0, 0, NULL);                   
    SetViewportOrgEx(di.hdcDraw, 0, 0, NULL);                 
    di.bOptimize = TRUE; //since we save the DC we can do this
  }                                                           
  di.prcBounds = &rectBoundsDP;                               
  GetZoomInfo(di);                                            
  HRESULT hRes = OnDraw(di);                                  
  if (bDeleteDC)                                              
    ::DeleteDC(di.hicTargetDev);                              
  if (!bMetafile)                                             
    RestoreDC(di.hdcDraw, -1);                                
  return hRes;                                                
}                                                             

CComControl::OnDrawAdvanced prepares a normalized device context for drawing and then calls your control class's OnDraw method. A normalized device context is so called because the device context has (some of) the normal defaults for a device contextspecifically, the mapping mode is MM_TEXT, the window origin is 0,0, and the viewport origin is 0,0. Override the OnDrawAdvanced method when you want to use the device context passed by the container as is, without normalizing it. For example, if you don't want these defaults values, you should override OnDrawAdvanced instead of OnDraw, for greater efficiency.

When a container asks a control to draw into a device context, the container specifies whether the control can use optimized drawing techniques. When the bOptimize flag in the ATL_DRAWINFO structure is TRUE, this means that the DC will have some[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.

[3] More specifically, those settings that get saved and restored by the SaveDC/RestoreDC GDI calls.

  • When IDataObject_GetData calls OnDrawAdvanced to retrieve a rendering of the control in a metafile, IDataObject_GetData saves the device context state, calls OnDrawAdvanced, and then restores the device context state. Therefore, IDataObject_GetData sets the bOptimize flag to trUE.

  • When OnPaint calls OnDrawAdvanced to have the control render to its window, the bOptimize flag is set to FALSE.

  • When IViewObject_Draw calls OnDrawAdvanced to have the control render to the container's window, the bOptimize flag is set to TRUE only if the container supports optimized drawing.

When you override OnDrawAdvanced, you should always check the value of the bOptimize flag and restore the state of the device context as necessary.

For a nonmetafile device context device, OnDrawAdvanced saves the state of the entire device context and restores it after calling your control's OnDraw method. Because of this, the default OnDrawAdvanced method sets the bOptimize flag to TRUE.

When you override OnDraw in ATL's current implementation, the bOptimize flag is always TRUE. This doesn't mean that you shouldn't check the flag. It means that you should always go to the effort of supporting optimized drawing when overriding OnDraw because such support is always used.

Listing 11.8 gives excerpts of the drawing code for the BullsEye control. A few features in this code are noteworthy:

  • BullsEye supports transparent drawing via the BackStyle stock property. When BackStyle is 1 (opaque), the control uses the background color to fill the area around the bull's eye. When BackStyle is 0 (transparent), the control doesn't draw to the area outside the circle of the bull's eye. This leaves the area around the circle transparent, and the underlying window contents show through.

  • BullsEye draws differently into a metafile device context than into another device context. You cannot do some operations when drawing to a metafile. Therefore, BullsEye sets up the device context slightly differently in these two cases.

  • BullsEye supports optimized drawing.

The OnDraw method handles the interface to ATL: receiving the ATL_DRAWINFO structure, and figuring out what mapping mode and coordinate system to use, depending on whether the DC is for a metafile. The DrawBullsEye method, on the other hand, actually does the drawing.

Listing 11.8. BullsEye OnDraw and DrawBullsEye Methods
#define ASSERT_SUCCESS( b ) ATLASSERT( ( b ) != 0 )
#define VERIFY_SUCCESS( c ) { BOOL bSuccess = ( c ); \
  ATLASSERT( bSuccess != 0 ); }

// Drawing code

static const int LOGWIDTH = 1000;

HRESULT CBullsEye::OnDraw(ATL_DRAWINFO &di) {
    CRect rc = *( ( RECT * )di.prcBounds );
    HDC hdc = di.hdcDraw;

    // Create brushes as needed
    ...

    // First, fill in background color in invalid area when
    // BackStyle is Opaque
    if (m_nBackStyle == 1 /* Opaque*/ ) {
        VERIFY_SUCCESS( ::FillRect( hdc, &rc, m_backBrush ) );
    }

    int nPrevMapMode;
    POINT ptWOOrig, ptVOOrig;
    SIZE szWEOrig, szVEOrig;

    BOOL bMetafile =
        GetDeviceCaps( di.hdcDraw, TECHNOLOGY ) == DT_METAFILE;
    if( !bMetafile ) {
        // OnDrawAdvanced normalized the device context
        // We are now using MM_TEXT and the coordinates
        // are in device coordinates.

        // Establish convenient coordinate system for on screen
        ...
    } else {
        // We will be played back in ANISOTROPIC mapping mode
        // Set up bounding rectangle and coordinate system
        // for metafile
        ...
    }

    // Draw the BullsEye
    DrawBullsEye( di );

    // Note on optimized drawing:
    // Even when using optimized drawing, a control cannot
    // leave a changed mapping mode, coordinate transformation
    // value, selected bitmap, clip region, or metafile in the
    // device context.

    if (!bMetafile) {
        ::SetMapMode (hdc, nPrevMapMode);

        ::SetViewportOrgEx (hdc, ptVOOrig.x,  ptVOOrig.y,  NULL);
        ::SetViewportExtEx (hdc, szVEOrig.cx, szVEOrig.cy, NULL);
    }

    ::SetWindowOrgEx (hdc, ptWOOrig.x,  ptWOOrig.y,  NULL);
    ::SetWindowExtEx (hdc, szWEOrig.cx, szWEOrig.cy, NULL);

    return S_OK;
}
void CBullsEye::DrawBullsEye(ATL_DRAWINFO &di) {
    HDC hdc = di.hdcDraw;

    // Create brushes as needed
    ...

    // Compute the width of a ring
    short sRingCount;
    HRESULT hr = get_RingCount( &sRingCount );
    ATLASSERT( SUCCEEDED( hr ) );

    int ringWidth = LOGWIDTH / (sRingCount * 2 - 1);

    // Draw the border between rings using the border pen
    HPEN hOldPen = (HPEN)SelectObject( hdc, m_borderPen );

    HBRUSH hOldBrush = 0;

    for( short i = sRingCount - 1; i >= 0; --i ) {
        // Draw the current ring
        ...

        // Set the correct ring color
        HBRUSH hBrush = ( HBRUSH )::SelectObject( hdc, ringBrush );
        // First time through, save the original brush
        if( hOldBrush == 0 ) {
            hOldBrush = hBrush;
        }
        ...
    }

    // When optimized drawing not in effect,
    // restore the original pen and brush
    if( !di.bOptimize ) {
        ::SelectObject( hdc, hOldPen );
        ::SelectObject( hdc, hOldBrush );
    }
}

Property Persistence

A control typically needs to save its state upon its container's request. Various containers prefer different persistence techniques. For example, Internet Explorer and Visual Basic prefer to save a control's state using a property bag, which is an association (or dictionary) of text name/value pairs. The dialog editor in Visual C++ prefers to save a control's state in binary form using a stream. Containers of embedded objects save the objects in structured storage.

ATL provides three persistence interface implementations, as discussed in Chapter 7, "Persistence in ATL":

IPersistStreamInitImpl

Saves properties in binary form into a stream

IPersistStorageImpl

Saves properties in binary form in structured storage

IPersistPropertyBagImpl

Saves properties as name/value pairs


Most controls should derive from all three persistence-implementation classes so that they support the widest variety of containers. The BullsEye control does this:

class ATL_NO_VTABLE CBullsEye :
    ...
    // Persistence
    public IPersistStreamInitImpl<CBullsEye>,
    public IPersistStorageImpl<CBullsEye>,
    public IPersistPropertyBagImpl<CBullsEye>,
};

As always, you need to add entries to the COM map for each supported interface. The persistence interfaces all derive from IPersist, so you need to add it to the COM map as well.

BEGIN_COM_MAP(CBullsEye)
    ...
    // Persistence
    COM_INTERFACE_ENTRY(IPersistStreamInit)
    COM_INTERFACE_ENTRY2(IPersist, IPersistStreamInit)
    COM_INTERFACE_ENTRY(IPersistStorage)
    COM_INTERFACE_ENTRY(IPersistPropertyBag)
END_COM_MAP()

All three persistence implementations save the properties listed in the control's property map. You define the property map using the BEGIN_PROP_MAP and END_PROP_MAP macros. Here's the CBullsEye class's property map:

BEGIN_PROP_MAP(CBullsEye)
  PROP_DATA_ENTRY("_cx", m_sizeExtent.cx, VT_UI4)
  PROP_DATA_ENTRY("_cy", m_sizeExtent.cy, VT_UI4)
  PROP_ENTRY("BackStyle",      DISPID_BACKSTYLE,
    CLSID_BullsEyePropPage)
  PROP_ENTRY("Beep",           DISPID_BEEP,
    CLSID_BullsEyePropPage)
  PROP_ENTRY("Enabled",        DISPID_ENABLED,
    CLSID_BullsEyePropPage)
  PROP_ENTRY("RingCount",      DISPID_RINGCOUNT,
    CLSID_BullsEyePropPage)
  PROP_ENTRY("AlternateColor", DISPID_ALTERNATECOLOR,
    CLSID_StockColorPage)
  PROP_ENTRY("BackColor",      DISPID_BACKCOLOR,
    CLSID_StockColorPage)
  PROP_ENTRY("CenterColor",    DISPID_CENTERCOLOR,
    CLSID_StockColorPage)
  PROP_ENTRY("ForeColor",      DISPID_FORECOLOR,
    CLSID_StockColorPage)
END_PROP_MAP()

The ATL Object Wizard adds the first two PROP_DATA_ENTRY macros to a control's property map when it generates the initial source code. These entries cause ATL to save and restore the extent of the control. When you describe a persistent property using a PROP_DATA_ENTRY macro, ATL directly accesses the member variable in the control.

You must explicitly add entries for any additional properties that the control needs to persist. The BullsEye control lists all but one of its persistent properties using the PROP_ENTRY macro. This macro causes ATL to save and restore the specified property by accessing the property using the default dispatch interface for the control. Alternatively, you can use the PROP_ENTRY_EX macro to specify the IID, other than IID_IDispatch, of the dispatch interface that supports the property. You use the PROP_ENTRY_EX macro when your control supports multiple dispatch interfaces with various properties accessible via different dispatch interfaces. Supporting multiple dispatch interfaces is generally not a good thing to do.[4]

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

HRESULT CBullsEye::IPersistPropertyBag_Load(
  LPPROPERTYBAG pPropBag,
  LPERRORLOG pErrorLog,
  ATL_PROPMAP_ENTRY* pMap) {
  if (NULL == pPropBag) return E_POINTER;

  // Load the properties described in the PROP_MAP
  HRESULT hr =
    IPersistPropertyBagImpl<CBullsEye>::IPersistPropertyBag_Load(
      pPropBag, pErrorLog, pMap);
  if (SUCCEEDED(hr))
    m_bRequiresSave = FALSE;

  if (FAILED (hr)) return hr;

  // Load the indexed property - RingValues

  // Get the number of rings
  short sRingCount;
  get_RingCount (&sRingCount);

  // For each ring, read its value
  for (short nIndex = 1; nIndex <= sRingCount; nIndex++) {

    // Create the base property name
    CComBSTR bstrName = OLESTR("RingValue");

    // Create ring number as a string
    CComVariant vRingNumber = nIndex;
    hr = vRingNumber.ChangeType (VT_BSTR);
    ATLASSERT (SUCCEEDED (hr));

    // Concatenate the two strings to form property name
    bstrName += vRingNumber.bstrVal;

    // Read ring value from the property bag
    CComVariant vValue = 0L;
    hr = pPropBag->Read(bstrName, &vValue, pErrorLog);
    ATLASSERT (SUCCEEDED (hr));
    ATLASSERT (VT_I4 == vValue.vt);

    if (FAILED (hr)) {
      hr = E_UNEXPECTED;
      break;
    }

    // Set the ring value
    put_RingValue (nIndex, vValue.lVal);
  }

  if (SUCCEEDED(hr)) m_bRequiresSave = TRUE;
  return hr;
}

IQuickActivate

Some control containers ask a control for its IQuickActivate interface and use it to quickly exchange a number of interfaces between the container and the control during the control's activation processthus the interface name.

ATL provides an implementation of this interface, IQuickActivateImpl, which, by default, full, composite, and HTML controls use. However, a control container also gives a control a few ambient properties during this quick activation process that the ATL implementation doesn't save. If your control needs these ambient propertiesBackColor, ForeColor, and Appearanceit's more efficient to save them during the quick activation process than to incur three more round-trips to the container to fetch them later.

The tricky aspect is that a container might not quick-activate your control. Therefore, the control should save the ambient properties when quick-activated or retrieve the ambient properties when the container provides the control's client site, but not both. Luckily, it's easy to add this functionality to your control.

When a container quick-activates your control, it calls the control's IQuickActivate::QuickActivate method, which is present in your control's IQuickActivateImpl base class. This method delegates the call to your control class's IQuickActivate_QuickActivate method. By default, a control class doesn't provide the method, so the call invokes a default implementation supplied by CComControlBase. You simply need to provide an implementation of the IQuickActivate_QuickActivate method that saves the ambient properties and forwards the call to the method in CComControlBase, like this:

HRESULT CBullsEye::IQuickActivate_QuickActivate(
    QACONTAINER *pQACont,
    QACONTROL *pQACtrl) {
    m_clrForeColor = pQACont->colorFore;
    m_clrBackColor = pQACont->colorBack;
    m_nAppearance = (short) pQACont->dwAppearance;
    m_bAmbientsFetched = true;

    HRESULT hr = CComControlBase::IQuickActivate_QuickActivate(
        pQACont, pQACtrl);
    return hr;
}

Note that the function also sets a flag, m_bAmbientsFetched, to remember that it has already obtained the ambient properties and, therefore, shouldn't fetch them again when the control receives its client site. BullsEye initializes the flag to false in its constructor and checks the flag in its IOleObject_SetClientSite method like this:

HRESULT CBullsEye::IOleObject_SetClientSite(
    IOleClientSite *pClientSite) {
    HRESULT hr =
        CComControlBase::IOleObject_SetClientSite(pClientSite);
    if (!m_bAmbientsFetched) {
        GetAmbientBackColor (m_clrBackColor);
        GetAmbientForeColor (m_clrForeColor);
        GetAmbientAppearance (m_nAppearance);
    }
    return hr;
}

Component Categories

Frequently, you'll want your control to belong to one or more component categories. For example, the BullsEye control belongs to the ATL Internals Sample Components category. Additionally, BullsEye is a member of the Safe for Initialization and Safe for Scripting categories, so scripts on an HTML page can initialize and access it without security warnings. Adding the proper entries to the control's category map registers the class as a member of the specified component categories. BullsEye uses this category map:

BEGIN_CATEGORY_MAP(CBullsEye)
  IMPLEMENTED_CATEGORY(CATID_ATLINTERNALS_SAMPLES)
  IMPLEMENTED_CATEGORY(CATID_SafeForScripting)
  IMPLEMENTED_CATEGORY(CATID_SafeForInitializing)
END_CATEGORY_MAP()

Registering a control as a member of the Safe for Initialization or Safe for Scripting component categories is a static decision. In other words, you're deciding that the control is or is not always safe. A control might prefer to restrict its functionality at runtime when it needs to be safe for initialization or scripting, but then prefer to have its full, potentially unsafe functionality available at other times.

Such controls must implement the IObjectSafety interface. ATL provides a default implementation of this interface in the IObjectSafetyImpl class. As a template parameter, you specify the safety options that the control supports. A container can use a method of this interface to selectively enable and disable each supported option. A control can determine its current safety level, and potentially disable or enable unsafe functionality, by checking the m_dwCurrentSafety member variable.

You use this implementation class like most of the others, derive your control class from the appropriate template class, and add the proper interface entry to the COM interface map. BullsEye would do it like this:

class ATL_NO_VTABLE CBullsEye :
  ...
  // Object safety support
  public IObjectSafetyImpl<CBullsEye,
    INTERFACESAFE_FOR_UNTRUSTED_CALLER |
    INTERFACESAFE_FOR_UNTRUSTED_DATA>,
  ...

BEGIN_COM_MAP(CBullsEye)
  // Object safety support
  COM_INTERFACE_ENTRY(IObjectSafety)
  ...
END_COM_MAP()

STDMETHODIMP(FormatHardDrive)( ) {
  if( m_dwCurrentSafety == 0 ) {
    // Container isn't asking for safe behavior, perform mayhem
    ...
    return S_OK; // We just erased a hard drive,
                 // how could we not be ok?
  }
  else {
    // Container has asked we play nice, so don't
    // actually erase the drive
    ...
    return S_FALSE; // Ok, we succeeded, but we're
                    // being a little grumpy
}

ICategorizeProperties

Visual Basic provides a property view that displays the properties of a control on a form. The property view can display the properties on a control alphabetically or grouped by arbitrary categories. Figure 11.3 shows the categorized list of the BullsEye control's properties when the control is contained on a Visual Basic form.

A control must implement the ICategorizeProperties interface so that Visual Basic can display the control's properties in the appropriate categories in its property view. Unfortunately, this interface isn't presently defined in any system IDL file or any system header file, and ATL provides no implementation class for the interface. So, here's what you need to do to support it.

First, here's the IDL for the interface:

[
    object, local,
    uuid(4D07FC10-F931-11CE-B001-00AA006884E5),
    helpstring("ICategorizeProperties Interface"),
    pointer_default(unique)
]
interface ICategorizeProperties : IUnknown {
    typedef [public] int PROPCAT;

    const int PROPCAT_Nil        = -1;
    const int PROPCAT_Misc       = -2;
    const int PROPCAT_Font       = -3;
    const int PROPCAT_Position   = -4;
    const int PROPCAT_Appearance = -5;
    const int PROPCAT_Behavior   = -6;
    const int PROPCAT_Data       = -7;
    const int PROPCAT_List       = -8;
    const int PROPCAT_Text       = -9;
    const int PROPCAT_Scale      = -10;
    const int PROPCAT_DDE        = -11;

    HRESULT MapPropertyToCategory([in] DISPID dispid,
        [out] PROPCAT* ppropcat);
    HRESULT GetCategoryName([in] PROPCAT propcat, [in] LCID lcid,
        [out] BSTR* pbstrName);
}

I keep this IDL in a separate file, CategorizeProperties.idl, and import the file into the BullsEye.idl file. At this point, it's highly unlikely that Microsoft will add this interface to a system IDL file, so having it in a separate IDL file makes it easier to reuse the definition in multiple projects.

You implement the interface like all interfaces in ATL: Derive your control class from ICategorizeProperties, add the interface entry to the control's interface map, and implement the two methods, MapPropertyToCategory and GetCategoryName. Note that there are 11 predefined property categories with negative values. You can define your own custom categories, but be sure to assign them positive values.

The MapPropertyToCategory method returns the appropriate property category value for the specified property.

const int PROPCAT_Scoring = 1;

STDMETHODIMP CBullsEye::MapPropertyToCategory(
    /*[in]*/ DISPID dispid, /*[out]*/ PROPCAT* ppropcat) {
    if (NULL == ppropcat) return E_POINTER;

    switch (dispid) {
    case DISPID_FORECOLOR:
    case DISPID_BACKCOLOR:
    case DISPID_CENTERCOLOR:
    case DISPID_ALTERNATECOLOR:
    case DISPID_RINGCOUNT:
    case DISPID_BACKSTYLE:
        *ppropcat = PROPCAT_Appearance;
        return S_OK;

    case DISPID_BEEP:
    case DISPID_ENABLED:
        *ppropcat = PROPCAT_Behavior;
        return S_OK;

    case DISPID_RINGVALUE:
        *ppropcat = PROPCAT_Scoring;
        return S_OK;

    default:
        return E_FAIL;
    }
}

The GetCategoryName method simply returns a BSTR containing the category name. You need to support only your custom category values because Visual Basic knows the names of the standard property category values.

STDMETHODIMP CBullsEye::GetCategoryName(/*[in]*/ PROPCAT propcat,
    /*[in]*/ LCID lcid,
    /*[out]*/ BSTR* pbstrName)
{
    if(PROPCAT_Scoring == propcat) {
        *pbstrName = ::SysAllocString(L"Scoring");
        return S_OK;
    }
    return E_FAIL;
}

BullsEye supports one custom category, Scoring, and associates its Ring-Value property with the category. Unfortunately, the RingValue property is an indexed property, and Visual Basic doesn't support indexed properties. Thus, the RingValue property doesn't appear in Visual Basic's property view, either in the alphabetic list or in the categorized list.

Per-Property Browsing

When Visual Basic and similar containers display a control's property in a property view, they can ask the control for a string that better describes the property's current value than the actual value of the property. For example, a particular property might have valid numerical values of 1, 2, and 3, which represent the colors red, blue, and green, respectively. When Visual Basic asks the control for a display string for the property when the property has the value 2, the control returns the string "blue".

A container uses the control's IPerPropertyBrowsing interface to retrieve the display strings for a control's properties. When the control doesn't provide a display string for a property, some containers, such as Visual Basic, provide default formatting, if possible.[5] Of course, the container can always simply display the actual property value.

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

STDMETHODIMP CBullsEye::GetDisplayString(DISPID dispid,
    BSTR *pBstr) {
    ATLTRACE2(atlTraceControls,2,
        _T("CBullsEye::GetDisplayString\n"));
    switch (dispid) {
    case DISPID_BEEP:
        if (VARIANT_TRUE == m_beep)
            *pBstr = SysAllocString (OLESTR("Yes"));
        else
            *pBstr = SysAllocString (OLESTR("No"));

        return *pBstr ? S_OK : E_OUTOFMEMORY;

    case DISPID_BACKSTYLE:
        if (1 == m_nBackStyle)
            *pBstr = SysAllocString (OLESTR("Opaque"));
        else
            *pBstr = SysAllocString (OLESTR("Transparent"));

        return *pBstr ? S_OK : E_OUTOFMEMORY;

    case DISPID_ALTERNATECOLOR: // Make VB apply default
    case DISPID_BACKCOLOR:      // formatting for these
    case DISPID_CENTERCOLOR:    // color properties.
    case DISPID_FORECOLOR:      // Otherwise it displays color
                                // values in decimal and doesn't
                                // draw the color sample
                                // correctly
        return S_FALSE;  // This is an undocumented return
                         // value that works...
    }
    return
        IPerPropertyBrowsingImpl<CBullsEye>::GetDisplayString(
            dispid, pBstr);
}

The IPerPropertyBrowsingImpl<T>::GetDisplayString implementation fetches the value of the specified property and, if it's not already a BSTR, converts the value into a BSTR using VariantChangeType. This produces relatively uninteresting display strings for anything but simple numerical value properties.

Visual Basic provides default formatting for certain property types, such as OLE_COLOR and VARIANT_BOOL properties, but only if your GetdisplayString method doesn't provide a string for the property. The default implementation fails when the property doesn't exist, when the property exists but cannot be converted into a BSTR, or when the BSTR memory allocation fails. This basically means that the default implementation of GetdisplayString often provides less than useful strings for many properties.

BullsEye's GetdisplayString method lets Visual Basic provide default formatting for all its OLE_COLOR properties by returning S_FALSE when asked for those properties. This value isn't documented as a valid return value for GetdisplayString, but there are a couple of convincing reasons to use it: First, the default ATL implementation of GetdisplayString returns this value when it cannot provide a display string for a property. Second, it works.

When you let Visual Basic provide the display string for an OLE_COLOR property, it displays the color value in hexadecimal and displays a color sample. ATL's default implementation displays the color value in decimal, and the sample image is typically always black. When you let Visual Basic provide the display string for a VARIANT_BOOL property, Visual Basic displays true and False. ATL's default imple-mentation displays 1 and 0, respectively.

Also notice in Figure 11.3 that when you click on a property in Visual Basic's property view to modify the property, a drop-down arrow appears to the right side of the property value. Clicking this arrow produces a drop-down list that contains strings representing the valid selections for the property. You provide this support via the IPerPropertyBrowsing interface, too. A container calls the interface's GetPredefinedStrings method to retrieve the strings that the container displays in the drop-down list. For each string, the method also provides a DWORD value (cookie). When a user selects one of the strings from the drop-down list, the container calls the interface's GetPredefinedValue method and provides the cookie. The method returns the property value associated with the selected string. The container then typically performs a property put IDispatch call to change the property to the predefined value.

The BullsEye control supports predefined strings and values for the Beep and BackStyle properties, as shown in the following code:

/************************/
/* GetPredefinedStrings */
/************************/

#define DIM(a) (sizeof(a)/sizeof(a[0]))

static const LPCOLESTR    rszBeepStrings [] = {
  OLESTR("Yes, make noise"), OLESTR("No, be mute") };
static const DWORD        rdwBeepCookies [] = { 0, 1 };
static const VARIANT_BOOL  rvbBeepValues [] = {
  VARIANT_TRUE, VARIANT_FALSE };
static const UINT cBeepStrings = DIM(rszBeepStrings);
static const UINT cBeepCookies = DIM(rdwBeepCookies);
static const UINT cBeepValues  = DIM(rvbBeepValues);

static const LPCOLESTR    rszBackStyleStrings [] = {
  OLESTR("Opaque"), OLESTR("Transparent") };
static const DWORD        rdwBackStyleCookies [] = { 0, 1 };
static const long          rvbBackStyleValues [] = { 1, 0 };

static const UINT cBackStyleStrings = DIM(rszBackStyleStrings);
static const UINT cBackStyleCookies = DIM(rdwBackStyleCookies);
static const UINT cBackStyleValues  = DIM(rvbBackStyleValues);

STDMETHODIMP CBullsEye::GetPredefinedStrings(
  /*[in]*/ DISPID dispid,
  /*[out]*/ CALPOLESTR *pcaStringsOut,
  /*[out]*/ CADWORD *pcaCookiesOut) {
   ATLTRACE2(atlTraceControls,2,
    _T("CBullsEye::GetPredefinedStrings\n"));
  if (NULL == pcaStringsOut || NULL == pcaCookiesOut)
    return E_POINTER;

  ATLASSERT (cBeepStrings == cBeepCookies);
  ATLASSERT (cBeepStrings == cBeepValues);

  ATLASSERT (cBackStyleStrings == cBackStyleCookies);
  ATLASSERT (cBackStyleStrings == cBackStyleValues);

  pcaStringsOut->cElems = 0;
  pcaStringsOut->pElems = NULL;
  pcaCookiesOut->cElems = 0;
  pcaCookiesOut->pElems = NULL;

  HRESULT hr = S_OK;
  switch (dispid) {
  case DISPID_BEEP:
    hr = SetStrings (cBeepValues, rszBeepStrings, pcaStringsOut);
    if (FAILED (hr)) return hr;
    return SetCookies (cBeepValues, rdwBeepCookies,
      pcaCookiesOut);

  case DISPID_BACKSTYLE:
    hr = SetStrings (cBackStyleValues, rszBackStyleStrings,
      pcaStringsOut);
    if (FAILED (hr)) return hr;
    return SetCookies (cBackStyleValues, rdwBackStyleCookies,
      pcaCookiesOut);
  }
  return
    IPerPropertyBrowsingImpl<CBullsEye>::GetPredefinedStrings(
      dispid, pcaStringsOut, pcaCookiesOut);
}

/**********************/
/* GetPredefinedValue */
/**********************/

STDMETHODIMP CBullsEye::GetPredefinedValue(
  DISPID dispid, DWORD dwCookie, VARIANT* pVarOut) {
  if (NULL == pVarOut) return E_POINTER;

  ULONG i;
  switch (dispid) {
  case DISPID_BEEP:
    // Walk through cookie array looking for matching value
    for (i = 0; i < cBeepCookies; i++) {
      if (rdwBeepCookies[i] == dwCookie) {
        pVarOut->vt = VT_BOOL;
        pVarOut->boolVal = rvbBeepValues [i];
        return S_OK;
      }
    }
    return E_INVALIDARG;

  case DISPID_BACKSTYLE:
    // Walk through cookie array looking for matching value
    for (i = 0; i < cBackStyleCookies; i++) {
      if (rdwBackStyleCookies[i] == dwCookie) {
        pVarOut->vt = VT_I4;
        pVarOut->lVal = rvbBackStyleValues [i];
        return S_OK;
      }
    }
    return E_INVALIDARG;
  }

  return
    IPerPropertyBrowsingImpl<CBullsEye>::GetPredefinedValue(
      dispid, dwCookie, pVarOut);
}

Some containers let you edit a control's property using the appropriate property page for the property. When you click on such a property in Visual Basic's property view, Visual Basic displays a small button containing . . . to the right of the property value. Clicking this button displays the control's property page for the property.

A container uses a control's IPerPropertyBrowsing::MapPropertyToPage method to find the property page for a property. ATL's implementation of this method uses the property map to determine which property page corresponds to a particular property. When a property doesn't have a property page, you specify CLSID_NULL as follows:

PROP_ENTRY("SomeProperty", DISPID_SOMEPROPERTY, CLSID_NULL)

IPerPropertyBrowsingImpl finds this entry in the property map and returns the error PERPROP_E_NOPAGEAVAILABLE. This prevents Visual Basic from displaying the property page ellipses button ("...").

Keyboard Handling for an ActiveX Control

When an ATL-based ActiveX control has the focus on a Visual Basic form, it does not give the focus to the default button on the form (the button with a Default property of TRue) when you press Enter. ATL provides implementations of the IOleControl::GetControlInfo and IOleInPlaceActiveObject::TranslateAccelerator methods. The GetControlInfo method returns E_NOTIMPL. A container calls a control's GetControlInfo method to get the control's keyboard mnemonics and keyboard behavior, and it calls the control's translateAccelerator method to process the key presses.

BullsEye overrides the default implementation of GetControlInfo provided by ATL with the following code:

STDMETHODIMP CBullsEye::GetControlInfo(CONTROLINFO *pci) {
    if(!pci) return E_POINTER;
    pci->hAccel  = NULL;
    pci->cAccel  = 0;
    pci->dwFlags = 0;
    return S_OK;
}

The default implementation of TRanslateAccelerator looks like this:

STDMETHOD(TranslateAccelerator)(LPMSG pMsg) {                
  T* pT = static_cast<T*>(this);                             
  HRESULT hRet = S_OK;                                       
  MSG msg = *pMsg;                                           
  if (pT->PreTranslateAccelerator(&msg, hRet))               
  return hRet;                                               
                                                             
  CComPtr<IOleControlSite> spCtlSite;                        
  hRet = pT->InternalGetSite(__uuidof(IOleControlSite),      
    (void**)&spCtlSite);                                     
  if (SUCCEEDED(hRet)) {                                     
    if (spCtlSite != NULL) {                                 
      DWORD dwKeyMod = 0;                                    
      if (::GetKeyState(VK_SHIFT) < 0)                       
        dwKeyMod += 1; // KEYMOD_SHIFT                       
      if (::GetKeyState(VK_CONTROL) < 0)                     
        dwKeyMod += 2; // KEYMOD_CONTROL                     
      if (::GetKeyState(VK_MENU) < 0)                        
        dwKeyMod += 4; // KEYMOD_ALT                         
      hRet = spCtlSite->TranslateAccelerator(&msg, dwKeyMod);
    }                                                        
    else                                                     
      hRet = S_FALSE;                                        
  }                                                          
  return (hRet == S_OK) ? S_OK : S_FALSE;                    
}                                                            

When the BullsEye control has the input focus, these method implementations pass all Tab and Enter key presses to the container for processing. This implementation allows one to tab into and out of the BullsEye control. While the control has the input focus, pressing the Enter key activates the default pushbutton on a Visual Basic form, if any.

For the BullsEye control, it doesn't make much sense to allow a user to tab into the control. You can use the MiscStatus bits for a control to inform the control's container that the control doesn't want to be activated by tabbing. A container asks a control for its MiscStatus setting by calling the control's IOle-Object::GetMiscStatus method. ATL provides an implementation of this method in the IOleControlImpl class:

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

This code simply delegates the call to the OleRegGetMiscStatus function, which reads the value from the control's Registry entry. A control can have multiple MiscStatus valuesone for each drawing aspect that the control supports. Most controls support the drawing aspect DVASPECT_CONTENT, which has the value of 1. You specify the drawing aspect as a subkey of MiscStatus. The value of the subkey is the string of decimal numbers comprising the sum of the desired OLEMISC enumeration values.

For example, BullsEye uses the following MiscStatus settings:

OLEMISC_SETCLIENTSITEFIRST

131072

OLEMISC_NOUIACTIVATE

16384

OLEMISC_ACTIVATEWHENVISIBLE

256

OLEMISC_INSIDEOUT

128

CANTLINKINSIDE

16

OLEMISC_RECOMPOSEONRESIZE

1


The sum of these values is 14,7857, so you specify that as the value of the subkey called 1 of your class.

ForceRemove {7DC59CC5-36C0-11D2-AC05-00A0C9C8E50D} =
s 'BullsEye Class' {
    ...
    'MiscStatus' = s '0'
    {
        '1' = s '147857'
    }
}

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

STDMETHODIMP CBullsEye::GetMiscStatus (
    DWORD dwAspect, DWORD *pdwStatus) {
    if (NULL == pdwStatus) return E_POINTER;

    *pdwStatus =
           OLEMISC_SETCLIENTSITEFIRST |
           OLEMISC_ACTIVATEWHENVISIBLE |
           OLEMISC_INSIDEOUT |
           OLEMISC_CANTLINKINSIDE |
           OLEMISC_RECOMPOSEONRESIZE |
           OLEMISC_NOUIACTIVATE ;

    return S_OK;
}

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


previous page
next page
Converted from CHM to HTML with chm2web Pro 2.75 (unicode)