Programming with Microsoft Visual C++.NET 6ed [Electronic resources]

George Shepherd, David Kruglinski

نسخه متنی -صفحه : 319/ 227
نمايش فراداده

Using ATL to Write an ActiveX Control

Although creating an ActiveX control using ATL is a pretty straightforward process, using ATL ends up being a bit more burdensome than using MFC. That's because ATL doesn't include all of MFC's amenities. For example, ATL doesn't include device context wrappers. When you draw on a device context, you have to use the raw device context handle.

Despite these issues, creating an ActiveX control using ATL is a whole lot easier than creating one from scratch. Also, using ATL gives you a certain amount of flexibility that you don't get when you use MFC. For example, adding dual interfaces to your control is a tedious process with MFC, but you get them for free when you use ATL. The ATL Control Wizard also makes it easy to add more COM classes (even noncontrol classes) to your project; adding new controls to an MFC-based DLL is a bit more difficult.

In this chapter's example, we'll represent a small pair of dice as an ATL-based ActiveX control. The dice control will illustrate the most important facets of ActiveX controls, including control rendering, incoming interfaces, properties, property pages, and events. We'll take a look at both classic ATL and attributed versions of this control.

Creating a Control

As always, the easiest way to create a COM server in ATL is to simply add an ATL class to your project using the ATL Control Wizard. You create a new ATL project by choosing New Project from the File menu and selecting ATL Project from the project templates. Name the project something clever, like ClassicATLDiceSvr. As you step through the ATL Project Wizard, leave the default options as they are except for the Attributed option—deselect that one.

After you create the DLL server, perform the following steps:

  1. Choose Add Class from the Project menu. Select ATL Control from the class templates.

  2. The Names page of the ATL Control Wizard (shown in Figure 26-1) lets you name the control. In the Short Name text box, give the control a name (such as ClassicATLDiceControl).

    Figure 26-1: The Names page of the ATL Control Wizard.

  3. On the Options page, you configure the control. For example, you can:

    • Select a standard control, a composite control, or a DHTML control (and minimal versions of each of these controls)

    • Designate the threading model for the control

    • Specify whether the main interface will be a dual or custom interface

    • Specify whether your control will support aggregation

    • Choose whether to use ActiveX control licensing and connection points in your control

  4. To make your life easier later, select Connection Points as the support option. (This will save you some typing later on.) Leave everything else as the default value. Figure 26-2 shows what the Options page will look like.

    Figure 26-2: The Options page of the ATL Control Wizard.

  5. On the Interfaces page, you specify what COM interfaces your control will support. Add IPropertyNotifySink to the supported list.

  6. On the Appearance page (shown in Figure 26-3), you can apply various traits to your control. For example, you can give the control behaviors based on regular Microsoft Windows controls such as buttons and edit controls. Other options include having your control appear invisible at run time or giving your control an opaque background.

    Figure 26-3: The Appearance page of the ATL Control Wizard.

  7. Finally, select the Stock Properties page (shown in Figure 26-4) if you want to give your control some stock properties. Stock properties are properties that you might expect any control to have, including background colors, border colors, foreground colors, and a caption.

    Figure 26-4: The Stock Properties page of the ATL Control Wizard.

  8. When you've finished selecting the attributes for the control, click Finish.

The ATL Control Wizard will add a header file and a source file that define the new control. In addition, it will set aside space in the IDL file to hold the control's main interface and assign a GUID to the interface. Here's the C++ definition of the control produced by the wizard:

class ATL_NO_VTABLE CClassicATLDiceControl : 
public CComObjectRootEx<CComSingleThreadModel>,
public CStockPropImpl<CClassicATLDiceControl, 
IClassicATLDiceControl>,
public IPersistStreamInitImpl<CClassicATLDiceControl>,
public IOleControlImpl<CClassicATLDiceControl>,
public IOleObjectImpl<CClassicATLDiceControl>,
public IOleInPlaceActiveObjectImpl<CClassicATLDiceControl>,
public IViewObjectExImpl<CClassicATLDiceControl>,
public IOleInPlaceObjectWindowlessImpl<CClassicATLDiceControl>,
public ISupportErrorInfo,
public IConnectionPointContainerImpl<CClassicATLDiceControl>,
public CProxy_IClassicATLDiceControlEvents<CClassicATLDiceControl>, 
public IPersistStorageImpl<CClassicATLDiceControl>,
public ISpecifyPropertyPagesImpl<CClassicATLDiceControl>,
public IQuickActivateImpl<CClassicATLDiceControl>,
public IDataObjectImpl<CClassicATLDiceControl>,
public IProvideClassInfo2Impl<&CLSID_ClassicATLDiceControl,
&__uuidof(_IClassicATLDiceControlEvents), 
&LIBID_ClassicATLDiceSvrLib>,
public IPropertyNotifySinkCP<CClassicATLDiceControl>,
public CComCoClass<CClassicATLDiceControl, 
&CLSID_ClassicATLDiceControl>,
public CComControl<CClassicATLDiceControl>
{

}

That's a pretty long inheritance list. You've already seen the template implementations of IUnknown and support for class objects. They exist in CComObjectRootEx and CComCoClass. You've also seen how ATL implements IDispatch within the IDispatchImpl template. However, for a basic control about 11 more interfaces are required to make everything work. These interfaces fall into several functional categories, as shown in the following table.

Function/Category

Interface

Handling self-description

IProvideClassInfo2

Handling persistence

IPersistStreamInit

IPersistStorage

Handling activation

IQuickActivate

(and some of IOleObject)

Interface from the original OLE control specification

IOleControl

Interface from the OLE

Document specification

IOleObject

Rendering

IOleInPlaceActiveObject

IViewObjectEx

IOleInPlaceObjectWindowless

IDataObject

Helping the container

manage property pages

ISpecifyPropertyPages

Handling connections

IPropertyNotifySinkCP

IConnectionPointContainer

Note

The interfaces listed in the table are by and large boilerplate interfaces—ones that a COM class must implement in order to qualify as an ActiveX control. Most of the implementations are standard and vary only slightly (if at all) from one control to the next. The beauty of ATL is that it implements this standard behavior and gives you programmatic hooks into which you can plug in your custom code, so you don't have to burn your eyes out by looking directly at the COM code. You can live a full and rich life without understanding exactly how these interfaces work. However, if you want to know more about the internal workings of ActiveX controls, be sure to check out Inside OLE by Kraig Brockschmidt (Microsoft Press, 1995) and ActiveX Controls Inside Out by Adam Denning (Microsoft Press, 1997).

ATL's Control Architecture

At the highest level, an ActiveX control has two aspects: its external state (what it renders on the screen) and its internal state (its properties). Once an ActiveX control is hosted by some sort of container (such as a Microsoft Visual Basic .NET form or an MFC dialog box), it maintains a symbiotic relationship with that container. The client code talks to the control through incoming COM interfaces such as IDispatch and OLE document interfaces such as IOleObject and IDataObject.

The control also has the opportunity to talk back to the client. One method of implementing this two-way communication is for the client to implement an IDispatch interface to represent the control's event set. The container maintains a set of properties called ambient properties that the control can use to find out about its host. For instance, a control can camouflage itself within the container because the container makes the information stored in these properties available through a specifically named IDispatch interface. The container can implement an interface named IPropertyNotifySink to find out when the properties within a control might change. Finally, the container will implement IOleClientSite and IOleControlSite as part of the control-embedding protocol.

The interfaces listed earlier allow the client and the object to exhibit the behaviors expected of an ActiveX control. We'll tackle some of these interfaces as we go along. The best place to begin looking at ATL-based controls is the CComControl class and its base classes.

The CComControl Class

You can find the definition of CComControl in Microsoft's AtlCtl.h file under Atlmfc's Include directory. CComControl is a template class that takes two class parameters: the CComControlBase class and the base window class WinBase.

template <class T, class WinBase =  CWindowImpl< T > >
class ATL_NO_VTABLE CComControl : public CComControlBase, 
public WinBase
{

};

CComControl is a rather lightweight class that does little by itself—it derives functionality from CComControlBase and WinBase. WinBase is the base class that implements windowing functions, and it defaults to CWindowImpl. CComControl expects the template parameter to be an ATL-based COM object that's derived from CComObjectRootEx. CComControl requires the template parameter for various reasons, the primary one being that from time to time the control class will use the template parameter to call back to the control's InternalQueryInterface.

CComControl implements several functions that make it easy for the control to call back to the client. For example, it implements a function named FireOnRequestEdit to allow controls to tell the client that a specified property is about to change. This function calls back to the client through the client-implemented interface IPropertyNotifySink. FireOnRequestEdit notifies all connected IPropertyNotifySink interfaces that the property specified by a certain DISPID is about to change.

CComControl also implements the FireOnChanged function. FireOnChanged is very much like FireOnRequestEdit in that it calls back to the client through the IPropertyNotifySink interface. This function tells the clients of the control (all clients connected to the control through IPropertyNotifySink) that a property specified by a certain DISPID has already changed.

In addition to mapping the IPropertyNotifySink interface to some more easily understood functions, CComControl implements a function named ControlQueryInterface, which simply forwards the call to the control's IUnknown interface. (This is how you can get a control's IUnknown interface from inside the control.) You can also find an implementation of MessageBox in CComControl now. Finally, CComControl implements a function named CreateControlWindow. The default behavior for this function is to call CWindowImpl::Create. If you want, you can override this function to do something other than create a single window. For example, you might want to create multiple windows for your control.

Most of the real functionality for CComControl exists within those two other classes—CComControlBase and CWindowImpl. Let's take a look at those classes now.

The CComControlBase Class

CComControlBase is a much more substantial class than CComControl. To begin with, CComControlBase maintains all the pointers used by the control to talk back to the client. It uses ATL's CComPtr smart pointer to include member variables that wrap the following interfaces implemented for calling back to the client:

  • A wrapper for IOleInPlaceSite (m_spInPlaceSite)

  • An advise holder for the client's data advise sink (m_spDataAdviseHolder)

  • An OLE advise holder for the client's OLE advise sink (m_spOleAdviseHolder)

  • A wrapper for IOleClientSite (m_spClientSite)

  • A wrapper for IAdviseSink (m_spAdviseSink)

CComControlBase also uses ATL's CComDispatchDriver to wrap the client's dispatch interface for exposing its ambient properties.

CComControlBase is also where you'll find the member variables that contain the control's sizing and positioning information: m_sizeNatural, m_sizeExtent, and m_rcPos. The other important data member within CComControlBase is the control's window handle. Most ActiveX controls are user interface gadgets, and as such they maintain a window. CWindowImpl and CWindowImplBaseT handle the windowing aspects of an ATL-based ActiveX control.

The CWindowImpl and CWindowImplBaseT Classes

CWindowImpl derives from CWindowImplBaseT, which derives from CWindowImplRoot, which in turn derives from TBase and CMessageMap. As a template class, CWindowImpl takes three parameters upon instantiation. The first template parameter is the control being created. CWindowImpl needs the control type because CWindowImpl calls back to the control during window creation. The second template parameter is the Windowing base class. The default is CWindow. The third parameter represents a set of windowing traits for the control, which applies the following traits to the control: WS_CHILD, WS_VISIBLE, WS_CLIPCHILDREN, and WS_CLIPSIBLINGS. Let's take a closer look at how ATL handles windowing.

ATL Windowing

Just as CComControl is relatively lightweight (most work happens in CComControlBase), so is CWindowImpl. CWindowImpl more or less handles only window creation. In fact, that's the only function it explicitly defines. CWindowImpl::Create creates a new window based on the window class information managed by a class named _ATL_WNDCLASSINFO. There's an ASCII character version and a wide-character version.

struct _ATL_WNDCLASSINFOA
{
WNDCLASSEXA m_wc;
LPCSTR m_lpszOrigName;
WNDPROC pWndProc;
LPCSTR m_lpszCursorID;
BOOL m_bSystemCursor;
ATOM m_atom;
CHAR m_szAutoName[5+sizeof(void*)*CHAR_BIT];
ATOM Register(WNDPROC* p)
{
return AtlWinModuleRegisterWndClassInfoA(&_AtlWinModule, 
&_AtlBaseModule, this, p);
}
};
struct _ATL_WNDCLASSINFOW
{
WNDCLASSEXW m_wc;
LPCWSTR m_lpszOrigName;
WNDPROC pWndProc;
LPCWSTR m_lpszCursorID;
BOOL m_bSystemCursor;
ATOM m_atom;
WCHAR m_szAutoName[5+sizeof(void*)*CHAR_BIT];
ATOM Register(WNDPROC* p)
{
return AtlWinModuleRegisterWndClassInfoW(&_AtlWinModule, 
&_AtlBaseModule, this, p);
}
};

ATL then uses typedefs to alias this structure to a single class named CWndClassInfo:

typedef _ATL_WNDCLASSINFOA CWndClassInfoA;
typedef _ATL_WNDCLASSINFOW CWndClassInfoW;
#ifdef UNICODE
#define CWndClassInfo CWndClassInfoW
#else
#define CWndClassInfo CWndClassInfoA
#endif

CWindowImpl uses a macro named DECLARE_WND_CLASS to add window class information to a CWindowImpl-derived class. DECLARE_WND_CLASS also adds a function named GetWndClassInfo. Here's the DECLARE_WND_CLASS macro:

#define DECLARE_WND_CLASS(WndClassName) static ATL::CWndClassInfo& GetWndClassInfo() { static ATL::CWndClassInfo wc = { { sizeof(WNDCLASSEX), CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS, StartWindowProc, 0, 0, NULL, NULL, NULL, (HBRUSH)(COLOR_WINDOW + 1), NULL, WndClassName, NULL }, NULL, NULL, IDC_ARROW, TRUE, 0, _T(") }; return wc; }

This macro expands to provide a CWndClassInfo structure for the control class. Because CWndClassInfo manages the information for a single window class, each window created through a specific instance of CWindowImpl will be based on the same window class.

CWindowImpl derives from CWindowImplBaseT. CWindowImplBaseT derives from CWindowImplRoot, which is specialized around the CWindow class and the CControlWinTraits classes, as follows:

template <class TBase = CWindow, class TWinTraits = CControlWinTraits>
class ATL_NO_VTABLE CWindowImplBaseT : public CWindowImplRoot< TBase >
{

};

CWindowImplRoot derives from CWindow by default and CMessageMap. CWindowImplBaseT manages the window procedure of a CWindowImpl-derived class. CWindow is a lightweight class that wraps window handles in the same way (but not as extensively) as MFC's CWnd class. CMessageMap is a tiny class that defines a single pure virtual function named ProcessWindowMessage. ATL-based message-mapping machinery assumes that this function is available, so ATL-based classes that want to use message maps must derive from CMessageMap. Let's take a quick look at ATL message maps.

ATL Message Maps

The root of ATL's message mapping machinery lies within the CMessageMap class. ATL-based controls expose message maps by virtue of indirectly deriving from CWindowImplBase. In MFC, by contrast, deriving from CCmdTarget enables message mapping. However, as in MFC, it's not enough to derive from a class that supports message maps. The message maps actually have to be there—and they are implemented via macros.

To implement a message map in an ATL-based control, you use message map macros. First, ATL's BEGIN_MSG_MAP macro goes into the control class's header file. BEGIN_MSG_MAP marks the beginning of the default message map. CWindowImpl::WindowProc uses this default message map to process messages sent to the window. The message map directs messages to the appropriate handler function or to another message map. ATL defines another macro named END_MSG_MAP to mark the end of a message map. Between BEGIN_MSG_MAP and END_MSG_MAP lie some other macros for mapping window messages to member functions in the control.

Here's a typical message map you might find in an ATL-based control:

BEGIN_MSG_MAP(CAFullControl)
CHAIN_MSG_MAP(CComControl<CAFullControl>)
DEFAULT_REFLECTION_HANDLER()
MESSAGE_HANDLER(WM_TIMER, OnTimer);
MESSAGE_HANDLER(WM_LBUTTONDOWN, OnLButton);
END_MSG_MAP()

This message map delegates most of the message processing to the control through the CHAIN_MSG_MAP macro and handles message reflection through the DEFAULT_REFLECTION_HANDLER macro. The message map also handles two window messages explicitly: WM_TIMER and WM_LBUTTONDOWN. These are standard window messages that are mapped using the MESSAGE_HANDLER macro. The macros simply produce a table that relates window messages to member functions in the class. In addition to handling regular messages, message maps are capable of handling other sorts of events. Here's a rundown of the kinds of macros that can go in a message map:

Macro

Description

MESSAGE_HANDLER

Maps a Windows message to a handler function

MESSAGE_RANGE_HANDLER

Maps a contiguous range of Windows-based messages to a handler function

COMMAND_HANDLER

Maps a WM_COMMAND message to a handler function based on the identifier and the notification code of the menu item, control, or accelerator

COMMAND_ID_HANDLER

Maps a WM_COMMAND message to a handler function based on the identifier of the menu item, control, or accelerator

COMMAND_CODE_HANDLER

Maps a WM_COMMAND message to a handler function based on the notification code

COMMAND_RANGE_HANDLER

Maps a contiguous range of WM_COMMAND messages to a handler function based on the identifier of the menu item, control, or accelerator

NOTIFY_HANDLER

Maps a WM_NOTIFY message to a handler function based on the notification code and the control identifier

NOTIFY_ID_HANDLER

Maps a WM_NOTIFY message to a handler function based on the control identifier

NOTIFY_CODE_HANDLER

Maps a WM_NOTIFY message to a handler function based on the notification code

NOTIFY_RANGE_HANDLER

Maps a contiguous range of WM_NOTIFY messages to a handler function based on the control identifier

NOTIFY_RANGE_CODE_HANDLER

Maps a WM_NOTIFY message to a handler function based on the notification code and a contiguous range of control identifiers

Handling messages within ATL works much the same as in MFC. ATL includes a single window procedure through which messages are routed. Technically, you can build your controls effectively without understanding everything about ATL's control architecture. However, such knowledge can be helpful as you develop a control, and it can be even more useful when you debug a control.

Developing a Control

Once the control is inserted into the server, you must add some code to make the control do something. If you were to compile and load ATL's default control into a container, the results wouldn't be particularly interesting. You'd simply see a blank rectangle with the string ATL 7.0 : ClassicATLDiceControl. You'd want to add code to render the control, to represent the internal state of the control, to respond to events, and to generate events to send back to the container.

Deciding What to Draw

A good place to start working on a control is its drawing code—you get instant gratification that way. Our control is visually represented by a couple of dice. The easiest way to render the dice control is to draw bitmaps representing each of the six possible dice faces and then show the bitmaps on the screen. This implies that the dice control will maintain some variables to represent its state. For example, the control needs to manage the bitmaps representing the dice as well as two numbers that represent the first value shown by each die. Here's the code from

ClassicATLDiceControl.h that represents the state of the dice:

#define MAX_DIEFACES 6
HBITMAP m_dieBitmaps[MAX_DIEFACES];
unsigned short m_nFirstDieValue;
unsigned short m_nSecondDieValue;

Before diving headfirst into the control's drawing code, you need to do a bit of preliminary work—the bitmaps need to be loaded. Presumably, each die rendered by the dice control will show any one of six dice faces, so the control needs one bitmap for each face. Figure 26-5 shows what one of the dice bitmaps looks like.

Figure 26-5: A bitmap for the dice control.

If you draw the bitmaps one at a time, they'll have sequential identifiers in the

resource.h file. Giving the bitmaps sequential identifiers will make them easier to load. Otherwise, you might need to modify the

Resource.h file, which contains the following identifiers:

#define IDB_DICE1                       220
#define IDB_DICE2                       221
#define IDB_DICE3                       222
#define IDB_DICE4                       223
#define IDB_DICE5                       224
#define IDB_DICE6                       225

Loading bitmaps is fairly straightforward. You cycle through the bitmap array and load the bitmap resources. When they're stored in an array like this, grabbing the bitmap out of the array and showing it is much easier than if you don't use an array. Here's the function that loads the bitmaps into the array:

BOOL CClassicATLDiceControl::LoadBitmaps() { 
int i;
BOOL bSuccess = TRUE;
for(i=0; i<MAX_DIEFACES; i++) {
DeleteObject(m_dieBitmaps[i]);
m_dieBitmaps[i] = LoadBitmap(_AtlBaseModule.m_hInst, 
MAKEINTRESOURCE(nID+i));
if(!m_dieBitmaps[i]) {
::MessageBox(NULL, 
"Failed to load bitmaps",
NULL, 
MB_OK);
bSuccess = FALSE;
} 
}
return bSuccess;
}

The best place to call LoadBitmaps is from within the control's constructor, as shown in the following code. To simulate a random roll of the dice, you set the control's state so the first and second die values are random numbers between 0 and 5. (These numbers will be used when the dice control is drawn.)

class ATL_NO_VTABLE CClassicATLDiceControl : // big inheritance list {
CClassicATLDiceControl () {
LoadBitmaps();
srand((unsigned)time(NULL));      
m_nFirstDieValue = (rand() % (MAX_DIEFACES)) + 1;
m_nSecondDieValue = (rand() % (MAX_DIEFACES)) + 1;

}

}

Once the bitmaps are loaded, you'll want to render them. The dice control should include a function for showing each die face based on the current internal state of the dice. Here's where you'll first encounter ATL's drawing machinery.

One of the most convenient things about ATL-based controls (and MFC-based controls) is that all the drawing code happens in one place: within the control's OnDraw function. OnDraw is a virtual function of COleControlBase. Here's OnDraw's signature:

virtual HRESULT OnDraw(ATL_DRAWINFO& di);

OnDraw takes a single parameter: a pointer to an ATL_DRAWINFO structure. Among other things, the ATL_DRAWINFO structure contains a device context on which to render your control. Here's the ATL_DRAWINFO structure:

struct ATL_DRAWINFO {
UINT cbSize;
DWORD dwDrawAspect;
LONG lindex;
DVTARGETDEVICE* ptd;
HDC hicTargetDev; 
HDC hdcDraw;
LPCRECTL prcBounds; //Rectangle in which to draw
LPCRECTL prcWBounds; //WindowOrg and Ext if metafile
BOOL bOptimize;
BOOL bZoomed;
BOOL bRectInHimetric;
SIZEL ZoomNum;      //ZoomX = ZoomNum.cx/ZoomNum.cy
SIZEL ZoomDen;
};

As you can see, there's a lot more information here than a simple device context. Although you can count on the framework filling it out correctly for you, it's good to know where the information in the structure comes from and how it fits into the picture.

ActiveX controls are interesting because they're drawn in two contexts. The first and most obvious context is when the control is active and it draws within the actual drawing space of the client. The other, less obvious context is during design time (such as when an ActiveX control resides in a Visual Basic form in design mode). In the first context, ActiveX controls render themselves to a live screen device context. In the second context, ActiveX controls render themselves to a metafile device context.

Many ATL-based controls are composed of at least one window. So ATL controls need to render themselves during the WM_PAINT message. Once the control receives the WM_PAINT message, the message routing architecture passes control to CComControlBase::OnPaint. (Remember that CComControlBase is one of the control's base classes.) CComControlBase::OnPaint performs several steps. The function begins by creating a painting device context (using BeginPaint). Then it creates an ATL_DRAWINFO structure on the stack and initializes the fields within the structure. OnPaint sets up ATL_DRAWINFO to show the entire content. (The dwDrawAspect field is set to DVASPECT_CONTENT.) OnPaint also sets the lindex field to –1, sets the drawing device context to the newly created painting device context, and sets up the bounding rectangle to be the client area of the control's window. Then it goes on to call OnDrawAdvanced.

The default OnDrawAdvanced function prepares a normalized device context for drawing. You can override this method if you want to use the device context passed by the container without normalizing it. ATL then calls your control class's OnDraw method.

The second context in which the OnDraw function is called is when the control draws to a metafile. The control draws itself to a metafile whenever someone calls IViewObjectEx::Draw. (IViewObjectEx is one of the interfaces implemented by the ActiveX control.) ATL implements the IViewObjectEx interface through the template class IViewObjectExImpl. IViewObjectExImpl::Draw is called whenever the control needs to take a snapshot of its presentation space for the container to store. In this case, the container creates a metafile device context and hands it to the control. IViewObjectExImpl initializes an ATL_DRAWINFO structure and puts it on the stack. The bounding rectangle, the index, the drawing aspect, and the device contexts are all passed in as parameters by the client. The rest of the drawing is the same in this case—the control calls OnDrawAdvanced, which in turn calls your version of OnDraw.

Once you're armed with this knowledge, writing functions to render the bitmaps becomes fairly straightforward. To show the first die face, you create a memory-based device context, select the object into the device context, and BitBlt the memory device context into the real device context. Here's the code:

void CClassicATLDiceControl::ShowFirstDieFace(ATL_DRAWINFO& di) {
BITMAP bmInfo;
GetObject(m_dieBitmaps[m_nFirstDieValue-1],
sizeof(bmInfo), &bmInfo);
SIZE size;
size.cx = bmInfo.bmWidth;
size.cy = bmInfo.bmHeight;
HDC hMemDC;
hMemDC = CreateCompatibleDC(di.hdcDraw);
HBITMAP hOldBitmap;
HBITMAP hbm = m_dieBitmaps[m_nFirstDieValue-1];
hOldBitmap = (HBITMAP)SelectObject(hMemDC, hbm); 
if (hOldBitmap == NULL)
return;     // destructors will clean up
BitBlt(di.hdcDraw,
di.prcBounds->left+1,
di.prcBounds->top+1,
size.cx,
size.cy,
hMemDC, 0,
0,
SRCCOPY);
SelectObject(di.hdcDraw, hOldBitmap);
DeleteDC(hMemDC);
}

Showing the second die face follows more or less the same process—just be sure that the dice are represented separately. For example, you probably want to change the call to BitBlt so the two dice bitmaps are shown side by side.

void CClassicATLDiceControl::ShowSecondDieFace(ATL_DRAWINFO& di) {
// 
// This code is exactly the same as ShowFirstDieFace
//  except the second die is positioned next to the first die.
//
BitBlt(di.hdcDraw,
di.prcBounds->left+size.cx + 2,
di.prcBounds->top+1,
size.cx,
size.cy,
hMemDC, 0,
0, SRCCOPY);
// The rest is the same as in ShowFirstDieFace
}

The last step is to call these two functions whenever the control is asked to render itself—in the control's OnDraw function. ShowFirstDieFace and ShowSecondDieFace will show the correct bitmap based on the state of m_nFirstDieValue and m_nSecondDieValue:

HRESULT OnDraw(ATL_DRAWINFO& di)
{
RECT& rc = *(RECT*)di.prcBounds;
HBRUSH hBrush = CreateSolidBrush(m_clrBackColor);
HBRUSH hOldBrush = (HBRUSH)SelectObject(di.hdcDraw, hBrush);
Rectangle(di.hdcDraw, rc.left, rc.top, rc.right, rc.bottom);
SelectObject(di.hdcDraw, hOldBrush);
DeleteObject(hBrush);
ShowFirstDieFace(di);
ShowSecondDieFace(di);
return S_OK;
}

Notice that the drawing code takes the background color into account. We'll be able to change the background color a little later.

At this point, if you compile and load this control into some ActiveX control container (such as a Visual Basic form or an MFC-based dialog box), you'll see two die faces staring back at you. Now it's time to add some code to enliven the control and roll the dice.

Responding to Window Messages

Just looking at two dice faces isn't that much fun. You want to make the dice work. A good way to get the dice to appear to jiggle is to use a timer to generate events and then respond to the timer by showing a new pair of dice faces. Setting up a Windows-based timer in the control means adding a function to handle the timer message and adding a macro to the control's message map. Let's start by using Class View's Properties window to add a handler for WM_TIMER. This will add a prototype for the OnTimer function and an entry into the message map to handle the WM_TIMER message. We'll then add some code to the OnTimer function to handle the WM_TIMER message. The following code shows the OnTimer function:

LRESULT CClassicATLDiceControl::OnTimer(UINT uMsg, WPARAM wParam, 
LPARAM lParam, BOOL& bHandled) {
if(m_nTimesRolled > 15) {
m_nTimesRolled = 0;
KillTimer(1);         } else {
m_nFirstDieValue = (rand() % (MAX_DIEFACES)) + 1;
m_nSecondDieValue = (rand() % (MAX_DIEFACES)) + 1;
FireViewChange();
m_nTimesRolled++;
}
bHandled = TRUE;
return 0;
}

This function responds to the timer message by generating two random numbers, setting up the control's state to reflect these two new numbers, and then asking the control to refresh itself by calling FireViewChange. Notice that the function kills the timer as soon as the dice have rolled a certain number of times. Also notice that the message handler tells the framework that it successfully handled the function by setting the bHandled variable to TRUE.

Notice there's an entry for WM_TIMER in the control's message map. Because WM_TIMER is just a plain vanilla window message, it's represented with a standard MESSAGE_HANDLER macro as follows:

BEGIN_MSG_MAP(CClassicATLDiceControl)
MESSAGE_HANDLER(WM_TIMER, OnTimer)
CHAIN_MSG_MAP(CComControl<CClassicATLDiceControl>)
DEFAULT_REFLECTION_HANDLER()
END_MSG_MAP()

As you can tell from this message map, the dice control already handles the gamut of Windows-based messages through the CHAIN_MSG_MAP macro. However, now the pair of dice can simulate rolling by responding to the timer message. Setting a timer causes the control to repaint itself with a new pair of dice numbers every quarter of a second or so. Of course, you need some way to start the dice rolling. Because this is an ActiveX control, it's reasonable to allow client code to start rolling the dice via a call to a function in one of its incoming interfaces. We'll use Class View's Properties window to add a RollDice function to the main interface. Right-click on the IClassicATLDiceControl interface that appears in Class View and choose Add Method from the shortcut menu. Then add a RollDice function. Visual C++ .NET will add a function named RollDice to your control. You implement RollDice by setting the timer for a reasonably short interval and then returning S_OK. Add the following boldface code:

STDMETHODIMP CClassicATLDiceControl::RollDice()
{
if(::IsWindow(m_hWnd)) {
    SetTimer(1, 250);
    }
return S_OK;
}

If you load the dice into an ActiveX control container, you'll be able to browse and call the control's methods and roll the dice.

In addition to using the incoming interface to roll the dice, the user might reasonably expect to roll the dice by double-clicking on the control. To enable this behavior, you just add a message handler to trap the mouse-button-down message by adding a function to handle a left mouse double-click:

LRESULT CClassicATLDiceControl::OnLButtonDblClick(UINT uMsg, 
WPARAM wParam, 
LPARAM lParam, 
BOOL& bHandled) {
RollDice();
bHandled = TRUE;
return 0;
}

Then be sure to add an entry to the message map to handle the WM_LBUTTONDOWN message:

BEGIN_MSG_MAP(CClassicATLDiceControl)
// Other message handlers
MESSAGE_HANDLER(WM_LBUTTONDBLCLK, OnLButtonDblClick)
END_MSG_MAP()

When you load the dice control into a container and double-click on it, you should see the dice roll. Now that you've added rendering code and given the control the ability to roll, it's time to add some properties.

Adding Properties and Property Pages

You've just seen that ActiveX controls have an external presentation state. (The presentation state is the state reflected when the control draws itself.) Most ActiveX controls also have an internal state. This internal state is a set of variables exposed to the outside world via interface functions. These internal variables are also known as properties.

For example, imagine a simple grid implemented as an ActiveX control. The grid has an external presentation state and a set of internal variables for describing the state of the grid. The properties of a grid control would probably include the number of rows in the grid, the number of columns in the grid, the color of the lines composing the grid, the type of font used, and so forth.

As you saw in Chapter 25, adding properties to an ATL-based class means adding member variables to the class and then creating get and put functions to access these properties. For example, two member variables that you might add to the dice control include the dice color and the number of times the dice should roll before stopping. These two properties can easily be represented as a pair of short integers, as shown here:

class ATL_NO_VTABLE CClassicATLDiceControl : 

{

short m_nDiceColor; short m_nTimesToRoll;

};

To make these properties accessible to the client, you need to add get and put functions to the control. You do this by right-clicking on the interface symbol in Class View and choosing Add Property from the shortcut menu. Using Class View to add DiceColor and TimesToRoll properties to the control will add four new functions to the control: get_DiceColor, put_DiceColor, get_TimesToRoll, and put_TimesToRoll.

The get_DiceColor function should retrieve the state of m_nDiceColor:

STDMETHODIMP CClassicATLDiceControl::get_DiceColor(short * pVal)
{
*pVal = m_nDiceColor;
return S_OK;
}

To make the control interesting, put_DiceColor should change the colors of the dice bitmaps and redraw the control immediately. Our example will use red and blue dice as well as the original black and white dice. To make the control show the new color bitmaps immediately after the client sets the dice color, the put_DiceColor function should load the new bitmaps according to new color and redraw the control:

STDMETHODIMP ClassicATLDiceControl::put_DiceColor(short newVal)
{
    if(newVal < 3 && newVal >= 0)
        m_nDiceColor = newVal;
    LoadBitmaps();
    FireViewChange();
return S_OK;
}

Of course, this means that LoadBitmaps needs to load the bitmaps based on the state of m_nDiceColor, so we need to add the following boldface code to our existing LoadBitmaps function:

BOOL CClassicATLDiceControl::LoadBitmaps() {
int i;
BOOL bSuccess = TRUE;
   int nID = IDB_WHITE1;
   switch(m_nDiceColor) {
      case 0:
         nID = IDB_WHITE1;
         break;
      case 1:
         nID = IDB_BLUE1;
         break;
      case 2:
         nID = IDB_RED1;
         break;
}
for(i=0; i<MAX_DIEFACES; i++) {
DeleteObject(m_dieBitmaps[i]);
m_dieBitmaps[i] = LoadBitmap(_AtlBaseModule.m_hInst, 
MAKEINTRESOURCE(nID+i));
if(!m_dieBitmaps[i]) {
::MessageBox(NULL, 
"Failed to load bitmaps",
NULL, 
MB_OK);
bSuccess = FALSE;
} 
}
return bSuccess;
}

Just as the dice color property reflects the color of the dice, the number of times the dice rolls should be reflected by the state of the TimesToRoll property. The get_TimesToRoll function needs to read the m_nTimesToRoll member, and the put_TimesToRoll function needs to modify m_nTimesToRoll. Add the boldface code shown here:

STDMETHODIMP CClassicATLDiceControl::get_TimesToRoll(short * pVal)
{
*pVal = m_nTimesToRoll;
return S_OK;
}
STDMETHODIMP CClassicATLDiceControl::put_TimesToRoll(short newVal)
{
m_nTimesToRoll = newVal;
return S_OK;
}

Finally, instead of hard-coding the number of times the dice rolls, use the m_nTimesToRoll variable to determine when to kill the timer:

LRESULT CClassicATLDiceControl::OnTimer(UINT uMsg, WPARAM wParam, 
LPARAM lParam, BOOL& bHandled)
{
if(m_nTimesRolled > m_nTimesToRoll) {
m_nTimesRolled = 0;
KillTimer(1);        
} else {
m_nFirstDieValue = (rand() % (MAX_DIEFACES)) + 1;
m_nSecondDieValue = (rand() % (MAX_DIEFACES)) + 1;            
FireViewChange();
m_nTimesRolled++;
}
bHandled = TRUE;
return 0;
}

Now these two properties are exposed to the outside world. When the client code changes the color of the dice, the control will load a new set of bitmaps and redraw the control with the new dice faces. When the client code changes the number of times to roll, the dice control will use that information to determine the number of times the dice control should respond to the WM_TIMER message. So the next question is, "How are these properties accessed by the client code?" One way is through a control's property pages.

Property Pages

Because ActiveX controls are usually user interface gadgets that are meant to be mixed into much larger applications, they often find a home within such places as Visual Basic forms and MFC form views and dialog boxes. When a control is instantiated, the client code can usually reach into the control and manipulate its properties by calling certain functions on the control's incoming interface functions. However, when an ActiveX control is in design mode, accessing the properties through the interface functions isn't always practical. It would be unkind to tool developers to force them to go through the interface functions all the time just to tweak some properties in the control. Why should the tool vendor who's creating the client have to provide a user interface for managing control properties? That's what property pages are for. Property pages are sets of dialog boxes that are implemented by the control for manipulating properties. That way, the tool vendors don't have to keep re-creating dialog boxes for tweaking the properties of an ActiveX control.

How property pages are used Property pages are usually used in one of two ways. The first way is through the control's IOleObject interface. The client can call IOleObject's DoVerb function, passing in the properties verb identifier (named OLEIVERB_PROPERTIES and defined as the number -7) to ask the control to show its property pages. The control then displays a dialog box, or property frame, that contains all of the control's property pages. For example, Figure 26-6 shows the Properties dialog box containing the property pages for the Microsoft Calendar 9.0 control.

Figure 26-6: The Microsoft Calendar 9.0 control executing the properties verb.

Property pages are a testament to the power of COM. As it turns out, each single property page is a separate COM object (which is named using a GUID and registered like all the other COM classes on your system). When a client asks an ActiveX control to show its property pages via the properties verb, the control passes its own list of property page GUIDs into a system API function named OleCreatePropertyFrame. OleCreatePropertyFrame enumerates the property page GUIDs, calling CoCreateInstance for each property page. The property frame gets a copy of an interface so that the frame can change the properties within the control. OleCreatePropertyFrame calls back to the control when the user clicks OK or Apply.

The second way clients use property pages is when the client asks the control for a list of property page GUIDs. The then client calls CoCreateInstance on each property page and installs each property page in its own frame. Figure 26-7 shows an example of how Visual C++ .NET uses the Microsoft Calendar property pages in its own property dialog frame. To see a control's property pages, highlight the control in the dialog box and select Property Pages from the View menu.

Figure 26-7: Visual C++ .NET inserting the Calendar 9.0 property pages into its own dialog box for editing resource properties.

This second method is by far the most common way for a control's property pages to be used. Notice that the property sheet in Figure 26-7 contains the same General tab shown in Figure 26-6. (The term property sheet generally refers to a collection of property pages.) The General property page in Figure 26-7 belongs to Visual C++. The Font and Color property pages are coming from the MFC libraries to which the control is linking (even though they're shown within the context of Visual C++).

In order for a property page to work correctly, the control that the property page is associated with must implement ISpecifyPropertyPages and the property page object must implement an interface named IPropertyPage. With this in mind, let's examine exactly how ATL implements property pages.

Adding a property page to your control You can use the Visual Studio .NET ATL Property Page Wizard to create property pages in your ATL project. To create a property page, perform the following steps:

  1. Choose Add Class from the Project menu.

  2. Select ATL Property Page from the template list. Fill in the required information on the ATL Property Page Wizard pages, and then click Finish.

The wizard will generate a dialog template and include it as part of a control's resources. In the dice control example, the two properties we're concerned with are the color of the dice and the number of times to roll the dice. The dialog template created by the ATL Property Page Wizard is blank, so you'll want to add a couple of controls to represent these properties. In this example, the user will be able to select the dice color from a combo box and enter the number of times the dice should roll in an edit control, as shown in Figure 26-8.

Figure 26-8: The property page dialog template.

The ATL Property Page Wizard also creates a C++ class for you that implements the interface necessary for the class to behave as a property page. In addition to generating this C++ class, the wizard makes the class part of the project. It adds the new property page class to the IDL file within the coclass section. In addition, the wizard appends the property page to the object map so DllGetClassObject can find the property page class. Finally, the wizard adds a new Registry script (so the DLL will make the correct Registry entries when the control is registered).

Here's the header file created by the ATL Property Page Wizard for a property page named DiceMainPropPage:

#pragma once
#include "resource.h"       // main symbols
#include "ClassicATLDiceSvr.h"
// CDiceMainPropPage
class ATL_NO_VTABLE CDiceMainPropPage :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CDiceMainPropPage, &CLSID_DiceMainPropPage>,
public IPropertyPageImpl<CDiceMainPropPage>,
public CDialogImpl<CDiceMainPropPage>
{
public:
CDiceMainPropPage() 
{
m_dwTitleID = IDS_TITLEDiceMainPropPage;
m_dwHelpFileID = IDS_HELPFILEDiceMainPropPage;
m_dwDocStringID = IDS_DOCSTRINGDiceMainPropPage;
}
DECLARE_PROTECT_FINAL_CONSTRUCT()
HRESULT FinalConstruct()
{
return S_OK;
}
void FinalRelease() 
{
}
enum {IDD = IDD_DICEMAINPROPPAGE};
DECLARE_REGISTRY_RESOURCEID(IDR_DICEMAINPROPPAGE)
BEGIN_COM_MAP(CDiceMainPropPage) 
COM_INTERFACE_ENTRY(IPropertyPage)
END_COM_MAP()
BEGIN_MSG_MAP(CDiceMainPropPage)
CHAIN_MSG_MAP(IPropertyPageImpl<CDiceMainPropPage>)
END_MSG_MAP()
// Handler prototypes:
//  LRESULT MessageHandler(UINT uMsg, WPARAM wParam, LPARAM lParam, 
//                         BOOL& bHandled);
//  LRESULT CommandHandler(WORD wNotifyCode, WORD wID, HWND hWndCtl, 
//                         BOOL& bHandled);
//  LRESULT NotifyHandler(int idCtrl, LPNMHDR pnmh, BOOL& bHandled);
STDMETHOD(Apply)(void)
{
ATLTRACE(_T("CDiceMainPropPage::Apply\n"));
for (UINT i = 0; i < m_nObjects; i++)
{
// Do something interesting here
// ICircCtl* pCirc;
// m_ppUnk[i]->QueryInterface(IID_ICircCtl, (void**)&pCirc);
// pCirc->put_Caption(CComBSTR("something special"));
// pCirc->Release();
}
m_bDirty = FALSE;
return S_OK;
}
};
OBJECT_ENTRY_AUTO(__uuidof(DiceMainPropPage), CDiceMainPropPage)

Examining this property page listing reveals that ATL's property page classes are composed of several ATL templates: CComObjectRootEx (to implement IUnknown), CComCoClass (the class object for the property page), IPropertyPageImpl (for implementing IPropertyPage), and CDialogImpl (for implementing the dialog-specific behavior).

As with most other COM classes created by ATL's wizards, most of the code involved in getting a property page to work is boilerplate code. Notice that besides the constructor and some various maps, the only other function is one named Apply.

Before we get into the mechanics of implementing a property page, it'll be helpful for you to understand how the property page architecture works. The code you need to type in to get the property pages working will then make more sense.

When the client decides it's time to show some property pages, a modal dialog frame must be constructed. The frame is constructed by the client or by the control itself. If the property pages are being shown via the DoVerb function, the control will construct the frame. If the property pages are being shown within the context of another application—such as when Visual C++ .NET shows the control's property pages within the IDE—the client will construct the dialog frame. The key to the dialog frame is that it holds property page sites (small objects that implement IPropertyPageSite) for each property page.

The client code (the modal dialog frame, in this case) then enumerates through a list of GUIDs, calling CoCreateInstance on each one of them and asking for the IPropertyPage interface. If the COM object produced by CoCreateInstance is a property page, it implements the IPropertyPage interface. The dialog frame uses the IPropertyPage interface to talk to the property page. Here's the declaration of the IPropertyPage interface:

interface IPropertyPage : public IUnknown {
HRESULT SetPageSite(IPropertyPageSite *pPageSite) = 0;
HRESULT Activate(HWND hWndParent,
LPCRECT pRect,
BOOL bModal) = 0;
HRESULT Deactivate( void) = 0;
HRESULT GetPageInfo(PROPPAGEINFO *pPageInfo) = 0;
HRESULT SetObjects(ULONG cObjects,
IUnknown **ppUnk) = 0;
HRESULT Show(UINT nCmdShow) = 0;
HRESULT Move(LPCRECT pRect) = 0;
HRESULT IsPageDirty( void) = 0;
HRESULT Apply( void) = 0;
HRESULT Help(LPCOLESTR pszHelpDir) = 0;
HRESULT TranslateAccelerator(MSG *pMsg) = 0;
};

Once a property page has been created, the property page and the client code need some channels to communicate back and forth with the control. After the property dialog frame successfully calls QueryInterface for IPropertyPage on the property page objects, the frame calls IPropertyPage::SetPageSite on each IPropertyPage interface pointer that it holds, passing in an IPropertyPageSite interface pointer. The property page sites within the property frame provide a way for each property page to call back to the frame. The property page site provides information to the property page and receives notifications from the page when changes occur. Here's the IPropertyPageSite interface:

interface IPropertyPageSite : public IUnknown {
public:
virtual HRESULT OnStatusChange(DWORD dwFlags) = 0;
virtual HRESULT GetLocaleID(LCID *pLocaleID) = 0;
virtual HRESULT GetPageContainer(IUnknown *ppUnk) = 0;
virtual HRESULT TranslateAccelerator(MSG *pMsg) = 0;
};

In addition to the frame and control connecting to each other through IPropertyPage and IPropertyPageSite, each property page needs a way to talk back to the control. This is usually done when the dialog frame calls IPropertyPage::SetObjects, passing in the control's IUnknown. Figure 26-9 depicts the property page architecture.

Figure 26-9: How the property pages, property frame, and property page sites communicate.

Now that you understand how ActiveX control property pages work in general, understanding how they work within ATL will be a lot easier. You'll see how ATL's property pages work when the client code exercises the control's properties verb as well as when environments such as Visual C++ .NET integrate a control's property pages into the IDE.

ATL and the properties verb The first way in which an ActiveX control shows its property pages is when the client invokes the properties verb by calling IOleObject::DoVerb using the constant OLEIVERB_PROPERTIES. When the client calls DoVerb in an ATL-based control, the call ends up in the function CComControlBase::DoVerbProperties, which simply calls OleCreatePropertyFrame, passing in its own IUnknown pointer and the list of property page GUIDs. OleCreatePropertyFrame takes the list of GUIDs, calling CoCreateInstance on each one to create the property pages, and arranges them within the dialog frame. OleCreatePropertyFrame uses each property page's IPropertyPage interface to manage the property page.

ATL property maps Of course, understanding how OleCreatePropertyFrame works from within the ATL-based control begs the next question: Where does the list of property pages actually come from? ATL uses macros to generate lists of property pages called property maps. When you add a new property page to an ATL-based control, you must set up the list of property pages using these macros. ATL includes several macros for implementing property maps: BEGIN_PROPERTY_MAP, PROP_ENTRY, PROP_ENTRY_EX, PROP_PAGE, PROP_DATA_ENTRY, and END_PROPERTY_MAP. Here are those macros in the raw:

struct ATL_PROPMAP_ENTRY
{
LPCOLESTR szDesc;
DISPID dispid;
const CLSID* pclsidPropPage;
const IID* piidDispatch;
DWORD dwOffsetData;
DWORD dwSizeData;
VARTYPE vt;
};
#define BEGIN_PROPERTY_MAP(theClass) __if_not_exists(__ATL_PROP_NOTIFY_EVENT_CLASS) { typedef ATL::_ATL_PROP_NOTIFY_EVENT_CLASS __ATL_PROP_NOTIFY_EVENT_CLASS; } typedef theClass _PropMapClass; static ATL::ATL_PROPMAP_ENTRY* GetPropertyMap(){static ATL::ATL_PROPMAP_ENTRY pPropMap[] = { {OLESTR("_cx"), 0, &CLSID_NULL, NULL, offsetof(_PropMapClass, 
m_sizeExtent.cx), sizeof(long), VT_UI4}, {OLESTR("_cy"), 0, &CLSID_NULL, NULL, offsetof(_PropMapClass, 
m_sizeExtent.cy), sizeof(long), VT_UI4},
// This one can be used on any type of object, but does not
// include the implicit m_sizeExtent
#define BEGIN_PROP_MAP(theClass) __if_not_exists(__ATL_PROP_NOTIFY_EVENT_CLASS) { typedef ATL::_ATL_PROP_NOTIFY_EVENT_CLASS __ATL_PROP_NOTIFY_EVENT_CLASS; } typedef theClass _PropMapClass; static ATL::ATL_PROPMAP_ENTRY* GetPropertyMap(){static ATL::ATL_PROPMAP_ENTRY pPropMap[] = {
#define PROP_ENTRY(szDesc, dispid, clsid) {OLESTR(szDesc), dispid, &clsid, &__uuidof(IDispatch), 0, 0, 0},
#define PROP_ENTRY_EX(szDesc, dispid, clsid, iidDispatch) {OLESTR(szDesc), dispid, &clsid, &iidDispatch, 0, 0, 0},
#define PROP_PAGE(clsid) {NULL, NULL, &clsid, &IID_NULL, 0, 0, 0},
#define PROP_DATA_ENTRY(szDesc, member, vt) {OLESTR(szDesc), 0, &CLSID_NULL, NULL, offsetof(_PropMapClass,  
member), sizeof(((_PropMapClass*)0)->member), vt},
#define END_PROPERTY_MAP() {NULL, 0, NULL, &IID_NULL, 0, 0, 0} }; return pPropMap; }
#define END_PROP_MAP() {NULL, 0, NULL, &IID_NULL, 0, 0, 0} }; return pPropMap; }

When you decide to add property pages to a COM class using ATL's property page macros, according to the ATL documentation you should put these macros into your class's header file. For example, if you want to add property pages to the dice control, you add the following code to the C++ class:

class ATL_NO_VTABLE CClassicATLDiceControl : 

{

BEGIN_PROP_MAP(CClassicATLDiceControl) PROP_DATA_ENTRY("_cx", m_sizeExtent.cx, VT_UI4) PROP_DATA_ENTRY("_cy", m_sizeExtent.cy, VT_UI4) PROP_ENTRY("Caption goes here...", 2, CLSID_MainPropPage) PROP_ENTRY_EX("Caption goes here...", 3, CLSID_SecondPropPage, DIID_SecondDualInterface) PROP_PAGE(CLSID_StockColorPage) END_PROPERTY_MAP() };

ATL's property map macros set up the list of GUIDs that represent property pages. ATL's property maps are composed of an array of ATL_PROPMAP_ENTRY structures. The BEGIN_PROPERTY_MAP macro declares a static variable of this structure. The PROP_DATA_ENTRY macros associate property dispatch names with internal class member variables (in this case the x and y extents). The PROP_PAGE macro inserts a GUID into the list of property pages. PROP_ENTRY inserts a property page GUID into the list and also associates a specific control property with the property page. The final macro, PROP_ENTRY_EX, lets you associate a certain dual interface to a property page. When client code invokes the control's properties verb, the control just rips through this list of GUIDs and hands the list over to the OleCreatePropertyFrame so the property can create the property pages.

Property pages and development tools Executing the properties verb isn't the only way for an ActiveX control to show its property pages. As mentioned before, folks who write tools such as Visual Basic .NET and Visual C++ .NET might want programmatic access to a control's property pages. For example, when you use MFC to work on a dialog box containing an ActiveX control, right-clicking on the control to view the properties will give you a dialog frame produced by Visual C++ .NET (as opposed to the dialog frame produced by OleCreatePropertyFrame).

Visual C++ .NET uses the control's ISpecifyPropertyPages interface to get the list of GUIDs (the list generated by the property page macros). Here's the ISpecifyPropertyPages interface definition:

interface ISpecifyPropertyPages : public IUnknown {
HRESULT GetPages(CAUUID *pPages);
};
typedef struct tagCAUUID 
{ 
ULONG     cElems; 
GUID FAR* pElems; 
} CAUUID;

ATL implements the ISpecifyPropertyPages::GetPages function by cycling through the list of GUIDs (produced by the property map macros) and returning them within the CAUUID structure. Environments such as Visual C++ .NET use each GUID in a call to CoCreateInstance to create a new property page. The property page site and the property page exchange interfaces. The property page site holds onto the property page's IPropertyPage interface, and the property page holds onto the property site's IPropertyPageSite interface. After the dialog frame constructs the property pages, it must reflect the current state of the ActiveX control through the dialog controls. For that, you must override the property page's Activate method.

Showing the property page The property page's Activate method is called whenever the property page is about to be shown. A good thing for a property page to do at this time is to fetch the values from the ActiveX control and populate the property page's controls. Remember that the property page holds onto an array of unknown pointers. (They're held in the IPropertyPageImpl's m_ppUnk array.) To access the ActiveX control's properties, you must call QueryInterface on the unknown pointers and ask for the interface that exposes the properties. In this case, the interface is IClassicATLDiceCopntrol. Once the property page has the interface, it can use the interface to fetch the properties and plug the values into the dialog box controls. Here's the overridden Activate method:

#include "ClassicATLDiceSvr.h"
class ATL_NO_VTABLE CDiceMainPropPage :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CDiceMainPropPage, &CLSID_DiceMainPropPage>,
public IPropertyPageImpl<CDiceMainPropPage>,
public CDialogImpl<CDiceMainPropPage>
{

STDMETHOD(Activate)(HWND hWndParent, LPCRECT prc, BOOL bModal) { // If we don't have any objects, this method should not be called // Note that OleCreatePropertyFrame will call Activate even if a call // to SetObjects fails, so this check is required if (!m_ppUnk) return E_UNEXPECTED; // Use Activate to update the property page's UI with information // obtained from the objects in the m_ppUnk array // We update the page to display the Name and ReadOnly properties of // the document // Call the base class HRESULT hr; hr = IPropertyPageImpl<CDiceMainPropPage>::Activate(hWndParent, prc, bModal); if (FAILED(hr)) return hr; for (UINT i = 0; i < m_nObjects; i++) { CComQIPtr<IClassicATLDiceControl, &IID_IClassicATLDiceControl> pClassicATLDiceControl(m_ppUnk[i]); short nColor = 0; if FAILED(pClassicATLDiceControl->get_DiceColor(&nColor)) { return E_FAIL; } HWND hWndComboBox = GetDlgItem(IDC_COLOR); ::SendMessage(hWndComboBox, CB_SETCURSEL, nColor, 0); short nTimesToRoll = 0; if FAILED(pClassicATLDiceControl->get_TimesToRoll (&nTimesToRoll)) { return E_FAIL; } SetDlgItemInt(IDC_TIMESTOROLL, nTimesToRoll, FALSE); } return S_OK; }

In addition to adding code to prepare to show the dialog box, you must add code to allow users to set the control's properties. Whenever the user changes a property, the property dialog box will activate the Apply button, indicating that the user can apply the newly set properties. When the user clicks the Apply button, control will jump to the property page's Apply function, so you must insert some code in here to make the Apply button work.

Handling the Apply button After the user finishes manipulating the properties, he'll click the Apply button or the OK button to save the changes. In response, the client code will ask the property page to apply the new properties to the control. Remember that the ActiveX control and the property page are separate COM objects, so they need to communicate via interfaces. Here's how the process works.

When you create a property page using the ATL Property Page Wizard, ATL overrides the Apply function from IPropertyPage for you. The property page site uses this function for notifying the property page of changes that need to be made to the control. When the property page's Apply function is called, it's time to synch up the state of the property page with the state of the control. Remember that the control's IUnknown interface was passed into the property page early in the game via a call to IPropertyPage::SetObjects. (The interface pointers are stored in the property page's m_ppUnk array.) Most property pages respond to the Apply function by setting the state of the ActiveX control properties through the interface provided. In the case of our ATL-based property page, this means examining the value in the combo box and the edit box and setting the new values inside the control itself, like this:

#include "ClassicATLDiceSvr.h"
class ATL_NO_VTABLE CDiceMainPropPage :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CDiceMainPropPage, &CLSID_DiceMainPropPage>,
public IPropertyPageImpl<CDiceMainPropPage>,
public CDialogImpl<CDiceMainPropPage>
{

STDMETHOD(Apply)(void) { USES_CONVERSION; ATLTRACE(_T("CDiceMainPropPage::Apply\n")); for (UINT i = 0; i < m_nObjects; i++) { CComQIPtr<IClassicATLDiceControl, &IID_IClassicATLDiceControl> pClassicATLDiceControl(m_ppUnk[i]); HWND hWndComboBox = GetDlgItem(IDC_COLOR); short nColor = (short)::SendMessage(hWndComboBox, CB_GETCURSEL, 0, 0); if(nColor >= 0 && nColor <= 2) { if FAILED(pClassicATLDiceControl->put_DiceColor(nColor)) { CComPtr<IErrorInfo> pError; CComBSTR strError; GetErrorInfo(0, &pError); pError->GetDescription(&strError); MessageBox(OLE2T(strError), _T("Error"), MB_ICONEXCLAMATION); return E_FAIL; } } short nTimesToRoll = (short)GetDlgItemInt(IDC_TIMESTOROLL); if FAILED(pClassicATLDiceControl->put_TimesToRoll(nTimesToRoll)) { 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; }

Property Persistence

Once you've added properties to the control, you might want to have those properties persist with their container. For example, imagine that a gaming company buys your dice control to include in its Windows version of a new game. The game vendor uses your dice control within one of their dialog boxes and configures the control so that the dice are blue and they roll 23 times before stopping. If the dice control has a sound property, the game authors can configure the dice to emit a beep every time they roll. When someone plays the game and rolls the dice, that person will see a pair of blue dice that roll 23 times before stopping and they'll hear the dice make a sound while rolling. Remember that these properties are all properties of the control. If you're using the control in an application, chances are good that you'll want these properties to be saved with the application.

Fortunately, adding persistence support to your control is almost free when you use the ATL property macros. You've already seen how to add the property pages to the control DLL using the property map macros. As it turns out, these macros also make the properties persistent.

You can find ATL's code for handling the persistence of a control's properties within the CComControlBase class. CComControlBase has a member function named IPersistStreamInit_Save that handles saving a control's properties to a stream provided by the client. Whenever the container calls IPersistStreamInit::Save, ATL ends up calling IPersistStreamInit_Save to do the actual work. IPersistStreamInit_Save works by retrieving the control's property map— the list of properties maintained by the control. (Remember that the BEGIN_PROPERTY_MAP macro adds a function named GetPropertyMap to the control.) The first item written out by IPersistStreamInit_Save is the control's extents (its size on the screen). IPersistStreamInit_Save then cycles through the property map to write the contents of the property map out to the stream. For each property, the control calls QueryInterface on itself to get its own dispatch interface. As IPersistStreamInit_Save goes through the list of properties, the control calls IDispatch::Invoke on itself to get the property based on the DISPID associated with the property. (The property's DISPID is included as part of the property map structure.) The property comes back from IDispatch::Invoke as a variant, and IPersistStreamInit_Save writes the property to the stream provided by the client.

Bidirectional Communication (Events)

Now that the dice control has properties and property pages and renders itself to a device context, the last thing to do is to add some events to the control. Events provide a way for the control to call back to the client code and inform the client code of certain events as they occur.

For example, the user can roll the dice. Then, when the dice stop rolling, the client application can fish the dice values out of the control. Another way to implement the control is to set it up so that the control uses an event to notify the client application when the dice have rolled. We'll add some events to the dice control shortly. But first, we'll look at how ActiveX control events work.

How events work When a control is embedded in a container (such as a Visual Basic .NET form or an MFC-based dialog box), one of the steps the client code takes is to establish a connection to the control's event set. That is, the client implements an interface that has been described by the control and makes that interface available to the control. That way, the control can talk back to the container.

Part of developing a control involves defining an interface that the control can use to call back to the client. For example, if you're developing the control using MFC, the wizards will define the interface and produce some functions you can call from within the control to fire events back to the client. If you're developing the control in ATL, you can accomplish the same result by defining the event callback interface in the control's IDL and using Class View to create a set of callback proxy functions for firing the events to the container. When the callback interface is defined by the control, the container must implement that interface and hand it over to the control. The client and the control do this through the IConnectionPointContainer and IConnectionPoint interfaces.

IConnectionPointContainer is the interface that a COM object implements to indicate that it supports connections. IConnectionPointContainer represents a collection of connections available to the client. Within the context of ActiveX controls, one of these connections is usually the control's main event set. Here's the IConnectionPointContainer interface:

interface IConnectionPointContainer : IUnknown {
HRESULT FindConnectionPoint(REFIID riid, 
IConnectionPoint **ppcp) = 0;
HRESULT EnumConnectionPoints(IEnumConnectionsPoint **ppec) = 0;
};

IConnectionPointContainer represents a collection of IConnectionPoint interfaces. Here's the IConnectionPoint interface:

interface IConnectionPoint : IUnknown {
HRESULT GetConnectionInterface(IID *pid) = 0;
HRESULT GetConnectionPointContainer(
IConnectionPointContainer **ppcpc) = 0;
HRESULT Advise(IUnknown *pUnk, DWORD *pdwCookie) = 0;
HRESULT Unadvise(dwCookie) = 0;
HRESULT EnumConnections(IEnumConnections **ppec) = 0;
}

The container creates the control by calling CoCreateInstance on the control. As the control and the container are establishing the interface connections between themselves, one of the interfaces the container will ask for is IConnectionPointContainer. (The container calls QueryInterface asking for IID_IConnectionPointContainer.) If the control supports connection points (if the control answers Yes when queried for IConnectionPointContainer), the control will use IConnectionPointContainer::FindConnectionPoint to get the IConnectionPoint interface that represents the main event set. The container will know the GUID that represents the main event set by looking at the control's type information as the control is inserted into the container.

If the container can establish a connection point to the control's main event set (if IConnectionPointContainer::FindConnectionPoint returns an IConnectionPoint interface pointer), the container will use IConnectionPoint::Advise to subscribe to the callbacks. Of course, in order to do this the container must implement the callback interface defined by the control (which the container can learn about by using the control's type library). Once the connection is established, the control can call back to the container whenever the control fires off an event. Next, we'll look at what it takes to make events work within an ATL-based ActiveX control.

Adding events to the dice control There are several steps to adding event sets to your control. Some are hidden by clever wizardry. First, you use IDL to describe the events. Second, you add a proxy that encapsulates the connection points and event functions. Finally, you fill out the control's connection map so the client and the object have a way to connect to each other. Let's examine each step in detail.

When you use ATL to write an ActiveX control, IDL is the place to start adding events to your control. The event callback interface is described within the IDL so the client will know how to implement the callback interface correctly. The IDL is compiled into a type library that the client will use to figure out how to implement the callback interface. The easiest way to add events to the IDL is to select the event callback interface from within Class View and add the event methods. For example, if you want to add events to indicate that the dice were rolled, doubles were rolled, and snake eyes were rolled, you describe the callback interface with a DiceRolled, a Doubles, and a SnakeEyes method. It's just like defining methods within the main interface. Here's the control's IDL file after adding the methods:

[
uuid(D66265FF-D959-47FB-BC36-585AFC4FFB49),
version(1.0),
helpstring("ClassicATLDiceSvr 1.0 Type Library")
]
library ClassicATLDiceSvrLib
{
importlib("stdole2.tlb");
[
uuid(2FECDCBE-D2C8-46EF-A4A1-E86CDC63B321),
helpstring("_IClassicATLDiceControlEvents Interface")
]
dispinterface _IClassicATLDiceControlEvents
{
properties:
methods:
[id(1)]void Doubles(short n);
[id(2)]void SnakeEyes();
[id(3)]void DiceRolled(short x, short y);
};
[
uuid(75E15528-7E89-431F-B170-D6991C26F944),
helpstring("ClassicATLDiceControl Class")
]
coclass ClassicATLDiceControl
{
[default] interface IClassicATLDiceControl;
[default, source] dispinterface _IClassicATLDiceControlEvents;
};
[
uuid(7A91E3F2-21BB-4286-B02E-4F067FD48DB3),
helpstring("CDiceMainPropPage Class")
]
};

The control's callback interface is defined as a dispatch interface (note the dispinterface keyword) because that's the most generic kind of interface available. When it comes to callback interfaces, most environments understand only IDispatch. The code describes a callback interface to be implemented by the client (if the client decides it wants to receive these callbacks).

Implementing the connection point After you've described the callback interface within the IDL and compiled the control, the control's type information will contain the callback interface description so the client will know how to implement the callback interface. However, you don't yet have a convenient way to fire these events from the control. You could, of course, call back to the client by setting up calls to IDispatch::Invoke by hand. However, a better way is to set up a proxy (a set of functions that wrap calls to IDispatch) to handle the hard work for you. To generate a set of functions that you can call to fire events in the container, you can use the Implement Connection Point Wizard available from Class View.

In Class View, click the right mouse button while the cursor is hovering over the CClassicATLDiceControl symbol. This will bring up the shortcut menu for the CClassicATLDiceControl item. Choose Add, and then choose Add Connection Point to launch the Implement Connection Point Wizard. This wizard will ask you to locate the type information that describes the interface you expect to use when you call back to the container (the _IClassicATLDiceControlEvents interface, in this case). By default, the wizard will look at your control's type library and show the interfaces found within it. Select _IClassicATLDiceControlEvents and then click Finish to create a C++ class that wraps the dice events interface. Given the above interface definition, here's the code generated by the Implement Connection Point Wizard:

#pragma once
template<class T>
class CProxy_IClassicATLDiceControlEvents :
public IConnectionPointImpl<T, 
&__uuidof(_IClassicATLDiceControlEvents)>
{
public:
HRESULT Fire_Doubles(short  n)
{
HRESULT hr = S_OK;
T * pThis = static_cast<T *>(this);
int cConnections = m_vec.GetSize();
for (int iConnection = 0; iConnection < cConnections; 
iConnection++)
{
pThis->Lock();
CComPtr<IUnknown> punkConnection = m_vec.GetAt(iConnection);
pThis->Unlock();
IDispatch * pConnection = 
static_cast<IDispatch *>(punkConnection.p);
if (pConnection)
{
CComVariant avarParams[1];
avarParams[0] = n;
DISPPARAMS params = { avarParams, NULL, 1, 0 };
hr = pConnection->Invoke(1, IID_NULL, LOCALE_USER_DEFAULT, 
DISPATCH_METHOD, &params, NULL, NULL, NULL);
}
}
return hr;
}
HRESULT Fire_SnakeEyes()
{
HRESULT hr = S_OK;
T * pThis = static_cast<T *>(this);
int cConnections = m_vec.GetSize();
for (int iConnection = 0; iConnection < cConnections; iConnection++)
{
pThis->Lock();
CComPtr<IUnknown> punkConnection = m_vec.GetAt(iConnection);
pThis->Unlock();
IDispatch * pConnection = 
static_cast<IDispatch *>(punkConnection.p);
if (pConnection)
{
DISPPARAMS params = { NULL, NULL, 0, 0 };
hr = pConnection->Invoke(2, IID_NULL, LOCALE_USER_DEFAULT, 
DISPATCH_METHOD, &params, NULL, NULL, NULL);
}
}
return hr;
}
HRESULT Fire_DiceRolled(short  x, short  y)
{
HRESULT hr = S_OK;
T * pThis = static_cast<T *>(this);
int cConnections = m_vec.GetSize();
for (int iConnection = 0; iConnection < cConnections; iConnection++)
{
pThis->Lock();
CComPtr<IUnknown> punkConnection = m_vec.GetAt(iConnection);
pThis->Unlock();
IDispatch * pConnection = 
static_cast<IDispatch *>(punkConnection.p);
if (pConnection)
{
CComVariant avarParams[2];
avarParams[1] = x;
avarParams[0] = y;
DISPPARAMS params = { avarParams, NULL, 2, 0 };
hr = pConnection->Invoke(3, IID_NULL, LOCALE_USER_DEFAULT, 
DISPATCH_METHOD, &params, NULL, NULL, NULL);
}
}
return hr;
}
};

The C++ class generated by the connection point generator serves a dual purpose. First, it acts as the specific connection point. (Notice that it derives from IConnectionPointImpl.) Second, it serves as a proxy to the interface implemented by the container. For example, if you want to call over to the client and tell the client that doubles were rolled, you can simply call the proxy's Fire_Doubles function. Notice how the proxy wraps the IDispatch call so you don't have to get your hands messy dealing with variants by yourself.

Establishing the connection and firing the events The final step in setting up the event set is to add the connection point to the dice control and turn on the IConnectionPointContainer interface. The Implement Connection Point Wizard added the CProxy_IClassicATLDiceControlEvents class to the dice control's inheritance list, which provides the IConnectionPoint implementation inside the control. An ATL class named IConnectionPointContainerImpl provides the implementation of IConnectionPointContainer. These two interfaces should be in the dice control's inheritance list, like this:

class ATL_NO_VTABLE CClassicATLDiceControl : 
public CComObjectRootEx<CComSingleThreadModel>,
public CStockPropImpl<CClassicATLDiceControl, IClassicATLDiceControl>,
public IPersistStreamInitImpl<CClassicATLDiceControl>,
public IOleControlImpl<CClassicATLDiceControl>,
public IOleObjectImpl<CClassicATLDiceControl>,
public IOleInPlaceActiveObjectImpl<CClassicATLDiceControl>,
public IViewObjectExImpl<CClassicATLDiceControl>,
public IOleInPlaceObjectWindowlessImpl<CClassicATLDiceControl>,
public ISupportErrorInfo,
public IConnectionPointContainerImpl<CClassicATLDiceControl>,
public CProxy_IClassicATLDiceControlEvents<CClassicATLDiceControl>, 
public IPersistStorageImpl<CClassicATLDiceControl>,
public ISpecifyPropertyPagesImpl<CClassicATLDiceControl>,
public IQuickActivateImpl<CClassicATLDiceControl>,
public IDataObjectImpl<CClassicATLDiceControl>,
public IProvideClassInfo2Impl<&CLSID_ClassicATLDiceControl, 
&__uuidof(_IClassicATLDiceControlEvents), 
&LIBID_ClassicATLDiceSvrLib>,
public IPropertyNotifySinkCP<CClassicATLDiceControl>,
public CComCoClass<CClassicATLDiceControl, &CLSID_ClassicATLDiceControl>,
public CComControl<CClassicATLDiceControl>
{

}

Having these classes in the inheritance list will insert the machinery in your control that makes connection points work. When you want to fire an event to the container, all you need to do is call one of the functions in the proxy. For example, a good time to fire these events is from within the control's OnTimer method—firing a DiceRolled event whenever the timer stops, firing a SnakeEyes event whenever both die faces have the value 1, and firing a Doubles event when both die faces are equal:

LRESULT CClassicATLDiceControl::OnTimer(UINT uMsg, WPARAM wParam, 
LPARAM lParam, BOOL& bHandled)
{
if(m_nTimesRolled > m_nTimesToRoll) {
m_nTimesRolled = 0;
KillTimer(1);
Fire_DiceRolled(m_nFirstDieValue, m_nSecondDieValue);
if(m_nFirstDieValue == m_nSecondDieValue) {
Fire_Doubles(m_nFirstDieValue);
}
if(m_nFirstDieValue == 1 &&
m_nSecondDieValue == 1) {
Fire_SnakeEyes();
}
} else {
m_nFirstDieValue = (rand() % (MAX_DIEFACES)) + 1;
m_nSecondDieValue = (rand() % (MAX_DIEFACES)) + 1;            
FireViewChange();
m_nTimesRolled++;
}
bHandled = TRUE;
return 0;
}

Finally, notice the connection map contains entries for the control's connection points:

BEGIN_CONNECTION_POINT_MAP(CClassicATLDiceControl)
CONNECTION_POINT_ENTRY(__uuidof(_IClassicATLDiceControlEvents))
END_CONNECTION_POINT_MAP()

The control uses this map to hand back connection points as the client requests them.

Using the Control

So, how do you use the control once you've written it? The beauty of COM is that as long as the client and the object agree on their shared interfaces, they don't need to know anything else about each other. All the interfaces implemented within the dice control are well understood by a number of programming environments. You've already seen how to use ActiveX controls within an MFC-based dialog box. The control you just wrote will work fine within an MFC-based dialog box—you just use the Customize Toolbox dialog box to add controls to the Toolbox.

To insert the ClassicATLDiceControl component into your project, choose Customize Toolbox from the Tools menu to open the Customize Toolbox dialog box. On the COM Components tab, select the ClassicATLDiceControl Class check box. Visual C++ .NET will read the dice control's type information and insert all the necessary COM glue to make the dialog box and the control talk with each other. (This includes all the OLE embedding interfaces as well as the connection and event interfaces.) You can also just as easily use this control from within a Visual Basic .NET form. When you're working on a Visual Basic .NET project, choose Add Reference from the Project menu, click on the COM tab, and select ClassicATLDiceSvr 1.0 Type Library to add the dice control to the Visual Basic .NET project.