26窗口消息Word文件下载.docx
《26窗口消息Word文件下载.docx》由会员分享,可在线阅读,更多相关《26窗口消息Word文件下载.docx(29页珍藏版)》请在冰豆网上搜索。
当一个线程调用一个函数来建立某个对象时,则该对象就归这个线程的进程所拥有。
这样,当进程结束时,如果没有明确删除这个对象,则操作系统会自动删除这个对象。
对窗口和挂钩(hook)这两种User对象,它们分别由建立窗口和安装挂钩的线程所拥有。
如果一个线程建立一个窗口或安装一个挂钩,然后线程结束,操作系统会自动删除窗口或卸载挂钩。
这种线程拥有关系的概念对窗口有重要的意义:
建立窗口的线程必须是为窗口处理所有消息的线程。
为了使这个概念更加明确具体,可以想像一个线程建立了一个窗口,然后就结束了。
在这种情况下,窗口不会收到一个WM_DESTROY或WM_NCDESTROY消息,因为线程已经结束,不可能被用来使窗口接收和处理这些消息。
这也意味着每个线程,如果它至少建立了一个窗口,都由系统对它分配一个消息队列。
这个队列用于窗口消息的派送(dispatch)。
为了使窗口接收这些消息,线程必须有它自己的消息循环。
本章要考查每个线程的消息队列。
特别是要看看消息是如何被放置在队列中的,以及线程如何从队列中取出消息并处理它们。
26.1线程的消息队列
前面已经说过,Windows的一个主要目标是为程序的运行提供一个强壮的环境。
为实现这个目标,要保证每个线程运行在一个环境中,在这个环境中每个线程都相信自己是唯一运行的线程。
更确切地说,每个线程必须有完全不受其他线程影响的消息队列。
而且,每个线程必须有一个模拟环境,使线程可以维持它自己的键盘焦点(keyboardfocus)、窗口激活、鼠标捕获等概念。
当一个线程第一次被建立时,系统假定线程不会被用于任何与用户相关的任务。
这样可以减少线程对系统资源的要求。
但是,一旦这个线程调用一个与图形用户界面有关的函数(例如检查它的消息队列或建立一个窗口),系统就会为该线程分配一些另外的资源,以便它能够执行与用户界面有关的任务。
特别是,系统分配一个THREADINFO结构,并将这个数据结构与线程联系起来。
这个THREADINFO结构包含一组成员变量,利用这组成员,线程可以认为它是在自己独占的环境中运行。
THREADINFO是一个内部的、未公开的数据结构,用来指定线程的登记消息队列(posted-messagequeue)、发送消息队列(send-messagequeue)、应答消息队列(reply-messagequeue)、虚拟输入队列(virtualized-inputqueue)、唤醒标志(wakeflag)、以及用来描述线程局部输入状态的若干变量。
图26-1描述了THREADINFO结构和与之相联系的三个线程。
图26-1三个线程及相应的THREADINFO结构
这个THREADINFO结构是窗口消息系统的基础,在阅读下面各节内容时,应该参考该图。
26.2将消息发送到线程的消息队列中
当线程有了与之相联系的THREADINFO结构时,线程就有了自己的消息队列集合。
如果一个进程建立了三个线程,并且所有这些线程都调用CreateWindow,则将有三个消息队列集合。
消息被放置在线程的登记消息队列中,这要通过调用PostMessage函数来完成:
BOOLPostMessage(
HWNDhwnd,
UINTuMsg,
WPARAMwParam,
LPARAMlParam);
当一个线程调用这个函数时,系统要确定是哪一个线程建立了用hwnd参数标识的窗口。
然后系统分配一块内存,将这个消息参数存储在这块内存中,并将这块内存增加到相应线程的登记消息队列中。
并且,这个函数还设置QS_POSTMESSAGE唤醒位(后面会简单讨论)。
函数PostMessage在登记了消息之后立即返回,调用该函数的线程不知道登记的消息是否被指定窗口的窗口过程所处理。
实际上,有可能这个指定的窗口永远不会收到登记的消息。
如果建立这个特定窗口的线程在处理完它的消息队列中的所有消息之前就结束了,就会发生这种事。
还可以通过调用PostThreadMessage将消息放置在线程的登记消息队列中。
BOOLPostThreadMessage(
DWORDdwThreadId,
注意可以通过调用GetWindowsThreadProcessId来确定是哪个线程建立了一个窗口。
DWORDGetWindowThreadProcessId(
PDWORDpdwProcessId);
这个函数返回线程的ID,这个线程建立了hwnd参数所标识的窗口。
线程ID在全系统范围内是唯一的。
还可以通过对pdwProcessId参数传递一个DWORD地址来获取拥有该线程的进程ID,这个进程ID在全系统范围内也是唯一的。
通常,我们不需要进程ID,只须对这个参数传递一个NULL。
PostThreadMessage函数所期望的线程由第一个参数dwThreadId所标记。
当消息被设置到队列中时,MSG结构的hwnd成员将设置成NULL。
当程序要在主消息循环中执行一些特殊处理时要调用这个函数。
要对线程编写主消息循环以便在GetMessage或PeekMessage取出一个消息时,主消息循环代码检查hwnd是否为NULL,并检查MSG结构的msg成员来执行特殊的处理。
如果线程确定了该消息不被指派给一个窗口,则不调用DispatchMessage,消息循环继续取下一个消息。
像PostMessage函数一样,PostThreadMessage在向线程的队列登记了消息之后就立即返回。
调用该函数的线程不知道消息是否被处理。
向线程的队列发送消息的函数还有PostQuitMessage:
VOIDPostQuitMessage(intnExitCode);
为了终止线程的消息循环,可以调用这个函数。
调用PostQuitMessage类似于调用:
PostThreadMessage(GetCurrentThreadId(),WM_QUIT,nExitCode,0);
但是,PostQuitMessage并不实际登记一个消息到任何一个THREADINFO结构的队列。
只是在内部,PostQuitMessage设定QS_QUIT唤醒标志(后面将要讨论),并设置THREADINFO结构的nExitCode成员。
因为这些操作永远不会失败,所以PostQuitMessage的原型被定义成返回VOID。
26.3向窗口发送消息
使用SendMessage函数可以将窗口消息直接发送给一个窗口过程:
LRESULTSendMessage(
窗口过程将处理这个消息。
只有当消息被处理之后,SendMessage才能返回到调用程序。
由于具有这种同步特性,比之PostMessage或PostThreadMessage,SendMessage用得更频繁。
调用这个函数的线程在下一行代码执行之前就知道窗口消息已经被完全处理。
SendMessage是如何工作的呢?
如果调用SendMessage的线程向该线程所建立的一个窗口发送一个消息,SendMessage就很简单:
它只是调用指定窗口的窗口过程,将其作为一个子例程。
当窗口过程完成对消息的处理时,它向SendMessage返回一个值。
SendMessage再将这个值返回给调用线程。
但是,当一个线程向其他线程所建立的窗口发送消息时,SendMessage的内部工作就复杂得多(即使两个线程在同一进程中也是如此)。
Windows要求建立窗口的线程处理窗口的消息。
所以当一个线程调用SendMessage向一个由其他进程所建立的窗口发送一个消息,也就是向其他的线程发送消息,发送线程不可能处理窗口消息,因为发送线程不是运行在接收进程的地址空间中,因此不能访问相应窗口过程的代码和数据。
实际上,发送线程要挂起,而由另外的线程处理消息。
所以为了向其他线程建立的窗口发送一个窗口消息,系统必须执行下面将讨论的动作。
首先,发送的消息要追加到接收线程的发送消息队列,同时还为这个线程设定QS_SENDMESSAGE标志(后面将讨论)。
其次,如果接收线程已经在执行代码并且没有等待消息(如调用GetMessage、PeekMessage或WaitMessage),发送的消息不会被处理,系统不能中断线程来立即处理消息。
当接收进程在等待消息时,系统首先检查QS_SENDMESSAGE唤醒标志是否被设定,如果是,系统扫描发送消息队列中消息的列表,并找到第一个发送的消息。
有可能在这个队列中有几个发送的消息。
例如,几个线程可以同时向一个窗口分别发送消息。
当发生这样的事时,系统只是将这些消息追加到接收线程的发送消息队列中。
当接收线程等待消息时,系统从发送消息队列中取出第一个消息并调用适当的窗口过程来处理消息。
如果在发送消息队列中再没有消息了,则QS_SENDMESSAGE唤醒标志被关闭。
当接收线程处理消息的时候,调用SendMessage的线程被设置成空闲状态(idle),等待一个消息出现在它的应答消息队列中。
在发送的消息处理之后,窗口过程的返回值被登记到发送线程的应答消息队列中。
发送线程现在被唤醒,取出包含在应答消息队列中的返回值。
这个返回值就是调用SendMessage的返回值。
这时,发送线程继续正常执行。
当一个线程等待SendMessage返回时,它基本上是处于空闲状态。
但它可以执行一个任务:
如果系统中另外一个线程向一个窗口发送消息,这个窗口是由这个等待SendMessage返回的线程所建立的,则系统要立即处理发送的消息。
在这种情况下,系统不必等待线程去调用GetMessage、PeekMessage或WaitMessage。
由于Windows使用上述方法处理线程之间发送的消息,所以有可能造成线程挂起(hang)。
例如,当处理发送消息的线程含有错误时,会导致进入死循环。
那么对于调用SendMessage的线程会发生什么事呢?
它会恢复执行吗?
这是否意味着一个程序中的bug会导致另一个程序挂起?
答案是确实有这种可能。
利用4个函数——SendMessageTimeout、SendMessageCallback、SendNotifyMessage和ReplyMessage,可以编写保护性代码防止出现这种情况。
第一个函数是SendMessageTimeout:
LRESULTSendMessageTimeout(
LPARAMlParam,
UINTfuFlags,
UINTuTimeout,
PDWORD_PTRpdwResult);
利用SendMessageTimeout函数,可以规定等待其他线程答回你消息的时间最大值。
前4个参数与传递给SendMessage的参数相同。
对fuFlags参数,可以传递值SMTO_NORMAL(定义为0)、SMTO_ABORTIFHUNG、SMTO_BLOCK、SMTO_NOTIMEOUTIFNOTHUNG或这些标志的组合。
SMTO_ABORTIFHUNG标志是告诉SendMessageTimeout去查看接收消息的线程是否处于挂起状态,如果是,就立即返回。
SMTO_NOTIMEOUTIFNOTHUNG标志使函数在接收消息的线程没有挂起时不考虑等待时间限定值。
SMTO_BLOCK标志使调用线程在SendMessageTimeout返回之前,不再处理任何其他发送来的消息。
SMTO_NORMAL标志在Winuser.h中定义成0,如果不想指定任何其他标志及组合,就使用这个标志。
前面说过,一个线程在等待发送的消息返回时可以被中断,以便处理另一个发送来的消息。
使用SMTO_BLOCK标志阻止系统允许这种中断。
仅当线程在等待处理发送的消息的时候(不能处理别的发送消息),才使用这个标志。
使用SMTO_BLOCK可能会产生死锁情况,直到等待时间期满。
例如,如果你的线程向另外一个线程发送一个消息,而这个线程又需要向你的线程发送消息。
在这种情况下,两个线程都不能继续执行,并且都将永远挂起。
SendMessageTimeout函数中的uTimeout参数指定等待应答消息时间的毫秒数。
如果这个函数执行成功,返回TRUE,消息的结果复制到一个缓冲区中,该缓冲区的地址由pdwResult参数指定。
顺便提一下,这个函数在WinUser.h头文件中的原型是不正确的。
这个函数的原型应该被定义成返回一个BOOL型值,因为LRESULT实际是通过函数的一个参数返回的。
这会引起一些问题,因为如果对函数传递一个无效的窗口句柄或者等待超时,SendMessageTimeout都会返回FALSE。
要知道函数失败详细情况的唯一办法是调用GetLastError。
如果函数是由于等待超时而失败,则GetLastError为0(ERROR_SUCCESS)。
如果对参数传递了一个无效句柄,GetLastError为1400(ERROR_INVALID_WINDOW_HANDLE)。
如果调用SendMessageTimeout向调用线程所建立的窗口发送一个消息,系统只是调用这个窗口的窗口过程,并将返回值赋给pdwResult。
因为所有的处理都必须发生在一个线程里,调用SendMessageTimeout函数之后出现的代码要等消息被处理完之后才能开始执行。
用来在线程间发送消息的第二个函数是SendMessageCallback:
BOOLSendMessageCallback(
SENDASYNCPROCpfnResultCallBack,
ULONG_PTRdwData);
同样,前4个参数同SendMessage中使用的一样。
当一个线程调用SendMessageCallback时,该函数发送消息到接收线程的发送消息队列,并立即返回使发送线程可以继续执行。
当接收线程完成对消息的处理时,一个消息被登记到发送线程的应答消息队列中。
然后,系统通过调用一个函数将这个应答通知给发送线程,该函数是使用下面的原型编写的。
VOIDCALLBACKResultCallBack(
ULONG_PTRdwData,
LRESULTlResult);
必须将这个函数的地址传递给SendMessageCallback函数作为pfnResultCallback参数值。
当调用这个函数时,要把完成消息处理的窗口的句柄传递到第一个参数,将消息值传递给第二个参数。
第三个参数dwData,总是取传递到SendMessageCallback函数的dwData参数的值。
系统只是取出对SendMessageCallback函数指定的参数值,再直接传递到ResultCallback函数。
ResultCallback函数的最后一个参数是处理消息的窗口过程返回的结果。
因为SendMessageCallback在执行线程间发送时会立即返回,所以在接收线程完成对消息的处理时不是立即调用这个回调函数。
而是由接收线程先将一个消息登记到发送线程的应答消息队列。
发送线程在下一次调用GetMessage、PeekMessage、WaitMessage或某个SendMessage*函数时,消息从应答消息队列中取出,并执行ResultCallBack函数。
SendMessageCallback函数还有另外一个用处。
Windows提供了一种广播消息的方法,用这种方法你可以向系统中所有现存的重叠(overlapped)窗口广播一个消息。
这可以通过调用SendMessage函数,对参数hwnd传递HWND_BROADCAST(定义为-1)。
使用这种方法广播的消息,其返回值我们并不感兴趣,因为SendMessage函数只能返回一个LRESULT。
但使用SendMessageCallback,就可以向每一个重叠窗口广播消息,并查看每一个返回结果。
对每一个处理消息的窗口的返回结果都要调用ResultCallback函数。
如果调用SendMessageCallback向一个由调用线程所建立的窗口发送一个消息,系统立即调用窗口过程,并且在消息被处理之后,系统调用ResultCallBack函数。
在ResultCallBack函数返回之后,系统从调用SendMessageCallback的后面的代码行开始执行。
线程间发送消息的第三个函数是SendNotifyMessage:
BOOLSendNotifyMessage(
SendNotifyMessage将一个消息置于接收线程的发送消息队列中,并立即返回到调用线程。
这一点与PostMessage函数一样,但SendNotifyMessage在两方面与PostMessage不同。
首先,SendNotifyMessage是向另外的线程所建立的窗口发送消息,发送的消息比起接收线程消息队列中存放的登记消息有更高的优先级。
换句话说,由SendNotifyMessage函数存放在队列中的消息总是在PostMessage函数登记到队列中的消息之前取出。
其次,当向一个由调用进程建立的窗口发送消息时,SendNotifyMessage同SendMessage函数完全一样:
SendNotifyMessage在消息被处理完之后才能返回。
我们已经知道,发送给窗口的大多数消息是用于通知的目的。
也就是,发送消息是因为窗口需要知道某个状态已经发生变化,在程序能够继续执行之前,窗口要做某种处理。
例如,WM_ACTIVATE、WM_DESTROY、WM_ENABLE、WM_SIZE、WM_SETFOCUS和WM_MOVE等都是系统发送给窗口的通知,而不是登记的消息。
这些消息是系统对窗口的通知,因此系统不需要停止运行以等待窗口过程处理这些消息。
与此相对应,如果