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.
ATL's implementation of the required container
interfaces is called CAxHostWindow.
// This class is not cocreateable
class ATL_NO_VTABLE CAxHostWindow :
public CComCoClass<CAxHostWindow, &CLSID_NULL>,
public CComObjectRootEx<CComSingleThreadModel>,
public CWindowImpl<CAxHostWindow>,
public IAxWinHostWindow,
public IOleClientSite,
public IOleInPlaceSiteWindowless,
public IOleControlSite,
public IOleContainer,
public IObjectWithSiteImpl<CAxHostWindow>,
public IServiceProvider,
public IAdviseSink,
#ifndef _ATL_NO_DOCHOSTUIHANDLER
public IDocHostUIHandler,
#endif
public IDispatchImpl<IAxWinAmbientDispatch,
&IID_IAxWinAmbientDispatch,
&LIBID_ATLLib>
{...};
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.
// This either registers a global class
// (if AtlAxWinInit is in ATL.DLL)
// or it registers a local class
ATLINLINE ATLAPI_(BOOL) AtlAxWinInit() {
CComCritSecLock<CComCriticalSection> lock(
_AtlWinModule.m_csWindowCreate, false);
if (FAILED(lock.Lock())) {
ATLTRACE(atlTraceHosting, 0,
_T("ERROR : Unable to lock critical section "
"in AtlAxWinInit\n"));
ATLASSERT(0);
return FALSE;
}
WM_ATLGETHOST = RegisterWindowMessage(_T("WM_ATLGETHOST"));
WM_ATLGETCONTROL = RegisterWindowMessage(
_T("WM_ATLGETCONTROL"));
WNDCLASSEX wc;
// first check if the class is already registered
wc.cbSize = sizeof(WNDCLASSEX);
BOOL bRet = ::GetClassInfoEx(
_AtlBaseModule.GetModuleInstance(),
CAxWindow::GetWndClassName(), &wc);
// register class if not
if(!bRet) {
wc.cbSize = sizeof(WNDCLASSEX);
#ifdef _ATL_DLL_IMPL
wc.style = CS_GLOBALCLASS | CS_DBLCLKS;
bAtlAxWinInitialized = true;
#else
wc.style = CS_DBLCLKS;
#endif
wc.lpfnWndProc = AtlAxWindowProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = _AtlBaseModule.GetModuleInstance();
wc.hIcon = NULL;
wc.hCursor = ::LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wc.lpszMenuName = NULL;
wc.lpszClassName =
CAxWindow::GetWndClassName(); // "AtlAxWin80"
wc.hIconSm = NULL;
ATOM atom= ::RegisterClassEx(&wc);
if(atom) {
_AtlWinModule.m_rgWindowClassAtoms.Add(atom);
bRet=TRUE;
} else {
bRet=FALSE;
}
}
if(bRet) {
// first check if the class is already registered
memset(&wc, 0, sizeof(WNDCLASSEX));
wc.cbSize = sizeof(WNDCLASSEX);
bRet = ::GetClassInfoEx(_AtlBaseModule.GetModuleInstance(),
CAxWindow2::GetWndClassName(), &wc);
// register class if not
if(!bRet) {
wc.cbSize = sizeof(WNDCLASSEX);
#ifdef _ATL_DLL_IMPL
wc.style = CS_GLOBALCLASS | CS_DBLCLKS;
#else
wc.style = CS_DBLCLKS;
#endif
wc.lpfnWndProc = AtlAxWindowProc2;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = _AtlBaseModule.GetModuleInstance();
wc.hIcon = NULL;
wc.hCursor = ::LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wc.lpszMenuName = NULL;
wc.lpszClassName =
CAxWindow2::GetWndClassName();//"AtlAxWinLic80"
wc.hIconSm = NULL;
ATOM atom= RegisterClassEx(&wc);
if (atom) {
_AtlWinModule.m_rgWindowClassAtoms.Add(atom);
bRet=TRUE;
} else {
bRet=FALSE;
}
}
}
return bRet;
}
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":
class CMainWindow : public CWindowImpl<CMainWindow, ...> {
...
LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam,
BOOL& lResult) {
// Create the host window, the CAxHostWindow object, and
// the BullsEye control, and host the control
RECT rect; GetClientRect(&rect);
LPCTSTR pszName = __T("ATLInternals.BullsEye");
HWND hwndContainer = m_ax.Create(__T("AtlAxWin80"),
m_hWnd, rect, pszName, WS_CHILD | WS_VISIBLE);
if( !hwndContainer ) return -1;
return 0;
}
private:
CWindow m_ax;
};
The creation of the CAxHostWindow
object and the corresponding control is initiated in the
WM_CREATE handler of the AtlAxWin80 window
procedure AtlAxWindowProc:
static LRESULT CALLBACK
AtlAxWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam,
LPARAM lParam) {
switch(uMsg) {
case WM_CREATE: {
// create control from a PROGID in the title
// This is to make sure drag drop works
::OleInitialize(NULL);
CREATESTRUCT* lpCreate = (CREATESTRUCT*)lParam;
int nLen = ::GetWindowTextLength(hWnd);
CAutoStackPtr<TCHAR> spName(
(TCHAR *)_malloca((nLen + 1) * sizeof(TCHAR)));
if(!spName) {
return -1;
}
// Extract window text to be used as name of control to host
::GetWindowText(hWnd, spName, nLen + 1);
::SetWindowText(hWnd, _T(""));
IAxWinHostWindow* pAxWindow = NULL;
...
USES_CONVERSION_EX;
CComPtr<IUnknown> spUnk;
// Create AxHostWindow instance and host the control
HRESULT hRet = AtlAxCreateControlLic(T2COLE_EX_DEF(spName),
hWnd, spStream, &spUnk, NULL);
if(FAILED(hRet)) return -1; // abort window creation
hRet = spUnk->QueryInterface(__uuidof(IAxWinHostWindow),
(void**)&pAxWindow);
if(FAILED(hRet)) return -1; // abort window creation
// Keep a CAxHostWindow interface pointer in the window's
// user data
::SetWindowLongPtr(hWnd, GWLP_USERDATA,
(DWORD_PTR)pAxWindow);
// continue with DefWindowProc
}
break;
case WM_NCDESTROY: {
IAxWinHostWindow* pAxWindow =
(IAxWinHostWindow*)::GetWindowLongPtr(hWnd, GWLP_USERDATA);
// When window goes away, release the host (and the control)
if(pAxWindow != NULL) pAxWindow->Release();
OleUninitialize();
}
break;
case WM_PARENTNOTIFY: {
if((UINT)wParam == WM_CREATE) {
ATLASSERT(lParam);
// Set the control parent style for the AxWindow
DWORD dwExStyle = ::GetWindowLong((HWND)lParam,
GWL_EXSTYLE);
if(dwExStyle & WS_EX_CONTROLPARENT) {
dwExStyle = ::GetWindowLong(hWnd, GWL_EXSTYLE);
dwExStyle |= WS_EX_CONTROLPARENT;
::SetWindowLong(hWnd, GWL_EXSTYLE, dwExStyle);
}
}
}
break;
default:
break;
}
return ::DefWindowProc(hWnd, uMsg, wParam, lParam);
}
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:
ATLINLINE ATLAPI AtlAxCreateControlLicEx(
LPCOLESTR lpszName,
HWND hWnd,
IStream* pStream,
IUnknown** ppUnkContainer,
IUnknown** ppUnkControl,
REFIID iidSink,
IUnknown* punkSink,
BSTR bstrLic) {
AtlAxWinInit();
HRESULT hr;
CComPtr<IUnknown> spUnkContainer;
CComPtr<IUnknown> spUnkControl;
hr = CAxHostWindow::_CreatorClass::CreateInstance(NULL,
__uuidof(IUnknown), (void**)&spUnkContainer);
if(SUCCEEDED(hr)) {
CComPtr<IAxWinHostWindowLic> pAxWindow;
spUnkContainer->QueryInterface(__uuidof(IAxWinHostWindow),
(void**)&pAxWindow);
CComBSTR bstrName(lpszName);
hr = pAxWindow->CreateControlLicEx(bstrName, hWnd, pStream,
&spUnkControl, iidSink, punkSink, bstrLic);
}
if(ppUnkContainer != NULL) {
if (SUCCEEDED(hr)) {
*ppUnkContainer = spUnkContainer.p;
spUnkContainer.p = NULL;
}
else
*ppUnkContainer = NULL;
}
if (ppUnkControl != NULL) {
if (SUCCEEDED(hr)) {
*ppUnkControl = SUCCEEDED(hr) ? spUnkControl.p : NULL;
spUnkControl.p = NULL;
}
else
*ppUnkControl = NULL;
}
return hr;
}
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:
interface IAxWinHostWindow : IUnknown {
HRESULT CreateControl([in] LPCOLESTR lpTricsData,
[in] HWND hWnd, [in] IStream* pStream);
HRESULT CreateControlEx([in] LPCOLESTR lpTricsData,
[in] HWND hWnd, [in] IStream* pStream,
[out] IUnknown** ppUnk,
[in] REFIID riidAdvise, [in] IUnknown* punkAdvise);
HRESULT AttachControl([in] IUnknown* pUnkControl,
[in] HWND hWnd);
HRESULT QueryControl([in] REFIID riid,
[out, iid_is(riid)] void **ppvObject);
HRESULT SetExternalDispatch([in] IDispatch* pDisp);
HRESULT SetExternalUIHandler(
[in] IDocHostUIHandlerDispatch* pDisp);
};
A second interface,
IAxWinHostWindowLic, derives from
IAxWinHostWindow and supports creating licensed
controls:
interface IAxWinHostWindowLic : IAxWinHostWindow {
HRESULT CreateControlLic([in] LPCOLESTR lpTricsData,
[in] HWND hWnd, [in] IStream* pStream, [in] BSTR bstrLic);
HRESULT CreateControlLicEx([in] LPCOLESTR lpTricsData,
[in] HWND hWnd, [in] IStream* pStream,
[out]IUnknown** ppUnk, [in] REFIID riidAdvise,
[in]IUnknown* punkAdvise, [in] BSTR bstrLic);
};
To create a new control in
CAxHostWindow, IAxWinHostWindow provides
CreateControl or CreateControlEx, which
AtlAxCreateControlEx then uses after the
CAxHostWindow object is created. The parameters for
CreateControl[Ex] are as follows:
-
lpTricsData. The name of the control
to create. It can take the form of a CLSID, a ProgID, a URL, a
filename, or raw HTML. We discuss this more later.
-
hWnd.
Parent window in which to host the control. This window is
subclassed by CAxHostWindow.
-
pStream. Stream that holds
object-initialization data. The control is initialized via
IPersistStreamInit. If pStream is
non-NULL, Load is called. Otherwise,
InitNew is called.
-
ppUnk. Filled with an interface to
the newly created control.
-
riidAdvise. If not
IID_NULL, CAxHostWindow attempts to set up a
connection-point connection between the control and the sink object
represented by the punkAdvise parameter.
CAxHostWindow manages the resultant cookie and tears down
the connection when the control is destroyed.
-
punkAdvise. An
interface to the sink object that implements the sink interface
specified by riidAdvise.
The CreateControlLicEx method adds one
more parameter:
-
bstrLic. This string contains the
licensing key. If the string is empty, the control is created using
the normal CoCreateInstance call. If there is a licensing key in the string, the control
is created via the IClassFactory2 interface, which
supports creating licensed controls.
The AttachControl method of the
IAxWinHostWindow method attaches a control that has
already been created and initialized to an existing
CAxHostWindow object. The QueryControl method
allows access to the control's interfaces being hosted by the
CAxHostWindow object. Both SetExternalDispatch
and SetExternalUIHandler are used when hosting the
Internet Explorer HTML control and will be discussed at the end of
the chapter in the HTML Controls section.
CreateControlLicEx
CAxHostWindow's implementation of
CreateControlLicEx subclasses 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:
STDMETHODIMP CAxHostWindow::CreateControlLicEx(
LPCOLESTR lpszTricsData,
HWND hWnd,
IStream* pStream,
IUnknown** ppUnk,
REFIID iidAdvise,
IUnknown* punkSink,
BSTR bstrLic)
{
HRESULT hr = S_FALSE;
// Used to keep track of whether we subclass the window
bool bReleaseWindowOnFailure = false;
// Release previously held control
ReleaseAll();
...
if (::IsWindow(hWnd)) {
if (m_hWnd != hWnd) { // Don't need to subclass the window
// if we already own it
// Route all messages to CAxHostWindow
SubclassWindow(hWnd);
bReleaseWindowOnFailure = true;
}
...
bool bWasHTML = false;
// Create control based on lpszTricsData
hr = CreateNormalizedObject(lpszTricsData,
__uuidof(IUnknown),
(void**)ppUnk, bWasHTML, bstrLic);
// Activate the control
if (SUCCEEDED(hr)) hr = ActivateAx(*ppUnk, false, pStream);
// Try to hook up any sink the user might have given us.
m_iidSink = iidAdvise;
if(SUCCEEDED(hr) && *ppUnk && punkSink) {
AtlAdvise(*ppUnk, punkSink, m_iidSink, &m_dwAdviseSink);
}
...
// If raw HTML, give HTML control its HTML
...
// If it's an URL, navigate the Browser control to the URL
...
if (FAILED(hr) || m_spUnknown == NULL) {
// We don't have a control or something failed so release
ReleaseAll();
...
}
}
return hr;
}
How the control name is interpreted depends on
another function, called CreateNormalizedObject. The
actual COM activation process is handled by the ActivateAx
function (discussed shortly).
CreateNormalizedObject
The
CreateNormalizedObject function creates an instance of a
COM object using strings of the form shown in Table 12.1.
Table 12.1. String Formats Understood by
CreateNormalizedObject
Type
|
Example
|
CLSID of Created Object
|
HTML
|
mshtml:<body><b>Wow!</b></body>
|
CLSID_HTMLDocument
|
CLSID
|
{7DC59CC5-36C0-11D2-AC05-00A0C9C8E50D}
|
Result of CLSIDFromString
|
ProgID
|
ATLInternals.BullsEye
|
Result of CLSIDFromProgID
|
URL
|
http://www.awl.com
res://htmlapp.exe/main.htm
|
CLSID_WebBrowser
|
Active document
|
D:\Atl Internals\10 Controls.doc
file://D:\Atl Internals\10 Controls.doc
|
CLSID_WebBrowser
|
Because CAxHostWindow uses the title of
the window to obtain the name passed to
CreateNormalizedObject, you can use any of these string
formats when creating an instance of the AtlWinInit window
class.
If no license key is supplied,
CreateNormalizedObject uses CoCreateInstance to
instantiate the object. If there is a license key,
CreateNormalizedObject instead calls
CoGetClassFactory and uses the factory's
IClassFactory2::CreateInstanceLic method to create the
control using the given license key.
ActivateAx
The ActivateAx function is the part of
the control-creation process that really performs the magic. Called
after CreateControlLicEx actually creates the
underlying COM object, ActivateAx takes an interface
pointer from the object that CreateNormalizedObject
creates and activates it as a COM control in the parent window.
ActivateAx is responsible for the following:
-
Setting the client sitethat is, the
CAxHostWindow's implementation of
IOleClientSitevia the control's implementation of
IOleObject.
-
Calling either
InitNew or Load (depending on whether the
pStream argument to AtlAxCreateControlEx is
NULL or non-NULL) via the control's
implementation of IPersistStreamInit.
-
Passing the CAxHostWindow's
implementation of IAdviseSink to the control's
implementation of IViewObject.
-
Setting the control's size to the size of the
parent window, also via the control's implementation of
IOleObject.
-
Finally, to show the control and allow it to
handle input and output, calling
DoVerb(OLEIVERB_INPLACEACTIVATE) via the control's
implementation of, again, IOleObject.
This process completes the activation of the
control. However, creating an instance of CAxHostWindow
via direct reference to the AtlAxWin80 window class is not
typical. The implementation details of AtlAxWin80 and
CAxHostWindow are meant to be hidden from the average ATL
programmer. The usual way a control is hosted under ATL is via an
instance of a wrapper class. Two such wrappers exist in ATL:
CAxWindow and CAxWindow2.
CAxWindow
CAxWindow simplifies the use of
CAxHostWindow with a set of wrapper functions. The initial
creation part of CAxWindow class is defined as
follows:
#define ATLAXWIN_CLASS "AtlAxWin80"
template <class TBase /* = CWindow */> class CAxWindowT :
public TBase {
public:
// Constructors
CAxWindowT(HWND hWnd = NULL) : TBase(hWnd) { AtlAxWinInit(); }
CAxWindowT< TBase >& operator=(HWND hWnd) {
m_hWnd = hWnd; return *this;
}
// Attributes
static LPCTSTR GetWndClassName() { return _T(ATLAXWIN_CLASS); }
// Operations
HWND Create(HWND hWndParent, _U_RECT rect = NULL, ...) {
return CWindow::Create(GetWndClassName(), hWndParent,
rect, ... );
}
...
};
typedef 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:
class CMainWindow : public CWindowImpl<CMainWindow, ...> {
...
LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam,
BOOL& lResult) {
// Create the host window, the CAxHostWindow object, and
// the BullsEye control, and host the control
RECT rect; GetClientRect(&rect);
LPCTSTR pszName = __T("ATLInternals.BullsEye");
HWND hwndContainer = m_ax.Create(m_hWnd, rect,
pszName, WS_CHILD | WS_VISIBLE);
if( !hwndContainer ) return -1;
return 0;
}
private:
CAxWindow m_ax;
};
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.
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:
template <class TBase> class CAxWindowT : public TBase {
public:
...
HRESULT CreateControl(LPCOLESTR lpszName,
IStream* pStream = NULL,
IUnknown** ppUnkContainer = NULL) {
return CreateControlEx(lpszName, pStream, ppUnkContainer);
}
HRESULT CreateControl(DWORD dwResID, IStream* pStream = NULL,
IUnknown** ppUnkContainer = NULL) {
return CreateControlEx(dwResID, pStream, ppUnkContainer);
}
HRESULT CreateControlEx(LPCOLESTR lpszName,
IStream* pStream = NULL,
IUnknown** ppUnkContainer = NULL,
IUnknown** ppUnkControl = NULL,
REFIID iidSink = IID_NULL, IUnknown* punkSink = NULL) {
ATLASSERT(::IsWindow(m_hWnd));
// We must have a valid window!
// Get a pointer to the container object
// connected to this window
CComPtr<IAxWinHostWindow> spWinHost;
HRESULT hr = QueryHost(&spWinHost);
// If QueryHost failed, there is no host attached to this
// window. We assume that the user wants to create a new
// host and subclass the current window
if (FAILED(hr)) {
return AtlAxCreateControlEx(lpszName, m_hWnd, pStream,
ppUnkContainer, ppUnkControl, iidSink, punkSink);
}
// Create the control requested by the caller
CComPtr<IUnknown> pControl;
if (SUCCEEDED(hr)) {
hr = spWinHost->CreateControlEx(lpszName, m_hWnd, pStream,
&pControl, iidSink, punkSink);
}
// Send back the necessary interface pointers
if (SUCCEEDED(hr)) {
if (ppUnkControl) { *ppUnkControl = pControl.Detach(); }
if (ppUnkContainer) {
hr = spWinHost.QueryInterface(ppUnkContainer);
ATLASSERT(SUCCEEDED(hr)); // This should not fail!
}
}
return hr;
}
HRESULT CreateControlEx(DWORD dwResID, IStream* pStream = NULL,
IUnknown** ppUnkContainer = NULL,
IUnknown** ppUnkControl = NULL,
REFIID iidSink = IID_NULL, IUnknown* punkSink = NULL) {
TCHAR szModule[MAX_PATH];
DWORD dwFLen =
GetModuleFileName(_AtlBaseModule.GetModuleInstance(),
szModule, MAX_PATH);
if( dwFLen == 0 ) return AtlHresultFromLastError();
else if( dwFLen == MAX_PATH )
return HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER);
CComBSTR bstrURL(OLESTR("res://"));
HRESULT hr=bstrURL.Append(szModule);
if(FAILED(hr)) { return hr; }
hr=bstrURL.Append(OLESTR("/"));
if(FAILED(hr)) { return hr; }
TCHAR szResID[11];
#if _SECURE_ATL && !defined(_ATL_MIN_CRT)
if (_stprintf_s(szResID, _countof(szResID),
_T("%0d"), dwResID) == -1) {
return HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER);
}
#else
wsprintf(szResID, _T("%0d"), dwResID);
#endif
hr=bstrURL.Append(szResID);
if(FAILED(hr)) { return hr; }
ATLASSERT(::IsWindow(m_hWnd));
return CreateControlEx(bstrURL, pStream, ppUnkContainer,
ppUnkControl, iidSink, punkSink);
}
...
};
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:
LRESULT
CMainWindow::OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam,
BOOL& lResult) {
RECT rect; GetClientRect(&rect);
// Phase one: Create the container (passing a null (0)
// window title)
// m_ax is declared in the CMainWindow class as:
// CAxWindow m_ax;
HWND hwndContainer = m_ax.Create(m_hWnd, rect, 0,
WS_CHILD | WS_VISIBLE);
if( !hwndContainer ) return -1;
// Phase two: Create the control
LPCOLESTR pszName = OLESTR("ATLInternals.BullsEye");
HRESULT hr = m_ax.CreateControl(pszName);
return (SUCCEEDED(hr) ? 0 : -1);
};
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:
template <class TBase = CWindow> class CAxWindowT :
public TBase {
public:
...
HRESULT AttachControl(IUnknown* pControl,
IUnknown** ppUnkContainer) {
ATLASSERT(::IsWindow(m_hWnd));
// We must have a valid window!
// Get a pointer to the container object connected
// to this window
CComPtr<IAxWinHostWindow> spWinHost;
HRESULT hr = QueryHost(&spWinHost);
// If QueryHost failed, there is no host attached
// to this window. We assume that the user wants to
// create a new host and subclass the current window
if (FAILED(hr))
return AtlAxAttachControl(pControl, m_hWnd,
ppUnkContainer);
// Attach the control specified by the caller
if (SUCCEEDED(hr))
hr = spWinHost->AttachControl(pControl, m_hWnd);
// Get the IUnknown interface of the container
if (SUCCEEDED(hr) && ppUnkContainer) {
hr = spWinHost.QueryInterface(ppUnkContainer);
ATLASSERT(SUCCEEDED(hr)); // This should not fail!
}
return hr;
}
...
};
AttachControl is used like this:
LRESULT
CMainWindow::OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam,
BOOL& lResult) {
RECT rect; GetClientRect(&rect);
// Phase one: Create the container
HWND hwndContainer = m_ax.Create(m_hWnd, rect, 0,
WS_CHILD | WS_VISIBLE);
if( !hwndContainer ) return -1;
// Create and initialize a control
CComPtr<IUnknown> spunkControl; // ...
// Phase two: Attach an existing control
HRESULT hr = m_ax.AttachControl(spunkControl);
return (SUCCEEDED(hr) ? 0 : -1);
};
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:
static LRESULT CALLBACK AtlAxWindowProc2(HWND hWnd, UINT uMsg,
WPARAM wParam, LPARAM lParam) {
switch(uMsg) {
case WM_CREATE: {
// ... same as AtlAxWindowProc ...
// Format of data in lpCreateParams
// int nCreateSize; // size of Create data in bytes
// WORD nMsg; // constant used to indicate type
// of DLGINIT data.
// See _DialogSplitHelper for values.
// DWORD dwLen; // Length of data stored for control
// in DLGINIT format in bytes.
// DWORD cchLicKey; // Length of license key in OLECHAR's
// OLECHAR *szLicKey; // This will be present only if
// cchLicKey is greater than 0.
// This is of variable length and will
// contain cchLicKey OLECHAR's
// that represent the license key.
// The following two fields will be present only if nMsg is
// WM_OCC_LOADFROMSTREAM_EX or WM_OCC_LOADFROMSTORAGE_EX.
// If present this information will be ignored since
// databinding is not supported.
// ULONG cbDataBinding; // Length of databinding
// information in bytes.
// BYTE *pbDataBindingInfo // cbDataBinding bytes that contain
// databinding information
// BYTE *pbControlData; // Actual control data persisted
// by the control.
// ... Load persistence data into stream ...
CComBSTR bstrLicKey;
HRESULT hRet = _DialogSplitHelper::ParseInitData(spStream,
&bstrLicKey.m_str);
if (FAILED(hRet)) return -1;
USES_CONVERSION_EX;
CComPtr<IUnknown> spUnk;
hRet = AtlAxCreateControlLic(T2COLE_EX_DEF(spName), hWnd,
spStream, &spUnk, bstrLicKey);
if(FAILED(hRet)) {
return 1; // abort window creation
}
hRet = spUnk->QueryInterface(__uuidof(IAxWinHostWindowLic),
(void**)&pAxWindow);
if(FAILED(hRet)) return -1; // abort window creation
::SetWindowLongPtr(hWnd, GWLP_USERDATA,
(DWORD_PTR)pAxWindow);
// continue with DefWindowProc
}
break;
// ... Rest is the same as AtlAxWindowProc ...
}
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:
template <class TBase /* = CWindow */>
class CAxWindow2T : public CAxWindowT<TBase> {
public:
// Constructors
CAxWindow2T(HWND hWnd = NULL) : CAxWindowT<TBase>(hWnd) { }
CAxWindow2T< TBase >& operator=(HWND hWnd);
// Attributes
static LPCTSTR GetWndClassName() {
return _T(ATLAXWINLIC_CLASS);
}
// Operations
HWND Create(HWND hWndParent, _U_RECT rect = NULL,
LPCTSTR szWindowName = NULL,
DWORD dwStyle = 0, DWORD dwExStyle = 0,
_U_MENUorID MenuOrID = 0U, LPVOID lpCreateParam = NULL) {
return CWindow::Create(GetWndClassName(), hWndParent, rect,
szWindowName, dwStyle, dwExStyle, MenuOrID, lpCreateParam);
}
HRESULT CreateControlLic(LPCOLESTR lpszName,
IStream* pStream = NULL,
IUnknown** ppUnkContainer = NULL, BSTR bstrLicKey = NULL) {
return CreateControlLicEx(lpszName, pStream, ppUnkContainer,
NULL, IID_NULL, NULL, bstrLicKey);
}
HRESULT CreateControlLic(DWORD dwResID,
IStream* pStream = NULL,
IUnknown** ppUnkContainer = NULL, BSTR bstrLicKey = NULL) {
return CreateControlLicEx(dwResID, pStream, ppUnkContainer,
NULL, IID_NULL, NULL, bstrLicKey);
}
HRESULT CreateControlLicEx(LPCOLESTR lpszName,
IStream* pStream = NULL,
IUnknown** ppUnkContainer = NULL,
IUnknown** ppUnkControl = NULL,
REFIID iidSink = IID_NULL, IUnknown* punkSink = NULL,
BSTR bstrLicKey = NULL);
HRESULT CreateControlLicEx(DWORD dwResID,
IStream* pStream = NULL,
IUnknown** ppUnkContainer = NULL,
IUnknown** ppUnkControl = NULL,
REFIID iidSink = IID_NULL, IUnknown* punkSink = NULL,
BSTR bstrLickey = NULL);
};
typedef 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:
class CMainWindow : public CWindowImpl<CMainWindow, ...> {
...
LRESULT OnSize(UINT, WPARAM, LPARAM lParam, BOOL&) {
if( m_ax ) { // m_ax will be Create'd earlier, e.g. WM_CREATE
RECT rect = { 0, 0, LOWORD(lParam), HIWORD(lParam) };
m_ax.MoveWindow(&rect); // Resize the control
}
return 0;
}
private:
CAxWindow m_ax;
};
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:
template <class TBase = CWindow> class CAxWindowT :
public TBase {
public:
...
HRESULT QueryControl(REFIID iid, void** ppUnk) {
CComPtr<IUnknown> spUnk;
HRESULT hr = AtlAxGetControl(m_hWnd, &spUnk);
if (SUCCEEDED(hr)) hr = spUnk->QueryInterface(iid, ppUnk);
return hr;
}
template <class Q> HRESULT QueryControl(Q** ppUnk)
{ return QueryControl(__uuidof(Q), (void**)ppUnk); }
};
Like QueryHost, QueryControl
uses a global function (AtlAxGetControl, in this case)
that sends a window message to the 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:
// Import interface definitions for BullsEye
#import "D:\ATLBook\src\atlinternals\Debug\BullsEyeCtl.dll" \
raw_interfaces_only raw_native_types no_namespace named_guids
LRESULT
CMainWindow::OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam,
BOOL& lResult) {
// Create the control
...
// Set initial BullsEye properties
CComPtr<IBullsEye> spBullsEye;
HRESULT hr = m_ax.QueryControl(&spBullsEye);
if( SUCCEEDED(hr) ) {
spBullsEye->put_Beep(VARIANT_TRUE);
spBullsEye->put_CenterColor(RGB(0, 0, 255));
}
return 0;
};
Notice the use of the
#import statement to pull in the definitions of the
interfaces of the control you're programming against. This is
necessary if you 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 attributesfor 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. For example, the BullsEye
control from the last chapter defined the following event
interface:
const int DISPID_ONRINGHIT = 1;
const int DISPID_ONSCORECHANGED = 2;
dispinterface _IBullsEyeEvents {
properties:
methods:
[id(DISPID_ONRINGHIT)]
void OnRingHit(short ringNumber);
[id(DISPID_ONSCORECHANGED)]
void OnScoreChanged(long ringValue);
};
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. However, as you recall from Chapter 9, "Connection Points,"
ATL provides a helper class called IDispEventImpl for
implementing an event dispinterface:
template <UINT nID, class T, const IID* pdiid = &IID_NULL,
const GUID* plibid = &GUID_NULL,
WORD wMajor = 0, WORD wMinor = 0,
class tihclass = CComTypeInfoHolder>
class ATL_NO_VTABLE IDispEventImpl :
public IDispEventSimpleImpl<nID, T, pdiid> {...};
IDispEventImpl uses a data structure
called a sink map, established via the following macros:
#define BEGIN_SINK_MAP(_class) ...
#define SINK_ENTRY_INFO(id, iid, dispid, fn, info) ...
#define SINK_ENTRY_EX(id, iid, dispid, fn) ...
#define SINK_ENTRY(id, dispid, fn) ...
#define END_SINK_MAP() ...
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 IDthat 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:
const UINT ID_BULLSEYE = 1;
class CMainWindow :
public CWindowImpl<CMainWindow, CWindow, CMainWindowTraits>,
public IDispEventImpl<ID_BULLSEYE,
CMainWindow,
&DIID__IBullsEyeEvents,
&LIBID_BullsEyeLib, 1, 0>
{
public:
...
LRESULT OnCreate(...) {
RECT rect; GetClientRect(&rect);
m_ax.Create(m_hWnd, rect, __T("AtlInternals.BullsEye"),
WS_CHILD | WS_VISIBLE, 0, ID_BULLSEYE);
...
return (m_ax.m_hWnd ? 0 : -1);
}
BEGIN_SINK_MAP(CMainWindow)
SINK_ENTRY_EX(ID_BULLSEYE, DIID__IBullsEyeEvents,
1, OnRingHit)
SINK_ENTRY_EX(ID_BULLSEYE, DIID__IBullsEyeEvents,
2, OnScoreChanged)
END_SINK_MAP()
void __stdcall OnRingHit(short nRingNumber);
void __stdcall OnScoreChanged(LONG ringValue);
private:
CAxWindow m_ax;
};
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
VARIANTs 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 conventionthat 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:
template <class T>
inline HRESULT AtlAdviseSinkMap(T* pT, bool bAdvise)
The first argument to AtlAdviseSinkMap
is a pointer to the object that wants to set up the connection
points with the objects listed in the sink map. The second
parameter is a Boolean determining 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:
LRESULT CMainWindow::OnCreate(...) {
... // Create the controls
AtlAdviseSinkMap(this, true); // Establish connection points
return 0;
}
LRESULT CMainWindow::OnDestroy(...) {
// Controls still live
AtlAdviseSinkMap(this, false); // Tear down connection points
return 0;
}
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 and can be published in the
coclass statement in the IDL (and, therefore, as part of
the type library). For example:
coclass BullsEye {
[default] interface IBullsEye;
[default, source] dispinterface _IBullsEyeEvents;
};
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:
#import "D:\ATLBook\src\atlinternals\Debug\BullsEyeCtl.dll" \
raw_interfaces_only raw_native_types no_namespace named_guids
#define ID_BULLSEYE 1
class CMainWindow :
public CWindowImpl<CMainWindow, CWindow, CMainWindowTraits>,
// Sink the default source interface
public IDispEventImpl< ID_BULLSEYE, CMainWindow> {
...
LRESULT OnCreate(...) {
RECT rect; GetClientRect(&rect);
m_ax.Create(m_hWnd, rect, __T("AtlInternals.BullsEye"),
WS_CHILD | WS_VISIBLE, 0, ID_BULLSEYE);
AtlAdviseSinkMap(this, true);
return (m_ax.m_hWnd ? 0 : -1);
}
LRESULT CMainWindow::OnDestroy(...)
{ AtlAdviseSinkMap(this, false); return 0; }
BEGIN_SINK_MAP(CMainWindow)
// Sink events from the default BullsEye event interface
SINK_ENTRY(ID_BULLSEYE, 1, OnRingHit)
SINK_ENTRY(ID_BULLSEYE, 2, OnScoreChanged)
END_SINK_MAP()
void __stdcall OnRingHit(short nRingNumber);
void __stdcall OnScoreChanged(LONG ringValue);
private:
CAxWindow m_ax;
};
Property
Changes
In
addition to a custom event interface, controls often source events
on the IPropertyNotifySink interface:
interface IPropertyNotifySink : IUnknown {
HRESULT OnChanged([in] DISPID dispID);
HRESULT OnRequestEdit([in] DISPID dispID);
}
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:
class CMainWindow :
public CWindowImpl<CMainWindow, ...>,
public IPropertyNotifySink {
public:
...
// IUnknown, assuming an instance on the stack
STDMETHODIMP QueryInterface(REFIID riid, void** ppv) {
if( riid == IID_IUnknown || riid == IID_IPropertyNotifySink )
*ppv = static_cast<IPropertyNotifySink*>(this);
else return *ppv = 0, E_NOINTERFACE;
return reinterpret_cast<IUnknown*>(*ppv)->AddRef(), S_OK;
}
STDMETHODIMP_(ULONG) AddRef() { return 2; }
STDMETHODIMP_(ULONG) Release() { return 1; }
// IPropertyNotifySink
STDMETHODIMP OnRequestEdit(DISPID dispID) { return S_OK; }
STDMETHODIMP OnChanged(DISPID dispID) {
m_bDirty = true; return S_OK;
}
private:
CAxControl m_ax;
bool m_bDirty;
};
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:
LRESULT CMainWindow::OnCreate(...) {
... // Create the control
// Set up IPropertyNotifySink connection point
CComPtr<IUnknown> spunkControl;
m_ax.QueryControl(spunkControl);
AtlAdvise(spunkControl, this, IID_IPropertyNotifySink,
&m_dwCookie);
return 0;
}
LRESULT CMainWindow::OnDestroy(...) {
// Tear down IPropertyNotifySink connection point
CComPtr<IUnknown> spunkControl;
m_ax.QueryControl(spunkControl);
AtlUnadvise(spunkControl, IID_IPropertyNotifySink,
&m_dwCookie);
return 0;
}
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:
LRESULT CMainWindow::OnCreate(...) {
... // Create the control host
// Create the control and set up IPropertyNotifySink
// connection point
m_ax.CreateControlEx(OLESTR("AtlInternals.BullsEye"), 0, 0, 0,
IID_IPropertyNotifySink, this);
return 0;
}
The connection-point cookie for
IPropertyNotifySink 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:
interface IAxWinAmbientDispatch : IDispatch {
[propput]
HRESULT AllowWindowlessActivation([in]VARIANT_BOOL b);
[propget]
HRESULT AllowWindowlessActivation(
[out,retval]VARIANT_BOOL* pb);
// DISPID_AMBIENT_BACKCOLOR
[propput, id(DISPID_AMBIENT_BACKCOLOR)]
HRESULT BackColor([in]OLE_COLOR clrBackground);
[propget, id(DISPID_AMBIENT_BACKCOLOR)]
HRESULT BackColor([out,retval]OLE_COLOR* pclrBackground);
// DISPID_AMBIENT_FORECOLOR
[propput, id(DISPID_AMBIENT_FORECOLOR)]
HRESULT ForeColor([in]OLE_COLOR clrForeground);
[propget, id(DISPID_AMBIENT_FORECOLOR)]
HRESULT ForeColor([out,retval]OLE_COLOR* pclrForeground);
// DISPID_AMBIENT_LOCALEID
[propput, id(DISPID_AMBIENT_LOCALEID)]
HRESULT LocaleID([in]LCID lcidLocaleID);
[propget, id(DISPID_AMBIENT_LOCALEID)]
HRESULT LocaleID([out,retval]LCID* plcidLocaleID);
// DISPID_AMBIENT_USERMODE
[propput, id(DISPID_AMBIENT_USERMODE)]
HRESULT UserMode([in]VARIANT_BOOL bUserMode);
[propget, id(DISPID_AMBIENT_USERMODE)]
HRESULT UserMode([out,retval]VARIANT_BOOL* pbUserMode);
// DISPID_AMBIENT_DISPLAYASDEFAULT
[propput, id(DISPID_AMBIENT_DISPLAYASDEFAULT)]
HRESULT DisplayAsDefault([in]VARIANT_BOOL bDisplayAsDefault);
[propget, id(DISPID_AMBIENT_DISPLAYASDEFAULT)]
HRESULT DisplayAsDefault(
[out,retval]VARIANT_BOOL* pbDisplayAsDefault);
// DISPID_AMBIENT_FONT
[propput, id(DISPID_AMBIENT_FONT)]
HRESULT Font([in]IFontDisp* pFont);
[propget, id(DISPID_AMBIENT_FONT)]
HRESULT Font([out,retval]IFontDisp** pFont);
// DISPID_AMBIENT_MESSAGEREFLECT
[propput, id(DISPID_AMBIENT_MESSAGEREFLECT)]
HRESULT MessageReflect([in]VARIANT_BOOL bMsgReflect);
[propget, id(DISPID_AMBIENT_MESSAGEREFLECT)]
HRESULT MessageReflect([out,retval]VARIANT_BOOL* pbMsgReflect);
// DISPID_AMBIENT_SHOWGRABHANDLES
[propget, id(DISPID_AMBIENT_SHOWGRABHANDLES)]
HRESULT ShowGrabHandles(VARIANT_BOOL* pbShowGrabHandles);
// DISPID_AMBIENT_SHOWHATCHING
[propget, id(DISPID_AMBIENT_SHOWHATCHING)]
HRESULT ShowHatching(VARIANT_BOOL* pbShowHatching);
// IDocHostUIHandler Defaults
...
};
QueryHost can be
used on a CAxWindow to obtain the
IAxWinAmbientDispatch interface so that these ambient
properties can be changed. For example:
LRESULT CMainWindow::OnSetGreenBackground(...) {
// Set up green ambient background
CComPtr<IAxWinAmbientDispatch> spAmbient;
hr = m_ax.QueryHost(&spAmbient);
if( SUCCEEDED(hr) ) {
spAmbient->put_BackColor(RGB(0, 255, 0));
}
return 0;
}
Whenever an ambient property is changed, the
control is notified via its implementation of the
IOleControl member function
OnAmbientPropertyChange. The control can then
QueryInterface any of its container interfaces for
IDispatch to obtain the interface for retrieving the
ambient properties (which is why 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:
LRESULT CMainWindow::OnEditProperties(...) {
CComPtr<IOleObject> spoo;
HRESULT hr = m_ax.QueryControl(&spoo);
if( SUCCEEDED(hr) ) {
CComPtr<IOleClientSite> spcs; m_ax.QueryHost(&spcs);
RECT rect; m_ax.GetClientRect(&rect);
hr = spoo->DoVerb(OLEIVERB_PROPERTIES, 0, spcs,
-1, m_ax.m_hWnd, &rect);
if( FAILED(hr) )
MessageBox(__T("Properties unavailable"), __T("Error"));
}
return 0;
}
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:
HRESULT ShowProperties(IUnknown* punkControl, HWND hwndParent) {
HRESULT hr = E_FAIL;
// Ask the control to specify its property pages
CComQIPtr<ISpecifyPropertyPages> spPages = punkControl;
if (spPages) {
CAUUID pages;
hr = spPages->GetPages(&pages);
if( SUCCEEDED(hr) ) {
// TO DO: Add your custom property pages here
CComQIPtr<IOleObject> spObj = punkControl;
if( spObj ) {
LPOLESTR pszTitle = 0;
spObj->GetUserType(USERCLASSTYPE_SHORT, &pszTitle);
// Show the property pages
hr = OleCreatePropertyFrame(hwndParent, 10, 10, pszTitle,
1, &punkControl, pages.cElems,
pages.pElems, LOCALE_USER_DEFAULT, 0, 0);
CoTaskMemFree(pszTitle);
}
CoTaskMemFree(pages.pElems);
}
}
return hr;
}
The ShowProperties function can be used
instead of the call to DoVerb. For example:
LRESULT CMainWindow::OnEditProperties(...) {
CComPtr<IUnknown> spunk;
if( SUCCEEDED(m_ax.QueryControl(&spunk)) ) {
if( FAILED(ShowProperties(spunk, m_hWnd)) ) {
MessageBox(__T("Properties unavailable"), __T("Error"));
}
}
return 0;
}
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:
bool CMainWindow::Save(LPCOLESTR pszFileName) {
// Make sure object can be saved
// Note: Our IPersistStream interface pointer could end up
// holding an IPersistStreamInit interface. This is OK
// since IPersistStream is a layout-compatible subset of
// IPersistStreamInit.
CComQIPtr<IPersistStream> spPersistStream;
HRESULT hr = m_ax.QueryControl(&spPersistStream);
if( FAILED(hr) ) {
hr = m_ax.QueryControl(IID_IPersistStreamInit,
(void**)&spPersistStream);
if( FAILED(hr) ) return false;
}
// Save object to stream in a storage
CComPtr<IStorage> spStorage;
hr = StgCreateDocfile(pszFileName,
STGM_DIRECT | STGM_WRITE |
STGM_SHARE_EXCLUSIVE | STGM_CREATE,
0, &spStorage);
if( SUCCEEDED(hr) ) {
CComPtr<IStream> spStream;
hr = spStorage->CreateStream(OLESTR("Contents"),
STGM_DIRECT | STGM_WRITE |
STGM_SHARE_EXCLUSIVE | STGM_CREATE,
0, 0, &spStream);
if( SUCCEEDED(hr) ) {
// Get and store the CLSID
CLSID clsid;
hr = spPersistStream->GetClassID(&clsid);
if( SUCCEEDED(hr) ) {
hr = spStream->Write(&clsid, sizeof(clsid), 0);
// Save the object
hr = spPersistStream->Save(spStream, TRUE);
}
}
}
if( FAILED(hr) ) return false;
return true;
}
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:
bool CMainWindow::Open(LPCOLESTR pszFileName) {
// Open object a stream in the storage
CComPtr<IStorage> spStorage;
CComPtr<IStream> spStream;
HRESULT hr;
hr = StgOpenStorage(pszFileName, 0,
STGM_DIRECT | STGM_READ | STGM_SHARE_EXCLUSIVE,
0, 0, &spStorage);
if( SUCCEEDED(hr) ) {
hr = spStorage->OpenStream(OLESTR("Contents"), 0,
STGM_DIRECT | STGM_READ | STGM_SHARE_EXCLUSIVE,
0, &spStream);
}
if( FAILED(hr) ) return false;
// Read a CLSID from the stream
CLSID clsid;
hr = spStream->Read(&clsid, sizeof(clsid), 0);
if( FAILED(hr) ) return false;
RECT rect; GetClientRect(&rect);
OLECHAR szClsid[40];
StringFromGUID2(clsid, szClsid, lengthof(szClsid));
// Create the control's host window
if( !m_ax.Create(m_hWnd, rect, 0, WS_CHILD | WS_VISIBLE, 0,
ID_CHILD_CONTROL) {
return false;
}
// Create the control, persisting from the stream
hr = m_ax.CreateControl(szClsid, spStream);
if( FAILED(hr) ) return false;
return true;
}
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:
BOOL CMainWnd:: PreTranslateAccelerator(MSG* pMsg) {
// Accelerators are only keyboard or mouse messages
if ((pMsg->message < WM_KEYFIRST ||
pMsg->message > WM_KEYLAST) &&
(pMsg->message < WM_MOUSEFIRST ||
pMsg->message > WM_MOUSELAST))
return FALSE;
// Find a direct child of this window from the window that has
// focus. This will be AxAtlWin80 window for the hosted
// control.
HWND hWndCtl = ::GetFocus();
if( IsChild(hWndCtl) && ::GetParent(hWndCtl) != m_hWnd ) {
do hWndCtl = ::GetParent(hWndCtl);
while( ::GetParent(hWndCtl) != m_hWnd );
}
// Give the control (via the AtlAxWin80) a chance to
// translate this message
if (::SendMessage(hWndCtl, WM_FORWARDMSG, 0, (LPARAM)pMsg) )
return TRUE;
// Check for dialog-type navigation accelerators
return IsDialogMessage(pMsg);
}
The crux of this function
forwards the message to the 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:
int WINAPI WinMain(...) {
...
CMainWindow wndMain;
...
HACCEL haccel = LoadAccelerators(_Module.GetResourceInstance(),
MAKEINTRESOURCE(IDC_MYACCELS));
MSG msg;
while( GetMessage(&msg, 0, 0, 0) ) {
if( !TranslateAccelerator(msg.hwnd, haccel, &msg) &&
!wndMain.PreTranslateAccelerator(&msg) ) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
...
}
The use of a PreTranslateAccelerator
function on every window that contains a control gives the keyboard
navigation keys a much greater chance of working, although the
individual controls have to cooperate, too.
|