窗口消息

本章介绍Microsoft Wi n d o w s的消息系统是如何支持带有图形用户界面的应用程序的。在设计Windows 2000或Windows 98所使用的窗口系统时,微软有两个主要目标:

• 尽可能保持与过去1 6位Wi n d o w s的兼容性,便于开发人员移植他们已有的1 6位Wi n d o w s程序。

• 使窗口系统强壮,一个线程不会对系统中其他线程产生不利影响。

但是,这两个目标是直接相互冲突的。在1 6位Wi n d o w s系统中,向窗口发送一个消息总是按同步方式执行的:发送程序要在接受消息的窗口完全处理完消息之后才能继续运行。这通常是一个所期望的特性。但是,如果接收消息的窗口花很长的时间来处理消息或者出现挂起,则发送程序就不能再执行。这意味着系统是不强壮的。

这种冲突给微软的设计人员带来了一定的困难。他们的解决方案是两个相互冲突目标之间的出色折衷方案。如果在阅读本章时记住这两个目标,你就会更多地理解微软为什么会做出这样的设计。

我们从一些基本原则开始讨论。Wi n d o w s允许一个进程至多建立10 000个不同类型的用户对象(User object):图符、光标、窗口类、菜单、加速键表等等。当一个线程调用一个函数来建立某个对象时,则该对象就归这个线程的进程所拥有。这样,当进程结束时,如果没有明确删除这个对象,则操作系统会自动删除这个对象。对窗口和挂钩( h o o k )这两种U s e r对象,它们分别由建立窗口和安装挂钩的线程所拥有。如果一个线程建立一个窗口或安装一个挂钩,然后线程结束,操作系统会自动删除窗口或卸载挂钩。

这种线程拥有关系的概念对窗口有重要的意义:建立窗口的线程必须是为窗口处理所有消息的线程。为了使这个概念更加明确具体,可以想像一个线程建立了一个窗口,然后就结束了。在这种情况下,窗口不会收到一个W M _ D E S T R O Y或W M _ N C D E S T R O Y消息,因为线程已经结束,不可能被用来使窗口接收和处理这些消息。

这也意味着每个线程,如果它至少建立了一个窗口,都由系统对它分配一个消息队列。这个队列用于窗口消息的派送( d i s p a t c h)。为了使窗口接收这些消息,线程必须有它自己的消息循环。本章要考查每个线程的消息队列。特别是要看看消息是如何被放置在队列中的,以及线程如何从队列中取出消息并处理它们。


26.1 线程的消息队列

前面已经说过,Wi n d o w s的一个主要目标是为程序的运行提供一个强壮的环境。为实现这个目标,要保证每个线程运行在一个环境中,在这个环境中每个线程都相信自己是唯一运行的线程。更确切地说,每个线程必须有完全不受其他线程影响的消息队列。而且,每个线程必须有一个模拟环境,使线程可以维持它自己的键盘焦点(keyboard focus)、窗口激活、鼠标捕获等概念。

当一个线程第一次被建立时,系统假定线程不会被用于任何与用户相关的任务。这样可以减少线程对系统资源的要求。但是,一旦这个线程调用一个与图形用户界面有关的函数(例如检查它的消息队列或建立一个窗口),系统就会为该线程分配一些另外的资源,以便它能够执行与用户界面有关的任务。特别是,系统分配一个T H R E A D I N F O结构,并将这个数据结构与线程联系起来。

这个T H R E A D I N F O结构包含一组成员变量,利用这组成员,线程可以认为它是在自己独占的环境中运行。T H R E A D I N F O是一个内部的、未公开的数据结构,用来指定线程的登记消息队列(posted-message queue)、发送消息队列( send-message queue)、应答消息队列( r e p l y -message queue)、虚拟输入队列(virtualized-input queue)、唤醒标志(wake flag)、以及用来描述线程局部输入状态的若干变量。图2 6 - 1描述了T H R E A D I N F O结构和与之相联系的三个线程。


图26-1 三个线程及相应的T H R E A D I N F O结构

这个T H R E A D I N F O结构是窗口消息系统的基础,在阅读下面各节内容时,应该参考该图。


26.2 将消息发送到线程的消息队列中

当线程有了与之相联系的T H R E A D I N F O结构时,线程就有了自己的消息队列集合。如果一个进程建立了三个线程,并且所有这些线程都调用C r e a t e Wi n d o w,则将有三个消息队列集合。消息被放置在线程的登记消息队列中,这要通过调用P o s t M e s s a g e函数来完成:

BOOL PostMessage(
   HWND hwnd,
   UINT uMsg,
   WPARAM wParam,
   LPARAM lParam);
当一个线程调用这个函数时,系统要确定是哪一个线程建立了用h w n d参数标识的窗口。然后系统分配一块内存,将这个消息参数存储在这块内存中,并将这块内存增加到相应线程的登记消息队列中。并且,这个函数还设置Q S _ P O S T M E S S A G E唤醒位(后面会简单讨论)。函数P o s t M e s s a g e在登记了消息之后立即返回,调用该函数的线程不知道登记的消息是否被指定窗口的窗口过程所处理。实际上,有可能这个指定的窗口永远不会收到登记的消息。如果建立这个特定窗口的线程在处理完它的消息队列中的所有消息之前就结束了,就会发生这种事。

还可以通过调用P o s t T h r e a d M e s s a g e将消息放置在线程的登记消息队列中。

BOOL PostThreadMessage(
   DWORD dwThreadId,
   UINT uMsg,
   WPARAM wParam,
   LPARAM lParam);
注意可以通过调用G e t Wi n d o w s T h r e a d P r o c e s s I d来确定是哪个线程建立了一个窗口。

DWORD GetWindowThreadProcessId(
   HWND hwnd,
   PDWORD pdwProcessId);
这个函数返回线程的I D,这个线程建立了h w n d参数所标识的窗口。线程I D在全系统范围内是唯一的。还可以通过对p d w P r o c e s s I d参数传递一个D W O R D地址来获取拥有该线程的进程I D,这个进程I D在全系统范围内也是唯一的。通常,我们不需要进程I D,只须对这个参数传递一个N U L L。

P o s t T h r e a d M e s s a g e函数所期望的线程由第一个参数d w T h r e a d I d所标记。当消息被设置到队列中时,M S G结构的h w n d成员将设置成N U L L。当程序要在主消息循环中执行一些特殊处理时要调用这个函数。

要对线程编写主消息循环以便在G e t M e s s a g e或P e e k M e s s a g e取出一个消息时,主消息循环代码检查h w n d是否为N U L L,并检查M S G结构的m s g成员来执行特殊的处理。如果线程确定了该消息不被指派给一个窗口,则不调用D i s p a t c h M e s s a g e,消息循环继续取下一个消息。

像P o s t M e s s a g e函数一样,P o s t T h r e a d M e s s a g e在向线程的队列登记了消息之后就立即返回。调用该函数的线程不知道消息是否被处理。

向线程的队列发送消息的函数还有P o s t Q u i t M e s s a g e:

VOID PostQuitMessage(int nExitCode);
为了终止线程的消息循环,可以调用这个函数。调用P o s t Q u i t M e s s a g e类似于调用:

PostThreadMessage(GetCurrentThreadId(), WM_QUIT, nExitCode, 0);
但是,P o s t Q u i t M e s s a g e并不实际登记一个消息到任何一个T H R E A D I N F O结构的队列。只是在内部, P o s t Q u i t M e s s a g e设定Q S_Q U I T唤醒标志(后面将要讨论),并设置T H R E A D I N F O结构的n E x i t C o d e成员。因为这些操作永远不会失败,所以P o s t Q u i t M e s s a g e的原型被定义成返回V O I D。


26.3 向窗口发送消息

使用S e n d M e s s a g e函数可以将窗口消息直接发送给一个窗口过程:

LRESULT SendMessage(
   HWND hwnd,
   UINT uMsg,
   WPARAM wParam,
   LPARAM lParam);
窗口过程将处理这个消息。只有当消息被处理之后, S e n d M e s s a g e才能返回到调用程序。由于具有这种同步特性,比之P o s t M e s s a g e或P o s t T h r e a d M e s s a g e,S e n d M e s s a g e用得更频繁。调用这个函数的线程在下一行代码执行之前就知道窗口消息已经被完全处理。

S e n d M e s s a g e是如何工作的呢?如果调用S e n d M e s s a g e的线程向该线程所建立的一个窗口发送一个消息,S e n d M e s s a g e就很简单:它只是调用指定窗口的窗口过程,将其作为一个子例程。当窗口过程完成对消息的处理时,它向S e n d M e s s a g e返回一个值。S e n d M e s s a g e再将这个值返回给调用线程。

但是,当一个线程向其他线程所建立的窗口发送消息时, S e n d M e s s a g e的内部工作就复杂得多(即使两个线程在同一进程中也是如此)。Wi n d o w s要求建立窗口的线程处理窗口的消息。所以当一个线程调用S e n d M e s s a g e向一个由其他进程所建立的窗口发送一个消息,也就是向其他的线程发送消息,发送线程不可能处理窗口消息,因为发送线程不是运行在接收进程的地址空间中,因此不能访问相应窗口过程的代码和数据。实际上,发送线程要挂起,而由另外的线程处理消息。所以为了向其他线程建立的窗口发送一个窗口消息,系统必须执行下面将讨论的动作。

首先,发送的消息要追加到接收线程的发送消息队列,同时还为这个线程设定Q S _ S E N D M E S S A G E标志(后面将讨论)。其次,如果接收线程已经在执行代码并且没有等待消息(如调用G e t M e s s a g e、P e e k M e s s a g e或Wa i t M e s s a g e),发送的消息不会被处理,系统不能中断线程来立即处理消息。当接收进程在等待消息时,系统首先检查Q S _ S E N D M E S S A G E唤醒标志是否被设定,如果是,系统扫描发送消息队列中消息的列表,并找到第一个发送的消息。有可能在这个队列中有几个发送的消息。例如,几个线程可以同时向一个窗口分别发送消息。当发生这样的事时,系统只是将这些消息追加到接收线程的发送消息队列中。

当接收线程等待消息时,系统从发送消息队列中取出第一个消息并调用适当的窗口过程来处理消息。如果在发送消息队列中再没有消息了,则Q S _ S E N D M E S S A G E唤醒标志被关闭。当接收线程处理消息的时候,调用S e n d M e s s a g e的线程被设置成空闲状态( i d l e),等待一个消息出现在它的应答消息队列中。在发送的消息处理之后,窗口过程的返回值被登记到发送线程的应答消息队列中。发送线程现在被唤醒,取出包含在应答消息队列中的返回值。这个返回值就是调用S e n d M e s s a g e的返回值。这时,发送线程继续正常执行。

当一个线程等待S e n d M e s s a g e返回时,它基本上是处于空闲状态。但它可以执行一个任务:如果系统中另外一个线程向一个窗口发送消息,这个窗口是由这个等待S e n d M e s s a g e返回的线程所建立的,则系统要立即处理发送的消息。在这种情况下,系统不必等待线程去调用G e t M e s s a g e、Peek Message或Wa i t M e s s a g e。

由于Wi n d o w s使用上述方法处理线程之间发送的消息,所以有可能造成线程挂起( h a n g)。例如,当处理发送消息的线程含有错误时,会导致进入死循环。那么对于调用S e n d M e s s a g e的线程会发生什么事呢?它会恢复执行吗?这是否意味着一个程序中的b u g会导致另一个程序挂起?答案是确实有这种可能。

利用4个函数—— S e n d M e s s a g e Ti m e o u t、S e n d M e s s a g e C a l l b a c k、S e n d N o t i f y M e s s a g e和R e p l y M e s s a g e,可以编写保护性代码防止出现这种情况。第一个函数是S e n d M e s s a g e Ti m e o u t:

LRESULT SendMessageTimeout(
   HWND hwnd,
   UINT uMsg,
   WPARAM wParam,
   LPARAM lParam,
   UINT fuFlags,
   UINT uTimeout,
   PDWORD_PTR pdwResult);
利用S e n d M e s s a g e Ti m e o u t函数,可以规定等待其他线程答回你消息的时间最大值。前4个参数与传递给S e n d M e s s a g e的参数相同。对f u F l a g s参数,可以传递值S M TO _ N O R M A L (定义为0 )、S M TO _ A B O RT I F H U N G、S M TO _ B L O C K、S M TO _ N O T I M E O U T I F N O T H U N G或这些标志的组合。

S M TO _ A B O RT I F H U N G标志是告诉S e n d M e s s a g e Ti m e o u t去查看接收消息的线程是否处于挂起状态,如果是,就立即返回。S M TO _ N O T I M E O U T I F N O T H U N G标志使函数在接收消息的线程没有挂起时不考虑等待时间限定值。S M T O _ B L O C K 标志使调用线程在S e n d M e s s a g e Ti m e o u t返回之前,不再处理任何其他发送来的消息。S M TO _ N O R M A L标志在Wi n u s e r. h中定义成0,如果不想指定任何其他标志及组合,就使用这个标志。

前面说过,一个线程在等待发送的消息返回时可以被中断,以便处理另一个发送来的消息。使用S M TO _ B L O C K标志阻止系统允许这种中断。仅当线程在等待处理发送的消息的时候(不能处理别的发送消息),才使用这个标志。使用S M TO _ B L O C K可能会产生死锁情况,直到等待时间期满。例如,如果你的线程向另外一个线程发送一个消息,而这个线程又需要向你的线程发送消息。在这种情况下,两个线程都不能继续执行,并且都将永远挂起。

S e n d M e s s a g e Ti m e o u t函数中的u Ti m e o u t参数指定等待应答消息时间的毫秒数。如果这个函数执行成功,返回T R U E,消息的结果复制到一个缓冲区中,该缓冲区的地址由p d w R e s u l t参数指定。

顺便提一下,这个函数在Wi n U s e r. h头文件中的原型是不正确的。这个函数的原型应该被定义成返回一个B O O L型值,因为L R E S U LT实际是通过函数的一个参数返回的。这会引起一些问题,因为如果对函数传递一个无效的窗口句柄或者等待超时, S e n d M e s s a g e Ti m e o u t都会返回FA L S E。要知道函数失败详细情况的唯一办法是调用G e t L a s t E r r o r。如果函数是由于等待超时而失败,则G e t L a s t E r r o r为0(E R R O R _ S U C C E S S)。如果对参数传递了一个无效句柄,G e t L a s t E r r o r为1 4 0 0(E R R O R _ I N VA L I D _ W I N D O W _ H A N D L E)。

如果调用S e n d M e s s a g e Ti m e o u t向调用线程所建立的窗口发送一个消息,系统只是调用这个窗口的窗口过程,并将返回值赋给p d w R e s u l t。因为所有的处理都必须发生在一个线程里,调用S e n d M e s s a g e Ti m e o u t函数之后出现的代码要等消息被处理完之后才能开始执行。

用来在线程间发送消息的第二个函数是S e n d M e s s a g e C a l l b a c k:

BOOL SendMessageCallback(
   HWND hwnd,
   UINT uMsg,
   WPARAM wParam,
   LPARAM lParam,
   SENDASYNCPROC pfnResultCallBack,
   ULONG_PTR dwData);
同样,前4个参数同S e n d M e s s a g e中使用的一样。当一个线程调用S e n d M e s s a g e C a l l b a c k时,该函数发送消息到接收线程的发送消息队列,并立即返回使发送线程可以继续执行。当接收线程完成对消息的处理时,一个消息被登记到发送线程的应答消息队列中。然后,系统通过调用一个函数将这个应答通知给发送线程,该函数是使用下面的原型编写的。

VOID CALLBACK ResultCallBack(
   HWND hwnd,
   UINT uMsg,
   ULONG_PTR dwData,
   LRESULT lResult);
必须将这个函数的地址传递给S e n d M e s s a g e C a l l b a c k函数作为p f n R e s u l t C a l l b a c k参数值。当调用这个函数时,要把完成消息处理的窗口的句柄传递到第一个参数,将消息值传递给第二个参数。第三个参数d w D a t a,总是取传递到S e n d M e s s a g e C a l l b a c k函数的d w D a t a参数的值。系统只是取出对S e n d M e s s a g e C a l l b a c k函数指定的参数值,再直接传递到R e s u l t C a l l b a c k函数。R e s u l t C a l l b a c k函数的最后一个参数是处理消息的窗口过程返回的结果。

因为S e n d M e s s a g e C a l l b a c k在执行线程间发送时会立即返回,所以在接收线程完成对消息的处理时不是立即调用这个回调函数。而是由接收线程先将一个消息登记到发送线程的应答消息队列。发送线程在下一次调用G e t M e s s a g e、P e e k M e s s a g e、Wa i t M e s s a g e或某个S e n d M e s s a g e*函数时,消息从应答消息队列中取出,并执行R e s u l t C a l l B a c k函数。

S e n d M e s s a g e C a l l b a c k函数还有另外一个用处。Wi n d o w s提供了一种广播消息的方法,用这种方法你可以向系统中所有现存的重叠( o v e r l a p p e d)窗口广播一个消息。这可以通过调用S e n d M e s s a g e函数,对参数h w n d传递H W N D _ B R O A D C A S T(定义为- 1)。使用这种方法广播的消息,其返回值我们并不感兴趣,因为S e n d M e s s a g e函数只能返回一个L R E S U LT。但使用S e n d M e s s a g e C a l l b a c k,就可以向每一个重叠窗口广播消息,并查看每一个返回结果。对每一个处理消息的窗口的返回结果都要调用R e s u l t C a l l b a c k函数。

如果调用S e n d M e s s a g e C a l l b a c k向一个由调用线程所建立的窗口发送一个消息,系统立即调用窗口过程,并且在消息被处理之后,系统调用R e s u l t C a l l B a c k函数。在R e s u l t C a l l B a c k函数返回之后,系统从调用S e n d M e s s a g e C a l l b a c k的后面的代码行开始执行。

线程间发送消息的第三个函数是S e n d N o t i f y M e s s a g e:

BOOL SendNotifyMessage(
   HWND hwnd,
   UINT uMsg,
   WPARAM wParam,
   LPARAM lParam);
S e n d N o t i f y M e s s a g e将一个消息置于接收线程的发送消息队列中,并立即返回到调用线程。这一点与P o s t M e s s a g e函数一样,但S e n d N o t i f y M e s s a g e在两方面与P o s t M e s s a g e不同。

首先,S e n d N o t i f y M e s s a g e是向另外的线程所建立的窗口发送消息,发送的消息比起接收线程消息队列中存放的登记消息有更高的优先级。换句话说,由S e n d N o t i f y M e s s a g e函数存放在队列中的消息总是在P o s t M e s s a g e函数登记到队列中的消息之前取出。

其次,当向一个由调用进程建立的窗口发送消息时, S e n d N o t i f y M e s s a g e同S e n d M e s s a g e函数完全一样:S e n d N o t i f y M e s s a g e在消息被处理完之后才能返回。

我们已经知道,发送给窗口的大多数消息是用于通知的目的。也就是,发送消息是因为窗口需要知道某个状态已经发生变化,在程序能够继续执行之前,窗口要做某种处理。例如,W M _ A C T I VAT E、W M _ D E S T R O Y、W M _ E N A B L E、W M _ S I Z E、W M _ S E T F O C U S和W M _ M O V E等都是系统发送给窗口的通知,而不是登记的消息。这些消息是系统对窗口的通知,因此系统不需要停止运行以等待窗口过程处理这些消息。与此相对应,如果系统向一个窗口发送一个W M _ C R E AT E消息,则在窗口处理完这个消息之前,系统必须等待。如果返回值是-1,则不再建立窗口。

第四个用于线程发送消息的函数是R e p l y M e s s a g e:

BOOL ReplyMessage(LRESULT lResult);
这个函数与前面讨论过的三个函数不同。线程使用S e n d M e s s a g e Ti m e o u t、S e n d M e s s a g eC a l l b a c k和S e n d N o t i f y M e s s a g e发送消息,是为了保护自己以免被挂起。而线程调用R e p l yM e s s a g e是为了接收窗口消息。当一个线程调用R e p l y M e s s a g e时,它是要告诉系统,为了知道消息结果,它已经完成了足够的工作,结果应该包装起来并登记到发送线程的应答消息队列中。这将使发送线程醒来,获得结果,并继续执行。

调用R e p l y M e s s a g e的线程在l R e s u l t参数中指出消息处理的结果。在调用R e p l y M e s s a g e之后,发送消息的线程恢复执行,而处理消息的线程继续处理消息。两个线程都不会被挂起,都可以正常执行。当处理消息的线程从它的窗口过程返回时,它返回的任何值都被忽略。

这里的问题是, R e p l y M e s s a g e必须在接收消息的窗口过程中调用,而不是由调用某个S e n d*函数的线程调用。为了编写保护性代码,最好不要用前面讨论过的三个S e n d*函数中的一个代替对S e n d M e s s a g e的调用,而是依靠窗口过程的实现者来调用R e p l y M e s s a g e。

还应该知道,如果在处理一个由同一线程发送来的消息时调用R e p l y M e s s a g e,则该函数什么也不做。实际上,这就是R e p l y M e s s a g e的返回值所指出的。如果你在处理线程间的消息发送时调用了R e p l y M e s s a g e ,则它返回T R U E ,如果你在处理线程内的消息发送时调用R e p l y M e s s a g e,它返回FA L S E。

有时候,你可能想知道究竟是在处理线程间的消息发送,还是在处理线程内的消息发送。为了搞清楚这一点,可以调用I n S e n d M e s s a g e:

BOOL InSendMessage();
这个函数的名字不能够确切说明它究竟做什么事。初看时,你会以为,当线程在处理一个发送的消息时,该函数返回T R U E,而在处理一个登记的消息时,它返回FA L S E。如果这样想你就错了。这个函数在线程处理线程间发送的消息时,返回T R U E,而在线程处理线程内发送的或登记的消息时,返回FA L S E。I n S e n d M e s s a g e和R e p l y M e s s a g e的返回值是一样的。

还可以调用另外一个函数来确定窗口过程正在处理的消息类型:

DWORD InSendMessageEx(PVOID pvReserved);
当调用这个函数时,必须对p v R e s e r v e d参数传递N U L L。这个函数的返回值指出正在处理的消息的类型。如果返回值是I S M E X _ N O S E N D(定义为0),表示线程正在处理一个线程内发送的或登记的消息。如果返回值不是I S M E X _ N O S E N D,就是表2 6 - 1中描述的位标志的组合。

表26-1 位标志的组合

标志描述
I S M E X _ S E N D线程在处理一个线程间发送的消息,该消息是用S e n d M e s s a g e或Send Message Ti m e o u t函数发送的。如果没有设定I S M E X _ R E P L I E D标志,发送线程被阻塞,等待应答
I S M E X _ N O T I F Y线程在处理一个线程间发送的消息,该消息是用SendNotifyM e s s a g e函数发送的。发送线程不等待应答,也不会阻塞
I S M E X _ C A L L B A C K线程在处理线程间发送的消息,该消息是用S e n d M e s s a g eC a l l b a c k发送的。发送线程不等待应答,也不会被阻塞
I S M E X _ R E P L I E D线程在处理线程间发送的消息,并已经调用R e p l y M e s s a g e。发送线程不会被阻塞


26.4 唤醒一个线程

当一个线程调用G e t M e s s a g e或Wa i t M e s s a g e,但没有对这个线程或这个线程所建立窗口的消息时,系统可以挂起这个线程,这样就不再给它分配C P U时间。当有一个消息登记或发送到这个线程,系统要设置一个唤醒标志,指出现在要给这个线程分配C P U时间,以便处理消息。正常情况下,如果用户不按键或移动鼠标,就没有消息发送给任何窗口。这意味着系统中大多数线程没有被分配给C P U时间。

26.4.1 队列状态标志

当一个线程正在运行时,它可以通过调用G e t Q u e u e S t a t u s函数来查询队列的状态:

DWORD GetQueueStatus(UINT fuFlags);
参数f u F l a g s是一个标志或一组由O R连接起来的标志,可用来测试特定的唤醒位。表2 6 - 2给出了各个标志取值及含义。

表26-2 标志取值及含义

标志队列中的消息
Q S _ K E YW M _ K E Y U P、W M _ K E Y D O W N、W M _ S Y S K E Y U P或W M _ S Y S K E Y D O W N
Q S _ M O U S E M O V EW M _ M O U S E M O V E
Q S _ M O U S E B U T TO NW M _ ? B U T TO N *(其中?代表L、M或R、*代表D O W N、U P或DBLCLK )
Q S _ M O U S E同Q S _ M O U S E M O V E | Q S _ M O U S E B U T TO N
Q S _ I N P U T同Q S _ M O U S E | Q S _ K E Y
Q S _ PA I N TW M _ PA I N T
Q S _ T I M E RW M _ T I M E R
Q S _ H O T K E YW M _ H O T K E Y
Q S _ P O S T M E S S A G E登记的消息(不同于硬件输入事件)。当队列在期望的消息过滤器范围内没有登记的消息时,这个标志要消除。除此之外,这个标志与Q S _ A L L P O S T M E S S A G E相同
Q S _ A L L P O S T M E S S A G E登记的消息(不同于硬件输入事件)。当队列完全没有登记的消息时(在任何消息过滤器范围),该标志被清除。除此之外,该标志与Q S _ P O S T M E S S A G E相同
Q S _ A L L E V E N T S同Q S _ I N P U T | Q S _ P O S T M E S S A G E | Q S _ T I M E R | Q S _ PA I N T | Q S _ H O T K E Y
Q S _ Q U I T已调用P o s t Q u i t M e s s a g e。注意这个标志没有公开,所以在Wi n U s e r.h 文件中没有。它由系统在内部使用
Q S _ S E N D M E S S A G E由另一个线程发送的消息
Q S _ A L L I N P U T同QS_ALLEVENTS|QS_SENDMESSAGE

当调用G e t Q u e u e S t a t u s函数时,f u F l a g s将队列中要检查的消息的类型告诉G e t Q u e u e S t a t u s。用O R连接的Q S _ *标识符的数量越少,调用执行的就越快。当G e t Q u e u e S t a t u s返回时,线程的队列中当前消息的类型在返回值的高字(两字节)中。这个返回的标志的集合总是所想要的标志集的子集。例如,对下面的调用:

BOOL fPaintMsgWaiting = HIWORD(GetQueueStatus(QS_TIMER)) & QS_PAINT;
f P a i n t M s g Wa i t i n g的值总是FA L S E,不论队列中是否有一个W M _ PA I N T消息在等待,因为G e t Q u e u e S t a t u s的参数中没有将Q S _ PA I N T指定为一个标志。

G e t Q u e u e S t a t u s返回值的低字指出已经添加到队列中,并且在上一次对函数G e t Q u e u eS t a t u s、G e t M e s s a g e或P e e k M e s s a g e调用以来还没有处理的消息的类型。

不是所有的唤醒标志都由系统平等对待。对于Q S _ M O U S E M O V E标志,只要队列中存在一个未处理的W M _ M O U S E M O V E消息,这个标志就要被设置。当G e t M e s s a g e或PeekMessage (利用P M _ R E M O V E )从队列中放入新的W M _ M O U S E M O V E消息之前,这个标志被关闭。Q S _ K E Y、Q S _ M O U S E B U T TO N和QS_HOTKEY 标志都根据相应的消息按与此相同的方式处理。

Q S _ PA I N T标志的处理与此不同。如果线程建立的一个窗口有无效的区域, Q S _ PA I N T标志被设置。当这个线程建立的所有窗口所占据的区域变成无效时(通常由于对Va l i d a t e R e c t、Va l i d a t e R e g i o n或B e g i n P a i n t的调用而引起),Q S _ PA I N T标志就被关闭。只有当线程建立的所有窗口都无效时,这个标志才关闭。调用G e t M e s s a g e或P e e k M e s s a g e对这个唤醒标志没有影响。

当线程的登记消息队列中至少有一个消息时, Q S _ P O S T M E S S A G E标志就被设置。这不包括线程的虚拟输入队列中的硬件事件消息。当线程的登记消息队列中的所有消息都已经处理,队列变空时,这个标志被复位。

每当一个定时器(由线程所建立)报时(go off),Q S _ T I M E R标志就被设置。在Get Message或P e e k M e s s a g e返回W M _ T I M E R事件之后,Q S _ T I M E R标志被复位,直到定时器再次报时。

Q S _ S E N D M E S S A G E标志指出有一个消息在线程的发送消息队中。系统在内部使用这个标志,用来确认和处理线程之间发送的消息。对于一个线程向自身发送的消息,不设置这个标志。虽然可以使用Q S _ S E N D M E S S A G E标志,但很少需要这样做。笔者还从未见到一个程序使用这个标志。

还有一个未公开的队列状态标志Q S _ Q U I T。当一个线程调用P o s t Q u i t M e s s a g e时, Q S _Q U I T标志就被设置。系统并不实际向线程的消息队列追加一个W M _ Q U I T消息。G e t Q u e u eS t a t u s函数也不返回这个标志的状态。

26.4.2 从线程的队列中提取消息的算法

当一个线程调用G e t M e s s a g e或P e e k M e s s a g e时,系统必须检查线程的队列状态标志的情况,并确定应该处理哪个消息。图2 6 - 2和下面叙述的步骤说明了系统是如何确定线程应该处理的下一个消息的情况。

1) 如果Q S _ S E N D M E S S A G E标志被设置,系统向相应的窗口过程发送消息。G e t M e s s a g e或P e e k M e s s a g e函数在内部进行这种处理,并且在窗口过程处理完消息之后不返回到线程,这些函数要等待其他要处理的消息。

2) 如果消息在线程的登记消息队列中,函数G e t M e s s a g e或P e e k M e s s a g e填充传递给它们的M S G结构,然后函数返回。这时,线程的消息循环通常调用D i s p a t c h M e s s a g e,让相应的窗口过程来处理消息。

3) 如果Q S _ Q U I T标志被设置。G e t M e s s a g e或P e e k M e s s a g e返回一个W M _ Q U I T消息(其中w P a r a m参数是规定的退出代码)并复位Q S _ Q U I T标志。

4) 如果消息在线程的虚拟输入队列,函数G e t M e s s a g e或P e e k M e s s a g e返回硬件输入消息。

5) 如果Q S _ PA I N T标志被设置, G e t M e s s a g e或P e e k M e s s a g e为相应的窗口返回一个W M -PA I N T消息。

6) 如果Q S _ T I M E R标志被设置,G e t M e s s a g e或P e e k M e s s a g e返回一个W M _ T I M E R消息。


图26-2 从线程队列中提取消息的算法

尽管很难令人相信,但确有理由这样做。微软在设计这个算法时有一个大前提,就是应用程序应该是用户驱动的,用户通过建立硬件输入事件(键盘和鼠标操作)来驱动应用程序。在使用应用程序时,用户可能按一个鼠标按钮,引起一系列要发生的事件。应用程序通过向线程的消息队列中登记消息使每个个别的事件发生。

所以如果按鼠标按钮,处理W M _ L B U T TO N D O W N消息的窗口可能向不同的窗口投送三个消息。由于是硬件事件引发三个软件事件,所以系统要在读取用户的下一个硬件事件之前,先处理这些软件事件。这也说明了为什么登记消息队列要在虚拟输入队列之前检查。

这种事件序列的一个很好的例子是调用Tr a n s l a t e M e s s a g e函数。这个函数检查是否有一个W M _ K E Y D O W N或一个W M _ S Y S K E Y D O W N消息从输入队列中取出。如果有一个这样的消息被取出,系统检查虚键(virtual key)信息是否能转换成等价的字符。如果虚键信息能够转换,Tr a n s l a t e M e s s a g e调用P o s t M e s s a g e将一个W M _ C H A R消息或一个W M _ S Y S C H A R消息放置在登记消息队列中。下次调用G e t M e s s a g e时,系统首先检查登记消息队列中的内容,如果其中有消息存在,从队列中取出消息并将其返回。返回的消息将是W M _ C H A R消息或W M _ S Y S C H A R消息。再下一次调用G e t M e s s a g e时,系统检查登记消息队列,发现队列已空。系统再检查输入队列,在其中找到W M _(S Y S)K E Y U P消息。G e t M e s s a g e返回这个消息。

由于系统是按这种方式工作,下面的硬件事件序列W M _ K E Y D O W N、W M _ K E Y U P生成下面的到窗口过程的消息序列(假定虚键信息可以转换成等价的字符):

WM_KEYDOWN
WM_CHAR
WM_KEYUP
现在我们再回过头来讨论系统如何确定从G e t M e s s a g e或P e e k M e s s a g e返回的消息。在系统检查了登记消息队列之后,但尚未检查虚拟输入队列时,它要检查Q S _ Q U I T标志。我们知道,当线程调用P o s t Q u i t M e s s a g e时设置Q S _ Q U I T标志。调用P o s t Q u i t M e s s a g e类似于(但不相同)调用P o s t T h r e a d M e s s a g e。P o s t T h r e a d M e s s a g e将消息放置在消息队列的尾端,并使消息在检查输入队列之前被处理。为什么P o s t Q u i t M e s s a g e设置一个标志,而不是将W M _ Q U I T消息放入消息队列中?有两个理由。

第一,在低内存(low memory)情况下,登记一个消息有可能失败。如果一个程序想退出,它应该被允许退出,即使是在低内存的情况下。第二个理由是使用标志可使线程在线程的消息循环结束前完成对所有其他登记消息的处理。例如对下面的代码段, W M _ U S E R消息将先于W M _ Q U I T消息从队列中取出,尽管W M _ U S E R消息是在调用P o s t Q u i t M e s s a g e之后登记到队列中的。

case WM_CLOSE:
   PostQuitMessage(0);
   PostMessage(hwnd, WM_USER, 0, 0);
最后两个消息是W M _ PA I N T和W M _ T I M E R。因为画屏幕是一个慢过程,所以W M _ PA I N T消息的优先级低。如果每当窗口变得无效时就发送一个W M _ PA I N T消息,系统运行就会太慢。在键盘输入之后放置W M _ PA I N T消息,系统会运行得很快。例如,选择一个调用对话框的菜单项,从框中选定一个项,在对话框出现在屏幕上之前一直按E n t e r。如果你的按键速度足够快,按键消息总是先于任何W M _ PA I N T消息从队列中取出。当按E n t e r接受对话框的选项,对话框窗口被清除,系统复位Q S _ PA I N T标志。

最后一个消息W M _ T I M E R,比W M _ PA I N T的优先级还低。为理解这一点,想一想有一个程序用每个W M _ T I M E R消息来更新它的显示画屏。如果定时器消息来的太快,则显示画屏就没有机会重画自己。在W M _ T I M E R消息之前先处理W M _ PA I N T消息,就可以避免这个问题,程序总能更新它的显示画屏。

注意要记住G e t M e s s a g e或P e e k M e s s a g e函数只检查唤醒标志和调用线程的消息队列。这意味着一个线程不能从与其他线程挂接的队列中取得消息,包括同一进程内其他线程的消息。

26.4.3 利用内核对象或队列状态标志唤醒线程

G e t M e s s a g e或P e e k M e s s a g e函数导致一个线程睡眠,直到该线程需要处理一个与用户界面(U I)相关的任务。有时候,若能让线程被唤醒去处理一个与U I有关的任务或其他任务,就会带来许多方便。例如,一个线程可能启动一个长时间运行的操作,并可以让用户取消这个操作。这个线程需要知道何时操作结束(与U I无关的任务),或用户是否按了C a n c e l按钮(与U I相关的任务)来结束操作。

一个线程可以调用M s g Wa i t F o r M u l t i p l e O b j e c t s或M s g Wa i t F o r M u l t i p l e O b j e c t s E x函数,使线程等待它自已的消息。

DWORD MsgWaitForMultipleObjects(
   DWORD nCount,
   PHANDLE phObjects,
   BOOL fWaitAll,
   DWORD dwMilliseconds,
   DWORD dwWakeMask);

DWORD MsgWaitForMultipleObjectsEx(
   DWORD nCount,
   PHANDLE phObjects,
   DWORD dwMilliseconds,
   DWORD dwWakeMask,
   DWORD dwFlags);
这两个函数类似于Wa i t F o r M u l t i p l e O b j e c t s函数(在第9章讨论过)。不同之处是,当一个内核对象变成有信号状态( s i g n a l e d)或当一个窗口消息需要派送到调用线程建立的窗口时,这两个函数用于线程调度。

在内部,系统只是向内核句柄的数组添加一个事件内核对象。d w Wa k e M a s k参数告诉系统何时让事件成为有信号状态。d w Wa k e M a s k参数的可能取值的合法范围与可传递到G e t Q u e u eS t a t u s函数的参数值一样。

正常情况下,当Wa i t F o r M u l t i p l e O b j e c t s函数返回时,它返回变成有信号状态的对象的索引以满足调用(WA I T _ O B J E C T _ O到WA I T _ O B J E C T _ O + n C o u n t-1)。增加d w Wa k e M a s k参数就如同向调用增加又一个句柄。如果由于唤醒掩码, M s g Wa i t F o r M u l t i p l e O b j e c t s ( E x )被满足,返回值将是WA I T _ O B J E C T _ O+n C o u n t。

这里是一个例子,说明如何调用M s g Wa i t F o r _ M u l t i p l e O b j e c t s:

MsgWaitForMultipleObjects(0, NULL, TRUE, INFINITE, QS_INPUT);
这条语句的意思是没有传递任何同步对象的句柄,因为n C o u n t和p h O b j e c t s参数设定了O和N U L L。这里让函数等待所有要变成有信号状态的对象,但只指定了一个要等待的对象,参数f Wa i t A l l可以变成FA L S E,而不会改变这个调用的作用。这里还告诉系统,程序将等待,不论等多长时间,直到有键盘消息或鼠标消息出现在调用线程的输入队列中。

当你要用M s g Wa i t F o r M u l t i p l e O b j e c t s函数做某些事的时候,就会发现这个函数缺少许多重要的特性。因此微软不得不又开发了M s g Wa i t F o r M u l t i p l e O b j e c t s E x函数。M s g Wa i t F o rM u l t i p l e O b j e c t s E x是M s g Wa i t F o r M u l t i p l e O b j e c t s的一个超集( s u p e r s e t)。新的特性是通过d w F l a g s参数引进的。对这个参数,可以指定下面标志的任意组合(见表2 6 - 3)。

表26-3 dwFlags 参数的标志

标志描述
M W M O _ WA I TA L L函数等待所有要变成有信号状态的内核对象及要出现在线程队列中的特定消息。如果没有这个标志,函数等待直到有一个内核对象变成s i g n a l e d,或指定的消息出现在线程的队列中
M W M O _ A L E RTA B L E函数在一个可报警状态等待
M W M O _ I N P U TAVA I L A B L E当任何指定的消息在线程的队列中时,函数醒来(本节后面详细解释)

如果不想要任何这些附加的特性,可对参数d w F l a g s传递零(0)。

下面是有关M s g Wa i t F o r M u l t i p l e O b j e c t s(E x)的一些重要内容:

• 由于这个函数只是向内核句柄的数组增加一个内部事件内核对象, n C o u n t参数的最大值是M A X I M U M _ WA I T _ O B J E C T减1或6 3。

• 当对f Wa i t A l l参数传递FA L S E时,那么当一个内核对象是有信号的( s i g n a l e d),或当指定的消息类型出现在线程的队列时,函数返回。

• 当对f Wa i t A l l参数传递T R U E时,那么当所有内核对象成为有信号状态,并且指定的消息类型出现在线程的队列中时,函数返回。这种行为似乎使许多开发人员感到惊讶。开发人员希望有一种办法,当所有内核对象变成有信号的或者当指定的消息类型出现在线程的队列中时,可以唤醒线程。但没有函数能够这样。

• 当调用这两个函数时,实际是查看是否有指定类型的新消息被放入调用线程的队列。

注意,上述最后一条会使许多开发人员吃惊。这里有一个例子。假定一个线程的队列目前包含有两个按键消息。如果这个线程现在要调用M s g Wa i t F o r M u l t i p l e O b j e c t s(E x),其中d w Wa k e M a s k参数设置成Q S _ I N P U T,线程将被唤醒,从队列中取出第一个按键消息,并处理这个消息。现在,如果这个线程要再调用M s g Wa i t F o r M u l t i p l e O b j e c t s(E x),线程将不会被唤醒,因为线程的队列中没有“新”的消息。

对开发人员来说,这已变成了一个主要问题,为此微软增加了M W M O _ I N P U TAVA I LA B L E标志,这只用于M s g Wa i t F o r M u l t i p l e O b j e c t s E x,而不用于M s g Wa i t F o r M u l t i p l e O b j e c t s。

这里是一个例子,讲述如何适当地编码一个使用M s g Wa i t F o r M u l t i p l e O b j e c t s E x的消息循环:

BOOL  fQuit = FALSE;       // Should the loop terminate?

while(!fQuit) 
{
   //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 our queue.

         //Dispatch all of the messages.
         MSG msg;
         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 while loop

26.5 通过消息发送数据

本节将讨论系统如何利用窗口消息在进程之间传送数据。一些窗口消息在其l P a r a m参数中指出了一个内存块的地址。例如, W M _ S E T T E X T消息使用l P a r a m参数作为指向一个以零结尾的字符串的指针,这个字符串为窗口规定了新的文本标题串。考虑下面的调用:

SendMessage(FindWindow(NULL, "Calculator"), WM_SETTEXT,
   0, (LPARAM) "A Test Caption");
这个调用看起来不会有害。它确定C a l c u l a t o r程序窗口的窗口句柄,并试图将窗口的标题改成“A Test Caption”。但我们要仔细看一看究竟会发生什么。

新标题的字符串包含在调用进程的地址空间里。所以这个在调用进程空间的字符串的地址将传递给l P a r a m参数。当C a l c u l a t o r的窗口的窗口过程收到这个消息时,它要查看l P a r a m参数,并要读取这个以零结尾的字符串,使其成为新的标题。

但l P a r a m中的地址指向调用进程的地址空间中的字符串,而不是C a l c u l a t o r的地址空间。这会发生内存存取违规这种严重问题。但当你执行上面的代码时,你会看到执行是成功的,为什么会是这样?

答案是系统特别要检查W M _ S E T T E X T消息,并用与处理其他消息不同的方法来处理这个消息。当调用S e n d M e s s a g e时,函数中的代码要检查是否要发送一个W M _ S E T T E X T消息。如果是,就将以零结尾的字符串从调用进程的地址空间放入到一个内存映像文件中,该内存映像文件可在进程间共享。然后再发送消息到其他进程的线程。当接收线程已准备好处理W M _ S E T T E X T消息时,它在自己的地址空间中确定包含新的窗口文本标题的共享内存映像文件的位置,再将W M _ S E T T E X T消息派送到相应的窗口过程。在处理完消息之后,内存映像文件被删除。这样做看起来是不是太麻烦了一些。

幸而大多数消息不要求这种类型的处理。仅当这种消息是程序在进程间发送的消息,特别是消息的w P a r a m或l P a r a m参数表示一个指向数据结构的指针时,要做这样的处理。

我们再来看另外一个要求系统特殊处理的例子—— W M _ G E T T E X T消息。假定一个程序包含下面的代码:

char szBuf[200];
SendMessage(FindWindow(NULL, "Calculator"), WM_GETTEXT,
   sizeof(szBuf), (LPARAM) szBuf);
W M _ G E T T E X T消息请求C a l c u l a t o r的窗口过程用该窗口的标题填充s z B u f所指定的缓冲区。当一个进程向另一个进程的窗口发送这个消息时,系统实际上必须发送两个消息。首先,系统要向那个窗口发送一个W M _ G E T T E X T L E N G T H消息。窗口过程通过返回窗口标题的字符数来响应这个消息。系统利用这个数字来建立一个内存映像文件,用于在两个进程之间共享。

当内存映像文件被建立时,系统就发送消息来填充它。然后系统再转回到最初调用S e n d M e s s a g e的进程,从共享内存映像文件中将数据复制到s z B u f所指定的缓冲区中,然后从S e n d M e s s a g e调用返回。

对于系统已经知道的消息,发送消息时都可以按相应的方式来处理。如果你要建立自己的(W M _ U S E R+x)消息,并从一个进程向另一个进程的窗口发送,那又会怎么样?系统不知道你要用内存映像文件并在发送消息时改变指针。为此,微软建立了一个特殊的窗口消息,W M _ C O P Y D ATA以解决这个问题:

COPYDATASTRUCT cds;
SendMessage(hwndReceiver, WM_COPYDATA,
   (WPARAM)hwndSender, (LPARAM) &cds);
C O P Y D ATA S T R U C T是一个结构,定义在Wi n U s e r. h文件中,形式如下面的样子:

typedef struct tagCOPYDATASTRUCT 
{
   ULONG_PTR dwData;
   DWORD cbData;
   PVOID lpData;
} COPYDATASTRUCT;
当一个进程要向另一个进程的窗口发送一些数据时,必须先初始化C O P Y D ATA S T R U C T结构。数据成员d w D a t a是一个备用的数据项,可以存放任何值。例如,你有可能向另外的进程发送不同类型或不同类别的数据。可以用这个数据来指出要发送数据的内容。

c b D a t a数据成员规定了向另外的进程发送的字节数, l p D a t a数据成员指向要发送的第一个字节。l p D a t a所指向的地址,当然在发送进程的地址空间中。

当S e n d M e s s a g e看到要发送一个W M _ C O P Y D ATA消息时,它建立一个内存映像文件,大小是c b D a t a字节,并从发送进程的地址空间中向这个内存映像文件中复制数据。然后再向目的窗口发送消息。在接收消息的窗口过程处理这个消息时, l P a r a m参数指向已在接收进程地址空间的一个C O P Y D ATA S T R U C T结构。这个结构的l p D a t a成员指向接收进程地址空间中的共享内存映像文件的视图。

关于W M _ C O P Y D ATA消息,应该注意三个重要问题:

• 只能发送这个消息,不能登记这个消息。不能登记一个W M _ C O P Y D ATA消息,因为在接收消息的窗口过程处理完消息之后,系统必须释放内存映像文件。如果登记这个消息,系统不知道这个消息何时被处理,所以也不能释放复制的内存块。

• 系统从另外的进程的地址空间中复制数据要花费一些时间。所以不应该让发送程序中运行的其他线程修改这个内存块,直到S e n d M e s s a g e调用返回。

• 利用W M _ C O P Y D ATA消息,可以实现1 6位和3 2位之间的通信。它也能实现3 2位与6 4位之间的通信。这是使新程序同旧程序交流的便捷方法。注意在Windows 2000和Wi n d o w s9 8上完全支持W M _ C O P Y D ATA。但如果你依然在编写1 6位Wi n d o w s程序, M i c r o s o f eVisual C++ 1.52没有W M _ C O P Y D ATA消息的定义,也没有C O P Y D ATA S T R U C T结构的定义。需要手工添加这些代码:

// Manually include this in your 16-bit Windows source code.
#define WM_COPYDATA   0x004A

typedef VOID FAR* PVOID;
typedef struct tagCOPYDATASTRUCT
{
   DWORD dwData;
   DWORD cbData;
   PVOID lpData;
} COPYDATASTRUCT, FAR* PCOPYDATASTRUCT;
在解决进程间的通信问题方面, W M _ C O P Y D ATA消息是一个非常好的工具,可以节省程序员的许多时间。关于使用W M _ C O P Y D ATA消息的一个精采例子,见第2 2章的L a s t M s gB o x I n f o示例程序。

C o p y D a t a示例程序

清单2 6 - 1所列的C o p y D a t a程序(“26 CopyData.exe)说明了如何使用W M _ C O P Y D ATA消息从一个程序向另一个程序发送一个数据块。该程序的源代码和资源文件在本书所附光盘的2 6 - C o p y D a t a目录下。要看它如何工作,至少需要让两个C o p y D a t a的实例运行。每次启动一个C o p y D a t a时,它要显示如图2 6 - 3所示的对话框。


图26-3 CopyData Application 对话框

为了观看从一个程序到另一个程序的数据复制,首先改变D a t a 1和D a t a 2编辑控制框中的文本。然后点击某个Send Data* to Other Wi n d o w s按钮,程序向所有运行的C o p y D a t a的实例发数据。每个实例更新自己的编辑框中的内容来反应新数据。

下面描述C o p y D a t a如何工作。当一个用户点击图2 6 - 3中两个按钮中的某一个时,C o p y D a t a执行下面的动作。

1) 如果用户点击Send Data1 To Other Wi n d o w s按钮,用0来初始化C O P Y D ATA S T R U C T的d w D a t a成员,如果用户点击Send Data2 To Other Wi n d o w s按钮,则用1来初始化d w D a t a成员。

2) 从相应的文本框中求取文本串的长度(按字符数计),并加1(对应一个零结束符)。这个值乘以S i z e o f ( T C H A R ),从字符数转换成字节数。结果存入C O P Y D ATA S T R U C T的c b D a t a成员中。

3) 调用_ a l l o c a分配一个内存块,大小足以容纳编辑框中的字符串加上零结束符。这个块的地址存放在C O P Y D ATA S T R U C T结构的l p D a t a成员中。

4) 从编辑框向分配的内存块复制字符串。

这个时候,一切就绪,准备向其他窗口发送数据。为了确定要向哪个窗口发送W M _ C O P Y D ATA消息,C o p y D a t a调用F i n d Wi n d o w E x函数传递它自己的对话框标题,以便只有其他的C o p y D a t a程序实例才会被枚举。当找到每个实例的窗口时,发送W M _ C O P Y D ATA消息,每个实例更新它的编辑控制框。

清单26-1 CopyData示例程序

/******************************************************************************
Module:  CopyData.cpp
Notices: Copyright (c) 2000 Jeffrey Richter
******************************************************************************/


#include "..\CmnHdr.h"     /* See Appendix A. */
#include <windowsx.h>
#include <tchar.h>
#include <malloc.h>
#include "Resource.h"


///


// WindowsX.h doesn't have a prototype for Cls_OnCopyData, so here it is
/* BOOL Cls_OnCopyData(HWND hwnd, HWND hwndFrom, PCOPYDATASTRUCT pcds) */


///


BOOL Dlg_OnCopyData(HWND hwnd, HWND hwndFrom, PCOPYDATASTRUCT cds) {
   
   Edit_SetText(GetDlgItem(hwnd, cds->dwData ? IDC_DATA2 : IDC_DATA1), 
      (PTSTR) cds->lpData);

   return(TRUE);
}


///


BOOL Dlg_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam) {

   chSETDLGICONS(hwnd, IDI_COPYDATA);

   // Initialize the edit controls with some test data.
   Edit_SetText(GetDlgItem(hwnd, IDC_DATA1), TEXT("Some test data"));
   Edit_SetText(GetDlgItem(hwnd, IDC_DATA2), TEXT("Some more test data"));
   return(TRUE);
}


///


void Dlg_OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) {

   switch (id) {
      case IDCANCEL:
         EndDialog(hwnd, id);
         break;

      case IDC_COPYDATA1:
      case IDC_COPYDATA2:
         if (codeNotify != BN_CLICKED)
            break;

         HWND hwndEdit = GetDlgItem(hwnd, 
            (id == IDC_COPYDATA1) ? IDC_DATA1 : IDC_DATA2);

         // Prepare the COPYDATASTRUCT.
         COPYDATASTRUCT cds;

         // Indicate which data field we're sending (0=ID_DATA1, 1=ID_DATA2)
         cds.dwData = (DWORD) ((id == IDC_COPYDATA1) ? 0 : 1);

         // Get the length (in bytes) of the data block we're sending.
         cds.cbData = (Edit_GetTextLength(hwndEdit) + 1) * sizeof(TCHAR);

         // Allocate a block of memory to hold the string.
         cds.lpData = _alloca(cds.cbData);

         // Put the edit control's string in the data block.
         Edit_GetText(hwndEdit, (PTSTR) cds.lpData, cds.cbData);

         // Get the caption of our window.
         TCHAR szCaption[100];
         GetWindowText(hwnd, szCaption, chDIMOF(szCaption));

         // Enumerate through all the top-level windows with the same caption
         HWND hwndT = NULL;
         do {
            hwndT = FindWindowEx(NULL, hwndT, NULL, szCaption);
            if (hwndT != NULL) {
               FORWARD_WM_COPYDATA(hwndT, hwnd, &cds, SendMessage);
            }
         } while (hwndT != NULL);
         break;
   }
}


///


INT_PTR WINAPI Dlg_Proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
   
   switch (uMsg) {
      chHANDLE_DLGMSG(hwnd, WM_INITDIALOG, Dlg_OnInitDialog);
      chHANDLE_DLGMSG(hwnd, WM_COMMAND,    Dlg_OnCommand);
      chHANDLE_DLGMSG(hwnd, WM_COPYDATA,   Dlg_OnCopyData);
   }
   return(FALSE);
}


///


int WINAPI _tWinMain(HINSTANCE hinstExe, HINSTANCE, PTSTR pszCmdLine, int) {

   DialogBox(hinstExe, MAKEINTRESOURCE(IDD_COPYDATA), NULL, Dlg_Proc);
   return(0);
}


 End of File //
//Microsoft Developer Studio generated resource script.
//
#include "Resource.h"

#define APSTUDIO_READONLY_SYMBOLS
/
//
// Generated from the TEXTINCLUDE 2 resource.
//
#include "afxres.h"

/
#undef APSTUDIO_READONLY_SYMBOLS

/
// English (U.S.) resources

#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
#ifdef _WIN32
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
#pragma code_page(1252)
#endif //_WIN32

/
//
// Dialog
//

IDD_COPYDATA DIALOG DISCARDABLE  38, 36, 220, 42
STYLE WS_MINIMIZEBOX | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "CopyData Application"
FONT 8, "MS Sans Serif"
BEGIN
    LTEXT           "Data&1:",IDC_STATIC,4,4,24,12
    EDITTEXT        IDC_DATA1,28,4,76,12
    PUSHBUTTON      "&Send Data1 to other windows",IDC_COPYDATA1,112,4,104,
                    14,WS_GROUP
    LTEXT           "Data&2:",IDC_STATIC,4,24,24,12
    EDITTEXT        IDC_DATA2,28,24,76,12
    PUSHBUTTON      "Send &Data2 to other windows",IDC_COPYDATA2,112,24,104,
                    14,WS_GROUP
END


/
//
// Icon
//

// Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
IDI_COPYDATA            ICON    DISCARDABLE     "CopyData.Ico"

#ifdef APSTUDIO_INVOKED
/
//
// TEXTINCLUDE
//

1 TEXTINCLUDE DISCARDABLE 
BEGIN
    "Resource.h\0"
END

2 TEXTINCLUDE DISCARDABLE 
BEGIN
    "#include ""afxres.h""\r\n"
    "\0"
END

3 TEXTINCLUDE DISCARDABLE 
BEGIN
    "\r\n"
    "\0"
END

#endif    // APSTUDIO_INVOKED

#endif    // English (U.S.) resources
/



#ifndef APSTUDIO_INVOKED
/
//
// Generated from the TEXTINCLUDE 3 resource.
//


/
#endif    // not APSTUDIO_INVOKED


26.6 Windows如何处理ANSI/Unicode字符和字符串

Windows 98只支持A N S I窗口类和A N S I窗口过程。

当你注册一个新的窗口类时,必须将负责为这个类处理消息的窗口过程的地址告诉系统。对某些消息(如W M _ S E T T E X T),消息的l P a r a m参数指向一个字符串。在此之前,为了派送消息,使它被正确地处理,系统需要知道窗口过程要求该字符串是A N S I字符串还是U n i c o d e字符串。

告诉系统一个窗口过程是要求A N S I字符串还是U n i c o d e字符串,实际上取决于注册窗口类时所使用的函数。如果构造W N D C L A S S结构,并调用R e g i s t e r C l a s s A,系统就认为窗口过程要求所有的字符串和字符都属于A N S I。而用R e g i s t e r C l a s s W注册窗口类,则系统就向窗口过程派送U n i c o d e字符串和字符。宏R e g i s t e r C l a s s对R e g i s t e r C l a s s A和R e g i s t e r C l a s s W都做了扩展,究竟代表哪一个要看在编译源模块时是否定义了U N I C O D E。

如果有了一个窗口句柄,就可以确定窗口过程所要求的字符和字符串类型。这可以通过调用下面的函数实现:

BOOL IsWindowUnicode(HWND hwnd);
如果这个窗口的窗口过程要求U n i c o d e,这个函数返回T R U E,否则返回FA L S E。

如果你建立一个A N S I串,并向一个窗口过程要求U n i c o d e串的窗口发送W M _ S E T T E X T消息,则系统在发送消息之前,为你自动地转换字符串。很少需要调用I s Window Unicode函数。

如果你对窗口过程派生子类,系统也会为你执行自动的转换。假定一个编辑控制框的窗口过程要求字符和字符串是U n i c o d e。在你的程序的某处建立了一个编辑控制框,并建立窗口过程的子类,这可以调用

LONG_PTR SetWindowLongPtrA(
   HWND hwnd,
   int nIndex,
   LONG_PTR dwNewLong);

LONG_PTR SetWindowLongPtrW(
   HWND hwnd,
   int nIndex,
   LONG_PTR dwNewLong);
并将n I n d e x参数设置成G C L P _ W N D P R O C,d w N e w L o n g参数设置成子类过程的地址。如果这个子类过程要求A N S I字符和字符串会出现什么情况?这可能引起严重的问题。系统决定怎样转换字符串和字符,要取决于究竟是用上面两个函数中的哪一个来建立子类。如果是调用S e t Wi n d o w L o n g P t r A,就是告诉系统新的窗口过程(即子类过程)要接收A N S I字符和字符串。实际上,如果在调用S e t Wi n d o w L o n g P t r A之后调用I s Wi n d o w U n i c o d e函数,将返回FA L S E,表示这个子类的编辑窗口过程不再要求U n i c o d e字符和字符串。

但现在又有一个新的问题:如何能够保证原来的窗口过程得到正确的字符和字符串类型?系统需要有两条信息,才能正确地转换字符和字符串。第一条信息就是字符和字符串当前所具有的形式。这可以通过调用C a l l Wi n d o w P r o c A或C a l l Wi n d o w P r o c W来告诉系统:

LRESULT CallWindowProcA(
   WNDPROC wndprcPrev,
   HWND hwnd,
   UINT uMsg,
   WPARAM wParam,
   LPARAM lParam);

LRESULT CallWindowProcW(
   WNDPROC wndprcPrev,
   HWND hwnd,
   UINT uMsg,
   WPARAM wParam,
   LPARAM lParam);
如果子类过程要把A N S I字符串传递给原来的窗口过程,子类过程必须调用C a l l Wi n d o wP r o c A。如果子类过程要把U n i c o d e字符串传递给原来的窗口过程,则子类过程必须调用C a l l Wi n d o w P r o c W。

系统需要的第二条信息是原来的窗口过程所要求的字符和字符串类型。系统从原来窗口过程的地址获取这个信息。当调用S e t Wi n d o w L o n g P t r A或S e t Wi n d o w L o n g P t r W函数时,系统要查看是否使用了一个A N S I子类过程派生了一个U n i c o d e窗口过程,或用一个U n i c o d e子类过程派生了一个A N S I窗口过程。如果没有改变所要求的字符串类型,则S e t Wi n d o w L o n g P t r只返回原先窗口过程的地址。如果改变了窗口过程要求的字符和字符串类型, S e t Wi n d o w L o n g P t r不是返回原先窗口过程的实际地址,而是返回一个内部子系统数据结构的句柄。

这个数据结构包含原先窗口过程的地址及一个数值,用来指示窗口过程是要求A N S I还是要求U n i c o d e字符串。当调用C a l l Wi n d o w P r o c时,系统要查看是传递了某个内部数据结构的地址,还是传递了一个窗口过程的地址。如果传递了一个窗口过程的地址,则调用原先的窗口过程,不需要执行字符和字符串转换。

如果传递了一个内部数据结构的句柄,则系统要将字符和字符串转换成适当的类型(A N S I或U n i c o d e),然后调用原先的窗口过程。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值