A Rewrite of KB103788: INFO: Creating a Modeless Dialog Box with MFC Libraries |
|
The MSDN article is confusing, misleading, and incomplete. This rewrite attempts to correct all these deficiencies.
To create a modeless dialog, first create a dialog template and populate it with controls. As you would for any dialog, create control variables, value variables, and event handlers. For purposes of this article, we will assume that the class name you have assigned to your class is CModeless. The parent class that launches the dialog will be called CMyClass.
1. In the parent class, add a pointer variable to the class
CModeless * pdlg;
2. In the constructor of the parent class, add the line
pdlg = NULL;
3. It is convenient to override the Create method to make it easier to create the modeless dialog; if you desire to do this, add the following method to the CModeless class:
BOOL Create(CWnd * pWnd = NULL) { return CDialog::Create(IDD, pWnd); }
4. The simplest form of creation is to skip step 3 entirely, and simply do
pdlg = new CModeless; if(!pdlg->Create(CModeless::IDD, this)) { ... deal with creation error here delete pdlg; ... avoid further processing }
but if you have overridden the Create member as described in step 3, you would only need to write
if(!pdlg->Create(this)) { ... deal with creation error here delete pdlg; ...avoid further processing }
5. Neither OnOK nor OnCancel can be permitted to use the default handlers. They must be overridden. Note that removing the OK and Cancel buttons is not sufficient, because an <enter> key can generate a WM_COMMAND:IDOK message (which will invoke the OnOK virtual method) and an <esc> key can generate a WM_COMMAND:IDCANCEL message. Therefore, it is mandatory that these methods be overridden. It is common to delete both the OK and Cancel buttons from the dialog; alternatively, delete only one of the buttons, and label the remaining button Close. Generally, it does not matter which button you delete and which button you relabel.
5a. The OnCancel handler must call DestroyWindow, and must NOT call the superclass
void CModeless::OnCancel() { // CDialog::OnCancel(); // MUST remove this line DestroyWindow(); }
5b. The OnOK handler must call DestroyWindow, and must NOT call the superclass.
void CModeless::OnOK() { // CDialog::OnOK(); // MUST remove this line DestroyWindow(); }
Note that if you are depending on DDV to validate values, DDV will not be invoked because the superclass call has been bypassed. If the values are validated, there must be additional code added to make these values available, because the PostNcDestroy handler will delete them. If you want to use DDX/DDV, you must add code as shown:
void CModeless::OnOK() { // CDialog::OnOK(); // MUST remove this line if(!UpdateData(TRUE)) { /* validation failed */ ...notify user that values are invalid return; } /* validation failed */ ...make values available to those who need them DestroyWindow(); }
6. The PostNcDestroy handler must be overridden to delete the CModeless object
void CModeless::PostNcDestroy() { delete this; }
While this simple sketch shows you the bare bones of modeless dialog creation, it is not sufficient.
There is a problem with this simplistic model. The creator of the modeless dialog will not know that the dialog has been destroyed, and will not know that the pointer to it is now invalid. Also, if the modeless dialog was created by a menu item or other user action, the action should not create another instance. There are several ways to handle these situations.
void CMyClass::OnWantModelessDialog() { if(pdlg == NULL) { /* dialog does not exist */ pdlg = new CModeless; if(!pdlg->Create(this)) // if step 3 override was done { /* failed */ ... deal with notification delete pdlg; pdlg = NULL; return; } /* failed */ else { /* already exists */ if(pdlg->IsIconic()) // in case the minimize button is enabled pdlg->ShowWindow(SW_RESTORE); } /* already exists */
To receive a notification of window termination, you must defined a user-defined message. This can be a WM_APP-based message or a Registered Window Message. Choose one of these two methods for specifying the message:
#define UWM_MODELESS_CLOSED (WM_APP + n) // for some value n, such as 200 static const UINT UWM_MODELESS_CLOSED = ::RegisterWindowMessage(_T("UWM_MODELESS_CLOSED-<guid here>"));
Add to your message map one of the following two lines, based on which type of message you are using
ON_MESSAGE(UWM_MODELESS_CLOSED, OnModelessClosed) ON_REGISTERED_MESSAGE(UWM_MODELESS_CLOSED, OnModelessClosed)
add the following handler
LRESULT CMyClass::OnModelessClosed(WPARAM, LPARAM) { pdlg = NULL; return 0; }
The techniques for message handling are discussed in my essay on Message Management.
Declare the handler in your parent class handler; for this example, the CMyClass class definition:
LRESULT OnModelessClosed(WPARAM, LPARAM);
In the dialog, add an OnDestroy handler:
void CModeless::OnDestroy() { GetParent()->SendMessage(UWM_MODELESS_CLOSED); CDialog::OnDestroy(); }
This will guarantee that the variable is NULL.
An alternative is to disable the control so another dialog instance is not created; this could be handled by adding an ON_UPDATE_COMMAND_UI handler. This would not allow the menu item/toolbar button to bring back a minimized dialog, but if the dialog cannot be minimized, this would be acceptable.
void CMyClass::OnUpdateWantModelessDialog(CCmdUI * pCmdUI) { pCmdUI->Enable(pdlg == NULL); }
Another approach which is often convenient is to simply hide, rather than destroy, a modeless dialog. This way, there is no need to have to reload the control values each time it is created. To do this, the code should be modified as follows:
Modify the OnOK and OnCancel handlers to read
void CModeless::OnCancel() { // CDialog::OnCancel(); // MUST remove this line ShowWindow(SW_HIDE); } void CModeless::OnOK() { // CDialog::OnOK(); // MUST remove this line ShowWindow(SW_HIDE); }
In addition add an OnClose handler
void CModeless::OnClose() { // CDialog::OnClose(); // MUST remove this line ShowWindow(SW_HIDE); }
Modify the show-dialog handler to be
void CMyClass::OnWantModelessDialog() { if(pdlg == NULL) { /* dialog does not exist */ pdlg = new CModeless; if(!pdlg->Create(this)) // if step 3 override was done { /* failed */ ... deal with notification delete pdlg; pdlg = NULL; return; } /* failed */ else { /* already exists */ if(!pDlg->IsWindowVisible()) pdlg->ShowWindow(SW_SHOW); if(pdlg->IsIconic()) // in case the minimize button is enabled pdlg->ShowWindow(SW_RESTORE); } /* already exists */ }
Some parent class has responsibility for managing this window; typically in its OnDestroy handler you will do
void CMyClass::OnDestroy() { if(pdlg != NULL) pdlg->DestroyWindow(); CMyParent::OnDestroy(); }
The views expressed in these essays are those of the author, and in no way represent, nor are they endorsed by, Microsoft.