MFC模态对话框的单一非模态性






4.92/5 (44投票s)
解释了基于CDialog的模态对话框的伪模态性以及CDialog::EndDialog实现中的一个问题
引言
对于非程序员来说,模态对话框是指那些在你关闭它们之前不会消失的对话框,通常通过点击“确定”或“取消”按钮来关闭。对于程序员(至少是非VB程序员)来说,模态对话框是指在创建时会禁用其直接父窗口,并在关闭时启用父窗口的对话框。因此,模态对话框的本质是,在关闭模态对话框之前,你无法对父窗口执行任何操作。无模式对话框则相对礼貌且不那么繁琐,它们不会强制要求你关闭才能访问父窗口。我相信,到目前为止,你可能对对话框的模态性存在的任何微小的疑虑都已经完全消除了。
MFC有一个名为CDialog
的类,它是CWnd
的派生类,专门用于在屏幕上创建和显示对话框——支持模态和无模式对话框。无模式对话框使用Create()
创建,需要自己使用DestroyWindow()
销毁,它们表现得就像任何普通的无模式窗口一样。模态对话框则使用CDialog
类的强大DoModal()
方法创建和显示。关闭模态对话框是通过调用EndDialog()
,或者通过调用OnOK()
或OnCancel()
(它们都内部调用EndDialog()
)来完成的。CDialog::EndDialog
方法将调用Win32 API函数EndDialog
,该函数定义在user32.dll中。EndDialog
(API函数)不会立即关闭对话框,而是设置一个标志,指示消息队列退出循环、销毁对话框窗口并启用父窗口。到目前为止一切都很好;一切看起来都很平静安宁,院子对面的树上的小鸟在唱着动听的歌。
非模态因子
Win32 API支持各种与对话框相关的函数,包括创建无模式和模态对话框的函数。有CreateDialogXXX
系列函数,如CreateDialog
、CreateDialogIndirect
等,用于创建无模式对话框;还有DialogBoxXXX
系列函数,如DialogBox
、DialogBoxIndirect
等,用于创建模态对话框。如前一节所述,还有一个EndDialog
函数,仅用于结束模态对话框。无模式对话框必须通过直接调用DestroyWindow
来终止。你不应尝试对无模式对话框使用EndDialog
的基本原因是,无模式对话框没有自己的模态消息循环,也不会禁用其父窗口,这基本上使EndDialog
在它们身上失去了使用价值。
好了,现在这是会让你大吃一惊的——基于MFC CDialog的模态对话框不是真正的模态对话框。万一你第一次没看清楚,再说一遍——基于MFC CDialog的模态对话框不是真正的模态对话框。我想说第三遍,但害怕被说成鹦鹉阻碍了我这样做。打开dlgcore.cpp查看源代码,你会发现这个令人震惊的陈述是真实的。MFC的CDialog
类使用CreateDialogIndirect
API函数来创建一个伪模态对话框,如果你在MSDN上查找CreateDialogIndirect
,你会发现它用于创建无模式对话框。MFC的命令路由机制使用消息映射和虚函数的组合来实现其功能,而真正的模态对话框将完全破坏这一机制,因为模态消息循环将由MFC命令路由机制范围之外来控制。因此,开发人员别无选择,只能创建一个伪模态的无模式对话框,然后极力地将其文档化为模态对话框。
基本上,当通过CDialog::DoModal
创建伪模态对话框时,会执行以下总结步骤:
- 布尔标志
bEnableParent
被设置为FALSE
- 如果父窗口是启用的,则禁用它,并将
bEnableParent
设置为TRUE
- 使用
CreateDialogIndirect
创建对话框 - 使用
CWnd::RunModalLoop
维护一个消息泵 - 一旦模态循环退出(当调用
CDialog::EndDialog
时),如果bEnableParent
为TRUE
,则启用父窗口 - 通过调用
DestroyWindow
销毁对话框窗口 - 并且
DoModal
返回传递给EndDialog
的参数
正如你所见,开发人员付出了巨大的努力来确保伪模态对话框的行为与模态对话框预期的一样。他们甚至试图处理可能出现两个模态对话框都拥有相同父窗口的异常情况——这就是bEnableParent
的作用。因此,当第二个模态对话框出现时,它不会将bEnableParent
设置为TRUE
,因为父窗口已经被禁用了。因此,当它被关闭时,它不会启用父窗口,而这正是需要的,因为另一个模态于同一父窗口的对话框仍然在屏幕上活动。
EndDialog——一个小缺陷
正如本章中已经提过几次的,模态对话框是使用EndDialog
关闭的。让我们看看EndDialog
的样子(有兴趣的人可以在dlgcore.cpp中找到定义)
void CDialog::EndDialog(int nResult) { ASSERT(::IsWindow(m_hWnd)); if (m_nFlags & (WF_MODALLOOP|WF_CONTINUEMODAL)) EndModalLoop(nResult); ::EndDialog(m_hWnd, nResult); }
调用CWnd::EndModalLoop
来退出由CWnd::RunModalLoop
维护的消息循环,这很好,然后调用EndDialog
API函数来终止模态对话框。这也很好,并且嘿,嘿,嘿,等等,等一下!!!!EndDialog
API函数应该只用于关闭模态对话框,但在这里我们试图用它来关闭一个伪模态对话框,它实际上只是一个假装拥有自己并不真正拥有的模态性的无模式对话框。[震惊的沉默...] 好了,在最初的震惊之后,让我们都放松一下。毕竟这不是世界末日,EndDialog
也不是一个非常有危害的函数调用;它只会尝试结束一个不存在的模态消息循环,并启用对话框窗口的父窗口。前者尝试显然会失败,而后者尝试只会做一些我们本来就会自己做的事情,因为一旦伪模态泵退出,DoModal
就会重新启用父窗口。那么,一切都好吗,鸟儿们又在快乐地歌唱了吗?
错误
还记得我们前面几段讨论过的bEnableParent
的事情吗?还记得我提到这个标志是如何用于确保当多个模态对话框同时存在且拥有同一个父窗口时,这个标志可以防止在一个模态对话框兄弟被关闭时过早地启用父窗口吗?猜猜怎么着?微软那位粗心大意的程序员,他编写的这个特定函数,已经使所有那些预防措施完全无效了。因为现在任何使用EndDialog
关闭的MFC模态对话框(这也意味着OnOK
和OnCancel
,因为那些函数会内部调用EndDialog
)都会启用其父窗口,而不管bEnableParent
的值是什么。这基本上意味着,如果你有两个或更多拥有相同父窗口的MFC模态对话框,那么一旦你关闭了其中任何一个模态对话框,所有其他的模态对话框都会失去它们的模态性,因为现在父窗口已经被重新启用了。
重现此错误的步骤
- 创建一个基于MFC对话框的应用程序
- 向其中添加一个新的对话框,并为其关联一个名为
CChildDialog
的类 - 在主对话框的
OnInitDialog
方法中设置两个计时器:
SetTimer(1000,1000,NULL); SetTimer(2000,2000,NULL);
void CModalDemoDlg::OnTimer(UINT nIDEvent) { KillTimer(nIDEvent); CChildDialog dlg(this); dlg.DoModal(); CDialog::OnTimer(nIDEvent); }
我附加的项目是使用VC++.NET 2003 Final Beta创建的,对于那些没有该版本的用户,我深表歉意。但遵循上述步骤最多只需要几分钟。令人惊讶的是,这个错误在MFC的几个版本中都没有被注意到。我检查了最早的VC++ 6,发现它也有完全相同的问题。在我看来,有人只需要注释掉或删除对EndDialog
API调用的引用。我猜测发生的情况是:MFC开发人员太习惯于从他们的包装函数中调用原生API(例如,他们会在CWnd::Messagebox
内部调用MessageBox
API),所以有人可能在不经思考的情况下自动键入了那一行,而QA人员也可能忽略了这个错误。而且这个问题之所以大部分未被发现,是因为程序拥有一个多模态窗口架构,其中几个模态对话框窗口拥有同一个父窗口的情况非常罕见。
解决方法
好吧,直到微软修正了这个错误,我们(除了必须修正MFC代码并重新编译MFC之外)可以做什么呢?我们可以重写“确定”和“取消”按钮的处理程序,并使用以下代码代替默认的:
{ if (m_nFlags & (WF_MODALLOOP|WF_CONTINUEMODAL)) EndModalLoop(IDOK); // or IDCANCEL }
请注意,我们**没有**调用基类(如果我们调用了,那么::EndDialog(...)
就会被调用,我们所有的努力都将白费)。如果你想在“确定”/“取消”按钮处理程序之外的地方退出模态对话框,你应该使用相同的代码,只是你可能想返回一个不同的值(例如,可能是IDYES
)。
结论
就我所知,我可能是镇上最大的傻瓜,并且可能有一个完全合理的解释,但某些东西告诉我,那是一个非常遥远的意外。顺便说一下,我想感谢Shog9在我深夜(对他来说相当于我们大多数人的中午)给我发来了VC++ 6版本的dlgcore.cpp。我还要明确指出,本文绝不旨在嘲笑开发了MFC库的了不起的微软程序员团队。
版本历史
- 2003年4月5日 - 文章首次发布
- 2003年4月7日 - 文章更新,添加了问题的解决方法