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.
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:
IDD_BULLSEYE DIALOG DISCARDABLE 0, 0, 342, 238
STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "BullsEye"
FONT 8, "MS Sans Serif"
BEGIN
CONTROL "",IDC_BULLSEYE,
"{7DC59CC5-36C0-11D2-AC05-00A0C9C8E50D}",
WS_TABSTOP,7,7,269,224
LTEXT "&Score:",IDC_STATIC,289,7,22,8
CTEXT "Static",IDC_SCORE,278,18,46,14,
SS_CENTERIMAGE | SS_SUNKEN
PUSHBUTTON "Close",IDCANCEL,276,41,50,14
END
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):
IDD_BULLSEYE DLGINIT
BEGIN
IDC_BULLSEYE, 0x376, 154, 0
0x0026, 0x0000, 0x007b, 0x0039, 0x0035,
...
0x0000, 0x0040, 0x0000, 0x0020, 0x0000,
0
END
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.
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.
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:
template <class T, class TBase /* = CWindow */>
class CAxDialogImpl : public CDialogImplBaseT< TBase > {
public:
...
static INT_PTR CALLBACK DialogProc(HWND hWnd, UINT uMsg,
WPARAM wParam, LPARAM lParam);
// modal dialogs
INT_PTR DoModal(HWND hWndParent = ::GetActiveWindow(),
LPARAM dwInitParam = NULL) {
_AtlWinModule.AddCreateWndData(&m_thunk.cd,
(CDialogImplBaseT< TBase >*)this);
return AtlAxDialogBox(_AtlBaseModule.GetResourceInstance(),
MAKEINTRESOURCE(static_cast<T*>(this)->IDD),
hWndParent, T::StartDialogProc, dwInitParam);
}
BOOL EndDialog(int nRetCode) {
return ::EndDialog(m_hWnd, nRetCode);
}
// modeless dialogs
HWND Create(HWND hWndParent, LPARAM dwInitParam = NULL) {
_AtlWinModule.AddCreateWndData(&m_thunk.cd,
(CDialogImplBaseT< TBase >*)this);
HWND hWnd = AtlAxCreateDialog(
_AtlBaseModule.GetResourceInstance(),
MAKEINTRESOURCE(static_cast<T*>(this)->IDD),
hWndParent, T::StartDialogProc, dwInitParam);
return hWnd;
}
// for CComControl
HWND Create(HWND hWndParent, RECT&,
LPARAM dwInitParam = NULL) {
return Create(hWndParent, dwInitParam);
}
BOOL DestroyWindow() {
return ::DestroyWindow(m_hWnd);
}
// Event handling support and Message map
HRESULT AdviseSinkMap(bool bAdvise) {
if(!bAdvise && m_hWnd == NULL) {
// window is gone, controls are already unadvised
return S_OK;
}
HRESULT hRet = E_NOTIMPL;
__if_exists(T::_GetSinkMapFinder) {
T* pT = static_cast<T*>(this);
hRet = AtlAdviseSinkMap(pT, bAdvise);
}
return hRet;
}
typedef CAxDialogImpl< T, TBase > thisClass;
BEGIN_MSG_MAP(thisClass)
MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)
MESSAGE_HANDLER(WM_DESTROY, OnDestroy)
END_MSG_MAP()
virtual HRESULT CreateActiveXControls (UINT nID) {
// Load dialog template and InitData
// Walk through template and create ActiveX controls
// Code omitted for clarity
}
LRESULT OnInitDialog(UINT /*uMsg*/, WPARAM /*wParam*/,
LPARAM /*lParam*/, BOOL& bHandled) {
// initialize controls in dialog with DLGINIT
// resource section
ExecuteDlgInit(static_cast<T*>(this)->IDD);
AdviseSinkMap(true);
bHandled = FALSE;
return 1;
}
LRESULT OnDestroy(UINT /*uMsg*/, WPARAM /*wParam*/,
LPARAM /*lParam*/, BOOL& bHandled) {
AdviseSinkMap(false);
bHandled = FALSE;
return 1;
}
// Accelerator handling - needs to be called from a message loop
BOOL IsDialogMessage(LPMSG pMsg) {
// Code omitted for clarity
}
};
template <class T, class TBase>
INT_PTR CALLBACK CAxDialogImpl< T, TBase >::DialogProc(
HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
CAxDialogImpl< T, TBase >* pThis = (
CAxDialogImpl< T, TBase >*)hWnd;
if (uMsg == WM_INITDIALOG) {
HRESULT hr;
if (FAILED(hr = pThis->CreateActiveXControls(
pThis->GetIDD()))) {
pThis->DestroyWindow();
SetLastError(hr & 0x0000FFFF);
return FALSE;
}
}
return CDialogImplBaseT< TBase >::DialogProc(
hWnd, uMsg, wParam, lParam);
}
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:
class CBullsEyeDlg :
public CAxDialogImpl<CBullsEyeDlg>,
public IDispEventImpl<IDC_BULLSEYE, CBullsEyeDlg> {
public:
BEGIN_MSG_MAP(CBullsEyeDlg)
MESSAGE_HANDLER(WM_DESTROY, OnDestroy)
MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)
COMMAND_ID_HANDLER(IDCANCEL, OnCancel)
END_MSG_MAP()
BEGIN_SINK_MAP(CBullsEyeDlg)
SINK_ENTRY(IDC_BULLSEYE, 0x2, OnScoreChanged)
END_SINK_MAP()
// Map this class to a specific dialog resource
enum { IDD = IDD_BULLSEYE };
// Hook up connection points
LRESULT OnInitDialog(...)
{ AtlAdviseSinkMap(this, true); return 0; }
// Tear down connection points
LRESULT OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam,
BOOL& bHandled)
{ AtlAdviseSinkMap(this, false); return 0; }
// Window control event handlers
LRESULT OnCancel(WORD, UINT, HWND, BOOL&);
// COM control event handlers
VOID __stdcall OnScoreChanged(LONG ringValue);
};
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:
class CBullsEyeDlg :
public CAxDialogImpl<CBullsEyeDlg>,
public IDispEventImpl<IDC_BULLSEYE, CBullsEyeDlg> {
public:
...
LRESULT OnInitDialog(...) {
// Attach to the BullsEye control
m_axBullsEye.Attach(GetDlgItem(IDC_BULLSEYE));
// Cache BullsEye interface
m_axBullsEye.QueryControl(&m_spBullsEye);
...
return 0;
}
...
private:
CAxWindow m_axBullsEye;
CComPtr<IBullsEye> m_spBullsEye;
};
In
this example, 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:
HRESULT GetDlgControl(int nID, REFIID iid, void** ppCtrl) {
if (ppCtrl == NULL) return E_POINTER;
*ppCtrl = NULL;
HRESULT hr = HRESULT_FROM_WIN32(ERROR_CONTROL_ID_NOT_FOUND);
HWND hWndCtrl = GetDlgItem(nID);
if (hWndCtrl != NULL) {
*ppCtrl = NULL;
CComPtr<IUnknown> spUnk;
hr = AtlAxGetControl(hWndCtrl, &spUnk);
if (SUCCEEDED(hr))
hr = spUnk->QueryInterface(iid, ppCtrl);
}
return hr;
}
The GetdlgControl member function calls
the AtlAxGetControl function, which uses the HWND
of the child window to retrieve an IUnknown interface.
AtlAxGetControl does this by sending the
WM_GETCONTROL window message that windows of the class
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:
LRESULT OnInitDialog(...) {
// Cache BullsEye interface
GetDlgControl(IDC_BULLSEYE, IID_IBullsEye,
(void**)&m_spBullsEye);
...
return 0;
}
The combination of the
CAxDialogImpl class, the control-containment wizards in
Visual C++, and the GetdlgControl member function makes
managing COM controls in a dialog much like managing Windows
controls.
|