这是老版书里的,新版的书里没有。所幸电子书里还有 – 所以就拿出来学习了一番。
除了会讲到窗口消息之外,还会讲Win32程序的消息循环,MFC的消息映射机制。
以上三部分都是Win32编程消息一块稍微重要的一块。
在开始讲消息之前,我们首先普及一些基本概念:
- Windows允许一个进程建立1W个不同类型的用户对象(User Object):图片、光标、窗口类、加速键表等。
- 当一个线程调用一个函数来建立某个对象,这个对象就归这个线程的进程所拥有。当进程结束时,如果没有明确删除这个对象,操作系统会自动删除这个对象。
- 但是要注意的是 窗口 和 挂钩(hook) 这两种用户对象,它们分别由建立窗口和安装挂钩的线程所拥有。如果一个线程建立一个窗口或安装一个挂钩,然后线程结束,操作系统会自动删除窗口或卸载挂钩。这种线程拥有关系的概念对窗口有重要的意义:建立窗口的线程必须是为窗口处理所有消息的线程。
可以想象一下:一个线程建立了一个窗口然后就结束了。这种情况下窗口不会收到一个WM_DESTROY 或 WM_NCDESTROY消息,因为线程已经退出了,没办法来为窗口接收和处理这些消息。
同样的:每个线程,如果它建立了一个窗口,都会由系统为它分配一个消息队列。这个队列用于窗口消息的派送(dispatch).为了使窗口接收这些消息,线程必须有它自己的消息循环。
我们后面会讲到每个线程的消息队列。特别要看看消息是如何被放置在队列中的,以及线程如何从队列中取出消息并处理它们。
消息的基本概念
消息系统对于一个Win32程序来说十分重要,它是一个程序运行的动力源泉。
消息,是系统定义的一个32位的值,它定义了一个唯一的事件。
我们可以向Windows发送一个通知,告诉应用程序某个事情发生了,例如:单击鼠标,改变窗口尺寸,按下键盘上的按键等等,都会产生一个消息由Windows发送给应用程序。
消息本身是作为一个记录传递给应用程序的,这个记录中包含了消息的类型,以及其他信息。例如:对单击鼠标消息来说,这个消息就应该包含单机鼠标时的坐标。这个记录类型叫做MSG。
1 2 3 4 5 6 7 8 |
typedef struct tagMSG { HWND hwnd; //窗口 UINT message; //消息ID WPARAM wParam; //32位消息的特定附加信息 LPARAM lParam; //32位消息的特定附加信息 DWORD time; //消息创建时间 POINT pt; //消息创建时鼠标位置 } MSG,*PMSG,*NPMSG,*LPMSG; |
线程的消息队列
Windows的主要目标就是为程序提供一个强壮的环境。为实现这个目标,要保证每个线程运行在一个环境中,在这个环境中每个线程都相信自己是唯一运行的线程。更确切地说,每个线程必须有完全不受其他线程影响的消息队列。而且每个线程必须有一个模拟环境,使线程可以维持它自己的键盘焦点(keyboard focus)、窗口激活、鼠标捕获等概念。
当一个线程第一次被建立时,系统假定线程不会被用于任何与用户相关的任务。这样就可以减少线程对系统资源的要求。但是一旦这个线程调用一个与图形界面有关的函数(比如检查消息队列,或者建立一个窗口),系统就会为该线程分配一些另外的资源,以便它能够执行与用户界面有关的任务。
特别的是:系统分配一个THREADINFO结构,并将这个数据结构与线程联系起来。这个THREADINFO结构包含一组成员变量,利用这组成员,线程可以认为它是在自己独占的环境中运行。
THREADINFO是一个内部的,未公开的数据结构,用来指定线程的 登记消息队列(Posted)、发送消息队列(Send)、应答消息队列(Reply)、虚拟输入队列、唤醒标志(WakeFlags)、以及用来描述线程局部输入状态的若干变量。
↑上图描述了线程的THREADINFO的基本样子。
这张图是的THREADINFO结构是窗口消息系统的基础,阅读后文是皆应以此图对照。
将消息发送到线程的消息队列中
当线程有了与之联系的THREADINFO结构时,它就有了自己的消息队列集合。
如果一个进程建立了三个线程,并且这些线程都调用了CreateWindow,则有三个消息队列集合。
如果要将消息放置在线程的登记消息队列中,这要通过调用PostMessage函数来完成:
1 2 3 4 5 |
BOOL PostMessage( HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam); |
- 当一个线程第一次调用这个函数时,系统要确定是哪一个线程建立了hwnd参数标识的窗口。然后系统分配一块内存,将这个消息参数存储在这块内存中,并将这块内存增加到相应线程的登记消息队列中。
- 并且这个参数还设置唤醒标志位的 QS_POSTMESSAGE 唤醒位。
- 函数PostMessage在登记了消息之后立即返回,调用该函数的线程不知道登记的消息是否被指定的窗口过程所处理。甚至发送的消息有可能根本没有被处理:比如建立特定窗口的线程还没有处理完消息队列中的消息,就已经结束了,就会发生这种事情。
还可以通过PostThreadMessage将消息放置在线程的登记消息队列中:
1 2 3 4 5 |
BOOL PostThreadMessage( DWORD idThread, UINT Msg, WPARAM wParam, LPARAM lParam); |
PostThreadMessage 函数有四个参数,第一个参数标记了函数所期望的线程,即将消息发送到哪个线程的消息队列中去。当消息被设置到队列中时,MSG(消息数据结构)的hwnd成员会被设置为NULL。
对线程编写主消息循环以便在GetMessage或PeekMessage取出一个消息时,主消息循环代码检查hwnd是否为NULL,并检查MSG(消息数据结构)的msg成员来执行特殊的处理。如果线程确定了该消息不被指派给一个窗口,则不会调用DispatchMessage函数,消息循环继续取下一个消息。
同PostMessage函数一样,PostThreadMessage在向线程的队列登记了消息之后就立即返回。调用该函数的线程不知道消息是否被处理。
通过 GetWindowThreadProcessId 来确定是哪个线程建立了一个窗口。
1 2 3 |
DWORD GetWindowThreadProcessId( HWND hWnd, LPDWORD lpdwProcessId); |
这个函数返回线程的ID,这个线程建立了hwnd参数所标识的窗口。线程ID在全系统范围内是唯一的。还可以通过对lpdwProcessId参数传递一个DWORD地址来获取该线程的进程ID – 这个进程ID也是唯一的哦,如果不需要进程ID的话,传递NULL即可。
向线程发送消息的函数还有PostQuitMessage:
1 |
VOID PostQuitMessage(int nExitCode); |
为了终止线程的消息循环,可以调用这个函数。调用这个函数就类似于调用:
1 |
PostThreadMessage(GetCurrentThreadId(), WM_QUIT, nExitCode, 0); |
但是!PostQuitMessage并不登记一个消息到任何一个THREADINFO结构的队列。只是在内部,PostQuitMessage设定了 QS_QUIT 唤醒标志,并设置THREADINFO结构的nExitCode成员。因为这些操作永远不会失败,所以PostQuitMessage的原型被定义为返回VOID。
PS.唤醒标志我们会在后面对它们进行讨论。
向窗口发送消息
使用SendMessage可以将消息发送给一个窗口:
1 2 3 4 5 |
LRESULT SendMessage( HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam); |
窗口过程将处理这个消息,而且只有消息被处理之后,SendMessage才能返回到调用程序。由于具有这种同步的特性,比之PostMessage或PostThreadMessage,SendMessage用地更频繁。
——调用这个函数的线程在下一行代码执行前就知道前一个窗口消息已经被完全处理了。
那么,SendMessage是如何工作的呢?有两种情况:
1. 调用SendMessage的线程向该线程所建立的一个窗口发送一个消息:
在这种情况下它只是调用指定窗口的窗口过程函数,将其作为一个子例程。当窗口过程函数完成对消息的处理时,它向SendMessage返回一个值,SendMessage再将这个值返回给调用它的线程。
2. 调用SendMessage的线程向其他线程所建立的一个窗口发送消息:
此时工作就稍微复杂一些。Windows要求了建立窗口的线程处理窗口的消息,所以当一个线程调用SendMessage向另一个线程建立的窗口发送一个消息,也就是向其他线程发送消息后,发送线程要挂起而由另外一个线程处理消息。
所以系统会执行下面的动作:
1. 首先,发送的消息要追加到接收线程的发送消息队列,同时还为这个线程(接收线程)设定QS_SENDMESSAGE标志。
2. 其次,如果接收消息的线程已经在执行代码并且没有等待消息(GetMessage、PeekMessage、WaitMessage),发送的消息不会被即时处理,系统不能中断线程来立即处理消息。要等它处理完才可以继续处理下一个消息。
当接收进程在等待消息的时候,系统首先会检查 QS_SENDMESSAGE 标志是否被设置了,如果是,系统扫描发送消息队列的列表,并找到第一个发送的消息(可能有多个发送来的消息,因为可能存在多个线程同时向一个线程发送消息的情况 – 注意:系统只是把消息追加到线程的发送消息队列中)并调用适当的窗口过程来处理消息。
如果在发送消息队列中再没有消息了,则 QS_SENDMESSAGE 唤醒标志被关闭。
3. 当接收线程处理消息的时候,调用SendMessage函数发送消息的线程被设置为空闲(Idle)状态,等待一个消息出现在它的应答消息队列中。在发送的消息处理之后,窗口过程的返回值被登记到发送线程的应答消息队列中,唤醒发送线程,取出包含在应答消息队列中的返回值,这个返回值就是调用SendMessage的返回值。然后发送线程就可以继续运行了。
存在的问题是:发送消息之后,线程就挂起了,那么当处理发送消息的线程含有错误的时候,会导致进入死循环。那么对于调用SendMessage的线程会发生什么事呢?它会恢复执行吗?是不是一个程序中的BUG会导致另一个程序挂起呢?的确是有这种可能!
那么问题就是,我们该怎么解决这种可能呢:
利用四个函数:SendMessageTimeout、 SendMessageCallback、SendNotifyMessage、ReplyMessage,可以编写保护型代码防止出现这种情况。我们一个个来看。
第一个函数是,SendMessageTimeout:
1 2 3 4 5 6 7 8 |
LRESULT SendMessageTimeout( HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam, UINT fuFlags, UINT uTimeout, PDWORD_PTR lpdwResult); |
相比SendMessage函数它多了三个参数:
第五个参数:fuFlags参数,给它传递的值可以是 SMTO_NORMAL(定义值为0)、 SMTO_ABORTIFHUNG、 SMTO_BLOCK、 SMTO_NOTIMEOUTIFNOTHUNG,或者定义为这些标志的组合。
这些标志意义如下:(空格是为了方便阅读 – 实际使用是没有的)
- SMTO_NORMAL标志在<Winuser.h>中定义成0,如果不想指定任何其他标志及组合,就使用这个标志。
- SMTO_ABORT IF HUNG – 查看接收消息线程是否处于挂起状态,如果是,立即返回。
- SMTO_NO TIME OUT IF NOT HUNG – 标志使函数在接收消息的线程没有挂起时不考虑等待时间限定值。
- SMTO_BLOCK – 标志使调用线程在 SendMessageTimeout 返回之前,不再处理任何其他发送来的消息。
前面提到过了,线程在等待发送的消息返回结果时可以被中断,以便处理另一个发送来的消息。如果我们在这个参数里面用BLOCK标志的话,就意味着我们不让系统同意这样的中断。要注意!使用这个标志可能会导致死锁情况的发生,而且会一直等待到时间期满为止。
第六个参数和第七个参数:uTimeout 参数指定了等待应答消息时间的毫秒数。如果函数执行成功,返回TRUE,消息的结果复制到一个缓冲区中,该缓冲区的地址由最后一个参数lpdwResult指定。
最后要注意的两个问题是:
1. 返回值问题。
如果给窗口传递一个无效的窗口句柄或者是等待超时,都会返回FALSE,唯一区别它们的办法是调用GetLastError函数。
- 如果是由于等待超时而失败,GetLastError得到的值为0(ERROR_SUCCESS)
- 如果对参数传递了一个无效的句柄,GetLastError为1400( ERROR_INVALID_WINDOW_HANDLE)
2. 同线程的窗口发送消息。
如果调用SendMessageTimeout向调用线程所建立的窗口发送一个消息,系统只是调用这个窗口的窗口过程,并将返回值赋给pdwResult。因为所有的处理都必须发生在一个线程里,调用SendMessageTimeout函数之后的代码要等待消息被处理完之后才能执行。
第二个函数是,SendMessageCallback:
1 2 3 4 5 6 7 |
BOOL SendMessageCallback( HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam, SENDASYNCPROC lpResultCallBack, ULONG_PTR dwData); |
当一个线程调用SendMessageCallback时,该函数发送消息到接收线程的消息队列,并立即返回使发送线程可以立即执行。当接收线程完成对消息的处理时,一个消息被登记到发送线程的登记消息队列中,然后系统通过调用一个函数将这个应答 通知给发送线程。
前四个参数不介绍,直接看第五个参数:
lpResultCallBack 是一个函数指针,指向的函数就是被系统调用,用来通知发送线程消息已经被接收线程处理的 – 函数。函数原型如下:
1 2 3 4 5 |
VOID CALLBACK ResultCallBack ( HWND hWnd, UINT Msg, ULONG_PTR dwData, LRESULT lResult); |
将完成消息处理的窗口的句柄传递到第一个参数,将消息的值传递给第二个参数。
第三个参数dwData总是取传递到SendMessageCallback函数的dwData参数的值。系统只是取出对SendMessageCallback函数指定的参数值,再直接传递到ResultCallBack函数。ResultCallBack函数的最后一个参数是处理消息的窗口过程返回的结果。
因为SendMessageCallback在执行线程间发送时会立即返回,所以在接收线程完成对消息的处理时不是立即调用这个回调函数。而是由接收线程先将一个消息登记到发送线程的应答消息队列。
发送线程在下一次调用GetMessage、PeekMessage、WaitMessage或 某个SendMessage* 函数时,消息从应答消息队列中取出,并执行ResultCallBack函数。
SendMessageCallback函数还有一个好处:消息广播!
Windows提供了一种广播消息方法,用这种方法你可以向系统中所有现存的重叠窗口广播一个消息。这可以通过调用SendMessage函数,在参数hWnd传递 HWND_BROADCAST(定义值为-1)。问题是,SendMessage只能返回一个结果,这样的话我们就没法获得全部的结果。但是如果我们使用SendMessageCallback函数,就可以向每一个重叠窗口广播消息,并查看每一个返回结果。简直棒!这样我们需要做的就是对每一个返回结果调用ResultCallBack函数。
同样的我们要解决一下 向同一个线程的窗口发送一个消息的情况,如果是这样的话,系统立即调用窗口过程,并且在消息被处理后,系统调用ResultCallBack函数。在ResultCallBack函数返回之后,系统从调用SendMessageCallback后面的代码开始执行。
第三个函数是,SendNotifyMessage:
1 2 3 4 5 |
BOOL SendNotifyMessage( HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam); |
该函数将一个消息置于接收线程的发送消息队列中,并立即返回到调用线程。立即返回这一点和PostMessage是一样的,但是SendNotifyMessage函数在两个方面与之不同:
1. 消息优先级问题:向另外的线程建立的窗口发送消息时,发送的消息 比 登记消息队列的消息有更高的优先级。即:由SendNotifyMessage函数存放在队列中的消息总是在PostMessage函数登记到消息队列中的消息之前取出。
2. 向同一个线程创建的窗口发送消息时:SendNotifyMessage同SendMessage函数完全一样。 SendNotifyMessage在消息处理完之后才能返回。
我们已经知道,发送给窗口的大多数消息是用于通知的目的。也就是,发送消息是因为窗口需要知道某个状态已经发生变化,在程序能够继续执行之前,窗口要做某种处理。例如:
WM_ACTIVATE – 窗口已激活消息
WM_DESTROY – 窗口已销毁消息
WM_ENABLE – 窗口状态已改变消息
WM_SIZE – 窗口大小已改变
WM_SETFOCUS – 窗口获得键盘输入焦点
WM_MOVE – 窗口已经被移动
以上都是系统发送给窗口的通知,而不是登记的消息。
这些消息是系统对窗口的通知,因此系统不需要停止运行以等待窗口过程处理这些消息。与此相对应,如果系统向一个窗口发送一个WM_CREATE消息,则在窗口处理完这个消息之前,系统必须等待。如果返回值是-1,则不再建立窗口。
第四个用于线程发送消息的函数是 – ReplyMessage
1 |
BOOL ReplyMessage(LRESULT lResult); |
这个函数与其他三个函数不一样。前三个函数的作用大家应该很清楚了,就是为了防止自己被挂起。
说实话 – 我没看懂!是的我没看懂。 – 我在网上找到了一个博客,内容如下:
- 接收线程可以在窗口过程还没处理完消息的情况下,提前向通过SendMessage发送消息的线程的应答消息队列添加一个应答消息,这会唤醒发送线程。
- 接收线程把lResult作为消息处理结果传递给发送线程。当调用ReplyMessage后,发送线程被提前唤醒。当接收线程真正从窗口过程中返回时,系统将忽略这个返回值,即不再向发送线程回复本应在窗口过程正常结束才发送的应答消息。
- ReplyMessage必须在接收消息的窗口过程中被调用,而不能由某个调用Send*的线程调用,因为他是用来回复调用SendMessage的线程。前面讨论过的3个SendMessage*函数会立即返回。而SendMessage函数的返回,可以由窗口过程的实现者通过调用ReplyMessage来控制。
- 如果消息不是通过SendMessage发送的,或者消息由同一个线程发送,ReplyMessage不起作用。这也是返回值指出的,如果在处理线程间的消息发送时调用了ReplyMessage返回TRUE。处理线程内的消息发送时调用ReplyMessage会返回FALSE。
- 可以调用InSendMessage(Ex)来确定是线程间的消息发送还是线程内的发送消息。如果是线程间发送的会返回TRUE,线程内Send或Post的会返回FALSE。
嗯,认真想了下,就是相当于一个提前返回的效果。
【Sending a Message示例】
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//WM_USER+5是由其他线程发送过来的消息。 //如果是其他进程发送过来的消息,那个进程里要先调用RegisterWindowMessage注册为全局的消息类型 case WM_USER + 5: ......//处理些其他事情 if (InSendMessage()) //是否是线程间发送的消息,只有线程间的才能Reply { //该消息己经处理了差不多了,SendMessage的线程可以被唤醒了, //因为后面我还要做些其他事情,比如弹出对话框(DialogBox),你就不要傻等下去了。 //注意:在哪里调用ReplyMessage就可以在哪里通知系统去唤醒SendMessage线程 ReplyMessage(TRUE); } DialogBox(hInst, "MyDialogBox", hwndMain, (DLGPROC) MyDlgProc); break; |
表示感谢 – 这里给出链接如下:我觉得他写的比我好,可以去参考一下。
http://www.cnblogs.com/5iedu/p/5295892.html
唤醒线程
当一个线程调用GetMessage或WaitMessage打算获取一个消息的时候,恰好又没有这个线程或者这个线程建立的窗口的时候,系统可以挂起这个线程,这样就不再给它分配CPU时间。
当有一个消息登记(Post)或发送(Send)到这个线程,系统要设置一个唤醒标志,指出要给这个线程分配CPU时间,以便处理消息。
正常情况下,如果用户不按键或者移动鼠标,就没有消息发送给窗口。这意味着系统中大多数线程没有被配给CPU时间。
队列状态标识
还记得我们前面提到的唤醒标志吗?这一小节我们要讲的就是和这个标志有关。
当一个线程正在运行的时候,是可以通过调用 GetQueueStatus 函数来查询队列的状态的:
1 |
DWORD GetQueueStatus(UINT fuFlags); |
参数fuFlags是一个标志或一组由OR连接起来的标志,可以用来测试特定的唤醒位。
以下是各个唤醒位的取值和含义:
标志 | 取值 – 及其对应的消息 |
QS_KEY | 0x0001 – WM_KEYUP、WM_KEYDOWN、WM_SYSKEYUP、WM_SYSKEYDOWN |
QS_MOUSEMOVE | 0x0002 – WM_MOUSEMOVE – 只要队列中存在一个未处理的WM_MOUSEMOVE消息,这个标志被设置,当最后一条WM_MOUSEMOV消息被从队列删除时,标志被关闭 |
QS_MOUSEBUTTON | 0x0004 – WM_(L/M/R)BUTTON(DOWN/UP/DBCLICK) – 鼠标消息 |
QS_MOUSE | (QS_MOUSEMOVE | QS_MOUSEBUTTON) |
QS_INPUT | (QS_MOUSE | QS_KEY | QS_RAWINPUT) |
QS_PAINT | 0x0020 – WM_PAINT – 该线程创建的窗口中,只要窗口存在无效区时,该位被设置。只有当线程建立的所有窗口都有效时,这个标志才关闭 |
QS_TIMER | 0x0010 – WM_TIMER – 当定时器报时,QS_TIMER被设置,当GetMessage返回WM_TIMER消息后,该标志被关闭 |
QS_HOTKEY | 0x0080 – WM_HOTKEY |
QS_POSTMESSAGE | 0x0008 – 登记的消息 – 如果队列在期望的消息过滤范围内没有post的消息,该标志被关闭。只要Posted-Message(登记消息)队列中有一条消息,该标志就被设置。 |
QS_ALLPOSTMESSAGE | 0x0100 – 登记的消息 – 如果Posted-Message(登记消息)队列中完全没有消息,这个标志被清除。 |
QS_ALLEVENTS | (QS_INPUT | QS_POSTMESSAGE | QS_TIMER | QS_PAINT | QS_HOTKEY) |
QS_QUIT | 已调用PostQuitMessage,则该标志被设置,但系统并不向线程添加WM_QUIT消息。 – 这个标志没有公开,它在系统内部使用。 |
QS_SENDMESSAGE | 0x0040 – 由另一个线程发送的消息 – 即Send-Message(发送消息)队列中有消息时被设置,没有时被清除。 |
QS_ALLINPUT | (QS_INPUT | QS_POSTMESSAGE | QS_TIMER | QS_PAINT | QS_HOTKEY | QS_SENDMESSAGE) – 同 QS_ALLEVENTS | QS_SENDMESSAGE |
当调用GetQueueStatus函数时fuFlags将队列中要检查的消息类型告诉GetQueueStatus。
如果我们用OR符号连接的标识符(QS_*)的数量越少,它执行的就越快。
当函数返回的时候,线程队列中当前消息的类型在返回值的高字中(一个字是两个字节哦)。
而在低字中指出已经添加到队列中,并且在上一次对函数GetQueueStatus、GetMessage 或 PeekMessage调用以来还没有处理的消息的类型。
但是要注意的是,并不是所有的唤醒标志都由系统平等对待,不同的标志处理方式也有不同。
QS_* 标志出现在返回值里,并不保证以后调用函数GetMessage或PeekMessage会返回一个消息。GetMesssge和PeekMesssge执行某些内部过滤会导致消息被内部处理。因此,GetQueueStatus的返回值只能被看作是否调用GetMessage或PeekMessage的提示。
从线程的队列中提取消息的算法
1. 如果QS_SENDMESSAGE 被设置,系统向相应的窗口过程发送消息。GetMessage 或 PeekMessage函数在内部进行这种处理,并且在窗口过程处理完消息之后不返回线程,这些函数要等待其他消息的处理 —— 可以是任何的消息,包括本线程或其他线程Send或Post过来的消息。
2. 系统查看posted-message(登记消息)队列,如果posted-message队列有消息,Get/PeekMessage会填充MSG结构体,然后函数返回。在线程的消息循环中通常会调用DispatchMessage,将这个消息分派到相应的窗口过程中去处理。
3. 如果 QS_QUIT标志被设置了,Get/Peek Message返回一个 WM_QUIT 消息(在消息的wParam参数中指出了退出代码),并关闭QS_QUIT标志。
4. 如果有消息在线程的虚拟输入队列,Get/Peek Message会返回硬件输入消息(如键盘或鼠标消息)。
5. 如果QS_PAINT 标志被设置, Get/Peek Message会返回一个WM_PAINT消息给相应窗口。
6. 如果QS_TIMER 标志被设置,Get/Peek Message会返回一个WM_TIMER消息。
认真把图片看懂的话,整个流程应该就清楚了。那么现在存在的问题就是:为什么我们会这样定义标志的优先级。
SendMessage 和 PostMessage 优先级就不说了。
1. 为什么 “posted-message”队列在“虚拟Input-Queue”队列之前被检查?
1. 由于某个硬件事件可能引起其他的软件事件,所以系统要等用户读取下一个硬件事件之前,先处理这些软件事件。
2. 比如,调用TranslateMessage函数。这个函数会检查是否有一个WM_KEYDOWN(或WM_SYSKEYDOW硬件消息)从“Input-Queue”(虚拟输入)队列中被取出。如果有,系统会检查虚键信息能否转化为等价的字符,当可以转换时TranslateMessage会调用PostMessage将一个WM_CHAR(或WM_SYSCHAR)的软件消息投递到“posted-message”队列。下次调用GetMessage时,系统首先会检查“posted-message”队列,将这个WM_CHAR(或WM_SYSCHAR)消息取出。直到这个队列为空,再去检查“input-queue”队列。所以才会生成WM_KEYDOWN → WM_CHAR → WM_KEYUP这样的消息序列。
2. 为什么QS_QUIT的检测在检查“post-message”队列之后。原因如下:
使用QS_QUIT标志可以让线程在结束消息循环前,处理完所有“posted-message”队列中的消息,比如下面代码:
12345 case WM_CLOSE:PostQuitMessage(0);//虽然比后面的PostMessage更早调用,但不会更早退出!PostMessage(hwnd,WM_USER,0,0);// 会先取出“posted-message”队列中的//WM_USER,队列空时再检查QS_QUIT标志。Break;
3. 为什么PostQuitMessage只设置QS_QUIT而不将WM_QUIT消息投递到“posted-message”队列?
调用PostQuitMessage类似于(但不同于)调用PostThreadMessage(,WM_QUIT,…)。后者会将消息添加到“posted-message”(登记消息)队列的尾端,并使消息在系统检查“input-queue”(虚拟输入队列)前被处理。但PostQuitMessage只会设置QS_QUIT标志位,而不会将WM_QUIT消息放入“posted-queue”队列。原因如下:
- 因为当低内存时,post一个消息到“posted-message”可能会失败,如果一个程序想退出,即使是低内存时也应该被允许,但postQuitMessage函数调用不会失败(其返回值为void),因为它只改变QS_QUIT标志。
- 使用标志可以使线程在线程消息循环结束前完成对其他登记消息的处理。
- 尽管PostQuitMessage不会向消息队列投递WM_QUIT消息,但当系统检测到QS_QUIT标志被设置时,会先填充MSG结构体,将uMsg字段设为WM_QUIT,然后设置GetMessage的返回值为FALSE。由于系统内部自动填充了这个带有WM_QUIT信息的MSG结构体,让人感觉GetMessage好象从posted-message队列取出了一条WM_QUIT消息。但实际上这条消息并不是从“posted-message”队列中取出的,而是系统伪造的一个MSG结构体。
4. WM_PAINT优先级低于键盘消息,WM_TIMER的优先级比WM_PAINT更低(防止用定器时在短时间内重复的WM_PAINT)。
5. Get/PeekMessage只检查唤醒标志和调用线程的消息队列。这意味不能从其他线程的消息队列中取得消息,包括同一进程内的其他线程的消息。
利用内核对象或队列状态标志唤醒线程
当内核对象触发或某种消息到达时唤醒线程,既可以兼顾让线程运行一个长时间的操作时,又可以响应界面操作,防止界面出现一种“假死”的现象。例如:一个线程可能启动一个长时间运行的操作,并可以让用户取消这个操作。这个线程 需要知道何时操作结束(这是一个与UI无关的任务),或用户是否按了cancel按钮(与UI有关的任务)来结束操作。
一个线程可以调用MsgWaitForMultipleObjects或 MsgWaitForMultipleObjectsEx函数,使线程等待它的消息。函数原型如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
DWORD MsgWaitForMultipleObjects( DWORD nCount, CONST HANDLE *pHandles, BOOL fWaitAll, DWORD dwMilliseconds, DWORD dwWakeMask); DWORD MsgWaitForMultipleObjectsEx( DWORD nCount, CONST HANDLE *pHandles, DWORD dwMilliseconds, DWORD dwWakeMask, DWORD dwFlags); |
这两个函数类似于WaitFroMultipleObjects函数,不同之处在于:当一个内核对象变成触发状态(有信号状态 – signaled) 或 当一个窗口消息需要派送到调用线程建立的窗口时,这两个函数用于线程调度。 —— 很不幸我又没看懂这句话,是不是这个版本的电子书有问题?还是翻译的有问题? – 求解
我们先来介绍下几个参数吧:
参数 | 意义 |
nCount | 要等待的句柄数量 |
phObjects | 内核对象句柄数组 |
dwMilliseconds | 最多等待的时间 – 单位是毫秒 |
dwWakeMask | 何时触发事件,即要等待哪些事件到达,这个参数与GetQueueStatus的参数是一样的。 |
dwFlags | 该参数MsgWaitForMultipleObjectsEx才有。可以是以下标志的组合,如果不要指定任何标志,可以设为0。
|
fWaitAll | 该参数MsgWaitForMultipleObjects才有的。
|
默认下,当调用这两个函数时,它们都会检查是否有指定的新的消息的到来,比如现在队列是有两个键盘消息,如果现在用带QS_INPUT标志的参数调用MsgWaitForMultipleOjbects(Ex)函数,线程会被唤醒。当第1个键盘消息被取出并处理完后,如果继续调用MsgWaitForMultipleOjbects(Ex)函数,则线程会挂起。此时虽然队列中还有一个键盘消息,但他不是新的。为了解决这个问题,在MsgWaitForMultipleOjbectsEx函数中增加了一个MWMO_INPUTAVAILABLE标志,只要队列中有指定消息,函数就立即返回,但这个标志只能在MsgWaitForMultipleOjbectsEx中使用。
我们来看看使用MsgWaitForMultipleOjbectsEx的消息循环函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
//是否要退出消息循环 //Should the loop terminate? BOOL fQuit = FALSE; while(!fQuit) { //当内核对象触发或UI消息到达时唤醒线程 //Wake when the kernel object is signaled OR if we have to process a UI message. DWORD dwResult = MsgWaitForMultipleObjectsEx( 1, &hEvent, INFINITE, QS_ALLEVENTS,//所有事件 MWMO_INPUTAVAILABLE); //只有消息队列还有消息,就返回 switch(dwResult) { case WAIT_OBJECT_0: //内核对象被触发 //The event became signaled. break; case WAIT_OBJECT_0 + 1: //消息出现在消息队列中 //A Message is in oue queue. //分派消息 //Dispatch all of the message. MSG msg; //与GetMessage不同,PeekMessage的返回值表示有无获取到消息。 //GetMessage:如果函数取得WM_QUIT之外的其他消息,返回非零值. // 如果函数取得WM_QUIT消息,返回值是零 //PeekMessage:TRUE表示获取到一条消息。FALSE表示队列中无消息 while(PeekMessage(&MSG, NULL, 0, 0, PM_REMOVE)) { if(msg.message == WM_QUIT) { //退出消息循环 //A WM_QUIT message, exit the loop. fQuit = TRUE; } else { //翻译和分派消息 //Translate and dispatch the message. TranslateMessage(&msg); DispatchMessage(&msg); } }//队列己空//Our queue is empty break; } }//循环结束//End of the loop |
上面如果看不懂的话 – 可以先放一放,后面会讲述Win32的标准消息循环的。
通过消息发送数据 – 如何利用窗口消息在进程之间传递数据
主要还是三个消息吧:WM_SETTEXT、WM_GETTEXT、WM_COPYDATA
例如我们用 WM_SETTEXT消息使用lParam参数作为指向一个以零结尾的字符串指针,这个字符串为窗口规定了新的文本标题串。考虑下面的调用:
1 |
SendMessage(FindWindow(NULL,"Calculator"),WM_SETTEXT,0,(LPARAM)"A Test Caption"); |
这个调用看起来是正常的,它确定Calculator的窗口程序句柄,并试图将窗口的标题改成“A Test Caption”。但我们要仔细看一看究竟会发生什么。
新标题的字符串包含在调用进程的地址空间里。所以这个在调用进程空间的字符串地址将传递给lParam参数。当Calculator窗口的窗口过程收到这个消息时,它要查看lParam参数,并要读取这个以零结尾的字符串,使其成为新的标题。
但是存在这么一个问题哈:还记不记进程地址空间的概念,lParam中的地址指向的是调用进程的地址空间,而不一定是Calculator窗口的进程地址空间。这会发送内存存取违规这种严重的问题!
但是,当我们执行这段代码的时候,看到的却是执行成功?因为系统真正的处理方式如下:
- 当调用SendMessage时,函数会检查是否要发送WM_SETTEXT消息。如果是,就将以零结尾的字符串从调用进程的地址空间放入到一个内存映射文件(可在进程间共享)。
- 然后再发送消息到共他进程的线程。
- 当接收线程己准备好处理WM_SETTEXT消息时,它在自己的地址空间中找到上述内存映射文件(该文件包含要发送的文本),并让lParam指向那个文本,然后消息被分派到指定的窗口过程去进行处理。
- 处理完消息之后,内存映射文件被删除。
是不是很棒!通过内存映射文件解决了进程地址空间并不是同一个空间的问题。问题是,这样是不是太麻烦了一点?所幸的是大多数消息并不要求这种类型的处理。仅当这种消息是程序在进程间发送的消息,特别是消息的wParam或lParam参数表示一个指向数据结构的指针的时候,才会做这样的处理。
然后还有另外两个消息对不对,我们先来看看WM_GETTEXT消息:假定一个程序包含下面的代码:
1 2 |
char szBuffer[200]; SendMessage(FindWindow(NULL,"Calculator",WM_GETTEXT,sizeof(szBuff),(LPARAM)szBuff); |
WM_GETTEXT消息请求Calculator窗口的窗口过程用该窗口的标题填充szBuffer所指定的缓冲区。当一个进程向另一个进程的窗口发送这个消息的时候,实际上我们又碰到了前面的进程地址空间的问题。
所以,SendMessage时,系统检测到WM_GETTEXT消息时,实际上会发送两个消息。
- 首先系统向那个窗口发送WM_GETTEXTLENGTH消息,以获得文本的长度。
- 系统利用这个长度创建一个内存映射文件,用于在两个进程间共享。然后再发送消息来填充它。
- 然后转到调用SendMessage的进程,从内存映射文件中复制文本到指定的缓冲区(由lParam参数指定)
- 最后SendMessage函数返回。
好!到现在我们已经讲了两个消息了!还有一个消息:WM_COPYDATA
这个消息是作用在什么情况下呢?还记不记得我们前面说到的,系统检测到WM_GETTEXT 或者 WM_SETTEXT才会做出用内存映射文件共享数据的方法,那如果是我们自定义的消息呢?
如果我们用自己定义的 WM_USER+X 消息,并从一个进程的窗口向另一个进程的窗口发送类似的消息,那会怎么样?系统并不知道要用内存映射文件共享数据并在发送消息的时候改变指针,那该怎么办呢?
为此系统特定定义了一个特殊的窗口消息,就是这里的 WM_COPYDATA 来解决问题:
1 2 |
COPYDATASTRUCT cds; SendMessage(hwndReceiver, WM_COPYDATA, (WPARAM)hwndSender, (lParam)&cds); |
COPYDATASTRUCT 是一个结构体,定义如下:
1 2 3 4 5 |
typedef struct tagCOPYDATASTRUCT { ULONG_PTR dwData; DWORD cbData; PVOID lpData; } COPYDATASTRUCT,*PCOPYDATASTRUCT; |
当一个进程要向另一个进程发送一些数据时,必须先初始化COPYDATASTRUCT结构。
dwData是一个备用数据项,可以存放任何值。
cbData数据成员规定了发送的字节数
lpData数据成员指向了发送的第一个字节,这个地址在发送进程地址空间中
所以系统对WM_COPYDATA消息的处理方式如下:
- 当SendMessage看到要发送一个WM_COPYDATA消息时,会建立一个内存映像文件,大小是cbData字节。
- 从发送进程的地址空间中将数据复制到这个内存映像文件。
- 然后向目标窗口发送消息
- 接收线程在处理这个消息时,lParam参数己指定接收进程地址空间中的一个COPYDATASTRUCT结构体。这个结构的lpData成员指向了接收进程地址空间中的共享内存映像文件的视图。
关于WM_COPYDATA这个消息,还要注意以下三点:
- 只能发送(Send)这个消息,不能登记(Post)这个消息。不能登记这个消息是 因为接收窗口的窗口过程处理完之后,必须释放掉内存映射文件。如果登记这个消息,我们就不知道什么时候处理完毕,就没办法释放。
- 系统从另外的进程的地址空间中复制数据要花费一些时间。所以不应该让发送程序中运行的其他线程修改这个内存块,直到SendMessage调用返回。
- WM_COPYDATA消息,可以实现16位到32位之间的通信,也能实现32位到64位之间的通信。但Win98以下没有WM_COPYDATA消息和COPYDATASTRUCT结构本的定义,要自己增加定义。
我想现在应该用不到第三条了,我就忽视它好了。
接下来本应该是一个CopyData示例程序的讲解,不过我们还是先来看看Win32的示例窗口代码。
Win32示例窗口代码
看这个链接就行,如果看不懂可以留言。PS.这个是最基础的东西了。
http://blog.tk-xiong.com/archives/1102
CopyData示例程序
我这人比较懒 – 没啥好讲解的,看代码。看不懂就留言。
VS2013 – Win窗口程序 – 可以直接运行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 |
#include <Windows.h> #include <windowsx.h> #include <malloc.h> #define IDC_DATA1 1001 #define IDC_DATA2 1002 #define IDC_COPYDATA1 1003 #define IDC_COPYDATA2 1004 #define IDI_COPYDATA 1005 //定义消息处理函数 LRESULT CALLBACK WinSunProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam ); INT WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, INT nCmdShow ) { //定义窗口类 WNDCLASS wndcls; wndcls.cbClsExtra = 0; wndcls.cbWndExtra = 0; wndcls.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); wndcls.hCursor = LoadCursor(NULL, IDC_ARROW); wndcls.hIcon = LoadIcon(NULL, IDI_APPLICATION); wndcls.hInstance = hInstance; wndcls.lpfnWndProc = WinSunProc; wndcls.lpszClassName = TEXT("WinMain"); wndcls.lpszMenuName = NULL; wndcls.style = CS_HREDRAW | CS_VREDRAW; //注册窗口类 ATOM nAtom = 0; nAtom = RegisterClass(&wndcls); if(nAtom == 0) { MessageBox(NULL, TEXT("Register Failed"), TEXT("ERROR"), MB_OK); return FALSE; } //创建窗口 HWND hWnd; hWnd = CreateWindow(TEXT("WinMain"), TEXT("CopyData Application"), WS_OVERLAPPEDWINDOW, 300, 300, 600, 200, NULL, NULL, hInstance, NULL); if(hWnd == NULL) { MessageBox(NULL, TEXT("Create Failed"), TEXT("ERROR"), MB_OK); PostQuitMessage(0); } //显示窗口 ShowWindow(hWnd, SW_SHOWNORMAL); //刷新窗口 UpdateWindow(hWnd); //消息循环 BOOL bRet; MSG msg; while(bRet = GetMessage(&msg, hWnd, 0, 0)) { if(bRet == -1) { return -1; } else { TranslateMessage(&msg); DispatchMessage(&msg); } } return msg.wParam; } LRESULT CALLBACK WinSunProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam ) { HWND hWndT = NULL; HWND hWndEdit = NULL; COPYDATASTRUCT* pcds = NULL; switch(uMsg) { case WM_CREATE: //静态数据 CreateWindow(TEXT("Edit"), TEXT("数据1:"), WS_VISIBLE | WS_CHILD | ES_READONLY, 10, 30, 50, 30, hWnd, (HMENU)(IDI_COPYDATA), NULL, NULL); CreateWindow(TEXT("Edit"), TEXT("数据2:"), WS_VISIBLE | WS_CHILD | ES_READONLY, 10, 90, 50, 30, hWnd, (HMENU)(IDI_COPYDATA), NULL, NULL); //创建文本框 CreateWindow(TEXT("Edit"), TEXT("测试数据1"), WS_VISIBLE | WS_CHILD | WS_BORDER | ES_MULTILINE, 70, 30, 200, 30, hWnd, (HMENU)(IDC_DATA1), NULL, NULL); CreateWindow(TEXT("Edit"), TEXT("其他测试数据"), WS_VISIBLE | WS_CHILD | WS_BORDER | ES_MULTILINE, 70, 90, 200, 30, hWnd, (HMENU)(IDC_DATA2), NULL, NULL); //创建按钮 CreateWindow(TEXT("Button"), TEXT("将数据1发送到其他窗口"), WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON, 280, 30, 200, 30, hWnd, (HMENU)(IDC_COPYDATA1), NULL, NULL); CreateWindow(TEXT("Button"), TEXT("将数据2发送到其他窗口"), WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON, 280, 90, 200, 30, hWnd, (HMENU)(IDC_COPYDATA2), NULL, NULL); break; case WM_PAINT: //处理重绘消息 HDC hDC; PAINTSTRUCT ps; hDC = BeginPaint(hWnd, &ps); //在这里画图 EndPaint(hWnd, &ps); break; case WM_COMMAND://处理命令消息 switch(LOWORD(wParam)) { case IDC_COPYDATA1: case IDC_COPYDATA2: //获取相应的文本框 hWndEdit = GetDlgItem(hWnd, (wParam == IDC_COPYDATA1) ? IDC_DATA1 : IDC_DATA2); //准备一个COPYDATASTRUCT结构体,其内容会被Copy到 //一个内存映像文件中 COPYDATASTRUCT cds; //指示哪个数据域的内容要被发送(0 = ID_DATA1,1 = ID_DATA2) cds.dwData = (DWORD)((wParam == IDC_COPYDATA1) ? 0 : 1); //获取文本的长度(字节) cds.cbData = (Edit_GetTextLength(hWndEdit) + 1) * sizeof(TCHAR); //在栈中分配一个内容以保存字符串内容 cds.lpData = _alloca(cds.cbData); //将编辑框里的字符串存到cbs.lpData中 Edit_GetText(hWndEdit, (PTSTR)cds.lpData, cds.cbData); //获取应用程序的标题 TCHAR szCaption[100]; GetWindowText(hWnd, szCaption, sizeof(szCaption) / sizeof(szCaption[0])); //枚举所有具有相同标题的顶层窗口 do { hWndT = FindWindowEx(NULL, hWndT, NULL, szCaption); if(hWndT != NULL) { //发送CopyData消息 FORWARD_WM_COPYDATA(hWndT, hWnd, &cds, SendMessage); } } while(hWndT != NULL); break; default: return DefWindowProc(hWnd, uMsg, wParam, lParam);; } break; case WM_COPYDATA: //收到了消息 pcds = (COPYDATASTRUCT*)lParam; //设置文本的内容 Edit_SetText(GetDlgItem(hWnd, pcds->dwData ? IDC_DATA2 : IDC_DATA1), (PTSTR)pcds->lpData); break; case WM_CLOSE: if(IDYES == MessageBox(hWnd, TEXT("是否真的结束"), TEXT("Message"), MB_YESNO)) { DestroyWindow(hWnd); } break; default: return DefWindowProc(hWnd, uMsg, wParam, lParam); } return 0; } |
MFC消息映射机制 – 的简单解析
额,为什么是简单解析呢,因为这里只涉及到应用和一些简单的原理,并没有深入到源码去查看。
嗯,在前面我们应该讲了Win32的消息处理,然后有一个窗口过程来调用,还记得吗?
不记得也没关系,回去再看一遍嘛…这里我们要分析的是:
1. MFC的消息映射机制是怎么回事
2. 它是如何封装了Win32的消息处理的
首先我们来看看消息映射机制是怎么回事吧:
我记得主要是分三部分:
消息处理函数声明 – 消息处理函数实现 – 消息映射表(BEGIN_MESSAGE_MAP 和 END_MESSAGE_MAP)
主要就是这三部分构成了消息映射机制…
然后,它是如何封装了Win32消息处理的
完蛋,这个就尴尬了。
大家可以看看这个博客吧,我感觉我理解不了。
http://blog.csdn.net/linzhengqun/article/details/1905671