窗口消息

本章介绍MicrosoftWindows的消息系统是如何支持带有图形用户界面的应用程序的。在设计Windows2000或Windows98所使用的窗口系统时,微软有两个主要目标:
•尽可能保持与过去16位Windows的兼容性,便于开发人员移植他们已有的16位Windows程序。
•使窗口系统强壮,一个线程不会对系统中其他线程产生不利影响。
但是,这两个目标是直接相互冲突的。在16位Windows系统中,向窗口发送一个消息总是按同步方式执行的:发送程序要在接受消息的窗口完全处理完消息之后才能继续运行。这通常是一个所期望的特性。但是,如果接收消息的窗口花很长的时间来处理消息或者出现挂起,则发送程序就不能再执行。这意味着系统是不强壮的。
这种冲突给微软的设计人员带来了一定的困难。他们的解决方案是两个相互冲突目标之间的出色折衷方案。如果在阅读本章时记住这两个目标,你就会更多地理解微软为什么会做出这样的设计。
26.1线程的消息队列
前面已经说过,Windows的一个主要目标是为程序的运行提供一个强壮的环境。为实现这个目标,要保证每个线程运行在一个环境中,在这个环境中每个线程都相信自己是唯一运行的线程。更确切地说,每个线程必须有完全不受其他线程影响的消息队列。而且,每个线程必须有一个模拟环境,使线程可以维持它自己的键盘焦点(keyboardfocus)、窗口激活、鼠标捕获等概念。
当一个线程第一次被建立时,系统假定线程不会被用于任何与用户相关的任务。这样可以减少线程对系统资源的要求。但是,一旦这个线程调用一个与图形用户界面有关的函数(例如检查它的消息队列或建立一个窗口),系统就会为该线程分配一些另外的资源,以便它能够执行与用户界面有关的任务。特别是,系统分配一个THREADINFO结构,并将这个数据结构与线程联系起来。
这个THREADINFO结构包含一组成员变量,利用这组成员,线程可以认为它是在自己独占的环境中运行。THREADINFO是一个内部的、未公开的数据结构,用来指定线程的登记消息队列(posted-messagequeue)、发送消息队列(send-messagequeue)、应答消息队列(reply-messagequeue)、虚拟输入队列(virtualized-inputqueue)、唤醒标志(wakeflag)、以及用来描述线程局部输入状态的若干变量。图26-1描述了THREADINFO结构和与之相联系的三个线程。
26 26.JPG
图26-1三个线程及相应的THREADINFO结构
这个THREADINFO结构是窗口消息系统的基础,在阅读下面各节内容时,应该参考该图。
26.2将消息发送到线程的消息队列中
当线程有了与之相联系的THREADINFO结构时,线程就有了自己的消息队列集合。如果一个进程建立了三个线程,并且所有这些线程都调用CreateWindow,则将有三个消息队列集合。消息被放置在线程的登记消息队列中,这要通过调用PostMessage函数来完成:
BOOL PostMessage(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam);
当一个线程调用这个函数时,系统要确定是哪一个线程建立了用hwnd参数标识的窗口。然后系统分配一块内存,将这个消息参数存储在这块内存中,并将这块内存增加到相应线程的登记消息队列中。并且,这个函数还设置QS_POSTMESSAGE唤醒位(后面会简单讨论)。函数PostMessage在登记了消息之后立即返回,调用该函数的线程不知道登记的消息是否被指定窗口的窗口过程所处理。实际上,有可能这个指定的窗口永远不会收到登记的消息。如果建立这个特定窗口的线程在处理完它的消息队列中的所有消息之前就结束了,就会发生这种事。
还可以通过调用PostThreadMessage将消息放置在线程的登记消息队列中。
BOOL PostThreadMessage(
DWORD,dwThreadId,
UINT uMsg,
WPARAM wParam,
LPARAM lParam);
注意可以通过调用GetWindowsThreadProcessId来确定是哪个线程建立了一个窗口。
DWORD GetWindowsThreadProcessId(
  HWND hwnd,
  PDWORD pdwProcessid);
这个函数返回线程的ID,这个线程建立了hwnd参数所标识的窗口。线程ID在全系统范围内是唯一的。还可以通过对pdwProcessId参数传递一个DWORD地址来获取拥有该线程的进程ID,这个进程ID在全系统范围内也是唯一的。通常,我们不需要进程ID,只须对这个参数传递一个NULL。
PostThreadMessage函数所期望的线程由第一个参数dwThreadId所标记。当消息被设置到队列中时,MSG结构的hwnd成员将设置成NULL。当程序要在主消息循环中执行一些特殊处理时要调用这个函数。
要对线程编写主消息循环以便在GetMessage或PeekMessage取出一个消息时,主消息循环代码检查hwnd是否为NULL,并检查MSG结构的msg成员来执行特殊的处理。如果线程确定了该消息不被指派给一个窗口,则不调用DispatchMessage,消息循环继续取下一个消息。像PostMessage函数一样,PostThreadMessage在向线程的队列登记了消息之后就立即返回。调用该函数的线程不知道消息是否被处理。
向线程的队列发送消息的函数还有PostQuitMessage:
VOID PostQuitMessage(int nExitCode);
为了终止线程的消息循环,可以调用这个函数。调用PostQuitMessage类似于调用:
PostThreadMessage(GetCurrentThreadId(),WM_QUIT,nExitCode,0);
但是,PostQuitMessage并不实际登记一个消息到任何一个THREADINFO结构的队列。只是在内部,PostQuitMessage设定QS_QUIT唤醒标志(后面将要讨论),并设置THREADINFO结构的nExitCode成员。因为这些操作永远不会失败,所以PostQuitMessage的原型被定义成返回VOID。
26.3向窗口发送消息
使用SendMessage函数可以将窗口消息直接发送给一个窗口过程:
LRESULT SendMessage(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam);
窗口过程将处理这个消息。只有当消息被处理之后,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:利用SendMessageTimeout函数,可以规定等待其他线程答回你消息的时间最大值。
用来在线程间发送消息的第二个函数是SendMessageCallback:当一个线程调用SendMessageCallback时,该函数发送消息到接收线程的发送消息队列,并立即返回使发送线程可以继续执行。
线程间发送消息的第三个函数是SendNotifyMessage:SendNotifyMessage将一个消息置于接收线程的发送消息队列中,并立即返回到调用线程。
第四个用于线程发送消息的函数是ReplyMessage:这个函数与前面讨论过的三个函数不同。线程使用SendMessageTimeout、SendMessageCallback和SendNotifyMessage发送消息,是为了保护自己以免被挂起。而线程调用ReplyMessage是为了接收窗口消息。当一个线程调用ReplyMessage时,它是要告诉系统,为了知道消息结果,它已经完成了足够的工作,结果应该包装起来并登记到发送线程的应答消息队列中。这将使发送线程醒来,获得结果,并继续执行。
26.4唤醒一个线程
当一个线程调用GetMessage或WaitMessage,但没有对这个线程或这个线程所建立窗口的消息时,系统可以挂起这个线程,这样就不再给它分配CPU时间。当有一个消息登记或发送到这个线程,系统要设置一个唤醒标志,指出现在要给这个线程分配CPU时间,以便处理消息。正常情况下,如果用户不按键或移动鼠标,就没有消息发送给任何窗口。这意味着系统中大多数线程没有被分配给CPU时间。
26.4.1队列状态标志
当一个线程正在运行时,它可以通过调用GetQueueStatus函数来查询队列的状态:
DWORD GetQueueStatus(UINT fuFlags);
参数fuFlags是一个标志或一组由OR连接起来的标志,可用来测试特定的唤醒位。表26-2给出了各个标志取值及含义。
表26-2标志取值及含义
标志                队列中的消息
QS_KEY         WM_KEYUP、WM_KEYDOWN、WM_SYSKEYUP或WM_SYSKEYDOWN
QS_MOUSEMOVE      WM_MOUSEMOVE
QS_MOUSEBUTTON    WM_?BUTTON*(其中?代表L、M或R、*代表DOWN、UP或DBLCLK)
QS_MOUSE  同QS_MOUSEMOVE|QS_MOUSEBUTTON
QS_INPUT  同QS_MOUSE|QS_KEY
QS_PAINT  WM_PAINT
QS_TIMER  WM_TIMER
QS_HOTKEY WM_HOTKEY
QS_POSTMESSAGE  登记的消息(不同于硬件输入事件)。当队列在期望的消息过滤器范围内没有登记的消息时,这个标志要消除。除此之外,这个标志与QS_ALLPOSTMESSAGE相同
QS_ALLPOSTMESSAGE 登记的消息(不同于硬件输入事件)。当队列完全没有登记的消息时(在任何消息过滤器范围),该标志被清除。除此之外,该标志与QS_POSTMESSAGE相同
QS_ALLEVENTS  同QS_INPUT|QS_POSTMESSAGE|QS_TIMER|QS_PAINT|QS_HOTKEY
QS_QUIT  已调用PostQuitMessage。注意这个标志没有公开,所以在WinUser.h文件中没有。它由系统在内部使用
QS_SENDMESSAGE 由另一个线程发送的消息
QS_ALLINPUT  同QS_ALLEVENTS|QS_SENDMESSAGE
当调用GetQueueStatus函数时,fuFlags将队列中要检查的消息的类型告诉GetQueueStatus。
用OR连接的QS_*标识符的数量越少,调用执行的就越快。当GetQueueStatus返回时,线程的队列中当前消息的类型在返回值的高字(两字节)中。这个返回的标志的集合总是所想要的标志集的子集。
不是所有的唤醒标志都由系统平等对待。对于QS_MOUSEMOVE标志,只要队列中存在一个未处理的WM_MOUSEMOVE消息,这个标志就要被设置。当GetMessage或PeekMessage(利用PM_REMOVE)从队列中放入新的WM_MOUSEMOVE消息之前,这个标志被关闭。QS_KEY、QS_MOUSEBUTTON和QS_HOTKEY标志都根据相应的消息按与此相同的方式处理。
QS_PAINT标志的处理与此不同。如果线程建立的一个窗口有无效的区域,QS_PAINT标志被设置。当这个线程建立的所有窗口所占据的区域变成无效时(通常由于对ValidateRect、ValidateRegion或BeginPaint的调用而引起),QS_PAINT标志就被关闭。只有当线程建立的所有窗口都无效时,这个标志才关闭。调用GetMessage或PeekMessage对这个唤醒标志没有影响。
当线程的登记消息队列中至少有一个消息时,QS_POSTMESSAGE标志就被设置。这不包括线程的虚拟输入队列中的硬件事件消息。当线程的登记消息队列中的所有消息都已经处理,队列变空时,这个标志被复位。
每当一个定时器(由线程所建立)报时(gooff),QS_TIMER标志就被设置。在GetMessage或PeekMessage返回WM_TIMER事件之后,QS_TIMER标志被复位,直到定时器再次报时。QS_SENDMESSAGE标志指出有一个消息在线程的发送消息队中。系统在内部使用这个标志,用来确认和处理线程之间发送的消息。对于一个线程向自身发送的消息,不设置这个标志。虽然可以使用QS_SENDMESSAGE标志,但很少需要这样做。笔者还从未见到一个程序使用这个标志。
还有一个未公开的队列状态标志QS_QUIT。当一个线程调用PostQuitMessage时,QS_QUIT标志就被设置。系统并不实际向线程的消息队列追加一个WM_QUIT消息。GetQueueStatus函数也不返回这个标志的状态。
26.4.2从线程的队列中提取消息的算法
当一个线程调用GetMessage或PeekMessage时,系统必须检查线程的队列状态标志的情况,并确定应该处理哪个消息。下面叙述的步骤说明了系统是如何确定线程应该处理的下一个消息的情况。
1)如果QS_SENDMESSAGE标志被设置,系统向相应的窗口过程发送消息。GetMessage或PeekMessage函数在内部进行这种处理,并且在窗口过程处理完消息之后不返回到线程,这些函数要等待其他要处理的消息。
2)如果消息在线程的登记消息队列中,函数GetMessage或PeekMessage填充传递给它们的MSG结构,然后函数返回。这时,线程的消息循环通常调用DispatchMessage,让相应的窗口过程来处理消息。
3)如果QS_QUIT标志被设置。GetMessage或PeekMessage返回一个WM_QUIT消息(其中wParam参数是规定的退出代码)并复位QS_QUIT标志。
4)如果消息在线程的虚拟输入队列,函数GetMessage或PeekMessage返回硬件输入消息。
5)如果QS_PAINT标志被设置,GetMessage或PeekMessage为相应的窗口返回一个WM-PAINT消息。
6)如果QS_TIMER标志被设置,GetMessage或PeekMessage返回一个WM_TIMER消息。
尽管很难令人相信,但确有理由这样做。微软在设计这个算法时有一个大前提,就是应用
程序应该是用户驱动的,用户通过建立硬件输入事件(键盘和鼠标操作)来驱动应用程序。在使用应用程序时,用户可能按一个鼠标按钮,引起一系列要发生的事件。应用程序通过向线程的消息队列中登记消息使每个个别的事件发生。
所以如果按鼠标按钮,处理WM_LBUTTONDOWN消息的窗口可能向不同的窗口投送三个消息。由于是硬件事件引发三个软件事件,所以系统要在读取用户的下一个硬件事件之前,先处理这些软件事件。这也说明了为什么登记消息队列要在虚拟输入队列之前检查。
注意要记住GetMessage或PeekMessage函数只检查唤醒标志和调用线程的消息队列。这意味着一个线程不能从与其他线程挂接的队列中取得消息,包括同一进程内其他线程的消息。
26.4.3利用内核对象或队列状态标志唤醒线程
GetMessage或PeekMessage函数导致一个线程睡眠,直到该线程需要处理一个与用户界面(UI)相关的任务。有时候,若能让线程被唤醒去处理一个与UI有关的任务或其他任务,就会带来许多方便。例如,一个线程可能启动一个长时间运行的操作,并可以让用户取消这个操作。
这个线程需要知道何时操作结束(与UI无关的任务),或用户是否按了Cancel按钮(与UI相关的任务)来结束操作。
一个线程可以调用MsgWaitForMultipleObjects或MsgWaitForMultipleObjectsEx函数,使线程等待它自已的消息。
这两个函数类似于WaitForMultipleObjects函数(在第9章讨论过)。不同之处是,当一个内核对象变成有信号状态(signaled)或当一个窗口消息需要派送到调用线程建立的窗口时,这两个函数用于线程调度。
26.5通过消息发送数据
本节将讨论系统如何利用窗口消息在进程之间传送数据。一些窗口消息在其lParam参数中指出了一个内存块的地址。例如,WM_SETTEXT消息使用lParam参数作为指向一个以零结尾的字符串的指针,这个字符串为窗口规定了新的文本标题串。考虑下面的调用:
SendMessage(FindWindow(NULL,”Calculator”),WM_SETTEXT,0,(LPARAM)”A Test Caption”);
这个调用看起来不会有害。它确定Calculator程序窗口的窗口句柄,并试图将窗口的标题改成“ATestCaption”。但我们要仔细看一看究竟会发生什么。
新标题的字符串包含在调用进程的地址空间里。所以这个在调用进程空间的字符串的地址将传递给lParam参数。当Calculator的窗口的窗口过程收到这个消息时,它要查看lParam参数,并要读取这个以零结尾的字符串,使其成为新的标题。
但lParam中的地址指向调用进程的地址空间中的字符串,而不是Calculator的地址空间。这会发生内存存取违规这种严重问题。但当你执行上面的代码时,你会看到执行是成功的,为什么会是这样?
答案是系统特别要检查WM_SETTEXT消息,并用与处理其他消息不同的方法来处理这个消息。当调用SendMessage时,函数中的代码要检查是否要发送一个WM_SETTEXT消息。如果是,就将以零结尾的字符串从调用进程的地址空间放入到一个内存映像文件中,该内存映像文件可在进程间共享。然后再发送消息到其他进程的线程。当接收线程已准备好处理WM_SETTEXT消息时,它在自己的地址空间中确定包含新的窗口文本标题的共享内存映像文件的位置,再将WM_SETTEXT消息派送到相应的窗口过程。在处理完消息之后,内存映像文件被删除。这样做看起来是不是太麻烦了一些。
幸而大多数消息不要求这种类型的处理。仅当这种消息是程序在进程间发送的消息,特别是消息的wParam或lParam参数表示一个指向数据结构的指针时,要做这样的处理。
我们再来看另外一个要求系统特殊处理的例子——WM_GETTEXT消息。假定一个程序包含下面的代码:
Char szBuf[200];
sendMessage(findWindow(NULL,”calculator”),WM_GETTEXT,sizeof(szBuf),(lparam)szBuf);
WM_GETTEXT消息请求Calculator的窗口过程用该窗口的标题填充szBuf所指定的缓冲区。当一个进程向另一个进程的窗口发送这个消息时,系统实际上必须发送两个消息。首先,系统要向那个窗口发送一个WM_GETTEXTLENGTH消息。窗口过程通过返回窗口标题的字符数来响应这个消息。系统利用这个数字来建立一个内存映像文件,用于在两个进程之间共享。
当内存映像文件被建立时,系统就发送消息来填充它。然后系统再转回到最初调用SendMessage的进程,从共享内存映像文件中将数据复制到szBuf所指定的缓冲区中,然后从SendMessage调用返回。
对于系统已经知道的消息,发送消息时都可以按相应的方式来处理。如果你要建立自己的(WM_USER+x)消息,并从一个进程向另一个进程的窗口发送,那又会怎么样?系统不知道你要用内存映像文件并在发送消息时改变指针。为此,微软建立了一个特殊的窗口消息,WM_COPYDATA以解决这个问题:
COPYDATASTRUCT cds;
sendMessage(hwndReceiver,WM_COPYDATA,
   (WPARAM)hwndSender,(LPARAM)&cds);
COPYDATASTRUCT是一个结构,定义在WinUser.h文件中,形式如下面的样子:
Typedef struct tagCOPYDATASTRUCT{
ULONG_PTR dwData;
DWORD cbData;
PVOID lpData;
}COPYDATASTRUCT;
当一个进程要向另一个进程的窗口发送一些数据时,必须先初始化COPYDATASTRUCT结构。数据成员dwData是一个备用的数据项,可以存放任何值。例如,你有可能向另外的进程发送不同类型或不同类别的数据。可以用这个数据来指出要发送数据的内容。
cbData数据成员规定了向另外的进程发送的字节数,lpData数据成员指向要发送的第一个字节。lpData所指向的地址,当然在发送进程的地址空间中。
当SendMessage看到要发送一个WM_COPYDATA消息时,它建立一个内存映像文件,大小是cbData字节,并从发送进程的地址空间中向这个内存映像文件中复制数据。然后再向目的窗口发送消息。在接收消息的窗口过程处理这个消息时,lParam参数指向已在接收进程地址空间的一个COPYDATASTRUCT结构。这个结构的lpData成员指向接收进程地址空间中的共享内存映像文件的视图。
关于WM_COPYDATA消息,应该注意三个重要问题:
•只能发送这个消息,不能登记这个消息。不能登记一个WM_COPYDATA消息,因为在接收消息的窗口过程处理完消息之后,系统必须释放内存映像文件。如果登记这个消息,系统不知道这个消息何时被处理,所以也不能释放复制的内存块。
•系统从另外的进程的地址空间中复制数据要花费一些时间。所以不应该让发送程序中运
行的其他线程修改这个内存块,直到SendMessage调用返回。
•利用WM_COPYDATA消息,可以实现16位和32位之间的通信。它也能实现32位与64位之间的通信。这是使新程序同旧程序交流的便捷方法。注意在Windows2000和Windows98上完全支持WM_COPYDATA。但如果你依然在编写16位Windows程序,Microsofe VisualC++1.52没有WM_COPYDATA消息的定义,也没有COPYDATASTRUCT结构的定义。需要手工添加些代码。
26.6Windows如何处理ANSI/Unicode字符和字符串
Windows98Windows98只支持ANSI窗口类和ANSI窗口过程。
当你注册一个新的窗口类时,必须将负责为这个类处理消息的窗口过程的地址告诉系统。
对某些消息(如WM_SETTEXT),消息的lParam参数指向一个字符串。在此之前,为了派送消息,使它被正确地处理,系统需要知道窗口过程要求该字符串是ANSI字符串还是Unicode字符串。
告诉系统一个窗口过程是要求ANSI字符串还是Unicode字符串,实际上取决于注册窗口类时所使用的函数。如果构造WNDCLASS结构,并调用RegisterClassA,系统就认为窗口过程要求所有的字符串和字符都属于ANSI。而用RegisterClassW注册窗口类,则系统就向窗口过程派送Unicode字符串和字符。宏RegisterClass对RegisterClassA和RegisterClassW都做了扩展,究竟代表哪一个要看在编译源模块时是否定义了UNICODE。
如果有了一个窗口句柄,就可以确定窗口过程所要求的字符和字符串类型。这可以通过调用下面的函数实现:
BOOL IswindowUnicode(HWND hwnd);
如果这个窗口的窗口过程要求Unicode,这个函数返回TRUE,否则返回FALSE。
如果你建立一个ANSI串,并向一个窗口过程要求Unicode串的窗口发送WM_SETTEXT消息,则系统在发送消息之前,为你自动地转换字符串。很少需要调用IsWindowUnicode函数。
如果你对窗口过程派生子类,系统也会为你执行自动的转换。假定一个编辑控制框的窗口过程要求字符和字符串是Unicode。在你的程序的某处建立了一个编辑控制框,并建立窗口过程的子类,这可以调用
LONG_PTR SetWindowLongPtrA(
HWND hwnd,
Int nIndex,
LONG_PTR dwNewLong);

LONG_PTR SetWindowLongPtrW(
HWND hwnd,
Int nIndex,
LONG_PTR dwNewLong);
并将nIndex参数设置成GCLP_WNDPROC,dwNewLong参数设置成子类过程的地址。如果这个子类过程要求ANSI字符和字符串会出现什么情况?这可能引起严重的问题。系统决定怎样转换字符串和字符,要取决于究竟是用上面两个函数中的哪一个来建立子类。如果是调用SetWindowLongPtrA,就是告诉系统新的窗口过程(即子类过程)要接收ANSI字符和字符串。
实际上,如果在调用SetWindowLongPtrA之后调用IsWindowUnicode函数,将返回FALSE,表
示这个子类的编辑窗口过程不再要求Unicode字符和字符串。
但现在又有一个新的问题:如何能够保证原来的窗口过程得到正确的字符和字符串类型?系统需要有两条信息,才能正确地转换字符和字符串。第一条信息就是字符和字符串当前所具有的形式。这可以通过调用CallWindowProcA或CallWindowProcW来告诉系统:
LRESULT CallWindowProcA(
WNDPROC wndprcPrev,
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam);

LRESULT CallWindowProcW(
WNDPROC wndprcPrev,
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam);
如果子类过程要把ANSI字符串传递给原来的窗口过程,子类过程必须调用CallWindowProcA。如果子类过程要把Unicode字符串传递给原来的窗口过程,则子类过程必须调用CallWindowProcW。
系统需要的第二条信息是原来的窗口过程所要求的字符和字符串类型。系统从原来窗口过程的地址获取这个信息。当调用SetWindowLongPtrA或SetWindowLongPtrW函数时,系统要查看是否使用了一个ANSI子类过程派生了一个Unicode窗口过程,或用一个Unicode子类过程派生了一个ANSI窗口过程。如果没有改变所要求的字符串类型,则SetWindowLongPtr只返回原先窗口过程的地址。如果改变了窗口过程要求的字符和字符串类型,SetWindowLongPtr不是返回原先窗口过程的实际地址,而是返回一个内部子系统数据结构的句柄。
这个数据结构包含原先窗口过程的地址及一个数值,用来指示窗口过程是要求ANSI还是要求Unicode字符串。当调用CallWindowProc时,系统要查看是传递了某个内部数据结构的地址,还是传递了一个窗口过程的地址。如果传递了一个窗口过程的地址,则调用原先的窗口过程,不需要执行字符和字符串转换。
如果传递了一个内部数据结构的句柄,则系统要将字符和字符串转换成适当的类型(ANSI或Unicode),然后调用原先的窗口过程。


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值