在最近的一个软件开发案例中,遇到了不容易处理的情况。
在我们的软件中,有那种一直浮在界面上的非模态对话框,例如那种浮动的工具面板。但是,这种窗口,貌似不会在主窗口弹出模态对话框的时候被Disable。如果模态对话框是在这个非模态对话框中弹出的,那没有问题。
用VS2013和升级到最新的VS2022各写了一个测试程序,发现这就是MFC的默认逻辑。
在主窗口上放了两个点击后各自会弹出非模态对话框和模态对话框的按钮。先弹出非模态对话框,然后再去弹出模态对话框。此时主窗口被Disable,无法响应鼠标、键盘消息。但非模态对话框不受影响。
本想靠调整产品设计「容忍」过去。但问题在于,如果在保持模态对话框弹出的情况下,去把非模态对话框先关闭了,那主窗口会被Enable。此时它跟模态对话框之间都能响应鼠标、键盘消息,效果就好像之前弹出的模态对话框变成了非模态对话框一样。这个时候就会有「后果」了,产品设计再怎么调整,也没法让软件在这种乱了套的情况下还能工作正常。
在网上Google了半天,不要说解决方案,连问题都没人提到。讲解模态/非模态对话框的文章有一些,但都是很入门的介绍。只是从「使用者」的角度去讲用法谈区别,并没有涉及我遇到的问题。很少从原理角度进行说明,更没有去分析源代码。大概MFC现在用的人真的不多了吧?
![]() |
| 图片由Google Gemini生成 |
还是得自己动手,在DoModal()处下了一个断点,单步跟踪进到MFC的代码里面看了一下,就明白了。以下代码来自VS2013,VS2022我貌似没有安装MFC源代码,但从表现上看二者在这个逻辑上应该相差无几。我们一起来看看CDialog::DoModal()到底干了些什么事情吧:
INT_PTR CDialog::DoModal()
{
// can be constructed with a resource template or InitModalIndirect
ASSERT(m_lpszTemplateName != NULL || m_hDialogTemplate != NULL ||
m_lpDialogTemplate != NULL);
// load resource as necessary
LPCDLGTEMPLATE lpDialogTemplate = m_lpDialogTemplate;
HGLOBAL hDialogTemplate = m_hDialogTemplate;
HINSTANCE hInst = AfxGetResourceHandle();
if (m_lpszTemplateName != NULL)
{
hInst = AfxFindResourceHandle(m_lpszTemplateName, RT_DIALOG);
HRSRC hResource = ::FindResource(hInst, m_lpszTemplateName, RT_DIALOG);
hDialogTemplate = LoadResource(hInst, hResource);
}
if (hDialogTemplate != NULL)
lpDialogTemplate = (LPCDLGTEMPLATE)LockResource(hDialogTemplate);
// return -1 in case of failure to load the dialog template resource
if (lpDialogTemplate == NULL)
return -1;
// disable parent (before creating dialog)
HWND hWndParent = PreModal();
AfxUnhookWindowCreate();
BOOL bEnableParent = FALSE;
CWnd* pMainWnd = NULL;
BOOL bEnableMainWnd = FALSE;
if (hWndParent && hWndParent != ::GetDesktopWindow() && ::IsWindowEnabled(hWndParent))
{
::EnableWindow(hWndParent, FALSE);
bEnableParent = TRUE;
pMainWnd = AfxGetMainWnd();
if (pMainWnd && pMainWnd->IsFrameWnd() && pMainWnd->IsWindowEnabled())
{
//
// We are hosted by non-MFC container
//
pMainWnd->EnableWindow(FALSE);
bEnableMainWnd = TRUE;
}
}
TRY
{
// create modeless dialog
AfxHookWindowCreate(this);
if (!CreateRunDlgIndirect(lpDialogTemplate, CWnd::FromHandle(hWndParent), hInst) && !m_bClosedByEndDialog)
{
// If the resource handle is a resource-only DLL, the dialog may fail to launch. Use the
// module instance handle as the fallback dialog creator instance handle if necessary.
CreateRunDlgIndirect(lpDialogTemplate, CWnd::FromHandle(hWndParent), AfxGetInstanceHandle());
}
m_bClosedByEndDialog = FALSE;
}
CATCH_ALL(e)
{
TRACE(traceAppMsg, 0, "Warning: dialog creation failed.\n");
DELETE_EXCEPTION(e);
m_nModalResult = -1;
}
END_CATCH_ALL
if (bEnableMainWnd)
pMainWnd->EnableWindow(TRUE);
if (bEnableParent)
::EnableWindow(hWndParent, TRUE);
if (hWndParent != NULL && ::GetActiveWindow() == m_hWnd)
::SetActiveWindow(hWndParent);
// destroy modal window
DestroyWindow();
PostModal();
// unlock/free resources as necessary
if (m_lpszTemplateName != NULL || m_hDialogTemplate != NULL)
UnlockResource(hDialogTemplate);
if (m_lpszTemplateName != NULL)
FreeResource(hDialogTemplate);
return m_nModalResult;
}
看到高亮的代码应该就能明白了。MFC在弹出模态对话框的时候,只是把hWndParent给Disable了。然后如果发现主窗口还没被Disable,再补上一刀。后面这个逻辑应该就是为了应对在非模态对话框中弹出模态对话框的情况。
看到了这些逻辑,就可以明白本文最开始讲到的情况是情理之中的。模态对话框只管住了大BOSS和父亲,对于兄弟和叔伯之类,都没有去管。如果情况复杂一点,例如非模态对话框中又弹了第二层的对话框,那么不管这个对话框是模态还是非模态,接下来都有机会整出点问题。
知道了问题的原因,接下来就要寻找靠谱的解决方案了。肯定有人解决过这类问题,因为有些软件没有这种问题。但不知道是因为觉得问题太简单了不值得说,还是想藏着掖着?
希望是前者。因为真正的解决方案确实也很简单。
之前有同事尝试解决这个问题,办法是让模态对话框去通知所有的非模态对话框(或者说需要被Disable的窗口)自行Disable,用了我们软件内部与Windows消息相互独立的另外一套通讯机制。
但这个解决方案有一些问题:模态对话框除了自己开发的那些,还有系统提供的MessageBox、文件打开对话框等,另外还包括第三方库中的那些,都是我们无力触达的。所以虽然它花了很多力气让对话框们去多重继承一个基类,但还是没法彻底解决这类问题。
回头想想,主窗口不是总是会被Disable的么?那么监听主窗口的WM_ENABLE消息,在事件处理函数中把那些需要额外Disable掉的窗口集中处理掉,不是就可以了?
实际做起来也很简单,基本上就是在主窗口的OnEnable()中用EnableWindow(bEnable)把状态传递过去。唯一需要动点脑筋的,就是让那些需要额外Disable掉的窗口把自己的HWND注册到一个主窗口拿得到的地方,也就是说待处理的窗口列表需要管理起来。
具体的实现代码我就不贴了。重要的是解决问题的思路,具体如何实现,可以有很多种办法,选择适合自己的哪一种就好。相信读到这里的,都是合格的Win32/MFC程序员。
















































