Introduction
Modeless dialog boxes have often puzzled newbie programmers. Basically a
modeless dialog box is one that allows us to interact with other windows even
when the modeless dialog is still on screen. If you keep in mind a few nifty
little tricks then programming modeless dialogs will be a piece of cake.
Creating the modeless dialog
The straightforward way to create a modeless dialog is using
Create()
. Pass the name of the dialog's template resource and an
optional CWnd*
which points to the parent window. If you don't pass
a parent window pointer the main application window will be used as the parent
window. Create()
will return true
if the call was
successful.
Since Create()
returns immediately unlike
DoModal()
, you must never declare your modeless dialog as a
local variable with scope and lifetime only within the function where it is
declared. Instead allocate the modeless dialog on the heap. If you don't do
that, the modeless dialog will be destroyed the moment, you exit the function
within which you declared it.
An alternative solution is to declare your modeless dialog as a heap member
object of your main frame window or your CWinApp
derived class. An
advantage with this method is that you actually have control over the modeless
dialog, since you have a pointer to it.
By the way, unlike modal dialogs, modeless dialogs need to have the
WS_VISIBLE
style set if you want them to be visible immediately
after creation. Otherwise you'll have to explicitly call
ShowWindow()
with SW_SHOW
. In fact I recommend that
you do this, instead of going all over the place, changing default styles.
CModeLess *m_pmodeless = new CModeLess(this);
m_pmodeless->Create(CModeLess::IDD);
m_pmodeless->ShowWindow(SW_SHOW);
The parent issue
The usual practice is to make the parent window the main window of your
application, which is typically the main frame window. Now one issue with this
is that the modeless dialog will remain on top of this parent window. It allows
you to interact with the main frame window, perhaps it contains a
CView
derived view. But it may be annoying and undesirable to have
the modeless dialog remain on top. The solution here is to create the modeless
dialog as a child of the desktop. Use GetDesktopWindow()
to get a
pointer to the Desktop and pass that as the parent window for the modeless
dialog in your call to Create()
.
m_pmodeless->Create(CModeLess::IDD,GetDesktopWindow());
Destroying the modeless dialog
Since we have allocated memory on the heap, we must delete it when the
modeless dialog is destroyed, otherwise we'll soon run into big trouble with
memory leaking left, right and center. When the dialog is destroyed the last
message our handler class receives is the WM_NCDESTROY
message. The
OnNcDestroy
function is invoked and this in turns calls the
virtual function PostNcDestroy
. That's exactly where we can
delete
our modeless dialog. First call the base class function so
that it does it's own cleaning up.
void CModeLess::PostNcDestroy()
{
CDialog::PostNcDestroy();
delete this;
}
Issue with member objects
If the modeless dialog is a member object of the parent window class, we have
a slight issue here. The member variable still holds a pointer reference, but
the memory it references has been deleted. There are workarounds to this
problem. One method is to post an user defined message to the parent window and
handle it in the parent class, by setting the modeless dialog member variable to
NULL
. Another method is to use GetParent()
to get the
parent window, if any and then cast it to the actual parent class. Now we have
access to the parent class's member variable that holds the pointer to the
modeless dialog. Set that to NULL
. The latter method is portrayed
later where I discuss how to restrict a modeless dialog to one instance. The
former method is shown below :-
void CModeLess::PostNcDestroy()
{
CDialog::PostNcDestroy();
GetParent()->PostMessage(WM_MODELESS_CLOSED,0,0);
delete this;
}
LRESULT CMainFrame::OnMyMethod(WPARAM wParam, LPARAM lParam)
{
m_pmodeless = NULL;
return 0;
}
Problems with OnOK() and OnCancel()
In modal dialog boxes, everybody including the queen's cook, has the
OK/Cancel buttons. In my opinion, and presumably in many other more learned
people's opinions, you'd do good to avoid having OK and Cancel on a modeless
dialog. But if for some unavoidable reason, you badly want to have them on your
modeless dialog, then you'll need to over-ride both functions.
Here is my modeless version of the OnCancel()
function. As you
can see I have simply called DestroyWindow()
and I haven't bothered
to call the base class. In fact don't call the base class at all. The
base class function will call EndDialog()
which is associated with
DoModal()
.
void CModeLess::OnCancel()
{
DestroyWindow();
}
Okay, now for my modeless version of OnOK()
. I have called
DestroyWindow()
as in the OnCancel()
, but there is
some extra code too as you can see. I am calling UpdateData
,
because that's what OnOK()
does in a modal dialog. If the DDV macro
validations are successful then UpdateData(true)
returns
true
and we destroy the window, else the DDV message box is
automatically shown to the user and we refuse to destroy the dialog. Thus we are
simulating the behavior of a modal dialog's OK button here.
void CModeLess::OnOK()
{
if(UpdateData(true))
{
DestroyWindow();
}
}
Passing back data
In modal dialogs, we can still access the data variables when
DoModal()
returns because the dialog object has not been destroyed
yet, only the underlying dialog window has been destroyed. This is also possible
with modeless dialogs using a nifty trick as shown below.
void CModeLess::OnOK()
{
if(UpdateData(true))
{
((CMainFrame*)m_parent)->m_x=m_sss;
DestroyWindow();
}
}
Here I have assigned the value of the dialog data variable m_sss
to the parent class's member variable, m_x
. Here, m_parent
is a pointer to the parent window. If you are wondering where I got this
m_parent
from, scroll up and see how I have constructed my modeless
dialog object. I'll repeat that single line to refresh your memory, and also to
help you avoid scrolling, thus saving you some energy.
CModeLess *m_pmodeless = new CModeLess(this);
As you can see, I have passed this
to the constructor. In my
case, this is a pointer to my CFrameWnd
derived class which App
Wizard has named as CMainFrame
for me. Now take a look at my
CModeLess
class's constructor.
CModeLess::CModeLess(CWnd* pParent )
: CDialog(CModeLess::IDD, pParent)
{
m_sss = 0;
m_parent=pParent;
}
It all slowly makes sense, eh?
Tracking the modeless dialog count
Let's say you want to have only one instance of the modeless dialog alive at
one time. In that case, each time the user initiates some action that results in
the bringing up of the modeless dialog you have to check and see if the modeless
dialog is already active. Say, m_pmodeless
is the modeless dialog
member of your class. In the class constructor set m_pmodeless
to
NULL
. Now each time you check to see if
m_pmodeless
is NULL
and if it is
NULL
, create a new modeless dialog, otherwise, show a
MessageBox
that the dialog is already active or use
SetForegroundWindow()
to bring the modeless dialog to the foreground..
Here is how I create my modeless dialog now that I want to restrict them to
just one at a time:-
if(m_pmodeless)
{
m_pmodeless->SetForegroundWindow();
}
else
{
m_pmodeless = new CModeLess(this);
m_pmodeless->Create(CModeLess::IDD);
m_pmodeless->ShowWindow(SW_SHOW);
}
But when the dialog is destroyed we need to inform the parent class that the
pointer it holds is now useless. What we do is to set that pointer to
NULL
in the PostNcDestroy
. In fact it is essential
that you do this, otherwise the next time the user tries to activate the
modeless dialog, your program will crash as it thinks m_pmodeless
is still pointing to a valid dialog window and tried to call
SetForegroundWindow()
on it. And here is my
PostNcDestroy
:-
void CModeLess::PostNcDestroy()
{
CDialog::PostNcDestroy();
((CMainFrame*)m_parent)->m_pmodeless = NULL;
delete this;
}