MFC 对话框
1、对话框基本要点和生命周期
MFC对话框支持“所见即所得”编程模式。其类型分为模式对话框和非模式对话框。
对话框由一个rc资源文件描述外观,通过ID与一个CPP类相连接,对话框内的控件使用基于ID的变量映射通讯。
模式对话框对象被定义后,通过调用DoModal()函数来显示对话框并进行相关操作,此函数当对话框被关闭时返回。其返回值标明了对话框是点“确定”退 出,还是“取消”。非模式对话框需要与某个View相关联,以便对话框退出时发送消息给对应的Vew进行必要的处理。
在对话框显示前,系统会调用On
ID对于一个组件来说非常重要,通过向导,我们可以将一个变量和一个组件进行关联(映射)来实现数据交换,而这种绑定的关键就是将一个组件的ID与成员变量关联。
2、数据交换机制
控件是对话框的重要组成部分,控件的访问可以通过关联变量实现,包括关联数据变量和控制变量。MFC编程中,通过建立类向导中 的操作可以将窗口控件和对应变量绑定,但是代码操作的是变量,用户操作窗口控件如何让他们同步?UpdateData(Bool true|false)函数正是实现这个功能。
DoDataExchange由框架类调用,用于交换和检验对话框的数据,该函数不直接调用,而是被UpdateData调用。通过update(TRUE)取得控件上的值,处理修改后通过update(FALSE)传回控件。
UpdateData(TRUE) -- 刷新控件的值到对应的变量
UpdateData(FALSE) -- 拷贝变量值到控件显示
UpdataData()---用来刷新对话框
3、特殊的Radio Button
Radio Button控件是分组的,同一组的Radio Button只能有一个被选中。这个机制的实现依赖于TAB顺序,在资源视图下按Ctrl-D键将显示对话框的TAB焦点顺序。举一个例子来说明:
Radio1、Radio2、Radio3是三个不同的Radio Button控件,其焦点顺序为1、2、3。为了实现分组Radio1的Group属性应该为TRUE,其余两个为FALSE。如果又有两个 Radio4、Radio5焦点顺序为6、7。则Radio4的Group属性应为TRUE,Radio4,Radio5被分为一组。
需要注意的是,Radio以Group属性来分组,为了结束前一个组,你应该将焦点顺序为4、8的控件的Group属性设为TRUE,否则编译器会产生一个警告。
4、一些技巧
通过向导,我们可以将一个类成员变量和控件关联以进行数据交换,例如将一个CString类型的变量和Edit控件关联。将一个int变量和一组Radio Button关联。但是,人总有错的时候,当我们修改或需要删除这种关联时,麻烦就来了。
在我的使用VS2005过程中没有发现提供了删除“已被关联的控件成员变量”的向导,所以我使用的是比较麻烦的手动删除。
1)在对话框头文件中删除成员变量的定义
2)在对话框cpp文件中删除构造函数初始化列表中的对应变量的初始化
3)在对话框cpp文件中,根据变量名删除DoDataExchange函数中的对应语句
此时,以class view中的向导中,已经可以重新设定控件所关联的成员变量了。
登录框的制作:
在显示主窗口之前显示一个模式对话框来提示用户登录一个常用的功能。只需要在PreCreateWindow函数中加入显示对话框的代码就可以完成这个功能。
有些时候,我们可能需要从一个控件对象来得到它的ID。比如,你的对话框中好几个滚动条,那么这些滚动条的事件都在On
在这两个函数中有一个CScrollBar *pScrollBar指针,我们可以通过调用pScrollBar->GetDlgCtrllD()来获得ID,ID是一个整数。
在对话框编程中往往需要改变某个控件的文字,比如EDIT控件和Static text控件。此时使用SetDlgItemText(int nID,LPCTSTR lpzString)函数比较方便
MFC窗口销毁过程
考虑单窗口情况:
假设自己通过new创建了一个窗口对象pWnd,然后pWnd->Create。则销毁窗口的调用次序:
1. 手工调用pWnd->DestroyWindow();
2. DestroyWindow会发送WM_DESTROY;
3. WM_DESTROY对应的消息处理函数是OnDestroy();
4. DestroyWindow会发送WM_NCDESTROY;
5. WM_NCDESTROY对应的消息处理函数是OnNcDestroy;
6. OnNcDestroy最后会调用PostNcDestroy;
7. PostNcDestroy经常被用户重载以提供释放内存操作。例如可以使用delete this;
通过这种方式,窗口对象对应的窗口和窗口对象本身都被释放了。
如果含有子窗口:
如果含有子窗口,则调用父窗口的DestroyWindow时,它会向子窗口发送WM_DESTROY和WM_NCDESTROY消息。
具体调用顺序参考下文的例子。
DestroyWindow对delete的影响:
应该说前者对后者并没有什么影响。但经常在DestroyWindow间接导致执行的PostNcDestroy中delete窗口对象指针,即delete this。
CView::PostNcDestroy中唯一的操作就是delete this;CframeWnd::PostNcDestory也是如此。而默认的CWnd::PostNcDestroy是空操作,CDialog中也没有对其进行重载,即也是空。
delete对Destroy的影响:
delete会导致析构函数。CWnd的析构函数中有对DestroyWindow的调用,但必须保证:
m_hWnd != NULL &&
this != (CWnd*) & wndTop && this != (CWnd*)&wndBottom &&
this != (CWnd*)&wndTopMost && this != (CWnd*)&wndNoTopMost。
Cdialog的析构函数中也有对DestroyWindow的调用,但条件比较松,只需要m_hWnd != NULL。另外Cdialog::DoModal也会调用DestroyWindow。
CFrameWnd的OnClose中会调用DestroyWindow,但其析构中不会调用DestroyWindow。
CView的析构也不会调用DestroyWindow。
一个SDI程序的销毁过程
有CMainFrame类、CMyView类。并且CMyView有两个子窗口CMyDlg和CmyWnd的实例。
点击退出按钮,CMainFrame会收到WM_CLOSE消息。CframeWnd(CMainFrame的父类)间接会调用CWnd::DestroyWindow;它首先向CMyView发送WM_DESTORY和WM_NCDESTROY消息,并引发相应的处理函数;然后向CMyDlg发送WM_DESTORY和WM_NCDESTROY消息,并引发相应的处理函数;然后向CMyWnd发送WM_DESTORY和WM_NCDESTROY消息,并引发相应的处理函数。
具体的执行顺序是:
1. 调用CMainFrame::DestroyWindow
2. CFrameWnd::OnDestroy
3. CMyView::OnDestroy
4. CmyWnd::OnDestroy
5. CmyDlg::OnDestroy
6. CmyWnd::PostNcDestroy
7. CmyWnd的析构
8. CmyDlg::OnDestroy
9. CmyDlg的析构
10. CMyView::PostNcDestroy
11. CmyView的析构
12. CMainFrame的析构
13. CMainFrame::DestroyWindow退出
上面情况是假设我们在CmyWnd和CmyDlg的PostNcDestroy中添加了delete this。如果没有添加,则7,10不会执行。
因为CView::PostNcDestroy中调用了delete this,所以然后会执行CMyView的析构操作。因为CframeWnd::PostNcDestroy中调用了delete this,所以最后执行CMainFrame的析构操作。
如果自己的CmyDlg和CmyWnd在PostNcDestroy中有delete this;则二者会被析构。否则内存泄漏。当然delete也可以放在CMyView的析构中做,只是不够OO。
总结
可以有两种方法销毁窗口对象对应的窗口和释放窗口对象指针。一种是通过DestroyWindow。这是比较好的方法,因为最后MFC会自动相应WM_CLOSE导致CframWnd::DestroyWindow被调用,然后会一次释放所有子窗口的句柄。用户需要做的是在PostNcDestroy中释放堆窗口对象指针。但因为某些对象是在栈中申请的,所以delete this可能出错。这就要保证写程序时自己创建的窗口尽量使用堆申请。
另一种是delete。Delete一个窗口对象指针有的窗口类(如CWnd,Cdialog)会间接调用DestroyWindow,有的窗口类(如CView,CframeWn)不会调用DestroyWindow。所以要小心应对。
二者是相互调用的,很繁琐。
一个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控件类。
1. 从CWnd类直接派生出来的子窗口对象(如用户定制的控件)。
2. 切分窗口类CSplitterWnd。
3. 缺省的控制条类(包括工具条、状态条和对话条)。
4. 模态对话框类。
具有自动清除功能的窗口类如下所示,这些窗口对象通常是在堆中创建的。
1. 主框架窗口类(直接或间接从CFrameWnd类派生)。
2. 视图类(直接或间接从CView类派生)。
读者在设计自己的派生窗口类时,可根据窗口对象的创建方法来决定是否将窗口类设计成可以自动清除的。例如,对于一个非模态对话框来说,其对象是创建在堆中的,因此应该具有自动清除功能。
综上所述,对于MFC窗口类及其派生类来说,在程序中一般不必显式删除窗口对象。也就是说,既不必调用DestroyWindow来删除窗口对象封装的窗口,也不必显式地用delete操作符来删除窗口对象本身。只要保证非自动清除的窗口对象是以变量的形式创建的,自动清除的窗口对象是在堆中创建的,MFC的运行机制就可以保证窗口对象的彻底删除。
如果需要手工删除窗口对象,则应该先调用相应的函数(如CWnd::DestroyWindow)删除窗口,然后再删除窗口对象.对于以变量形式创建的窗口对象,窗口对象的删除是框架自动完成的.对于在堆中动态创建了的非自动清除的窗口对象,必须在窗口被删除后,显式地调用delete来删除对象(一般在拥有者或父窗口的析构函数中进行).对于具有自动清除功能的窗口对象,只需调用CWnd::DestroyWindow即可删除窗口和窗口对象。注意,对于在堆中创建的窗口对象,不要在窗口还未关闭的情况下就用delete操作符来删除窗口对象
对话框经常被使用,因为对话框可以从模板创建,而对话框模板是可以使用资源编辑器方便地进行编辑的。
一,模式与非模式对话框
1.模式对话框
一个模式对话框是一个有系统菜单、标题栏、边线等的弹出式窗口。在创建对话框时指定WS_POPUP, WS_SYSMENU, WS_CAPTION和 DS_MODALFRAME风格。即使没有指定WS_VISIBLE风格,模
式对话框也会被显示。创建对话框窗口时,将发送WM_INITDIALOG消息(如果指定对话框的DS_SETFONT风格,还有WM_SETFONT消息)给对话框过程。
对话框窗口被创建之后,Windows使得它成为一个激活的窗口,它保持激活直到对话框过程调用::EndDialog函数结束对话框的运行或者Windows激活另一个应用程序为止,在激活时,用户
或者应用程序不可以激活它的所属窗口(Owner window)。从某个窗口创建一个模式对话框时,Windows自动地禁止使用(Disable)这个窗口和它的所有子窗口,直到该模式对话框被关闭和
销毁。虽然对话框过程可以Enable所属窗口,但是这样做就失去了模式对话框的作用,所以不鼓励这样做。
Windows创建模式对话框时,给当前捕获鼠标输入的窗口(如果有的话)发送消息WM_CANCLEMODE。收到该消息后,应用程序应该终止鼠标捕获(Release the mouse capture)以便于用户能
把鼠标移到模式对话框;否则由于Owner窗口被禁止,程序将失去鼠标输入。
为了处理模式对话框的消息,Windows开始对话框自身的消息循环,暂时控制整个应用程序的消息队列。如果Windows收到一个非对话框消息时,则它把消息派发给适当的窗口处理;如果收
到了WM_QUIT消息,则把该消息放回应用程序的消息队列里,这样应用程序的主消息循环最终能处理这个消息。
当应用程序的消息队列为空时,Windows发送WM_ENTERIDLE消息给Owner窗口。在对话框运行时,程序可以使用这个消息进行后台处理,当然应该注意经常让出控制给模式对话框,以便它能
接收用户输入。如果不希望模式对话框发送 WM_ENTERIDlE消息,则在创建模式对话框时指定DS_NOIDLEMSG风格。
一个应用程序通过调用::EndDialog函数来销毁一个模式对话框。一般情况下,当用户从系统菜单里选择了关闭(Close)命令或者按下了确认(OK)或取消(CANCLE)按钮,::EndDialog
被对话框过程所调用。调用::EndDialog时,指定其参数nResult的值,Windows将在销毁对话框窗口后返回这个值,一般,程序通过返回值判断对话框窗口是否完成了任务或者被用户取消。
2. 无模式对话框
一个无模式对话框是一个有系统菜单、标题栏、边线等的弹出式窗口。在创建对话框模板时指定WS_POPUP、WS_CAPTION、WS_BORDER和WS_SYSMENU风格。如果没有指定WS_VISIBLE风格,无
模式对话框不会自动地显示出来。一个无模式对话框既不会禁止所属窗口,也不会给它发送消息(WM_ENTERIDlE)。当创建一个无模式对话框时,Windows使它成为活动窗口,但用户或者程序
可以随时改变和设置活动窗口。如果对话框失去激活,那么即使所属窗口是活动的,在Z轴顺序上,它仍然在所属窗口之上。
应用程序负责获取和派发输入消息给对话框。大部分应用程序使用主消息循环来处理,但是为了用户可以使用键盘在控制窗口之间移动或者选择控制窗口,应用程序应该调
用::IsDialogMessage函数。
这里,顺便解释::IsDialogMessage函数。虽然该函数是为无模式对话框设计的,但是任何包含了控制子窗口的窗口都可以调用它,用来实现类似于对话框的键盘选择操作。
当::IsDialogMessage处理一个消息时,它检查键盘消息并把它们转换成相应对话框的选择命令。例如,当Tab 键被压下时,下一个或下一组控制被选中,当Down Arrow键按下后,一组控制中
的下一个控制被选择。::IsDialogMessage完成了所有必要的消息转换和消息派发,所以该函数处理的消息一定不要传递给TranslateMessage和DispatchMessage处理。一个无模式对话框不能
像模式对话框那样返回一个值给应用程序。但是对话框过程可以使用::SendMessage给所属窗口传递信息。
在应用程序结束之前,它必须销毁所有的无模式对话框。使用::DestroyWindow销毁一个无模式对话框,不是使用::EndDiaLog。一般来说,对话框过程响应用户输入,如用户选择了“取消
”按钮,则调用::DestroyWindow;如果用户没有有关动作,则应用程序必须调用::DestroyWindow。
二,对话框的MFC实现
1.在MFC中,对话框窗口的功能主要由CWnd和CDialog两个类实现。
- class CDialog : public CWnd
- {
- DECLARE_DYNAMIC(CDialog)
- //初始化函数
- public:
- virtual BOOL Create(LPCTSTR lpszTemplateName, CWnd* pParentWnd = NULL);
- virtual BOOL Create(UINT nIDTemplate, CWnd* pParentWnd = NULL);
- virtual BOOL CreateIndirect(LPCDLGTEMPLATE lpDialogTemplate, CWnd* pParentWnd = NULL,void* lpDialogInit = NULL);
- virtual BOOL CreateIndirect(HGLOBAL hDialogTemplate, CWnd* pParentWnd = NULL);
- BOOL InitModalIndirect(LPCDLGTEMPLATE lpDialogTemplate, CWnd* pParentWnd = NULL,void* lpDialogInit = NULL);
- BOOL InitModalIndirect(HGLOBAL hDialogTemplate, CWnd* pParentWnd = NULL);
- //构造函数
- public:
- CDialog();
- explicit CDialog(LPCTSTR lpszTemplateName, CWnd* pParentWnd = NULL);
- explicit CDialog(UINT nIDTemplate, CWnd* pParentWnd = NULL);
- // 操作函数
- public:
- // modal processing
- virtual INT_PTR DoModal();
- // support for passing on tab control - use 'PostMessage' if needed
- void NextDlgCtrl() const;
- void PrevDlgCtrl() const;
- void GotoDlgCtrl(CWnd* pWndCtrl);
- // default button access
- void SetDefID(UINT nID);
- DWORD GetDefID() const;
- // termination
- void EndDialog(int nResult);
- //重载函数
- protected:
- virtual void OnOK();
- virtual void OnCancel();
- public:
- virtual BOOL OnInitDialog();
- virtual void OnSetFont(CFont* pFont);
- public:
- virtual ~CDialog();
- virtual BOOL PreTranslateMessage(MSG* pMsg);
- virtual BOOL OnCmdMsg(UINT nID, int nCode, void* pExtra,AFX_CMDHANDLERINFO* pHandlerInfo);
- virtual BOOL CheckAutoCenter();
- protected:
- UINT m_nIDHelp; // Help ID (0 for none, see HID_BASE_RESOURCE)
- LPCTSTR m_lpszTemplateName; // name or MAKEINTRESOURCE
- HGLOBAL m_hDialogTemplate; // indirect (m_lpDialogTemplate == NULL)
- LPCDLGTEMPLATE m_lpDialogTemplate; // indirect if (m_lpszTemplateName == NULL)
- void* m_lpDialogInit; // DLGINIT resource data
- CWnd* m_pParentWnd; // parent/owner window
- HWND m_hWndTop; // top level parent window (may be disabled)
- virtual void PreInitDialog();
- HWND PreModal();
- void PostModal();
- };
2.模式对话框的实现
在Win32 SDK编程下的模式对话框使用了Windows提供给对话框窗口的窗口过程和自己的对话框过程,对话框过程将被窗口过程调用。但在MFC下,所有的窗口类都使用了同一个窗口过程,
CDialog也不例外。CDialog对象在创建Windows对话框时,采用了类似于CWnd的创建函数过程,采用子类化的手段将Windows提供给对话框的窗口过程取代为AfxWndProc或者AfxBaseWndProc,
同时提供了对话框过程AfxDlgProc。那么,这些“过程”是如何实现或者协调的呢?下文将予以分析。
I.MFC对话框过程
//MFC对话框过程AfxDlgProc的原型和实现如下:
- BOOL CALLBACK AfxDlgProc(HWND hWnd,UINT message, PARAM, LPARAM)
- {
- if (message == WM_INITDIALOG)
- {
- //处理WM_INITDIALOG消息
- CDialog* pDlg = DYNAMIC_DOWNCAST(CDialog,CWnd::FromHandlePermanent(hWnd));
- if (pDlg != NULL)
- return pDlg->OnInitDialog();
- else
- return 1;
- }
- return 0;
- }
由上可以看出,MFC的对话框函数AfxDlgProc仅处理消息WM_INITDIALOG,其他都留给对话框窗口过程处理。因此,它不同于SDK编程的对话框过程。程序员在SDK的对话框过程处理消息和事
件,实现自己的对话框功能。AfxDlgProc处理WM_INITDIALOG消息时调用虚拟函数OnInitDialog,给程序员一个机会处理对话框的初始化。
II.模式对话框窗口过程
AfxWndProc是所有的MFC窗口类使用的窗口过程,它取代了模式对话框原来的窗口过程(Windows提供),那么,MFC如何完成Win32下对话框窗口的功能呢?考查模式对话框的创建过程。
CDialog::DoModal用来创建模式对话框窗口并执行有关任务,和DoModal相关的是MFC内部使用的成员函数CDialog::PreModal和CDialog::PostModal。下面分别讨论它们的实现。
- HWND CDialog::PreModal()
- {
- ASSERT(m_hWnd == NULL);
- // allow OLE servers to disable themselves
- AfxGetApp()->EnableModeless(FALSE);
- // 得到父窗口
- CWnd* pWnd = CWnd::GetSafeOwner(m_pParentWnd, &m_hWndTop);
- // 如同CWnd处理其他窗口的创建,设置一个窗口创建HOOK
- AfxHookWindowCreate(this);
- //返回父窗口的句柄
- return pWnd->GetSafeHwnd();
- }
- void CDialog::PostModal()
- {
- //取消窗口创建前链接的HOOK
- AfxUnhookWindowCreate(); // just in case
- //MFC对话框对象和对应的Windows对话框窗口分离
- Detach(); // just in case
- // m_hWndTop是当前对话框的父窗口或所属窗口,则恢复它
- if (::IsWindow(m_hWndTop))
- ::EnableWindow(m_hWndTop, TRUE);
- m_hWndTop = NULL;
- AfxGetApp()->EnableModeless(TRUE);
- }
- int CDialog::DoModal()
- {
- ASSERT(m_lpszTemplateName != NULL || m_hDialogTemplate != NULL || m_lpDialogTemplate != NULL);
- //加载对话框资源
- 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);
- if (lpDialogTemplate == NULL)
- return -1;
- //创建对话框前禁止父窗口,为此要调用PreModal得到父窗口句柄
- HWND hWndParent = PreModal();
- AfxUnhookWindowCreate();
- CWnd* pParentWnd = CWnd::FromHandle(hWndParent);
- BOOL bEnableParent = FALSE;
- if (hWndParent != NULL && ::IsWindowEnabled(hWndParent))
- {
- ::EnableWindow(hWndParent, FALSE);
- bEnableParent = TRUE;
- }
- //创建对话框,注意是无模式对话框
- TRY
- {
- //链接一个HOOK到HOOK链以处理窗口创建,
- AfxHookWindowCreate(this);
- //CreateDlgIndirect间接调用::CreateDlgIndirect,最终调用了::CreateWindowEX来创建对话框窗口。
- //HOOK过程_AfxCbtFilterHook用子类化的方法取代原来的窗口过程为AfxWndProc。
- if (CreateDlgIndirect(lpDialogTemplate, CWnd::FromHandle(hWndParent), hInst))
- {
- if (m_nFlags & WF_CONTINUEMODAL)
- {
- // enter modal loop
- DWORD dwFlags = MLF_SHOWONIDLE;
- //RunModalLoop接管整个应用程序的消息处理
- if (GetStyle() & DS_NOIDLEMSG)
- dwFlags |= MLF_NOIDLEMSG;
- VERIFY(RunModalLoop(dwFlags) == m_nModalResult);
- }
- // hide the window before enabling the parent, etc.
- if (m_hWnd != NULL)
- SetWindowPos(NULL, 0, 0, 0, 0, SWP_HIDEWINDOW|SWP_NOSIZE|SWP_NOMOVE|SWP_NOACTIVATE|SWP_NOZORDER);
- }
- }
- CATCH_ALL(e)
- {
- DELETE_EXCEPTION(e);
- m_nModalResult = -1;
- }
- END_CATCH_ALL
- //Enable并且激活父窗口
- if (bEnableParent)
- ::EnableWindow(hWndParent, TRUE);
- if (hWndParent != NULL && ::GetActiveWindow() == m_hWnd)
- ::SetActiveWindow(hWndParent);
- //::EndDialog仅仅关闭了窗口,现在销毁窗口
- DestroyWindow();
- PostModal();
- // 必要的话,解锁/释放资源
- if (m_lpszTemplateName != NULL || m_hDialogTemplate != NULL)
- UnlockResource(hDialogTemplate);
- if (m_lpszTemplateName != NULL)
- FreeResource(hDialogTemplate);
- return m_nModalResult;
- }
III.使用原对话框窗口过程作消息的缺省处理
对话框的消息处理过程和其他窗口并没有什么不同。这里主要分析的是如何把一些消息传递给对话框原窗口过程处理。下面,通过解释MFC对WM_INITDIALOG消息的处理来解释MFC窗口过程
和原对话框窗口过程的关系及其协调作用。MFC提供了WM_INITDIALOG消息的处理函数CDialog::HandleInitDialog,WM_INITDIALOG消息按照标准Windows的处理送给HandleInitDialog处理。
HandleInitDialog调用缺省处理过程Default,导致CWnd的Default函数被调用。CWnd::Default的实现如下:
- LRESULT CWnd::Default()
- {
- // call DefWindowProc with the last message
- _AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData();
- return DefWindowProc(pThreadState->m_lastSentMsg.message,pThreadState->m_lastSentMsg.wParam,pThreadState->m_lastSentMsg.lParam);
- }
顺便指出,从Default的实现可以看出线程状态的一个用途:它把本线程最新收到和处理的消息记录在成员变量m_lastSentMsg中。在Default的实现中,CWnd的DefWindowsProc被调用,其
实现如下:
- LRESULT CWnd::DefWindowProc(UINT nMsg,WPARAM wParam, LPARAM lParam)
- {
- //若“窗口超类(SuperClass)”的窗口过程m_pfnSuper非空,则调用它
- if (m_pfnSuper != NULL)
- return ::CallWindowProc(m_pfnSuper, m_hWnd, nMsg, wParam, lParam);
- //在MFC中,GetSuperWndProcAddr的作用就是返回m_pfnSuper,为什么还
- //要再次调用呢?因为虽然该函数现在是Obsolete,但原来曾经是有用的。
- //如果返回非空,就调用该窗口过程进行处理,否则,由Windows进行缺省处理。
- WNDPROC pfnWndProc;
- if ((pfnWndProc = *GetSuperWndProcAddr()) == NULL)
- return ::DefWindowProc(m_hWnd, nMsg, wParam, lParam);
- else
- return ::CallWindowProc(pfnWndProc, m_hWnd, nMsg, wParam, lParam);
- }
综合上述分析,HandleInitDialog最终调用了窗口过程m_pfnSuper,即Windows提供给“对话框窗口类”的窗口过程,于是该窗口过程调用了对话框过程AfxDlgProc,导致虚拟函数
OnInitDialog被调用。OnInitDialog的MFC缺省实现主要完成三件事情:
调用ExecInitDialog初始化对话框中的控制;调用UpdateData初始化对话框控制中的数据;确定是否显示帮助按钮。所以,程序员覆盖该函数时,一定要调用基类的实现。
MFC采用子类化的方法取代了对话框的窗口过程,原来SDK下对话框过程要处理的东西大部分转移给MFC窗口过程处理,如处理控制窗口的控制通知消息等。如果不能处理或者必须借助于原来的
窗口过程的,则通过缺省处理函数Default传递给原来的窗口过程处理,如同这里对WM_INITDIALOG的处理一样。
IV.Dialog命令消息和控制通知消息的处理
通过覆盖CWnd的命令消息发送函数OnCmdMsg,CDialog实现了自己的命令消息发送路径
- BOOL CDialog::OnCmdMsg(UINT nID, int nCode, void* pExtra,AFX_CMDHANDLERINFO* pHandlerInfo)
- {
- //首先,让对话框窗口自己或者基类处理
- if (CWnd::OnCmdMsg(nID, nCode, pExtra, pHandlerInfo))
- return TRUE;
- //如果还未处理,且是控制通知消息或者状态更新消息或者系统命令则停止进一步的发送
- if ((nCode != CN_COMMAND && nCode != CN_UPDATE_COMMAND_UI) ||!IS_COMMAND_ID(nID) || nID >= 0xf000)
- return FALSE; // not routed any further
- //尝试给父窗口处理
- CWnd* pOwner = GetParent();
- if (pOwner != NULL)
- {
- ASSERT(pOwner != this);
- if (pOwner->OnCmdMsg(nID, nCode, pExtra, pHandlerInfo))
- return TRUE;
- }
- // 最后,给当前线程对象处理
- CWinThread* pThread = AfxGetThread();
- if (pThread != NULL)
- {
- if (pThread->OnCmdMsg(nID, nCode, pExtra, pHandlerInfo))
- return TRUE;
- }
- return FALSE;
- }
从上述实现可以看出,CDialog处理命令消息遵循如下顺序:对话框自身→父窗口→线程对象
例如,模式对话框产生的WM_ENTERIDLE消息就发送给父窗口处理。
CDialog::OnCmdMsg不仅适用于模式对话框,也适用于无模式对话框。
V.消息预处理和Dialog消息
另外,对话框窗口的消息处理还有一个特点,就是增加了对Dialog消息的处理,如同在介绍::IsDialogMessage函数时所述。如果是 Dialog消息,MFC框架将不会让它进入下一步的消息循
环。为此,MFC覆盖了CDialog的虚拟函数PreTranslateMessage,该函数的实现如下:
- BOOL CDialog::PreTranslateMessage(MSG* pMsg)
- {
- ASSERT(m_hWnd != NULL);
- //过滤tooltip messages
- if (CWnd::PreTranslateMessage(pMsg))
- return TRUE;
- //在Shift+F1帮助模式下,不转换Dialog messages
- CFrameWnd* pFrameWnd = GetTopLevelFrame();
- if (pFrameWnd != NULL && pFrameWnd->m_bHelpMode)
- return FALSE;
- //处理Escape键按下的消息
- if (pMsg->message == WM_KEYDOWN &&(pMsg->wParam == VK_ESCAPE || pMsg->wParam == VK_CANCEL) &&
- (::GetWindowLong(pMsg->hwnd, GWL_STYLE) & ES_MULTILINE) &&_AfxCompareClassName(pMsg->hwnd, _T("Edit")))
- {
- HWND hItem = ::GetDlgItem(m_hWnd, IDCANCEL);
- if (hItem == NULL || ::IsWindowEnabled(hItem))
- {
- SendMessage(WM_COMMAND, IDCANCEL, 0);
- return TRUE;
- }
- }
- // 过滤来自控制该对话框子窗口的送给该对话框的Dialog消息
- return PreTranslateInput(pMsg);
- }
用到的函数:
- BOOL CWnd::PreTranslateMessage(MSG* pMsg)
- {
- // handle tooltip messages (some messages cancel, some may cause it to popup)
- AFX_MODULE_STATE* pModuleState = _AFX_CMDTARGET_GETSTATE();
- if (pModuleState->m_pfnFilterToolTipMessage != NULL)
- (*pModuleState->m_pfnFilterToolTipMessage)(pMsg, this);
- // no default processing
- return FALSE;
- }
- BOOL CWnd::PreTranslateInput(LPMSG lpMsg)
- {
- ASSERT(::IsWindow(m_hWnd));
- // don't translate non-input events
- if ((lpMsg->message < WM_KEYFIRST || lpMsg->message > WM_KEYLAST) &&
- (lpMsg->message < WM_MOUSEFIRST || lpMsg->message > AFX_WM_MOUSELAST))
- return FALSE;
- return IsDialogMessage(lpMsg);
- }
- BOOL CWnd::IsDialogMessage(LPMSG lpMsg)
- {
- ASSERT(::IsWindow(m_hWnd));
- if (m_nFlags & WF_OLECTLCONTAINER)
- return afxOccManager->IsDialogMessage(this, lpMsg);
- else
- return ::IsDialogMessage(m_hWnd, lpMsg);
- }
根据IsDialogMessage(),对话框无法处理WM_KEYUP等消息,我们可以重载PreTranslateMessage()。PreTranslateMessage的实现不仅用于模式对话框,而且用于无模式对话框。
VI.模式对话框的消息循环
从DoModal的实现可以看出,DoModal调用CreateDlgIndirect创建的是无模式对话框,MFC如何来接管和控制应用程序的消息队列,实现一个模式对话框的功能呢?
CDialog调用了RunModalLoop来实现模式窗口的消息循环。RunModalLoop是CWnd的成员函数,它和相关函数的实现如下:
- int CWnd::RunModalLoop(DWORD dwFlags)
- {
- ASSERT(::IsWindow(m_hWnd));
- BOOL bIdle = TRUE;
- LONG lIdleCount = 0;
- BOOL bShowIdle = (dwFlags & MLF_SHOWONIDLE) &&!(GetStyle() & WS_VISIBLE);
- HWND hWndParent = ::GetParent(m_hWnd);
- m_nFlags |= (WF_MODALLOOP|WF_CONTINUEMODAL);
- MSG* pMsg = &AfxGetThread()->m_msgCur;
- //获取和派发消息直到模式状态结束
- for (;;)
- {
- ASSERT(ContinueModal());/
- //第一阶段,判断是否可以进行Idle处理
- while (bIdle &&!::PeekMessage(pMsg, NULL, NULL, NULL, PM_NOREMOVE))
- {
- ASSERT(ContinueModal());
- //必要的话,当Idle时显示对话框窗口
- if (bShowIdle)
- {
- ShowWindow(SW_SHOWNORMAL);
- UpdateWindow();
- bShowIdle = FALSE;
- }
- //进行Idle处理,必要的话发送WM_ENTERIDLE消息给父窗口
- if (!(dwFlags & MLF_NOIDLEMSG) &&hWndParent != NULL && lIdleCount == 0)
- {
- ::SendMessage(hWndParent, WM_ENTERIDLE,
- MSGF_DIALOGBOX, (LPARAM)m_hWnd);
- }
- //必要的话发送WM_KICKIDLE消息给父窗口
- if ((dwFlags & MLF_NOKICKIDLE) ||!SendMessage(WM_KICKIDLE, MSGF_DIALOGBOX, lIdleCount++))
- {
- //终止Idle处理
- bIdle = FALSE;
- }
- }
- //第二阶段,发送消息
- do
- {
- ASSERT(ContinueModal());
- // 若是WM_QUIT消息,则发送该消息到消息队列,返回;否则发送消息。
- if (!AfxGetThread()->PumpMessage())
- {
- AfxPostQuitMessage(0);
- return -1;
- }
- //必要的话,显示对话框窗口
- if (bShowIdle &&(pMsg->message == 0x118 || pMsg->message == WM_SYSKEYDOWN))
- {
- ShowWindow(SW_SHOWNORMAL);
- UpdateWindow();
- bShowIdle = FALSE;
- }
- if (!ContinueModal())
- goto ExitModal;
- //在派发了“正常 ”消息后,重新开始Idle处理
- if (AfxGetThread()->IsIdleMessage(pMsg))
- {
- bIdle = TRUE;
- lIdleCount = 0;
- }
- } while (::PeekMessage(pMsg, NULL, NULL, NULL, PM_NOREMOVE));
- }
- ExitModal:
- m_nFlags &= ~(WF_MODALLOOP|WF_CONTINUEMODAL);
- return m_nModalResult;
- }
和CWinThread::Run的处理过程比较,RunModalLoop也分两个阶段进行处理。这里不同于Run的Idle处理,RunModalLoop是给父窗口发送WM_ENTERIDLE消息(如果需要的话);另外,当前对
话框的父窗口被Disabled,是不接收用户消息的。RunModalLoop是一个实现自己的消息循环的示例,消息循环的条件是模式化状态没有结束。实现线程自己的消息循环见8.5.6节。当用户按下
按钮“取消”、“确定”时,将导致RunModalLoop退出消息循环,结束对话框模式状态,并调用::EndDialog关闭窗口。
OnOk、OnCancle、EndDialog都可以用来关闭对话框窗口。其中:
OnOk首先进行数据交换,获取对话框中各个控制子窗口的数据,然后调用EndDialog结束对话框。
OnCancle直接EndDialog结束对话框。
三.数据交换
对话框数据交换指以下两种动作,或者是把内存数据写入对应的控制窗口,或者是从控制窗口读取数据并保存到内存变量中。MFC为了简化这些操作,以CDataExchange类和一些数据交换函
数为基础,提供了一套数据交换和校验的机制。
相应的DoDataExchange的实现如下:
- void CExDialog::DoDataExchange(CDataExchange* pDX)
- {
- CDialog::DoDataExchange(pDX);
- DDX_Control(pDX, IDC_NAME, m_name);
- DDX_Text(pDX, IDC_AGE, m_nAge);
- DDV_MinMaxInt(pDX, m_nAge, 1, 100);
- }
DDX_ Control表示把IDC_NAME子窗口的内容传输到窗口m_name,或者相反。
DDX_ Text表示把IDC_AGE子窗口的内容按整数类型保存到m_nAge,或者相反。
DDV_MinMaxInt表示m_nAge应该在1和100之间取值
1.CDataExchange类
- class CDataExchange
- {
- public:
- BOOL m_bSaveAndValidate; // TRUE 则 保存和验证数据
- CWnd* m_pDlgWnd; // 指向一个对话框
- HWND PrepareCtrl(int nIDC); //返回指定ID的控制窗口的句柄
- HWND PrepareEditCtrl(int nIDC); //返回指定ID的编辑控制窗口句柄
- void Fail(); // 用来扔出例外
- CDataExchange(CWnd* pDlgWnd, BOOL bSaveAndValidate);
- HWND m_hWndLastControl; // last control used (for validation)
- BOOL m_bEditLastControl; // last control was an edit item
- };
DoDataExchange类似于Serialize函数,CDataExchange类似于 CArchive。CDataExchange使用成员变量m_pDlgWnd保存要进行数据交换的对话框,使用成员变量 m_bSaveAndValidate指示数
据传输的方向,如果该变量真,则从控制窗口读取数据到成员变量,如果假,则从成员变量写数据到控制窗口。
在构造一个CDataExchange对象时,将保存有关信息在对象的成员变量中。构造函数如下:
- CDataExchange::CDataExchange(CWnd* pDlgWnd, BOOL bSaveAndValidate)
- {
- ASSERT_VALID(pDlgWnd);
- m_bSaveAndValidate = bSaveAndValidate;
- m_pDlgWnd = pDlgWnd;
- m_hWndLastControl = NULL;
- }
2.数据交换和验证函数
在进行数据交换或者验证时,首先使用PrePareCtrl或者PrePareEditCtrl得到控制窗口的句柄,然后使用::GetWindowsText从控制窗口读取数据,或者使用::SetWindowsText写入数据到控制
窗口。下面讨论几个例子:
- static void AFX_CDECL DDX_TextWithFormat(CDataExchange* pDX,int nIDC,LPCTSTR lpszFormat, UINT nIDPrompt, ...)
- {
- va_list pData; //用来处理个数可以变化的参数
- va_start(pData, nIDPrompt);//得到参数
- //得到编辑框的句柄
- HWND hWndCtrl = pDX->PrepareEditCtrl(nIDC);
- TCHAR szT[32];
- if (pDX->m_bSaveAndValidate) //TRUE,从编辑框读出数据
- {
- //从编辑框得到内容
- ::GetWindowText(hWndCtrl, szT, _countof(szT));
- //转换编辑框内容为指定的格式,支持“ %d, %u, %ld, %lu”
- if (!AfxSimpleScanf(szT, lpszFormat, pData))
- {
- AfxMessageBox(nIDPrompt);
- pDX->Fail(); //数据交换失败
- }
- }
- else //FALSE,写入数据到编辑框
- {
- //把要写的内容转换成指定格式,不支持浮点运算
- wvsprintf(szT, lpszFormat, pData);
- //设置编辑框的内容
- AfxSetWindowText(hWndCtrl, szT);
- }
- va_end(pData);//结束参数分析
- }
- void AFXAPI DDX_Text(CDataExchange* pDX, int nIDC, long& value)
- {
- if (pDX->m_bSaveAndValidate)
- DDX_TextWithFormat(pDX, nIDC, _T("%ld"), AFX_IDP_PARSE_INT, &value);
- else
- DDX_TextWithFormat(pDX, nIDC, _T("%ld"), AFX_IDP_PARSE_INT, value);
- }
3.UpdateData函数
- BOOL CWnd::UpdateData(BOOL bSaveAndValidate)
- {
- ASSERT(::IsWindow(m_hWnd));
- //创建CDataChange对象
- CDataExchange dx(this, bSaveAndValidate);
- DoDataExchange(&dx);
- //.................
- return bOK;
- }
四,模式对话框的使用
CFormView是MFC使用无模式对话框的一个典型例子。CFormView是基于对话框模板创建的视,它的直接基类是CSrcollView,CSrcollView的直接基类才是CView。
这样不做详细介绍了