CDialogImpl
Dialogs represent a declarative style of user
interface development. Whereas normal windows provide all kinds of
flexibility (you can put anything you want in the client area of a
window), dialogs are more static. Actually, dialogs are just
windows whose layout has been predetermined. The built-in dialog
box window class knows how to interpret dialog box resources to
create and manage the child windows that make up a dialog box. To
show a dialog box modallythat is,
while the dialog is
visible, the parent is inaccessibleWindows provides the
DialogBoxParam function:
int DialogBoxParam(
HINSTANCE hInstance, // handle to application instance
LPCTSTR lpTemplate, // identifies dialog box template
HWND hWndParent, // handle to owner window
DLGPROC lpDialogFunc, // pointer to dialog box procedure
LPARAM dwInitParam); // initialization value
The result of the DialogBoxParam
function is the command that closed the dialog, such as
IDOK or IDCANCEL. To show the dialog box
modelesslythat is, the parent
window is still accessible while the dialog is showingthe
CreateDialogParam function is used
instead:
HWND CreateDialogParam(
HINSTANCE hInstance, // handle to application instance
LPCTSTR lpTemplate, // identifies dialog box template
HWND hWndParent, // handle to owner window
DLGPROC lpDialogFunc, // pointer to dialog box procedure
LPARAM dwInitParam); // initialization value
Notice that the parameters to
CreateDialogParam are identical to those to
DialogBoxParam, but the return value is different. The
return value from Create-DialogParam represents the
HWND of the new dialog box window, which will live until
the dialog box window is destroyed.
Regardless of how a dialog is shown, however,
developing a dialog is the same. First, you lay out a dialog
resource using your favorite resource editor. Second, you develop a
dialog box procedure (DlgProc). The WndProc of
the dialog box class calls the DlgProc to give you an
opportunity to handle each message (although you never have to call
DefWindowProc in a DlgProc). Third, you call
either DialogBoxParam or CreateDialogParam (or
one of the variants) to show the dialog. This is the same kind of
grunt work we had to do when we wanted to show a window (that is,
register a window class, develop a WndProc, and create the
window). And just as ATL lets us work with CWindow-derived
objects instead of raw windows, ATL also lets us work with
CDialogImpl-derived classes instead of raw dialogs.
Showing a
Dialog
CDialogImpl
provides a set of wrapper functions around common dialog operations
(such as DialogBoxParam and
CreateDialogParam):
template <class T, class TBase /* = CWindow */>
class ATL_NO_VTABLE CDialogImpl :
public CDialogImplBaseT< TBase > {
public:
// modal dialogs
INT_PTR DoModal(HWND hWndParent = ::GetActiveWindow(),
LPARAM dwInitParam = NULL) {
BOOL result;
result = m_thunk.Init(NULL,NULL);
if (result == FALSE) {
SetLastError(ERROR_OUTOFMEMORY);
return -1;
}
_AtlWinModule.AddCreateWndData(&m_thunk.cd,
(CDialogImplBaseT< TBase >*)this);
return ::DialogBoxParam(
_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) {
BOOL result;
result = m_thunk.Init(NULL,NULL);
if (result == FALSE) {
SetLastError(ERROR_OUTOFMEMORY);
return NULL;
}
_AtlWinModule.AddCreateWndData(&m_thunk.cd,
(CDialogImplBaseT< TBase >*)this);
HWND hWnd = ::CreateDialogParam(_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);
}
};
A couple of interesting things are going on in
this small class. First, notice the use of the thunk. ATL sets up a
thunk between Windows and the ProcessWindowMessage member
function of your CDialogImpl-based objects, just as it
does for CWindowImpl-based objects. In addition to all the
tricks that WindowProc performs (see the section
"The
Window Procedure," earlier in this chapter), the static member
function CDialogImpl::DialogProc manages the weirdness of
DWL_MSGRESULT. For some dialog messages, the
DlgProc must return the result of the message. For others,
the result is set by calling SetWindowLong with
DWL_MSGRESULT. And although I can never remember which is
which, ATL can. Our dialog message handlers need only return the
value; if it needs to go into the DWL_MSGRESULT, ATL puts
it there.
Something else interesting to notice is that
CDialogImpl derives from CDialogImplBaseT, which
provides some other helper functions:
template <class TBase>
class ATL_NO_VTABLE CDialogImplBaseT :
public CWindowImplRoot<TBase> {
public:
virtual WNDPROC GetDialogProc() { return DialogProc; }
static LRESULT CALLBACK StartDialogProc(HWND, UINT,
WPARAM, LPARAM);
static LRESULT CALLBACK DialogProc(HWND, UINT, WPARAM, LPARAM);
LRESULT DefWindowProc() { return 0; }
BOOL MapDialogRect(LPRECT pRect) {
return ::MapDialogRect(m_hWnd, pRect);
}
virtual void OnFinalMessage(HWND /*hWnd*/) {}
};
Again, notice the use of
the StartDlgProc to bootstrap the thunk and the
DialogProc function that actually does the mapping to the
ProcessWindowMessage member function. Also notice the
DefWindowProc member function. Remember, for
DlgProcs, there's no need to pass an unhandled message to
DefWindowProc. Because the message-handling infrastructure
of ATL requires a DefWindowProc, the
CDialogImplBaseT class provides an inline, do-nothing
function that the compiler is free to toss away.
More useful to the dialog developer are the
CDialogImplBaseT member functions MapDialogRect
and OnFinalMessage. MapDialogRect is a wrapper
around the Windows function MapDialogRect, which maps
dialog-box units to pixels. Finally, OnFinalMessage is
called after the last dialog message has been processed, just like
the CWindowImpl::OnFinalMessage member function.
You might wonder how far the inheritance
hierarchy goes for CDialogImpl. Refer again to Figure 10.1.
Notice that CDialogImpl ultimately derives from
CWindow, so all the wrappers and helpers that are
available for windows are also available to dialogs.
Before we get too far away from
CDialogImpl, notice where the dialog resource identifier
comes from. The deriving class is required to provide a numeric
symbol called IDD indicating the resource identifier. For
example, assuming a resource ID of IDD_ABOUTBOX, a typical
"about" box would be implemented like this:
class CAboutBox : public CDialogImpl<CAboutBox> {
public:
BEGIN_MSG_MAP(CAboutBox)
MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)
COMMAND_ID_HANDLER(IDOK, OnOK);
END_MSG_MAP()
enum { IDD = IDD_ABOUTBOX };
private:
LRESULT OnInitDialog(UINT, WPARAM, LPARAM, BOOL&) {
CenterWindow();
return 1;
}
LRESULT OnOK(WORD, UINT, HWND, BOOL&) {
EndDialog(IDOK);
return 0;
}
};
CAboutBox has all the elements. It derives
from CDialogImpl, has a message map, and provides a value
for IDD that indicates the resource ID. You can add an ATL
dialog object from the Add Item option in Visual Studio, but it's
not difficult to do by hand.
Using a CDialogImpl-derived class is a
matter of creating an instance of the class and calling either
DoModal or Create:
LRESULT CMainWindow::OnHelpAbout(WORD, WORD, HWND, BOOL&) {
CAboutBox dlg;
dlg.DoModal();
return 0;
}
Simple
Dialogs
For simple modal dialogs, such as "about" boxes,
that don't have interaction requirements beyond the standard
buttons (such as OK and Cancel), ATL provides
CSimpleDialog:
template <WORD t_wDlgTemplateID, BOOL t_bCenter /* = TRUE */>
class CSimpleDialog : public CDialogImplBase {
public:
INT_PTR DoModal(HWND hWndParent = ::GetActiveWindow()) {
_AtlWinModule.AddCreateWndData(&m_thunk.cd,
(CDialogImplBase*)this);
INT_PTR nRet =
::DialogBox(_AtlBaseModule.GetResourceInstance(),
MAKEINTRESOURCE(t_wDlgTemplateID), hWndParent,
StartDialogProc);
m_hWnd = NULL;
return nRet;
}
typedef CSimpleDialog<t_wDlgTemplateID, t_bCenter> thisClass;
BEGIN_MSG_MAP(thisClass)
MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)
COMMAND_RANGE_HANDLER(IDOK, IDNO, OnCloseCmd)
END_MSG_MAP()
LRESULT OnInitDialog(UINT, WPARAM, LPARAM, BOOL&) {
// initialize controls in dialog with
// DLGINIT resource section
ExecuteDlgInit(t_wDlgTemplateID);
if(t_bCenter)
CenterWindow(GetParent());
return TRUE;
}
LRESULT OnCloseCmd(WORD, WORD wID, HWND, BOOL& ) {
::EndDialog(m_hWnd, wID);
return 0;
}
};
Notice that the resource ID
is passed as a template parameter, as is a flag indicating whether
the dialog should be centered. This reduces the definition of the
CAboutBox class to the following type definition:
typedef CSimpleDialog<IDD_ABOUTBOX> CAboutBox;
However, the use of CAboutBox remains
the same.
The call to ExecuteDlgInit in the
OnInitDialog method is worth mentioning. When you build a
dialog using the Visual Studio dialog editor and you add a combo
box, the contents of that combo box are not stored in the regular
Dialog resource. Instead, they're stored in a custom resource type
named RT_DLGINIT. The normal dialog APIs completely ignore
this initialization data. ExecuteDlgInit contains the code
needed to read the RT_DLGINIT resource and properly
initialize any combo boxes on the dialog.
Data Exchange and
Validation
Unfortunately, most dialogs aren't simple. In
fact, most are downright complicated. This complication is mostly
due to two reasons: writing data to child controls managed by the
dialog, and reading data from child controls managed by the dialog.
Exchanging data with a modal dialog typically goes like this:
-
The application creates an instance of a
CDialogImpl-derived class.
-
The application copies some data into the dialog
object's data members.
-
The application calls DoModal.
-
The dialog handles
WM_INITDIALOG by copying data members into child
controls.
-
The dialog handles the OK button by validating
that data held by child controls. If the data is not valid, the
dialog complains to the users and makes them keep trying until
either they get it right or they get frustrated and click the
Cancel button.
-
If the data is valid, the data is copied back
into the dialog's data members, and the dialog ends.
-
If the application gets IDOK from
DoModal, it copies the data from the dialog data members
over its own copy.
If the dialog is to be shown modelessly, the
interaction between the application and the dialog is a little
different, but the relationship between the dialog and its child
controls is the same. A modeless dialog sequence goes like this
(differences from the modal case are shown in italics):
-
The application creates an instance of a
CDialogImpl-derived class.
-
The application copies some data into the dialog
object's data members.
-
The applications calls Create.
-
The dialog handles WM_INITDIALOG by
copying data members into child controls.
-
The dialog handles the Apply button by validating
that data held by child controls. If the data is not valid, the
dialog complains to the users and makes them keep trying until
either they get it right or they get frustrated and click the
Cancel button.
-
If the data is valid, the data is copied back
into the dialog's data members and the
application is notified to read the updated data from the dialog.
-
When the application
is notified, it copies the data from the dialog data members
over its own copy.
Whether modal or modeless, the dialog's job is
to exchange data between its data members and the child controls,
and to validate it along the way. MFC has something called DDX/DDV
(Dialog Data eXchange/Dialog Data Validation) for just this
purpose. ATL has no such support, but it turns out to be quite easy
to build yourself. For example, to beef up our standalone windows
application sample, imagine a dialog that allows us to modify the
display string, as shown in Figure 10.5.
The CDialogImpl-based class looks like
this:
class CStringDlg : public CDialogImpl<CStringDlg> {
public:
CStringDlg() { *m_sz = 0; }
BEGIN_MSG_MAP(CStringDlg)
MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)
COMMAND_ID_HANDLER(IDOK, OnOK)
COMMAND_ID_HANDLER(IDCANCEL, OnCancel)
END_MSG_MAP()
enum { IDD = IDD_SET_STRING };
TCHAR m_sz[64];
private:
bool CheckValidString() {
// Check the length of the string
int cchString =
::GetWindowTextLength(GetDlgItem(IDC_STRING));
return cchString ? true : false;
}
LRESULT OnInitDialog(UINT, WPARAM, LPARAM, BOOL&) {
CenterWindow();
// Copy the string from the data member
// to the child control (DDX)
SetDlgItemText(IDC_STRING, m_sz);
return 1; // Let dialog manager set initial focus
}
LRESULT OnOK(WORD, UINT, HWND, BOOL&) {
// Complain if the string is of zero length (DDV)
if( !CheckValidString() ) {
MessageBox("Please enter a string", "Hey!");
return 0;
}
// Copy the string from the child control
// to the data member (DDX)
GetDlgItemText(IDC_STRING, m_sz, lengthof(m_sz));
EndDialog(IDOK);
return 0;
}
LRESULT OnCancel(WORD, UINT, HWND, BOOL&) {
EndDialog(IDCANCEL);
return 0;
}
};
In this example, DDX-like functionality happens
in OnInitDialog and OnOK. OnInitDialog
copies the data from the data member into the child edit control.
Likewise, OnOK copies the data from the child edit control
back to the data member and ends the dialog if the data is valid.
The validity of the data (DDV-like) is checked before the call to
EndDialog in OnOK by calling the helper function
CheckValidString. I decided that a zero-length string
would be too boring, so I made it invalid. In this case,
OnOK puts up a message box and doesn't end the dialog. To
be fair, MFC would have automated all this with a macro-based
table, which makes handling a lot of DDX/DDV chores easier, but ATL
certainly doesn't prohibit data exchange or validation. The WTL
library mentioned earlier also has rich DDX/DDV support.
I can do even better in the data validation area
with this example. This sampleand MFC-based DDX/DDVvalidates only
when the user presses the OK button, but sometimes it's handy to
validate as the user enters the data. For example, by handling
EN_CHANGE notifications from the edit control, I can check
for a zero-length string as the user enters it. If the string ever
gets to zero, disabling the OK button would make it impossible for
the user to attempt to commit the data at all, making the complaint
dialog unnecessary. The following updated sample shows this
technique:
class CStringDlg : public CDialogImpl<CStringDlg> {
public:
...
BEGIN_MSG_MAP(CStringDlg)
...
COMMAND_HANDLER(IDC_STRING, EN_CHANGE, OnStringChange)
END_MSG_MAP()
private:
void CheckValidString() {
// Check the length of the string
int cchString =
::GetWindowTextLength(GetDlgItem(IDC_STRING));
// Enable the OK button only if the string
// is of non-zero length
::EnableWindow(GetDlgItem(IDOK), cchString ? TRUE : FALSE);
}
LRESULT OnInitDialog(UINT, WPARAM, LPARAM, BOOL&) {
CenterWindow();
// Copy the string from the data member
// to the child control (DDX)
SetDlgItemText(IDC_STRING, m_sz);
// Check the string length (DDV)
CheckValidString();
return 1; // Let dialog manager set initial focus
}
LRESULT OnOK(WORD, UINT, HWND, BOOL&) {
// Copy the string from the child control to the data member (DDX)
GetDlgItemText(IDC_STRING, m_sz, lengthof(m_sz));
EndDialog(IDOK);
return 0;
}
LRESULT OnStringChange(WORD, UINT, HWND, BOOL&) {
// Check the string length each time it changes (DDV)
CheckValidString();
return 0;
}
... // The rest is the same
};
In this case, notice that OnInitDialog
takes on some DDV responsibilities and OnOK loses some. In
OnInitDialog, if the string starts with a zero length, the
OK button is immediately disabled. In the OnStringChange
handler for EN_CHANGE, as the text in the edit control
changes, we revalidate the data, enabling or disabling the OK
button as necessary.
Finally, we know that if we reach the OnOK handler, the OK
button must be enabled and the DDV chores must already have been
done. Neither ATL nor MFC can help us with this kind of DDV, but
neither hinders us from providing a UI that handles both DDX and
DDV in a friendly way.
|