Chapter 31

Creating ActiveX Controls with ATL

Microsoft has positioned ActiveX as a one-size-fits-all solution for application and control developers. Unfortunately, writing ActiveX controls has been a difficult process that requires a great deal of specialized knowledge. One of the primary benefits of the Active Template Library (ATL) is that it greatly eases the pain of writing ActiveX code. 

In Chapter 29, Creating ActiveX Controls, you learned how to use MFC to write an ActiveX control. In Chapter 30, Using the ActiveX Template Library, you learned how to apply ATL in your applications. This chapter combines these two seemingly disparate topics to create ActiveX Controls with ATL. 

ON THE WEB


http://www.microsoft.com/visualc/prodinfo/atlinst.htm[em] Remember, this chapter specifically covers ATL 2.1. If you're using an older version of ATL, you can get the latest version from Microsoft's Web site. If you're using Visual C++ 4.0, 4.1, or 4.2, you must also upgrade to version 4.2b or higher of Visual C++ to use ATL 2.1. 


With the release of Visual C++ 5.0, the ActiveX Template Library (ATL) has matured into a complete ActiveX development framework. The 1.0 and 1.1 releases of ATL introduced ATL to the growing ActiveX development community as an alternative to MFC. These early versions of ATL, however, could only create ActiveX COM objects and ActiveX Automation servers. Although useful in their own right, these capabilities didn't satisfy developers' demands for an easier way to write ActiveX controls. ATL version 2.1, the version that ships with VC++ 5.0, now supports the creation of ActiveX controls. Version 1.0 and 1.1 included a single AppWizard for creating basic ATL projects. Version 2.1 includes a number of AppWizards that can be used to create various types of ActiveX components. 

In this chapter, you will create an ActiveX control with a complete set of basic methods, properties, and events. The control will also support persistence, as well as understand how to draw its user interface when needed. 

Creating A Basic Control

As with most of the other projects that you've created in earlier chapters, the quickest way to build a new ATL control is to use the Visual C++ AppWizard. Start the Developer Studio development environment, and choose the File, New command. When the New dialog box appears, select the Projects tab (see Figure 31.1), which enables you to define several attributes of the generated application or control; these attributes include the type of application to create, the name of the application, and the location in which you want the project to be created. 

Define the new ATL control project with the New dialog box. 

 FIG. 31.1 

To create an ATL control project, select ATL COM AppWizard in the Type field, and then enter the name that you want to use for the control in the Project Name field. For this project, name it "ATLControl." The AppWizard will fill in the Location field, but you can change it if needed. After you've set these fields appropriately, click the OK button to start the ATL COM AppWizard. 

The next AppWizard step defines the basic architecture of your ATL project (see Figure 31.2). When creating an ActiveX control, you're really creating an in-process DLL server, so you should use the Dynamic Link Library (DLL) option button. 
 
An OCX is the same thing as a DLL. Before Microsoft started naming everything in sight "Active," OLE controls were named with the OCX extension. Because they're just DLLs, though, the OCX suffix was quietly dropped. You can use either extension in your projects. 


When you're building an ActiveX control, you don't need to use MFC or to support merging the proxy/stub marshaling, so you can leave the Support MFC and Allow merging of proxy/stub code buttons unselected. Click the Finish button to continue. 

Define the basic architecture of the ATL COM object with the ATL COM AppWizard. 

FIG. 31.2 

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

Confirm the new project settings with the New Project Information dialog box. 

FIG. 31.3 

But wait, you say. I haven't defined any of my control properties. The ATL COM AppWizard takes a different approach from that of the MFC AppWizard. In MFC, you normally use the AppWizard to customize your application by adding new classes, properties, and methods. For ATL projects, the AppWizard only creates the basic source files, and the remainder of the project is defined by the Object Wizard. The Object Wizard gives you more control of the project implementation because it lets you 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. 

After creating the project, the next step is to add your new ATL control implementation to the project. From the Insert menu, select the New ATL Object menu item. The ATL Object Wizard appears. Select the Controls item in the left panel, and you'll see a dialog like the one in Figure 31.4. Your implementation will be a Full Control, so select the Full Control icon. The Object Wizard also enables you to create two other types of components: an Internet Explorer control that supports only the interfaces necessary to be hosted by the Internet Explorer Web browser and a Property Page component, which you'll need if your control requires property page support. After you've selected the appropriate control type, click the Next button to continue. 
 
An Internet Explorer control can only be used in Web browsers that support the Internet Explorer control interface. A Full Control, however, can be used in any application that supports ActiveX controls, including Internet Explorer. Use IE controls when you want them to run only in Web browsers; otherwise, use full controls. 

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

FIG. 31.4 

The next dialog box that you will see is the ATL Object Wizard Properties dialog box, which is used to define the specific properties of the new object that you're adding to your project. Select the Names tab, and in the Short Name edit field, type ATLControlWin, as shown in Figure 31.5. The remainder of the edit fields will automatically update to reflect the short name that you added. 


Use the Names tab of the ATL Object Wizard Properties dialog box to name the new control object. 

FIG. 31.5 

Select the Attributes tab so that you can define the attributes of the control project (see Figure 31.6). 


Define the attributes of the new control object with the Attributes tab of the ATL Object Wizard Properties dialog box. 

FIG. 31.6 

Here's a brief summary of what the most important attributes of the control are: 
For this control, make sure to check the Support ISupportErrorInfo and Support Connection Points check boxes so your control can return rich error messages and handle events. Leave the other settings alone for now. 

You use the Miscellaneous tab to define how the control will draw and act while contained and whether or not your control implementation subclasses a built-in Windows control (see Figure 31.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. 



The Miscellaneous tab is used to define some of the basic control behaviors, including whether or not the control is subclassed from a Windows control. 

FIG. 31.7 

The Stock Properties tab is used to specify which stock properties the control will support (see Figure 31.8). We'll discuss properties more later in "Using Properties" later in this chapter; for now, leave the Stock Properties tab as is, and click the OK button to create the new control object. 


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

FIG. 31.8 

Use the Build menu's Build ATLControl.dll command to compile your control. VC++ will compile the control and register it for you, as described in the next section. 
 
It might seem like Visual C++ is taking an extremely long time to build your ATL control. Because ATL makes heavy use of C++ templates, the compiler has to do quite a bit of extra work, which slows it down. For more information on C++ templates, see "Exploring Templates" in Chapter 21, "Exceptions and Other Power Features." 

Registering ATL Controls

ATL provides control registration and unregistration support for you, so you don't have to do anything explicit in your code to support registration. MFC uses a set of constants whose values are used at compile time; in contrast, ATL uses resource information, in the form of a registry script file, to define the information that is added to the registry database. A registry script file is automatically added to the project whenever a new control object is added; one script file is added for each control object. 

When you build an ATL control project, the registry script file or files are compiled into the control project as resources. In addition, Visual C++ automatically registers your controls by running REGSVR32 on your registration scripts. 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 syntax, see the Visual C++ books online subject ?Registry Scripting Examples[md]Active Template Library, Articles.? 


Listing 31.1 shows the registry script file for the CATLControlWin control object that you added. 


Listing 31.1[em]ATLCONTROLWIN.RGS[md]Registration Script for the ATLControlWin Control 

{ 

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 also manually register the control with the Tools | Register Control command, or by using the File | Register Controls command in the Active X Control Test Container application, TSTCON32.EXE

Testing Your Control with TSTCON32

One of the most powerful features of ActiveX controls is that they can be used in any ActiveX-enabled container. Microsoft includes a test container application as part of the Visual C++ suite; it's officially called the ActiveX Control Test Container, and you can find it at DevStudio\VC\bin\TSTCON32.EXE. It will also appear in Visual C++'s Tools menu. 

TSTCON32's purpose is to provide a simple, small container for you to use when testing your ActiveX controls. Figure 31.9 shows TSTCON32 with three embedded ATLControl controls in it. You can use TSTCON32 as a container to insert, remove, and inspect ActiveX objects. 


FIG. 31.9 

As soon as you've compiled your control, you can test it with the ActiveX Control Test Container application. 

Try loading your new control into TSTCON32. Launch the application, and then use the Edit | Insert OLE Control command. When the Insert OLE Control dialog appears, choose "ATLControlWin Class" and click OK. You'll see your control appear as a box that simply says "ATL 2.0." Notice that you can insert many instances of your control[md]or any other[md]and drag, remove, and highlight them individually. 
 
TSTCON32 is deceptively simple; it actually offers a good simulation of a complex container like Imagineer Technical or Microsoft Word. You can save embedded objects, call individual methods of embedded controls, and view logs of events sent to controls[md]and that's just for starters! Check out TSTCON32's online help for full details on its capabilities. 

Creating Methods

Now that you've successfully created, compiled, and tested your basic ActiveX control project, it's time to customize what the control does by adding your own methods to it. As with the MFC classes you've studied in preceding chapters, adding new methods or overriding existing ones is the primary way of getting the desired behavior from objects, and ATL is no exception. 

Let's add a method called ShowCaption. The method will accept two parameters: a string that the control will display within its client area and a flag indicating whether the caption should be left-, right-, or center-aligned. We'll make the second parameter optional to illustrate how you can mix required and optional parameters in method calls. 

Adding Methods to the Control

To add methods to an ATL control, you don't use the familiar MFC ClassWizard. Instead, you right-click the class that you want to extend and use the Add Method menu item. 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. The Add Method to Interface dialog box shown in Figure 31.10 appears. 

Define the ShowCaption method with the Add Method to Interface dialog box. 

FIG. 31.10 


In the Add Method to Interface dialog box, add the Method Name, ShowCaption, and the Parameters, [in] BSTR bstrCaption, [in, optional] VARIANT varAlignment, [out, retval] long * lRetVal. The Attributes button displays a dialog for adding Interface Definition Language (IDL) attributes for the entire function declaration. 
 
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 control's responsibility to determine whether or not the VARIANT parameter passed to the method contains data and whether or not to do the following:  I

You?ve also added the direction that the parameters flow in the form of [in] and [out] parameter attributes. See Table 31.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[md]such as creating and destroying memory[md]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. If you're a die-hard IDL hacker, you can add your own attributes, but you don't have to know IDL to use Click OK to add the method to the control. 

Table 31.1[em]Parameter Flow Attributes 
Direction   Description 
in  Parameter is passed from control to container. 
out  Parameter is returned from container to control. 
in, out  Parameter is passed from controlto container, and the container returns a parameter. 
out, retval  Parameter is the return value of the method and is returned from the container to the control. 




To complete ShowCaption's implementation, you need to add an enumeration that specifies all the valid alignment settings (see Listing 31.2), as well as two member variables: m_lptstrCaption stores the caption string, and m_lAlignment holds the alignment setting. Listing 31.3 shows the relevant portion of the CATLControlWin class definition. 
 
The complete source code and executable file for the ATLControlWin control is located in the Chap31\ATLControlWin folder on the book's CD-ROM. 



Listing 31.2[em]ALIGNMENTENUMS.H[md]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 
 
The EALIGNMENT enumeration is defined in its own stand-alone include file so that you can use it elsewhere. It's good practice to separate enumerations such as this so that you can easily use them in more than one source file. 


Listing 31.3[em]ATLCONTROLWIN.H[md]Alignment Enumeration Definition 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; 

}; 


Note that m_lAlignment is declared as type long and not as the EALIGNMENT enumeration type. ActiveX Automation restricts the types of data that can be passed to methods; 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 avoid having to explicitly convert the value by casting to the enumerated type when it is retrieved from the VARIANT parameter in the ShowCaption method. 

Of course, you'll also need to initialize your member variables in the control's constructor, as shown in Listing 31.4. 


Listing 31.4[em]ATLCONTROLWIN.H[md]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 ShowCaption Implementation

Just like the MFC AppWizard, the ATL Object Wizard only generates a stub implementation for methods you add; you must still write the code that makes them work. The ShowCaption method contains all of the code for setting the caption and the alignment style, and it demonstrates how to do data type conversions and what to do with optional parameters. Let's examine it section by section, starting with the code in Listing 31.5. 

Listing 31.5[em]ATLCONTROLWIN.CPP[md]Receiving and Converting Parameters for the Method 

STDMETHODIMP CATLControlWin::ShowCaption (BSTR bstrCaption, VARIANT varAlignment, long * lRetVal) 

{ 

USES_CONVERSION; // needed for the W2A macro 

HRESULT hResult = S_OK; 

// initialize our return value  

*lRetVal = FALSE; 

// convert the input string to ANSI: OLE provides us a BSTR, 

// but we need an ordinary null-terminated string instead. 

LPTSTR lptstrTempCaption = W2A(bstrCaption); 


ShowCaption can be called in two different ways: directly through IATLControlWin's custom COM interface or via the IDispatch implementation used by ActiveX Automation. Supporting both interfaces requires a few changes to the method's implementation as compared to a similar MFC method. First, the function is declared as STDMETHODIMP, which expands to an HRESULT return type. The return value is used by OLE to determine whether or not the method call succeeded. 

The caption parameter is passed in differently too. Instead of a CString or some form of null-terminated string, OLE passes all strings with Unicode encoding. MFC hides the implementation details concerning how the strings are managed; the developer simply uses the appropriate string data type based on the target application and platform. Because ATLControlWin doesn't use MFC, it must convert the string to ANSI. Note the use of the USES_CONVERSION and W2A macros to convert the string from UNICODE to ANSI. 
 
The files ATLCONV.H and ATLCONV.CPP, which can be found in \DevStudio\VC\ATL\include directory, contain a number of helper functions and macros for data conversions, including Unicode-ANSI string converters. Because ATL doesn't require you to use MFC, you can use these routines instead of writing your own. 

Handling the Alignment Parameter

ShowCaption will always be passed a caption, but the alignment parameter was declared as optional[md]so it might not be there! The code that handles the alignment parameter is shown in Listing 31.6. 

Listing 31.6[em]ATLCONTROLWIN.CPP[md]Handling the Optional Alignment Parameter 

// If the variant is a long, just use the value. If the user 

// didn't supply a value (remember, this parameter's optional), 

// we'll assign the default left-justified value. 

if(VT_I4 == varAlignment.vt) 

{ 

// use the incoming value 

m_lAlignment = varAlignment.lVal; 

// tell caller that all is well 

*lRetVal = TRUE; 

} 

else if(VT_ERROR == varAlignment.vt || VT_EMPTY == varAlignment.vt) 

{ 

// use the default value 

m_lAlignment = EALIGN_LEFT; 

// tell caller that all is OK 

*lRetVal = TRUE; 

} 


If the variant were actually supplied as a long (which OLE calls type VT_I4, meaning that it's a four-byte integer variant), our control can use it directly. By contrast, if the variant weren?t supplied, or if something went wrong during its assignment, the varAlignment.vt field will tell you, and you can assign the default alignment instead. 

However, there's another case: what if the variant is there, but it isn't a VT_I4? In that case, OLE's type coercion functions might be able to convert whatever was passed into a form that we can use. Listing 31.7 shows the relevant code. 


Listing 31.7[em]ATLCONTROLWIN.CPP[md]Converting from Other Variant Types to VT_I4 

else 

{ 

// If we get here, we have to try to convert the variant. 

// To do so, we'll make a new VARIANT and let OLE handle 

// the conversion if it can. 

VARIANT varConvertedValue;  

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

} 

*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 "VariantChangeType" call. 

} 

} 


This code starts by creating a new VARIANT to attempt a conversion. Note the call to VariantInit: all VARIANT variables must be initialized prior to their use, or your control (or the OLE subsystem, or even the container) might behave unpredictably or even crash. 

The call to VariantChangeType actually attempts the conversion from whatever type was supplied to VT_I4. If the conversion succeeds, you can use the returned value as your alignment. If it fails, you can handle the error by displaying a dialog box, throwing an exception, or silently using the default. 

If you want your control to handle only specific data types, you can also choose to add code to handle the case when the method did not receive a valid data type. If VariantChangeType is unable to convert the data to the type that you request, it returns a value of FALSE, so you can tell that the conversion failed and then handle the error appropriately. 

Drawing the Caption

Now that you?ve accepted and validated ShowCaption's parameters, you can actually redraw the control, provided no errors occurred during the validation process described earlier. The relevant code is shown in Listing 31.8. If the control already has a cached caption, it's freed, and the m_lptstrCaption variable's reloaded with the new caption. The last step before the actual redrawing is to check that the m_lAlignment member variable contains valid data. 

Listing 31.8[em]ATLCONTROLWIN.CPP[md]Copying the Caption String and Forcing the Control to Redraw 

// if everything was OK 

if(TRUE == *lRetVal) 

{ 

// if we have a string 

if(lptstrTempCaption != NULL) 

{ 

// if we've already stored a string 

if(m_lptstrCaption) 

{ 

// delete the existing string 

delete [] m_lptstrCaption; 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); 

} 

// if we got bad data, fix it before redrawing 

if(m_lAlignment < EALIGN_LEFT || m_lAlignment > EALIGN_RIGHT) 

m_lAlignment = EALIGN_LEFT; 

// force the control to repaint itself 

this->FireViewChange(); 

} 

// return the result of the function call 

return hResult; 

} 


If this were an ordinary MFC-based control or ActiveX control, the code would call the derived InvalidateControl function to force the control to repaint itself. Because ATL-based controls use OLE events to trigger actions, however, this code instead calls the FireViewChange method to trigger the redraw. We'll discuss events more in the "Responding To OLE Events" section, but first we need to add property support to ATLControlWin. 

Using Properties

Methods encapsulate what a control can do (draw itself, play a sound, load an URL, and so on.) Properties encapsulate the data held by the control: its foreground and background colors, the name of a .WAV file, or an URL. ActiveX control properties fall into three categories: user-defined, stock, or ambient. 

Creating Normal User-Defined Properties

A normal property is a property that is declared as a single type, for example, long or BSTR. Normal properties have no parameters. Because the control's ShowCaption method will have a member variable to track the text's alignment, we'll expose it as a normal property so that it can be externally controlled. 

You add properties to your control 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 choose the Add Property menu item. 

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


Define the Alignment property attributes in the Add Property to Interface dialog box. 

FIG. 31.11 

When you add a property in this way, the Object Wizard adds stub routines called accessors to your control's class. Accessors usually come in pairs: the get_XXX accessor enables you to read a property value, and its twin, set_XXX, lets you change a property's value. (Of course, instead of XXX, you'll use the property name.) The Get Function and Put Function check boxes in the Function Type group of the Add Property to Interface dialog box control whether one, both, or neither accessors are automatically added when you add a property. Accessor definitions are also added to the control's IDL file. 

After adding the property, you should also add a dispid constant that can be used from within the control implementation source files. Listing 31.9 shows the typedef that is added to the IDL file. The dispid constants 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 changes, you won't have to search your source code trying to find where you used the value. When you define the dispid constant in the IDL file, you should also change the Alignment property function declarations to use the constant; these changes are also shown in Listing 31.9. 

Listing 31.9[em]ATLCONTROL.IDL[md]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 ShowCaption")]  

HRESULT ShowCaption([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); 

}; 

. . . 


Now that you've modified the IDL definitions, it's time to flesh out the get_Alignment/put_Alignment accessors. To get to these functions, open the ATLControlWin.cpp file, or open the functions by double-clicking them in the ClassView tab of the Project Workspace window. 

Both of these functions use the m_lAlignment member variable that you added earlier. get_Alignment, as shown in Listing 31.10, is simple: It returns m_lAlignment's value in the output parameter. 

Listing 31.10[em]ATLCONTROLWIN.CPP[md]The get_Alignment Property Accessor 

STDMETHODIMP CATLControlWin::get_Alignment(long * pVal) 

{ 

HRESULT hResult = S_OK; 

// return our current setting 

*pVal = m_lAlignment; 

return hResult; 

} 


The set_Alignment function is simple too, but more complicated than get_Alignment. As shown in Listing 31.11, set Alignment first tests the new alignment; if it's out of range, it returns S_FALSE to the caller and doesn't change anything. If the value is within limits, it updates m_lAlignment and notifies the control of the change by calling SetDirty

Listing 31.11[em]ATLCONTROLWIN.CPP[md]The set_Alignment Property Accessor 

STDMETHODIMP CATLControlWin::put_Alignment(long newVal) 

{ 

// Don't do anything if the supplied value is out of range 

if (newVal < EALIGN_LEFT || newVal > EALIGN_RIGHT) 

return S_FALSE; 

m_lAlignment = newVal; 

// tell the control that its property's changed 

this->SetDirty(TRUE); 

// update the browser for this property 

this->FireOnChanged(dispidAlignment); 

// redraw the control 

this->FireViewChange(); 

return S_OK; 

} 


Next, 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 ShowCaption method?? Well, you could have, but it wouldn?t do much[md]ShowCaption will never be executed while the control is used in design mode, which is the purpose of FireOnChanged. The last thing that the set_Alignment method does is invalidate the control's drawing area so that it will redraw using the new information. 

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 the ShowCaption method. Parameterized properties are added in the same manner as normal properties: 
  1. From the ClassView tab in the Project Workspace window, select the IATLControlWin interface.
  2. Click the right mouse button, and select the Add Property menu item.
  3. In the Add Property to Interface dialog box, set the Property Type to BSTR and the Property Name to CaptionProp.
  4. Add the Parameters string [in, optional] VARIANT varAlignment, and leave the remainder of the settings at their default values (see Figure 31.12).
  5. Click OK to confirm the entry, and then close the dialog box.
 
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 alignment parameter has to be added as optional because the ATL ClassWizard won't allow you to generate two separate functions that have the same name and ID. The [optional] attribute can be removed from the put_CaptionProp function later without any adverse effect. 

  

Define the Caption property propget function attributes. 

FIG. 31.12 

As with the normal user-defined properties shown earlier in Listing 31.9, you should add a dispid constant to your IDL file, as shown in Listing 31.12. Don't forget to update the accessor definitions in the IDL file to use dispidCaptionProp instead of the existing id

Listing 31.12[em]ATLCONTROL.IDL[md]Adding a New dispid Constant for the CaptionProp Property 

typedef enum propdispids 



dispidAlignment = 2, 

dispidCaption = 3 

}PROPDISPIDS; 


The get_CaptionProp method, as shown in Listing 31.13, returns data from the property to the caller. Your implementation should ignore the alignment parameter because it is of no use to you in this context; you simply return the m_lpstrCaption member. If the return value, BSTR * pVal, passed to the get_CaptionProp function already points to another string, it must be freed. To return the requested value, get_CaptionProp uses the SysAllocString function to create a new BSTR. SysAllocString expects an OLE string of type OLECHAR, so it's necessary to convert the ANSI string to an OLECHAR string with the T2OLE macro and then allocate a BSTR from that. 

Listing 31.13[em]ATLCONTROLWIN.CPP[md]The get_CaptionProp Accessor 

STDMETHODIMP CATLControlWin::get_CaptionProp(VARIANT varAlignment, BSTR * pVal) 



USES_CONVERSION; // needed for the T2OLE macro 

// if there is a string already, free it because we are going to replace it 

if(*pVal) 

::SysFreeString(*pVal); 

// 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 ShowCaption implementation because ShowCaption already does everything that you need. This delegation enables you to reuse ShowCaption simply by wrapping it in an accessor that external callers can use. See Listing 31.14 to see how the delegation is implemented. 

Listing 31.14[em]ATLCONTROLWIN.CPP[md]The put_CaptionProp Delegates Its Work to ShowCaption 

STDMETHODIMP CATLControlWin::put_CaptionProp(VARIANT varAlignment, BSTR newVal) 



long lRetVal=0; 

// defer to the ShowCaption implementation 

HRESULT hResult = this->ShowCaption (newVal, varAlignment, &lRetVal); 

// if the function returned success, notify the control 

if(TRUE == lRetVal) 

this->SetDirty(TRUE); 

return hResult; 


Using 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 by using the ATL Object Wizard during the actual creation of the control. You might recall that earlier in the chapter, one of the options in the ATL Object Wizard Properties dialog box is the Stock Properties tab (see Figure 31.8 to refresh your memory). If you add any 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 could do this manually by adding the CStockPropImpl<?> class after your control object has been created. Note that the CStockPropImpl<?> class replaces the IDispatchImpl<?> class.) 


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 ATL documentation on support of stock properties. 


The second method of adding a stock property is the same method as for user-defined properties: 
  1. First, from the ClassView tab in the Project Workspace window, select the IATLControlWin class, 
  2. Click the right mouse button, and select the Add Property menu item. 
  3. In the Add Property to Interface dialog box, set the Property Type to OLE_COLOR and the Property Name to BackColor, and leave the remainder of the settings at their default values (see Figure 31.13). 
  4. Click OK to confirm the entry and close the dialog box. 


Add the BackColor stock property to the control with the ATL Add Property to Interface dialog box. 

FIG. 31.13 
Next, you must modify the IDL file to reflect the correct dispid for the BackColor property. Because it's a stock property, you need to replace the existing ID value with DISPID_BACKCOLOR. (If you add stock properties with the Stock Properties tab of the ATL Object Wizard, the correct dispids will be automatically inserted.) 

After updating the IDL file, you should add a member variable (which we'll call m_BackColor) to your class declaration so that you can store the BackColor property's value. Make sure to add code in the class constructor to initialize m_BackColor. The final step is to update the automatically generated stubs of the get_BackColor and put_BackColor functions to return and store the BackColor property (see Listing 31.15). 

Listing 31.15[em]ATLCONTROLWIN.CPP[md]The get_BackColor and put_BackColor Accessors 

STDMETHODIMP CATLControlWin::get_BackColor(OLE_COLOR *pVal) 



// return the color 

*pVal = m_BackColor; 

return S_OK; 



STDMETHODIMP CATLControlWin::put_BackColor(OLE_COLOR newVal) 



// Don't do anything if the supplied value is the same as what we already have 

if(newVal != m_BackColor) 



m_BackColor = newVal; 

// tell the control that its property's changed 

this->SetDirty(TRUE); 

// update the browser for this property 

this->FireOnChanged(dispidAlignment); 

// redraw the control 

this->FireViewChange(); 



return S_OK; 




No matter how you added the BackColor stock property to your class, you can access it in your class through the member variable, just as you do any other property. 

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 need 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 enables 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 control?s user 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 uses 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 determine how much ambient property support you provide in your control. 

When you create a Full Control or Internet Explorer control, your control is automatically subclassed from CComControl. The CComControl class family provides accessors for all of the standard ambient properties. For example, GetAmbientBackColor() returns the ambient background color, while GetAmbientTextAlign() returns the ambient text alignment setting. Because ambient properties come from the container, you can't set them from your control, and CComControl doesn't provide any set functions. 

Creating Property Sheets

Property sheets give controls a way to display their properties for review and editing, usually using a tabbed-dialog interface. The original intent of property sheets was to provide a way to set control properties when the control container did not support property browsing facilities. Although property sheets are still common and are frequently used, they are probably not necessary for all implementations. Your specific requirements will determine whether or not your control should contain a property sheet. The official OC96 line is that all controls should have property sheets; this is good advice for commercially developed and distributed controls, but your specific control might not need or benefit from their use. The majority of development environments[md]especially Visual Basic 5.0 and Delphi 2.x[md]already have excellent property-browsing facilities. 

Adding the Property Sheet Object

Before you can implement the property sheet, you must first 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 Figure 31.14). Your implementation will be a Property Page, so select the Property Page icon. Click the Next button to continue. 


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

FIG. 31.14 

Select the Names tab in the ATL Object Wizard Properties dialog box, which you saw earlier in Figure 31.5, 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. The other fields can be changed, but in this case, you can use the default values. 

Next, 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 will add the necessary code to your control implementation to use the new property page object, including changing the OBJECT_MAP, the IDL file, and all of the registry and resource information. 

Because property sheets are tabbed dialog boxes, most of your work on the property sheets will be 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 box. 

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


Add the controls to the Property Sheet dialog box that will display your property data. 

FIG. 31.15
Now, right-click the combo box 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. 

The next step is to add some code to the control to make it use the dialog resources that you just created. Close the resource editor, and open the ATLControlWinPPG.h header file. 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 31.16.) 

Listing 31.16[em]ATLCONTROLWINPPG.H[md]Adding Include Statements to Reference the Alignment Enumerator and the Main Control Definition 

. . . 

#include "resource.h" // main symbols 

#include "alignmentenums.h" 

#include "ATLControl.h" 

EXTERN_C const CLSID CLSID_ATLControlWinPPG; 

. . . 

Because the property page is based on a dialog box, you need to 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. To add the handler, though, you must modify the message map. ATL message maps are very similar to MFC's; 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 links the message and receiving function that you want to implement, in this case WM_INITDIALOG and OnInitDialog. In addition, you'll need to add a command handler to catch notifications sent when the user changes the combo box selections. Listing 31.17 shows the message map with these changes. 

Listing 31.17[em]ATLCONTROLWINPPG.H[md]Adding Two Handler Entries to the Dialog Box?s Message Map 

. . . 

BEGIN_MSG_MAP(CATLControlWinPPG) 

MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog) 

COMMAND_HANDLER(IDC_ALIGNMENTCOMBO, CBN_SELCHANGE, OnAlignmentComboSelChange) 

CHAIN_MSG_MAP(IPropertyPageImpl<CATLControlWinPPG>) 

END_MSG_MAP() 

. . . 


The OnInitDialog function, which is shown in Listing 31.18, begins by retrieving the window handle for the Alignment combo box and resetting its content. Next, it fills the combo box with text representations of all of the available Alignment styles that have text equivalents. . Then it obtains a pointer to the control's custom COM interface (which contains the get_Alignment accessor you added earlier) and uses it to get the current value of the Alignment property by calling get_Alignment. Finally, OnInitDialog sets the current combo box selection to match the alignment it got from the accessor. Note that this code uses the standard Win32 SendMessage function to update the data and settings of the combo box; MFC-based code would probably use the MFC classes that wrap most of the core Win32 functions. 

Listing 31.18[em]ATLCONTROLWINPPG.H[md]Using the OnInitDialog Function to Set Up the Property Page Before Use 

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 = EALIGN_LEFT; 

pControl->get_Alignment(&lAlignment); 

// get the current selection in the listbox 

::SendMessage(::GetDlgItem(m_hWnd, IDC_ALIGNMENTCOMBO), CB_SETCURSEL, lAlignment, 0); 

return TRUE; 


 Handling Property Control Changes

When the user changes the value of the alignment combo box, the property page has to be notified of the change so that it can update the control with the new property value. To enable this notification, you are required to add a WM_COMMAND message handler for CBN_SELCHANGE[md]the 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 31.17) and breaks the submessage into its appropriate values for you. The OnAlignmenComboSelChange function simply sets the dirty flag for the property page and exits; it's shown in Listing 31.19. 

Listing 31.19[em]ATLCONTROLWINPPG.H[md]Triggering the OnAlignmentComboSelChange function by changing the combo box selection 

LRESULT OnAlignmentComboSelChange(WORD wNotify, WORD wID, HWND hWnd, 

BOOL& bHandled) 



SetDirty(TRUE); 

return FALSE; 


Updating The Control

The last step is the actual updating of the control?s properties when the property page exits. When OnAlignmentComboSelChange sets the property page's dirty flag, this triggers the Apply function, which enables you the opportunity to update all the property values that the property page owns. The Apply function is already supplied to you as part of the property page implementation, but the supplied version doesn't do anything. Listing 31.20 has the modified version. 

Listing 31.20[em]ATLCONTROLWINPPG.H[md]Updating Each Control which Uses the Property Page with the Apply Function 

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; generate an error if it fails 

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; 




Because more than one object can have a reference to the property page, Apply loops through all of its references, checking to see whether or not each implements the IATLControlWin custom COM interface. If it finds an interface that it can use, you can 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, resets the m_bDirty flag, and exits the function. At this point, the property dialog box closes, and control returns to the development environment hosting the control. 

Linking the Page and Control Together

The property page implementation is complete; however, the control is still not aware of the property page?s existence and the fact 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 31.21). The macro PROP_ENTRY is used in place of the PROP_PAGE macro so that the property will be automatically made persistent when the control is saved. 

Listing 31.21[em]ATLCONTROLWIN.H[md]Adding an Additional Entry to Take Advantage of the Property Page 

BEGIN_PROPERTY_MAP(CATLControlWin) 

// PROP_ENTRY("Description", dispid, clsid) 

PROP_ENTRY("Alignment", dispidAlignment, CLSID_ATLControlWinPPG) 

PROP_PAGE(CLSID_StockColorPage) 

END_PROPERTY_MAP() 

Responding to OLE 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 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, 2nd Edition, published by Microsoft Press. 

Adding the Event Interface

Because no ClassWizards are available to aid you, adding events in ATL requires a bit more work than is required in MFC. The first step is to add the event interface to your IDL file, as shown in Listing 31.22. This COM interface makes it possible for your control to receive events sent by other controls or its container. 

Listing 31.22[em]ATLCONTROL.IDL[md]Manually Adding Event Interfaces to Your Control's 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; 

}; 


Remember to use the guidgen.exe program to create a new UUID for your IDispatch interface. Do not reuse the UUIDs in the sample, because they might collide with other registered UUIDs on your machine. 

Defining an Event Method

In addition to the event interface, you need to define an event method. For this control, we'll add the Change method. Any event methods that you add must also be added to the event interface. Change 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 give 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; the code in Listing 31.22 contains these changes. 

Event interfaces are based on connection point interfaces, IConnectionPoint and IConnectionPointContainer. You must add these interfaces to your control implementation. Doing so by hand is tricky; to make it easier to add interfaces, ATL provides a proxy generator. To generate the proxy code, follow these steps: 

  1. From the Project menu, choose the Add to Project menu item, and then choose the Components and Controls submenu item. 
  2. In the Components and Controls Gallery dialog box, double-click the Developer Studio Components folder. 
  3. After the Components and Controls Gallery dialog box is refreshed with data, double-click the ATL Proxy Generator icon (see Figure 31.16).      Select the ATL Proxy Generator from the Developer Studio Components. FIG. 31.16 
  4. Click OK to confirm that you want to insert the ProxyGen component. 
  5. When the ATL Proxy Generator dialog box appears, click the ? button to display the Open dialog box, and then select the ATLControl.tlb file and click Open. 
  6. Choose the DATLControlWin entry in the Not Selected list box, and click the > button to move the entry to the Selected list box (see Figure 31.17). Ensure that the Proxy Type is set to Connection Point, and click Insert. Select the DATLControlWin class in the ATL Proxy Generator dialog box. FIG. 31.17 
  7. A Save dialog appears with the file CPATLControl.h in the File name edit box. Click Save to save the generated file, then click OK in the confirmation dialog box that indicates that the operation was successful. 
  8. Close the ATL Proxy Generator and Components and Controls Gallery dialogs. 

The CPATLControl.h file contains the CProxyDATLControlWin class, which implements your event interface. It also provides a stub for the Change event method, in the form of a routine named Fire_Change. The next step is to add the CProxyDATLControlWin interface to your control implementation. First, add the CPATLControl.h file as an include file to your ATLControlWin.h file: 

#include ?CPATLControl.h? 

Next, update CATLControlWin's inheritance structure by adding a line to CATLControlWin's class definition. This line makes CATLControlWin inherit from CProxyDATLControlWin as well as the other classes it already inherits from: 

public CProxyDATLControlWin<CATLControlWin> 

You also have to add the unique ID of the event interface to the IProvideClassInfo2Impl interface. Because the event interface is IDispatch-based, all you have to do is supply the dispatch interface ID (DIID) in place of the default NULL parameter: 

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


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


Finally, add the event interface to the BEGIN_CONNECTION_POINT_MAP macro in the control?s class declaration. This macro is empty by default. Every time you add an event interface, you'll add a corresponding entry here, like this: 

BEGIN_CONNECTION_POINT_MAP(CATLControlWin) 

CONNECTION_POINT_ENTRY(DIID_DATLControlWin) 

END_CONNECTION_POINT_MAP() 

Your event interface is now completely implemented. The only remaining step is to add the Fire_Change method calls wherever appropriate in your control. Because your implementation of the Fire_Change method enables the user of the control to change the data that is passed to the event, using a universal helper function makes the code easier to maintain. By implementing a simple helper function, FireChange (with no parameters), you can encapsulate the data management associated with the method and its parameters (see Listing 31.23). Remember to add the FireChange function prototype to your class definition in the ATLControlWin.h header file. 

Listing 31.23[em]ATLCONTROLWIN.CPP[md]Isolating the Details of the Change Mechanism from the Code that Fires It 

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 FireChange function enables you to hide the details involved in implementing the change event from the rest of the program. If you decide to change what the event does in the future, the changes will only be in FireChange, so changes will impact only one function rather than many. 

One last thing[md]the ShowCaption method requires that you fire a Change event if the data changes, so you have to add one line to the code in Listing 31.8 (right before the call to FireViewChange) to do so: 

this->FireChange() 

You need to add a corresponding call to the put_Alignment function, but not to put_CaptionProp, which delegates its work to ShowCaption. If you add other new methods to ATLControlWin, you might need to add calls to FireChange() there as well. 

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

Adding property persistence in ATL is trivial. The only thing that you have to do is add the property to the BEGIN_PROPERTY_MAP macro (see Listing 31.24 for an example), and you are finished; the rest is up to ATL. You can make user-defined or stock properties persistent, but not ambient properties, because they're supplied by the container. 

Listing 31.24[em]ATLCONTROLWIN.H[md]Adding Properties to the Property Map 

BEGIN_PROPERTY_MAP(CATLControlWin) 

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. (However, because the release of the OC 96 specification and ActiveX, UI is no longer a requirement; you can build invisible controls that the user can't see but which can offer services to other controls and programs.) 

Drawing the UI of a control has long been regarded as one of the most critical tasks of a control because it affects 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 the control's drawing speed. 


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


Overlapped painting and unnecessary drawing[md]drawing areas of the control that do not need to be drawn[md]are the greatest sources of flicker and flash. Try to draw only to the areas of the control that have been invalidated. 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 redraw the border, for 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, as shown in Listing 31.25. These variables and functions will be used by OnDraw

Listing 31.25[em]ATLCONTROLWIN.H[md]Adding Additional Members and Variables to the Class 

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; 


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

Listing 31.26[em]ATLCONTROLWIN.CPP[md]Initializing the New Member Variables in the Class Constructor 

CATLControlWin() 



. . . 

// clear the font 

m_pFont = NULL; 

// clear the brush 

hOldBrush = hBrush = NULL; 

// clear the flag 

bRetrievedDimensions = FALSE; 



Next, we need to add the implementation for the LoadFont helper function. You also need a default font descriptor in case you cannot retrieve the font from the container. LoadFont is shown in Listing 31.27. If the object's m_pFont variable is NULL, no font has been loaded, so LoadFont will try to load the container's ambient font by using the custom GetAmbientFont interface call. If that fails[md]most likely because the container doesn't support the ambient font property[md]LoadFont will create a new font definition from the static descriptor. 

Listing 31.27[em]ATLCONTROLWIN.CPP[md]Using the Ambient Font Property if It's Available 

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




GetTextExtent is a function that is supported in MFC but not in Win32, so we created our own implementation (it's not shown here, but it's on the CD-ROM.) It simply calculates the drawing area for the control's caption; it determines the width and height of the font of the current DC, and then it calculates the size in points of the string that was supplied to the function. 

The only remaining step is to provide the actual drawing code. The OnDraw implementation is fairly straightforward, as shown in Listing 31.28; here's what it does: 

  1. Sets the desired font into the control's DC 
  2. Gets the background color from the control's property list, remapping colors in the palette if necessary 
  3. Colors the control's background 
  4. Draws the caption text, using the correct foreground color and justifying the text according to the control's Alignment property 
  5.  Draws the control's border 
  6. Resets the DC graphics settings to their original values 

Listing 31.28[em]ATLCONTROLWIN.CPP[md]Drawing the Control, Border, and Caption with OnDraw 

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; 




The integration of ATL 2.1 into the VC++ IDE makes ATL an equal partner with MFC; although each has different strengths and weaknesses, ATL is a worthy competitor for MFC in the field of ActiveX/COM development. Because ATL is focused on ActiveX support, ATL controls are smaller and more efficient than MFC-based controls. In addition, ATL is so close to the COM interface level that you can easily visualize how to implement COM interfaces and components in your controls. Of course, ATL's not perfect; you have to implement connection point and event support by hand, and its wizards lack the comprehensive integration of the MFC AppWizard and ClassWizard. 

In this chapter, you learned how to create a simple ActiveX control using only the ATL. Although there are some subtle differences between ATL- and MFC-based ActiveX controls, the creation process is quite similar. As part of constructing your control, you added methods, properties, and events, which together make up the backbone of every control implementation. This chapter also touched on the issues of property persistence and drawing; not all controls need these features, but many do. 

The next chapter explores advanced programming with ATL, including how to implement optimized drawing and asynchronous properties[md]both important for Internet-oriented controls.