Chapter 12. Control Containment

Containment of COM controls can take many forms. A window can contain any number of COM controls, as can a dialog or another control (called a composite control). All these containers share common characteristics, which is the subject of this chapter.

How Controls Are Contained

To contain a COM control, [1] a container must do two things:

  1. Provide a window to act as the parent for the child COM control. The parent window can be used by a single child COM control or can be shared by many.

  2. Implement a set of COM interfaces that the control uses to communicate with the container. The container must provide at least one object per control, called the site, to implement these interfaces. However, the interfaces can be spread between up to two other container-provided objects, called the document and the frame.

The window that the container provides can be a parent window of the control or, in the case of a windowless control, can be shared by the control. The control uses the window in its interaction with the user. The interfaces that the container implements are used for integration with the control and mirror those that the control implements. Figure 12.1 shows the major interfaces that the container implements and how they are mirrored by those that the control implements.

Figure 12.1. Container and control interfaces

[View full size image]

_images/12atl01.jpg

As mentioned in Chapter 11, “ActiveX Controls,” full coverage of the interaction between controls and containers is beyond the scope of this book. Refer to the sources listed in Chapter 11 for more information. [2] However, this chapter presents those things you need to know to host controls both in standalone applications and inside COM servers. Your hosting options include windows, dialogs, and composite controls. Before diving into the details of dialogs or controls hosting other controls, let’s start with the basics by examining control containment in a simple frame window.

Basic Control Containment

Control Creation

The control-creation process in ATL exposes the core of how ATL hosts controls. Figure 12.2 shows the overall process. What follows is a detailed look at the relevant bits of code involved.

Figure 12.2. A UML sequence diagram of the ATL control-creation process

[View full size image]

_images/12atl02.jpg

ATL’s implementation of the required container interfaces is called CAxHostWindow. [3]

 1// This class is not cocreateable
 2class ATL_NO_VTABLE CAxHostWindow :
 3  public CComCoClass<CAxHostWindow, &CLSID_NULL>,
 4  public CComObjectRootEx<CComSingleThreadModel>,
 5  public CWindowImpl<CAxHostWindow>,
 6  public IAxWinHostWindow,
 7  public IOleClientSite,
 8  public IOleInPlaceSiteWindowless,
 9  public IOleControlSite,
10  public IOleContainer,
11  public IObjectWithSiteImpl<CAxHostWindow>,
12  public IServiceProvider,
13  public IAdviseSink,
14#ifndef _ATL_NO_DOCHOSTUIHANDLER
15  public IDocHostUIHandler,
16#endif
17  public IDispatchImpl<IAxWinAmbientDispatch,
18    &IID_IAxWinAmbientDispatch,
19    &LIBID_ATLLib>
20{...};

Notice that a CAxHostWindow is two things: a window (from CWindowImpl) and a COM implementation (from CComObjectRootEx). When the container wants to host a control, it creates an instance of CAxHostWindow, but not directly. Instead, it creates an instance of a window class defined by ATL, called AtlAxWin80. This window acts as the parent window for the control and eventually is 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 called AtlAxWinInit to register the AtlAxWin80 window class.

 1// This either registers a global class
 2// (if AtlAxWinInit is in ATL.DLL)
 3// or it registers a local class
 4ATLINLINE ATLAPI_(BOOL) AtlAxWinInit() {
 5  CComCritSecLock<CComCriticalSection> lock(
 6    _AtlWinModule.m_csWindowCreate, false);
 7  if (FAILED(lock.Lock())) {
 8    ATLTRACE(atlTraceHosting, 0,
 9      _T("ERROR : Unable to lock critical section "
10        "in AtlAxWinInit\n"));
11    ATLASSERT(0);
12    return FALSE;
13  }
14  WM_ATLGETHOST = RegisterWindowMessage(_T("WM_ATLGETHOST"));
15  WM_ATLGETCONTROL = RegisterWindowMessage(
16    _T("WM_ATLGETCONTROL"));
17  WNDCLASSEX wc;
18  // first check if the class is already registered
19  wc.cbSize = sizeof(WNDCLASSEX);
20  BOOL bRet = ::GetClassInfoEx(
21    _AtlBaseModule.GetModuleInstance(),
22    CAxWindow::GetWndClassName(), &wc);
23
24
25  // register class if not
26  if(!bRet) {
27    wc.cbSize = sizeof(WNDCLASSEX);
28#ifdef _ATL_DLL_IMPL
29    wc.style = CS_GLOBALCLASS | CS_DBLCLKS;
30    bAtlAxWinInitialized = true;
31#else
32    wc.style = CS_DBLCLKS;
33#endif
34    wc.lpfnWndProc = AtlAxWindowProc;
35    wc.cbClsExtra = 0;
36    wc.cbWndExtra = 0;
37    wc.hInstance = _AtlBaseModule.GetModuleInstance();
38    wc.hIcon = NULL;
39    wc.hCursor = ::LoadCursor(NULL, IDC_ARROW);
40    wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
41    wc.lpszMenuName = NULL;
42    wc.lpszClassName =
43      CAxWindow::GetWndClassName(); // "AtlAxWin80"
44    wc.hIconSm = NULL;
45
46
47    ATOM atom= ::RegisterClassEx(&wc);
48    if(atom) {
49     _AtlWinModule.m_rgWindowClassAtoms.Add(atom);
50     bRet=TRUE;
51    } else {
52      bRet=FALSE;
53    }
54  }
55
56
57  if(bRet) {
58    // first check if the class is already registered
59    memset(&wc, 0, sizeof(WNDCLASSEX));
60    wc.cbSize = sizeof(WNDCLASSEX);
61    bRet = ::GetClassInfoEx(_AtlBaseModule.GetModuleInstance(),
62      CAxWindow2::GetWndClassName(), &wc);
63    // register class if not
64    if(!bRet) {
65      wc.cbSize = sizeof(WNDCLASSEX);
66#ifdef _ATL_DLL_IMPL
67      wc.style = CS_GLOBALCLASS | CS_DBLCLKS;
68#else
69      wc.style = CS_DBLCLKS;
70#endif
71      wc.lpfnWndProc = AtlAxWindowProc2;
72      wc.cbClsExtra = 0;
73      wc.cbWndExtra = 0;
74      wc.hInstance = _AtlBaseModule.GetModuleInstance();
75      wc.hIcon = NULL;
76      wc.hCursor = ::LoadCursor(NULL, IDC_ARROW);
77      wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
78      wc.lpszMenuName = NULL;
79      wc.lpszClassName =
80        CAxWindow2::GetWndClassName();//"AtlAxWinLic80"
81      wc.hIconSm = NULL;
82      ATOM atom= RegisterClassEx(&wc);
83
84      if (atom) {
85        _AtlWinModule.m_rgWindowClassAtoms.Add(atom);
86        bRet=TRUE;
87      } else {
88      bRet=FALSE;
89    }
90   }
91  }
92  return bRet;
93}

This function actually registers two window classes: AtlAxWin80 and AtlAxWinLic80. The difference, if you haven’t guessed from the names, is that the latter supports embedding controls that require a runtime license.

You don’t need to manually call AtlAxWinInit: It is called from the ATL code at the various places where these window classes are needed, as you’ll see later. Multiple calls are fine: The function checks to make sure the window classes are registered first before doing anything.

After the AtlAxWin80 class has been registered, creating an instance of one also creates an instance of CAxHostWindow. The CAxHostWindow object uses the title of the window as the name of the control to create and to host. For example, the following code creates a CAxHostWindow and causes it to host a new instance of the BullsEye control developed in Chapter 10, “Windowing”:

 1class CMainWindow : public CWindowImpl<CMainWindow, ...> {
 2...
 3  LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam,
 4    BOOL& lResult) {
 5    // Create the host window, the CAxHostWindow object, and
 6    // the BullsEye control, and host the control
 7    RECT rect; GetClientRect(&rect);
 8    LPCTSTR pszName = __T("ATLInternals.BullsEye");
 9    HWND hwndContainer = m_ax.Create(__T("AtlAxWin80"),
10      m_hWnd, rect, pszName, WS_CHILD | WS_VISIBLE);
11    if( !hwndContainer ) return -1;
12    return 0;
13  }
14
15private:
16  CWindow m_ax;
17};

The creation of the CAxHostWindow object and the corresponding control is initiated in the WM_CREATE handler of the AtlAxWin80 window procedure AtlAxWindowProc:

 1static LRESULT CALLBACK
 2AtlAxWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam,
 3  LPARAM lParam) {
 4  switch(uMsg) {
 5  case WM_CREATE: {
 6    // create control from a PROGID in the title
 7    // This is to make sure drag drop works
 8    ::OleInitialize(NULL);
 9
10    CREATESTRUCT* lpCreate = (CREATESTRUCT*)lParam;
11    int nLen = ::GetWindowTextLength(hWnd);
12    CAutoStackPtr<TCHAR> spName(
13      (TCHAR *)_malloca((nLen + 1) * sizeof(TCHAR)));
14    if(!spName) {
15      return -1;
16    }
17    // Extract window text to be used as name of control to host
18    ::GetWindowText(hWnd, spName, nLen + 1);
19    ::SetWindowText(hWnd, _T(""));
20    IAxWinHostWindow* pAxWindow = NULL;
21    ...
22
23    USES_CONVERSION_EX;
24    CComPtr<IUnknown> spUnk;
25    // Create AxHostWindow instance and host the control
26    HRESULT hRet = AtlAxCreateControlLic(T2COLE_EX_DEF(spName),
27      hWnd, spStream, &spUnk, NULL);
28    if(FAILED(hRet)) return -1; // abort window creation
29
30    hRet = spUnk->QueryInterface(__uuidof(IAxWinHostWindow),
31      (void**)&pAxWindow);
32    if(FAILED(hRet)) return -1; // abort window creation
33    // Keep a CAxHostWindow interface pointer in the window's
34    // user data
35    ::SetWindowLongPtr(hWnd, GWLP_USERDATA,
36      (DWORD_PTR)pAxWindow);
37    // continue with DefWindowProc
38    }
39  break;
40
41  case WM_NCDESTROY: {
42    IAxWinHostWindow* pAxWindow =
43      (IAxWinHostWindow*)::GetWindowLongPtr(hWnd, GWLP_USERDATA);
44    // When window goes away, release the host (and the control)
45    if(pAxWindow != NULL) pAxWindow->Release();
46    OleUninitialize();
47  }
48  break;
49
50  case WM_PARENTNOTIFY: {
51    if((UINT)wParam == WM_CREATE) {
52      ATLASSERT(lParam);
53      // Set the control parent style for the AxWindow
54      DWORD dwExStyle = ::GetWindowLong((HWND)lParam,
55        GWL_EXSTYLE);
56      if(dwExStyle & WS_EX_CONTROLPARENT) {
57        dwExStyle = ::GetWindowLong(hWnd, GWL_EXSTYLE);
58        dwExStyle |= WS_EX_CONTROLPARENT;
59        ::SetWindowLong(hWnd, GWL_EXSTYLE, dwExStyle);
60      }
61    }
62  }
63  break;
64
65  default:
66      break;
67  }
68
69  return ::DefWindowProc(hWnd, uMsg, wParam, lParam);
70}

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 AtlAxCreateControlLic, passing the name of the control, forwards to AtlAxCreateControlLicEx, which furthers things by creating a CAxHostWindow object and asking it to create and host the control:

 1ATLINLINE ATLAPI AtlAxCreateControlLicEx(
 2  LPCOLESTR lpszName,
 3  HWND hWnd,
 4  IStream* pStream,
 5  IUnknown** ppUnkContainer,
 6  IUnknown** ppUnkControl,
 7  REFIID iidSink,
 8  IUnknown* punkSink,
 9  BSTR bstrLic) {
10  AtlAxWinInit();
11  HRESULT hr;
12  CComPtr<IUnknown> spUnkContainer;
13  CComPtr<IUnknown> spUnkControl;
14
15  hr = CAxHostWindow::_CreatorClass::CreateInstance(NULL,
16    __uuidof(IUnknown), (void**)&spUnkContainer);
17  if(SUCCEEDED(hr)) {
18    CComPtr<IAxWinHostWindowLic> pAxWindow;
19    spUnkContainer->QueryInterface(__uuidof(IAxWinHostWindow),
20      (void**)&pAxWindow);
21    CComBSTR bstrName(lpszName);
22    hr = pAxWindow->CreateControlLicEx(bstrName, hWnd, pStream,
23      &spUnkControl, iidSink, punkSink, bstrLic);
24  }
25  if(ppUnkContainer != NULL) {
26    if (SUCCEEDED(hr)) {
27      *ppUnkContainer = spUnkContainer.p;
28      spUnkContainer.p = NULL;
29    }
30    else
31      *ppUnkContainer = NULL;
32  }
33  if (ppUnkControl != NULL) {
34    if (SUCCEEDED(hr)) {
35      *ppUnkControl = SUCCEEDED(hr) ? spUnkControl.p : NULL;
36      spUnkControl.p = NULL;
37    }
38    else
39      *ppUnkControl = NULL;
40  }
41  return hr;
42}

IAxWinHostWindow

AtlAxCreateControlEx uses the IAxWinHostWindow interface to create the control. IAxWinHostWindow is one of the few interfaces that ATL defines and is one of the interfaces that CAxHostWindow implements. Its job is to allow for management of the control that it’s hosting:

 1interface IAxWinHostWindow : IUnknown {
 2  HRESULT CreateControl([in] LPCOLESTR lpTricsData,
 3    [in] HWND hWnd, [in] IStream* pStream);
 4  HRESULT CreateControlEx([in] LPCOLESTR lpTricsData,
 5    [in] HWND hWnd, [in] IStream* pStream,
 6    [out] IUnknown** ppUnk,
 7    [in] REFIID riidAdvise, [in] IUnknown* punkAdvise);
 8  HRESULT AttachControl([in] IUnknown* pUnkControl,
 9    [in] HWND hWnd);
10  HRESULT QueryControl([in] REFIID riid,
11    [out, iid_is(riid)] void **ppvObject);
12  HRESULT SetExternalDispatch([in] IDispatch* pDisp);
13  HRESULT SetExternalUIHandler(
14    [in] IDocHostUIHandlerDispatch* pDisp);
15};

A second interface, IAxWinHostWindowLic, derives from IAxWinHostWindow and supports creating licensed controls:

1interface IAxWinHostWindowLic : IAxWinHostWindow {
2  HRESULT CreateControlLic([in] LPCOLESTR lpTricsData,
3    [in] HWND hWnd, [in] IStream* pStream, [in] BSTR bstrLic);
4  HRESULT CreateControlLicEx([in] LPCOLESTR lpTricsData,
5    [in] HWND hWnd, [in] IStream* pStream,
6    [out]IUnknown** ppUnk, [in] REFIID riidAdvise,
7    [in]IUnknown* punkAdvise, [in] BSTR bstrLic);
8};

To create a new control in CAxHostWindow, IAxWinHostWindow provides CreateControl or CreateControlEx, which AtlAxCreateControlEx then uses after the CAxHostWindow object is created. The parameters for CreateControl[Ex] are as follows:

  • lpTricsData. The name of the control to create. It can take the form of a CLSID, a ProgID, a URL, a filename, or raw HTML. We discuss this more later.

  • hWnd. Parent window in which to host the control. This window is subclassed by CAxHostWindow.

  • pStream. Stream that holds object-initialization data. The control is initialized via IPersistStreamInit. If pStream is non-NULL, Load is called. Otherwise, InitNew is called.

  • ppUnk. Filled with an interface to the newly created control.

  • riidAdvise. If not IID_NULL, CAxHostWindow attempts to set up a connection-point connection between the control and the sink object represented by the punkAdvise parameter. CAxHostWindow manages the resultant cookie and tears down the connection when the control is destroyed.

  • punkAdvise. An interface to the sink object that implements the sink interface specified by riidAdvise. The CreateControlLicEx method adds one more parameter:

  • bstrLic. This string contains the licensing key. If the string is empty, the control is created using the normal CoCreateInstance call. If there is a licensing key in the string, the control is created via the IClassFactory2 interface, which supports creating licensed controls.

The AttachControl method of the IAxWinHostWindow method 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 being hosted by the CAxHostWindow object. Both SetExternalDispatch and SetExternalUIHandler are used when hosting the Internet Explorer HTML control and will be discussed at the end of the chapter in the HTML Controls section.

CreateControlLicEx

CAxHostWindow’s implementation of CreateControlLicEx subclasses [4] 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 to be navigated to via a URL, CreateControlLicEx does that, too:

 1STDMETHODIMP CAxHostWindow::CreateControlLicEx(
 2  LPCOLESTR lpszTricsData,
 3  HWND hWnd,
 4  IStream* pStream,
 5  IUnknown** ppUnk,
 6  REFIID iidAdvise,
 7  IUnknown* punkSink,
 8  BSTR bstrLic)
 9{
10  HRESULT hr = S_FALSE;
11  // Used to keep track of whether we subclass the window
12
13  bool bReleaseWindowOnFailure = false;
14// Release previously held control
15  ReleaseAll();
16  ...
17  if (::IsWindow(hWnd)) {
18    if (m_hWnd != hWnd) { // Don't need to subclass the window
19                          // if we already own it
20      // Route all messages to CAxHostWindow
21      SubclassWindow(hWnd);
22      bReleaseWindowOnFailure = true;
23    }
24    ...
25    bool bWasHTML = false;
26    // Create control based on lpszTricsData
27    hr = CreateNormalizedObject(lpszTricsData,
28       __uuidof(IUnknown),
29       (void**)ppUnk, bWasHTML, bstrLic);
30
31    // Activate the control
32    if (SUCCEEDED(hr)) hr = ActivateAx(*ppUnk, false, pStream);
33
34    // Try to hook up any sink the user might have given us.
35    m_iidSink = iidAdvise;
36    if(SUCCEEDED(hr) && *ppUnk && punkSink) {
37      AtlAdvise(*ppUnk, punkSink, m_iidSink, &m_dwAdviseSink);
38    }
39    ...
40
41    // If raw HTML, give HTML control its HTML
42    ...
43
44    // If it's an URL, navigate the Browser control to the URL
45    ...
46
47    if (FAILED(hr) || m_spUnknown == NULL) {
48      // We don't have a control or something failed so release
49      ReleaseAll();
50      ...
51    }
52  }
53  return hr;
54}

How the control name is interpreted depends on another function, called CreateNormalizedObject. The actual COM activation process is handled by the ActivateAx function (discussed shortly).

CreateNormalizedObject

The CreateNormalizedObject function creates an instance of a COM object using strings of the form shown in Table 12.1.

Table 12.1. String Formats Understood by ``CreateNormalizedObject``

Type

Example

CLSID of Created Object

HTML

mshtml:<body><b>Wow!</b></body>

CLSID_HTMLDocument

CLSID

{7DC59CC5-36C0-11D2-AC05-00A0C9C8E50D}

Result of CLSIDFromString

ProgID

ATLInternals.BullsEye

Result of CLSIDFromProgID

URL

CLSID_WebBrowser

Active document

  • D:\Atl Internals\10 Controls.doc

  • file://D:\Atl Internals\10 Controls.doc

CLSID_WebBrowser

Because 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.

If no license key is supplied, CreateNormalizedObject uses CoCreateInstance to instantiate the object. If there is a license key, CreateNormalizedObject instead calls CoGetClassFactory and uses the factory’s IClassFactory2::CreateInstanceLic method to create the control using the given license key.

ActivateAx

The ActivateAx function is the part of the control-creation process that really performs the magic. Called after CreateControlLicEx [5] actually creates the underlying COM object, ActivateAx 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, calling DoVerb(OLEIVERB_INPLACEACTIVATE) via the control’s implementation of, again, IOleObject.

This process completes the activation of the control. However, creating an instance of CAxHostWindow via direct reference to the AtlAxWin80 window class is not typical. The implementation details of AtlAxWin80 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. Two such wrappers exist in ATL: CAxWindow and CAxWindow2.

CAxWindow

CAxWindow simplifies the use of CAxHostWindow with a set of wrapper functions. The initial creation part of CAxWindow class is defined as follows:

 1#define ATLAXWIN_CLASS "AtlAxWin80"
 2
 3template <class TBase /* = CWindow */> class CAxWindowT :
 4  public TBase {
 5public:
 6// Constructors
 7  CAxWindowT(HWND hWnd = NULL) : TBase(hWnd) { AtlAxWinInit(); }
 8
 9  CAxWindowT< TBase >& operator=(HWND hWnd) {
10    m_hWnd = hWnd; return *this;
11  }
12// Attributes
13  static LPCTSTR GetWndClassName() { return _T(ATLAXWIN_CLASS); }
14
15// Operations
16  HWND Create(HWND hWndParent, _U_RECT rect = NULL, ...) {
17    return CWindow::Create(GetWndClassName(), hWndParent,
18      rect, ... );
19  }
20...
21};
22
23typedef CAxWindowT<CWindow> CAxWindow;

Notice that the Create function still requires the parent window and the name of the control but does not require passing the name of the CAxHostWindow window class. Instead, CAxWindow knows the name of the appropriate class itself (available via its static member function GetWndClassName) and passes it to the CWindow base class just like we had done manually. Using CAxWindow reduces the code required to host a control to the following:

 1class CMainWindow : public CWindowImpl<CMainWindow, ...> {
 2...
 3  LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam,
 4    BOOL& lResult) {
 5    // Create the host window, the CAxHostWindow object, and
 6    // the BullsEye control, and host the control
 7    RECT rect; GetClientRect(&rect);
 8    LPCTSTR pszName = __T("ATLInternals.BullsEye");
 9    HWND hwndContainer = m_ax.Create(m_hWnd, rect,
10        pszName, WS_CHILD | WS_VISIBLE);
11    if( !hwndContainer ) return -1;
12    return 0;
13  }
14
15private:
16  CAxWindow m_ax;
17};

The combination of a custom window class and a CWindow-based wrapper provides exactly the same model as the window control wrappers that I discussed in Chapter 10, “Windowing.” For example, EDIT is a window class, and the CEdit class provides the client-side wrapper. The implementation of the EDIT window class happens to use the window text passed via CreateWindow as the text to edit. The AtlAxWin80 class, on the other hand, uses the window text as the name of the control to create. The job of both wrapper classes is to provide a set of member functions that replace calls to SendMessage. CEdit provides member functions such as CanUndo and GetLineCount, which send the EM_CANUNDO and EM_GETLINECOUNT messages. CAxWindow, on the other hand, provides member functions that also send window messages to the AtlAxWin80 class (which I discuss later). The only real difference between EDIT and AtlAxWin80 is that EDIT is provided with the operating system, whereas AtlAxWin80 is provided only with ATL. [6]

Two-Step Control Creation

You might have noticed that AtlAxCreateControlEx takes some interesting parameters, such as an IStream interface pointer and an interface ID/interface pointer pair, to specify an initial connection point. However, although the window name can be used to pass the name of the control, there are no extra parameters to CreateWindow for a couple interface pointers and a globally unique identifier (GUID). Instead, CAxWindow provides a few extra wrapper functions: CreateControl and CreateControlEx:

 1template <class TBase> class CAxWindowT : public TBase {
 2public:
 3
 4  ...
 5  HRESULT CreateControl(LPCOLESTR lpszName,
 6    IStream* pStream = NULL,
 7    IUnknown** ppUnkContainer = NULL) {
 8    return CreateControlEx(lpszName, pStream, ppUnkContainer);
 9  }
10
11  HRESULT CreateControl(DWORD dwResID, IStream* pStream = NULL,
12    IUnknown** ppUnkContainer = NULL) {
13    return CreateControlEx(dwResID, pStream, ppUnkContainer);
14  }
15
16  HRESULT CreateControlEx(LPCOLESTR lpszName,
17    IStream* pStream = NULL,
18    IUnknown** ppUnkContainer = NULL,
19    IUnknown** ppUnkControl = NULL,
20    REFIID iidSink = IID_NULL, IUnknown* punkSink = NULL) {
21    ATLASSERT(::IsWindow(m_hWnd));
22    // We must have a valid window!
23
24    // Get a pointer to the container object
25    // connected to this window
26    CComPtr<IAxWinHostWindow> spWinHost;
27    HRESULT hr = QueryHost(&spWinHost);
28
29    // If QueryHost failed, there is no host attached to this
30    // window. We assume that the user wants to create a new
31    // host and subclass the current window
32    if (FAILED(hr)) {
33      return AtlAxCreateControlEx(lpszName, m_hWnd, pStream,
34        ppUnkContainer, ppUnkControl, iidSink, punkSink);
35    }
36
37    // Create the control requested by the caller
38    CComPtr<IUnknown> pControl;
39    if (SUCCEEDED(hr)) {
40      hr = spWinHost->CreateControlEx(lpszName, m_hWnd, pStream,
41        &pControl, iidSink, punkSink);
42    }
43
44    // Send back the necessary interface pointers
45    if (SUCCEEDED(hr)) {
46      if (ppUnkControl) { *ppUnkControl = pControl.Detach(); }
47
48      if (ppUnkContainer) {
49        hr = spWinHost.QueryInterface(ppUnkContainer);
50        ATLASSERT(SUCCEEDED(hr)); // This should not fail!
51      }
52    }
53    return hr;
54  }
55
56  HRESULT CreateControlEx(DWORD dwResID, IStream* pStream = NULL,
57    IUnknown** ppUnkContainer = NULL,
58    IUnknown** ppUnkControl = NULL,
59    REFIID iidSink = IID_NULL, IUnknown* punkSink = NULL) {
60    TCHAR szModule[MAX_PATH];
61    DWORD dwFLen =
62      GetModuleFileName(_AtlBaseModule.GetModuleInstance(),
63        szModule, MAX_PATH);
64    if( dwFLen == 0 ) return AtlHresultFromLastError();
65    else if( dwFLen == MAX_PATH )
66      return HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER);
67
68    CComBSTR bstrURL(OLESTR("res://"));
69    HRESULT hr=bstrURL.Append(szModule);
70    if(FAILED(hr)) { return hr; }
71    hr=bstrURL.Append(OLESTR("/"));
72    if(FAILED(hr)) { return hr; }
73    TCHAR szResID[11];
74#if _SECURE_ATL && !defined(_ATL_MIN_CRT)
75    if (_stprintf_s(szResID, _countof(szResID),
76      _T("%0d"), dwResID) == -1) {
77      return HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER);
78    }
79#else
80    wsprintf(szResID, _T("%0d"), dwResID);
81#endif
82    hr=bstrURL.Append(szResID);
83    if(FAILED(hr)) { return hr; }
84
85    ATLASSERT(::IsWindow(m_hWnd));
86    return CreateControlEx(bstrURL, pStream, ppUnkContainer,
87      ppUnkControl, iidSink, punkSink);
88  }
89  ...
90};

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 the dwResID parameter, which serves as an ID of an HTML page embedded in the resources of the module. This parameter is formatted into a string of the format res://<module path>/<dwResID> before being passed to AtlAxCreateControlEx.

These functions are meant to be used in a two-stage construction of first the host and then its control. For example:

 1LRESULT
 2CMainWindow::OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam,
 3  BOOL& lResult) {
 4  RECT rect; GetClientRect(&rect);
 5  // Phase one: Create the container (passing a null (0)
 6  // window title)
 7  // m_ax is declared in the CMainWindow class as:
 8  // CAxWindow m_ax;
 9  HWND hwndContainer = m_ax.Create(m_hWnd, rect, 0,
10    WS_CHILD | WS_VISIBLE);
11  if( !hwndContainer ) return -1;
12
13  // Phase two: Create the control
14  LPCOLESTR pszName = OLESTR("ATLInternals.BullsEye");
15  HRESULT hr = m_ax.CreateControl(pszName);
16  return (SUCCEEDED(hr) ? 0 : -1);
17};

I show you how to persist a control and how to handle events from a control later in this chapter. 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:

 1template <class TBase = CWindow> class CAxWindowT :
 2  public TBase {
 3public:
 4...
 5  HRESULT AttachControl(IUnknown* pControl,
 6    IUnknown** ppUnkContainer) {
 7    ATLASSERT(::IsWindow(m_hWnd));
 8    // We must have a valid window!
 9
10    // Get a pointer to the container object connected
11    // to this window
12    CComPtr<IAxWinHostWindow> spWinHost;
13    HRESULT hr = QueryHost(&spWinHost);
14
15    // If QueryHost failed, there is no host attached
16    // to this window. We assume that the user wants to
17    // create a new host and subclass the current window
18    if (FAILED(hr))
19      return AtlAxAttachControl(pControl, m_hWnd,
20        ppUnkContainer);
21
22    // Attach the control specified by the caller
23    if (SUCCEEDED(hr))
24      hr = spWinHost->AttachControl(pControl, m_hWnd);
25
26    // Get the IUnknown interface of the container
27    if (SUCCEEDED(hr) && ppUnkContainer) {
28      hr = spWinHost.QueryInterface(ppUnkContainer);
29      ATLASSERT(SUCCEEDED(hr)); // This should not fail!
30    }
31
32    return hr;
33  }
34...
35};

AttachControl is used like this:

 1LRESULT
 2CMainWindow::OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam,
 3  BOOL& lResult) {
 4  RECT rect; GetClientRect(&rect);
 5  // Phase one: Create the container
 6  HWND hwndContainer = m_ax.Create(m_hWnd, rect, 0,
 7    WS_CHILD | WS_VISIBLE);
 8  if( !hwndContainer ) return -1;
 9
10  // Create and initialize a control
11  CComPtr<IUnknown> spunkControl; // ...
12
13  // Phase two: Attach an existing control
14  HRESULT hr = m_ax.AttachControl(spunkControl);
15  return (SUCCEEDED(hr) ? 0 : -1);
16};

CAxWindow2 and the AtlAxWinLic80 Window Class

Earlier, I mentioned that ATL actually registers two window classes: AtlAxWin80 and AtlAxWinLic80. Why two window classes? AtlAxWinLic80 supports one feature that AtlAxWin80 does not: the creation of licensed controls.

This might sound odd at first. After all, the actual control-creation step is done by the CreateControlLicEx function, which handles the creation of controls with or without license keys. However, the AtlAxWin80 class never actually passes a license key, so CreateControlLicEx never uses the IClassFactory2 when AtlAxWin80 is the calling class.

The AtlAxWinLic80 window class, on the other hand, has a slightly different window proc:

 1static LRESULT CALLBACK AtlAxWindowProc2(HWND hWnd, UINT uMsg,
 2  WPARAM wParam, LPARAM lParam) {
 3  switch(uMsg) {
 4  case WM_CREATE: {
 5    // ... same as AtlAxWindowProc ...
 6
 7    // Format of data in lpCreateParams
 8    // int nCreateSize;   // size of Create data in bytes
 9    // WORD nMsg;         // constant used to indicate type
10                          // of DLGINIT data.
11                          // See _DialogSplitHelper for values.
12    // DWORD dwLen;       // Length of data stored for control
13                          // in DLGINIT format in bytes.
14    // DWORD cchLicKey;   // Length of license key in OLECHAR's
15    // OLECHAR *szLicKey; // This will be present only if
16                          // cchLicKey is greater than 0.
17                          // This is of variable length and will
18                          // contain cchLicKey OLECHAR's
19                          // that represent the license key.
20    // The following two fields will be present only if nMsg is
21    // WM_OCC_LOADFROMSTREAM_EX or WM_OCC_LOADFROMSTORAGE_EX.
22    // If present this information will be ignored since
23    // databinding is not supported.
24    // ULONG cbDataBinding;    // Length of databinding
25                               // information in bytes.
26    // BYTE *pbDataBindingInfo // cbDataBinding bytes that contain
27                               // databinding information
28    // BYTE *pbControlData;    // Actual control data persisted
29                               // by the control.
30
31    // ... Load persistence data into stream ...
32
33
34    CComBSTR bstrLicKey;
35    HRESULT hRet = _DialogSplitHelper::ParseInitData(spStream,
36      &bstrLicKey.m_str);
37    if (FAILED(hRet)) return -1;
38
39    USES_CONVERSION_EX;
40    CComPtr<IUnknown> spUnk;
41    hRet = AtlAxCreateControlLic(T2COLE_EX_DEF(spName), hWnd,
42      spStream, &spUnk, bstrLicKey);
43    if(FAILED(hRet)) {
44      return 1; // abort window creation
45    }
46    hRet = spUnk->QueryInterface(__uuidof(IAxWinHostWindowLic),
47      (void**)&pAxWindow);
48    if(FAILED(hRet)) return -1; // abort window creation
49    ::SetWindowLongPtr(hWnd, GWLP_USERDATA,
50      (DWORD_PTR)pAxWindow);
51    // continue with DefWindowProc
52  }
53  break;
54
55  // ... Rest is the same as AtlAxWindowProc ...
56}

The difference is in the WM_CREATE handler. The CREATESTRUCT passed in can contain the license key information; if it does, AtlAxWinLic80 extracts the license key and passes it on to be used in the creation of the control.

Just as the CAxWindow class wraps the use of AtlAxWin80, the CAxWindow2 class wraps the use of AtlAxWinLic80:

 1template <class TBase /* = CWindow */>
 2class CAxWindow2T : public CAxWindowT<TBase> {
 3public:
 4// Constructors
 5  CAxWindow2T(HWND hWnd = NULL) : CAxWindowT<TBase>(hWnd) { }
 6
 7  CAxWindow2T< TBase >& operator=(HWND hWnd);
 8
 9// Attributes
10  static LPCTSTR GetWndClassName() {
11    return _T(ATLAXWINLIC_CLASS);
12  }
13
14// Operations
15  HWND Create(HWND hWndParent, _U_RECT rect = NULL,
16    LPCTSTR szWindowName = NULL,
17    DWORD dwStyle = 0, DWORD dwExStyle = 0,
18    _U_MENUorID MenuOrID = 0U, LPVOID lpCreateParam = NULL) {
19    return CWindow::Create(GetWndClassName(), hWndParent, rect,
20      szWindowName, dwStyle, dwExStyle, MenuOrID, lpCreateParam);
21  }
22
23  HRESULT CreateControlLic(LPCOLESTR lpszName,
24    IStream* pStream = NULL,
25    IUnknown** ppUnkContainer = NULL, BSTR bstrLicKey = NULL) {
26    return CreateControlLicEx(lpszName, pStream, ppUnkContainer,
27      NULL, IID_NULL, NULL, bstrLicKey);
28  }
29
30  HRESULT CreateControlLic(DWORD dwResID,
31    IStream* pStream = NULL,
32    IUnknown** ppUnkContainer = NULL, BSTR bstrLicKey = NULL) {
33    return CreateControlLicEx(dwResID, pStream, ppUnkContainer,
34      NULL, IID_NULL, NULL, bstrLicKey);
35  }
36
37  HRESULT CreateControlLicEx(LPCOLESTR lpszName,
38    IStream* pStream = NULL,
39    IUnknown** ppUnkContainer = NULL,
40    IUnknown** ppUnkControl = NULL,
41    REFIID iidSink = IID_NULL, IUnknown* punkSink = NULL,
42    BSTR bstrLicKey = NULL);
43
44  HRESULT CreateControlLicEx(DWORD dwResID,
45    IStream* pStream = NULL,
46    IUnknown** ppUnkContainer = NULL,
47    IUnknown** ppUnkControl = NULL,
48    REFIID iidSink = IID_NULL, IUnknown* punkSink = NULL,
49    BSTR bstrLickey = NULL);
50};
51
52typedef CAxWindow2T<CWindow> CAxWindow2;

The first thing to note is that CAxWindow2 derives from CAxWindow, so you can use CAxWindow to create nonlicensed controls just as easily. CAxWindow2 adds the various CreateControlLic[Ex] overloads to enable you to pass the licensing information when you create the control.

For the rest of this chapter, I talk about CAxWindow and AtlAxWin80. Everything in the discussion also applies to CAxWindow2 and AtlAxWinLic80.

Using the Control

After you’ve created the control, it’s really two things: a window and a control. The window is an instance of AtlAxWin80 and hosts the control, which might or might not have its own window. (CAxHostWindow provides full support for windowless controls.) Because CAxWindow derives from CWindow, you can treat it like a window (that is, you can move it, resize it, and hide it); AtlAxWin80 handles those messages by translating them into the appropriate COM calls on the control. For example, if you want the entire client area of a frame window to contain a control, you can handle the WM_SIZE message like this:

 1class CMainWindow : public CWindowImpl<CMainWindow, ...> {
 2...
 3  LRESULT OnSize(UINT, WPARAM, LPARAM lParam, BOOL&) {
 4    if( m_ax ) { // m_ax will be Create'd earlier, e.g. WM_CREATE
 5      RECT rect = { 0, 0, LOWORD(lParam), HIWORD(lParam) };
 6      m_ax.MoveWindow(&rect); // Resize the control
 7    }
 8    return 0;
 9  }
10private:
11  CAxWindow m_ax;
12};

In addition to handling Windows messages, COM controls are COM objects. They expect to be programmed via their COM interfaces. To obtain an interface on the control, CAxWindow provides the QueryControl method:

 1template <class TBase = CWindow> class CAxWindowT :
 2  public TBase {
 3public:
 4  ...
 5  HRESULT QueryControl(REFIID iid, void** ppUnk) {
 6    CComPtr<IUnknown> spUnk;
 7    HRESULT hr = AtlAxGetControl(m_hWnd, &spUnk);
 8    if (SUCCEEDED(hr)) hr = spUnk->QueryInterface(iid, ppUnk);
 9    return hr;
10  }
11
12  template <class Q> HRESULT QueryControl(Q** ppUnk)
13  { return QueryControl(__uuidof(Q), (void**)ppUnk); }
14};

Like QueryHost, QueryControl uses a global function (AtlAxGetControl, in this case) that sends a window message to the AtlAxWin80 window to retrieve an interface, but this time from the hosted control itself. When the control has been created, QueryControl can be used to get at the interfaces of the control:

 1// Import interface definitions for BullsEye
 2#import "D:\ATLBook\src\atlinternals\Debug\BullsEyeCtl.dll" \
 3  raw_interfaces_only raw_native_types no_namespace named_guids
 4
 5LRESULT
 6CMainWindow::OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam,
 7  BOOL& lResult) {
 8  // Create the control
 9  ...
10
11  // Set initial BullsEye properties
12  CComPtr<IBullsEye> spBullsEye;
13  HRESULT hr = m_ax.QueryControl(&spBullsEye);
14  if( SUCCEEDED(hr) ) {
15    spBullsEye->put_Beep(VARIANT_TRUE);
16    spBullsEye->put_CenterColor(RGB(0, 0, 255));
17  }
18
19  return 0;
20};

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 have only 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 – for example, 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++ creates a language mapping that uses the compiler-provided wrapper classes (such as _bstr_t, _variant_t, and _com_ptr_t), which are different from the ATL-provided types (such as CComBSTR, CComVariant, and CComPtr). Although the compiler-provided classes have their place, I find that it is best not to mix them with the ATL-provided types. Apparently, the ATL team agrees with me because the ATL Wizardgenerated #import statements also use these attributes (we talk more about the control containmentrelated wizards later).

Sinking Control Events

Not only are you likely to want to program against the interfaces that the control implements, but you’re also likely 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. [7] For example, the BullsEye control from the last chapter defined the following event interface:

 1const int DISPID_ONRINGHIT      = 1;
 2const int DISPID_ONSCORECHANGED = 2;
 3
 4dispinterface _IBullsEyeEvents {
 5properties:
 6methods:
 7  [id(DISPID_ONRINGHIT)]
 8  void OnRingHit(short ringNumber);
 9  [id(DISPID_ONSCORECHANGED)]
10  void OnScoreChanged(long ringValue);
11};

An implementation of IDispatch is required for a control container to handle events fired on a dispinterface. Implementations of IDispatch are easy if the interface is defined as a dual interface, but they are much harder if it is defined as a raw dispinterface. [8] However, as you recall from Chapter 9, “Connection Points,” ATL provides a helper class called IDispEventImpl for implementing an event dispinterface:

1template <UINT nID, class T, const IID* pdiid = &IID_NULL,
2  const GUID* plibid = &GUID_NULL,
3  WORD wMajor = 0, WORD wMinor = 0,
4  class tihclass = CComTypeInfoHolder>
5class ATL_NO_VTABLE IDispEventImpl :
6  public IDispEventSimpleImpl<nID, T, pdiid> {...};

IDispEventImpl uses a data structure called a sink map, established via the following macros:

1#define BEGIN_SINK_MAP(_class) ...
2#define SINK_ENTRY_INFO(id, iid, dispid, fn, info) ...
3#define SINK_ENTRY_EX(id, iid, dispid, fn) ...
4#define SINK_ENTRY(id, dispid, fn) ...
5#define END_SINK_MAP() ...

Chapter 9 explains the gory details of these macros, but the gist is that the sink map provides 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. However, if the object is a COM control, use of IDispEventImpl and the sink map is quite simple, as you’re about to see.

To handle events, the container of the controls derives from one instance of IDispEventImpl per control. Notice that the first template parameter of IDisp-EventImpl is an ID. This ID matches the contained control via the child window ID – that is, the nID parameter to Create. This same ID is used in the sink map to route events from a specific control to the appropriate event handler. The child window ID makes IDispEventImpl so simple in the control case. Nonvisual objects have no child window ID, and the mapping is somewhat more difficult (although, as Chapter 9 described, still entirely possible).

So, handling the events of the BullsEye control merely requires an IDisp-EventImpl base class and an appropriately constructed sink map:

 1const UINT ID_BULLSEYE = 1;
 2
 3class CMainWindow :
 4  public CWindowImpl<CMainWindow, CWindow, CMainWindowTraits>,
 5  public IDispEventImpl<ID_BULLSEYE,
 6                        CMainWindow,
 7                        &DIID__IBullsEyeEvents,
 8                        &LIBID_BullsEyeLib, 1, 0>
 9{
10public:
11...
12  LRESULT OnCreate(...) {
13    RECT rect; GetClientRect(&rect);
14    m_ax.Create(m_hWnd, rect, __T("AtlInternals.BullsEye"),
15                WS_CHILD | WS_VISIBLE, 0, ID_BULLSEYE);
16    ...
17    return (m_ax.m_hWnd ? 0 : -1);
18  }
19
20BEGIN_SINK_MAP(CMainWindow)
21  SINK_ENTRY_EX(ID_BULLSEYE, DIID__IBullsEyeEvents,
22    1, OnRingHit)
23  SINK_ENTRY_EX(ID_BULLSEYE, DIID__IBullsEyeEvents,
24    2, OnScoreChanged)
25END_SINK_MAP()
26
27  void __stdcall OnRingHit(short nRingNumber);
28  void __stdcall OnScoreChanged(LONG ringValue);
29
30private:
31  CAxWindow m_ax;
32};

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 that IDispEventImpl must unpack the array of VARIANT``s passed to ``Invoke, push them on the stack, and call our event handler. It does this using type information at runtime, but, as mentioned in Chapter 9, it still has to know about the calling convention – that is, in what order the parameters should 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.

When we have IDispEventImpl and the sink map set up, we’re not done. Unlike Windows controls, COM controls have no real sense of their “parent.” This means that instead of implicitly knowing to whom to send events, as 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 has to call QueryInterface for IConnectionPointContainer, call FindConnectionPoint to obtain the IConnectionPoint interface, and finally call Advise to establish the container as the sink for events fired by the control. For one control, that’s not so much work, and ATL even provides a function called AtlAdvise to help. However, for multiple controls, it can become a chore to manage the communication with each of them. And because we’ve got a list of all the controls with which we want to establish communications in the sink map, it makes sense to leverage that knowledge to automate the chore. Luckily, we don’t even have to do this much because ATL has already done it for us with AtlAdviseSinkMap:

1template <class T>
2inline 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 whether 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, both 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:

 1LRESULT CMainWindow::OnCreate(...) {
 2  ... // Create the controls
 3  AtlAdviseSinkMap(this, true); // Establish connection points
 4  return 0;
 5}
 6
 7LRESULT CMainWindow::OnDestroy(...) {
 8  // Controls still live
 9  AtlAdviseSinkMap(this, false); // Tear down connection points
10  return 0;
11}

The combination of IDispEventImpl, the sink map, and the AtlAdviseSinkMap function is all that is needed to sink events from a COM control. However, we can further simplify things. 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 [9] and can be published in the coclass statement in the IDL (and, therefore, as part of the type library). For example:

1coclass BullsEye {
2  [default] interface IBullsEye;
3  [default, source] dispinterface _IBullsEyeEvents;
4};

If IDispEventImpl is used with IID_NULL as the template parameter (which is the default value) describing the sink interface, ATL does its best to establish communications with the default source interface via a function called AtlGetObjectSourceInterface. This function attempts to obtain the object’s default source interface, using the type information obtained via the GetTypeInfo member function of IDispatch. It first attempts the use of IProvideClassInfo2; if that’s not available, it digs 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. For example, the following is the complete code necessary to sink events from the BullsEye control:

 1#import "D:\ATLBook\src\atlinternals\Debug\BullsEyeCtl.dll" \
 2  raw_interfaces_only raw_native_types no_namespace named_guids
 3
 4#define ID_BULLSEYE 1
 5
 6class CMainWindow :
 7  public CWindowImpl<CMainWindow, CWindow, CMainWindowTraits>,
 8  // Sink the default source interface
 9  public IDispEventImpl< ID_BULLSEYE, CMainWindow> {
10...
11  LRESULT OnCreate(...) {
12    RECT rect; GetClientRect(&rect);
13    m_ax.Create(m_hWnd, rect, __T("AtlInternals.BullsEye"),
14                WS_CHILD | WS_VISIBLE, 0, ID_BULLSEYE);
15    AtlAdviseSinkMap(this, true);
16    return (m_ax.m_hWnd ? 0 : -1);
17  }
18
19  LRESULT CMainWindow::OnDestroy(...)
20  { AtlAdviseSinkMap(this, false); return 0; }
21
22BEGIN_SINK_MAP(CMainWindow)
23  // Sink events from the default BullsEye event interface
24  SINK_ENTRY(ID_BULLSEYE, 1, OnRingHit)
25  SINK_ENTRY(ID_BULLSEYE, 2, OnScoreChanged)
26END_SINK_MAP()
27
28  void __stdcall OnRingHit(short nRingNumber);
29  void __stdcall OnScoreChanged(LONG ringValue);
30
31private:
32  CAxWindow m_ax;
33};

Property Changes

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

1interface IPropertyNotifySink : IUnknown {
2  HRESULT OnChanged([in] DISPID dispID);
3  HRESULT OnRequestEdit([in] DISPID dispID);
4}

A control uses the IPropertyNotifySink interface to ask the container if it’s okay to change a property (OnRequestEdit) and to notify the container that a property has been changed (OnChanged). OnRequestEdit is used for data binding, which is beyond the scope of this book, but 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 is required. Normal C++ inheritance and AtlAdvise will do. For example:

 1class CMainWindow :
 2  public CWindowImpl<CMainWindow, ...>,
 3  public IPropertyNotifySink {
 4public:
 5...
 6  // IUnknown, assuming an instance on the stack
 7  STDMETHODIMP QueryInterface(REFIID riid, void** ppv) {
 8    if( riid == IID_IUnknown || riid == IID_IPropertyNotifySink )
 9      *ppv = static_cast<IPropertyNotifySink*>(this);
10    else return *ppv = 0, E_NOINTERFACE;
11    return reinterpret_cast<IUnknown*>(*ppv)->AddRef(), S_OK;
12  }
13
14  STDMETHODIMP_(ULONG) AddRef() { return 2; }
15  STDMETHODIMP_(ULONG) Release() { return 1; }
16
17  // IPropertyNotifySink
18  STDMETHODIMP OnRequestEdit(DISPID dispID) { return S_OK; }
19  STDMETHODIMP OnChanged(DISPID dispID) {
20    m_bDirty = true; return S_OK;
21  }
22
23private:
24  CAxControl m_ax;
25  bool       m_bDirty;
26};

You have two choices when setting up and tearing down the IPropertyNotifySink connection point with the control. You can use AtlAdvise after the control is successfully created and AtlUnadvise just before it is destroyed. This requires managing the connection point cookie yourself. For example:

 1LRESULT CMainWindow::OnCreate(...) {
 2  ... // Create the control
 3  // Set up IPropertyNotifySink connection point
 4  CComPtr<IUnknown> spunkControl;
 5  m_ax.QueryControl(spunkControl);
 6  AtlAdvise(spunkControl, this, IID_IPropertyNotifySink,
 7    &m_dwCookie);
 8  return 0;
 9}
10
11LRESULT CMainWindow::OnDestroy(...) {
12  // Tear down IPropertyNotifySink connection point
13  CComPtr<IUnknown> spunkControl;
14  m_ax.QueryControl(spunkControl);
15  AtlUnadvise(spunkControl, IID_IPropertyNotifySink,
16    &m_dwCookie);
17  return 0;
18}

The second choice 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. This simplifies the code considerably:

1LRESULT CMainWindow::OnCreate(...) {
2  ... // Create the control host
3  // Create the control and set up IPropertyNotifySink
4  // connection point
5  m_ax.CreateControlEx(OLESTR("AtlInternals.BullsEye"), 0, 0, 0,
6                     IID_IPropertyNotifySink, this);
7  return 0;
8}

The connection-point cookie for IPropertyNotifySink is managed by the CAxHostWindow object; when the control is destroyed, the connection is torn down automatically. Although this trick works for only one connection-point interface, this technique, combined with the sink map, is likely all you’ll ever need when handling events from controls.

Ambient Properties

In addition to programming the properties of the control, you might want to program the properties of the control’s environment, known as ambient properties. For this purpose, CAxHostWindow implements the IAxWinAmbientDispatch interface:

 1interface IAxWinAmbientDispatch : IDispatch {
 2  [propput]
 3  HRESULT AllowWindowlessActivation([in]VARIANT_BOOL b);
 4  [propget]
 5  HRESULT AllowWindowlessActivation(
 6    [out,retval]VARIANT_BOOL* pb);
 7
 8  // DISPID_AMBIENT_BACKCOLOR
 9  [propput, id(DISPID_AMBIENT_BACKCOLOR)]
10  HRESULT BackColor([in]OLE_COLOR clrBackground);
11  [propget, id(DISPID_AMBIENT_BACKCOLOR)]
12  HRESULT BackColor([out,retval]OLE_COLOR* pclrBackground);
13
14  // DISPID_AMBIENT_FORECOLOR
15  [propput, id(DISPID_AMBIENT_FORECOLOR)]
16  HRESULT ForeColor([in]OLE_COLOR clrForeground);
17  [propget, id(DISPID_AMBIENT_FORECOLOR)]
18  HRESULT ForeColor([out,retval]OLE_COLOR* pclrForeground);
19
20  // DISPID_AMBIENT_LOCALEID
21  [propput, id(DISPID_AMBIENT_LOCALEID)]
22  HRESULT LocaleID([in]LCID lcidLocaleID);
23  [propget, id(DISPID_AMBIENT_LOCALEID)]
24  HRESULT LocaleID([out,retval]LCID* plcidLocaleID);
25
26  // DISPID_AMBIENT_USERMODE
27  [propput, id(DISPID_AMBIENT_USERMODE)]
28  HRESULT UserMode([in]VARIANT_BOOL bUserMode);
29  [propget, id(DISPID_AMBIENT_USERMODE)]
30  HRESULT UserMode([out,retval]VARIANT_BOOL* pbUserMode);
31
32  // DISPID_AMBIENT_DISPLAYASDEFAULT
33  [propput, id(DISPID_AMBIENT_DISPLAYASDEFAULT)]
34  HRESULT DisplayAsDefault([in]VARIANT_BOOL bDisplayAsDefault);
35  [propget, id(DISPID_AMBIENT_DISPLAYASDEFAULT)]
36  HRESULT DisplayAsDefault(
37    [out,retval]VARIANT_BOOL* pbDisplayAsDefault);
38
39  // DISPID_AMBIENT_FONT
40  [propput, id(DISPID_AMBIENT_FONT)]
41  HRESULT Font([in]IFontDisp* pFont);
42  [propget, id(DISPID_AMBIENT_FONT)]
43  HRESULT Font([out,retval]IFontDisp** pFont);
44
45  // DISPID_AMBIENT_MESSAGEREFLECT
46  [propput, id(DISPID_AMBIENT_MESSAGEREFLECT)]
47  HRESULT MessageReflect([in]VARIANT_BOOL bMsgReflect);
48  [propget, id(DISPID_AMBIENT_MESSAGEREFLECT)]
49  HRESULT MessageReflect([out,retval]VARIANT_BOOL* pbMsgReflect);
50
51  // DISPID_AMBIENT_SHOWGRABHANDLES
52  [propget, id(DISPID_AMBIENT_SHOWGRABHANDLES)]
53  HRESULT ShowGrabHandles(VARIANT_BOOL* pbShowGrabHandles);
54
55  // DISPID_AMBIENT_SHOWHATCHING
56  [propget, id(DISPID_AMBIENT_SHOWHATCHING)]
57  HRESULT ShowHatching(VARIANT_BOOL* pbShowHatching);
58
59  // IDocHostUIHandler Defaults
60  ...
61};

QueryHost can be used on a CAxWindow to obtain the IAxWinAmbientDispatch interface so that these ambient properties can be changed. For example:

1LRESULT CMainWindow::OnSetGreenBackground(...) {
2  // Set up green ambient background
3  CComPtr<IAxWinAmbientDispatch> spAmbient;
4  hr = m_ax.QueryHost(&spAmbient);
5  if( SUCCEEDED(hr) ) {
6    spAmbient->put_BackColor(RGB(0, 255, 0));
7  }
8  return 0;
9}

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 IAxWinAmbientDispatch is a dual interface).

Hosting Property Pages

If your container is a development environment, you might want to allow the user to show the control’s property pages. This can be accomplished by calling the IOleObject member function DoVerb, passing in the OLEIVERB_PROPERTIES verb ID:

 1LRESULT CMainWindow::OnEditProperties(...) {
 2  CComPtr<IOleObject> spoo;
 3  HRESULT hr = m_ax.QueryControl(&spoo);
 4  if( SUCCEEDED(hr) ) {
 5    CComPtr<IOleClientSite> spcs; m_ax.QueryHost(&spcs);
 6    RECT rect; m_ax.GetClientRect(&rect);
 7    hr = spoo->DoVerb(OLEIVERB_PROPERTIES, 0, spcs,
 8      -1, m_ax.m_hWnd, &rect);
 9    if( FAILED(hr) )
10      MessageBox(__T("Properties unavailable"), __T("Error"));
11  }
12  return 0;
13}

If you want to add your own property pages to those of the control or you want to show the property pages of a control that doesn’t support the OLEIVERB_PROPERTIES verb, you can take matters into your own hands with a custom property sheet. First, you need to ask the control for its property pages via the ISpecifyPropertyPages member function GetPages. Second, you might want to augment the control’s property pages with your own. Finally, you show the property pages (each a COM object with its own CLSID) via the COM global function OleCreatePropertyFrame, as demonstrated in the ShowProperties function I developed for this purpose:

 1HRESULT ShowProperties(IUnknown* punkControl, HWND hwndParent) {
 2  HRESULT hr = E_FAIL;
 3
 4  // Ask the control to specify its property pages
 5  CComQIPtr<ISpecifyPropertyPages> spPages = punkControl;
 6  if (spPages) {
 7
 8    CAUUID pages;
 9    hr = spPages->GetPages(&pages);
10    if( SUCCEEDED(hr) ) {
11      // TO DO: Add your custom property pages here
12
13      CComQIPtr<IOleObject> spObj = punkControl;
14      if( spObj ) {
15        LPOLESTR pszTitle = 0;
16        spObj->GetUserType(USERCLASSTYPE_SHORT, &pszTitle);
17
18        // Show the property pages
19        hr = OleCreatePropertyFrame(hwndParent, 10, 10, pszTitle,
20          1, &punkControl, pages.cElems,
21          pages.pElems, LOCALE_USER_DEFAULT, 0, 0);
22
23        CoTaskMemFree(pszTitle);
24      }
25      CoTaskMemFree(pages.pElems);
26    }
27  }
28
29  return hr;
30}

The ShowProperties function can be used instead of the call to DoVerb. For example:

1LRESULT CMainWindow::OnEditProperties(...) {
2  CComPtr<IUnknown> spunk;
3  if( SUCCEEDED(m_ax.QueryControl(&spunk)) ) {
4    if( FAILED(ShowProperties(spunk, m_hWnd)) ) {
5    MessageBox(__T("Properties unavailable"), __T("Error"));
6   }
7  }
8  return 0;
9}

Either way, if the control’s property pages are shown and the Apply or OK button is pressed, your container should receive one IPropertyNotifySink call per property that has changed.

Persisting a Control

You might want to persist between application sessions. As discussed in Chapter 7, “Persistence in ATL,” you can do this with any number of persistence interfaces. 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:

 1bool CMainWindow::Save(LPCOLESTR pszFileName) {
 2  // Make sure object can be saved
 3  // Note: Our IPersistStream interface pointer could end up
 4  // holding an IPersistStreamInit interface. This is OK
 5  // since IPersistStream is a layout-compatible subset of
 6  // IPersistStreamInit.
 7  CComQIPtr<IPersistStream> spPersistStream;
 8  HRESULT hr = m_ax.QueryControl(&spPersistStream);
 9  if( FAILED(hr) ) {
10    hr = m_ax.QueryControl(IID_IPersistStreamInit,
11      (void**)&spPersistStream);
12    if( FAILED(hr) ) return false;
13  }
14
15  // Save object to stream in a storage
16  CComPtr<IStorage> spStorage;
17  hr = StgCreateDocfile(pszFileName,
18    STGM_DIRECT | STGM_WRITE |
19    STGM_SHARE_EXCLUSIVE | STGM_CREATE,
20    0, &spStorage);
21  if( SUCCEEDED(hr) ) {
22    CComPtr<IStream> spStream;
23    hr = spStorage->CreateStream(OLESTR("Contents"),
24      STGM_DIRECT | STGM_WRITE |
25      STGM_SHARE_EXCLUSIVE | STGM_CREATE,
26      0, 0, &spStream);
27  if( SUCCEEDED(hr) ) {
28    // Get and store the CLSID
29    CLSID clsid;
30    hr = spPersistStream->GetClassID(&clsid);
31    if( SUCCEEDED(hr) ) {
32      hr = spStream->Write(&clsid, sizeof(clsid), 0);
33
34      // Save the object
35      hr = spPersistStream->Save(spStream, TRUE);
36    }
37   }
38  }
39
40  if( FAILED(hr) ) return false;
41  return true;
42}

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. For example:

 1bool CMainWindow::Open(LPCOLESTR pszFileName) {
 2  // Open object a stream in the storage
 3  CComPtr<IStorage>   spStorage;
 4  CComPtr<IStream>    spStream;
 5  HRESULT                   hr;
 6  hr = StgOpenStorage(pszFileName, 0,
 7    STGM_DIRECT | STGM_READ | STGM_SHARE_EXCLUSIVE,
 8    0, 0, &spStorage);
 9  if( SUCCEEDED(hr) ) {
10    hr = spStorage->OpenStream(OLESTR("Contents"), 0,
11      STGM_DIRECT | STGM_READ | STGM_SHARE_EXCLUSIVE,
12      0, &spStream);
13  }
14
15  if( FAILED(hr) ) return false;
16
17  // Read a CLSID from the stream
18  CLSID clsid;
19  hr = spStream->Read(&clsid, sizeof(clsid), 0);
20  if( FAILED(hr) ) return false;
21
22  RECT rect; GetClientRect(&rect);
23  OLECHAR szClsid[40];
24  StringFromGUID2(clsid, szClsid, lengthof(szClsid));
25
26  // Create the control's host window
27  if( !m_ax.Create(m_hWnd, rect, 0, WS_CHILD | WS_VISIBLE, 0,
28                   ID_CHILD_CONTROL) {
29    return false;
30  }
31
32  // Create the control, persisting from the stream
33  hr = m_ax.CreateControl(szClsid, spStream);
34  if( FAILED(hr) ) return false;
35  return true;
36}

When a NULL IStream interface pointer is provided to either CreateControl or CreateControlEx, ATL attempts to call the IPersistStreamInit member function InitNew to make sure that either InitNew or Load is called, as appropriate.

Accelerator Translations

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:

 1BOOL CMainWnd:: PreTranslateAccelerator(MSG* pMsg) {
 2  // Accelerators are only keyboard or mouse messages
 3  if ((pMsg->message < WM_KEYFIRST ||
 4    pMsg->message > WM_KEYLAST) &&
 5    (pMsg->message < WM_MOUSEFIRST ||
 6    pMsg->message > WM_MOUSELAST))
 7    return FALSE;
 8
 9  // Find a direct child of this window from the window that has
10  // focus. This will be AxAtlWin80 window for the hosted
11  // control.
12  HWND hWndCtl = ::GetFocus();
13  if( IsChild(hWndCtl) && ::GetParent(hWndCtl) != m_hWnd ) {
14  do hWndCtl = ::GetParent(hWndCtl);
15  while( ::GetParent(hWndCtl) != m_hWnd );
16  }
17
18  // Give the control (via the AtlAxWin80) a chance to
19  // translate this message
20  if (::SendMessage(hWndCtl, WM_FORWARDMSG, 0, (LPARAM)pMsg) )
21    return TRUE;
22
23  // Check for dialog-type navigation accelerators
24  return IsDialogMessage(pMsg);
25}

The crux of this function forwards the message to the AtlAxWin80 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 is 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 this:

 1int WINAPI WinMain(...) {
 2  ...
 3  CMainWindow wndMain;
 4  ...
 5  HACCEL haccel = LoadAccelerators(_Module.GetResourceInstance(),
 6    MAKEINTRESOURCE(IDC_MYACCELS));
 7  MSG msg;
 8  while( GetMessage(&msg, 0, 0, 0) ) {
 9    if( !TranslateAccelerator(msg.hwnd, haccel, &msg) &&
10      !wndMain.PreTranslateAccelerator(&msg) ) {
11      TranslateMessage(&msg);
12      DispatchMessage(&msg);
13    }
14  }
15  ...
16}

The use of a PreTranslateAccelerator function on every window that contains a control gives the keyboard navigation keys a much greater chance of working, although the individual controls have to cooperate, too.

Hosting a Control in a Dialog

Inserting a Control into a Dialog Resource

So far, I’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 a dialog resource and choosing Insert ActiveX Control. As of Visual C++ 6.0, ATL supports creating dialogs that host the controls inserted into dialog resources.

To add an ActiveX Control to a dialog, you must first add it to the Visual Studio toolbox. This is pretty simple. Get the toolbox onto the screen, and then right-click and select Choose Items. This brings up the Choose Toolbox Items dialog box, shown in Figure 12.3.

Figure 12.3. Choose Toolbox Items

[View full size image]

_images/12atl03.jpg

Select the ActiveX controls you want, and they’re in the toolbox to be added to dialogs. To add the control, simply drag and drop it from the toolbox onto the dialog editor.

The container example provided as part of this chapter has a simple dialog box with a BullsEye control inserted, along with a couple static controls and a button. This is what that dialog resource looks like in the .rc file:

 1IDD_BULLSEYE DIALOG DISCARDABLE 0, 0, 342, 238
 2STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
 3CAPTION "BullsEye"
 4FONT 8, "MS Sans Serif"
 5BEGIN
 6    CONTROL "",IDC_BULLSEYE,
 7        "{7DC59CC5-36C0-11D2-AC05-00A0C9C8E50D}",
 8        WS_TABSTOP,7,7,269,224
 9    LTEXT "&Score:",IDC_STATIC,289,7,22,8
10    CTEXT "Static",IDC_SCORE,278,18,46,14,
11        SS_CENTERIMAGE | SS_SUNKEN
12    PUSHBUTTON "Close",IDCANCEL,276,41,50,14
13END

Control Initialization

Notice that the window text part of the CONTROL resource is a CLSIDspecifically, the CLSID of the BullsEye control. This window text is passed to an instance of the AtlAxWin80 window class to determine the type of 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 is handed to the BullsEye control at creation time (via IPersistStreamInit):

1IDD_BULLSEYE DLGINIT
2BEGIN
3    IDC_BULLSEYE, 0x376, 154, 0
40x0026, 0x0000, 0x007b, 0x0039, 0x0035,
5...
60x0000, 0x0040, 0x0000, 0x0020, 0x0000,
7    0
8END

Because most folks prefer not to enter this information directly, the properties show up in the Visual Studio properties window. Simply click on the control and bring up the Property tab. Figure 12.4 shows the BullsEye properties window.

Figure 12.4. VS Dialog Editor properties window for ``BullsEye`` control

_images/12atl04.jpg

Also note that the RingCount property has an ellipsis button next to it. If you click that button, Visual Studio brings up the property page for that property.

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 is automatically restored at runtime.

Sinking Control Events in a Dialog

Recall 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. Although 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; the IDispEventImpl and sink map entries are added for you. Figure 12.5 shows the Event Handlers dialog box.

Figure 12.5. ``BullsEye`` Event Handlers dialog box

[View full size image]

_images/12atl05.jpg

The Event Handler Wizard adds the IDispEventImpl classes and manages the sink map. The CAxDialogImpl class (discussed next) handles the call to AtlAdvise-SinkMap that actually connects to the events in the control.

CAxDialogImpl

In Chapter 10, I discussed the CDialogImpl class, which, unfortunately, is not capable of hosting controls. Recall that the member function wrappers DoModal and Create merely call the Win32 functions DialogBoxParam and CreateDialogParam. Because 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. Specifically, it must preprocess the dialog box resource looking for CONTROL enTRies and replacing them with entries that will create an instance of an AtlAxWin80 window. AtlAxWin80 then 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 chapter dissecting. To hook up this preprocessing step when hosting controls in dialogs, we use the CAxDialogImpl base class:

  1template <class T, class TBase /* = CWindow */>
  2class CAxDialogImpl : public CDialogImplBaseT< TBase > {
  3public:
  4...
  5  static INT_PTR CALLBACK DialogProc(HWND hWnd, UINT uMsg,
  6    WPARAM wParam, LPARAM lParam);
  7
  8  // modal dialogs
  9  INT_PTR DoModal(HWND hWndParent = ::GetActiveWindow(),
 10    LPARAM dwInitParam = NULL) {
 11    _AtlWinModule.AddCreateWndData(&m_thunk.cd,
 12      (CDialogImplBaseT< TBase >*)this);
 13      return AtlAxDialogBox(_AtlBaseModule.GetResourceInstance(),
 14        MAKEINTRESOURCE(static_cast<T*>(this)->IDD),
 15        hWndParent, T::StartDialogProc, dwInitParam);
 16  }
 17
 18  BOOL EndDialog(int nRetCode) {
 19    return ::EndDialog(m_hWnd, nRetCode);
 20  }
 21
 22  // modeless dialogs
 23  HWND Create(HWND hWndParent, LPARAM dwInitParam = NULL) {
 24    _AtlWinModule.AddCreateWndData(&m_thunk.cd,
 25      (CDialogImplBaseT< TBase >*)this);
 26    HWND hWnd = AtlAxCreateDialog(
 27      _AtlBaseModule.GetResourceInstance(),
 28      MAKEINTRESOURCE(static_cast<T*>(this)->IDD),
 29      hWndParent, T::StartDialogProc, dwInitParam);
 30    return hWnd;
 31}
 32
 33  // for CComControl
 34  HWND Create(HWND hWndParent, RECT&,
 35    LPARAM dwInitParam = NULL) {
 36    return Create(hWndParent, dwInitParam);
 37  }
 38  BOOL DestroyWindow() {
 39    return ::DestroyWindow(m_hWnd);
 40  }
 41
 42  // Event handling support and Message map
 43  HRESULT AdviseSinkMap(bool bAdvise) {
 44    if(!bAdvise && m_hWnd == NULL) {
 45    // window is gone, controls are already unadvised
 46      return S_OK;
 47    }
 48    HRESULT hRet = E_NOTIMPL;
 49    __if_exists(T::_GetSinkMapFinder) {
 50      T* pT = static_cast<T*>(this);
 51      hRet = AtlAdviseSinkMap(pT, bAdvise);
 52    }
 53    return hRet;
 54  }
 55
 56  typedef CAxDialogImpl< T, TBase > thisClass;
 57  BEGIN_MSG_MAP(thisClass)
 58    MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)
 59    MESSAGE_HANDLER(WM_DESTROY, OnDestroy)
 60  END_MSG_MAP()
 61
 62  virtual HRESULT CreateActiveXControls     (UINT nID) {
 63    // Load dialog template and InitData
 64    // Walk through template and create ActiveX controls
 65    // Code omitted for clarity
 66  }
 67
 68  LRESULT OnInitDialog(UINT /*uMsg*/, WPARAM /*wParam*/,
 69    LPARAM /*lParam*/, BOOL& bHandled) {
 70    // initialize controls in dialog with DLGINIT
 71    // resource section
 72    ExecuteDlgInit(static_cast<T*>(this)->IDD);
 73    AdviseSinkMap(true);
 74    bHandled = FALSE;
 75    return 1;
 76  }
 77
 78  LRESULT OnDestroy(UINT /*uMsg*/, WPARAM /*wParam*/,
 79    LPARAM /*lParam*/, BOOL& bHandled) {
 80    AdviseSinkMap(false);
 81    bHandled = FALSE;
 82    return 1;
 83  }
 84
 85    // Accelerator handling - needs to be called from a message loop
 86    BOOL IsDialogMessage(LPMSG pMsg) {
 87         // Code omitted for clarity
 88    }
 89};
 90
 91template <class T, class TBase>
 92INT_PTR CALLBACK CAxDialogImpl< T, TBase >::DialogProc(
 93  HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
 94  CAxDialogImpl< T, TBase >* pThis = (
 95    CAxDialogImpl< T, TBase >*)hWnd;
 96  if (uMsg == WM_INITDIALOG) {
 97    HRESULT hr;
 98    if (FAILED(hr = pThis->CreateActiveXControls(
 99      pThis->GetIDD()))) {
100      pThis->DestroyWindow();
101      SetLastError(hr & 0x0000FFFF);
102      return FALSE;
103    }
104  }
105  return CDialogImplBaseT< TBase >::DialogProc(
106    hWnd, uMsg, wParam, lParam);
107}

Notice that the DoModal and Create wrapper functions call AtlAxDialogBox and AtlAxCreateDialog instead of DialogBoxParam and CreateDialogParam, respectively. These functions take the original dialog template (including the ActiveX control information that Windows can’t handle) and create a second in-memory template that has those controls stripped out. The stripped dialog resource is then passed to the appropriate DialogBoxParam function so that Windows can do the heavy lifting. The actual creation of the ActiveX controls is done in the CreateActiveXControls method, which is called as part of the WM_INITDIALOG processing.

Using CAxDialogImpl as the base class, we can have a dialog that hosts COM controls like this:

 1class CBullsEyeDlg :
 2  public CAxDialogImpl<CBullsEyeDlg>,
 3  public IDispEventImpl<IDC_BULLSEYE, CBullsEyeDlg> {
 4public:
 5BEGIN_MSG_MAP(CBullsEyeDlg)
 6  MESSAGE_HANDLER(WM_DESTROY, OnDestroy)
 7  MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)
 8  COMMAND_ID_HANDLER(IDCANCEL, OnCancel)
 9END_MSG_MAP()
10
11BEGIN_SINK_MAP(CBullsEyeDlg)
12  SINK_ENTRY(IDC_BULLSEYE, 0x2, OnScoreChanged)
13END_SINK_MAP()
14
15  // Map this class to a specific dialog resource
16  enum { IDD = IDD_BULLSEYE };
17
18  // Hook up connection points
19  LRESULT OnInitDialog(...)
20  { AtlAdviseSinkMap(this, true); return 0; }
21
22  // Tear down connection points
23  LRESULT OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam,
24    BOOL& bHandled)
25  { AtlAdviseSinkMap(this, false); return 0; }
26  // Window control event handlers
27  LRESULT OnCancel(WORD, UINT, HWND, BOOL&);
28
29  // COM control event handlers
30  VOID __stdcall OnScoreChanged(LONG ringValue);
31};

Notice that, just like a normal dialog, the message map handles messages for the dialog itself (such as WM_INITDIALOG and WM_DESTROY) and also 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 are 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. Because ATL has created an instance of the AtlAxWin80 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; thereafter, you use the CAxWindow object to manipulate the host window. This is very much like you’d use the Attach member function of the window wrapper classes discussed in Chapter 10 to manipulate an edit control. After you’ve attached a CAxWindow object to an AtlAxWin80 window, you can use the member functions of CAxWindow to communicate with the control host window. Recall the QueryControl member function to obtain an interface from a control, as shown here:

 1class CBullsEyeDlg :
 2  public CAxDialogImpl<CBullsEyeDlg>,
 3  public IDispEventImpl<IDC_BULLSEYE, CBullsEyeDlg> {
 4public:
 5...
 6  LRESULT OnInitDialog(...) {
 7    // Attach to the BullsEye control
 8    m_axBullsEye.Attach(GetDlgItem(IDC_BULLSEYE));
 9
10    // Cache BullsEye interface
11    m_axBullsEye.QueryControl(&m_spBullsEye);
12    ...
13    return 0;
14  }
15...
16private:
17  CAxWindow                m_axBullsEye;
18  CComPtr<IBullsEye> m_spBullsEye;
19};

In this example, I’ve cached both the HWND to the AtlAxWin80, for continued communication with the control host window, and one of the control’s interfaces, for communication with the control itself. If you need only an interface, not the HWND, you might want to consider using GetdlgControl instead.

GetDlgControl

Because the CDialogImpl class derives from CWindow, it provides the GetdlgItem function to retrieve the HWND of a child window, given the ID of the child. Likewise, CWindow provides a GetdlgControl member function, but to retrieve an interface pointer instead of an HWND:

 1HRESULT GetDlgControl(int nID, REFIID iid, void** ppCtrl) {
 2  if (ppCtrl == NULL) return E_POINTER;
 3  *ppCtrl = NULL;
 4  HRESULT hr = HRESULT_FROM_WIN32(ERROR_CONTROL_ID_NOT_FOUND);
 5  HWND hWndCtrl = GetDlgItem(nID);
 6  if (hWndCtrl != NULL) {
 7    *ppCtrl = NULL;
 8    CComPtr<IUnknown> spUnk;
 9    hr = AtlAxGetControl(hWndCtrl, &spUnk);
10    if (SUCCEEDED(hr))
11      hr = spUnk->QueryInterface(iid, ppCtrl);
12  }
13  return hr;
14}

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 AtlAxWin80 understand. If the child window is not an instance of the AtlAxWin80 window class, or if the control does not support the interface being requested, GetdlgControl returns a failed HRESULT. Using GetdlgControl simplifies the code to cache an interface on a control considerably:

1LRESULT OnInitDialog(...) {
2  // Cache BullsEye interface
3  GetDlgControl(IDC_BULLSEYE, IID_IBullsEye,
4    (void**)&m_spBullsEye);
5  ...
6  return 0;
7}

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

Declarative User Interfaces for Controls

There’s beauty in using a dialog resource for managing the user interface (UI) of a window. Instead of writing pages of code to create, initialize, and place controls on a rectangle of gray, we can use the resource editor to do it for us. At design time, we lay out the size and location of the elements of the UI, and the ATL-augmented dialog manager is responsible for the heavy lifting. This is an extremely useful mode of UI development, and it’s not limited to dialogs. 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 a Windows control, 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. In fact, if you combine all the programming techniques from Chapter 10 with the techniques I’ve shown you thus far in this chapter, you have a composite control.

CComCompositeControl

ATL provides support for composite controls via the CComCompositeControl base class:

1template <class T>
2class CComCompositeControl :
3  public CComControl< T, CAxDialogImpl< T > > {...};

Notice that CComCompositeControl derives from both CComControl and CAxDialogImpl, combining the functionality of a control and the drawing of the dialog manager, augmented with the COM control hosting capabilities of AtlAxWin80. 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:

 1class ATL_NO_VTABLE CDartBoard :
 2  public CComObjectRootEx<CComSingleThreadModel>,
 3  public IDispatchImpl<IDartBoard, &IID_IDartBoard,
 4    &LIBID_CONTROLSLib>,
 5  public CComCompositeControl<CDartBoard>,
 6  ...
 7  public CComCoClass<CDartBoard, &CLSID_DartBoard> {
 8public:
 9...
10  enum { IDD = IDD_DARTBOARD };
11
12  CDartBoard() {
13    // Composites can't be windowless
14    m_bWindowOnly = TRUE;
15
16    // Calculate natural extent based on dialog resource size
17    CalcExtent(m_sizeExtent);
18  }
19...
20};

Notice that the construction of the composite control sets the m_bWindowOnly flag, disabling windowless operation. The control’s window must 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

Because 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 causes them to redraw, is all that’s required to update the visual state of a control.

For example, the DartBoard example available with the source code of this book uses a dialog resource to lay out its elements, as shown in Figure 12.6.

Figure 12.6. DartBoard composite control dialog resource

_images/12atl06.jpg

This dialog resource holds a BullsEye control, two static controls, and a button. When the user clicks on a ring of the target, the score is incremented. When the Reset button is pressed, the score is cleared. The composite control takes care of all the logic, but the dialog manager performs the drawing.

However, when a composite control is shown but not activated, the composite control’s window is not created and the drawing must be done manually. For example, a composite control must perform its own drawing when hosted in a dialog resource during the design mode of the Visual C++ resource editor. The ATL Object Wizard generates an implementation of OnDraw that handles this case, as shown in Figure 12.7.

Figure 12.7. Default ``CComCompositeControl``’s inactive ``OnDraw`` implementation

_images/12atl07.jpg

I find this implementation somewhat inconvenient because it doesn’t show the dialog resource as I’m using the control. Specifically, it doesn’t show the size of the resource. Toward that end, I’ve provided another implementation that is a bit more helpful (see Figure 12.8).

Figure 12.8. Updated ``CComCompositeControl``’s inactive ``OnDraw`` implementation

_images/12atl08.jpg

This implementation shows the light gray area as the recommended size of the control based on the control’s dialog resource. The dark gray area is the part of the control that is still managed by the control but that is outside the area managed for the control by the dialog manager. The updated OnDraw implementation is shown here:

 1// Draw an inactive composite control
 2virtual HRESULT OnDraw(ATL_DRAWINFO& di) {
 3  if( m_bInPlaceActive ) return S_OK;
 4
 5  // Draw background rectangle
 6  SelectObject(di.hdcDraw, GetStockObject(BLACK_PEN));
 7  SelectObject(di.hdcDraw, GetStockObject(GRAY_BRUSH));
 8  Rectangle(di.hdcDraw, di.prcBounds->left, di.prcBounds->top,
 9            di.prcBounds->right, di.prcBounds->bottom);
10
11  // Draw proposed dialog rectangle
12  SIZE sizeMetric; CalcExtent(sizeMetric);
13  SIZE sizeDialog; AtlHiMetricToPixel(&sizeMetric, &sizeDialog);
14  SIZE sizeBounds = {
15    di.prcBounds->right - di.prcBounds->left,
16    di.prcBounds->bottom - di.prcBounds->top };
17  SIZE sizeDialogBounds = {
18    min(sizeDialog.cx, sizeBounds.cx),
19    min(sizeDialog.cy, sizeBounds.cy) };
20  RECT rectDialogBounds = {
21    di.prcBounds->left, di.prcBounds->top,
22    di.prcBounds->left + sizeDialogBounds.cx,
23    di.prcBounds->top + sizeDialogBounds.cy };
24  SelectObject(di.hdcDraw, GetStockObject(LTGRAY_BRUSH));
25  Rectangle(di.hdcDraw,
26    rectDialogBounds.left, rectDialogBounds.top,
27    rectDialogBounds.right, rectDialogBounds.bottom);
28
29  // Report natural and current size of dialog resource
30  SetTextColor(di.hdcDraw, ::GetSysColor(COLOR_WINDOWTEXT));
31  SetBkMode(di.hdcDraw, TRANSPARENT);
32
33  TCHAR sz[256];
34  wsprintf(sz, __T("Recommended: %d x %d\r\nCurrent: %d x %d"),
35    sizeDialog.cx, sizeDialog.cy,
36    sizeBounds.cx, sizeBounds.cy);
37
38  DrawText(di.hdcDraw, sz, -1, &rectDialogBounds, DT_CENTER);
39
40  return S_OK;
41}

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. A composite control is a powerful way to lay out a control’s UI elements at design time. However, if you really want to wield the full power of a declarative UI when building a control, you need an HTML control.

HTML Controls

Generating an HTML Control

You create an HTML control via the ATL Control Wizard. On the Options page, choose DHTML Control (Minimal is also an option, as described in Chapter 11). The wizard-generated code creates a control that derives from CComControl, sets m_bWindowOnly to TRUE, and provides a resource for the layout of the UI elements of your control. This is similar to the resource that results from running the Object Wizard for a composite control, except that instead of using a dialog resource, an HTML control uses an HTML resource. The same WebBrowser control that provides the UI for Internet Explorer provides the parsing for the HTML resource at runtime. This allows a control to use a declarative style of UI development, but with all the capabilities of the HTML engine in Internet Explorer. The following are a few of the advantages that HTML provides over a dialog resource:

  • Support for resizing via height and width attributes, both in absolute pixels and percentages

  • Support for scripting when using top-level initialization code, defining functions, and handling events

  • Support for extending the object model of the HTML document via an “external” object

  • Support for flowing of mixed text and graphics

  • Support for multiple font families, colors, sizes, and styles

In fact, pretty much everything you’ve ever seen on a web site can be performed using the HTML control.

By default, you get this HTML as a starting point in a wizard-generated string resource named IDH_<PROJECTNAME>:

 1<HTML>
 2<BODY id=theBody>
 3<BUTTON onclick='window.external.OnClick(theBody, "red");'>
 4Red
 5</BUTTON>
 6<BR>
 7<BR>
 8<BUTTON onclick='window.external.OnClick(theBody, "green");'>
 9Green
10</BUTTON>
11<BR>
12<BR>
13<BUTTON onclick='window.external.OnClick(theBody, "blue");'>
14Blue
15</BUTTON>
16</BODY>
17</HTML>

From here, you can start changing the HTML to do whatever you like.

HTML Control Creation

The magic of hooking up the WebBrowser control is performed in the OnCreate handler generated by the ATL Object Wizard:

 1LRESULT CSmartDartBoard::OnCreate(UINT, WPARAM, LPARAM, BOOL&) {
 2  // Wrap the control's window to use it to host control
 3  // (not an AtlAxWin80, so no CAxHostWindow yet created)
 4CAxWindow wnd(m_hWnd);
 5
 6  // Create a CAxWinHost: It will subclass this window and
 7  // create a control based on an HTML resource.
 8  HRESULT hr = wnd.CreateControl(IDH_SMARTDARTBOARD);
 9  ...
10  return SUCCEEDED(hr) ? 0 : -1;
11}

Because m_bWindowOnly is set to TRue, activating the HTML control creates a window. To give this window control-containment capabilities so that it can host an instance of the WebBrowser control, the HTML control’s window must be subclassed and sent through the message map provided by CAxHostWindow, just like every other control container. However, because the HTML control’s window is not an instance of the AtlAxWin80 class, the subclassing must be handled manually. Notice that the first thing the wizard-generated OnCreate function does is wrap an instance of CAxWindow around the control’s HWND. The call to CreateControl creates an instance of CAxHostWindow. The CAxHostWindow object subclasses the HTML control’s window, creates an instance of the WebBrowser control, and feeds it a URL of the form res://<module name>/<resource ID>, using CreateNormalizedControl. In effect, the HWND of the HTML control is a parent for the WebBrowser control, which then uses the HTML resource to manage the UI elements of the control. This is exactly analogous to the composite control, in which the HWND of the control was an instance of the dialog manager, using a dialog resource to manage the elements of the UI.

Element Layout

For example, the SmartDartBoard HTML control that is available with the source code of this book uses HTML to lay out its UI, just like the DartBoard composite control. However, in HTML, I can use the auto-layout capabilities of a <table> element to autosize the HTML control to whatever size the user wants, instead of limiting myself to a single size, as with the dialog resource. The following shows how the SmartDartBoard control’s HTML resource lays out its elements:

 1<!-- Use all of the control's area -->
 2<table width=100% height=100%>
 3<tr>
 4    <td colspan=2>
 5    <object id=objBullsEye width=100% height=100%
 6            classid="clsid:7DC59CC5-36C0-11D2-AC05-00A0C9C8E50D">
 7    </object>
 8    </td>
 9</tr>
10<tr height=1>
11    <td>Score: <span id=spanScore>0</td>
12    <td align=right>
13        <input type=button id=cmdReset value="Reset">
14    </td>
15</tr>
16</table>

To test the UI of the HTML control without compiling, you can right-click the HTML file and choose Preview, which shows the HTML in the Internet Explorer. For example, Figures 12.9 and 12.10 show the control’s UI resizing itself properly to fit into the space that it’s given.

Figure 12.9. Small ``SmartDartBoard`` UI

_images/12atl09.jpg

Figure 12.10. Large ``SmartDartBoard`` UI

_images/12atl10.jpg

Accessing the HTML from the Control

When you create an instance of the WebBrowser control, you’re actually creating two things: a WebBrowser control, which knows about URLs, and an HTML Document control, which knows about parsing and displaying HTML. The WebBrowser control forms the core of the logic of Internet Explorer and lives in shdocvw.dll. The WebBrowser control implements the IWebBrowser2 interface, with methods such as Navigate, GoBack, GoForward, and Stop. Because the CAxWindow object is merely used to bootstrap hosting the WebBrowser control, and you’d need to be able to access the WebBrowser control in other parts of your code, the OnCreate code generated by the wizard uses QueryControl to cache the IWebBrowser2 interface:

1LRESULT CSmartDartBoard::OnCreate(UINT, WPARAM, LPARAM, BOOL&) {
2...
3  // Cache the IWebBrowser2 interface
4  if (SUCCEEDED(hr))
5    hr = wnd.QueryControl(IID_IWebBrowser2,
6        (void**)&m_spBrowser);
7
8...
9}

To display the HTML, the WebBrowser control creates an instance of the HTML Document control, which is implemented in mshtml.dll. The HTML Document represents the implementation of the Dynamic HTML object model (DHTML). This object model exposes each named element on a page of HTML as a COM object, each of which implements one or more COM interfaces and many of which fire events. Although the full scope and power of DHTML is beyond the scope of this book, the rest of this chapter is dedicated to showing you just the tip of the iceberg of functionality DHTML provides.

The HTML Document object implements the IHTMLDocument2 interface, whose most important property is the all property. Getting from the WebBrowser control to the HTML Document control is a matter of retrieving the IWebBrowser2 Document property and querying for the IHTMLDocument2 interface. Accessing any named object on the HTML page – that is, any tag with an id attributeis done via the all property of the IHTMLDocument2 interface. Retrieving a named object on an HTML page in C++ can be accomplished with a helper function such as the GetHtmlElement helper shown here:

 1HRESULT CSmartDartBoard::GetHtmlElement(
 2  LPCOLESTR pszElementID,
 3  IHTMLElement** ppElement) {
 4  ATLASSERT(ppElement);
 5  *ppElement = 0;
 6
 7  // Get the document from the browser
 8  HRESULT hr = E_FAIL;
 9  CComPtr<IDispatch> spdispDoc;
10  hr = m_spBrowser->get_Document(&spdispDoc);
11  if( FAILED(hr) ) return hr;
12
13  CComQIPtr<IHTMLDocument2> spDoc = spdispDoc;
14  if( !spDoc ) return E_NOINTERFACE;
15
16  // Get the All collection from the document
17  CComPtr<IHTMLElementCollection> spAll;
18  hr = spDoc->get_all(&spAll);
19  if( FAILED(hr) ) return hr;
20
21  // Get the element from the All collection
22  CComVariant varID = pszElementID;
23  CComPtr<IDispatch> spdispItem;
24  hr = spAll->item(varID, CComVariant(0), &spdispItem);
25
26  // Return the IHTMLElement interface
27  return spdispItem->QueryInterface(ppElement);
28}

When you have the IHtmlElement interface, you can change just about anything about that element. For example, notice the <span> tag named spanScore in the SmartDartBoard HTML resource:

1...
2<td>Score: <span id=spanScore>0</span></td>
3...

A span is just a range of HTML with a name so that it can be programmed against. As far as we’re concerned, every named object in the HTML is a COM object that we can access from our control’s C++ code. This span’s job is to hold the control’s current score, so after the WebBrowser control has been created, we need to set the span to the m_nScore property of the control. The SetScoreSpan helper function in the SmartDartBoard HTML control uses the GetHtmlElement helper and the IHTMLElement interface to set the innerText property of the span:

 1HRESULT CSmartDartBoard::SetScoreSpan() {
 2  // Convert score to VARIANT
 3  CComVariant varScore = m_nScore;
 4  HRESULT hr = varScore.ChangeType(VT_BSTR);
 5  if( FAILED(hr) ) return hr;
 6
 7  // Find score span element
 8  CComPtr<IHTMLElement> speScore;
 9  hr = GetHtmlElement(OLESTR("spanScore"), &speScore);
10  if( FAILED(hr) ) return hr;
11
12  // Set the element's inner text
13  return speScore->put_innerText(varScore.bstrVal);
14}

Whenever the score changes, this function is used to update the contents of the HTML span object from the control’s C++ code.

Sinking WebBrowser Events

You might be tempted to use the SetScoreSpan function right from the OnCreate handler to set the initial score value when the control is activated. Unfortunately, the architecture of the HTML Document object dictates that we wait for the document to be completely processed before the object model is exposed to us. To detect when that happens, we need to sink events on the DWebBrowserEvents2 interface. Specifically, we need to know about the OnDocumentComplete event. When we receive this event, we can access all the named objects on the page. Because DWebBrowserEvents2 is a dispinterface, sinking the events can be accomplished with IDispEventImpl and an entry in the sink map:

 1typedef IDispEventImpl< 1, CSmartDartBoard,
 2  &DIID_DWebBrowserEvents2, &LIBID_SHDocVw, 1, 0>
 3  BrowserEvents;
 4
 5class ATL_NO_VTABLE CSmartDartBoard :
 6    public CComObjectRootEx<CComSingleThreadModel>,
 7    ...
 8    // Sink browser events
 9      public BrowserEvents {
10    ...
11BEGIN_SINK_MAP(CSmartDartBoard)
12  SINK_ENTRY_EX(1, DIID_DWebBrowserEvents2,
13    0x00000103, OnDocumentComplete)
14END_SINK_MAP()
15
16     void __stdcall OnDocumentComplete(IDispatch*, VARIANT*);
17    ...
18};

The OnCreate handler and the OnDestroy handler are good places to establish and shut down the DWebBrowserEvent2 connection point:

 1LRESULT CSmartDartBoard::OnCreate(UINT, WPARAM, LPARAM, BOOL&) {
 2  ...
 3  // Set up connection point w/ the browser
 4  if( SUCCEEDED(hr) )
 5    hr = BrowserEvents::DispEventAdvise(m_spBrowser);
 6  ...
 7  return SUCCEEDED(hr) ? 0 : -1;
 8}
 9LRESULT CSmartDartBoard::OnDestroy(UINT, WPARAM, LPARAM, BOOL&) {
10  DispEventUnadvise(m_spBrowser);
11  return 0;
12}

In the implementation of the OnDocumentComplete event handler, we can finally access the HTML object:

1void __stdcall CSmartDartBoard::OnDocumentComplete(IDispatch*,
2  VARIANT*) {
3  // Set the spanScore object's inner HTML
4  SetScoreSpan();
5}

Accessing the Control from the HTML

In addition to accessing the named HTML objects from the control, you might find yourself wanting to access the control from the HTML. For this to happen, the HTML must have some hook into the control. This is provided with the window.external property. The script can expect this to be another dispinterface of the control itself. In fact, the ATL Object Wizard generates two dual interfaces on an HTML control. The first, I<ControlName>, is the default interface available to the control’s clients. The second, I<ControlName>UI, is an interface given to the WebBrowser control via the SetExternalDispatch function on the CAxWindow object:

1HRESULT CAxWindow::SetExternalDispatch(IDispatch* pDisp);

The wizard-generated implementation of OnCreate sets this interface as part of the initialization procedure:

1LRESULT CSmartDartBoard::OnCreate(UINT, WPARAM, LPARAM, BOOL&) {
2...
3  if (SUCCEEDED(hr))
4    hr = wnd.SetExternalDispatch(static_cast<ISmartDartBoardUI*>(this));
5...
6  return SUCCEEDED(hr) ? 0 : -1;
7}

In the SmartDartBoard example, the interface used by control containers is ISmartDartBoard:

1[dual] interface ISmartDartBoard : IDispatch {
2  [propget] HRESULT Score([out, retval] long *pVal);
3  [propput] HRESULT Score([in] long newVal);
4  HRESULT ResetScore();
5};

On the other hand, the interface used by the HTML script code is ISmartDartBoardUI:

1[dual] interface ISmartDartBoardUI : IDispatch {
2  HRESULT AddToScore([in] long ringValue);
3  HRESULT ResetScore();
4};

This interface represents a bidirectional communication channel. A script block in the HTML can use this interface for whatever it wants. For example:

 1<table width=100% height=100%>
 2...
 3</table>
 4
 5<script language=vbscript>
 6    sub objBullsEye_OnScoreChanged(ringValue)
 7       ' Access the ISmartDartBoardUI interface
 8        window.external.AddToScore(ringValue)
 9    end sub
10
11    sub cmdReset_onClick
12       ' Access the ISmartDartBoardUI interface
13        window.external.ResetScore
14    end sub
15</script>

In this example, we’re using the ISmartDartBoardUI interface as a way to raise events to the control from the HTML, but instead of using connection points, we’re using the window’s external interface, which is far easier to set up.

Sinking HTML Element Events in C++

Notice that the previous HTML code handled events from the objects in the HTML itself. We’re using the object_event syntax of VBScript; for example, cmdReset_onClick is called when the cmdReset button is clicked. It probably doesn’t surprise you to learn that all the HTML objects fire events on an interface established via the connection-point protocol. There’s no reason we can’t sink the events from the HTML objects in our control directly instead of using the window.external interface to forward the events. For example, the cmdReset button fires events on the HTMLInputTextElementEvents dispinterface. Handling these events is, again, a matter of deriving from IDispEventImpl and adding entries to the sink map:

 1typedef IDispEventImpl<2, CSmartDartBoard,
 2  &DIID_HTMLInputTextElementEvents,
 3  &LIBID_MSHTML, 4, 0>
 4  ButtonEvents;
 5
 6class ATL_NO_VTABLE CSmartDartBoard :
 7    public CComObjectRootEx<CComSingleThreadModel>,
 8    ...
 9  // Sink events on the DHTML Reset button
10  public ButtonEvents {
11  ...
12BEGIN_SINK_MAP(CSmartDartBoard)
13  ...
14  SINK_ENTRY_EX(2, DIID_HTMLInputTextElementEvents, DISPID_CLICK,
15                OnClickReset)
16END_SINK_MAP()
17  VARIANT_BOOL __stdcall OnClickReset();
18  ...
19};

Because we need to have an interface on the cmdReset button, we need to wait until the OnDocumentComplete event to establish a connection point with the button:

 1void __stdcall CSmartDartBoard::OnDocumentComplete(IDispatch*,
 2  VARIANT*) {
 3  // Set the spanScore object's inner HTML
 4  SetScoreSpan();
 5  // Retrieve the Reset button
 6  HRESULT hr;
 7  CComPtr<IHTMLElement> speReset;
 8  hr = GetHtmlElement(OLESTR("cmdReset"), &speReset);
 9  if( FAILED(hr) ) return;
10
11  // Set up the connection point w/ the button
12  ButtonEvents::DispEventAdvise(speReset);
13}

When we’ve established the connection with the Reset button, every time the user clicks on it, we get a callback in our OnClickReset event handler. This means that we no longer need the cmdReset_onClick handler in the script. However, from a larger perspective, because we program and handle events back and forth between the C++ and the HTML code, we have the flexibility to use whichever is more convenient when writing the code. This is quite a contrast from a dialog resource, in which the resource is good for laying out the elements of the UI (as long as the UI was a fixed size), but only our C++ code can provide any behavior.

Extended UI Handling

It turns out that the external dispatch is but one setting you can set on the CAxHostWindow that affects the HTML Document control. Several more options can be set via the IAxWinAmbientDispatch interface implemented by CAxHostWindow:

 1typedef enum tagDocHostUIFlagDispatch {
 2  docHostUIFlagDIALOG            = 1,
 3  docHostUIFlagDISABLE_HELP_MENU = 2,
 4  docHostUIFlagNO3DBORDER        = 4,
 5  docHostUIFlagSCROLL_NO         = 8,
 6  docHostUIFlagDISABLE_SCRIPT_INACTIVE = 16,
 7  docHostUIFlagOPENNEWWIN        = 32,
 8  docHostUIFlagDISABLE_OFFSCREEN = 64,
 9  docHostUIFlagFLAT_SCROLLBAR = 128,
10  docHostUIFlagDIV_BLOCKDEFAULT = 256,
11  docHostUIFlagACTIVATE_CLIENTHIT_ONLY = 512,
12} DocHostUIFlagDispatch;
13
14typedef enum tagDOCHOSTUIDBLCLKDispatch {
15  docHostUIDblClkDEFAULT         = 0,
16  docHostUIDblClkSHOWPROPERTIES  = 1,
17  docHostUIDblClkSHOWCODE        = 2,
18} DOCHOSTUIDBLCLKDispatch;
19interface IAxWinAmbientDispatch : IDispatch {
20  ...
21  // IDocHostUIHandler Defaults
22  [propput, helpstring("Set the DOCHOSTUIFLAG flags")]
23  HRESULT DocHostFlags([in]DWORD dwDocHostFlags);
24  [propget, helpstring("Get the DOCHOSTUIFLAG flags")]
25  HRESULT DocHostFlags([out,retval]DWORD* pdwDocHostFlags);
26
27  [propput, helpstring("Set the DOCHOSTUIDBLCLK flags")]
28  HRESULT DocHostDoubleClickFlags([in]DWORD dwFlags);
29  [propget, helpstring("Get the DOCHOSTUIDBLCLK flags")]
30  HRESULT DocHostDoubleClickFlags([out,retval]DWORD* pdwFlags);
31
32  [propput, helpstring("Enable or disable context menus")]
33  HRESULT AllowContextMenu([in]VARIANT_BOOL bAllowContextMenu);
34  [propget, helpstring("Are context menus enabled")]
35  HRESULT AllowContextMenu([out,retval]VARIANT_BOOL* pbAllowContextMenu);
36
37  [propput, helpstring("Enable or disable UI")]
38  HRESULT AllowShowUI([in]VARIANT_BOOL bAllowShowUI);
39  [propget, helpstring("Is UI enabled")]
40  HRESULT AllowShowUI([out,retval]VARIANT_BOOL* pbAllowShowUI);
41
42  [propput, helpstring("Set the option key path")]
43  HRESULT OptionKeyPath([in]BSTR bstrOptionKeyPath);
44  [propget, helpstring("Get the option key path")]
45  HRESULT OptionKeyPath([out,retval]BSTR* pbstrOptionKeyPath);
46};

The DocHostFlags property can be any combination of DocHostUIFlagDispatch flags. The DocHostDoubleClickFlags property can be any one of the DOCHOSTUIDBLCLKDispatch flags. The AllowContextMenu property enables you to shut off the context menu when the user right-clicks on the HTML control. [10] The AllowShowUI property controls whether the host will be replacing the IE menus and toolbars. The OptionKeyPath property tells the HTML document where in the Registry to read and write its settings.

All these settings affect the CAxWindowHost object’s implementation of IDocHost-UIHandler, an interface that the HTML Document control expects from its host to fine tune its behavior. If you want to control this interaction even more, CAxWindowHost enables you to set your own IDocHostUIHandlerDispatch interface [11] via the CAxWindow member function SetExternalUIHandler:

1HRESULT CAxWindow::SetExternalUIHandler(
2    IDocHostUIHandlerDispatch* pHandler);

The IDocHostUIHandlerDispatch interface is pretty much a one-to-one mapping to IDocHostUIHandler, but in dual interface form. When a CAxHostWindow object gets a call from the HTML Document control on IDocHostUIHandler, the call is forwarded if there is an implementation of IDocHostUIHandlerDispatch set. For example, if you want to replace the context menu of the HTML control instead of just turn it off, you have to expose the IDocHostUIHandlerDispatch interface, call SetExternalUIHandler during the OnCreate handler, and implement the ShowContextMenu member function. When there is no implementation of IDocHostUIHandlerDispatch set, the CAxHostWindow object uses the properties set via the IAxWinAmbientDispatch interface.

Hosting HTML Is Not Limited to the HTML Control

By hosting the WebBrowser control or the HTML Document control, you’ve given yourself a lot more than just hosting HTML. The HTML Document control represents a flexible COM UI framework called Dynamic HTML. The combination of declarative statements to lay out the UI elements and the capability to control each element’s behavior, presentation style, and events at runtime gives you a great deal of flexibility.

Nor is this functionality limited to an HTML control. You can achieve the same effect by creating a CAxWindow object that hosts either the WebBrowser or the HTML Document control in a window, a dialog, or a composite control, as well as in an HTML control. [12]

ATL’s Control Containment Limitations

ATL introduced COM control containment in Version 3.0. Although several significant bugs have been fixed and the control-containment code is very capable, it’s not quite all you might like. Several limitations affect the way that ATL implements control containment:

  • All the control-hosting interfaces are exposed by a single class, CAxHostWindow, which acts as the site, the document, and the frame. The actual implementations of some of the hosting interfaces are split into other classes, but there are no template parameters or runtime variables that you can change to pick a different implementation. If you contain multiple controls, you get duplicate implementation of interfaces that could have been shared. This is particularly cumbersome if the control asks for the HWND for the document or the frame. Instead of allowing the client application programmer to designate his own document or frame window, ATL creates a new window, resulting in potentially three container windows per control. If the control creates its own window, that’s four windows instead of just one, to host a single control.

  • Each control must have its own window on the container side for the CAxHostWindow object to work with. Even a windowless control has one window associated with itnot exactly the windowless-ness for which one would hope.

  • Finally, unlike the rest of ATL, the control-containment architecture is not well factored. You can’t easily reach in and change how one piece works without also changing the other pieces. For example, if you want to have a shared frame for all the control sites, you have to replace CAxHostWindow (probably by deriving from it) and also change how CAxWindow hooks up the control site so that it uses your new class. Ideally, we would like to override some CAxHostWindow members but still be able to use the rest of the containment framework as is.

What do these limitations mean to you? For a lot of the work you’re likely to do with control containment, not much. This chapter has shown you the considerable use to which you can put ATL’s control-containment framework. However, if you’re looking for the C++ equivalent of a general-purpose, full-featured control container such as Visual Basic, ATL isn’t quite there. [13]

Summary

ATL provides the capability to host controls in windows, dialogs, and other controls. Control containment under ATL is based on two new window classes, AtlAxWin80 and AtlAxWinLic80. As wrappers around these window classes, ATL provides the CAxWindow and CAxWindow2 classes, respectively. After 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.

Many objects in ATL can make use of these window classes to host controls. Windows can use them manually via the CAxWindow class. Dialogs can use them automatically when using the CAxDialogImpl class. Controls can contain other controls in ATL when derived from CComCompositeControl. Finally, HTML controls can host the WebBrowser control, combining the best of the dialog resource declarative model and a full-featured COM UI framework model.