26 窗口消息
本章介绍Microsoft的消息系统是如何支持带有图形界面的应用程序的。首先设计Win2K
以后的消息系统时,有两个主要目标:
- 尽可能保持与过去16位Windows兼容,偏于移植现有的16位Windows程序。
- 使窗口系统强壮,一个线程不会对系统的其他线程产生不利的影响。
只不过往往事非所愿。16位系统中,向窗口发送一个消息总是同步执行的,发送消息的程序要等接收消息的程序完全处理消息后才能继续运行。但是如果接受消息的窗口要花很长的时间来处理消息,或者直接挂起了,那么发送程序就不能继续执行。这是不强壮的。那只能折衷一下了。
首先是一些基本原则。由于Windows允许一个进程最多建立10 000个不同类型的用户对象(图像、光标、窗口类、菜单、加速键等等),这些用户对象归创建这些对象的线程的进程
所有,当进程结束而又没有明确地删除这个对象,操作系统会自动删除这些对象;对于窗口和挂钩(hook
) 这两种用户对象,就分别由建立窗口和安装挂钩的线程所拥有,如果线程结束,操作系统就会自动删除窗口或卸载挂钩。
所以,建立窗口的线程必须是为窗口处理所有消息的线程
。这意味着:
- 如果线程建立了一个窗口,然后就结束了,那它不会收到
WM_DESTORY
或者WM_NCDESTROY
消息。 - 每个线程,如果建立了至少一个窗口,都由系统对它分配一个消息队列,用于窗口消息的派送。而为了接收这些消息,线程又要有自己的消息循环。
26.1 线程的消息队列
在成功创建进程之后,线程就有了运行的环境,并且每一个线程都相信自己是唯一的线程,有一个独立的环境,用来维持自己的键盘焦点、窗口激活、鼠标捕获等概念。
而线程被成功创建后,系统会假定线程不会用户和用户的任何任务,减少系统资源的耗费。但是一旦和图形用户界面关联(检查一个消息队列或者建立一个窗口),系统就会为线程分配一个重要的结构THREADINFO
。这个结构如下所示:
26.2 将消息发送到线程的消息队列中
当上面的操作完成,线程就有了自己的消息队列集合,进程中有几个线程而且它们都调用CreateWindow
就有几个消息队列集合。消息被放置在线程的登记消息队列
中,这要通过调用PostMessage
函数来完成:
BOOL PostMessage(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam);
消息的接收窗口就用hwnd
句柄来标识。然后这个消息就被放在一块系统分配的内存,添加到线程的登记消息队列中。并且函数还给消息设置QS_POSTMESSGEAGE
唤醒位,一旦被登记立即返回,不过也因为这样调用这个函数的线程就没有办法知道消息何时会被处理,甚至会不会被处理。
可用PostThreadMessage
函数可以将消息放置在线程的登记消息队列中。
BOOL PostThreadMessage(
DWORD dwThreadId,
UINT uMsg,
WPARAM wParam,
LPARAM lParam);
可用GetWindowsThreadProcessId
函数确定是哪个线程建立了一个窗口。
DWORD GetWindowsThreadProcessId(
HWND hwnd,
PDWORD pdwProcessId);
这个函数返回线程的ID,这个线程建立了hwnd
参数标识的窗口。如果传递参数pdwProcessId
还可以获取拥有线程的进程的ID,也可以不传递。
PostThreadMessage
接收消息的线程由第一个参数标记。当消息被设置到队列中后,MSG结构的hwnd
。如果程序中需要执行一些特殊的处理就要调用这个函数。
在主消息循环中处理消息时,要检查hwnd
是否为NULL, 并进程MSG结构的msg程序来执行特别的处理,如果没有消息被处理就调用DispatchMessage
。再之后再循环处理下一个消息。
PostThreadMessage
和PostMessage
一样,都是登记了消息就返回。
向线程发送消息的还有PostQuitMessage
可以终止线程的消息循环,但是它并不实际登记一个消息到任何一个THREADINFO
结构的队列,只是在内部设置QS_QUIT
唤醒标志和结构体里的nExitCode
成员。这个函数不会失败,所以返回值是VOID。
26.3 向窗口发送消息
使用SendMessage
函数可以把窗口消息直接发给一个窗口过程:
LRESULT SendMessage(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM wParam);
只有当消息被处理之后,SendMessage
才能够返回调用它的程序,调用这行代码的线程在执行下一行代码前就知道这个信息已经被处理了,并得到一个来自目标窗口过程的返回值。
当消息是发送到另一个线程的窗口过程的时候更复杂,因为发送消息的线程并非运行在接收消息的进程的地址空间中,只能挂起等待,由系统执行以下的动作。
- 发送的消息被追加到接收线程的发送消息队列,并为接收线程设定
QS_SENDMESSGE
标志,并处于idle
状态,等待一个消息出现在它的应答消息队列中。- 如果接收消息的线程没有处理消息的过程(如调用
GetMessage
),发送的消息不会被处理,系统不会中断线程来立即处理消息。 - 如果线程在等待消息,就扫描
QS_SENDMESSGE
标志:- 如果是,系统扫描发送消息队列中的列表,并找到第一个信息,用合适的窗口过程处理消息。
- 如果没有发送消息队列中没有消息了,
QS_SENDMESSGE
标志则被关闭。
- 如果接收消息的线程没有处理消息的过程(如调用
- 当发送的消息被处理后,窗口过程的返回值被登记到发送线程的应答消息队列,发送线程被唤醒,并处理这个返回值,发送线程回归正常运行。
当然,如果发送线程即便被挂起,也是可以执行一些任务的,如果它也有窗口过程,等待处理消息的话。
使用SendMessage
会造成线程挂起,这是有可能引起bug的。如果处理消息的过程含有错误进入了死循环,那么发送消息的线程就有可能永远挂起了。这个时候,就要使用SendMessageTimeout
、SendMessageCallback
、SendNotifyTimeout
和ReplyMessage
就可以防止这种情况。
LRESULT SendMessageTimeout(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam,
UINT fuFlags,
UINT uTimeout,
PDWORD_PTR pdwResult);
- 对于
fuFlags
参数,可以是以下的值:SMTO_NORMAL
如果不想使用以下的标志,就使用这个。SMTO_ABORTIFHUNG
,告诉函数去查看接收消息的线程是否处理挂起状态,是就立即返回。SMTO_NOTIMEOUTIFNOTHUNG
使得接收消息的线程不考虑等待时间的限定值。SMTO_BLOCK
使得线程在函数返回前不处理任何收到的信息。- 前面说过线程即使因为调用了
SendMessage
而挂起也是有可能处理其他的任务的,现在可以使用SMTO_BLOCK
屏蔽这种可能。当然也会产生死锁,知道timeout指定的时间期限结束。
uTimeout
等待应答时间的毫秒数。pdwResult
保存返回值。- 这个函数的返回值应该是
BOOL
而不是LRESULTE
类型,这会引起一些问题。- 如果发生错误,返回值是FALSE,而
GetLastError
为0(ERROR_SUCCESS)。 - 如果对参数传递了一个无效的句柄,
GetLastError
为1400(ERROR_INVALID_WINDOW_HANDLE)。
- 如果发生错误,返回值是FALSE,而
BOOL SendMessageCallback(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam,
SENDASYNCPROC pfnResultCallback,
ULONG_PTR dwData);
接收线程得到的消息放在发送消息队列,发送线程就可以立即返回。当消息的处理完成时,一个消息被登记到发送线程的应答消息队列中,系统也通过调用一个函数将这个应答发送给线程,函数的原型如下:
VOID CALLBACK ResultCallback(
HWND hwnd,
UINT uMsg,
ULONG dwData,
LRESULT pdwResult);
这个函数的地址就当作SendMessageCallback
的参数。这个函数的第一个参数是窗口的句柄,第二个参数是消息,第三个消息dwData
是SendMessageCallback
函数中的dwData
参数。最后一个参数pdwResult
处理消息的窗口过程返回的结果。
要注意回调函数的时机并不是在SendMessageCallback
函数返回后就执行,而是先把消息登记到一个发送线程的应答消息队列。发送线程调用处理消息的函数时,消息从应答消息队列中取出,并执行ResultCallback
函数。
SendMessageCallback
还可以实现广播的效果。它通过向每一个重叠(overlapped)窗口广播一个消息,并查看每一个结果。对每个处理消息的返回结果都要调用ResultCallback
函数。
如果SendMessageCallback
把消息发送给一个有窗口的线程,系统立即调用窗口过程,并在消息被处理后调用ResultCallback
函数。ResultCallback
执行完之后,SendMessageCallback
之后的代码才开始执行。(变回同步了?)
BOOL SendNotifyMessage(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam);
函数将一个消息至于接收线程的发送消息队列中,并立即返回到调用线程,这一点和PostMessage一样
但也有不同:
SendNotifyMessage
是向其他线程建立的窗口发送信息,发送的消息比起接收线程消息队列中的登记消息有更高的优先权。- 如果向一个建立了窗口的线程发送消息,
SendNotifyMessage
在消息处理完后才能返回。
消息的目的是通知,是让接收方知道某个状态已经发生变化,而在这之前有做某些处理的机会。
第四个用于向线程发送消息的函数是ReplyMessage
:
BOOL ReplyMessage(LRESULTE lResult);
这个函数略微的不同,是为了接收窗口消息。当这个函数被调用的时候,线程是想让系统知道它已经完成了足够的工作,结果应该包装起来并登记到发送线程的应答消息队列中。强迫发送线程获得结果,恢复运行。
唯一的参数指出消息处理的结果。在调用ReplyMessage
后,发送消息的线程恢复运行,而处理消息的线程继续处理消息,两个线程都不会被挂起,可以正常地执行。
这里唯一的问题是,在ReplyMessage
函数前面介绍的三个函数都不适合用来实现一些保护性的代码,而是推荐使用ReplyMessage
。另外如果在发送消息的线程在调用这个函数,它其实什么也不做。如果你在处理线程间的消息的时候调用了ReplyMessage
,则它返回TRUE;如果你在处理线程内的信息发送时调用了ReplyMessage
,它返回FALSE。如果你想要知道是前者还是后者,可以调用InMessage
。这个函数在处理线程间发送的消息时,返回TRUE;而线程处理线程内发送的或登记的消息时,返回FALSE。另外ReplyMessageEx
也可以做同样的事,只是唯一的参数要填NULL。 ReplyMessageEx
的返回值是DWORD
,代表正在处理的消息的类型,如果是0(ISMEX_NOSEND
),表示处理的消息是线程内发送或者登记的消息;否则就是ISMEX_SEND
、ISMEX_NOTIFY
、ISMEX_CALLBACK
、ISMEX_REPLIED
的组合。
ISMEX_SEND
:处理线程间的消息,使用SendMessage
或者SendMessageTimeout
发送,如果没有ISMEX_REPLIED
标志,发送线程被阻塞,等待应答。ISMEX_NOTIFY
:处理线程间的消息,使用SendNotifyMessage
发送,发送线程不阻塞,也不应答。ISMEX_CALLBACK
:处理线程间的消息,使用SendMessageCallback
发送,发送线程不阻塞,也不应答。ISMEX_REPLIED
:处理线程间的消息,已经调用ReplyMessage
,发送线程不阻塞。
26.4 唤醒一个线程
当一个线程调用GetMessage
或WaitMessage
,但没有对这个线程或者这个线程建立的窗口的消息时,系统可以挂起这个线程,这样系统就不再给它分配CPU时间。当一个消息被登记或发送到这个线程,系统要设置一个唤醒标志,指出要给这个线程分配CPU时间以处理消息。
26.4.1 队列状态标志
当一个线程正在运行,可以通过GetQueueStatus
函数查询队列的状态:
DWORD GetQueueStatus(UINT fuFlags);
fuFlags
参数是由一个或以上的标志连接起来,可用来测试特定的唤醒位,可以用OR连接,连接得越少查询得越快。返回值的高字(HIWORD
)是消息的类型,低字(LOWWORD
)。
内存映像文件 共享 wParam
或lParam
表示一个指向数据结构的指针的时候
WM_GETTEXT
是两个消息,先搞到长度,再复制内容
如果是WM_USER+X类的消息又如何?
用特殊的窗口WM_COPYDATA
消息 COPYDATASTRUCT
结构体
dwData
指定字节数 lpData
是要发送的内容的第一个字节
CopyData
示例程序。