深入探讨mfc消息循环和消息泵_第1页
深入探讨mfc消息循环和消息泵_第2页
深入探讨mfc消息循环和消息泵_第3页
深入探讨mfc消息循环和消息泵_第4页
深入探讨mfc消息循环和消息泵_第5页
已阅读5页,还剩8页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

深入探讨 MFC消息循环和消息泵首先,应该清楚 MFC的消息循环(:GetMessage,:PeekMessage),消息泵(CWinThread:PumpMessage)和MFC的消息在窗口之间的路由是两件不同的事情。在 MFC的应用程序中(应用程序类基于 CWinThread继承),必须要有一个消息循环,他的作用是从应用程序的消息队列中读取消息,并把它派送出去(:DispatchMessage)。而消息路由是指消息派送出去之后,系统(USER32.DLL)把消息投递到哪个窗口,以及以后消息在窗口之间的传递是 怎样的。消息分为队列消息(进入线程的消息队列)和非队列消息(不进入线程的消息队列)。对于队列消息,最常见的是鼠标和键盘触发的消息,例如 WM_MOUSERMOVE,WM_CHAR等消息;还有例如:WM_PAINT、WM_TIMER和 WM_QUIT。当鼠标、键盘事件被触发后,相应的鼠标或键盘驱动程序就会把这些事件转换成相应的消息,然后输送到系统消息队列,由 Windows系统负责把消息加入到相应线程的消息队列中,于是就有了消息循环(从消息队列中读取并派送消息)。还有一种是非队列消息,他绕过系统队列和消息队列,直接将消息发送到窗口过程。例如,当用户激活一个窗口系统发送 WM_ACTIVATE, WM_SETFOCUS, and WM_SETCURSOR。创建窗口时发送 WM_CREATE消息。在后面你将看到,MS 这么设计是很有道理的,以及他的整套实现机制。这里讲述 MFC的消息循环,消息泵。先看看程序启动时,怎么进入消息循环的:_tWinMain -AfxWinMain -AfxWinInit -CWinThread:InitApplication -CWinThread:InitInstance -CWinThread:Run非对话框程序的消息循环的事情都从这 CWinThread的一 Run开始.第一部分:非对话框程序的消息循环机制。/thrdcore.cpp/ main running routine until thread exitsint CWinThread:Run()ASSERT_VALID(this);/ for tracking the idle time stateBOOL bIdle = TRUE;LONG lIdleCount = 0;/ acquire and dispatch messages until a WM_QUIT message is received.for (;)/ phase1: check to see if we can do idle workwhile (bIdle / assume “no idle“ state/ phase2: pump messages while availabledo/ pump message, but quit on WM_QUITif (!PumpMessage()return ExitInstance();/ reset “no idle“ state after pumping “normal“ messageif (IsIdleMessage(lIdleCount = 0; while (:PeekMessage( /无限循环,退出条件是收到 WM_QUIT消息。ASSERT(FALSE); / not reachable这是一个无限循环,他的退出条件是收到 WM_QUIT消息:if (!PumpMessage()return ExitInstance();在 PumpMessage中,如果收到 WM_QUIT消息,那么返回 FALSE,所以 ExitInstance()函数执行,跳出循环,返回程序的退出代码。所以,一个程序要退出,只用在代码中调用函数VOID PostQuitMessage( int nExitCode )。指定退出代码 nExitCode就可以退出程序。下面讨论一下这个函数 Run的流程,分两步:1,第一个内循环 phase1。bIdle 代表程序是否空闲。他的意思就是,如果程序是空闲并且消息队列中没有要处理的消息,那么调用虚函数 OnIdle进行空闲处理。在这个处理中将更新 UI界面(比如工具栏按钮的 enable和 disable状态),删除临时对象(比如用 FromHandle得到的对象指针。由于这个原因,在函数之间传递由 FromHandle得到的对象指针是不安全的,因为他没有持久性)。OnIdle 是可以重载的,你可以重载他并返回 TRUE使消息循环继续处于空闲状态。NOTE:MS 用临时对象是出于效率上的考虑,使内存有效利用,并能够在空闲时自动撤销资源。关于由句柄转换成对象,可以有若干种方法。一般是先申明一个对象 obj,然后使用 obj.Attatch来和一个句柄绑定。这样产生的对象是永久的,你必须用 obj.Detach来释放对象。2,第二个内循环 phase2。在这个循环内先启动消息泵(PumpMessage),如果不是 WM_QUIT消息,消息泵将消息发送出去(:DispatchMessage)。消息的目的地是消息结构中的 hwnd字段所对应的窗口。/thrdcore.cppBOOL CWinThread:PumpMessage()ASSERT_VALID(this);/如果是 WM_QUIT就退出函数(return FALSE),这将导致程序结束.if (!:GetMessage(m_nDisablePumpCount+; / application must die/ Note: prevents calling message loop things in ExitInstance/ will never be decremented#endifreturn FALSE;#ifdef _DEBUGif (m_nDisablePumpCount != 0)TRACE0(“Error: CWinThread:PumpMessage called when not permitted.n“);ASSERT(FALSE);#endif#ifdef _DEBUGif (afxTraceFlags #endif/ process this messageif (m_msgCur.message != WM_KICKIDLE /键转换:DispatchMessage( /派送消息return TRUE;?在这一步有一个特别重要的函数大家一定认识:PreTranslateMessage。这个函数在:DispatchMessage发送消息到窗口之前,进行对消息的预处理。PreTranslateMessage 函数是 CWinThread的成员函数,大家重载的时候都是在 View类或者主窗口类中,那么,它是怎么进入别的类的呢?代码如下:/thrdcore.cppBOOL CWinThread:PreTranslateMessage(MSG* pMsg)ASSERT_VALID(this);/ 如果是线程消息,那么将会调用线程消息的处理函数if (pMsg-hwnd = NULL / walk from target to main windowCWnd* pMainWnd = AfxGetMainWnd();if (CWnd:WalkPreTranslateTree(pMainWnd-GetSafeHwnd(), pMsg)return TRUE;/ in case of modeless dialogs, last chance route through main/ windows accelerator tableif (pMainWnd != NULL)CWnd* pWnd = CWnd:FromHandle(pMsg-hwnd);if (pWnd-GetTopLevelParent() != pMainWnd)return pMainWnd-PreTranslateMessage(pMsg);return FALSE; / no special processing由上面这个函数可以看出: 第一,如果(pMsg-hwnd = NULL),说明这是一个线程消息。调用CWinThread:DispatchThreadMessageEx到消息映射表找到消息入口,然后调用消息处理函数。NOTE: 一般用 PostThreadMessage函数发送线程之间的消息,他和窗口消息不同,需要指定线程 id,消息激被系统放入到目标线程的消息队列中;用 ON_THREAD_MESSAGE( message, memberFxn )宏可以映射线程消息和他的处理函数。这个宏必须在应用程序类(从 CWinThread继承)中,因为只有应用程序类才处理线程消息。如果你在别的类(比如视图类)中用这个宏,线程消息的消息处理函数将得不到线程消息。第二,消息的目标窗口的 PreTranslateMessage函数首先得到消息处理权,如果函数返回 FALSE,那么他的父窗口将得到消息的处理权,直到主窗口;如果函数返回 TRUE(表示消息已经被处理了),那么就不需要调用父类的 PreTranslateMessage函数。这样,保证了消息的目标窗口以及他的父窗口都可以有机会调用 PreTranslateMessage-在消息发送到窗口之前进行预处理(如果自己处理完然后返回 FALSE的话 -_-b),如果你想要消息不传递给父类进行处理的话,返回 TRUE就行了。第三,如果消息的目标窗口和主窗口没有父子关系,那么再调用主窗口的 PreTranslateMessage函数。为什么这样?由第二步知道,一个窗口的父窗口不是主窗口的话,尽管它的 PreTranslateMessage返回FALSE,主窗口也没有机会调用 PreTranslateMessage函数。我们知道,加速键的转换一般在框架窗口的PreTranslateMessage函数中。我找遍了 MFC中关于加速键转换的处理,只有 CFrameWnd,CMDIFrameWnd,CMDIChildWnd 等窗口类有。所以,第三步的意思是,如果消息的目标窗口(他的父窗口不是主窗口,比如一个这样的非模式对话框)使消息的预处理继续漫游的话(他的 PreTranslateMessage返回 FALSE),那么给一次机会给主窗口调用PreTranslateMessage(万一他是某个加速键消息呢?),这样能够保证在有非模式对话框的情况下还能保证主窗口的加速键好使。我做了一个小例子,在对话框类的 PreTranslateMessage中,返回 FALSE。在主窗口显示这个非模式对话框,在对话框拥有焦点的时候,仍然能够激活主窗口的快捷键。 总之,整个框架就是让每个消息的目标窗口(包括他的父窗口)都有机会参与消息到来之前的处理。呵呵至此,非对话框的消息循环和消息泵的机制就差不多了。这个机制在一个无限循环中,不断地从消息队列中获取消息,并且保证了程序的线程消息能够得到机会处理,窗口消息在预处理之后被发送到相应的窗口处理过程。那么,还有一点疑问,为什么要一会儿调用:PeekMessage,一会儿调用:GetMessage 呢,他们有什么区别?NOTE:一般来说,GetMessage 被设计用来高效地从消息队列获取消息。如果队列中没有消息,那么函数 GetMessage将导致线程休眠(让出 CPU时间)。而 PeekMessage是判断消息队列中如果没有消息,它马上返回 0,不会导致线程处于睡眠状态。在上面的 phase1第一个内循环中用到了 PeekMessage,它的参数 PM_NOREMOVE表示并不从消息队列中移走消息,而是一个检测查询,如果消息队列中没有消息他立刻返回 0,如果这时线程空闲的话将会引起消息循环调用 OnIdle处理过程(上面讲到了这个函数的重要性)。如果将:PeekMessage 改成:GetMessage(*),那么如果消息队列中没有消息,线程将休眠,直到线程下一次获得 CPU时间并且有消息出现才可能继续执行,这样,消息循环的空闲时间没有得到应用,OnIdle 也将得不到执行。这就是为什么既要用:PeekMessage(查询),又要用:GetMessage(做实际的工作)的缘故。第二部分: 对话框程序的消息循环机制基于对话框的 MFC工程和上面的消息循环机制不一样。实际上 MFC的对话框工程程序就是模式对话框。他和上面讲到的非对话框程序的不同之处,主要在于应用程序对象的 InitInstance()不一样。/dlg_5Dlg.cppBOOL CDlg_5App:InitInstance()AfxEnableControlContainer();#ifdef _AFXDLLEnable3dControls(); / Call this when using MFC in a shared DLL#elseEnable3dControlsStatic(); / Call this when linking to MFC statically#endifCDlg_5Dlg dlg; /定义一个对话框对象m_pMainWnd = int nResponse = dlg.DoModal(); /对话框的消息循环在这里面开始if (nResponse = IDOK)/ TODO: Place code here to handle when the dialog is/ dismissed with OKelse if (nResponse = IDCANCEL)/ TODO: Place code here to handle when the dialog is/ dismissed with Cancel/ Since the dialog has been closed, return FALSE so that we exit the/ application, rather than start the applications message pump.return FALSE;NOTE: InitInstance函数返回 FALSE,由最上面程序启动流程可以看出,CWinThread:Run 是不会得到执行的。也就是说,上面第一部分说的消息循环在对话框中是不能执行的。实际上,对话框也有消息循环,她的消息循环在 CDialog:DoModal()虚函数中的一个 RunModalLoop函数中。这个函数的实现体在 CWnd类中:int CWnd:RunModalLoop(DWORD dwFlags)ASSERT(:IsWindow(m_hWnd); / window must be createdASSERT(!(m_nFlags / window must not already be in modal state/ for tracking the idle time stateBOOL bIdle = TRUE;LONG lIdleCount = 0;BOOL bShowIdle = (dwFlags HWND hWndParent = :GetParent(m_hWnd);m_nFlags |= (WF_MODALLOOP|WF_CONTINUEMODAL);MSG* pMsg = / acquire and dispatch messages until the modal state is donefor (;)ASSERT(ContinueModal();/ phase1: check to see if we can do idle workwhile (bIdle / show the dialog when the message queue goes idleif (bShowIdle)ShowWindow(SW_SHOWNORMAL);UpdateWindow();bShowIdle = FALSE;/ call OnIdle while in bIdle stateif (!(dwFlags if (dwFlags / phase2: pump messages while availabledoASSERT(ContinueModal();/ pump message, but quit on WM_QUIT/PumpMessage(消息泵)的实现和上面讲的差不多。都是派送消息到窗口。if (!AfxGetThread()-PumpMessage()AfxPostQuitMessage(0);return -1;/ show the window when certain special messages recdif (bShowIdle UpdateWindow();bShowIdle = FALSE;if (!ContinueModal()goto ExitModal;/ reset “no idle“ state after pumping “normal“ messageif (AfxGetThread()-IsIdleMessage(pMsg)bIdle = TRUE;lIdleCount = 0; while (:PeekMessage(pMsg, NULL, NULL, NULL, PM_NOREMOVE); /无限循环ExitModal:m_nFlags return m_nModalResult;先说说怎么退出这个无限循环,在代码中:if (!ContinueModal()goto ExitModal;决定是否退出循环,消息循环函数返回也就是快要结束结束程序了。BOOL CWnd:ContinueModal()return m_nFlags NOTE: CWnd:ContinueModal()函数检查对话框是否继续模式。返回 TRUE,表示现在是模式的;返回FALSE,表示对话框已经不是模式(将要结束)。如果要结束对话框,在内部最终会调用函数 CWnd:EndModalLoop,它取消 m_nFlags的模式标志(消息循环中的 ContinueModal函数将返回 FALSE,消息循环将结束,程序将退出);然后激发消息循环读取消息。也就是说,结束模式对话框是一个标志,改变这个标志就可以了。他的代码是:/wincore.cppvoid CWnd:EndModalLoop(int nResult)ASSERT(:IsWindow(m_hWnd);/ this result will be returned from CWnd:RunModalLoopm_nModalResult = nResult;/ make sure a message goes through to exit the modal loopif (m_nFlags PostMessage(WM_NULL);NOTE: PostMessage(NULL)是有用的。如果消息队列中没有消息的话,可能消息循环中的ContinueModal()不会马上执行,发送一个空消息是激发消息循环马上工作。下面说一下 CWnd:RunModalLoop函数中的消息循环究竟干了些什么事情:1,第一个内循环。首先从消息队列中查询消息,如果对话框空闲,而且消息队列中没有消息,他做三件事情,大家应到都能从字面上明白什么意思。最重要的是发送 WM_KICKIDLE消息。为什么呢?第一部分讲到了,非对话框程序用 OnIdle来更新用户界面(UI),比如工具栏,状态栏。那么,如果对话框中也有工具栏和状态栏呢,在哪里更新(网上有很多这样的程序)?可以处理 WM_KICKIDLE消息:LRESULT CDlg_5Dlg:OnKickIdle(WPARAM w,LPARAM l)/调用 CWnd:UpdateDialogControls更新用户界面UpdateDialogControls(this, TRUE);return 0;NOTE: CWnd:UpdateDialog函数发送 CN_UPDATE_COMMAND_UI消息给所有的用户界面对话框控件。 2,第二个内循环。最重要的还是 PumpMessage派送消息到目标窗口。其他的,像第二个 if语句,0x118消息好像是 WM_SYSTIMER消息(系统用来通知光标跳动的一个消息)。也就是说,如果消息为 WM_SYSTIMER或者 WM_SYSKEYDOWN,并且空闲显示标志为真的话,就显示窗口并通知窗口立刻重绘。总之,对话框的消息循环机制和非对话框(比如 SDI,MDI)还是类似的,仅仅侧重点不同。模式对话框是模式显示,自然有他的特点。下面部分讨论一下模式对话框和非模式对话框的区别。因为模式对话框有自己的特殊消息循环;而非模式对话框,共用程序的消息循环,和普通的窗口已经没有什么大的区别了。第三部分:模式对话框和非模式对话框的区别这个话题已经有很多人讨论,我说说我所理解的意思。在 MFC框架中,一个对话框对象 DoModal一下就能产生一个模式对话框,Create 一下就能产生一个非模式对话框。实际上,无论是模式对话框还是非模式对话框,在 MFC内部都是调用:CreateDialogIndirect(*)函数来创建非模式对话框。只是模式对话框作了更多的工作,包括使父窗口无效,然后进入自己的消息循环等等。:CreateDialogIndirect(*)函数最终调用 CreateWindowEx函数通知系统创建窗体并返回句柄,他内部没有实现自己的消息循环。非模式对话框创建之后立即返回,并且和主程序共用一个消息循环。非模式对话框要等对话框结束之后才返回,自己有消息循环。比如下面的代码:CMyDlg* pdlg = new CMyDlg;pdlg -Create(IDD_DIALOG1);pdlg-ShowWindow(SW_SHOW);MessageBox(“abc“);非模式对话框和消息框 MessageBox几乎是同时弹出来。而如果将 Create改成 DoModal,那么,只能弹出模式对话框,在关闭了对话框之后(模式对话框自己的消息循环结束),消息框才弹出来。 NOTE:可以在模式对话框中调用 GetParent()-EnableWindow(true);这样,主窗口的菜单,工具栏又激活了,能用了。MFC 使用非模式对话框来模拟模式对话框,而在 win32 SDK程序中,模式对话框激发他的父窗口 Enable操作是没有效果的。关于消息循环总结:1,我们站在一个什么高度看消息循环?消息循环其实没有什么深奥的道理。如果一个邮递员要不断在一个城市中送信,我们要求他做什么?要求他来回跑,但他一次只能在一个地方出现。如果我们的应用程序只有一个线程的话,我们要他不断地为窗口传递消息,我们怎么做?在一个循环中不断的检测消息,并将他发送到适当的窗口。窗口可以有很多个,但消息循环只有一个,而且每时每刻最多只有一个地方在执行代码。为什么? 看第二点。2,因为是单线程的(程序进程启动的时候,只有而且有一个线程,我们称他为主线程),所以就像邮递员一样,每次只能在某一个地方干活。什么意思呢?举个例子,用:DiapatchMessage 派送消息,在窗口处理过程(WinProc,窗口函数)返回之前,他是阻塞的,不会立即返回,也就是消息循环此时不能再从消息队列中读取消息,直到:DispatchMessage 返回。如果你在窗口函数中执行一个死循环操作,就算你用PostQuitMessage函数退出,程序也会 down掉。while(1)PostQuitMessage(0); /程序照样 down.所以,当窗口函数处理没有返回的时候,消息循环是不会从消息队列中读取消息的。这也是为什么在模式对话框中要自己用无限循环来继续消息循环,因为这个无限循环阻塞了原来的消息循环,所以,在这个无限循环中要用 GetMessage,PeekMessage,DispatchMessage来从消息队列中读取消息并派送消息了。要不然程序就不会响应了,这不是我们所希望的。 所以说,消息循环放在程序的什么的地方都基本上是过的去的,比如放在 DLL里面。但是,最好在任何时候,只有一个消息循环在工作(其他的都被阻塞了)。然后,我们要作好的一件事情,就是怎么从消息循环中退出!当然用 WM_QUIT是可以拉(PostThreadMessage 也是个好主意),这个消息循环退出后,可能程序退出,也可能会激活另外一个被阻塞的消息循环,程序继续运行。这要看你怎么想,怎么去做。最后一个消息循环结束的时候,也许就是程序快结束的时候,因为主线程的执行代码也快要完了(除非 BT的再作个死循环)。 NOTE: 让 windows系统知道创建一个线程的唯一方法是调用 API CreatThread函数(_beginthreadex之类的都要在内部调用他创建新线程)。好像 windows核心编程说,在 win2000下,系统用CreateRemoteThread函数来创建线程,CreateThread 在内部调用 CreateRemoteThread。不过这不是争论的焦点,至少 win98下 CreateRemoteThread并不能正常工作,还是 CreateThread主持大局。3,在整个消息循环的机制中,还必须谈到窗口函数的可重入性。什么意思?就是窗口函数(他是个回调函数)的代码什么时候都可以被系统(调用者一般是 user32模块)调用。比如在窗口过程中,向自己的窗口SendMessage(*);那么执行过程是怎样的?我们知道,SendMessage 是要等到消息发送并被目标窗口执行完之后才返回的。那么窗口在处理消息,然后又等待刚才发送到本窗口的消息被处理后之后(SendMessage 返回)才继续往下执行,程序不就互相死锁了吗? 其实是不会的。windows 设计一套适合 SendMessage的算法,他判断如果发送的消息是属于本线程创建的窗口的,那么直接由 user32模块调用窗口函数(可能就有窗口重入),并将消息的处理结果结果返回。这样做体现了窗口重入。上面的例子,我们调用 SendMessage(*)发送消息到本窗口,那么窗口过程再次被调用,处理完消息之后将结果返回,然后 SendMessage之后的程序接着执行。对于非队列消息,如果没有窗口重入,不知道会是什么样子。NOTE: 由于窗口的可重入性。在 win32 SDK程序中应尽量少用全局变量和静态变量,因为在窗口函数执行过程中可能窗口重入,如果重入后将这些变量改了,但你的程序在窗口重入返回之后继续执行,可能就是使用已

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论