关于子类化的话题虽然有些旧,但它至今仍然不失为一种开发Windows的强有力技术,在MFC的内核、甚至.NET的内核中都离不开它,希望本连载能对Windows开发的爱好者有所帮助。

原文标题:Safe Subclassing in Win32
作者:Kyle Marsh
MSDN技术组

点击此处查看原文

摘要

本文描述了Win32环境下的子类化,描述了它是如何工作的以及实现安全的子类化必须要遵循的规则。本文涵盖了实例子类化和全局子类化。而超类化则作为一个全局子类化的可选替代方案被介绍。
从Win16到Win32,子类化并没有发生特别显著的变化,但是,在Win32中,一个应用程序还是要遵守几个新的子类化规则。其中最重要(也是最明显的)就是一个应用程序不能子类化属于另一个进程的窗口或者类,除非有工作区提供给应用程序使用,否则这条规则不能被打破。

子类化的定义

子类化是这样一种技术,它允许一个应用程序截获发往另一个窗口的消息。一个应用程序通过截获属于另一个窗口的消息,从而实现增加、监视或者修改那个窗口的缺省行为。子类化是用来改变或者扩展一个已存在的窗口的行为、而不用重新开发的有效途径。想要获得那些预定义控件窗口类(按钮控件、编辑控件、列表控件、下拉列表控件、静态控件和滚动条控件)的功能而又要修改它们的某些行为的一个便利的方法就是对它们进行子类化。例如,对于一个在对话框中的多行编辑框来说,当用户按下Enter键时,对话框会关闭。通过对编辑控件子类化,一个应用程序就能拥有一个可以往文本中插入回车和换行,而同时又不会关闭对话框的编辑控件,应用程序不用为这个特殊的需要而去专门开发一个编辑控件。

子类化基础

创建一个窗口的第一步是填充一个WNDCLASS结构,并调用RegisterClass函数来注册一个窗口类。WNDCLASS结构的其中一个成员是这个窗口类的窗口过程地址,当一个窗口被建立,32位版本的Microsoft Windows操作系统会取出WNDCLASS结构中的窗口过程地址,并把它复制到一个新的窗口信息结构中。当一条消息被发往这个窗口时,Windows通过窗口信息结构中的这个地址调用这个窗口的窗口过程。为了子类化一个窗口,你要用一个新的窗口过程地址取代原窗口过程地址,从而使新的窗口过程可以接收发往原窗口过程的所有消息。
当一个应用程序子类化一个窗口时,它可对消息采取三种操作:(1)把消息传递给原窗口过程;(2)修改消息然后再传递给原窗口过程;(3)不再往下传递消息。
应用程序子类化一个窗口时,可以决定什么情况下对所接收的消息做出反应,应用程序可以在将消息传递给原窗口过程之前或(和)之后处理这条消息。

子类化的类型

有两种子类化的类型,它们是实例子类化全局子类化。.

实例子类化是子类化一个独立的窗口信息结构,在实例子类化后,只有属于一个特定的窗口实例的消息会被发送到新窗口过程。
全局子类化是替换一个窗口类的WNDCLASS结构中的窗口过程地址,所有在这之后使用该窗口类建立起来的窗口都具有这个被替换的窗口过程地址。全局子类化只对那些在子类化生效之后创建的窗口有效,在进行子类化之前,如果已经存在任何用这个被全局子类化的窗口类创建的窗口,这些已经存在的窗口不会被子类化。如果应用程序想要使子类化对这些已经存在的窗口生效,应用程序必须子类化每一个已经存在的该窗口类的实例。

有两条规则应用到Win32下的实例子类化和全局子类化。

子类化仅被允许用在进程内,一个应用程序不能子类化属于另一个进程的窗口或窗口类。

这条规则的起因很简单:Win32进程具有独立的进程地址空间。在一个特定的进程里,一个窗口过程有一个地址,而在另一个不同的进程里,这个地址值并未指向这个窗口过程,结果就是,在一个进程中,使用从另一个进程获得的地址替换后的地址并不能获得期望的结果,因此32位的Windows不允许这种地址替换发生。SetWindowLong和SetClassLong函数中防止了这种类型的子类化发生。你不能子类化属于另一个进程的窗口或窗口类,你能做的就到此为止。

不过,也还是有些途径能让你把子类化的功能用到每一个进程上。只要能得到位于某个进程地址空间里的某个函数,你就能该进程里的任何东西进行子类化。有几个方法可以达到这个目的,其中最容易(也是最不讲理的)的一个方法就是在下面这个注册表键中添加一个动态链接库名称:
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows\APPINIT_DLLS

这个键导致Windows把你的DLL附加到系统里的每一个进程上。你的DLL需要一些能在它要子类化的事件发生时被唤醒的方法,通常一个WH_CBT钩子可以实现。这个DLL可以监视HCBT_CREATEWND 事件,然后子类化它想子类化的窗口。例子程序CTL3D就使用了WH_CBT钩子去实现它的子类化,尽管它没有使用注册表键为每个进程实现子类化,想要得到CTL3D功能的应用程序可以把他链接到进程里。

另一个把你的子类化代码附加到每个进程的方法是使用一个系统钩子。当一个系统钩子在另一个进程的上下文中被调用时,系统就把包含这个钩子的DLL加载到这个进程空间中。样例CTL3D 的代码按照使用当前线程中本地WH_CBT 钩子的方式使用系统WH_CBT 钩子。

第三个把子类化代码附加到另一个进程方法更复杂:它使用OpenProcess, WriteProcessMemory和 CreateRemoteThread 函数将代码注入其它进程。我并不推荐这个方法,并且不打算更详细地介绍怎么实现这个方法。如果有人坚持要使用这个方法, Jeffrey Richter 曾说过他计划在Microsoft Systems Journal中他的一个即将开辟的Win32 Q&A专栏中描述这项技术。

现在很多Windows 3.1的应用程序子类化其它进程来扩展操作和增加一些很酷的功能。当Windows转向一个面向对象的系统时,对象链接和嵌入技术(OLE)提供了更好的办法去实现这些功能。在Windows的未来版本中,子类化其它进程可能会变得更加困难,而使用OLE也许会更容易。我推荐的是,只要可能,你应该让你的应用程序转向OLE,而不要子类化其它进程。

子类化操作不要直接使用原窗口过程地址。

在Win16中,一个应用程序会去使用从SetWindowLong或 SetClassLong函数返回的窗口过程地址来直接调用窗口过程,毕竟返回值只是一个简单的指向函数的指针,为什么不使用它呢?在Win32中,这种做法却是个禁忌。SetWindowLong或 SetClassLong函数的返回值可能根本不是原窗口过程的指针。Win32可能会返回指向一个数据结构的指针,该数据结构能被用来调用当前的窗口过程。这种情况发生在Windows NT中,当一个应用程序用一个非Unicode的窗口过程子类化一个Unicode窗口时,或者用一个Unicode的窗口过程子类化一个非Unicode窗口时。在这两种情况下,操作系统必须为窗口收到的消息执行一个Unicode和ANSI之间的转换。如果应用程序直接使用指向这个结构的指针调用窗口过程,应用程序会立即导致一个异常。使用SetWindowLong或 SetClassLong函数返回的窗口地址的唯一做法是将返回值作为参数调用CallWindowProc函数。

实例子类化

SetWindowLong函数用来子类化一个窗口的一个实例。应用程序必须知道子类化函数的地址,子类化函数是这样一个函数:它用来接收从Windows发来的消息,并把消息传递给原窗口过程。子类化函数必须在应用程序中或DLL的模块定义文件中导出。

应用程序子类化窗口时,使用将要被子类化的窗口的句柄、GWL_WNDPROC标志(在WINDOWS.H中定义)以及新的子类化函数地址作为参数调用函数SetWindowLong。函数SetWindowLong返回一个DWORD类型的值,它是窗口的原窗口过程地址,应用程序应该保存该地址以用于将截获的消息传递给原窗口过程,以及将来为窗口移除子类化之用。应用程序使用原窗口过程的地址以及Windows消息所使用的hWndMessagewParamlParam参数调用函数CallWindowProc向原窗口过程传递消息。通常应用程序只是简单地把它从Windows接收来的数据传递给函数CallWindowProc

应用程序同时需要原窗口过程地址来为窗口移除子类化。应用程序通过再次调用函数SetWindowLong来为窗口移除子类化,应用程序向函数传递原窗口过程地址、GWL_WNDPROC 标志以及已经被子类化的窗口的句柄。

下面的代码演示子类化一个编辑框控件以及为它移除子类化:

LONG FAR PASCAL SubClassFunc(HWND hWnd,UINT Message,WPARAM wParam,LONG lParam);

FARPROC lpfnOldWndProc;
HWND hEditWnd;

//
// Create an edit control and subclass it.
// The details of this particular edit control are not important.
//

hEditWnd = CreateWindow("EDIT", "EDIT Test",
   WS_CHILD | WS_VISIBLE | WS_BORDER ,
   0, 0, 50, 50,
   hWndMain,NULL,hInst,NULL);

//
// Now subclass the window that was just Created.
//

lpfnOldWndProc = (FARPROC)SetWindowLong(hEditWnd,GWL_WNDPROC, (DWORD) SubClassFunc);

.
.
.

//
// Remove the subclass for the edit control.
//

SetWindowLong(hEditWnd, GWL_WNDPROC, (DWORD) lpfnOldWndProc);

//
// Here is a sample subclass function.
//

LONG FAR PASCAL SubClassFunc(HWND hWnd,
   UINT Message,WPARAM wParam,LONG lParam)
{
   //
   // When the focus is in an edit control   inside a dialog box, the
   //  default ENTER key action will not occur.
   //
   if ( Message == WM_GETDLGCODE )
       return DLGC_WANTALLKEYS;

return CallWindowProc(lpfnOldWndProc, hWnd, Message, wParam,lParam);
}

潜在的缺陷

只要留意下面的保障规则,实例子类化通常是安全的。

当子类化一个窗口时,你必须了解谁在对窗口的行为负责,例如,Windows负责它所提供的所有控件的行为,而应用程序则对它自己定义的所有窗口负责。子类化可以作用在同一进程中的任何一个窗口上,但是,当一个应用程序子类化一个不属于它自己负责的窗口时,应用程序必须确保子类化函数不会破坏那个窗口原有的行为。由于应用程序并未控制那个窗口,它不应该依赖任何在未来很有可能会被窗口的拥有者改变的窗口信息。除非确切地知道窗口附加字节或窗口类附加字节的含义以及原窗口过程如何使用它们,否则一个子类化函数不应该使用它们。退一步说,即使应用程序了解关于窗口附加字节或窗口类附加字节的每一件事,除非应用程序负责这个窗口,否则也不要使用它们。如果一个应用程序使用了由另一个组件负责的窗口的窗口附加字节,当那个组件决定更新窗口并且改变附加字节的结构时,子类化过程很有可能会失败。正是因为这个原因,Microsoft 建议你不要子类化控件类,因为Windows负责着这些它所提供的控件,而且下一个版本的Windows可能会改变这些控件的外观。如果你的应用程序必须子类化一个Windows提供的控件,在新版本的Windows发布后,代码很可能也需要更新。

由于实例子类化发生在一个窗口被创建之后,应用程序子类化窗口无法向窗口添加任何附加字节,应用程序也许需要将被子类化的窗口的实例所需的所有数据保存在窗口的属性列表中。

SetProp函数可以把属性附加到一个窗口。应用程序使用窗口的句柄、一个标示属性的字符串和属性数据的句柄作为参数调用SetProp函数。数据的句柄通常从调用LocalAlloc或 GlobalAlloc函数获得。当应用程序要使用一个窗口的属性列表中的这些数据时,可以使用窗口的句柄和标示属性的字符串作参数调用GetProp函数,它返回用SetProp函数设置的数据的句柄。当应用程序不再需要这些数据或者当窗口将要被销毁时,应用程序必须使用窗口的句柄和标示属性的字符串作参数调用RemoveProp函数从属性列表中移除这些属性数据,该函数返回数据的句柄,然后应用程序使用该句柄调用函数LocalFree或函数GlobalFree。关于函数SetPropGetPropRemoveProp的更多信息,参见平台SDK文档。

当一个应用程序子类化一个已经被子类化过的窗口时,所有的子类化会被按照们被设置时的相反顺序移除。

全局子类化

全局子类化类似于实例子类化。应用程序通过调用函数SetClassLong对一个窗口类进行全局子类化,就象在实例子类化中一样,应用程序同样需要子类化函数的地址,并且这个子类化函数必须在应用程序中或DLL的模块定义文件中导出。

要全局子类化一个窗口类,应用程序必须拥有一个该类的窗口实例。想要获得该类的窗口实例,大多数应用程序采取建立一个属于将要被全局子类化的窗口类的窗口的方法,当应用程序要移除子类化,也必须有一个窗口句柄,该句柄应该是属于应用程序要子类化的窗口类的,因此,为此而专门创建并保存一个窗口是个不错的办法。如果应用程序需要创建它所要子类化的窗口类的窗口实例,这个窗口实例通常应该是不可见的。在拥有了一个正确类型的窗口句柄之后,应用程序可以使用该窗口句柄、GCL_WNDPROC 标志(在WINDOWS.H 中有定义)和新子类化函数的地址作为参数调用函数SetClassLong,该函数返回一个DWORD值,该值是该窗口类的原窗口过程地址。原窗口过程地址在全局子类化中的用处和在实例子类化中一样,窗口消息也象在实例子类化中一样通过调用函数CallWindowProc传递给原窗口过程。应用程序可以通过再次调用SetClassLong函数来从窗口类移除子类化,这时需通过传递参数是原窗口过程地址、GCL_WNDPROC 标志和被子类化的窗口类的窗口实例句柄。全局子类化一个控件的应用程序必须在应用程序结束时移除所做的子类化。

下面的代码演示了全局子类化一个编辑框控件以及为它移除子类化:

Win32中安全的子类化(翻译)-LMLPHPLONG FAR PASCAL SubClassFunc(HWND hWnd,UINT,Message,WORD wParam,LONG lParam);
Win32中安全的子类化(翻译)-LMLPHPFARPROC lpfnOldClassWndProc;
Win32中安全的子类化(翻译)-LMLPHPHWND hEditWnd;
Win32中安全的子类化(翻译)-LMLPHP
Win32中安全的子类化(翻译)-LMLPHP//
Win32中安全的子类化(翻译)-LMLPHP// Create an edit control and subclass it.
Win32中安全的子类化(翻译)-LMLPHP// Notice that the edit control is not visible.
Win32中安全的子类化(翻译)-LMLPHP// Other details of this particular edit control are not important.
Win32中安全的子类化(翻译)-LMLPHP//
Win32中安全的子类化(翻译)-LMLPHPhEditWnd = CreateWindow("EDIT", "EDIT Test",
Win32中安全的子类化(翻译)-LMLPHP                        WS_CHILD,
Win32中安全的子类化(翻译)-LMLPHP                        0, 0, 50, 50,
Win32中安全的子类化(翻译)-LMLPHP                        hWndMain,
Win32中安全的子类化(翻译)-LMLPHP                        NULL,
Win32中安全的子类化(翻译)-LMLPHP                        hInst,
Win32中安全的子类化(翻译)-LMLPHP                        NULL);
Win32中安全的子类化(翻译)-LMLPHP
Win32中安全的子类化(翻译)-LMLPHPlpfnOldClassWndProc = (FARPROC)SetClassLong(hEditWnd, GCL_WNDPROC, (DWORD)SubClassFunc);
Win32中安全的子类化(翻译)-LMLPHP.
Win32中安全的子类化(翻译)-LMLPHP.
Win32中安全的子类化(翻译)-LMLPHP.
Win32中安全的子类化(翻译)-LMLPHP//
Win32中安全的子类化(翻译)-LMLPHP// To remove the subclass:
Win32中安全的子类化(翻译)-LMLPHP//
Win32中安全的子类化(翻译)-LMLPHPSetClassLong(hEditWnd, GWL_WNDPROC, (DWORD) lpfnOldClassWndProc);
Win32中安全的子类化(翻译)-LMLPHPDestroyWindow(hEditWnd);
Win32中安全的子类化(翻译)-LMLPHP

潜在的缺陷

全局子类化具有和实例子类化一样的限制,除非明确知道原窗口过程如何使用窗口类和窗口实例的附加字节,否则应用程序不应尝试去使用它们。如果数据必须和一个窗口相关联,可以象实例子类化中介绍的一样,使用窗口属性列表。

在Win32中, 全局子类化不会对任何其它进程中的窗口类或从这些类创建的窗口实例生效,这对于Win16环境是个重大的变化。在系统中,Windows分别为每个Win32进程单独保存窗口类的信息,可以参见MSDN中的技术文章Window Classes in Win32来了解Windows在这方面的细节。目前全局子类化不能对其它进程生效,这对开发人员来讲,是个有用的技术。在Win16中,全局子类化对被子类化的窗口类的每一个窗口实例都生效:不仅仅是属于执行了子类化操作的应用程序,还包括了属于整个系统的,这点让人感到失望。通常这是应用程序不想达到的效果,所以应用程序不得不使用更不方便,不好用的方法来改变从系统窗口类创建的窗口实例行为。而现在,在Win32中,使用全局子类化却是非常容易的。

超类化

子类化一个窗口类,导致原本属于窗口过程的消息被发送至子类化函数,然后该子类化函数再把消息传递给原窗口过程,而超类化(也被称作窗口类克隆)是创建一个新的窗口类。这个新窗口类使用一个已经存在的窗口类的窗口过程,来为它自己添加和已经存在的窗口类一样的功能,超类化是基于其它窗口类的――也被称为基类。基类常常是Windows预定义的控件类,但它也可以是任何其它窗口类。

注意   不要超类化滚动条控件类,因为Windows 使用该类名来为滚动条提供标准的行为。

超类化拥有它自己的窗口过程――超类化过程,它能起和子类化函数一样的作用。超类化过程可以对消息实施三种动作: (1)直接将消息传递给原窗口过程。(2)在传递给原窗口过程前修改消息。 (3)不在往下传递消息。超类化可以在把消息传递给原窗口过程之前、之后或两者都有的情况下对消息进行操作。

和子类化函数不一样的是,一个超类化过程也可以从Windows接收创建消息(例如WM_NCCREATE, WM_CREATE 之类的),超类化可以处理这些消息,但它必须把这些消息传递给原基类窗口过程,这样基类窗口过程才能进行初始化操作

应用程序调用函数GetClassInfo来使一个超类化基于一个基类。函数GetClassInfo使用从基类的WNDCLASS结构得来的值填充一个新WNDCLASS结构。然后超类化基类的应用程序把新WNDCLASS结构的hInstance域的值设置成应用程序自己的实例句柄,同时也必须把lpszClassName域的值设置成它要给该超类化的新名称。如果基类拥有一个菜单,超类化该基类的应用程序必须提供一个新菜单,该新菜单必须和基类的菜单拥有相同的菜单标识。如果该超类化打算处理WM_COMMAND消息的,并且不再把该消息传递给基类的窗口过程,那么菜单的标识可以不必和基类的一样。函数GetClassInfo不会返回WNDCLASS结构中域 lpszMenuNamelpszClassName, 和 hInstance的值。

最后一个必须在超类化的WNDCLASS结构中设置的是域lpfnWndProc,函数GetClassInfo用原窗口过程的地址填充它。应用程序必须保存这个地址,以便能用函数CallWindowProc把消息传递给基类的窗口过程。应用程序要在WNDCLASS结构中把该地址值设置成它的超类化过程的地址。这个地址并不是个过程实例地址,因为函数RegisterClass才能得到过程实例地址。应用程序可以修改WNDCLASS结构中其它域的值,以便符合应用程序的需要。

应用程序可以往窗口类附加字节和窗口实例附加字节后添加内容,这是因为它注册了一个新窗口类。当应用程序做这件事时,必须遵从两个规则: (1) 原类附加字节和窗口实例附加字节不能被子类化覆盖,这和在实例子类化与全局子类化中的原因一样。(2) 如果应用程序因自身需要为窗口类或窗口实例添加了附加字节,它在引用这些附加字节时,必须保持是相对于基类所使用的附加字节数来引用的。而且因为某个版本的基类所使用的附加字节数可能会与下一个版本不同,所以超类化自己的附加字节的起始偏移也因基类版本不同而不同。

当填充完WNDCLASS结构后,应用程序应该调用函数RegisterClass来注册新的窗口类,现在,就可以创建并使用属于该新窗口类的窗口实例了。

应用程序通常是在Win16环境下使用超类化,因为在Win16环境下全局子类化是令人沮丧的。现在在Win32下,全局子类化不再令人失望,所以超类化就不再那么具有吸引力了。但在你的应用程序要改变一些窗口的行为,而这些窗口又只是从一个系统窗口类所创建的所有窗口中的一部分时,你仍然可以发现使用超类化是很有用的,相反,对从一个系统窗口类所创建的所有窗口都有效,那是全局子类化的功能。

总结

子类化是个强大的技术,而且在Win32中的使用也没有发生什么特别重大的改变,唯一的比较主要的变化是你不能再属于另一个进程的窗口或窗口类,虽然有方法可以绕过这个限制,如果你确实需要这种能力,我还是建议你把你的应用程序移植到OLE,这比仍然依赖子类化更好。

好了,至此整篇文章都翻译完了(终于赶在放假前弄完了),在Win32中安全的子类化(1)中,提供了本文的英语原文的链接,由于本人时间、水平有限,所以欢迎大家指正文中的错误和疏漏之处,谢谢!

原文链接:http://www.cnblogs.com/tonybain/archive/2006/01/19/320366.html

05-01 22:31