Programming a Modeless Dialog Box
The dialog boxes we've worked with so far in this chapter have been ordinary modal dialog boxes. Now let's move on to the modeless dialog box and to the common dialog boxes for modern versions of the Windows base class CDialog. They both use a dialog resource that you can build with the dialog editor. If you're using a modeless dialog box with a view, you'll need to know some specialized programming techniques.
Creating Modeless Dialog Boxes
For modal dialog boxes, you've already learned that you construct a dialog object using a CDialog constructor that takes a dialog resource template ID as a parameter, and then you display the modal dialog box by calling the DoModal member function. The window ceases to exist as soon as DoModal returns. Thus, you can construct a modal dialog object on the stack, knowing that the dialog box has been destroyed by the time the dialog object goes out of scope.Modeless dialog boxes are more complicated. You start by invoking the CDialog default constructor to construct the dialog object, but to create the dialog box you need to call the CDialog::Create member function instead of DoModal. Create takes the resource ID as a parameter and returns immediately with the dialog box still on the screen. You must worry about exactly when to construct the dialog object, when to create the dialog box, when to destroy the dialog box, and when to process user-entered data.
Table 7-2 summarizes the differences between creating a modal dialog box and a modeless dialog box.
Modal Dialog Box | Modeless Dialog Box | |
---|---|---|
Constructor Used | Constructor with resource ID param | Default constructor (no params) |
Function Used to Create Window | DoModal | Create with resource ID param |
User-Defined Messages
Suppose you want the modeless dialog box to be destroyed when the user clicks the dialog box's OK button. This presents a problem. How does the view know that the user has clicked the OK button? The dialog box could call a view class member function directly, but that would "marry" the dialog box to a particular view class. A better solution is for the dialog box to send the view a user-defined message as the result of a call to the OK button message-handling function. When the view gets the message, it can destroy the dialog box (but not the object so that it can maintain any user data specified in the dialog box). This sets the stage for the creation of a new dialog box.You have two options for sending Windows messages: the CWnd::SendMessage function or the PostMessage function. The former causes an immediate call to the message-handling function, and the latter posts a message in the Windows message queue. Because there's a slight delay with the PostMessage option, it's reasonable to expect that the handler function has returned by the time the view gets the message.
Dialog Box Ownership
Now suppose you've accepted the dialog box default pop-up style, which means that the dialog box isn't confined to the view's client area. As far as Windows is concerned, the dialog box's "owner" is the application's main frame window (which you'll see in Chapter 12 ), not the view. You need to know the dialog box's view to send the view a message. Therefore, your dialog class must track its own view through a data member that the constructor sets. The CDialog constructor's pParent parameter doesn't have any effect here, so don't bother using it.
The Ex07c Example: A Modeless Dialog Box
We could convert the monster dialog box created earlier in the chapter to a modeless dialog box, but starting from scratch with a simpler dialog box is easier. Example Ex07c uses a dialog box with one edit control, an OK button, and a Cancel button. As in the Dialog Box That Ate Cincinnati example, pressing the left mouse button while the mouse cursor is inside the view window brings up the dialog box, but now we have the option of destroying it in response to another event—pressing the right mouse button when the mouse cursor is inside the view window. We'll allow only one open dialog box at a time, so we must be sure that a second left button press doesn't bring up a duplicate dialog box.To summarize the upcoming steps, the Ex07c view class has a single associated dialog object that is constructed on the heap when the view is constructed. The dialog box is created and destroyed in response to user actions, but the dialog object is not destroyed until the application terminates.Here are the steps to create the Ex07c example:
Create a new MFC Application project named Ex07c. In the MFC Application Wizard, accept all the defaults but two: On the Application Type page, select Single Document, and on the Advanced Features page, deselect Printing And Print Preview.
Create a new dialog resource. On the Project menu, select Add Resource and add a new dialog box. The dialog editor assigns the ID IDD_DIALOG1 to the new dialog box. Using the Properties window for the dialog box, change the Caption property to Modeless Dialog and set the Visible property to True. Leave the default OK and Cancel buttons with IDs IDOK and IDCANCEL
Add controls to the dialog box. Add a static text control and an edit control with the default ID IDC_EDIT1. Change the Caption property of the static text control to Edit 1. Here is the completed dialog box:
Use the MFC Class Wizard to create the CEx07cDialog class. In Class View, right-click on the Ex07c project, choose Add and then Add Class, select the MFC Class template, and click Open to start the MFC Class Wizard. Name the class CEx07cDialog, make sure it derives from CDialog, and set the Dialog ID to IDD_DIALOG1, as shown here. When you're finished, click the Finish button.
Add message handlers for IDCANCEL and IDOK. Select the CEx07cDialog class in Class View, click the Events button in the Properties window, and add the OnBnClickedCancel and OnBnClickedOk message handlers as shown in the following table.
Object ID | Message | Member Function |
---|---|---|
IDCANCEL | BN_CLICKED | OnBnClickedCancel |
IDOK | BN_CLICKED | OnBnClickedOk |
Add a variable to the CEx07cDialog class. Select the CEx07cDialog class in Class View. Choose Add Variable from the Project menu, and use the Add Member Variable Wizard to add member variables for the IDC_EDIT1 control. Make the variable of type CString and name it m_strEdit1, as shown here:
Edit Ex07cDialog.h to add a view pointer and function prototypes. Type the following boldface code in the CEx07cDialog class declaration:
private:
CView* m_pView;
Also, add the function prototypes as follows:
public:
CEx07cDialog(CView* pView);
BOOL Create();
Note | Using the CView class rather than the CEx07cView class allows the dialog class to be used with any view class. |
Edit Ex07cDialog.h to define the WM_GOODBYE message ID. Add the following line of code at the top of Ex07cDialog.h :
#define WM_GOODBYE WM_USER + 5
The Windows constant WM_USER is the first message ID available for user-defined messages. The application framework uses a few of these messages, so we'll skip over the first five messages.
Note | Visual C++ maintains a list of symbol definitions in your project's resource.h file, but the resource editor does not understand constants based on other constants. Don't manually add WM_GOODBYE to resource.h because Visual C++ might delete it. |
Add the modeless constructor in the file Ex07cDialog.cpp . You could modify the existing CEx07cDialog constructor, but if you add a separate one, the dialog class can serve for both modal and modeless dialog boxes. Add the following code to Ex07cDialog.cpp .
CEx07cDialog::CEx07cDialog(CView* pView) // modeless constructor
: m_strEdit1(_T("))
{
m_pView = pView;
}
You should also add the following line to the modal constructor generated by the MFC Application Wizard:
IMPLEMENT_DYNAMIC(CEx07cDialog, CDialog)
CEx07cDialog::CEx07cDialog(CWnd* pParent /*=NULL*/)
: CDialog(CEx07cDialog::IDD, pParent)
, m_strEdit1(_T("))
{
m_pView = NULL;
}
The C++ compiler is clever enough to distinguish between the modeless constructor CEx07cDialog(CView*) and the modal constructor CEx07cDialog(CWnd*). If the compiler sees an argument of class CView or a derived CView class, it generates a call to the modeless constructor. If it sees an argument of class CWnd or another derived CWnd class, it generates a call to the modal constructor.
Add the Create function in Ex07cDialog.cpp . This derived dialog class Create function calls the base class function with the dialog resource ID as a parameter. Add the following lines:
BOOL CEx07cDialog::Create()
{
return CDialog::Create(CEx07cDialog::IDD);
}
Note | Create is not a virtual function. You can choose a different name if you want to. |
Edit the OnBnClickedCancel and OnBnClickedOk functions in Ex07cDialog.cpp .These virtual functions generated in an earlier step are called in response to dialog button clicks. Add the following code shown in boldface:
void CEx07cDialog::OnBnClickedCancel()
{
if (m_pView != NULL) {
// modeless case -- do not call base class OnCancel
m_pView->PostMessage(WM_GOODBYE, IDCANCEL);
}
else {
CDialog::OnCancel(); // modal case
}
}
void CEx07cDialog::OnBnClickedOk()
{
if (m_pView != NULL) {
// modeless case -- do not call base class OnOK
UpdateData(TRUE);
m_pView->PostMessage(WM_GOODBYE, IDOK);
}
else {
CDialog::OnOK(); // modal case
}
}
If the dialog box is being used as a modeless dialog box, it sends the user-defined message WM_GOODBYE to the view. We'll worry about handling the message later.
Important | For a modeless dialog box, be sure to not call the CDialog::OnOK or CDialog::OnCancel function. This means you must override these virtual functions in your derived class; otherwise, using the Esc key, the Enter key, or a button click will result in a call to the base class functions, which call the Windows EndDialog function. EndDialog is appropriate only for modal dialog boxes. In a modeless dialog box, you must call DestroyWindow instead and, if necessary, you must call UpdateData to transfer data from the dialog controls to the class data members. |
Edit the Ex07cView.h header file. You need a data member to hold the dialog box pointer:
private:
CEx07cDialog* m_pDlg;
You also need to add the forward declaration at the beginning of Ex07cView.h :
class CEx07cDialog;
You won't have to include Ex07cDialog.h in every module that includes Ex07cView.h .
Modify the CEx07cView constructor and destructor in the file Ex07cView.cpp . The CEx07cView class has a data member, m_pDlg, that points to the view's CEx07cDialog object. The view constructor constructs the dialog box object on the heap, and the view destructor deletes it. Add the following code shown in boldface:
CEx07cView::CEx07cView()
{
m_pDlg = new CEx07cDialog(this);
}
CEx07cView::~CEx07cView()
{
delete m_pDlg; // destroys window if not already destroyed
}
Add code to the virtual OnDraw function in the Ex07cView.cpp file. Edit the CEx07cView OnDraw function (whose skeleton was generated by the MFC Application Wizard ) as follows to prompt the user to press the mouse button:
void CEx07cView::OnDraw(CDC* pDC)
{
CEx07cDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
pDC->TextOut(0, 0, "Press the left mouse button here.");
}
Add message handlers in CEx07cView for WM_LBUTTONDOWN and WM_RBUTTONDOWN. Select the CEx07cView class in Class View, click the Messages button in the Properties window, and add the OnLButtonDown and OnRButtonDown functions. Then edit the code in file Ex07cView.cpp as follows:
void CEx07cView::OnLButtonDown(UINT nFlags, CPoint point)
{
// creates the dialog if not created already
if (m_pDlg->GetSafeHwnd() == 0) {
m_pDlg->Create(); // displays the dialog window
}
}
void CEx07cView::OnRButtonDown(UINT nFlags, CPoint point)
{
m_pDlg->DestroyWindow();
// no problem if window was already destroyed
}
For most window types except main frame windows, the DestroyWindow function does not destroy the C++ object. We want this behavior because we'll take care of the dialog object's destruction in the view destructor.
Add the dialog box header include statement to the file Ex07cView.cpp . While you're in Ex07cView.cpp , add the following dialog box header include statement after the view header include statement:
#include "Ex07cView.h"
#include "Ex07cDialog.h"
Add your own message code for the WM_GOODBYE message. Because Class View does not support user-defined messages, you must write the code yourself. This task makes you appreciate the work Visual Studio does for the other messages.In Ex07cView.cpp , add the following line between the BEGIN_MESSAGE_MAP and END_MESSAGE_MAP statements:
ON_MESSAGE(WM_GOODBYE, OnGoodbye)
Also in Ex07cView.cpp , add the message handler function itself:
LRESULT CEx07cView::OnGoodbye(WPARAM wParam, LPARAM lParam)
{
// message received in response to modeless dialog OK
// and Cancel buttons
TRACE("CEx07cView::OnGoodbye %x, %lx\n", wParam, lParam);
TRACE("Dialog edit1 contents = %s\n",
(const char*) m_pDlg->m_strEdit1);
m_pDlg->DestroyWindow();
return 0L;
}
In Ex07cView.h , add the following function prototype after the afx_msg prototypes for OnLButtonDown and OnRButtonDown:
afx_msg LRESULT OnGoodbye(WPARAM wParam, LPARAM lParam);
With Win32, the wParam and lParam parameters are the usual means of passing message data. In a mouse button down message, for example, the mouse x and y coordinates are packed into the lParam value. With the MFC library, message data is passed in more meaningful parameters. The mouse position is passed as a CPoint object. User-defined messages must use wParam and lParam, so you can use these two variables however you want. In this example, we've put the button ID in wParam.
Build and test the application.Build and run Ex07c. Press the left mouse button and then the right mouse button. (Be sure the mouse cursor is outside the dialog box when you press the right mouse button.) Press the left mouse button again and enter some data in the Edit 1 edit control, and then click the dialog box's OK button. Does the view's TRACE statement correctly list the edit control's contents?
Note | If you use the Ex07c view and dialog classes in an MDI application, each MDI child window can have one modeless dialog box. When the user closes an MDI child window, the child's modeless dialog box will be destroyed because the view's destructor calls the dialog box destructor, which in turn destroys the dialog box. |