关于窗口进程
每个窗口都是特定窗口类的成员。 窗口类确定了单个窗口用于处理其消息的默认窗口过程。 属于同一类的所有窗口都使用相同的默认窗口过程。 例如,系统为组合框类 (COMBOBOX) 定义一个窗口过程;随后,所有组合框都使用该窗口过程。
应用程序通常至少注册一个新的窗口类及其关联的窗口过程。 注册类后,应用程序可以创建该类的许多窗口,所有这些窗口都使用相同的窗口过程。 由于这意味着多个源可以同时调用同一段代码,因此在从窗口过程中修改共享资源时必须小心。 有关详细信息,请参阅窗口类。
对话框的窗口过程(称为对话框过程)的结构和功能与常规窗口过程类似。 本部分中引用窗口过程的所有点也适用于对话框过程。 有关详细信息,请参阅对话框。
本部分讨论了以下主题。
窗口过程的结构
窗口过程是一个具有 4 个参数并返回有符号值的函数。 这些参数由一个窗口句柄、一个 UINT 消息标识符,以及两个用 WPARAM 和 LPARAM 数据类型声明的消息参数组成。 有关详细信息,请参阅 WindowProc。
消息参数通常包含其低序字和高序字中的信息。 应用程序可以使用多个宏从消息参数中提取信息。 例如,LOWORD 宏从消息参数中提取低序字(位 0 到 15)。 其他宏包括 HIWORD、LOBYTE 和 HIBYTE 宏。
返回值的解释由特定消息决定。 请查看每条消息的说明来确定相应的返回值。
由于可以递归方式调用窗口过程,因此必须尽量减少其使用的局部变量数。 处理单个消息时,应用程序应在窗口过程外部调用函数,以避免过度使用局部变量,过度使用可能会导致堆栈在深度递归期间溢出。
默认窗口过程
默认窗口过程函数 DefWindowProc 定义所有窗口共享的某些基本行为。 默认窗口过程为窗口提供最少的功能。 应用程序定义的窗口过程应将它未处理的任何消息都传递给 DefWindowProc 函数进行默认处理。
窗口过程子类化
当应用程序创建窗口时,系统会分配一个内存块用于存储特定于窗口的信息(包括处理窗口消息的窗口过程的地址)。 当系统需要将消息传递到窗口时,它会搜索窗口特定的信息来获知窗口过程的地址,并将消息传递给该过程。
子类化是一种技术,使应用程序能够截获和处理在窗口有机会处理消息之前发送或发布到特定窗口的消息。 通过将窗口子类化,应用程序可以增强、修改或监视窗口的行为。 应用程序可将属于系统全局类的窗口(例如编辑控件或列表框)子类化。 例如,应用程序可以将编辑控件子类化,以防止控件接受某些字符。 但是,不能将属于其他应用程序的窗口或类进行子类化。 所有子类化都必须在同一进程中执行。
应用程序通过将窗口的原始窗口过程的地址替换为新窗口过程(称为“子类过程”)的地址来将窗口子类化。 此后,子类过程接收发送或发布到窗口的任何消息。
子类过程可在收到消息时执行 3 个操作:它可以将消息传递到原始窗口过程,修改消息并将其传递给原始窗口过程,或者处理消息,但不将其传递给原始窗口过程。 如果子类过程处理消息,则它可以在将消息传递到原始窗口过程之前和/或之后执行该操作。
系统提供两种类型的子类化:实例和全局。 在“实例子类化”中,应用程序会替换窗口的单个实例的窗口过程地址。 应用程序必须使用实例子类化来对现有窗口进行子类化。 在全局子类化操作中,应用程序会在窗口类的 WNDCLASSEX 结构中替换窗口过程的地址。 使用类创建的所有后续窗口都具有子类过程的地址,但该类的现有窗口不受影响。
实例子类化
应用程序使用 SetWindowLongPtr 函数对窗口的实例进行子类化。 应用程序会将 GWL_WNDPROC 标志和窗口的句柄传递给子类,并将子类过程的地址传递给 SetWindowLongPtr。 子类过程可以驻留在应用程序的可执行文件或 DLL 中。
传递 GWL_WNDPROC 标志时,SetWindowLongPtr 会返回此窗口的原始窗口过程的地址。 应用程序必须保存此地址,并在针对 CallWindowProc 函数的后续调用中使用它,以便将截获的消息传递给原始窗口过程。 应用程序还必须具有原始窗口过程地址,才能从窗口中删除子类。 若要删除子类,应用程序需再次调用 SetWindowLongPtr,并将附带 GWL_WNDPROC 标志和句柄的原始窗口过程的地址传递给此窗口。
系统拥有系统全局类,在系统的不同版本之间,控件的各个方面可能有所不同。 如果应用程序必须将属于系统全局类的窗口子类化,那么在新版本的系统发布时,开发人员可能需要更新应用程序。
由于创建窗口后发生实例子类化,因此无法向窗口添加任何额外的字节。 进行窗口子类化的应用程序应使用窗口的属性列表来存储已子类化的窗口实例所需的任何数据。 有关详细信息,请参阅窗口属性。
当应用程序将已子类化的窗口进行子类化时,它必须按执行子类的相反顺序删除这些子类。 如果未按相反顺序删除,则可能会发生不可恢复的系统错误。
全局子类化
若要将窗口类进行全局子类化,应用程序必须具有类窗口的句柄。 应用程序还需要该句柄才能删除子类。 为了获取句柄,应用程序通常会创建要子类化的类的隐藏窗口。 获取句柄后,应用程序需调用 SetClassLongPtr 函数,从而指定句柄、GCL_WNDPROC 标志和子类过程的地址。 SetClassLongPtr 会返回该类的原始窗口过程的地址。
原始窗口过程地址在全局子类化中按与在实例子类化中相同的方式使用。 子类过程通过调用 CallWindowProc 将消息传递到原始窗口过程。 应用程序可通过再次调用 SetClassLongPtr 从窗口类中删除子类,从而指定原始窗口过程的地址、GCL_WNDPROC 标志以及要子类化的类的窗口的句柄。 将控件类全局子类化的应用程序必须在应用程序终止时删除子类;否则,可能会出现不可恢复的系统错误。
全局子类化具有与实例子类化相同的限制,并且还有其他一些限制。 应用程序在不确切知道原始窗口过程如何使用额外字节的情况下,不得对类或窗口实例使用这些额外的字节。 如果应用程序必须将数据与窗口相关联,则应使用窗口属性。
窗口过程超类化
超类化是一种技术,使应用程序能够使用现有类的基本功能以及应用程序提供的增强功能创建新的窗口类。 超类基于名为基类的现有窗口类。 通常,基类是一个系统全局窗口类(例如编辑控件),但它可以是任何窗口类。
超类有自己的窗口过程,称为超类过程。 超类过程可在收到消息时执行 3 个操作:它可以将消息传递到原始窗口过程,修改消息并将其传递给原始窗口过程,或者处理消息,但不将其传递给原始窗口过程。 如果超类过程处理消息,则它可以在将消息传递到原始窗口过程之前和/或之后执行该操作。
与子类过程不同,超类过程可以处理窗口创建消息(WM_NCCREATE、WM_CREATE 等),但它还必须将这些消息传递给原始基类窗口过程,使基类窗口过程可以执行其初始化过程。
若要对窗口类进行超类化,应用程序需首先调用 GetClassInfoEx 函数来检索有关基类的信息。 GetClassInfoEx 会使用基类的 WNDCLASSEX 结构中的值来填充 WNDCLASSEX 结构。 接着,应用程序会将自己的实例句柄复制到 WNDCLASSEX 结构的 hInstance 成员中,并将超类的名称复制到 lpszClassName 成员中。 如果基类具有菜单,应用程序必须提供具有相同菜单标识符的新菜单,并将菜单名称复制到 lpszMenuName 成员中。 如果超类过程处理 WM_COMMAND 消息,并且不将其传递给基类的窗口过程,则菜单不需要相应的标识符。 GetClassInfoEx 不会返回 WNDCLASSEX 结构的 lpszMenuName、lpszClassName 或 hInstance 成员。
应用程序还须设置 WNDCLASSEX 结构的 lpfnWndProc 成员。 GetClassInfoEx 函数会使用该类的原始窗口过程的地址来填充此成员。 应用程序必须保存此地址才能将消息传递到原始窗口过程,然后将超类过程的地址复制到 lpfnWndProc 成员中。 如有必要,应用程序可修改 WNDCLASSEX 结构的任意其他成员。 填充 WNDCLASSEX 结构后,应用程序会通过将此结构的地址传递给 RegisterClassEx 函数来注册超类。 然后,可以使用超类来创建窗口。
由于超类化注册了一个新的窗口类,因此应用程序可以同时添加到额外的类字节和额外的窗口字节。 超类不得对基类或窗口使用原始额外字节,原因与实例子类或全局子类不应使用这些字节相同。 此外,如果应用程序为类或窗口实例添加额外的字节,则必须引用相对于原始基类使用的额外字节数的额外字节。 由于基类使用的字节数可能因基类的具体版本而异,因此超类自身额外字节的起始偏移量也可能因基类的不同版本而异。