MFC将thread分成winddow thread和worker thread,在讨论多现程(Multi-thread)之前,我们先只考虑window thread。
windows programming的基本工作方式和console application的不同,基本上是这样运行的,程序从WinMain()开始,然后进入一个message loop,程序在这里等待发给它的所有消息然后一一处理,直到接收到WM_QUIT的消息的时候,message loop终止,程序结束。所以整个主程序运行的过程就是等待消息,接收消息,然后处理消息的过程。
窗口建立的时候CreateWindow, RegisterWindow之类的不必太费心,MFC已经全管理妥当了,需要提起一点注意的是程序开始时HINSTANCE hInstance这个参数,在和DLL打交道的时候会帮你解决很多问题,如果一个Bitmap Load不上来,或者一个Dialog DoModal之后不出来,估计就得向这个参数求助了,这是后话。
具体处理的消息的函数叫window procedure,具体处理消息的code叫message handler。它可以是当前应用程序的API,也可以是调用的不同DLL的API。不同的DLL叫不同的module。以后的文章中我会具体说明module state。是个很重要的话题。(当项目大的时候)
没有message handler的消息交给DefWindowProc()函数处理,差不多可以理解为什么也不作了。
消息包括四个参数,window handle,message ID,和另外两个参数wParam, lParam。window handle可以作为window的识别ID来用。所以在发送消息的时候如果可以有两种格式:
CWnd *pWnd = ....
if (pWnd && pWnd->GetSafeWnd())
pWnd->SendMessage(message, wParam = 0, lParam);
或者
SendMessage(pWnd->GetSafeWnd(), message, wParam, lParam )
发送消息如果用SendMessage消息将立刻发送,如果用PostMessage,消息将进入Message queue按当前顺序发送,一般没有特别的要求PostMessage已经足够了。
处理消息的时候根据不同的Message ID交给不同的message handler去处理,一般的message handler的接收格式是用wParam传一个关键的参数,如这次操作的具体ID,把其余的大量辅助信息放在lParam里。需要注意的是如果lParam传递的是一个指针(一般情况下是CObject类的或从CObject衍生出来的),这个指针指向的变量的寿命需要足够长,因为信息Post出去之后发送函数很可能就运行完毕了。如果发送的指针是个局部变量,接收方就一定会Crash。当然如果是发送方new出来的变量,接收方得负责帮他delete掉,这个操作很危险,而且不一定合适。有时候发送方把信息传给N个窗口,第一个窗口delete掉了第二个窗口就麻烦了,不delete掉又不能保证第二个窗口一定delete掉,所以如果可能,不用new为上策。用点什么成员变量,常数变量之类的比较好。
由于可能收到的信息种类很多,用传统的switch来处理在程序中会显得很乱,于是MFC采用了Message Map机制。Message Map 机制实现了收到的信息和处理信息函数的mapping。在BEGIN_MESSAGE_MAP和END_MESSAGE_MAP之间定义的消息会在window接收到之后一一传给对应的message handler处理。
所有用于处理信息的函数的申明需要有afx_msg关键字。对于系统所要处理的message,ON_WN_XXX 一般跟三个参数,WPARAM(wParam),LOWORD(lParam) 和HIWORD(lParam).用不到的参数会省略。
下面列举一下Message Map中可能用到的关于宏
1。Window间SendMessage或PostMessage收到的消息如果是系统将要管理的,在Message Map中一般用ON_WM_XXX。“XXX”是具体消息名字。例如画窗口是ON_WM_PAINT。如果是自定义的消息ON_MESSAGE()。例如窗口pWndA发消息给窗口pWndB。pWndB->PostMessage(WM_MYMSG1),那么Window B要处理这个消息需要在Message Map里面写上 ON_MESSAGE(WM_MYMSG1, OnMessage1),然后写OnMessage1函数作message handler。WM_MYMSG1的定义应放在user message中,WM_USER+NNN。注意最好不要和其它已有的ID重复,这个没有办法自动检查。
2。ON_COMMAND,ON_UPDATE_COMMAND_UI,ON_COMAND_RANGE
ON_COMMAND只要用于menu和toolbar的点击处理,也可以用在accelerator中。管理用户的键盘输入用ON_COMMAND比管理ON_WN_CHAR好。ON_UPDATE_COMMAND_UI是用于更新menu或toolbar的。在这个message handler里面你可以根据不同的要求enable或者disable当前的菜单选项或toolbar button。ON_COMMAND_RANGE主要用于动态的菜单的选项中。当N个动态菜单选项加入时,你可以用一串连续的ID作为它们的消息管理ID,在ON_COMMAND_RANGE定义这串ID的起始和最大值,然后响应函数就可以知道具体是哪个动态菜单选项被选中了。
3。ON_COMMAND_EX,ON_COMMAND_RANGE_EX
用处比较少,当多个class (CCmdTarget class)需要处理同一消息的时候,一个class如果用ON_COMMAND_EX处理,返回TRUE,表示消息处理完毕。返回FALSE则其它class可以继续处理。
4。ON_REGISTERED_MESSAGE
用于确认系统中新message ID唯一。
5。ON_CONTROL,ON_WM_XXX_REFLECT,ON_CONTROL_RANGE
用于window接收其控件发来的特殊消息。
MFC基于消息,使用事件驱动(Message Based,Event Driven)机制
MFC程序基于消息,而使用事件驱动(Message Based,Event Driven)。也就是说MFC就是一个死循环,里面有很多的条件,每个条件对应一个方法。这些条件就是有消息类定义,当用户触发事件时,将发送消息到响应的窗口。当程序收到消息时进行解析,判断如果符合条件,将运行当前事件的处理方法。
- MSG msg;
- while(GetMessage(&msg,NULL,NULL,NULL))
- {
- TranslateMessage(&msg);
- DispatchMessage(&msg);
- }
每一个程序都存在上述的循环,而MSG是一个结构,是Windows内设的一种数据格式,可以在WinUser.h中找到,代码如下:
-
-
-
- typedef struct tagMSG {
- HWND hwnd;
- UINT message;
- WPARAM wParam;
- LPARAM lParam;
- DWORD time;
- POINT pt;
- #ifdef _MAC
- DWORD lPrivate;
- #endif
- } MSG, *PMSG, NEAR *NPMSG, FAR *LPMSG;
接受并处理消息的主角是窗口,每一个窗口都必须要有能够处理消息的方法,称为“窗口函数”(Window Procedure/Function)。当窗口获得消息后,必须判断消息的类别,将消息转换(TranslateMessage(&msg)转换键盘消息),然后将消息传递到(DispatchMessage(&msg))窗口函数去处理。
窗口函数是一个回调函数(用户定义的函数用于Windows操作系统调用的函数),它的形式如下所示。
- LRESULT CALLBACK WinProc(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam)
其中wParam和lParam的意义因消息的不同而不同,但可以知道的是wParam的位数是随着操作系统的位数而定的,在32位的操作系统中为32位,当然64位的就为64位。知道了这个函数后,如果要将每一个消息对应到响应的处理函数中就需要如switch/case结构来判断,为了让程序更好的模块化,需要了解Message Map(消息映射)的原理。
一、消息映射机制
消息系统对于一个win32程序来说十分重要,它是一个程序运行的动力源泉。一个消息,是系统定义的一个32位的值,他唯一的定义了一个事件,向Windows发出一个通知,告诉应用程序某个事情发生了。例如,单击鼠标、改变窗口尺寸、按下键盘上的一个键都会使Windows发送一个消息给应用程序。消息可以由系统或者应用程序产生。系统在发生输入事件时产生消息。举个例子, 当用户敲键, 移动鼠标或者单击控件。系统也产生消息以响应由应用程序带来的变化, 比如应用程序改变系统字体改变窗体大小。应用程序可以产生消息使窗体执行任务,或者与其他应用程序中的窗口通讯。
在菜单中选择View-->Class Wizard,也可以用单击鼠标右键,选择Class Wizard,同样可以激活Class Wizard。选择Message Map标签,从Class name组合框中选取我们想要添加消息的类。在Object IDs列表框中,选取类的名称。此时, Messages列表框显示该类的大多数(若不是全部的话)可重载成员函数和窗口消息。类重载显示在列表的上部,以实际虚构成员函数的大小写字母来表示。其他为窗口消息,以大写字母出现,描述了实际窗口所能响应的消息ID。选中我们想添加的消息,单击Add Function按钮,Class Wizard自动将该消息添加进来。
1、一个MFC消息响应函数在程序中有三处相关信息:函数原型,函数实现,关联消息和消息响应函数的宏。
函数原型——头文件CDrawView——两个AFX_MSG注释宏之间——消息响应函数原型的声明 —— afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
函数实现——源文件CDrawView()——OnLButtonDown(UINT nFlags, CPoint point)
关联消息和消息响应函数的宏——源文件CDrawView()——BEGIN_MESSAGE_MAP 和END_MESSAGE_MAP()之间
2、、 MFC消息映射机制的具体实现方法是:在每个能接收和处理消息的类中,定义一个消息和消息函数静态对照表,即:消息映射表。在消息映射表中,消息与对应消息处理函数指针是成对出现的。某个类能处理的所有消息及其对应的消息处理函数的地址都列在这个类所对应的静态表中。当有消息需要处理时,程序只要搜索该消息静态表,查看表中是否含有该消息,就可知道该类能否处理此消息。如果能处理该消息,则同样依照静态表能很容易找到并调用对应的消息处理函数。
3、 MFC消息映射机制的实际实现过程:MFC在后台维护了一个窗口句柄与对应的C++对象指针的对照表。
具体请参考:http://blog.youkuaiyun.com/ocean2006/article/details/5498265
4、MFC建立消息的步骤如下:消息的声明、消息的映射、消息的实现
声明:
-
- afx_msg void OnTimer(UINT nIDEvent);
- afx_msg void OnPaint();
-
- DECLARE_MESSAGE_MAP()
映射:
- BEGIN_MESSAGE_MAP(CTestDialog, CDialog)
-
- ON_WM_TIMER()
- ON_WM_PAINT()
-
- END_MESSAGE_MAP()
实现:
- void CTestDialog::OnPaint()
- {
- }
-
- void CTestDialog::OnTimer(UINT nIDEvent)
- {
- CDialog::OnTimer(nIDEvent);
- }
二、事件驱动机制
EVENT有两种状态:发信号,不发信号。 SetEvent/ResetEvent分别将EVENT置为这两种状态分别是发信号与不发信号。WaitForSingleObject()等待,直到参数所指定的OBJECT成为发信号状态时才返回,OBJECT可以是EVENT,也可以是其它内核对象。当你创建一个线程时,其实那个线程是一个循环,不像上面那样只运行一次的。这样就带来了一个问题,在那个死循环里要找到合适的条件退出那个死循环,那么是怎么样实现它的呢?在Windows里往往是采用事件的方式,当然还可以采用其它的方式。在这里先介绍采用事件的方式来通知从线程运行函数退出来,它的实现原理是这样,在那个死循环里不断地使用 WaitForSingleObject函数来检查事件是否满足,如果满足就退出线程,不满足就继续运行。当在线程里运行阻塞的函数时,就需要在退出线程时,先要把阻塞状态变成非阻塞状态,比如使用一个线程去接收网络数据,同时使用阻塞的SOCKET时,那么要先关闭SOCKET,再发送事件信号,才可以退出线程的。CreateEvent、SetEvent、WaitForSingleObj
事件驱动过程需要使用以下三个函数:
1、CreateEvent
函功能描述:创建或打开一个命名的或无名的事件对象.
- HANDLE CreateEvent(
- <blockquote>LPSECURITY_ATTRIBUTES lpEventAttributes,
- </blockquote><blockquote>BOOL bManualReset,
- </blockquote><blockquote>BOOL bInitialState,
- </blockquote><blockquote>LPCTSTR lpName
- </blockquote>);
2. WaitForSingleObject
功能描述:用来检测 hHandle事件的信号状态,当函数的执行时间超过dwMilliseconds就返回,但如果参数dwMilliseconds为INFINITE时函数将直到相应时间事件变成有信号状态才返回,否则就一直等待下去,直到WaitForSingleObject有返回直才执行后面的代码。
- DWORD WaitForSingleObject(
-
-
- HANDLE hHandle,
-
-
- DWORD dwMilliseconds
-
-
- );
参数hHandle是一个事件的句柄,第二个参数dwMilliseconds是时间间隔。如果时间是有信号状态返回WAIT_OBJECT_0,如果时间超过dwMilliseconds值但时间事件还是无信号状态则返回WAIT_TIMEOUT。
hHandle可以是下列对象的句柄:
Change notification 、Console input 、Event 、Job 、Memory resource notification、 Mutex 、Process、 Semaphore、 Thread 、Waitable timer等等。
3、SetEvent
功能描述:设置事件的状态为有标记,释放任意等待线程。如果事件是手工的,此事件将保持有标记直到调用ResetEvent。这种情况下将释放多个线程,如果事件是自动的,此事件将保持有标记,直到一个线程被释放,系统将设置事件的状态为无标记。如果没有线程在等待,则此事件将保持有标记,直到一个线程被释放。
4、ResetEvent
功能描述:这个函数把指定的事件对象设置为无信号状态。
- BOOL ResetEvent(
- <blockquote>HANDLE hEvent
- );</blockquote>
参数说明:
hEvent
[in] 指向事件对象的句柄.由 CreateEvent or OpenEvent 函数返回。 这个句柄需要拥有EVENT_MODIFY_STATE 访问权限.
函数成功,返回非0值,否则返回0值,可以调用GetLastError得到错误的详细信息。
注意:
一个事件对象一直都保持在无信号状态,直到显式调用 SetEvent or PulseEvent 函数把它设置到有信号状态。 这些无信号的事件对象会阻塞任何在内部调用wait函数的线程。 这个函数用于手动重置的事件对象。手动重置的对象在线程释放后必须手动置为无信号状态。 自动重置的事件对象在一个等待它成功的线程释放后会自动变为无信号状态。
5、CloseHandle
功能描述:使用CloseHandle函数关闭句柄。当进程停止时,系统将自动关闭句柄。当最后一个句柄被关闭后,事件对象将被销毁。
6、应用:主要应用是锁定功能,实现PV操作(PV操作与信号量的处理相关,P表示通过的意思,V表示释放的意思,解决进程同步与互斥问题)
在调用的过程中,所有线程都可以在一个等待函数中指定事件对象句柄。当指定的对象的状态被置为有信号状态时,单对象等待函数将返回。
对于多对象等待函数,可以指定为任意或所有指定的对象被置为有信号状态。当等待函数返回时,等待线程将被释放去继续运行。
初始状态在bInitialState参数中进行设置。使用SetEvent函数将事件对象的状态置为有信号状态。使用ResetEvent函数将事件对象的状态置为无信号状态。
当一个手动复原的事件对象的状态被置为有信号状态时,该对象状态将一直保持有信号状态,直至明确调用ResetEvent函数将其置为无符号状态。
当事件的对象被置为有信号状态时,任意数量的等待中线程,以及随后开始等待的线程均会被释放。
当一个自动复原的事件对象的状态被置为有信号状态时,该对象状态将一直保持有信号状态,直至一个等待线程被释放;系统将自动将此函数置为无符号状态。如果没有等待线程正在等待,事件对象的状态将保持有信号状态。
多个进程可持有同一个事件对象的多个句柄,可以通过使用此对象来实现进程间的同步。下面的对象共享机制是可行的:
在CreateEvent函数中,lpEventAttributes参数指定句柄可被继承时,通过CreateProcess函数创建的子进程继承的事件对象句柄。
一个进程可以在DuplicateHandle函数中指定事件对象句柄,从而获得一个复制的句柄,此句柄可以被其它进程使用。
一个进程可以在OpenEvent或CreateEvent函数中指定一个名字,从而获得一个有名的事件对象句柄。
使用CloseHandle函数关闭句柄。当进程停止时,系统将自动关闭句柄。当最后一个句柄被关闭后,事件对象将被销毁。
7、以下是简单的事件驱动实例:
- #include <WINDOWS.H>//需要访问windows API 函数,因此许加入此头文件
- #include <IOSTREAM.H>
-
- DWORD WINAPI Fun1Pro(LPVOID lpParameter);
- DWORD WINAPI Fun2Pro(LPVOID lpParameter);
-
- int nTickets = 100;
- HANDLE g_hEvent;
-
- void main()
- {
- HANDLE hThread1;
- HANDLE hThread2;
-
-
-
-
- g_hEvent = CreateEvent(NULL,FALSE,TRUE,"Tickets");
- if (g_hEvent)
- {
- if (ERROR_ALREADY_EXISTS == GetLastError())
- {
- cout<<"Only one thread can run..."<<endl;
- return;
- }
- }
-
- hThread1 = CreateThread(NULL,0,Fun1Pro,NULL,0,NULL);
- hThread2 = CreateThread(NULL,0,Fun2Pro,NULL,0,NULL);
- CloseHandle(hThread1);
- CloseHandle(hThread2);
-
- Sleep(4000);
- CloseHandle(g_hEvent);
- }
-
- DWORD WINAPI Fun1Pro(LPVOID lpParameter)
- {
- while (TRUE)
- {
- WaitForSingleObject(g_hEvent,INFINITE);
-
- if (nTickets>0)
- {
- Sleep(1);
- cout<<"Thread1 sells tickets : "<<nTickets--<<endl;
- SetEvent(g_hEvent);
- }
- else
- {
- SetEvent(g_hEvent);
- break;
- }
- }
- return 0;
- }
-
- DWORD WINAPI Fun2Pro(LPVOID lpParameter)
- {
- while (TRUE)
- {
- WaitForSingleObject(g_hEvent,INFINITE);
-
- if (nTickets>0)
- {
- Sleep(1);
- cout<<"Thread2 sells tickets : "<<nTickets--<<endl;
- SetEvent(g_hEvent);
- }
- else
- {
- SetEvent(g_hEvent);
- break;
- }
- }
- return 0;
- }