与模态对话框不同,非模态对话框不垄断用户的输入,用户打开非模态对话框后,仍然可以与其它界面进行交互。
非模态对话框的设计与模态对话框基本类似,也包括设计对话框模板和设计 CDialog 类的派生类两部分。但是,在对话框的创建和删除过程中,非模态对话框与模态对话框相比有下列不同之处:
非模态对话框的模板必须具有Visible 风格,否则对话框将不可见,而模态对话框则无需设置该项风格。更保险的办法是调用CWnd::ShowWindow(SW_SHOW) 来显示对话框,而不管对话框是否具有Visible 风格。
非模态对话框对象是用new 操作符在堆中动态创建的,而不是以成员变量的形式嵌入到别的对象中或以局部变量的形式构建在堆栈上。通常应在对话框的拥有者窗口类内声明一个指向对话框类的指针成员变量,通过该指针可访问对话框对象。
通过调用CDialog::Create 函数来启动对话框,而不是CDialog::DoModal ,这是模态对话框的关键所在。由于Create 函数不会启动新的消息循环,对话框与应用程序共用同一个消息循环,这样对话框就不会垄断用户的输入。Create 在显示了对话框后就立即返回,而DoModal 是在对话框被关闭后才返回的。众所周知,在MFC 程序中,窗口对象的生存期应长于对应的窗口,也就是说,不能在未关闭屏幕上窗口的情况下先把对应的窗口对象删除掉。由于在Create 返回后,不能确定对话框是否已关闭,这样也就无法确定对话框对象的生存期,因此只好在堆中构建对话框对象,而不能以局部变量的形式来构建之。
必须调用CWnd::DestroyWindow 而不是CDialog::EndDialog 来关闭非模态对话框。调用CWnd::DestroyWindow 是直接删除窗口的一般方法。由于缺省的CDialog::OnOK 和CDialog::OnCancel 函数均调用EndDialog ,故程序员必须编写自己的OnOK 和OnCancel 函数并且在函数中调用DestroyWindow 来关闭对话框。
因为是用new 操作符构建非模态对话框对象,因此必须在对话框关闭后,用delete 操作符删除对话框对象。在屏幕上一个窗口被删除后,框架会调用CWnd::PostNcDestroy ,这是一个虚拟函数,程序可以在该函数中完成删除窗口对象的工作,具体代码如下 void CModelessDialog::PostNcDestroy { delete this; // 删除对象本身 } 这样,在删除屏幕上的对话框后,对话框对象将被自动删除。拥有者对象就不必显式的调用delete 来删除对话框对象了。
必须有一个标志表明非模态对话框是否是打开的。这样做的原因是用户有可能在打开一个模态对话框的情况下,又一次选 择打开命令。程序根据标志来决定是打开一个新的对话框,还是仅仅把原来打开的对话框激活。通常可以用拥有者窗口中的指向对话框对象的指针作为这种标志,当 对话框关闭时,给该指针赋NULL 值,以表明对话框对象已不存在了。
提示:在C++ 编程中,判断一个位于堆中的对象是否存在的常用方法是判断指向该对象的指针是否为空。这种机制要求程序员将指向该对象的指针初始化为 NULL 值,在创建对象时将返回的地址赋给该指针,而在删除对象时将该指针置成 NULL 值。
根据上面的分析,我们很容易把 Register 程序中的登录数据对话框改成非模态对话框。这样做的好处在于如果用户在输入数据时发现编辑视图中有错误的数据,那么不必关闭对话框,就可以在编辑视图中进行修改。
请读者按下面几步操作:
在登录数据对话框模板的属性对话框的 More Styles 页中选择 Visible 项。
在 RegisterView.h 头文件的 CRegisterView 类的定义中加入 public: CRegisterDialog* m_pRegisterDlg;
在 RegisterView.h 头文件的头部加入对 CRegisterDialog 类的声明 class CRegisterDialog; 加入该行的原因是在 CRegisterView 类中有一个 CRegisterDialog 类型的指针,因此必须保证 CRegisterDialog 类的声明出现在 CRegisterView 之前,否则编译时将会出错。解决这个问题有两种办法,一种办法是保证在 #include “ RegisterView.h ”语句之前有 #include “ RegisterDialog.h ”语句,这种办法造成了一种依赖关系,增加了编译负担,不是很好;另一种办法是在 CRegisterView 类的声明之前加上一个对 CRegisterDialog 的声明来暂时“蒙蔽”编译器,这样在有 #include “ RegisterView.h ”语句的模块中,除非要用到 CRegisterDialog 类,否则不用加入 #include “ RegisterDialog.h ”语句。
在 RegisterDialog.cpp 文件的头部的 #include 语句区的末尾添加下面两行 #include "RegisterDoc.h" #include "RegisterView.h"
利用 ClassWizard 为 CRegisterDialog 类加入 OnCancel 和 PostNcDestroy 成员函数。加入的方法是进入 ClassWizard 后选择 Message Maps 页,并在 Class name 栏中选择 CRegisterDialog 。然后,在 Object IDs 栏中选择 IDCANCEL 后,在 Messages 栏中双击 BN_CLICKED ,这就创建了 OnCancel 。要创建 PostNcDestroy ,先在 Object IDs 栏中选择 CRegisterDialog ,再在 Messages 栏中双击 PostNcDestroy 即可。
分别按清单 5.10 和 5.11 ,对 CRegisterView 类和 CRegisterDialog 类进行修改。
清单 5.10 CRegisterView 类的部分代码
CRegisterView::CRegisterView()
{
// TODO: add construction code here
m_pRegisterDlg=NULL; // 指针初始化为 NULL
}
void CRegisterView::OnEditRegister()
{
// TODO: Add your command handler code here
if(m_pRegisterDlg)
m_pRegisterDlg->SetActiveWindow(); // 激活对话框
else
{
// 创建非模态对话框
m_pRegisterDlg=new CRegisterDialog(this);
m_pRegisterDlg->Create(IDD_REGISTER,this);
}
}
清单 5.11 CRegisterDialog 的部分代码
void CRegisterDialog::PostNcDestroy()
{
// TODO: Add your specialized code here and/or call the base class
delete this; // 删除对话框对象
}
void CRegisterDialog::OnCancel()
{
// TODO: Add extra cleanup here
((CRegisterView*)m_pParent)->m_pRegisterDlg=NULL;
DestroyWindow(); // 删除对话框
}
CRegisterView::OnEditRegister 函数判断登录数据对话框是否已打开,若是,就激活对话框,否则,就创建该对话框。该函数中主要调用了下列函数:
调用 CWnd::SetActiveWindow 激活对话框,该函数的声明为 CWnd* SetActiveWindow( ); 该函数使本窗口成为活动窗口,并返回原来活动的窗口。
调用 CDialog::Create 来显示对话框,该函数的声明为 BOOL Create( UINT nIDTemplate, CWnd* pParentWnd = NULL ); 参数 nIDTemplate 是对话框模板的 ID 。 pParentWnd 指定了对话框的父窗口或拥有者。
当用户在登录数据对话框中点击“取消”按钮后, CRegisterDialog::OnCancel 将被调用,在该函数中调用 CWnd::DestroyWindow 来关闭对话框,并且将 CRegisterView 的成员 m_pRegisterDlg 置为 NULL 以表明对话框被关闭了。调用 DestroyWindow 导致了对 CRegisterDialog::PostNcDestroy 的调用,在该函数中用 delete 操作符删除了 CRegisterDialog 对象本身。
编译并运行 Register ,现在登录数据对话框已经变成一个非模态对话框了。
5.4.2 窗口对象的自动清除
一个 MFC 窗口对象包括两方面的内容:一是窗口对象封装的窗口,即存放在 m_hWnd 成员中的 HWND (窗口句柄),二是窗口对象本身是一个 C++ 对象。要删除一个 MFC 窗口对象,应该先删除窗口对象封装的窗口,然后删除窗口对象本身。
删除窗口最直接方法是调用 CWnd::DestroyWindow 或 ::DestroyWindow ,前者封装了后者的功能。前者不仅会调用后者,而且会使成员 m_hWnd 保存的 HWND 无效 (NULL) 。如果 DestroyWindow 删除的是一个父窗口或拥有者窗口,则该函数会先自动删除所有的子窗口或被拥有者,然后再删除父窗口或拥有者。在一般情况下,在程序中不必直接调用 DestroyWindow 来删除窗口,因为 MFC 会自动调用 DestroyWindow 来删除窗口。例如,当用户退出应用程序时,会产生 WM_CLOSE 消息,该消息会导致 MFC 自动调用 CWnd::DestroyWindow 来删除主框架窗口,当用户在对话框内按了 OK 或 Cancel 按钮时, MFC 会自动调用 CWnd::DestroyWindow 来删除对话框及其控件。
窗口对象本身的删除则根据对象创建方式的不同,分为两种情况。在 MFC 编程中,会使用大量的窗口对象,有些窗口对象以变量的形式嵌入在别的对象内或以局部变量的形式创建在堆栈上,有些则用 new 操作符创建在堆中。对于一个以变量形式创建的窗口对象,程序员不必关心它的删除问题,因为该对象的生命期总是有限的,若该对象是某个对象的成员变量,它会随着父对象的消失而消失,若该对象是一个局部变量,那么它会在函数返回时被清除。
对于一个在堆中动态创建的窗口对象,其生命期却是任意长的。初学者在学习 C++ 编程时,对 new 操作符的使用往往不太踏实,因为用 new 在堆中创建对象,就不能忘记用 delete 删除对象。读者在学习 MFC 的例程时,可能会产生这样的疑问,为什么有些程序用 new 创建了一个窗口对象,却未显式的用 delete 来删除它呢?问题的答案就是有些 MFC 窗口对象具有自动清除的功能。
如前面讲述非模态对话框时所提到的,当调用 CWnd::DestroyWindow 或 ::DestroyWindow 删除一个窗口时,被删除窗口的 PostNcDestroy 成员函数会被调用。缺省的 PostNcDestroy 什么也不干,但有些 MFC 窗口类会覆盖该函数并在新版本的 PostNcDestroy 中调用 delete this 来删除对象,从而具有了自动清除的功能。此类窗口对象通常是用 new 操作符创建在堆中的,但程序员不必操心用 delete 操作符去删除它们,因为一旦调用 DestroyWindow 删除窗口,对应的窗口对象也会紧接着被删除。
不具有自动清除功能的窗口类如下所示。这些窗口对象通常是以变量的形式创建的,无需自动清除功能。
所有标准的 Windows 控件类。
从 CWnd 类直接派生出来的子窗口对象(如用户定制的控件)。
切分窗口类 CSplitterWnd 。
缺省的控制条类(包括工具条、状态条和对话条)。
模态对话框类。
具有自动清除功能的窗口类如下所示,这些窗口对象通常是在堆中创建的。
主框架窗口类(直接或间接从 CFrameWnd 类派生)。
视图类(直接或间接从 CView 类派生)。
读者在设计自己的派生窗口类时,可根据窗口对象的创建方法来决定是否将窗口类设计成可以自动清除的。例如,对于一个非模态对话框来说,其对象是创建在堆中的,因此应该具有自动清除功能。
综上所述,对于 MFC 窗口类及其派生类来说,在程序中一般不必显式删除窗口对象。也就是说,既不必调用 DestroyWindow 来删除窗口对象封装的窗口,也不必显式地用 delete 操作符来删除窗口对象本身。只要保证非自动清除的窗口对象是以变量的形式创建的,自动清除的窗口对象是在堆中创建的, MFC 的运行机制就可以保证窗口对象的彻底删除。
如果需要手工删除窗口对象,则应该先调用相应的函数(如 CWnd::DestroyWindow )删除窗口,然后再删除窗口对象.对于以变量形式创建的窗口对象,窗口对象的删除是框架自动完成的.对于在堆中动态创建了的非自动清除的窗口对象,必须在窗口被删除后,显式地调用 delete 来删除对象(一般在拥有者或父窗口的析构函数中进行).对于具有自动清除功能的窗口对象,只需调用 CWnd::DestroyWindow 即可删除窗口和窗口对象。注意,对于在堆中创建的窗口对象,不要在窗口还未关闭的情况下就用 delete 操作符来删除窗口对象.
提示:在非模态对话框的OnCancel 函数中可以不调用 CWnd::DestroyWindow ,取而代之的是调用 CWnd::ShowWindow(SW_HIDE) 来隐藏对话框.在下次打开对话框时就不必调用 Create 了,只需调用 CWnd::ShowWindow(SW_SHOW) 来显示对话框.这样做的好处在于对话框中的数据可以保存下来,供以后使用.由于拥有者窗口在被关闭时会调用 DestroyWindow 删除每一个所属窗口,故只要非模态对话框是自动清除的,程序员就不必担心对话框对象的删除问题.