Полезная информация

Chapter 8
Using ATL to Create a Basic ActiveX Control


Using ATL to Create a Basic ActiveX Control

With the coming of Visual C++ 5.0, the ActiveX Template Library (ATL) has matured to the level necessary for a complete ActiveX development framework. The two releases of ATL, versions 1.0 and 1.1, that came before VC++ 5.0 introduced ATL to the growing ActiveX development community as an alternative to MFC. However, the first two versions of ATL allowed only for the creation of ActiveX COM Objects and ActiveX Automation Servers (no small feats in their own right). ATL version 2.1, the version that ships with VC++ 5.0, now supports the creation of ActiveX Controls.

Versions 1.0 and 1.1 included an AppWizard for creating a basic ATL project. Version 2.1 includes a number of AppWizards that can be used to create various ActiveX components, thus furthering ATL's capability to compete with MFC as a rapid development tool.

In this chapter, you will create an ActiveX control with all the basics: methods, properties, events, persistence, and drawing. Also, in this chapter and Chapter 9, you will explore some of the more advanced features and lesser known aspects of control development, such as methods with optional parameters, asynchronous properties, Clipboard support, and optimized drawing, to name a few.

Creating the Basic Control Project

To create an ATL ActiveX control, you want to take advantage of the AppWizard provided by Visual C++. Run the Visual C++ development environment, and from the File menu, select New. When the New dialog displays, select the Projects tab (see fig 8.1). The Projects tab allows you the opportunity to define several aspects of how the application will be created, for example, the type of application to create, the name of the application, and the location where you want the project created. For the type, select ATL COM AppWizard; enter the Project Name ATLControl, and the Location will be C:\que\ActiveX\ATLControl. Click the OK button to start the ATL COM AppWizard so you can further define the properties of your control.

FIG. 8.1
Define the new ATL control project with the New dialog.

The next step in the AppWizard is defining the basic architecture of your ATL project (see fig. 8.2). Since you are creating an ActiveX Control, you choose the Dynamic Link Library (DLL) radio button.

FIG. 8.2
Define the basic architecture of the ATL COM Object with the ATL COM AppWizard.An OCX is in reality nothing more than a DLL. The extension OCX was carried over from the early days of control development. You have the option of changing the extension from DLL to OCX.

Since the sample implementation will be a control, it is not necessary to support merging of the proxy/stub marshaling code, nor will the implementation require the use of MFC. Click the Finish button to continue.

The New Project Information dialog is used to confirm the settings that were selected for the project prior to the creation of the actual source files (see fig. 8.3). This step is the last one in the ATL COM AppWizard.

FIG. 8.3
Confirm the new project settings with the New Project Information dialog.

"But wait," you say, "I haven't defined any of my control properties." The ATL COM AppWizard takes a slightly different approach from that of MFC. Only the basic source files are created with the AppWizard. The remainder of the project is defined by the Object Wizard, which allows much better control of the project implementation versus MFC since the developer can add any number of ActiveX Controls, Servers, or plain COM Objects after the basic project is created. After you confirm your project settings, click the OK button to close the ATL COM AppWizard and create the ATLControl project.

The next step is to add your control implementations to the project. From the Insert menu, select the New ATL Object menu item. Within the ATL Object Wizard, select the Controls item in the left panel to display the types of control components that can be added (see fig. 8.4). Your implementation will be a Full Control, so select the Full Control icon. The other types of components that can be created are a Microsoft Internet Explorer control that supports all of the necessary interfaces to be hosted by the Internet Explorer Web browser and a Property Page component, which you need if your control requires property page support. The Internet Explorer control simply supports fewer interfaces than the Full Control. The Full Control will also work within a Web browser. For more information, see the ATL documentation. Click the Next button to continue.

FIG. 8.4
Select the type of ATL object to add to your project.

The next dialog is the ATL Object Wizard Properties dialog, which is used to define the specific properties of the new object that will be added to your project. Select the Names tab, and in the Short Name edit field, type ATLControlWin (see fig. 8.5). The remainder of the edit fields will automatically update, reflecting the short name that you added. The other fields can be changed, but in this case, you use the default values.

FIG. 8.5
Define the name of the new control object.

Select the Attributes tab so that you can define the attributes of the control project (see fig. 8.6). Check the Support ISupportErrorInfo and Support Connection Points check boxes to add OLE rich error support and events to the control. See Chapter 4 and the ATL documentation for more information regarding the options available to you. Leave the remainder of the settings at their default values.

FIG. 8.6
Define the attributes of the new control object.

You use the Miscellaneous tab to define how the control will draw and act while contained and whether your control implementation subclasses a built-in Windows control (see fig. 8.7). For your implementation, you want the control to always create a window whether or not the container is capable of supporting windowless controls, so check the Windowed Only check box. Leave the remainder of the controls at their default settings.

FIG. 8.7
The Miscellaneous tab is used to define some of the basic control behaviors.

The Stock Properties tab is used to define any number of the basic stock properties that the control project will support (see fig. 8.8). For now, leave the Stock Properties tab as is, and click the OK button to create the new control object.

FIG. 8.8
The Stock Properties tab is used to define the stock properties that the control object will be created with.

As with the MFC and later the BaseCtl implementation, you need to define two more controls to complete the sample implementation. From the Insert menu, select the New ATL Object menu item. Within the ATL Object Wizard, select the Controls item in the left panel, and select the Full Control icon (refer to fig. 8.4). Click the Next button to continue.

On the Names tab within the ATL Object Wizard Properties dialog, add the Short Name ATLControlNoWin, and on the Attributes tab, check the Support ISupportErrorInfo and Support Connection Points check boxes to add OLE rich error support and events to the control.

On the Miscellaneous tab, do not check the Windowed Only check box--so that the control will create a window for itself only if the container cannot. Leave the Stock Properties tab at its default settings. Click OK to add the control object to the project.

For the last control implementation, you create a control that subclasses another window's control. Again, from the Insert menu, select the New ATL Object menu item. Within the ATL Object Wizard (refer again to fig. 8.4), select the Controls item in the left panel, and select the Full Control icon. Click the Next button to continue.

On the Names tab, within the ATL Object Wizard Properties dialog, add the Short Name ATLControlSubWin, and on the Attributes tab, check the Support ISupportErrorInfo and Support Connection Points check boxes to add OLE rich error support and events to the control.

On the Miscellaneous tab, select the Button control from the Add control based on list box, and check the Windowed Only check box to ensure that a window is always created for the control whether the container supports windowless controls or not. Leave the Stock Properties tabs at its default settings. Click OK to add the control object to the project.

At this point in the MFC sample (see Chapter 6), you are also able to add other ActiveX features as part of the AppWizard implementation. The ATL AppWizard and Object Wizard do not allow for defining any other ActiveX features at this point, but they are defined in Chapter 9.

All of the basic source files and control objects are now added to the control project. The next step in any control project is to ensure that the project contains registration support. Without registration, the control cannot be used by any application.

Control Registration

Control registration and unregistration support is provided for you by ATL. You are not required to make any code changes or additions to support it. Unlike MFC, which uses a set of constants, ATL relies on resource information in the form of a registry script file to define the information that is added to the registry database. The registry script file is added automatically to the project when the control object is added; one script file is added for each control object.

The registry script file or files are compiled into the control project as resources and can be viewed in binary form in the resource editor. The files, which have the extension .rgs, are normal text files that can be edited within the IDE. For more information about the use of registry script files and their particular syntax, see the VC++ books online subject "Registry Scripting Examples--Active Template Library, Articles." Listing 8.1 shows the registry script file for the CATLControlWin control object that you added.

Listing 8.1 ATLCONTROLWIN.RGS--Sample Registry Script File for the CATLControlWin Control Class



HKCR
{
ATLControlWin.ATLControlWin.1 = s `ATLControlWin Class'
{
CLSID = s `{A19F6964-7884-11D0-BEF3-00400538977D}'
}
ATLControlWin.ATLControlWin = s `ATLControlWin Class'
{
CurVer = s `ATLControlWin.ATLControlWin.1'
}
NoRemove CLSID
{
ForceRemove {A19F6964-7884-11D0-BEF3-00400538977D} = s `ATLControlWin Class'
{
ProgID = s `ATLControlWin.ATLControlWin.1'
VersionIndependentProgID = s `ATLControlWin.ATLControlWin'
ForceRemove `Programmable'
InprocServer32 = s `%MODULE%'
{
val ThreadingModel = s `Apartment'
}
ForceRemove `Control'
ForceRemove `Programmable'
ForceRemove `Insertable'
ForceRemove `ToolboxBitmap32' = s `%MODULE%, 1'
`MiscStatus' = s `0'
{
`1' = s `131473'
}
`TypeLib' = s `{A19F6957-7884-11D0-BEF3-00400538977D}'
`Version' = s `1.0'
}
}
}

You can now compile and register the control you've created, but it won't be of much use because it doesn't contain any methods, properties, or events.

Creating Methods

Creating Methods

Now that you have successfully created your basic ActiveX control project, you can add a method, which is one of the basic aspects of component development.

For the purposes of the sample control, you are going to add a method called CaptionMethod. The method will accept two parameters, the second being optional. The first parameter is a string that the control will display within its client area, and the second, optional parameter is the alignment of the caption within the client area, either left, right, or center. Adding methods to an ATL control differs from MFC in that MFC relies on the familiar ClassWizard, and ATL does not. From the ClassView tab in the Project Workspace window, select the IATLControlWin interface, click the right mouse button, and select the Add Method menu item (see fig. 8.9).

In the Add Method to Interface dialog, add the Method Name, CaptionMethod, and the Parameters, [in] BSTR bstrCaption, [in, optional] VARIANT varAlignment, [out, retval] long * lRetVal (see fig. 8.10). The Attributes button displays a dialog for adding Interface Definition Language (IDL) attributes for the entire function declaration.

FIG. 8.9
Add a new method to the control project.

FIG. 8.10
Define the CaptionMethod method.

NOTE: All optional parameters must be of type VARIANT, and they must fall at the end of the parameter list. Optional parameters are not managed in any way by OLE. It is the Server application's responsibility to determine whether the VARIANT parameter passed to the method contains data and whether to



You've also added the direction that the parameters flow in the form of [in] and [out] parameter attributes. See Table 8.1 for a complete description of the possible attributes that can be used.

Parameter attributes are used to aid development tools in determining how parameters are used within a function call. A tool like Visual Basic will hide the details of how parameters are handled--such as creating and destroying memory--based on these and other attributes in the type library. This is why the type library is so important to ActiveX component development.

Note that the IDL parameter attributes are added directly to the parameter list. Click OK to add the method to the control.
Table 8.1 Parameter Flow Attributes
Direction Description
in Parameter is passed from caller to callee.
out Parameter is returned from callee to caller.
in, out Parameter is passed from caller to callee, and the callee returns a parameter.
out, retval Parameter is the return value of the method and is returned from the callee to the caller.


To aid your CaptionMethod implementation, you need to add an enumeration for all the valid alignment settings and two member variables to your class definition (see Listing 8.2). The enumeration is included in the header file Alignmentenums.h (see Listing 8.3). The two member variables, m_lptstrCaption and m_lAlignment, are used to store the caption string and the alignment setting while the control is being used.

Note the data type used for the m_lAlignment member variable. The variable is declared as type long and not as the enumeration type because of the data type restrictions imposed upon you by ActiveX Automation. Remember that only data types that can be passed in a VARIANT can be used in methods and properties. By declaring the m_lAlignment member as long, you do not have to explicitly convert the value by casting to the enumerated type when it is retrieved from the VARIANT parameter in the caption method. On the other hand, casting the value to the enumerated type is a trivial issue, and its implementation is based completely on your preference.

Listing 8.2 ATLCONTROLWIN.H--Alignment Enumeration Include File and Member Variables Added to Class Definition



// ATLControlWin.h : Declaration of the CATLControlWin
#ifndef __ATLCONTROLWIN_H_
#define __ATLCONTROLWIN_H_
#include "resource.h" // main symbols
#include "alignmentenums.h"
/////////////////////////////////////////////////////////////////////////////
// CATLControlWin
class ATL_NO_VTABLE CATLControlWin :
. . .
protected:
// storage variable for the caption
LPTSTR m_lptstrCaption;
// storage variable for the alignment
long m_lAlignment;
};

The enumeration is added as an include file. By adding the enumeration to an include file, you are able to use the enumeration in other files simply by including the file reference, which will be necessary as you proceed through the chapter (see Listing 8.3).

Listing 8.3 ALIGNMENTENUMS.H--Alignment Enumeration Include File



#if !defined _ALIGNMENTENUMS_H
#define _ALIGNMENTENUMS_H
// caption alignment enumeration
typedef enum tagAlignmentEnum
{
EALIGN_LEFT = 0,
EALIGN_CENTER = 1,
EALIGN_RIGHT = 2,
}EALIGNMENT;
#define EALIGN_LEFT_TEXT "Left"
#define EALIGN_CENTER_TEXT "Center"
#define EALIGN_RIGHT_TEXT "Right"
#endif // #if !defined _ALIGNMENTENUMS_H

You initialize your member variables in the constructor of your control (see Listing 8.4).

Listing 8.4 ATLCONTROLWIN.H--Member Variable Initialization



. . .
public IConnectionPointContainerImpl<CATLControlWin>,
public ISpecifyPropertyPagesImpl<CATLControlWin>
{
public:
CATLControlWin()
{
// NULL terminate the string reference
m_lptstrCaption = new TCHAR[1];
m_lptstrCaption[0] = `\0';
// set the alignment to the default of left
m_lAlignment = EALIGN_LEFT;
}
DECLARE_REGISTRY_RESOURCEID(IDR_ATLCONTROLWIN)
. . .

The CaptionMethod contains all of the code for setting the caption and the alignment style, and like the MFC implementation, deals with the optional parameter correctly (see Listing 8.5). See Chapter 6 for more information about optional parameters and their use.

Since the CaptionMethod is used for the IDispatch implementation and the custom interface, the method is implemented in a slightly different way than its MFC counterpart. First the function is declared as STDMETHODIMP, which expands to an HRESULT return type. The return value is used by OLE to determine whether the method call succeeded. The string parameter is passed in differently also. All strings are passed as UNICODE in OLE. This is true even for MFC. The only difference is that MFC hides the implementation details of how the strings are managed; the developer simply uses the appropriate string data type based on the target application and platform, that is, Win32 ANSI versus Win32 UNICODE. Note the use of the USES_CONVERSION and W2A macros to convert the string from UNICODE to ANSI.

Useful Helper Functions and Conversion Macros
The files ATLCONV.H and ATLCONV.CPP, which can be found in the directory ...\DevStudio\VC\ATL\include, contain a number of helper functions and macros for converting data such as UNICODE strings to ANSI. Since ATL does not require the use of MFC, you are wise to examine these files before writing functions to convert data.




Next, if the VARIANT is of a valid data type other than VT_I4, the method tries to convert it to a VT_I4 type. You try to convert the data for the cases where a user passes valid data in the form of a different data type, for example, a short or a string.

One very important thing to note is the use of the function VariantInit:. All VARIANT variables must be initialized prior to their use. This practice guarantees that the VARIANT does not contain invalid data type information or invalid values. This practice follows the basic C++ tenet of initializing all member variables to ensure that they do not contain invalid information.

If the requirements of your control demand that you deal with only specific data types, you can also choose to add code (error messages, exceptions, and so on) to deal with the fact that the method did not receive a valid data type. If the function VariantChangeType is unable to convert the data, the method exits and returns a value of FALSE. A return of FALSE indicates to the caller of the method that the method didn't succeed. Again, you can also choose to add additional error handling code to the method to give the user more information about the error that occurred. See Chapters 3 through 5 on generating OLE exceptions for more information.

Before proceeding, the method ensures that the m_lAlignment member variable contains valid data.

If the method received valid data or converted the data to a valid value, as indicated by the variable lResult equaling TRUE, the method stores the caption and the alignment values in the class member variables, invalidates the control so it will redraw its User Interface (UI) based on the new information, and exits the function.

Listing 8.5 contains another important difference from that of its MFC counterpart: use of the function FireViewChange in place of the MFC InvalidateControl function to force the control to repaint itself. Wherever appropriate, we will point out the differences between MFC and ATL.

Listing 8.5 ATLCONTROLWIN.CPP--CaptionMethod Implementation



STDMETHODIMP CATLControlWin::CaptionMethod(BSTR bstrCaption, VARIANT
varAlignment, long * lRetVal)
{
// needed for the W2A macro
USES_CONVERSION;
HRESULT hResult = S_OK;
// return value initialized to failure result
*lRetVal = FALSE;
// convert the string to ANSI
LPTSTR lptstrTempCaption = W2A(bstrCaption);
// if the variant is a long just use the value
if(VT_I4 == varAlignment.vt)
{
// assign the value to our member variable
m_lAlignment = varAlignment.lVal;
// set the return value
*lRetVal = TRUE;
}
// if the user didn't supply an alignment parameter we will assign the default
else if(VT_ERROR == varAlignment.vt || VT_EMPTY == varAlignment.vt)
{
// assign the value to our member variable
m_lAlignment = EALIGN_LEFT;
// set the return value
*lRetVal = TRUE;
}
else
{
// get a variant that we can use for conversion purposes
VARIANT varConvertedValue;
// initialize the variant
::VariantInit(&varConvertedValue);
// see if we can convert the data type to something useful
// VariantChangeTypeEx() could also be used
if(S_OK == ::VariantChangeType(&varConvertedValue,
(VARIANT *) &varAlignment, 0, VT_I4))
{
// assign the value to our member variable
switch(varConvertedValue.lVal)
{
case EALIGN_CENTER:
m_lAlignment = EALIGN_CENTER;
break;
case EALIGN_RIGHT:
m_lAlignment = EALIGN_RIGHT;
break;
default:
m_lAlignment = EALIGN_LEFT;
break;
}
// set the return value
*lRetVal = TRUE;
}
else
{
// at this point we could either throw an error indicating
// there was a problem converting
// the data or change the return type of the method and
// return the HRESULT value from the
// the "VariantChangeType" call.
}
}
// if everything was OK
if(TRUE == *lRetVal)
{
// if we have a string
if(lptstrTempCaption != NULL)
{
// if we have a string
if(m_lptstrCaption)
{
// delete the existing string
delete [] m_lptstrCaption;
// clear the reference just to be safe
m_lptstrCaption = NULL;
}
// allocate a new string
m_lptstrCaption = new TCHAR[lstrlen(lptstrTempCaption) + 1];
// assign the string to our member variable
lstrcpy(m_lptstrCaption, lptstrTempCaption);
}
// did they pass us bad data?
if(m_lAlignment < EALIGN_LEFT || m_lAlignment > EALIGN_RIGHT)
// sure did, lets fix their little red wagon
m_lAlignment = EALIGN_LEFT;
// force the control to repaint itself
this->FireViewChange();
// this->InvalidateControl(); <== MFC Version
}
// return the result of the function call
return hResult;
}

Properties

Properties can be categorized as user defined, stock, or ambient.

User defined properties are properties that are implementation-specific and have meaning only to the component that contains them. User defined properties can be further broken into those properties that are defined only as their specific data type (normal properties) and those with additional parameters (parameterized properties).

Stock properties are a set of properties that are already defined by OLE in terms of the basic meaning. Stock properties are not implemented in the control of the container by default. They still require implementation by the control developer. They are predefined only to imply a certain level of uniformity between various control implementations.

Ambient properties, on the other hand, are properties that are supported by the container to provide a default value to the control that uses them.

In the remainder of this section, you will create normal, parameterized, and stock properties. You will also learn how to use ambient properties.

Creating Normal User Defined Properties

A normal property is a property that is declared as a single type, for example, long or BSTR, and has no parameters. You will expose your control's Alignment member variable through a property.

Properties are added in much the same way as methods. From the ClassView tab in the Project Workspace window, select the IATLControlWin interface, click the right mouse button, and select the Add Property menu item (see fig. 8.11).

FIG. 8.11
Add a new property to the control with the ATL ClassWizard.

In the Add Property to Interface dialog, set the Property Type to long, the Property Name to Alignment, and leave the remainder of the settings at their default values (see fig. 8.12). Click OK to confirm the entry and close the dialog.

FIG. 8.12
Define the Alignment property attributes.

After adding the property, you need to update the IDL file with a dispid constant name that can be used from within the control implementation source files. Listing 8.6 shows the typedef that is added to the IDL file. The dispids are added as an enumeration so that the MIDL compiler will generate an enumeration in the ATLControl.h header file, which defines the interfaces and classes available in the control IDL file. The reason for adding the dispids as a set of constants is the same reason for having any constant. If the value of the dispid were to change, you wouldn't have to search your source code trying to find where you used the value. Listing 8.6 also shows the change that is made to the Alignment property declaration functions to reflect the dispid constant.

Listing 8.6 ATLCONTROL.IDL--Dispid Enumeration Added to the IDL File to Aid in the Support of Properties in the Control

. . .
typedef enum propdispids
{
dispidAlignment = 2,
}PROPDISPIDS;
. . .
interface IATLControlWin : IDispatch
{
[id(1), helpstring("method CaptionMethod")]
HRESULT CaptionMethod([in] BSTR bstrCaption,
[in, optional] VARIANT varAlignment, [out, retval] long * lRetVal);
[propget, id(dispidAlignment), helpstring("property Alignment")]
HRESULT Alignment([out, retval] long *pVal);
[propput, id(dispidAlignment), helpstring("property Alignment")]
HRESULT Alignment([in] long newVal);
};
. . .

Open the file ATLControlWin.cpp so that you can modify the get_Alignment/put_Alignment functions. You could also select the functions from the Class View tab of the Project Workspace window.

As you can see, Listing 8.7 takes advantage of the member variable m_Alignment, which you added earlier, and uses the member to get and set the property value.

The GetAlignment function is simple in that it returns only the value stored in the m_lAlignment member variable.

The SetAlignment function does a little more. This function checks to see if the value is within the valid ranges of values and, if so, stores the value in the m_lAlignment member variable. The function then calls the SetDirty and the FireOnChanged functions--note the MFC equivalent functions still in the code--to notify the control and the container, respectively, that the value of the property has changed. FireOnChanged has the effect of forcing the container to refresh its property browser to reflect the new value. This step is very important because the value of the property could change without the container's knowledge, either through the control's property sheet or, in some cases, in response to another function call.

You might be asking "Why didn't I add FireOnChanged to the CaptionMethod?" Well, you could have, but it wouldn't do much because the CaptionMethod can never be executed while the control is in design mode, which is the purpose of FireOnChanged. The FireOnChanged function is where you make use of the dispid constants that were defined earlier.

The last thing the SetAlignment method does is to invalidate the control's UI so it will repaint using the new information.

Listing 8.7 ATLCONTROLWIN.CPP--Alignment Property Get/Put Method Implementation

STDMETHODIMP CATLControlWin::get_Alignment(long * pVal)
{
HRESULT hResult = S_OK;
// return our current setting
*pVal = m_lAlignment;
return hResult;
}
STDMETHODIMP CATLControlWin::put_Alignment(long newVal)
{
HRESULT hResult = S_OK;
// if we are in the valid range for the property
if(newVal >= EALIGN_LEFT && newVal <= EALIGN_RIGHT)
{
// set the new property value
m_lAlignment = newVal;
// let the control know that the property has changed
this->SetDirty(TRUE);
// this->SetModifiedFlag(); <== MFC version
// refresh the property browser
this->FireOnChanged(dispidAlignment);
// this->BoundPropertyChanged(dispidAlignment); <== MFC Version
// force the control to repaint itself
this->FireViewChange();
// this->InvalidateControl(); <== MFC Version
}
return hResult;
}

Creating Parameterized User Defined Properties

A parameterized property is a property that, in addition to being of a specific type (for example, BSTR or long), accepts one or more additional parameters to further define the data of the property. Parameterized properties can be useful for properties that represent collections of data where the additional parameter is the index into the collection.

You are going to expose the control's m_lptstrCaption member variable as a parameterized property in addition to your CaptionMethod function.

Parameterized properties are added in the same manner as normal properties. From the ClassView tab in the Project Workspace window, select the IATLControlWin interface, click the right mouse button, and select the Add Property menu item.

In the Add Property to Interface dialog, set the Property Type to BSTR and the Property Name to CaptionProp, add the Parameters string [in, optional] VARIANT varAlignment, and leave the remainder of the settings at their default values (see fig. 8.13). Click OK to confirm the entry and close the dialog.

FIG. 8.13
Define the Caption property propget function attributes.

NOTE: Even though the VARIANT varAlignment is defined as [optional] for both the get_CaptionProp and put_CaptionProp functions, only the get_CaptionProp implementation is truly optional. The parameter was added in this fashion because it is impossible to use the ATL ClassWizard to generate two separate functions each having the same name and ID. The [optional] attribute can be removed from the put_CaptionProp function if you want without adverse effect on the implementation.

As with the normal user defined parameter, you add a constant, dispidCaptionProp, to the IDL file (see Listing 8.8) that represents the dispid of the property. You also need to replace the id of the property with the newly added constant dispidCaptionProp.

Listing 8.8 ATLCONTROL.IDL--Update the IDL File to Support the Parameterized Property

. . .
typedef enum propdispids
{
dispidAlignment = 2,
dispidCaptionProp = 3,
}PROPDISPIDS;
[
object,
uuid(A19F6963-7884-11D0-BEF3-00400538977D),
dual,
helpstring("IATLControlWin Interface"),
pointer_default(unique)
]
interface IATLControlWin : IDispatch
{
[id(1), helpstring("method CaptionMethod")]
HRESULT CaptionMethod([in] BSTR bstrCaption,
[in, optional] VARIANT varAlignment, [out, retval] long * lRetVal);
[propget, id(dispidCaptionProp), helpstring("property CaptionProp")]
HRESULT CaptionProp([in, optional] VARIANT varAlignment,
[out, retval] BSTR *pVal);
[propput, id(dispidCaptionProp), helpstring("property CaptionProp")]
HRESULT CaptionProp([in] VARIANT varAlignment, [in] BSTR newVal);
[propget, id(dispidAlignment), helpstring("property Alignment")]
HRESULT Alignment([out, retval] long *pVal);
[propput, id(dispidAlignment), helpstring("property Alignment")]
HRESULT Alignment([in] long newVal);
};
. . .

The method get_CaptionProp is called to return data from the property. In your implementation, you are going to ignore the alignment parameter because it is of no use to you in this context; you simply return the caption (see Listing 8.9). You need to make sure that the string variable, BSTR * pVal, that is passed to the get_CaptionProp function does not already point to another string; if it does, you need to destroy it. Next get_CaptionProp uses the function SysAllocString to create a BSTR that is returned from the function call. Note that it is first necessary to convert the ANSI string to an OLECHAR string and then allocate a BSTR from that. The macros USES_CONVERSION and T2OLE accomplish this for you.

Listing 8.9 ATLCONTROLWIN.CPP--get_CaptionProp Implementation

STDMETHODIMP CATLControlWin::get_CaptionProp(VARIANT varAlignment, BSTR * pVal)
{
// needed for the T2OLE macro
USES_CONVERSION;
// if there is a string
if(*pVal)
{
// free the string because we are going to replace it
::SysFreeString(*pVal);
// clear the reference just to be safe
*pVal = NULL;
}
// convert the ANSI string to an OLECHAR and then allocate a BSTR
*pVal = ::SysAllocString(T2OLE(m_lptstrCaption));
return S_OK;
}

put_CaptionProp simply defers to the CaptionMethod implementation because the CaptionMethod already does everything that you need (see Listing 8.10).

Listing 8.10 ATLCONTROLWIN.CPP--SetCaptionProp Implementation

STDMETHODIMP CATLControlWin::put_CaptionProp(VARIANT varAlignment, BSTR newVal)
{
long lRetVal;
// defer to the CaptionMethod implementation
HRESULT hResult = this->CaptionMethod(newVal, varAlignment, &lRetVal);
// if the function returned success
if(TRUE == lRetVal)
// let the control know that the property has changed
this->SetDirty(TRUE);
// this->SetModifiedFlag(); <== MFC version
// return the result
return hResult;
}

Creating Stock Properties

A stock property is a property that is understood by a control and its container and that has a predefined meaning to both. Stock properties are intended to provide basic uniform functionality to all the controls and containers that implement them. Stock properties do not require you to implement a lot of code; you just hook into the existing property.

Stock properties can be added to a control in two ways. The first way is during the actual creation of the control using the ATL Object Wizard. You may recall that earlier in the chapter one of the options in the ATL Object Wizard Properties dialog was the Stock Properties tab (refer to fig. 8.8). If you add the stock properties at this point, the ATL Object Wizard will add the class CStockPropImpl<...> to your class declaration and will add the necessary IDL get_/put_ function declarations for each one of the properties.

The Object Wizard will also add a member variable to your class for each one of the properties. The CStockPropImpl<...> template class contains declarations for all of the available stock properties in the form of IMPLEMENT_STOCKPROP and IMPLEMENT_BSTR_STOCKPROP macros. The macros define all of the appropriate get_/put_ function implementations for you; you need to use only the member variable when you want to use the stock property. If you want to, you can also add the CStockPropImpl<...> class after your control object has been created and follow the same steps that were outlined in the preceding paragraph when creating stock properties. Note that the CStockPropImpl<...> class replaces the IDispatchImpl<...> class.

The second method of adding a stock property is the same method as for any other user defined property. From the ClassView tab in the Project Workspace window, select the IATLControlWin class, click the right mouse button, and select the Add Property menu item.

NOTE: The documentation about the use of the IMPLEMENT_STOCKPROP and IMPLEMENT_BSTR_STOCKPROP macros within your class is somewhat misleading. The macros depend on the CStockPropImpl<...> class, and they cannot simply be added to your control implementation, as is implied by the documentation on ATL support of stock properties.



In the Add Property to Interface dialog, set the Property Type to OLE_COLOR, the Property Name to BackColor, and leave the remainder of the settings at their default values (see fig. 8.14). Click OK to confirm the entry and close the dialog.

FIG. 8.14
Add the BackColor stock property to the control with the ATL ClassWizard.

The next step is to modify the IDL file to reflect the correct dispid for the BackColor property. In this case, you need to replace the id with DISPID_BACKCOLOR.

Listing 8.11 ATLCONTROL.IDL--Add the Constant DISPID_BACKCOLOR to the IDL to Support the BackColor Stock Property

. . .
interface IATLControlWin : IDispatch
{
[id(1), helpstring("method CaptionMethod")]
HRESULT CaptionMethod([in] BSTR bstrCaption,
[in, optional] VARIANT varAlignment, [out, retval] long * lRetVal);
[propget, id(DISPID_BACKCOLOR), helpstring("property BackColor")]
HRESULT BackColor([out, retval] OLE_COLOR *pVal);
[propput, id(DISPID_BACKCOLOR), helpstring("property BackColor")]
HRESULT BackColor([in] OLE_COLOR newVal);
[propget, id(dispidCaptionProp), helpstring("property CaptionProp")]
HRESULT CaptionProp([in, optional] VARIANT varAlignment,
[out, retval] BSTR *pVal);
[propput, id(dispidCaptionProp), helpstring("property CaptionProp")]
HRESULT CaptionProp([in, optional] VARIANT varAlignment,
[in] BSTR newVal);
[propget, id(dispidAlignment), helpstring("property Alignment")]
HRESULT Alignment([out, retval] long *pVal);
[propput, id(dispidAlignment), helpstring("property Alignment")]
HRESULT Alignment([in] long newVal);
};
. . .

Next you add a member variable to your class declaration that will store the BackColor property (see Listing 8.12).

Listing 8.12 ATLCONTROLWIN.H--m_BackColor Member Variable Added to the Class Declaration

. . .
protected:
// storage variable for the caption
LPTSTR m_lptstrCaption;
// storage variable for the alignment
long m_lAlignment;
// storage variable for the BackColor stock property
OLE_COLOR m_BackColor;
};
#endif //__ATLCONTROLWIN_H_

Finally you must update the get_BackColor/put_BackColor functions to return and store the BackColor property (see Listing 8.13).

Listing 8.13 ATLCONTROLWIN.CPP--BackColor Property Source File Implementation

STDMETHODIMP CATLControlWin::get_BackColor(OLE_COLOR * pVal)
{
// return the color
*pVal = m_BackColor;
return S_OK;
}
STDMETHODIMP CATLControlWin::put_BackColor(OLE_COLOR newVal)
{
// if the value is the same
if(newVal == m_BackColor)
return S_OK;
// store the value
m_BackColor = newVal;
// let the container know that the value has changed
this->FireOnChanged(DISPID_BACKCOLOR);
// this->AmbientPropertyChanged(DISPID_BACKCOLOR); <== MFC Version
// redraw the control
this->FireViewChange();
// this->InvalidateControl(); <== MFC Version
// set the dirty flag
this->SetDirty(TRUE);
// this->SetModifiedFlag(); <== MFC Version
// exit
return S_OK;
}

The last thing is to initialize the m_BackColor member within the constructor of the class (see Listing 8.14).

Listing 8.14 ATLCONTROLWIN.H--Initialize the BackColor Property to an Initial Value

. . .
CATLControlWin()
{
// NULL terminate the string reference
m_lptstrCaption = new TCHAR[1];
m_lptstrCaption[0] = `\0';
// set the alignment to the default of left
m_lAlignment = EALIGN_LEFT;
// set the backcolor to the system default
m_BackColor = 0x80000000 | COLOR_WINDOW;
}
. . .

Regardless of the implementation style that you use, you can now access the BackColor stock property through the member variable in your class, just as you do any other property. This implementation differs from the MFC implementation of stock properties, which hides all of the details regarding their implementation from you.

Using Ambient Properties

Ambient properties are properties implemented in the container in which the control resides, as opposed to stock properties, which are implemented in the control and not the container. Ambient properties share the same set of predefined meanings and dispids as those of stock properties. To use an ambient property, the control must request only the property value from the container and apply it in whatever manner is appropriate for the property type. The use of ambient properties allows the control to conform to the same settings as those of the container in which it resides. This procedure provides much better integration between the control and its container.

Take the previous example of adding the BackColor stock property to the sample control implementation. Defined as a stock property, the user of the control can change the background color of the control or leave it as is. If the color is different from that of the container or if the container's background color changes for some reason, the colors won't match and will give the appearance of a poorly integrated and written application. However, if the control simply used the ambient background color of its container, the control's background will always match that of the container. The specific requirements of your control implementation will decide which route you choose when implementing the properties your control supports.

To access an ambient property, you can call one of the many ambient property functions defined in the CComControlBase class, for example, GetAmbientBackColor() in the case of the BackColor property.

Creating Property Sheets

Property sheets are a way for a control to display its properties for review and editing using a tabbed-dialog format. The original intent of property sheets were for the cases when the control container did not support property browsing facilities. While property sheets have their purpose, they are probably not necessary for all implementations. Your specific requirements will determine whether your control should contain a property sheet. The official OLE line is that all controls should have property sheets. This is true for commercially developed and distributed controls but is probably not the case for in-house implementations. The majority of development environments already have excellent property browsing facilities. Implement property sheets only if you feel that you absolutely have to.

Removing the property sheets and their corresponding implementation infrastructure will definitely reduce the size of your control and should not take away from its implementation in any way.

Before you can work on the implementation of the property sheet, you must add a property sheet object to your control. This is done through the ATL Object Wizard. From the Insert menu, select the New ATL Object menu item. Within the ATL Object Wizard, select the Controls item in the left panel to display the types of control components that can be added (see fig. 8.15). Your implementation will be a Property Page, so select the Property Page icon. Click the Next button to continue.

FIG. 8.15
Add a property page object to your class with the ATL Object Wizard.

Select the Names tab in the ATL Object Wizard Properties dialog, and in the Short Name edit field, type ATLControlWinPPG; the remainder of the edit fields will automatically update, reflecting the short name that you added (see fig. 8.16). The other fields can be changed, but in this case, you use the default values.

FIG. 8.16
Enter the name of the property page object.

Select the Strings tab, and enter the string General to the Title edit field and ATLControlWin Property Page to the Doc String edit fields. The Helpfile edit field is where you add the name of the help file associated with the property page; for now, leave the value at its default setting. Click OK to add the property page object to your control.

The ATL Object Wizard adds all of the necessary code to your control implementation to include the new property page object, including the OBJECT_MAP, the IDL file, and all of the registry and resource information.

Because property sheets are tabbed dialogs, most of your work is done with the dialog editor. Select the Resource View in the Project Workspace window. From the list of dialogs, select IDD_ATLCONTROLWINPPG, and double-click the entry to open the resource editor.

Using the resource editor, remove the static text control with the caption Insert your controls here, and place a static text control and a combo box on the dialog.

Using the mouse, select the label control on the form, and click the right mouse button. In the menu that appears, select the Properties menu item. On the General tab, set the ID of the control to IDC_ALIGNMENTLABEL, and set the Caption to Alignment; you see the control in Figure 8.17. Select the Styles tab, and set the Align Text property to Right. Close the dialog to save the information.

FIG. 8.17
Add the controls to the Property Sheet dialog to display your property data.

Again, using the mouse, select the combo box, use the right mouse to click the control, and in the menu that appears, select the Properties menu item. On the General tab, set the ID of the control to IDC_ALIGNMENTCOMBO. On the Styles tab, set the Type to Dropdown, and uncheck the Sort check box. Close the dialog to save the information.

You have placed your two controls onto the property sheets and successfully modified their properties. Now you need to add some code to complete the implementation. Close the resource editor, and open the file ATLControlWinPPG.h. Your implementation of the property page requires several resource definitions and the Alignment enumeration that you created earlier. The values are added to the property page header file in the form of include statements (see Listing 8.15).

Listing 8.15 ATLCONTROLWINPPG.H--Add the Necessary Include Files to the Property Page Header File

. . .
#include "resource.h" // main symbols
#include "alignmentenums.h"
#include "ATLControl.h"
EXTERN_C const CLSID CLSID_ATLControlWinPPG;
. . .

Since the property page is based on a dialog, you add a message handler for the WM_INITDIALOG message so that you can load the Alignment combo box with the value from the control's property when the property page is first created (see Listing 8.16). ATL message maps are very similar to MFC; however, ATL lacks the ClassWizard support that makes MFC so attractive. The message handler consists of a single line added to the BEGIN_MSG_MAP macro. This line identifies the message and receiving function that you want to implement, in this case WM_INITDIALOG and OnInitDialog. The OnInitDialog function retrieves the window handle to the Alignment combo box and resets its content. Next you add the three string representations of the different Alignment styles possible to the combo box. Then you retrieve a pointer to the control and get the current property value of the Alignment property, and using the value, set the current selection in the Alignment combo box. Note the use of the standard Win32 SendMessage function to update the data and settings of the combo box, which is different from MFC, which has classes that wrap most of the core Win32 functions.

Listing 8.16 ATLCONTROLWINPPG.H--Add the OnInitDialog Message Handler so the Property Page Can Be Initialized

. . .
BEGIN_MSG_MAP(CATLControlWinPPG)
MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)
COMMAND_HANDLER(IDC_ALIGNMENTCOMBO, CBN_SELCHANGE,
OnAlignmentComboSelChange)
CHAIN_MSG_MAP(IPropertyPageImpl<CATLControlWinPPG>)
END_MSG_MAP()
LRESULT OnInitDialog(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL & bHandled)
{
// get the window handle of the combobox control
HWND hTempWnd = ::GetDlgItem(m_hWnd, IDC_ALIGNMENTCOMBO);
// make sure that the control is empty
::SendMessage(hTempWnd, CB_RESETCONTENT, 0, 0);
// set the selection strings in the control - it is important that the control
// be unsorted since the entries index will relate to the property setting
::SendMessage(hTempWnd, CB_ADDSTRING, 0, (LPARAM)(LPCTSTR) EALIGN_LEFT_TEXT);
::SendMessage(hTempWnd, CB_ADDSTRING, 0, (LPARAM)(LPCTSTR)
EALIGN_CENTER_TEXT);
::SendMessage(hTempWnd, CB_ADDSTRING, 0, (LPARAM)(LPCTSTR) EALIGN_RIGHT_TEXT);
// see if the object supports the interface we need
CComQIPtr<IATLControlWin, &IID_IATLControlWin>pControl(m_ppUnk[0]);
long lAlignment;
// set the control with the new value and if it failed
pControl->get_Alignment(&lAlignment);
// get the current selection in the listbox
::SendMessage(::GetDlgItem(m_hWnd, IDC_ALIGNMENTCOMBO),
CB_SETCURSEL, lAlignment, 0);
return TRUE;
}
LRESULT OnAlignmentComboSelChange(WORD wNotify, WORD wID, HWND hWnd,
BOOL& bHandled)
{
SetDirty(TRUE);
return FALSE;
}
. . .

The property page contains a combo box that the user can use to change the value of the Alignment property. When the user changes the value, the property page has to be notified of the change so that the property page will update the control with the new property value. As for the WM_INITDIALOG message, you are required to add a WM_COMMAND message handler for the CBN_SELCHANGE message that the combo box will fire when its value changes. Adding the message handler is done through a slightly different type of message map called a COMMAND_HANDLER, which assumes that the primary message is WM_COMMAND (see Listing 8.16) and breaks the submessage into its appropriate values for you. The function OnAlignmenComboSelChange simply sets the dirty flag for the property page and exits.

The last step is the actual updating of the control's properties when the property page exits. By setting the dirty flag of the property page, you instruct the Apply function to execute, which allows you the opportunity to update all the property values that the property page is responsible for. The Apply function is already supplied to you as part of the property page implementation (see Listing 8.17). You need only add your support for your specific properties.

Since more than one object may have a reference to the property page, you are required to notify all of the objects that the properties have changed. The Apply function cycles through all of the references checking to see whether each contains the interface that you are looking for. If you find an interface that you can use, you then need only call the appropriate member functions for each of the properties that you support. If, for some reason, the setting of the property causes an error, the property page notifies the user with an error message. You then reset the m_bDirty flag to FALSE and exit the function. At this point, the property dialog closes, and control returns to the development environment hosting the control.

Listing 8.17 ATLCONTROLWINPPG.H--Modify the Apply Function to Update All of the Properties in the Control When the Property Page Exits

. . .
STDMETHOD(Apply)(void)
{
USES_CONVERSION;
ATLTRACE(_T("CATLControlWinPPG::Apply\n"));
for (UINT i = 0; i < m_nObjects; i++)
{
// see if the object supports the interface we need
CComQIPtr<IATLControlWin, &IID_IATLControlWin> pControl(m_ppUnk[i]);
// get the current selection in the listbox
long lAlignment = (long) ::SendMessage(
::GetDlgItem(m_hWnd, IDC_ALIGNMENTCOMBO), CB_GETCURSEL, 0, 0);
// set the control with the new value and if it failed
if FAILED(pControl->put_Alignment(lAlignment))
{
// generate an error message
CComPtr<IErrorInfo> pError;
CComBSTR strError;
GetErrorInfo(0, &pError);
pError->GetDescription(&strError);
MessageBox(OLE2T(strError), _T("Error"), MB_ICONEXCLAMATION);
return E_FAIL;
}
}
m_bDirty = FALSE;
return S_OK;
}
. . .

The property page implementation is complete; however, the control is still not aware of the property page's existence and that the control and the property page are related. To connect the property page to the control, you must add an entry to the BEGIN_PROPERTY_MAP macro in the class definition of the control (see Listing 8.18). The macro PROP_ENTRY is used in place of the PROP_PAGE macro so that the property will be persisted automatically when the control is saved.

Listing 8.18 ATLCONTROLWIN.H--Add the Property Page Reference to the Control Class Declaration to Complete the Property Page Implementation

. . .
BEGIN_PROPERTY_MAP(CATLControlWin)
// PROP_ENTRY("Description", dispid, clsid)
PROP_ENTRY("Alignment", dispidAlignment, CLSID_ATLControlWinPPG)
PROP_PAGE(CLSID_CColorPropPage)
END_PROPERTY_MAP()
. . .

Adding Events

Properties and methods are a way for a programmer to communicate with a control from within the container application. Events are a way for the control to communicate with the container. For ActiveX controls, events are nothing more than IDispatch interfaces that are implemented on the container side of the container/control relationship. The mechanism that events are based on is known as a connection point. A connection point is simply a description of the type of interface that is required in order to communicate with the container. Connection points are not restricted to only IDispatch interfaces; rather, they can be of any COM interface that is understood by both components. For that matter, connection points/events are not restricted only to controls; they can be used in any COM implementation. Controls were simply the first to take advantage of them. For more information regarding connection points, refer to the documentation in the OLE online help or to Kraig Brockschmidt's Inside OLE, Second Edition, published by Microsoft Press.

Because no ClassWizards are available to aid you, adding events in ATL requires a bit more work than is required in MFC and BaseCtl. The first step is to add the event interface to your IDL file (see Listing 8.19). Remember to create a new UUID with the GUIDGEN.EXE program; do not reuse the UUIDs in the sample. In addition to the event interface, you need an event method. You also need to add the method called Change to the event interface. The Change method has two parameters. The first parameter is a string called bstrCaption, passed by reference (BSTR *). The second is a long called lAlignment, also passed by reference (long *). You are passing the parameters by reference to allow the container application the opportunity to change the values if necessary. You must also add the event interface to the CoClass interface for the control as the default, source interface.

Listing 8.19 ATLCONTROL.IDL--Add the Event Interface and the Change Method to the ATLControl.IDL File

. . .
[
uuid(C31D4C71-7AD7-11d0-BEF6-00400538977D),
helpstring("ATLControlWin Event Interface")
]
dispinterface _DATLControlWin
{
properties:
methods:
[id(1)] void Change([in, out]BSTR * bstrCaption,
[in, out] long * lAlignment);
};
[
uuid(A19F6964-7884-11D0-BEF3-00400538977D),
helpstring("ATLControlWin Class")
]
coclass ATLControlWin
{
[default] interface IATLControlWin;
[default, source] dispinterface _DATLControlWin;
};
. . .

Event interfaces are based on connection point interfaces, which you will add to your control implementation. Connection points are implemented with the IConnectinPoint and the IConnectionPointContainer interfaces. To aid in the creation of the interfaces, ATL provides a proxy generator. To generate the proxy code follow these steps:

  1. From the Project menu, select the Add to Project menu item, and then select the Com_ponents and Controls submenu item.

  2. In the Components and Controls Gallery dialog, double-click the Developer Studio Components folder.

  3. After the Components and Controls Gallery dialog is refreshed with data, double-click the ATL Proxy Generator icon (see fig. 8.18).

    FIG. 8.18
    Select the ATL Proxy Generator from the Developer Studio Components.

  4. Click OK to close the Insert the ProxyGen Component dialog.

  5. Click the ... button, in the ATL Proxy Generator dialog to display the Open dialog. Select the ATLControl.tlb file, and click Open.

  6. Select the _DATLControlWin entry in the Not Selected list box, and click the > button to move the entry to the Selected list box (see fig. 8.19). Ensure that the Proxy Type is set to Connection Point, and click Insert.

    FIG. 8.19
    Select the _DATLControlWin class in the ATL Proxy Generator dialog.

  7. A Save dialog appears with the file CPATLControl.h in the File name edit box. Click Save to continue. Click OK in the confirmation dialog that indicates that the operation was successful.

  8. Click Close in the ATL Proxy Generator and Components and Controls Gallery dialogs.

The file CPATLControl.h contains that class CProxy_DATLControlWin, which implements your event interface, and the Change event method in the form Fire_Change, which you added earlier to the IDL file.

The next step is to add the CProxy_DATLControlWin interface to your control implementation. Add the CPATLControl.h file as an include file to your ATLControlWin.h file:

#include "CPATLControl.h

You must also update the inheritance structure of the CATLControlWin class with the CProxy_DATLControlWin class

public CProxy_DATLControlWin<CATLControlWin>

and add IID of the event interface to the IProvideClassInfo2Impl interface

public IProvideClassInfo2Impl<&CLSID_ATLControlWin, &DIID__DATLControlWin, &LIBID_ATLCONTROLLib>

See the ATL and ActiveX documentation for information regarding the implementation of the IProvideClassInfo2 interface.

Last you add the event interface to the BEGIN_CONNECTION_POINT_MAP macro in the control's class declaration:

BEGIN_CONNECTION_POINT_MAP(CATLControlWin)
CONNECTION_POINT_ENTRY(DIID__DATLControlWin)
END_CONNECTION_POINT_MAP()

Your event interface is now completely implemented. The last step is only a matter of adding the Fire_Change method calls wherever appropriate in your control implementation. Since your implementation of the Fire_Change method allows the user of the control to change the data that is passed to the event, using a universal helper function is easier to maintain the code by implementing a simple helper function, FireChange (with no parameters), that encapsulates the data management associated with the method and its parameters (see Listing 8.20). Remember to add the FireChange function prototype to your class definition in the ATLCONTROLWIN.H header file.

Listing 8.20 ATLCONTROLWIN.CPP--FireChange Helper Function Added to the Control



void CATLControlWin::FireChange(void)
{
// needed for the W2A macro
USES_CONVERSION;
// get a BSTR that can be passed via the event
BSTR bstrCaption = ::SysAllocString(T2OLE(m_lptstrCaption));
// fire the change event
this->Fire_Change(&bstrCaption, &m_lAlignment);
// convert the string to ANSI
LPTSTR lptstrTempCaption = W2A(bstrCaption);
// free the data that was passed back
::SysFreeString(bstrCaption);
// if we have a string
if(m_lptstrCaption)
{
// delete the existing string
delete [] m_lptstrCaption;
// clear the reference just to be safe
m_lptstrCaption = NULL;
}
// allocate a new string
m_lptstrCaption = new TCHAR[lstrlen(lptstrTempCaption) + 1];
// assign the string to our member variable
lstrcpy(m_lptstrCaption, lptstrTempCaption);
}

Finally your event code is completely implemented. The common FireChange function allows you to hide the details surrounding the change event from the rest of the program. If you decide to change the FireChange implementation in the future, it will impact only one function rather than a number of them.

The CaptionMethod will require that you fire a Change event if the data changes, so you add your new event to the CaptionMethod (see Listing 8.21). You also want to add the FireChange event to the put_Alignment function, but do not add FireChange to the put_CaptionProp function because it defers to the CaptionMethod for its implementation. Also, do not forget to add the FireChange call to any new functions that are added to the control as its implementation progresses.

Listing 8.21 ATLCONTROLWIN.CPP--FireChange Event Added to the CaptionMethod Implementation



STDMETHODIMP CATLControlWin::CaptionMethod(BSTR bstrCaption, VARIANT

varAlignment,
long * lRetVal)
{
. . .
// if everything was OK
if(TRUE == *lRetVal)
{
// if we have a string
if(lptstrTempCaption != NULL)
{
// if we have a string
if(m_lptstrCaption)
{
// delete the existing string
delete [] m_lptstrCaption;
// clear the reference just to be safe
m_lptstrCaption = NULL;
}
// allocate a new string
m_lptstrCaption = new TCHAR[lstrlen(lptstrTempCaption) + 1];
// assign the string to our member variable
lstrcpy(m_lptstrCaption, lptstrTempCaption);
}
// did they pass us bad data?
if(m_lAlignment < EALIGN_LEFT || m_lAlignment > EALIGN_RIGHT)
// sure did, lets fix their little red wagon
m_lAlignment = EALIGN_LEFT;
// fire the global change event
this->FireChange();
// force the control to repaint itself
this->FireViewChange();
// this->InvalidateControl(); <== MFC Version
}
// return the result of the function call
return hResult;
}

Persistence

Persistence refers to the capability of a component to retain its state across execution lifetimes. In other words, regardless of the number of times that the control is started and stopped, it remembers that you changed its background color from white to mauve (even if it finds the color mauve revolting).

Property persistence in ATL is by far the simplest of all of the frameworks currently available. Just add the property to the BEGIN_PROPERTY_MAP macro (see Listing 8.22), and you are finished; the rest is up to ATL.

Listing 8.22 ATLCONTROLWIN.H--BackColor Property Added to the Property Map for Persistence

. . .
BEGIN_PROPERTY_MAP(CATLControlWin)
// PROP_ENTRY("Description", dispid, clsid)
PROP_ENTRY("Alignment", dispidAlignment, CLSID_ATLControlWinPPG)
PROP_ENTRY("BackColor", DISPID_BACKCOLOR, CLSID_ATLControlWinPPG)
PROP_PAGE(CLSID_CColorPropPage)
END_PROPERTY_MAP()
. . .

Drawing the Control

Most controls will have some form of User Interface (UI). However, since the release of the OC 96 specification and ActiveX, UI is no longer a requirement.

Drawing the UI of a control has long been regarded as one of the most critical aspects of a control, in terms of both appearance and performance. A control that flashes or flickers a lot appears poorly developed, regardless of its internal implementation. The same is true for how fast the control draws. With the advent of the Internet and ActiveX, it is even more crucial that a control draw fast and well.

Drawing can be broken into two major categories: standard and optimized. In this chapter, you focus only on standard drawing. Chapter 9 introduces optimized drawing.

Standard Drawing

Standard drawing is just that: standard. You have complete freedom to draw the control any way you see fit, using any method that is appropriate. You can use pens and brushes, rectangles and circles. Remember that drawing smart is the goal of any application with UI.

TIP: Probably the greatest sources of flicker and flash are overlapped painting and unnecessary drawing -- drawing areas of the control that do not need to be drawn. Try to draw only to the areas of the control that have been invalidated. Doing so will save time and prevent annoying flash. For example, if your control has a white background and a black border, draw only the white portion where it is going to be seen. Don't draw over the border as this causes the control to flash every time it gets a paint message.



Before implementing the OnDraw method, you need to add a number of member variables and functions to aid in your drawing implementation (see Listing 8.23).

Listing 8.23 ATLCONTROLWIN.H--Drawing Implementation Member Variables and Functions

IFont * m_pFont;
void LoadFont(void);
HBRUSH hBrush, hOldBrush;
COLORREF TranslateColor(OLE_COLOR clrColor, HPALETTE hpal = NULL)
{COLORREF cr = RGB(0x00,0x00,0x00);OleTranslateColor(clrColor, hpal, &cr);
return cr;}
void FillSolidRect(HDC hDC, LPCRECT lpRect, COLORREF clr)
{::SetBkColor(hDC, clr);
::ExtTextOut(hDC, 0, 0, ETO_OPAQUE, lpRect, NULL, 0, NULL);}
void GetTextExtent(HDC hDC, LPCTSTR lpctstrString, int & cx, int & cy);
BOOL bRetrievedDimensions;
int iCharWidthArray[256];
int iCharacterSpacing, iCharacterHeight;
};
#endif //__ATLCONTROLWIN_H_

Initialize the member variables in the constructor of the class (see Listing 8.24) to useful starting values. It is very important to initialize the members because the implementation is dependent on the members being either NULL, indicating that they are empty, or not NULL, indicating that they contain a valid value.

Listing 8.24 ATLCONTROLWIN.H--Initialize the New Member Variables in the Constructor

CATLControlWin()
{
. . .
// clear the font
m_pFont = NULL;
// clear the brush
hOldBrush = hBrush = NULL;
// clear the flag
bRetrievedDimensions = FALSE;
}

Next add the implementation for the helper functions GetTextExtent and LoadFont. You also need a default font descriptor in the case when you cannot retrieve the font from the container (see Listing 8.25).

GetTextExtent is a function that is supported in MFC but not in Win32, so we created our own implementation. The function will determine the width and height of the font of the current DC and will then calculate the size in points of the string that was supplied to the function. This function is used for displaying your text with the correct alignment; left, right, or center. We optimized the method so as to retrieve the information only once. If the control supported fonts for properties, it would be a simple matter to clear the flag bRetrievedDimensions to refresh the width and height of the new font when the control redrew itself.

The function LoadFont tries to load the ambient font from the container and, if unable, creates a new font from the default settings.

Listing 8.25 ATLCONTROLWIN.CPP--Drawing Helper Functions

static FONTDESC _fdDefault =
{
sizeof(FONTDESC),
L"MS Sans Serif",
FONTSIZE(8),
FW_NORMAL,
DEFAULT_CHARSET,
FALSE,
FALSE,
FALSE
};
void CATLControlWin::LoadFont(void)
{
// if there isn't a font object
if(!m_pFont)
// get the font from the container
this->GetAmbientFont(&m_pFont);
// if there still isn't a font object
if(!m_pFont)
// create a default font object
::OleCreateFontIndirect(&_fdDefault, IID_IFont, (void **) &m_pFont);
}
void CATLControlWin::GetTextExtent(HDC hDC, LPCTSTR lpctstrString, int & cx, int & cy)
{
// if we haven't gotten the dimensions yet
if(!bRetrievedDimensions)
{
// get all of the widths for all of the chars
::GetCharWidth(hDC, 0, 255, &iCharWidthArray[0]);
// get the spacing between the chars
iCharacterSpacing = ::GetTextCharacterExtra(hDC);
// make sure that this only executes once
bRetrievedDimensions = TRUE;
// get the metrics of this DC
TEXTMETRIC tmMetrics;
::GetTextMetrics(hDC, &tmMetrics);
// get the height
iCharacterHeight = tmMetrics.tmHeight;
}
// return the height
cy = iCharacterHeight;
// set the initial value to 0
int iTextWidth = 0;
// get the number of characters in our string
long lTextLength = lstrlen(lpctstrString);
// if we have a character
if(lTextLength)
{
long lEndCharPos = lTextLength - 1;
// add up the widths of the characters and the spacing
for(long lCount = 0; lCount <= lEndCharPos; lCount++)
iTextWidth += (iCharWidthArray[(BYTE)(lpctstrString[lCount])]
+ iCharacterSpacing);
}
// return the width
cx = iTextWidth;
}

Listing 8.26 contains the code for drawing the control. The OnDraw implementation is fairly straightforward. Select the font into the DC, get all of the colors that you are going to use, and draw the background, text, and border. Last reset everything back to the way it was when you started.

Listing 8.26 ATLCONTROLWIN.CPP--Standard Drawing Added to the OnDraw Function

HRESULT CATLControlWin::OnDraw(ATL_DRAWINFO & di)
{
// ****** Get the text font ******
// **
HFONT hFont = NULL, hOldFont = NULL;
// if there isn't a font object
if(!m_pFont)
// try to load one
this->LoadFont();
if(m_pFont)
{
// get a font handle
m_pFont->get_hFont(&hFont);
// increment the ref count so the font doesn't drop
// out from under us
m_pFont->AddRefHfont(hFont);
::SelectObject(di.hdcDraw, hFont);
}
// **
// ****** Get the text font ******
// ****** Get the colors ******
// **
// use the window color as the background color
OLE_COLOR tColor;
this->get_BackColor(&tColor);
COLORREF clrTextBackgroundColor = this->TranslateColor(tColor);
// then use the normal windows color for the text
COLORREF clrTextForegroundColor =
this->TranslateColor(::GetSysColor(COLOR_WINDOWTEXT));
// set to the system color
COLORREF clrEdgeBackgroundColor = ::GetSysColor(COLOR_3DFACE);
COLORREF clrEdgeForegroundColor = ::GetSysColor(COLOR_3DFACE);
// **
// ****** Get the colors ******
// ****** Draw the background ******
// **
// set the text color
COLORREF clrOldBackgroundColor = ::SetBkColor(di.hdcDraw, clrTextBackgroundColor);
COLORREF clrOldForegroundColor = ::SetTextColor(di.hdcDraw,
clrTextForegroundColor);
// if we don't have a brush
if(hBrush == NULL)
// create a solid brush
hBrush = ::CreateSolidBrush(clrTextBackgroundColor);
// select the brush and save the old one
hOldBrush = (HBRUSH)::SelectObject(di.hdcDraw, hBrush);
// draw the background
::Rectangle(di.hdcDraw, di.prcBounds->left, di.prcBounds->top,
di.prcBounds->right, di.prcBounds->bottom);
// **
// ****** Draw the background ******
// ****** Draw the text ******
// **
int iHor, iVer;
// get the size of the text for this DC
int cx = 0, cy = 0;
this->GetTextExtent(di.hdcDraw, m_lptstrCaption, cx, cy);
switch(m_lAlignment)
{
case EALIGN_CENTER:
iHor = (di.prcBounds->right - cx) / 2;
iVer = di.prcBounds->top + 3;
break;
case EALIGN_RIGHT:
iHor = di.prcBounds->right - cx - 3;
iVer = di.prcBounds->top + 3;
break;
// case EALIGN_LEFT:
default:
iHor = di.prcBounds->left + 3;
iVer = di.prcBounds->top + 3;
break;
}
// output our text
::ExtTextOut(di.hdcDraw, iHor, iVer, ETO_CLIPPED | ETO_OPAQUE,
(LPCRECT) di.prcBounds, m_lptstrCaption, lstrlen(m_lptstrCaption), NULL);
// **
// ****** Draw the text ******
// ****** Draw the border ******
// **
// set the edge style and flags
UINT uiBorderStyle = EDGE_SUNKEN;
UINT uiBorderFlags = BF_RECT;
// set the edge color
::SetBkColor(di.hdcDraw, clrEdgeBackgroundColor);
::SetTextColor(di.hdcDraw, clrEdgeForegroundColor);
// draw the 3D edge
::DrawEdge(di.hdcDraw, (LPRECT)(LPCRECT) di.prcBounds,
uiBorderStyle, uiBorderFlags);
// **
// ****** Draw the border ******
// ****** Reset the colors ******
// **
// restore the original colors
::SetBkColor(di.hdcDraw, clrOldBackgroundColor);
::SetTextColor(di.hdcDraw, clrOldForegroundColor);
// **
// ****** Reset the colors ******
// ****** release the text font ******
// **
if(hOldFont)
// select the old object
::SelectObject(di.hdcDraw, hOldFont);
// increment the ref count so the font doesn't drop
// out from under us
if(m_pFont && hFont)
m_pFont->ReleaseHfont(hFont);
// **
// ****** Get the text font ******
// select the old brush back
::SelectObject(di.hdcDraw, hOldBrush);
// destroy the brush we created
::DeleteObject(hBrush);
// clear the brush handles
hBrush = hOldBrush = NULL;
return S_OK;
}

From Here...

This chapter focused on creating a basic control implementation. You added methods, properties, and events, which are the backbone of every control implementation. This chapter also addressed the issues of persistence and drawing without which a control implementation is definitely incomplete.

Version 2.1 has brought ATL into the realm of complete ActiveX development frameworks alongside MFC. The integration of ATL into the VC++ IDE was long overdue and sorely needed. Finally there is a competitor for MFC in terms of rapid ActiveX/COM development, with the added benefit of the ATL framework being lean and to the point. With ATL being so close to the COM interface level, you also have the added benefit of seeing how truly simple and easy it is to implement COM interfaces and components.

The only drawbacks to ATL might be its support of events and some of its tools for creating components. We are certain, however, that ATL will continue to mature and be the premier ActiveX development framework available.

Chapter 9 expands upon the knowledge you gained in this chapter and adds new features and function to your control implementation to make your control truly unique and interesting.