Click Here to Install Silverlight*
United StatesChange|All Microsoft Sites
MSDN
|Developer Centers|Library|Downloads|Code Center|Subscriptions|MSDN Worldwide
Search for


Advanced Search
MSDN Home > MSJ > April 1999
April 1999

Microsoft Systems Journal Homepage

Write ActiveX Controls Using Custom Interfaces Provided by ATL 3.0, Part III

Brent Rector and Chris Sells

This third and final installment on ActiveX controls examines the steps necessary to host these controls using ATL 3.0. You'll learn how to host controls both in standalone applications as well as from within COM servers.

This article assumes you're familiar with C++, ATL, and COM

Code for this article: Container.exe (106KB)
This article is adapted from the forthcoming book, ATL Internals, by Brent Rector and Chris Sells (Addison-Wesley, 1999). Brent is founder of Wise Owl Consulting Inc., a COM consulting firm. Reach Brent at http://www.wiseowl.com/brent.htm. Chris is an independent consultant specializing in designing and building distributed systems using COM. Chris can be reached at http://www.sellsbrothers.com.


This article represents the third and final installment in our ActiveX® control series. In the previous two parts (which appeared in the February and March 1999 issues of MSJ), we concentrated on COM control creation using the latest version of ATL included with Visual C++® 6.0. Now we're going to complete the control picture by examining the requirements necessary to host these controls, again using ATL 3.0. When we're through you can expect to understand the ATL control creation mechanics, use, and management in both window and dialog-based host applications.
Full coverage of the interaction between controls and containers is beyond the scope of this article. For more information, refer to the Platform SDK. We will go over what you need to know to host controls both in standalone applications and inside of COM servers. Before diving into the details of dialogs or controls hosting other controls, let's start with the basics: control containment in a simple frame window.
COM control containers can take many forms. A window can contain any number of COM controls, as can a dialog or another control (called a composite control). To contain a COM control, a container must provide a window to act as the parent for the child COM control and implement a set of COM interfaces for communication between the control and the container.

Figure 1  Integrating Containers and Controls
Figure 1 Integrating Containers and Controls

The window provided by the container may be a parent window of the control or, in the case of a windowless control, it may be shared by the control. The control will use the window in its interaction with the user. The interfaces implemented by the container are used for integration with the control and mirror those implemented by the control (see Figure 1).

Control Creation Internals

The control creation process in ATL exposes the core of how ATL hosts controls. Figure 2 shows the overall process. Let's take a detailed look at the relevant bits of code involved.
Figure 2  Control Creation Process
Figure 2 Control Creation Process

ATL's implementation of the required container interfaces is called CAxHostWindow:
 // This class is not cocreateable
 class ATL_NO_VTABLE CAxHostWindow :
   public CComCoClass<CAxHostWindow , &CLSID_NULL>,
   public CComObjectRootEx<CComSingleThreadModel>,
   public CWindowImpl<CAxHostWindow>,
   public IAxWinHostWindow,
   public IOleClientSite,
   public IOleInPlaceSiteWindowless,
   public IOleControlSite,
   public IOleContainer,
   public IObjectWithSiteImpl<CAxHostWindow>,
   public IServiceProvider,
   public IAdviseSink,
 #ifndef _ATL_NO_DOCHOSTUIHANDLER
   public IDocHostUIHandler,
 #endif
   public IDispatchImpl<IAxWinAmbientDispatch, &IID_IAxWinAmbientDispatch, &LIBID_ATLLib>
 {...};
As you can see, a CAxHostWindow is two things: a window (from CWindowImpl) and a COM implementation (from CComObjectRootEx). When the container wants to host a control, it will create an instance of CAxHostWindow, but not directly. Instead, it will create an instance of a window class defined by ATL called AtlAxWin. This window will act as the parent window for the control and will eventually be subclassed by an instance of CAxHostWindow. Before an instance of this window class can be created, the window class must first be registered. ATL provides a function to register the AtlAxWin window class called AtlAxWinInit (see Figure 3).
Once the AtlAxWin class has been registered, creating a window based on this class will also create an instance of CAxHostWindow. The CAxHostWindow object will then use the title of the window as the name of the control to create and to host. For example, the following code will create a CAxHostWindow and cause it to host a new instance of the BullsEye control developed in previous issues of MSJ:
 class CMainWindow : public CWindowImpl<CMainWindow,...> {
 •
 •
 •
   LRESULT OnCreate(UINT uMsg, WPARAM wParam,
                   LPARAM lParam, BOOL& lResult) {
     // Create the host window, the CAxHostWindow object
     // and the BullsEye control, and host the control
     RECT rect; GetClientRect(&rect);
     LPCTSTR pszName = __T("ATLInternals.BullsEye");
     HWND hwndContainer = m_ax.Create(__T("AtlAxWin"),
         m_hWnd, rect, pszName, WS_CHILD | WS_VISIBLE);
     if( !hwndContainer ) return -1;
     return 0;
   }

 private:
   CWindow m_ax;
 };
Creation of the CAxHostWindow object and the corresponding control is initiated in the WM_CREATE handler of the AtlAxWin WndProc, AtlAxWindowProc (see Figure 4). Notice that the window's text, as passed to the call to CWindow::Create, is used as the name of the control to create. The call to AtlAxCreateControl passes the name of the control forward to AtlAxCreateControlEx, which furthers things along by creating a CAxHostWindow object and asking it to create and host the control:
 ATLINLINE ATLAPI
 AtlAxCreateControlEx(
   LPCOLESTR   lpszName,
   HWND        hWnd,
   IStream*    pStream,
   IUnknown**  ppUnkContainer,
   IUnknown**  ppUnkControl,
   REFIID      iidSink,
   IUnknown*   punkSink)
 {
   AtlAxWinInit();
   HRESULT hr;
   CComPtr<IUnknown> spUnkContainer;
   CComPtr<IUnknown> spUnkControl;

   hr = CAxHostWindow::_CreatorClass::CreateInstance(
       NULL, IID_IUnknown, (void**)&spUnkContainer);
   if (SUCCEEDED(hr)) {
       CComPtr<IAxWinHostWindow> pAxWindow;
       spUnkContainer->
          QueryInterface(IID_IAxWinHostWindow,
                          (void**)&pAxWindow);
       CComBSTR bstrName(lpszName);
       hr = pAxWindow->CreateControlEx(bstrName, hWnd, pStream, &spUnkControl, iidSink, punkSink);
   }
 •
 •
 •
   return hr;
 }
AtlAxCreateControlEx uses the IAxWinHostWindow interface to create the control. IAxWinHostWindow is one of the few interfaces that ATL defines, and one of the interfaces that CAxHostWindow implements. Its job is to allow for management of the control that it's hosting:
 interface IAxWinHostWindow : IUnknown {
   HRESULT CreateControl([in] LPCOLESTR lpTricsData,
                         [in] HWND hWnd,
                         [in] IStream* pStream);
   HRESULT CreateControlEx([in] LPCOLESTR lpTricsData,
                           [in] HWND hWnd,
                           [in] IStream* pStream,
                           [out] IUnknown** ppUnk,
                           [in] REFIID riidAdvise,
                           [in] IUnknown* punkAdvise);
   HRESULT AttachControl([in] IUnknown* pUnkControl,
                         [in] HWND hWnd);
   HRESULT QueryControl([in] REFIID riid,
                        [out, iid_is(riid)] void
                         **ppvObject);
   HRESULT SetExternalDispatch([in] IDispatch*pDisp);
   HRESULT SetExternalUIHandler(
       [in] IDocHostUIHandlerDispatch* pDisp);
 };
To create a new control in the CAxHostWindow, IAxWinHostWindow provides CreateControl or CreateControlEx, which is what AtlAxCreateControlEx uses after the CAxHostWindow object is created.
The first parameter of CreateControl[Ex] is lpTricsData, the name of the control to create. It can take the form of a CLSID, a ProgID, a URL, a file name, or raw HTML. We'll discuss more about this later. hWnd holds the parent window to host control. This window will be sub-classed by CAxHostWindow. pStream is the stream that holds object initialization data. The control will be initialized via IPersistStreamInit. If pStream is non-NULL, Load will be called. Otherwise, InitNew will be called. ppUnk will be filled with an interface to the newly created control. If riidAdvise is not IID_NULL, CAxHostWindow will attempt to set up a connection point connection between the control and the sink object represented by the punkAdvise parameter. CAxHostWindow will manage the resultant cookie and tear down the connection when the control is destroyed. punkAdvise is an interface to the sink object that implements the sink interface specified by riidAdvise.
The AttachControl method of IAxWinHostWindow attaches a control that has already been created and initialized to an existing CAxHostWindow object. The QueryControl method allows access to the control's interfaces hosted by the CAxHostWindow object. Both SetExternalDispatch and SetExternalUIHandler are for use when hosting the Microsoft
® Internet Explorer HTML Control, and they're beyond the scope of this article.
CAxHostWindow's implementation of CreateControlEx subclasses the parent window for the new control, creates a new control, and then activates it. If an initial connection point is requested, AtlAdvise is used to establish that connection. If the newly created control is to be initialized from raw HTML or navigated to via a URL, CreateControlEx does that, too (see Figure 5).
How the control name is interpreted depends on another function called CreateNormalized- Object. The activation is handled by the ActivateAx function. The CreateNormalizedObject will create an instance of a COM object using strings of the form shown in Figure 6. Since CAxHostWindow uses the title of the window to obtain the name passed to CreateNormalizedObject, you can use any of these string formats when creating an instance of the AtlWinInit window class.
The ActivateAx function really performs the magic of the control creation process. It takes an interface pointer from the object that CreateNormalizedObject creates and activates it as a COM control in the parent window. ActivateAx is responsible for the following:
  • Setting the client site—that is, the CAxHostWindow's implementation of IOleClientSite—via the control's implementation of IOleObject.
  • Calling either InitNew or Load (depending on whether the pStream argument to AtlAxCreateControlEx is NULL or non-NULL) via the control's implementation of IPersistStreamInit.
  • Passing the CAxHostWindow's implementation of IAdviseSink to the control's implementation of IViewObject.
  • Setting the control's size to the size of the parent window, also via the control's implementation of IOleObject.
  • Finally, to show the control and allow it to handle input and output, call DoVerb(OLEIVERB_INPLACEACTIVATE) via the control's implementation of IOleObject.

This completes the activation of the control. However, creating an instance of the CAxHostWindow via direct reference to the AtlAxWin window class is not typical. The implementation details of AtlAxWin and CAxHostWindow are meant to be hidden from the average ATL programmer. The usual way a control is hosted under ATL is via an instance of a wrapper class called CAxWindow.
CAxWindow simplifies the use of CAxHostWindow with a set of wrapper functions. The initial creation part of CAxWindow class is defined like so:
  template <class TBase =
    CWindow>
    class CAxWindowT :
    public TBase {
  public:
    // Constructors
    CAxWindowT(HWND hWnd =
      NULL) : TBase(hWnd) {}
    CAxWindowT< TBase >&
      operator=(
      HWND hWnd) { m_hWnd =
         hWnd; return *this;
       }

   // Attributes
   static LPCTSTR GetWndClassName()
     { return _T("AtlAxWin"); }

   // Operations
   HWND Create(HWND hWndParent, RECT& rc,
               LPCTSTR szName = NULL, ...)
   { return CWindow::Create(GetWndClassName(),
                            hWndParent, rc,
                            szName, ...); }

   HWND Create(HWND hWndParent, LPRECT lpr = NULL,
               LPCTSTR szName = NULL...)
   { return CWindow::Create(GetWndClassName(),
              hWndParent, lpr, szName, ...); }
 •
 •
 •
 };

 typedef CAxWindowT<CWindow> CAxWindow;
Notice that both Create functions still require the parent window and the name of the control, but they do not require passing the name of the CAxHostWindow window class. Instead, CAxWindow knows the name of the appropriate class itself (available via the static member function GetWndClassName) and passes it to the CWindow base class. Using CAxWindow reduces the code required to host a control to the following:
 class CMainWindow : public CWindowImpl<CMainWindow,...> {
 .
 .
 .
   LRESULT OnCreate(UINT uMsg, WPARAM wParam,
                   LPARAM lParam, BOOL& lResult)
  {
     // Create the host window, the CAxHostWindow object
     // and the BullsEye control, and host the control
     RECT    rect; GetClientRect(&rect);
     LPCTSTR pszName = __T("ATLInternals.BullsEye");
     HWND    hwndContainer = m_ax.Create(m_hWnd, rect,
                      pszName, WS_CHILD | WS_VISIBLE);
     if( !hwndContainer ) return -1;
     return 0;
   }

 private:
   CAxWindow m_ax;
 };

Two-step Control Creation

You may have noticed that AtlAxCreateControlEx takes some interesting parameters, like an IStream interface pointer and an interface ID/interface pointer pair to specify an initial connection point. However, while the window name can be used to pass the name of the control, there are no extra parameters to CreateWindow except for a couple of interface pointers and a GUID. Instead, CAxWindow provides a few extra wrapper functions, CreateControl and CreateControlEx (see Figure 7). CreateControl and CreateControlEx allow for the extra parameters that AtlAxCreateControlEx supports. The extra parameter that the CAxWindow wrappers support beyond those passed to AtlAxCreateControlEx is dwResID, which serves as an ID of an HTML page embedded in the resources of the module. This parameter will be formatted into a string of the format "res://<module path>/<dwResID>" before being passed to AtlAxCreateControlEx.
These functions seem to be designed for use in a two-stage construction of first the host and then its control:

 LRESULT
 CMainWindow::OnCreate(UINT uMsg, WPARAM wParam,
                       LPARAM lParam, BOOL& lResult)
 {
   RECT    rect; GetClientRect(&rect);
   // Phase one: Create the container
   HWND    hwndContainer = m_ax.Create(m_hWnd, rect, 0,
                                WS_CHILD | WS_VISIBLE);
   if( !hwndContainer ) return -1;

   // Phase two: Create the control
   LPCOLESTR pszName = OLESTR("ATLInternals.BullsEye");
   // This leaks, keep reading!
   HRESULT hr = m_ax.CreateControl(pszName);
   return (SUCCEEDED(hr) ? 0 : -1);
 };
We'll show you how to persist a control and how to handle events from a control later on. If you've already created a control and initialized it, you can still use the hosting functionality of ATL by attaching the existing control to a host window via the AttachControl function:
 template <class TBase = CWindow> class CAxWindowT : public TBase {
 public:
 .
 .
 .
   HRESULT AttachControl(IUnknown* pControl,
                         IUnknown** ppUnkContainer) {
     ATLASSERT(::IsWindow(m_hWnd));
     return AtlAxAttachControl(pControl, m_hWnd,
                               ppUnkContainer);
   }
 .
 .
 .
 };
AttachControl is meant to be used like so:
 LRESULT
 CMainWindow::OnCreate(UINT uMsg, WPARAM wParam,
                       LPARAM lParam, BOOL& lResult)
 {
   RECT    rect; GetClientRect(&rect);
   // Phase one: Create the
   // container
   HWND    hwndContainer = m_ax.Create(m_hWnd, rect, 0,
                                WS_CHILD | WS_VISIBLE);
   if( !hwndContainer ) return -1;

   // Create and initialize a control
   CComPtr<IUnknown> spunkControl; // ...

   // Phase two: Attach an existing control
   // This also leaks!
   HRESULT hr = m_ax.AttachControl(spunkControl);
   return (SUCCEEDED(hr) ? 0 : -1);
 };
As attractive as the CreateControl, CreateControlEx, and AttachControl member functions are, you should avoid using them like this as of ATL 3.0. They leak. Phase one creates an instance of CAxHostWindow, even if the window text is empty. All three of the second-phase control creator functions also create an instance of CAxHostWindow and never destroy the first one. You're leaking the size of a CAxHostWindow object (224 bytes) every time you call one of these three functions.
Luckily, there's a workaround. Using the QueryHost member function of CAxWindow, you can obtain the IAxWinHostWindow interface on the existing CAxHostWindow object:
 template <class TBase = CWindow> class CAxWindowT :
 public TBase {
 public:

   HRESULT QueryHost(REFIID iid, void** ppUnk) {
     CComPtr<IUnknown> spUnk;
     HRESULT hr = AtlAxGetHost(m_hWnd, &spUnk);
     if (SUCCEEDED(hr)) hr = spUnk->QueryInterface(iid,
                                                ppUnk);
     return hr;
   }
   template <class Q> HRESULT QueryHost(Q** ppUnk)
   { return QueryHost(
       __uuidof(Q), 	      (void**)ppUnk); }
 };
QueryHost uses the AtlAxGetHost function to send a custom window message to the AtlAxWin window to obtain an interface pointer on the host. Once you have the IAxWinHostWindow interface, you can call the interface member functions CreateControl, CreateControlEx, or AttachControl without worry of leaking because now you're reusing the existing CAxHostWindow instead of creating a new one.
 LRESULT
 CMainWindow::OnCreate(
     UINT uMsg,
     WPARAM wParam,
     LPARAM lParam, BOOL& lResult)
 {
   RECT    rect; GetClientRect(&rect);
   // Phase one: Create the container
   HWND    hwndContainer = m_ax.Create(m_hWnd, rect, 0,
                                WS_CHILD | WS_VISIBLE);
   if( !hwndContainer ) return -1;

   // Phase two: Create the control in the existing
   // container
   CComPtr<IAxWinHostWindow> spAxWindow;
   HRESULT hr = m_ax.QueryHost(&spAxWindow);
   if( FAILED(hr) ) return -1;

   LPCOLESTR pszName = OLESTR("ATLInternals.BullsEye");
   hr = spAxWindow->CreateControl(pszName,
                           m_ax.m_hWnd, 0);
   if( FAILED(hr) ) return -1;

   return 0;
 };
Because it's kind of a pain to do this all manually, the source code for this article, which you can find here, contains an alternative to CAxWindow called CAxWindow2. CAxWindow2 derives from CAxWindow and provides implementations of CreateControl, CreateControlEx, and AttachControl that reuse the existing CAxHostWindow object instead of creating a new one. The CAxWindow2 class allows you to use the CAxWindow member functions CreateControl, CreateControlEx, and AttachControl as we've shown previously, but without the leak.

Using the Control

Once you've created the control, it's really two things: a window and a control. The window is an instance of AtlAxWin and hosts the control, which may or may not have its own window. (CAxHostWindow provides full support for windowless controls.) Since CAxWindow derives from CWindow, you can treat it like a window (move it, resize it, or hide it), and AtlAxWin will handle those messages by translating them into the appropriate COM calls on the control. For example, if you'd like the entire client area of a frame window to contain a control, you can handle the WM_SIZE message like so:
 class CMainWindow : public
   CWindowImpl<CMainWindow,
   ...> {
 .
 .
 .
   LRESULT OnSize(UINT,
     WPARAM, LPARAM lParam,
     BOOL&) {
     // m_ax will be
     // created earlier,
     // e.g. WM_CREATE
     if( m_ax ) {
       RECT rect = { 0, 0,
         LOWORD(lParam),
         HIWORD(lParam) };
      // Resize the control
      m_ax.MoveWindow(
        &rect);
     }
     return 0;
   }

 private:
   CAxWindow m_ax;
 };
Unlike Windows® controls, COM controls do not accept functionality requests via Windows messages (remember, a COM control may not even have a window). Instead, because COM controls are COM objects, they expect to be programmed via their COM interfaces. If you want to obtain an interface on the control, CAxWindow provides the QueryControl method:
 template <class TBase = CWindow> class CAxWindowT :
 public TBase {
 public:
 .
 .
 .
   HRESULT QueryControl(REFIID iid, void** ppUnk) {
     CComPtr<IUnknown> spUnk;
     HRESULT hr = AtlAxGetControl(m_hWnd, &spUnk);
     if (SUCCEEDED(hr)) hr = spUnk->QueryInterface(iid,
                                                ppUnk);
     return hr;
   }

   template <class Q> HRESULT QueryControl(Q** ppUnk)
   { return QueryControl(__uuidof(Q), (void**)ppUnk); }
 };
Like QueryHost, QueryControl uses a global function (AtlAxGetControl in this case) that sends a window message to the AtlAxWin window to retrieve an interface, this time from the hosted control itself. Once the control has been created, QueryControl can be used to get at the interfaces of the control:
 // Import interface definitions for BullsEye
 #import "D:\ATLBook\src\atlinternals\Debug\BullsEyeCtl.dll" \
         raw_interfaces_only raw_native_types no_namespace named_guids

 LRESULT
 CMainWindow::OnCreate(UINT uMsg, WPARAM wParam,
                       LPARAM lParam, BOOL& lResult)
 {
   // Create the control
   .
   .
   .

   // Set initial BullsEye properties
   CComPtr<IBullsEye>  spBullsEye;
   HRESULT hr = m_ax.QueryControl(&spBullsEye);
   if( SUCCEEDED(hr) ) {
     spBullsEye->put_Beep(VARIANT_TRUE);
     spBullsEye->put_CenterColor(RGB(0, 0, 255));
   }

   return 0;
 };
Notice the use of the #import statement to pull in the definitions of the interfaces of the control you're programming against. This is necessary if you've only got the control's server DLL and the bundled type library, but no original IDL (a common occurrence when programming against controls). Notice also the use of the #import statement attributes, such as raw_interfaces_only. These attributes are used to mimic as closely as possible the C++ language mapping you would have gotten had you used midl.exe on the server's IDL file. Without these attributes, Visual C++ will create a language mapping that uses the compiler-provided wrapper classes, such as _bstr_t, _variant_t, and _com_ptr_t, which are different than the ATL-provided types, such as CComBSTR, CComVariant, and CComPtr. While the compiler-provided classes have their place, we find it's best not to mix them with the ATL-provided types if possible. Apparently the ATL team agrees with me, as the ATL object Wizard-generated #import statements also use these attributes. (I'll talk more about the control-containment-related wizards later.)

Sinking Control Events

Not only are you likely going to want to program against the interfaces that the control implements, you're likely going to want to handle events fired by the control. Most controls have an event interface which, for maximum compatibility with the largest number of clients, is often a dispinterface. For example, the BullsEye control defines the following event interface:
 const int DISPID_ONRINGHIT      = 1;
 const int DISPID_ONSCORECHANGED = 2;

 dispinterface _IBullsEyeEvents {
 properties:
 methods:
   [id(DISPID_ONRINGHIT)] void OnRingHit(
       short ringNumber);
   [id(DISPID_ONSCORECHANGED)] void OnScoreChanged(
       long ringValue);
 };
For a control container to handle events fired on a dispinterface would require an implementation of IDispatch. Implementations of IDispatch are easy if the interface is defined as a dual, but much harder if defined as a raw dispinterface. ATL provides a helper class for implementing an event dispinterface called IDispEventImpl:
 template <UINT nID, class T,
           const IID* pdiid = &IID_NULL,
           const GUID* plibid = &GUID_NULL,
           WORD wMajor = 0, WORD wMinor = 0,
           class tihclass = CComTypeInfoHolder>
 class ATL_NO_VTABLE IDispEventImpl :
   public IDispEventSimpleImpl<nID, T, pdiid> {...};
IDispEventImpl uses a data structure called a sink map established via the following macros:
 #define BEGIN_SINK_MAP(_class) ...
 #define SINK_ENTRY_INFO(id, iid, dispid, fn, info) ...
 #define SINK_ENTRY_EX(id, iid, dispid, fn) ...
 #define SINK_ENTRY(id, dispid, fn) ...
 #define END_SINK_MAP() ...
The gory details of these are beyond the scope of this article, but the sink maps provide a mapping between a specific object/iid/dispid that defines an event and a member function to handle that event. If the object is a nonvisual one, the sink map can be a bit involved. If the object is a COM control, usage of IDispEventImpl and the sink map are quite simple, as you're about to see.
To handle events, the container of the controls will derive from one instance of IDispEventImpl per control. Notice that the first template parameter of IDispEventImpl is an ID. This ID is going to match to the contained control via the child window ID, the nID parameter to Create. This same ID is going to be used in the sink map to route events from a specific control to the appropriate event handler. The child window ID is what makes the IDispEventImpl so simple in the control case.
Handling the events of the BullsEye control merely requires an IDispEventImpl base class and an appropriately constructed sink map (see Figure 8). Notice that the child window control ID (ID_ BULLSEYE) is used in four places. The first is the IDispEventImpl base class. The second is the call to Create, marking the control as the same one that will be sourcing events. The last two uses of ID_BULLSEYE are the entries in the sink map, which route events from the ID_BULLSEYE control to their appropriate handlers.
Notice also that the event handlers are marked __stdcall. Remember that we're using IDispEventImpl to implement IDispatch for a specific event interface (as defined by the DIID_IBullsEyeEvents interface identifier). That means IDispEventImpl must unpack the array of VARIANTs passed to Invoke, push them on the stack, and call our event handler. It does this using type information at runtime, but it still has to know about the calling convention: in what order should the parameters be passed on the stack and who's responsible for cleaning them up. To alleviate any confusion, IDispEventImpl requires that all event handlers have the same calling convention, which __stdcall defines.
Once we've got IDispEventImpl and the sink map set up, we're still not finished. Unlike Windows controls, COM controls have no real sense of their parent. This means that instead of implicitly knowing to whom to send events, like an edit control does, a COM control must be told who wants the events. Because events are established between controls and containers with the connection point protocol, somebody's got to call QueryInterface for IConnectionPointContainer, FindConnectionPoint to obtain the IConnectionPoint interface, and finally Advise to establish the container as the sink for events fired by the control.
That's not so much work for one control, and ATL even provides a function called AtlAdvise to help. For multiple controls, managing the communication with each of them can become a chore. And since we've got a list of all the controls with which we'd like to establish communications in the sink map, it makes sense to use that knowledge to automate the chore. Luckily, we don't even have to do this much, because ATL's already done it for us with AtlAdviseSinkMap:
 template <class T> inline HRESULT AtlAdviseSinkMap(
     T* pT, bool bAdvise)
The first argument to AtlAdviseSinkMap is a pointer to the object that wants to set up the connection points with the objects listed in the sink map. The second parameter is a Boolean determining if we are setting up or tearing down communication. Because AtlAdviseSinkMap depends on the child window ID to map to a window that already contains a control, setting up and tearing down connection points must occur when the child windows are still living and contain controls. Handlers for the WM_CREATE and WM_DESTROY messages are excellent for this purpose:
 LRESULT CMainWindow::OnCreate(...) {
   // Create the controls
 •
 •
 •
   // Establish connection points
   AtlAdviseSinkMap(this, true);
   return 0;
 }

 LRESULT CMainWindow::OnDestroy(...) {
   // Controls still live
   // Tear down connection points
   AtlAdviseSinkMap(this, false);
   return 0;
 }
IDispEventImpl, the sink map, and the AtlAdviseSinkMap function are all that you need to sink events from a COM control. But things can be simplified even further. Most controls implement only a single event interface and publish this fact in one of two places. The default source interface can be provided by an implementation of IProvideClassInfo2 and it can be published in the coclass statement in the IDL (and, therefore, as part of the type library):
 coclass BullsEye {
   [default]         interface IBullsEye;
   [default, source] dispinterface _IBullsEyeEvents;
 };
In the event that IDispEventImpl is used with IID_NULL as the template parameter (which is the default value) describing the interface to be sinked, ATL will do its best to establish communications with the default source interface via a function called AtlGetObjectSourceInterface. This function will attempt to obtain the object's default source interface, using the type information obtained via the GetTypeInfo member function of IDispatch. It first attempts to use IProvideClassInfo2, and if that's not available, it will dig through the coclass looking for the [default, source] interface. The upshot is that if you want to source the default interface of a control, the parameters to IDispEventImpl are fewer and you can use the simpler SINK_ENTRY:
 class CMainWindow :
   public CWindowImpl<CMainWindow, CWindow,
                      CMainWindowTraits>,
   // Sink the default source interface
   public IDispEventImpl<ID_BULLSEYE, CMainWindow>
 {
 .
 .
 .
 BEGIN_SINK_MAP(CMainWindow)
   // Sink events from the default BullsEye event
   // interface
   SINK_ENTRY(ID_BULLSEYE, 1, OnRingHit)
   SINK_ENTRY(ID_BULLSEYE, 2, OnScoreChanged)
 END_SINK_MAP()

   void __stdcall OnRingHit(short nRingNumber);
   void __stdcall OnScoreChanged(LONG ringValue);

 private:
   CAxWindow m_ax;
 };

Property Changes

In addition to a custom event interface, controls will often source events on the IPropertyNotifySink interface:

 interface IPropertyNotifySink : IUnknown {
   HRESULT OnChanged([in] DISPID dispID);
   HRESULT OnRequestEdit([in] DISPID dispID);
 }
IPropertyNotifySink is used by the control to ask the container if it's OK to change a property (OnRequestEdit) and to notify the container that a property has been changed (OnChanged). OnRequestEdit is used for data binding. OnChanged can be a handy notification, especially if the container expects to persist the control and wants to use OnChanged as an is-dirty notification. Even though IPropertyNotifySink is a connection point interface, it's not a dispinterface, so neither IDispEventImpl nor a sink map are required—normal C++ inheritance and AtlAdvise will do:
 class CMainWindow :
   public CWindowImpl<CMainWindow, ...>,
   public IPropertyNotifySink {
 public:
 .
 .
 .
   // IUnknown, assuming an instance on the stack
   STDMETHODIMP QueryInterface(REFIID riid, void** ppv) {
     if( riid == IID_IUnknown || riid ==
         IID_IPropertyNotifySink )
        *ppv = static_cast<IPropertyNotifySink*>(this);
     else return *ppv = 0, E_NOINTERFACE;
     return reinterpret_cast<IUnknown*>(*ppv)->AddRef(),
         S_OK;
   }

   STDMETHODIMP_(ULONG) AddRef() { return 2; }
   STDMETHODIMP_(ULONG) Release() { return 1; }

   // IPropertyNotifySink
   STDMETHODIMP OnRequestEdit(DISPID dispID) {
       return S_OK; }
   STDMETHODIMP OnChanged(DISPID dispID) {
       m_bDirty = true; return S_OK; }

 private:
   CAxControl m_ax;
   bool       m_bDirty;
 };
The simplest way to establish the IPropertyNotifySink connection is to use the CAxWindow member function CreateControlEx, which allows for a single connection point interface to be established and the cookie to be managed by the CAxHostWindow object, as shown here:
 LRESULT CMainWindow::OnCreate(...) {
   // Create the control host
 •
 •
 •
   // Create the control and set up IPropertyNotifySink
   // connection point
   m_ax.CreateControlEx(OLESTR("AtlInternals.BullsEye"),
                        0, 0, 0,
                        IID_IPropertyNotifySink, this);
   return 0;
 }
The connection point cookie for IPropertyNotifySink will be managed by the CAxHostWindow object. When the control is destroyed, the connection will be torn down automatically. While this trick only works for one connection point interface, this and the sink map are likely all you'll ever need when handling events from controls.
In addition to programming the properties of the control, you may wish to program the properties of the control's environment, known as ambient properties. For this purpose, CAxHostWindow implements the IAxWinAmbientDispatch interface (see Figure 9).
QueryHost can be used on a CAxWindow to obtain the IWinAmbientDispatch interface so that these ambient properties can be changed.
 LRESULT CMainWindow::OnSetGreenBackground(...) {
   // Set up green ambient
   // background
   CComPtr<IAxWinAmbientDispatch> spAmbient;
   hr = m_ax.QueryHost(
       &spAmbient);
   if( SUCCEEDED(hr) ) {
     spAmbient->put_BackColor(RGB(0, 255, 0));
   }
   return 0;
 }
Whenever an ambient property is changed, the control is notified via its implementation of the IOleControl member function OnAmbientPropertyChange. The control can then QueryInterface any of its container interfaces for IDispatch to obtain the interface for retrieving the ambient properties (which is why IWinAmbientDispatch is a dual interface).

Persisting a Control and Accelerator Translations

It may be that the control's state is something that you'd like to persist between application sessions. This can be done with any number of persistence interfaces, of which most controls implement IPersistStreamInit (although IPersistStream is a common fallback).
For example, saving a control to a file can be done with a stream in a structured storage document (see Figure 10). Restoring a control from a file is somewhat easier because both the CreateControl and the CreateControlEx member functions of CAxWindow take an IStream interface pointer to use for persistence (see Figure 11). If a NULL IStream interface pointer is provided to either CreateControl or CreateControlEx, ATL will attempt to call the IPersistStreamInit member function InitNew to make sure that either InitNew or Load is called as appropriate.
It's common for contained controls to contain other controls. For keyboard accelerators such as the Tab key to provide for navigation between controls, the main message loop must be augmented with a call to each window hosting a control to allow it to pretranslate the message as a possible accelerator. This functionality must ask the host of the control with focus if it wants to handle the message. If the control does handle the message, no more handling need be done on that message. Otherwise, the message processing can proceed as normal. A typical implementation of a function to attempt to route messages from the container window to the control itself (whether it's a windowed or a windowless control) is shown here:
 BOOL CMainWnd:: PreTranslateAccelerator(MSG* pMsg) {
     // Accelerators are only keyboard or mouse messages
     if ((pMsg->message < WM_KEYFIRST || pMsg->message >
          WM_KEYLAST) && (pMsg->message <
          WM_MOUSEFIRST || pMsg->message >
          WM_MOUSELAST))
         return FALSE;

     // Find a direct child of this window from the
     // window that has focus.
     // This will be AtlAxWin window for the hosted
     // control.
     HWND hWndCtl = ::GetFocus();
     if( IsChild(hWndCtl) && ::GetParent(hWndCtl) !=
         m_hWnd ) {
         do hWndCtl = ::GetParent(hWndCtl);
         while( ::GetParent(hWndCtl) != m_hWnd );
     }

     // Give the control (via the AtlAxWin) a chance to
     // translate this message
     if (::SendMessage(hWndCtl, WM_FORWARDMSG, 0,
                       (LPARAM)pMsg) ) return TRUE;

     // Check for dialog-type navigation accelerators
     return IsDialogMessage(pMsg);
 }
The crux of this function forwards the message to the AtlAxWin via the WM_FORWARDMSG message. This message is interpreted by the host window as an attempt to let the control handle the message, if it so desires. This message will be forwarded to the control via a call to the IOleInPlaceActiveObject member function TranslateAccelerator. The PreTranslateAccelerator function should be called from the application's main message pump like so:
 int WINAPI WinMain(...) {
   .
   .
   .
   CMainWindow wndMain;
   .
   .
   .
   HACCEL  haccel = LoadAccelerators(_Module.GetResourceInstance(),
                        MAKEINTRESOURCE(IDC_MYACCELS));
   MSG msg;
   while( GetMessage(&msg, 0, 0, 0) ) {
     if(!TranslateAccelerator(msg.hwnd, haccel, &msg) &&
        !wndMain.PreTranslateAccelerator(&msg) ) {
         TranslateMessage(&msg);
         DispatchMessage(&msg);
     }
   }
   .
   .
   .
 }
The use of a PreTranslateAccelerator function on every window that contains a control will give the keyboard navigation keys a much greater chance of working, although the individual controls have to cooperate, too.

Control Initialization

So far, we've discussed the basics of control containment using a frame window as a control container. An even more common place to contain controls is the ever-popular dialog. For quite a while, the Visual C++ resource editor has allowed a control to be inserted into a dialog resource by right-clicking on a dialog resource and choosing Insert ActiveX Control. As of Visual C++ 6.0, ATL supports creating dialogs that host the controls inserted into a dialog resource. The Insert ActiveX Control dialog is shown in Figure 12.
Figure 12 Visual C++ 6.0 Resource Editor
Figure 12 Visual C++ 6.0 Resource Editor

Our container sample, which you can find at the link at the top of this article, has a simple dialog box with a BullsEye control, along with a couple of static controls and a button. This is what that dialog resource looks like in the .rc file:
 IDD_BULLSEYE DIALOG DISCARDABLE  0, 0, 342, 238
 STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION |
     WS_SYSMENU
 CAPTION "BullsEye"
 FONT 8, "MS Sans Serif"
 BEGIN
     CONTROL "",IDC_BULLSEYE,
         "{7DC59CC5-36C0-11D2-AC05-00A0C9C8E50D}",
         WS_TABSTOP,7,7,269,224
     LTEXT "&Score:",IDC_STATIC,289,7,22,8
     CTEXT "Static",IDC_SCORE,278,18,46,14,
         SS_CENTERIMAGE | SS_SUNKEN
     PUSHBUTTON "Close",
         IDCANCEL,
         276,41,50,14
 END
Notice that the window text part of the CONTROL resource is a CLSID, specifically the CLSID of the BullsEye control. This window text will be passed to an instance of the AtlAxWin window class to determine the type of the control to create. In addition, another part of the .rc file maintains a separate resource called a DLGINIT resource, which is identified with the same ID as the BullsEye control on the dialog, IDC_ BULLSEYE. This resource contains the persistence information, converted to text format, that will be handed to the BullsEye control at creation time (via IPersistStreamInit):
 IDD_BULLSEYE DLGINIT
 BEGIN
     IDC_BULLSEYE, 0x376, 154, 0
 0x0026, 0x0000, 0x007b, 0x0039, 0x0035, 0x0036, 0x0046, 0x0043, 0x0032,
 •
 •
 •
 0x0000, 0x0040, 0x0000, 0x0020, 0x0000,
     0
 END
As most folks prefer not to enter this information directly, right-clicking on a COM control and choosing Properties will show the control's property pages, along with the custom property pages of the resource editor. Figure 13 shows the BullsEye properties dialog.
Figure 13 BullsEye Properties
Figure 13 BullsEye Properties

The DLGINIT resource for each control is constructed by asking each control for IPersistStreamInit, calling Save, converting the result to a text format, and dumping it into the .rc file. In this way, all information set at design time will be automatically restored at runtime.

Sinking Control Events in a Dialog

Remember that sinking control events requires adding one IDispEventImpl per control to the list of base classes of your dialog class and populating the sink map. While this has to be done by hand if a window is the container, it can be performed automatically if a dialog is to be the container. By right-clicking on the control and choosing Events, you can choose the events to handle, and the IDispEventImpl and sink map entries will be added for you. Figure 14 shows the Event Handlers dialog.
Figure 14 Handling BullsEye Events
Figure 14 Handling BullsEye Events

While the wizard will add the IDispEventImpl classes and manage the sink map, it will not insert code to call AtlAdviseSinkMap, either in WM_INITDIALOG to establish connection points with the controls or in WM_DESTROY to tear down the connection points. You have to remember to do this yourself.
Since the built-in dialog box manager window class has no idea how to host controls, ATL has to perform some magic on the dialog resource. It must preprocess the dialog box resource looking for CONTROL entries and replacing them with entries that will create an instance of an AtlAxWin window. Once this is done, the AtlAxWin uses the name of the window to create the control and the DLGINIT data to initialize it, providing all the control-hosting functionality we've spent most of this article dissecting. To hook up this preprocessing step when hosting controls in dialogs, use the CAxDialogImpl base class (see Figure 15). Notice that the DoModal and Create wrapper functions call AtlAxDialogBox and AtlAxCreateDialog instead of DialogBoxParam and CreateDialogParam, respectively. These functions perform the preprocessing necessary to add swap instances of AtlAxWin for each CONTROL entry in the dialog resource.
Using CAxDialogImpl as the base class, we can have a dialog that hosts COM controls as shown in Figure 16. Notice that, just like a normal dialog, the message map handles messages for the dialog itself (like WM_INITDIALOG and WM_DESTROY) as well provides a mapping between the class and the dialog resource id (via the IDD symbol). The only thing new is that, because we've used CAxDialogImpl as the base class, the COM controls will be created as the dialog is created.

Attaching a CAxWindow

During the life of the dialog, you will likely need to program against the interfaces of the contained COM controls, which means you'll need some way to obtain an interface on a specific control. One way to do this is with an instance of CAxWindow. Since ATL has created an instance of the AtlAxWin window class for each of the COM controls on the dialog, you use the Attach member function of a CAxWindow to attach to a COM control, and thereafter use the CAxWindow object to manipulate the host window. Once you've attached a CAxWindow object to an AtlAxWin window, you can use the member functions of CAxWindow to communicate with the control host window. Remember that you use the QueryControl member function to obtain an interface from a control:
 class CBullsEyeDlg :
   public CAxDialogImpl<CBullsEyeDlg>,
   public IDispEventImpl<IDC_BULLSEYE, CBullsEyeDlg> {
 public:
 •
 •
 •
   LRESULT OnInitDialog(…) {
     // Attach to the BullsEye control
     m_axBullsEye.Attach(GetDlgItem(IDC_BULLSEYE));

     // Cache BullsEye interface
     m_axBullsEye.QueryControl(&m_spBullsEye);
 •
 •
 •
     return 0;
   }
 •
 •
 •
 private:
   CAxWindow           m_axBullsEye;
   CComPtr<IBullsEye>  m_spBullsEye;
 };
In this example, we've cached both the HWND to the AtlAxWin for continued communication with the control host window and one of the control's interfaces for communication with the control itself. If you don't need the HWND, but only an interface, you may want to consider using GetDlgControl instead.
The CDialogImpl class, because it derives from CWindow, provides the GetDlgItem function to retrieve the HWND of a child window given the ID of the child. CWindow also provides a GetDlgControl member function, but it's for retrieving an interface pointer instead of an HWND:
   HRESULT CWindow::GetDlgControl(int nID, REFIID iid, void** ppUnk) {
   HRESULT hr = E_FAIL;
   HWND hWndCtrl = GetDlgItem(nID);
   if (hWndCtrl != NULL) {
     *ppUnk = NULL;
     CComPtr<IUnknown> spUnk;
     hr = AtlAxGetControl(hWndCtrl, &spUnk);
     if (SUCCEEDED(hr)) hr = spUnk->QueryInterface(iid,
                                                ppUnk);
   }
   return hr;
   }
The GetDlgControl member function calls the AtlAxGetControl function, which uses the HWND of the child window to retrieve an IUnknown interface. AtlAxGetControl does this by sending the WM_GETCONTROL window message that windows of the class AtlAxWin understand. In the event that the child window is not an instance of the AtlAxWin window class, or if the control does not support the interface being requested, GetDlgControl will return a failed HRESULT. Using GetDlgControl simplifies the code to cache an interface on a control considerably:
 LRESULT OnInitDialog(...)
   {
   // Cache BullsEye
   // interface
   GetDlgControl(
     IDC_BULLSEYE,
     IID_IBullsEye,
         (void**)&m_spBullsEye);
 .
 .
 .
   return 0;
 }
The combination of the CAxDialogImpl class, the control containment wizards in Visual C++, and the GetDlgControl member function makes managing COM controls in a dialog much like managing Windows controls.

Composite Controls

There's beauty in using a dialog resource for managing the UI of a window. Instead of writing pages of code to create, initialize, and place controls on a rectangle of gray, you can use the resource editor to do it for you. At design time you lay out the size and location of the elements of the UI; the ATL-augmented dialog manager is responsible for the heavy lifting. This is an extremely useful mode of UI development—and it can also be used for composite controls. A composite control is a COM control that uses a dialog resource to lay out its UI elements. These UI elements can be Windows controls or other COM controls.
To the Windows controls, a composite control appears as a parent window. To a COM control, the composite control appears as a control container. To a control container, the composite control appears as a control itself. To the developer of the control, a composite control is all three.
ATL provides support for composite controls via the CComCompositeControl base class:
 template <class T>
 class CComCompositeControl : public CComControl< T,
                         CAxDialogImpl< T > > {...};
Notice that CComCompositeControl derives both from CComControl and CAxDialogImpl, combining the functionality of a control and the drawing of the dialog manager, augmented with the COM control hosting capabilities of AtlAxWin. Both of the wizard-generated composite control types (Composite Control and Lite Composite Control) derive from CComCompositeControl instead of CComControl and provide an IDD symbol mapping to the control's dialog resource:
 class ATL_NO_VTABLE CDartBoard :
   public CComObjectRootEx<CComSingleThreadModel>,
   public IDispatchImpl<IDartBoard, &IID_IDartBoard,
                        &LIBID_CONTROLSLib>,
   public CComCompositeControl<CDartBoard>,
•
•
•
   public CComCoClass<CDartBoard, &CLSID_DartBoard> {
 public:
•
•
•
   enum { IDD = IDD_DARTBOARD };

   CDartBoard() {
     // Composites can't be
     // windowless
     m_bWindowOnly = TRUE;

     // Calculate natural
     // extent based on
     // dialog resource size
     CalcExtent(
       m_sizeExtent);
     }
 .
 .
 .
   };
Notice that the construction of the composite control sets the m_bWindowOnly flag, disabling windowless operation. The control's window needs to be of the same class as that managed by the dialog manager. Also notice that the m_sizeExtent member variable is set by a call to CalcExtent, a helper function provided in CComCompositeControl. CalcExtent is used to set the initial preferred size of the control to be exactly that of the dialog box resource.

Composite Control Drawing

Since a composite control is based on a dialog resource, and its drawing will be managed by the dialog manager and the child controls, no real drawing chores have to be performed. Instead, setting the state of the child controls, which will cause them to redraw, is all that's required to update the visual state of a control.
Using a dialog resource and deriving from CComCompositeControl are the only differences between a control that manages its own UI elements and one that leans on the dialog manager. If you'd like even more powerful layout management, there's a special kind of composite control that hosts only one control, the Internet Explorer HTML Control, which can be generated with the HTML Control and Lite HTML Control object types in the ATL Object Wizard. This control, instead of being based on a dialog resource, is based on an HTML resource.

Summary

ATL provides the ability to host controls in windows, dialogs, or other controls. Control containment under ATL is based on a new window class, AtlAxWin. As a wrapper around this window class, ATL provides the CAxWindow class. Once a control hosting window has been created, it can be treated as a window, using the functionality of the CWindow base class. It can also be used as a COM object, using the interfaces available with the QueryControl member function of the CAxWindow class. The interfaces of the control can be used to sink events, persist the control's state, or program against the control's custom interfaces.

Also see
Part I
Part II

From the April 1999 issue of Microsoft Systems Journal. Get it at your local newsstand, or better yet, subscribe.


For related information see:
ATLCON: Demonstrates Creating a Simple Container at http://premium.microsoft.com/msdn/library /devprods/vs6/vc++/vcsample/_sample_atl_atlcon.htm.
Also check http://www.microsoft.com/msdn for daily updates on developer programs, resources and events.

© 1999 Microsoft Corporation. All rights reserved.
Terms of Use
.

© 2014 Microsoft Corporation. All rights reserved. Contact Us |Terms of Use |Trademarks |Privacy & Cookies
Microsoft