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

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.
You might also want to refer to the MSDN article “Notes on Implementing an OLE Control Container” for control containerspecific information at ` http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnaxctrl/html/msdn_contcntr.asp <http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnaxctrl/html/msdn_contcntr.asp>`__ (http://tinysells.com/60).
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

ATL’s implementation of the required container
interfaces is called CAxHostWindow
. [3]
This
class is defined in atlhost.h
.
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 byCAxHostWindow
.pStream
. Stream that holds object-initialization data. The control is initialized viaIPersistStreamInit
. IfpStream
is non-NULL
,Load
is called. Otherwise,InitNew
is called.ppUnk
. Filled with an interface to the newly created control.riidAdvise
. If notIID_NULL
,CAxHostWindow
attempts to set up a connection-point connection between the control and the sink object represented by thepunkAdvise
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 byriidAdvise
. TheCreateControlLicEx
method adds one more parameter:bstrLic
. This string contains the licensing key. If the string is empty, the control is created using the normalCoCreateInstance
call. If there is a licensing key in the string, the control is created via theIClassFactory2
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:
This is subclassing in the User32 sense, not the C++ sense.
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 |
{7DC59CC5-36C0-11D2-AC05-00A0C9C8E50D} |
Result of |
ProgID |
ATLInternals.BullsEye |
Result of |
URL |
|
|
Active document |
|
|
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:
Despite
the name, the CreateControlLicEx
function handles the
creation of COM controls with or without licensing information.
Setting the client site – that is, the
CAxHostWindow
’s implementation ofIOleClientSite
– via the control’s implementation ofIOleObject
.Calling either
InitNew
orLoad
(depending on whether thepStream
argument toAtlAxCreateControlEx
isNULL
or non-NULL
) via the control’s implementation ofIPersistStreamInit
.Passing the
CAxHostWindow
’s implementation ofIAdviseSink
to the control’s implementation ofIViewObject
.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]
Arguably, a window class whose function is to host COM controls should be part of the OS.
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:
The
scripting engines that Internet Explorer (IE) hosts allow you to
handle only events defined in a dispinterface
.
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
:
ATL’s
IDispatchImpl
can be used only to implement dual
interfaces.
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:
The
scripting engines that Internet Explorer hosts enable you to handle
events only on the default source interface as reported by
IProvideClassInfo2
.
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

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

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

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

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

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

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
andwidth
attributes, both in absolute pixels and percentagesSupport 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

Figure 12.10. Large ``SmartDartBoard`` UI

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.
There’s no reason for the user to know you’re just bootlegging IE functionality, is there?
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
:
This
interface is defined by ATL in atliface.idl
.
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]
You can build entire applications using ATL as the glue code that initialized a first-tier thin client built entirely from HTML, but that’s beyond the scope of this book.
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 theHWND
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 howCAxWindow
hooks up the control site so that it uses your new class. Ideally, we would like to override someCAxHostWindow
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]
For more information about bringing ATL’s COM control containment up to snuff, read “Extending ATL 3.0 Control Containment to Help You Write Real-world Containers” (MSJ, December 1999), at www.microsoft.com/msj/1299/Containment/Containment.aspx (http://tinysells.com/61).
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.