Which Framework Should You Use? Building Active...

We were unable to locate this content in de-de.

Here is the same content in en-us.

This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.
MIND

Which Framework Should You Use? Building ActiveX Controls with ATL and MFC

George Shepherd
This article assumes you're familiar with MFC, ATL, and COM
Level of Difficulty    1   2   3 

SUMMARY Currently MFC and ATL represent two frameworks targeted at different types of Windows-based development. MFC represents a simple and consistent means of creating standalone apps for Windows; ATL provides a framework to implement the boilerplate code necessary to create COM clients and servers. The two frameworks overlap in their usefulness for developing ActiveX controls.
      We'll take a look at both frameworks as they apply to creating ActiveX controls-highlighting strengths and weaknesses, and walking through the process of creating a control-so you can determine when you might want to use one framework or the other.

I
f you want to use C++ to write ActiveX® controls, there are two widely available frameworks: Microsoft® Foundation Classes (MFC) and ATL. I'll explain in depth the support for developing ActiveX controls provided by these two frameworks so you can better decide which of the two models best suits your development culture and needs.

The Gestalt of ActiveX Controls

      ActiveX controls are a testament to the power of COM. The underlying principle of COM that makes ActiveX controls possible is the fact that an object's interface and implementation can and should be treated separately. As long as a COM object and its client code agree on what an interface should look like, it doesn't matter how it is implemented. ActiveX controls exhibit a number of interfaces that ActiveX control containers understand. Because the client code and the control agree on what these interfaces should look like, you can write an ActiveX control and just drop it inside a container. The container will drive the control through these well-defined interfaces and the control will respond appropriately in its own way.
      At a higher level, an ActiveX control is a COM object that implements several main ActiveX technologies, including regular incoming COM interfaces, the OLE embedding protocol, connection points, and property pages. At a lower programmatic level, ActiveX controls are just COM classes implementing certain families of interfaces. When some client code successfully queries for one of these interfaces, the client code then knows it's dealing with an ActiveX control.
      The interfaces exposed by an ActiveX control fall into three general categories. First, ActiveX controls are embeddable objects; that is, they implement most of the OLE Document in-place activation and embedding protocols. ActiveX controls implement the following interfaces: IOleObject, IPersistStorage, IDataObject, IOleInPlaceActiveObject, IOleInPlaceObject, IViewObject2, and IRunnableObject (this is rarely used). Second, ActiveX controls usually support property pages so the client has a means of modifying the control's properties. Finally, ActiveX controls usually implement outgoing interfaces that the client can find out about using COM's connection point technology.
      To aid in the comparison of the ATL and MFC frameworks, I'll look at the same control written within each framework. This control watches the message traffic passing on the thread that created the control. The message traffic control is a good example because it illustrates all the major aspects of an ActiveX control, including incoming interfaces, outgoing interfaces, properties, persistence, and property pages. Let's start by examining the standard COM support provided by these two frameworks.

Basic COM Support within MFC

      Microsoft created MFC to make the development of applications for Windows® much easier than it was using the SDK. With MFC, Microsoft added COM support to an existing framework. This meant the developers of MFC had to keep the framework intact while adding substantial functionality. In addition, the Visual C++® compiler did not support templates back then, so they had to rely on some means other than templates for mixing COM functionality into their classes. Microsoft solved this problem by adding some virtual functions to the CCmdTarget class and some macros for implementing COM interfaces in MFC.
      COM support within MFC begins within CCmdTarget. The CCmdTarget class implements IUnknown. It also includes a member variable for reference counting (m_dwRef) and six functions for implementing IUnknown: InternalAddRef, InternalRelease, InternalQueryInterface, ExternalAddRef, ExternalRelease, and ExternalQueryInterface. The two versions of QueryInterfaceâ€"AddRef and Releaseâ€"exist to support COM aggregation. InternalAddRef, InternalRelease, and InternalQueryInterface do the reference counting and QueryInterface operations, while ExternalAddRef, ExternalRelease, and ExternalQueryInterface delegate to the object controlling the aggregation (if the object is participating in aggregation).
      MFC uses the nested class composition approach to implement COM interfaces. In MFC, any class that wants to implement COM interfaces derives from CCmdTarget. Each interface implemented by the CCmdTarget-derived class gets its own nested class. MFC uses the macros BEGIN_INTERFACE_PART and END_INTERFACE_PART to generate the nested classes.
      Finally, MFC implements a table-driven QueryInterface. MFC's interface maps work in much the same way as its message maps: MFC's message maps relate a Windows message to a function in a C++ class; MFC's interface maps relate an interface's GUID to the address of a specific vptr representing that interface. Each CCmdTarget-based class implementing COM interfaces gets an interface map added through some more macros: DECLARE_INTERFACE_MAP, BEGIN_INTERFACE_MAP, INTERFACE_PART, and END_INTERFACE_MAP.
      To get an idea of what all these macros look like in real life, take a look at Figure 1, which shows the MFC class for implementing ActiveX controls, COleControl. As you peruse the code, notice that COleControl has the signatures for each interface sandwiched between a pair of BEGIN_INTERFACE_PART and END_INTERFACE_PART macros. Also notice that COleControl has 22 entries in its interface map.
      In addition to implementing IUnknown, MFC includes a standard implementation of IClassFactory. Again, MFC provides this support through some macros. MFC has two macros for providing class objects: DECLARE_OLECREATE_EX and IMPLEMENT_OLECREATE_EX. Using these macros within a CCmdTarget-based class adds a static member of type COleObjectFactory to the class. If you look at the definition for COleObjectFactory in AFXDISP.H, you'll see MFC's nested class macros used within the COleObjectFactory class defining a nested class for implementing IClassFactory2. The MFC version of IClassFactory::CreateInstance uses MFC's dynamic creation mechanism (turned on by using the DECLARE_DYNCREATE and IMPLEMENT_DYNCREATE macros) for instantiating COM classes, so that buying into MFC's COM support also means buying into its dynamic creation mechanism.
      The final piece of basic COM support provided by MFC within an ActiveX control is support for IDispatch. Implementing a dispatch interface using Visual C++ and MFC is almost trivial. To implement a dispatch interface in MFC, just use the ClassWizard. The Automation tab on the ClassWizard has one button for adding properties and another button for adding methods.
      In MFC, IDispatch support comes through the CCmdTarget class. MFC's actual implementation of IDispatch lies within a class named COleDispatchImpl. COleDispatchImpl derives from IDispatch and implements all four of the IDispatch functions: GetTypeInfoCount, GetTypeInfo, GetIDsOfNames, and Invoke. CCmdTarget-derived classes insert the IDispatch vptr into their interface maps by calling EnableAutomation. When clients call QueryInterface for IDispatch on an MFC-based ActiveX control, CCmdTarget hands out the vptr attached to COleDispatchImpl.
      Each time you add an Automation property or a method to a class using the ClassWizard, you add an entry to the class's dispatch map. A dispatch map is simply a table that relates DISPIDs (tokens used to invoke dispatch members) to their human-readable names and to some C++ code that actually does the work. COleDispatchImpl's Invoke and GetIDsOfNames functions work by looking up the dispatch member in the class' dispatch map and invoking the function corresponding to that DISPID.
      MFC has very good support for certain high-level COM-based technologies such as OLE Documents, OLE Drag and Drop, and Automation. However, if you want to tweak the frameworkâ€"for example, if you want to turn dispatch interfaces into dual interfacesâ€"you're in for a bit of typing. ATL, on the other hand, is much more COM-centric.

Basic COM Support within ATL

      ATL's purpose in life is to save developers from having to rewrite IUnknown, IDispatch, IClassFactory, and all the hooks for turning regular DLLs and EXEs into COM-based DLLs and EXEs. In this respect, ATL is a much lighter framework than MFC, and ATL was designed and built with COM support in mind. It uses a template-based approach where developers mix in various pieces of COM functionality by inheriting from templates provided by ATL.
      ATL's raw COM support begins with support for IUnknown. The ATL implementation of IUnknown is divided between the CComObjectRootEx class, which handles the reference counting portion of IUnknown, and the CComObjectRootBase class, which handles QueryInterface.
      CComObjectRootEx is a template-based class that takes the threading model as its single parameter. This is a really interesting example of how ATL uses templates to pass algorithms as template parameters. ATL has two classes for handling reference counting appropriately for the various threading models: CComSingleThreadModel and CComMultiThreadModel. These classes each have an Increment and a Decrement function. The difference between them is that CComSingleThreadModel implements Increment and Decrement using the standard C++ operators (++ and --), while CComMultiThreadModel implements the two functions using the thread-safe InterlockedIncrement and InterlockedDecrement functions. Depending on the template parameter used to instantiate CComObjectRootEx, it will perform correctly for a given apartment type. You'll see an instance of how this is used shortly.
      Like MFC, ATL uses a table-based lookup mechanism to implement QueryInterface. CComObjectRootBase handles the class's QueryInterface function through an interface map. The BEGIN_ COM_MAP and END_COM_MAP macros define the start and end of an interface map. However, unlike MFC, ATL provides 17 ways to compose an interface map, such as using vptrs brought in from ATL's template-based interface implementation classes like IOleObjectImpl. This includes vptrs that come from tear-off classes or those provided by aggregates.
      In ATL, C++ classes become COM classes by inheriting from CComObjectRootEx, specifying the apartment model they would like to use (remember, MFC's IUnknown support is built into CCmdTarget). ATL's class object (and IClassFactory) support comes through templates as well.
      Whereas MFC's class object support is enabled through COleObjectFactory and some macros, ATL's class object support comes from the CComCoClass/CComClassFactory family of classes and the CComCreator family of classes. CComCoClass holds the class's GUID and defines the error handling facilities of the COM class. The CComCreator classes provide implementations of CreateInstance for use by the CComClassFactory classes. As with MFC, you can turn all this support on through some macros. ATL includes the DECLARE_CLASS_FACTORY, DECLARE_CLASS_ FACTORY2, DECLARE_CLASS_FACTORY_AUTO_THREAD, and DECLARE_CLASS_FACTORY_SINGLETON macros for turning on various incarnations of the class factory support.
      Finally, ATL's support for IDispatch comes through a template class, tooâ€"its name is IDispatchImpl. ATL's support for IDispatch is much more COM-centric that MFC's IDispatch support. Whereas MFC uses a hand-rolled implementation of IDispatch, ATL uses the more standard approach of loading an interface's type information and delegating to the standard type library parser.
       Figure 2 shows a standard ATL-based control. The most important thing to notice here is how MFC and ATL bring in the various interfaces necessary to implement a control. MFC's support for the standard control interfaces is built into the COleControl class. You derive your control from COleControl and inherit all the functionality wholesale. Notice that ATL brings in each piece of functionality in a piecemeal fashion via template inheritance. This is an important distinction because it means that with ATL you may strip out unwanted functionality by omitting some of the interface implementation templates (for example, to make your control lighter). You cannot do the same with MFCâ€"you get all the interfaces whether you want them or not.

About the Sample App

      The sample I'll use here is an ActiveX control that monitors message traffic through a hook procedure, displaying a real-time graph of the message flow over time. These two controls function virtually the same way. They both render their graphs to the screen. They both have incoming interfaces so the container can tell the control to start and stop the graph. They both support the graph line color and message interval length as properties that may be persisted. Finally, they both support a default event set for notifying the container about the number of messages processed within a certain interval. Figure 3 shows the two controls.
Figure 3 Monitoring ActiveX Control Messages
Figure 3 Monitoring ActiveX Control Messages

Developing a Control in MFC

      Developing an ActiveX control in MFC involves using the ActiveX ControlWizard in Visual Studio®. To start a new control, select New from the File menu and select the MFC ActiveX control Wizard from the list of project types. First, the ControlWizard asks you to decide how many controls you want in the DLL. Then you can choose how you want to implement your controls.
      The first set of options presented by the ControlWizard apply to the control's DLL as a whole. They include licensing support, comments in the source code, and online help. Choosing licensing causes the ControlWizard to use BEGIN_OLEFACTORY and END_OLEFACTORY (instead of DECLARE__OLECREATE). The BEGIN_OLEFACTORY and END_OLEFACTORY macros override VerifyUserLicense and GetLicenseKey, thereby providing licensing support for your control. Asking the ControlWizard to include comments inserts all those TODO comments into the code. Finally, asking the ControlWizard to include online help creates boilerplate help file source code for the DLL.
      Once you get past the first dialog box, the ControlWizard presents a dialog for configuring the controls inside the DLL. These configuration choices include options for making the control invisible at runtime, making the control activate when it becomes visible, making the object insertable, giving the control an About box, and making the control behave as a simple frame control. Figure 4 explains how the various options affect the code generated by the ControlWizard.
      The ControlWizard also has an option for implementing the control as a standard Windows control, like an edit control or a button. This is an interesting option. For example, if you elect to subclass your control from a button, the control's window is actually a button. In this case, PreCreateWindow intercepts the control's window creation and uses the BUTTON window class when creating the control's window.
      ControlWizard lets you choose some advanced options, which include windowless activation, giving your control an unclipped device context, implementing flicker-free activation, having your control receive mouse messages when it's inactive, and having your control load its properties asynchronously. Here's a rundown of how each option affects the code generated by the ControlWizard:

Windowless activation This option overrides COleControl::GetControlFlags and tacks the windowlessActivate flag onto the control flags. With windowless activation enabled, the container delegates input messages to the control's IOleInPlaceObjectWindowless interface. COleControl's implementation of this interface dispatches the messages through your control's message map. You can then process the messages like ordinary window messages by simply adding the corresponding entries to the message map.

Unclipped device context Selecting this option overrides COleControl::GetControlFlags and turns off the clipPaintDC bit, which removes a call to IntersectClipRect in COleControl's OnPaint function. This is useful if you know for sure that your control doesn't paint outside its client rectangle because there's a detectable speed gain by disabling the call to IntersectClipRect.

Flicker-free activation Choosing this option overrides COleControl::GetControlFlags and bitwise-ors the default control flags with noFlickerActivate. The control checks the flags during activation time to prevent the control from being redrawn when moving between the active and inactivate states. This is especially useful if your control draws itself identically in the inactive and active states.

Mouse pointer notifications when inactive This choice overrides COleControl::GetControlFlags and tacks the pointerInactive bit. The IPointerInactive interface enables an object to remain inactive most of the time, yet still participate in interaction with the mouse for operations such as drag and drop.

Optimized drawing code This option overrides COleControl::GetControlFlags and turns on the canOptimizeDraw bit. Controls with optimized drawing code check this flag (through COleControl's IsOptimizedDraw function) to find out whether the control needs to restore old objects back into the device context when it has finished drawing.

Load properties asynchronously This option adds the stock ReadyState property and the stock ReadyStateChange events to the control. These let the control load its properties asynchronously. For example, a control that loads a large amount of data as one of its properties could take a long time to load, thereby locking up the control. This stock property and event let the control start the loading process right away. The container uses this event and property to know when the control is finished loading.

      When the ControlWizard is finished doing its stuff, you get source code that compiles to a DLL (with an extension of .OLX) containing the controls. The source code produced by the wizard includes a class derived from COleControlModule (which is in turn derived from CWinApp). This class contains the initialization code for the entire control module. Then the wizard produces the source code for the COleControl-based classes representing each control. Finally, the wizard produces some ODL code that is used for building type information.
      Once the wizard generates the control DLL, you're faced with developing the control. This means adding rendering code, developing an incoming interface (methods and properties), rigging up the property pages, and exposing some events. But before I show you how all that works, here's a look at what it takes to create a control using ATL.

Developing a Control in ATL

      As with an MFC-based control, you can get a kick-start developing an ATL-based control by using the ATL COM App Wizard. Using ATL to create controls is a two-step process. Whereas the MFC ControlWizard asks you to decide on how many controls you want in your DLL up front, the ATL COM Wizard simply creates the DLLâ€"you add the controls later using the ATL Object option from the Insert menu. When creating a new ATL-based DLL, you have the choice of mixing in MFC support. You also have the option of merging any proxy/stub code with the control's DLL. This allows you to distribute only one file if someone decides they want to remote the interfaces implemented by your control.
      Once the ATL-based DLL is generated, you can start adding COM classes to it. The Insert New ATL Object menu item makes this task easy. Selecting this menu item displays a dialog box for creating any one of a number of COM classes, including plain vanilla COM objects, ActiveX controls, and Microsoft Transaction Server components (part of Windows NT Server).
      When adding ATL-based controls to your project, the ATL ObjectWizard provides a wider range of options than the MFC ControlWizard. For starters, ATL gives you a choice of implementing your control using any of the current threading models. You can mark your class as either single threaded or apartment threaded. The ATL ObjectWizard prevents you from creating a free or both threaded control, as controls are generally UI-oriented.
      If you create a single-threaded control, a client hosting your control will always load it into its main, single-threaded apartment (STA). As a result, only the single main thread running in the client's process space will touch your object, thereby relieving you of the duty to protect your control's state from concurrent access. In addition, because all instances of your object will only be touched by one thread, you don't have to worry about global data in your DLL either.
      If your control is apartment threaded, you are still relieved of much of the burden of protecting your control's internal state. However, you still have to protect global data inside the DLL. Here's why. First, imagine that your control is created by the client's single main thread. Now imagine that the client tries to create another copy of your controlâ€"but from a thread running in the process' multithreaded apartment. By marking your object as apartment threaded, you're telling COM that you want your control protected from concurrent access. COM creates a new STA for your control when it loads. Now when threads call into your object, they will have to access it through the apartment boundary and the remoting layer will synchronize calls into the object. However, while the state of the particular control is protected from concurrent access as a by-product of being in an STA, data shared by instances of the control (like global data in the DLL) is vulnerable. This is because your global DLL data (which is servicing several objects, each running on a separate thread) can be touched by those multiple threads at once.
      While MFC-based COM classes are always aggregatable (the support is built in), the ATL ObjectWizard lets you specify whether your control supports aggregation, is only aggregatable, or is a standalone object. Depending on the aggregation option you select, the ATL ObjectWizard uses a macro to enforce the aggregation policies. For example, the default implementation of a COM class is aggregatableâ€"the object will run in both standalone mode and as part of an aggregate. If you make your COM object not aggregatable, the ObjectWizard inserts the DECLARE_NOT_ AGGREGATABLE macro into your class definition. If you select only aggregatable, the ObjectWizard inserts the DECLARE_ ONLY_AGGREGATABLE macro into the class declaration.
      Here's how the macros work. Default object creation happens within a class named _CreatorClass. _CreatorClass becomes the creation mechanism for your COM class when it's added to the server-wide object map (that's part of what the OBJECT_ENTRY macro does). _CreatorClass is simply an alias for a class named CComCreator2, which takes as parameters two classes specialized from the CComCreator class. The macros specialize the CComCreator class depending upon the selected aggregation mode, using CComObject, CComAggObject, CComFailCreator, or CComPolyObject accordingly:


#define DECLARE_NOT_AGGREGATABLE(x) public: \
   typedef CComCreator2< CComCreator< CComObject< x > >, \
   CComFailCreator<CLASS_E_NOAGGREGATION> > _CreatorClass;
#define DECLARE_AGGREGATABLE(x) public: \
   typedef CComCreator2< CComCreator< CComObject< x > >, \
   CComCreator< CComAggObject< x > > > _CreatorClass;
#define DECLARE_ONLY_AGGREGATABLE(x) public: \
   typedef CComCreator2< CComFailCreator<E_FAIL>, \
   CComCreator< CComAggObject< x > > > _CreatorClass;
#define DECLARE_POLY_AGGREGATABLE(x) public: \
   typedef CComCreator< CComPolyObject< x > > _CreatorClass;
      The last three checkboxes available in the ATL ObjectWizard Attributes page include supporting COM exceptions (for example, the ISupportErrorInfo interface), connection points, and the freethreaded marshaler (FTM). You can also add ISupportErrorInfo to the control's inheritance list and provide an implementation of ISupportErrorInfo::InterfaceSupportsErrorInfo. Turning on connection points adds the IConnectionPointImpl template class to the control's inheritance list.
      Aggregating your object to the FTM makes interapartment (and Windows 2000 intercontext) calls happen more efficiently if the two objects happen to be in the same process. However, you should never check this option when writing controls as you are more or less violating apartment (and Windows 2000 context) rules when you use the FTM. See Don Box's book, Effective COM (Addison-Wesley Longman, 1998), for more details on the FTM.
      In addition to the normal options that you can apply to all COM objects, the ATL ObjectWizard gives you several options specific to control creation. First, the ATL ObjectWizard lets you subclass your control from a regular control (like a button or an edit control). You may specify several other options for your control to make it opaque, to give it a solid background, to be invisible at runtime, or to make your control act like a button. Here's a rundown of the options available in the Control property page.

Opaque and solid background If you want to make sure that none of the container shows behind the control boundaries, select the "opaque" checkbox. This is status information that the control gives to its containers. In effect, the control is saying that it will draw its entire rectangle. Choosing this option sets the VIEWSTATUS_OPAQUE bit so that IViewObjectExImpl::GetViewStatus indicates an opaque control to the container. You may also choose a solid background. This option sets the VIEWSTATUS_ SOLIDBKGND bit so GetViewStatus indicates that the control has a solid background.

Invisible at runtime This option makes your control invisible at runtime. You can use invisible controls to perform operations in the background, such as firing events at timed intervals. This option causes the control to flip the OLEMISC_INVISIBLEATRUNTIME bit when it places entries in the registry.

Acts like button This enables your control to act like a button. In this case, the control will display itself as the default button based on the value of the container's ambient property DisplayAsDefault. If the control's site is marked as the default button, the control will draw itself with a thicker frame. Selecting this option causes the control to flip the OLEMISC_ACTSLIKEBUTTON bit when it places entries in the registry.

Acts like label Choose this option to enable your control to replace the container's native label. This causes the control to mark the OLEMISC_ACTSLIKELABEL bit when it places entries in the Windows registry.

Add control based on superclass Choose this option to cause your control to subclass from one of the standard window classes. The dropdown list contains window class names defined by Windows. When you choose one of these window class names, the wizard adds a CContainedWindow member variable to your control's class. CContainedWindow::Create will superclass the window class you specify.

Normalize DC Choose this option to have your control create a normalized device context when it is called to draw itself. This standardizes the control's appearance, but is less efficient. This option generates code to override the OnDrawAdvanced method (instead of the normal OnDraw method).

Insertable Choose this option to have your control appear in the Insert Object dialog box of applications such as Microsoft Excel and Word. Your control can then be inserted by any application that supports embedded objects. Choosing this option adds the Insertable key as part of its registry entries.

Windowed only Choose this option to force your control to be windowed, even in containers that support windowless objects. If you do not select this option, your control will automatically be windowless in containers that support windowless objects, and windowed in containers that do not. This causes the CComControlBase::m_bWindowOnly flag to be set to TRUE in the constructor. ATL uses this flag to decide whether to query for the container's IOleInPlaceSiteWindowless interface during control activation.
      ATL asks you to decide on your object's stock properties up front in the Stock Properties page. You can select properties such as Caption or Border Color, or you can select all the stock properties at once by clicking the >> button. This adds properties to the control's property map.
      After running the ATL COM App Wizard and the ObjectWizard, you get a DLL complete with all the hooks necessary to be a COM DLL. The well-known exports exposed by the control include DllGetClassObject, DllCanUnloadNow, DllRegisterServer, and DllUnregisterServer. In addition, you get an object that satisfies the main requirements of COMâ€"including a main incoming interface and a class object.
      Once you've started a project using each of the wizards, the next step is to get the control doing something interesting. Usually the place to begin is the control's rendering code. That way you get some visual feedback right away. Let's examine the way in which rendering happens in an MFC-based control.

Rendering Controls

      MFC and ATL are similar in the way they handle rendering. In each framework, the class implementing the control has a virtual function named OnDraw. You just insert your rendering code inside the OnDraw function. However, the OnDraw function in each framework works slightly differently.
      MFC's OnDraw is called under two contexts. The first context occurs when the control responds to a WM_PAINT message. In this case, the device context passed to the OnDraw function represents a real device context. If the control is being asked to render itself in response to the client calling IViewObjectEx::Draw, the device context is either a metafile device context or a regular device context. The following code shows how the MFC-based control is rendered:


void CMFCMsgTrafficCtrl::OnDraw(CDC* pdc, const CRect& rcBounds, 
    const CRect& rcInvalid)
{
   // TODO: Replace the following code with your own drawing code.
   pdc->FillRect(rcBounds, 
       CBrush::FromHandle((HBRUSH)GetStockObject(WHITE_BRUSH)));
   ShowGraph(*pdc, const_cast<CRect&>(rcBounds), nMessagesToShow);
}
      The signature for COleControl::OnDraw includes a rectangle representing the size of the control and a rectangle representing the invalid area of the control. MFC calls the control's OnDraw function when responding to a WM_PAINT message. In this case, the OnDraw function receives a real device context to draw on. MFC also calls the control's OnDraw function when responding to a call through IViewObject::Draw. MFC's implementation calls COleControl::OnDrawMetafile, and its default OnDrawMetafile calls COleControl::OnDraw. Of course, this implies that the control's live rendering is the same as the control's metafile representation that it saves with the container at design time. You can make the control's live rendering different from its design-time rendering by overriding COleControl::OnDrawMetafile. You can force a redraw to occur by calling your control's InvalidateControl method.
      ATL's rendering mechanism is very similar to the one used by MFC. CComControlBase::OnPaint sets up an ATL_DRAWINFO structure, including creating a painting device context. Then ATL calls your control's OnDrawAdvanced function. OnDrawAdvanced sets up the metafile, then calls your control's OnDraw method, which uses the information in the ATL_DRAWINFO structure to know how to draw on the screen. 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;
};
      ATL fills this structure for you. When you're drawing on the screen, the most important fields you're interested in are hdcDraw and prcBounds. The other fields are important if you're interested in drawing on a metafile or you need to pay attention to zoom factors and such. The following code shows how the ATL-based message traffic control handles drawing:


HRESULT CATLMsgTrafficCtl::OnDraw(ATL_DRAWINFO& di)
{
   RECT& rc = *(RECT*)di.prcBounds;
   HBRUSH hBrush = CreateSolidBrush(RGB(255, 255, 255));
   FillRect(di.hdcDraw, &rc, hBrush);
   DeleteObject(hBrush);
   Rectangle(di.hdcDraw, rc.left, rc.top, rc.right, rc.bottom);
   ShowGraph(di.hdcDraw, rc, nMessagesToShow);
   return S_OK;
}
Notice that when you use ATL you have to deal with both device and GDI handles.
      In ATL, you call your control's FireViewChange function to force a redraw of your control.

Developing an Incoming Interface

      When developing an MFC-based ActiveX control, the default incoming interface is a dispatch interface. Visual C++ and MFC make it very easy to develop the incoming dispatch interfaceâ€"just use the ClassWizard to generate methods and properties. Every time you add a new property or method using the ClassWizard, it inserts an entry into your control's dispatch map. MFC uses the dispatch map to satisfy invocation requests made by the client.
      The downside of MFC is that it's a tedious process to add a regular COM interface to your control. The process involves working with MFC's COM macros to set up nested classes to implement the interfaces.
      When developing the main incoming interface for your ATL-based COM control, class view is the best way to add properties and methods. The ATL Object Wizard adds a default incoming interface as soon as you generate the code for the control. This can be either a dual interface or a regular custom interface, depending on the project options you set earlier.
      The Visual Studio class view shows you all of the classes and interfaces included in your project. When you right-click on an interface definition within the class view, you have the opportunity to add either a property or a method. It's handy to use the class view to define the interface because each time you add a method or a property, class view updates the IDL, the class source, and the header files.
      Unlike with MFC, it's easy to add a regular COM interface to your control. In ATL you simply add new interface boilerplate goo (a GUID, the keyword object, and the keyword interface). The class view will show the new interface, and you can proceed to add new members to it.

Adding Properties

      An ActiveX control often contains properties. These are member variables that describe the state of the control. The best way to add properties to an MFC-based control is to use the ClassWizard. The Automation tab within ClassWizard lets you add member variables and map them to the default incoming dispatch interface. ClassWizard gives you two choices: you can add a member variable and include a change notification function, or you can add a pair of Get/Set functions and add the member variable to the class manually. In addition to adding your own custom properties to the control, ClassWizard lets you add stock properties like the background color and a caption. ClassWizard even adds a member variable to your class.
      Adding properties to an ATL-based control is a bit different. You add individual accessor and mutator functions (propget and propput functions) for each property within your control. However, the class view defines only the interface functions for you. You need to add the data members to your class by hand. Then simply fill in and implement the functions.
      ATL-based controls also support stock properties. The ATL ControlWizard asks you up front which stock properties you'd like to include in your control. Adding at least one stock property to your control causes it to inherit from ATL's CStockPropImpl class. CStockPropImpl is an implementation of IDispatch geared toward exposing ActiveX control stock properties, containing IDispatch-compliant get and put functions for each of the standard stock properties.
      The ControlWizard also adds data members representing the stock properties to your control. For example, if you add the background color stock property to your control, the ControlWizard adds a data member named m_clrBackColor to your class. CStockPropImple adds implementations for all the standard stock property get and put functions at once. All these functions expect to see the appropriate member variable in your class (like m_clrBackColor for the background color).
      The compiler will choke on those get and put functions for the stock properties not included. The implementations will expect to see the member variables in your class. To resolve the compiler errors, CComControlBase adds a union containing all the member names that the stock get and put functions expect to see. However, adding the data member to the control overrides the name in the union, and the CStopPropImpl class uses the member variable in the control inside its get and put functions.
      If you forget to add the stock properties up front using the ControlWizard, you can always add the code by handâ€"that is, inherit from CStockPropImpl and add the member variables for the properties you want to expose.

Property Persistence

      MFC's property persistence mechanism is very straightforward. From a programmatic point of view, all you need to do is fill out the function DoPropExchange that's already been provided by the ControlWizard. DoPropExchange moves the state of the control's properties from the member variables to some persistent media.
      MFC has the three persistence mechanisms built into COleControl: IPersistPropertyBag, IPersistStorage, and IPersistStream[Init]. All three of these persistence mechanisms are wrapped by MFC's CPropExchange class, very much like CArchive wraps a file for you when you need to serialize a document. The client chooses to persist the object using one of these three interfaces. Regardless of the persistence mechanism used by the client, execution always lands in your control's DoPropExchange function.
      The following code shows how the MFCMsgTraffic control saves its line color and interval properties:


void CMFCMsgTrafficCtrl::DoPropExchange(CPropExchange* pPX)
{
    ExchangeVersion(pPX, MAKELONG(_wVerMinor, _wVerMajor));
    COleControl::DoPropExchange(pPX);

    PX_Color(pPX, "GraphLineColor", m_graphLineColor);    
    PX_Long(pPX, "GraphInterval", m_interval);
}
MFC includes several PX_ functions to move data between the control and the storage medium including:


PX_Short
PX_UShort
PX_Long
PX_ULong
PX_Color
PX_Bool
PX_String
PX_Currency
PX_Float
PX_Double
PX_Blob
PX_Font
PX_Picture
PX_IUnknown
PX_VBXFontConvert
PX_DataPath
      Managing control property persistence in ATL involves two steps. The first step is to throw in the ATL implementations of the persistence interfaces you want the client to be able to use. ATL includes the classes IPersistStorageImpl, IPersistStreamInitImpl, and IPersistPropertyBagImpl, which implement the three main COM persistence mechanisms.
      The second step to persisting properties is to insert the properties into the control's property map. Whenever a client asks to save or load the ATL-based control, ATL looks to a control's property map to transfer the control's properties to and from the storage medium. The property map is a table of property names, DISPIDs, and sometimes a property page GUID. ATL goes through this table to find out which properties to persist and persists them to the appropriate medium. Figure 5 shows the ATLMsgTraffic control inheriting the persistence interface implementations and a property map.

Property Pages

      ActiveX controls often provide property pages to help developers as they place controls into various containers. Developers placing the message traffic controls into a dialog box may want to configure various aspects of the control, such as the control's sampling interval or the color of the graph line. For example, when the control is placed in a dialog box and you hit the right mouse button to get the control's properties, Visual Studio shows a tabbed dialog box. Here's how it works.
      Visual Studio goes to the control and asks it to show its property pages inside a dialog frame. (Visual Studio asks the control for a list of property pages through an interface named ISpecifyPropertyPages.) The property page is shown by Visual Studio, but remains connected to the control through a COM interface provided by the control. Whenever you finish editing the properties and dismiss the dialog box from Visual Studio, it asks the property page to update the properties in the control.
      When you generate a control in MFC, the wizard gives you a dialog box template and a class derived from COlePropertyPage that represent the default property page for your control. Visual Studio makes it easy to connect a control's properties to properties within the property page. When you add properties to your MFC-based control using ControlWizard's Automation tab, you give the property an external name. This name is how external clients (including the property page) will identify the property.
      You develop the property page as you would any other dialog boxâ€"by adding controls to the dialog box template and associating dialog box member variables to the controls. ControlWizard adds the DDX/DDV code to exchange the data between the dialog box controls and the member variables. However, as you associate member variables to the dialog box controls, ControlWizard gives you an opportunity to apply the external property name to the dialog box member variable. That external name is the string you typed when you added the property to the control. When the property page needs to apply the new changes to the control (for example, when the Apply button is pressed), the property page uses the control's IDispatch interface and the external name to modify the control's property.
      In MFC, you can add a new property page through the ClassWizard. You add a new dialog box template to the project and ask the ClassWizard to create a class for youâ€"just make sure the class derives from COlePropertyPage. Then, to make the new property page available to the outside world, include the new property page's GUID in the control's property page map (look for the BEGIN_ PROPPAGEIDS and END_PROPPAGEIDS macros in your control's .CPP file).
      Unlike the MFC ActiveX ControlWizard, the ATL COM App Wizard does not add a default property page to the DLL. That means you need to do it yourself. Fortunately, there's a wizard for adding property pages to an ATL-based DLL. Just select Insert ATL Object and find the property page object. The wizard adds a dialog template and a C++ class with all the necessary COM goo to be a property page. It's your job to make it do something.
      Unfortunately, the ATL property page isn't quite as wizard-driven as the MFC-based property page. You need to handle the apply and show operations by hand. This means providing implementations of functions named Apply and Show to your property page class. The Apply function just extracts the state of the controls sitting on the dialog box and walks through the list of interface pointers to the control held by the property page, using the interface pointers to modify the control properties. The Show function usually extracts the state of your control and populates the dialog box controls. This code shows how the ATL-based property page handles the Apply function:


STDMETHOD(Apply)(void)
{
    long nInterval = GetDlgItemInt(IDC_EDITINTERVAL);
    ATLTRACE(_T("CMainPropPage::Apply\n"));
    for (UINT i = 0; i < m_nObjects; i++)
    {
        IATLMsgTrafficCtl* pATLMsgTrafficCtl;
        m_ppUnk[i]->QueryInterface(IID_IATLMsgTrafficCtl, 
                                   (void**)&pATLMsgTrafficCtl);
        if(pATLMsgTrafficCtl) {
            pATLMsgTrafficCtl->put_Interval(nInterval);
            pATLMsgTrafficCtl->Release();
        }
    }
    m_bDirty = FALSE;
    return S_OK;
}
      The second step in providing a property page to the ATL-based control is to make sure the CLSID of the property page appears somewhere in the control's property map. The property persistence code shown in Figure 5 provides an example of this. The message map indicates the control's graph line color and is managed by the standard color property page. The control's sampling interval is managed by the control's main property page.

Window Messaging

      MFC and ATL share many similarities in the way they handle window messages. Both employ message maps and have wizards to generate code to handle window messages. In MFC, message maps can be added to any CCmdTarget-derived class. Then you can use ClassWizard to set up event handlers for your control. Figure 6 shows how the MFC-based control handles the WM_ TIMER message. In addition, MFC provides macros for handling commands and control notifications.
      Like MFC, ATL handles window messages through message maps. As long as your class derives from CWindowImpl and the class contains the ATL message map macros, you can use class view to set up event handlers for your class. Figure 7 shows how the ATL message traffic control handles the WM_TIMER message.
      ATL maps the standard window messages to a C++ class using the MESSAGE_HANDLER macro. The macro simply produces a table relating window messages to member functions in the class. In addition to regular messages, message maps are capable of handling other sorts of events. Figure 8 shows the kinds of macros that can go in a message map.

Connections and Events

      The last comparison to make is how MFC and ATL handle connection points and event sets. To manage connection points and event sets, a COM class needs to implement IConnectionPointContainer on its identity and then create a way to provide clients with pointers to IConnectionPoint as necessary. MFC's main control class, COleControl, already has IConnectionPointContainer built in, and MFC provides connection points through connection maps. MFC already defines the connection points for IPropertyNotifySink and the control's default event set.
      To flesh out the default event set in an MFC-based control, you simply employ the ClassWizard's ActiveX Event tab. As you add events using the ClassWizard, Visual Studio updates your control's .ODL file, describing the outgoing events for potential containers. In addition, Visual Studio adds a function to your class that you can call to fire the event back to the container. Figure 9 illustrates the MFC-based control's event firing mechanism. The MFC-based control's event firing functions are simple wrappers around an IDispatch pointer provided by the container while the container and the control set up their connection points.
      Setting up events in the ATL-based control is a bit different than setting up events in the MFC-based control. In the ATL-based control, you start by defining the events in the control's .IDL file. You then compile the project, which builds the type library. Figure 10 shows the ATL-based control's event set described in IDL.
      Once the type library is compiled, you can ask the class view to create a callback proxy for you by selecting the control's class from class view, right-clicking on the class, and selecting Implement Connection Point. Visual Studio pops up a dialog box listing all the available event interfaces in the control's type library. You select those you want a callback proxy for and Visual Studio writes a proxy for you. Figure 11 shows the ATL-based message traffic control's callback proxy. The callback proxy generated by Visual Studio represents a set of C++-friendly functions that call back to the interface implemented by the client.
      While MFC's IConnectionPointContainer implementation is hardwired into COleControl and each connection point is handled by a connection map, ATL's implementation is handled using multiple inheritance. Your control class inherits from IConnectionPointContainerImpl and the proxy generated by the class view. If you select "Supports connection points" when you start the project, the ObjectWizard inserts IConnectionPointContainerImpl for you. If you forgot to mark the checkbox, you can just type it in. This code shows how the connection point mechanism is brought into a control.


class ATL_NO_VTABLE CATLMsgTrafficCtl : 
{
���
   public IConnectionPointContainerImpl<CATLMsgTrafficCtl>,
   public CProxy_DATLMsgTrafficEvents<CATLMsgTrafficCtl>
���
    {
    LRESULT OnTimer(UINT msg, WPARAM wParam, 
                    LPARAM lParam, 
                    BOOL& bHandled) {
    //���    
        if(nMessagesToShow > m_threshold)
        {
            Fire_ExceededThreshold(nMessagesToShow, m_threshold);
        }
    //���
    }
};

ATL versus MFC as an Application Framework

      Very recently, many developers have become interested in using ATL as a framework for developing applications as well as controls. Of course, MFC has been around for eons and is a fully mature framework capable of generating double-clickable Windows-based apps. For example, MFC includes such features as a whole document/view architecture, Object Linking and Embedding support, and toolbars and status bars.
      However, all this functionality comes at a price. Some of the more common complaints include a fairly substantial footprint for MFC (in either the DLL or the statically linked version) as well as a sort of interdependence unto itself. For example, buying into one feature within MFC means buying into MFC's Object Linking and Embedding, which means buying into MFC's document/view architecture. ATL, on the other hand, is a raw framework without any of the application framework goodies.
      As you have seen, both frameworks represent viable ways to create controls. However, each has specific advantages and disadvantages. Writing controls using MFC is generally easierâ€"especially if you're not doing anything COM-intensive and you need windowing and drawing support. While ATL's architecture lies closer to the heart of COM, you'll often find yourself writing a lot of SDK-style codeâ€"that is, you'll be back to using window and device context handles. ATL provides great support for a wide variety of control types (such as composite controls, HTML-based controls, lighter-weight controls without the design-time interfaces, and so on). MFC provides only full-fledged controls.
      Hooking into ATL's implementations is fairly straightforward. For example, adding an interface is usually a matter of adding the interface to the inheritance list, adding an entry to the COM map, and implementing the interface functions. Hooking into MFC's implementation is usually quite an ordeal. For example, adding an interface to an MFC-based control means dealing with all those interface map macros.
      Finally, ATL offers a tremendous level of debugging support, including per-interface reference counting and support for debugging QueryInterface, which is completely absent from MFC.
      The differences between the two architectures are fairly stark. Generally, MFC enables you to get your project up and running more quickly, but sacrifices flexibility. ATL isn't quite as quick and easy to use, but it is COM-friendly. In addition, ATL appears to be getting easier to use as it matures.

George Shepherd is an instructor with DevelopMentor and a software engineer at RogueWave Software. George is coauthor of MFC Internals (Addison-Wesley, 1996) and Programming Visual C++ (Microsoft Press, 1998).


From the April 2000 issue of MSDN Magazine.

Page view tracker