Safe Subclassing in Win32 - Win32中的安全子类化 from MSDN.net 2003

来源:百度文库 编辑:神马文学网 时间:2024/04/20 20:40:33
http://blog.csdn.net/chenhao518530/archive/2006/03/18/628556.aspx http://www.cnblogs.com/tonybain/archive/2006/01/19/320366.html
 关于子类化的话题虽然有些旧,但它至今仍然不失为一种开发Windows的强有力技术,在MFC的内核、甚至.NET的内核中都离不开它,希望本连载能对Windows开发的爱好者有所帮助。

  Safe Subclassing in Win32 - Win32中的安全子类化 from MSDN.net 2003

Kyle Marsh
Microsoft Developer Network Technology Group

Created: January 25, 1994 原文:http://msdn.microsoft.com/en-us/library/ms997565.aspx 

摘要

这篇文章描述了Win32®环境下的子类化(subclassing)技术,它怎样实现,以及为了使子类化安全而必须遵循的规则(rules)。整篇文章覆盖了实例(instance)子类化和全局(global)子类化。超类化(superclassing)被描述为全局子类化的一个可选(alternative)部分。

子类化从Win16Win32没有发生显著的(dramatically)变化。但是在Win32中有些新的子类化规则应用程序必须遵循。其中最重要的(也是最显而易见的)是一个application不能子类化一个属于其他进程的窗口和类。这个规则不能被打破,但这里有某些workarounds(这个词不会翻译)应用程序可以使用。

Subclassing的定义

子类化是一种允许一个application截获送往其他窗口的消息的技术。一个application可以通过截获供其他窗口使用的消息来增加,监视,或者修改一个窗口的默认行为(behavior)。子类化对于改变或扩展一个窗口的行为是一种快捷高效的方法,而你并不需要重新开发这个窗口。子类化默认的控件窗口类(按钮控件,编辑控件,列表控件,组合框控件,静态控件和滚动条控件)是一种获得控件的功能并修改它的行为的方便方法。例如,如果一个多行(multiline)编辑控件包含在一个对话框中,并且用户按下了ENTER键,则对话框将被关闭。通过子类化这个编辑控件,当用户按下ENTER键时,一个程序可以插入一个回车符号并且文本换行而对话框没有退出。为了一个应用程序的这个需要,通过子类化技术,并不需要重新开发一个编辑控件。

基础

创建一个窗口的第一步就是填充WNDCLASS 结构并调用RegisterClass函数注册(register)一个窗口类。WNDCLASS结构的;一个元素就是窗口过程(window procedure)的地址。当一个窗口被创建,32位版本的微软Windows™操作系统就读取WNDCLASS结构中的窗口过程地址并且拷贝其到新窗口的信息结构(information structure)。当一个消息被发送给该窗口,Windows通过保存在窗口的信息结构中的地址调用相应的窗口过程。要子类化一个窗口,你可以通过用新的窗口过程地址替换原来的窗口过程地址来使一个新的窗口过程接收发送给原始窗口的所有消息。

当一个应用程序子类化了一个窗口,它可以对消息执行三种动作:(1)传递这个消息到原始窗口过程;(2)修改这个消息并传递它到原始窗口过程;(3)不继续传送该消息。

一个子类化了一个窗口的应用程序可以决定什么时候对它接收到的消息作出反应。这个程序可以在发送该消息到原始窗口过程之前或者之后,或者之前并且之后处理该消息(The application can process the message before, after or both before and after passing the message to the original window procedure.)

Types of Subclassing子类化的类型

子类化有两种类型,分别使实例子类化(instance subclassing)全局子类化(global subclassing)

例子类化是指子类化一个独立窗口的信息结构。使用实例子类化,只有特定窗口实例的消息会被发送到新的窗口过程(it only substitutes the address of window procedure in the window’s information structure, not WNDCLASS structure. We can see from the Topic “The Basics”each window has its own information structure.

全局子类化是指替换一个窗口类的WNDCLASS结构中的窗口过程地址.所有后来被创建的这种窗口类的窗口拥有被替换了的窗口过程地址。全局子类化只对子类化发生后创建的窗口有影响。在子类化时,如果任何该窗口类的窗口已经存在,则存在的窗口不受全局子类化的影响。如果程序需要影响已经存在的窗口的行为,这个程序必须子类化每个存在的该窗口类的实例。

Win32 子类化规则

Win32中,有两种子类化规则适()用于(apply to)实例和全局子类化。

子类化只允许在一个进程中发生,一个程序不能子类化属于另一个进程的窗口或类。

这条规则的原因很简单:Win32进程拥有独立的地址空间。一个窗口过程拥有其自己的地址。在一个不同的进程,窗口进程拥有不同的地址。作为结果,从一个进程中替换一个来自另一个进程的地址不能带来期望的结果,所以32位版本的Windows不允许这种替换发生(也就是说(that is),从一个不同的进程中进行子类化)SetWindowLongSetClassLong函数不允许这种类型的子类化。你不能子类化其他进程中的窗口或者类。

然而,仍然有某些方法可以使你能对任何进程增加子类化功能。一旦你得到一个进程的地址空间内的一个函数,你可以子类化该进程的任何部分。有少数几个方法可以达到这个目的。最简单(也使最无礼的)方法(approach)是向注册表的下面这个主键添加一个DLL(Dynamic-link library)的名字。

HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\ CurrentVersion\Windows\APPINIT_DLLS

这个主键导致Windows添加你的DLL到系统中的所有进程。在任何你的DLL想要子类化的事件发生后,你的DLL可能需要某种方法唤醒。WH_CBT钩子通常可以做到这点。DLL可以监视HCBT_CREATEWND事件,然后子类化期望的窗口。CTL3D例子程序使用WH_CBT钩子来进行子类化,不过它没有包含子类化任何进程的注册表入口。应用程序想要实现CTL3D的可以链接它到自己的进程中。

另一种添加你的子类化代码(subclassing code)到任意进程的方法是使用一个系统范围的钩子。当一个系统范围的钩子被从另一进程的上下文中调用,系统就把包含这个钩子的DLL载入该进程。CTL3D代码以同本地WH_CBT钩子同样的方式处理系统范围的WH_CBT钩子(In fact we called them global hook and thread-specific hook)

第三中方法非常复杂:它包括使用OpenProcess, WriteProcessMemory, and CreateRemoteThread函数来向其他进程注入代码。我不推荐这种方法,也不想详细讨论怎么实现它。对于坚持想用这种方法的开发人员,Jeffrey Richter(我最崇拜的Windows编程大师,技术作家,《windows核心编程》的作者)告诉我他正打算在他最近的在Microsoft Systems Journal 中的Win32 Q&A(Questions and Answers)专栏中描述这个技术。

今天,许多Windows 3.1程序子类化其他进程来增强该进程并增加某些非常酷的功能。Windows正向面向对象系统发展,对象链接和嵌入(OLE)提供了一个实现该功能更好的方法。在未来版本的Windows中,子类化其他进程可能会变得更难,而使用OLE可能变得更容易。如果可能,我推荐你将你的程序转换为OLE,而不是子类化其他进程。

子类化的进程可能不直接使用原始的窗口进程。

在Win16时代,一个程序可以直接通过 SetWindowLong or SetClassLong的返回值来调用原始的窗口过程。毕竟,这两个函数的返回值就是一个函数指针,所以为什么不直接调用它?在Win32时代,这是绝对不能做的事(definitive no-no). SetWindowLong and GetClassLong返回的值可能根本就不是指向之前的窗口过程的地址的指针。这发生在Window NT™中,当一个应用程序用一个非Unicode的窗口过程子类化一个Unicode™窗口,或者说一个拥有Unicode窗口过程的非Unicode窗口。在这种情况下,操作系统必须为窗口接收的消息执行一个Unicode和ANSI之间的转换。如果一个程序使用指向结构的指针调用窗口过程,程序将立即产生一个异常。使用从SetWindowLong or SetClassLong返回的值调用窗口过程的唯一方法就是把该返回值作为一个参数传给CallWindowProc。

实例子类化(subclassing a window)

SetWindowLong 用来子类化一个窗口实例。程序必须拥有子类华函数(subclass function)的地址。子类化函数是指从窗口接收消息并传递给原始窗口过程的函数。子类化函数必须被导出到程序或DLL的模块定义文件中(The subclass function must be exported in the application's or the DLL's module definition file.).

想要子类化某个窗口的程序使用该窗口的句柄,GWL_WNDPROC选项(在WINDOWS.H中定义), 新的子类化函数地址来调用SetWindowLong函数。SetWindowLong 返回一个DWORD值,这是该窗口的原始窗口过程的地址。程序必须保存这个地址来传递截获的消息给原始窗口过程并且用来从该窗口中移除子类化。通过使用原始窗口过程的地址和窗口消息中的hWnd, Message, wParam, lParam参数来调用CallWindowProc ,程序可以将消息发送给原始的窗口过程。通常,程序只简单的传送它从Windows接收到的参数给CallWindowProc.

程序同样需要原始窗口过程地址来从窗口移除子类化。程序通过再次调用SetWindowLong 来从窗口移除子类化。程序传递原始窗口过程地址,GWL_WNDPROC 选项和被子类化的窗口句柄句柄给SetWindowLong 函数。

下面的代码例子子类化一个编辑控件(edit control)并在之后移除子类化。

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);
}

Potential pitfalls潜在的缺陷

实例子类化普通情况下是安全的,但注意下面的规则可以确保安全。

当子类化一个窗口,你必须知道由谁来对该窗口的行为负责。例如,Windows对它提供的所有控件负责,而程序对它所定义的所有窗口负责。子类化可对同一进程中的任意窗口进行,然而,当一个程序对一个它不负责的窗口进行子类化,这个程序必须保证子类化函数不会破坏该窗口的原始行为(original behavior)。因为这个程序并不控制该窗口,所以不能依赖于任何关于该窗口的信息,因为对该窗口负责的组件可能在未来改变。一个子类化函数不应该使用窗口中的额外窗口字节(extra window bytes)和类字节(class bytes),除非它确切了解这些字节的含义和原始窗口过程如何使用它们。即使这个程序对额外窗口字节和类字节很了解,它也不应该使用它们,除非该程序决定更新(update)这个窗口并改变这些额外字节的某些方面,否则子类化过程很有可能失败。因为这个原因,Microsoft建议你不要子类化控件类(control classes).Windows对它提供的控件负责,而控件的某些方面可能随着Windows版本的改变而改变。如果你的程序必须子类化一个Windows提供的控件,当新的Windows版本发布(release)时,你也许得更新你的代码。

因为实例子类化(instance subclassing)发生在窗口被创建后,子类化窗口的程序不能向该窗口增加任何额外字节(extra bytes)。程序应该将需要存储的数据放在被子类化的窗口的属性列表中(property list)。

可以设置一个窗口的属性。程序使用窗口句柄,一个标识属性的字符串,以及一个指向数据的句柄来调用SetProp 函数。指向数据的句柄通常通过调用LocalAlloc or GlobalAlloc 来得到。当一个程序需要使用窗口的属性列表中的数据,它可以用该窗口的句柄以及标识该属性的字符串作为参数来调用GetProp 函数。GetProp 返回由SetProp设置的指向数据的句柄,当程序使用完这些数据,或者当窗口即将被销毁,程序必须调用RemoveProp 来从窗口的属性列表中移除这些属性,参数是窗口的句柄和属性的字符串标识。RemoveProp 返回数据的句柄,此时程序用这些句柄来调用LocalFree or GlobalFree以释放内存。

如果一个程序子类化一个已经子类化了的窗口,则移除子类化时必须以相反的顺序进行,即后子类化的先移除。

全局子类化(Subclassing a window class)

全局子类化与实例子类化相似。程序调用SetClassLong 来全局的子类化一个窗口类(window class)。与实例子类化一样,程序需要子类化函数的地址,并且子类化函数必须在程序或DLL的模块定义文件中导出。

要全局子类化一个窗口类,程序必须拥有该窗口类的一个窗口的句柄。要得到期望的窗口类的窗口句柄,多数程序建立一个相应类的窗口。当程序要移除子类化,它需要一个指向它子类化了的窗口类的窗口句柄,因此,为此目的建立并维护一个窗口是最好的技术(technique)。如果程序建立一个它想子类化的类的窗口,一般会把该窗口隐藏。在得到正确的窗口类的窗口句柄后,程序用该窗口句柄,GCL_WNDPROC 选项(defined in WINDOWS.H),以及子类化函数的地址来调用SetClassLong. SetClassLong 返回一个DWORD值,这是该窗口类的原始窗口过程地址。此时通过调用CallWindowProc,程序可以将消息发送给原始窗口过程。程序可以通过再次调用SetClassLong移除子类化,只需要向开始那样,只是把子类化函数地址换成原始窗口过程地址。


LONG FAR PASCAL SubClassFunc(HWND hWnd,UINT,Message,WORD wParam,LONG lParam);
FARPROC lpfnOldClassWndProc;
HWND hEditWnd;

//
// Create an edit control and subclass it.
// Notice that the edit control is not visible.
// Other details of this particular edit control are not important.
//
hEditWnd = CreateWindow("EDIT", "EDIT Test",
                        WS_CHILD,
                        0, 0, 50, 50,
                        hWndMain,
                        NULL,
                        hInst,
                        NULL);

lpfnOldClassWndProc = (FARPROC)SetClassLong(hEditWnd, GCL_WNDPROC, (DWORD)SubClassFunc);
.
.
.
//
// To remove the subclass:
//
SetClassLong(hEditWnd, GWL_WNDPROC, (DWORD) lpfnOldClassWndProc);
DestroyWindow(hEditWnd);

潜在的缺陷

全局子类化和实例子类化拥有一样。程序不应该尝试(attempt to)使用窗口类或窗口的额外字节(extra bytes)。除非它确切(exactly)知道原始过程怎样使用它们。如果必须在窗口上附着数据,应该向实例子类化那样使用窗口属性列表。

在Win32中,全局子类化不影响其他进程的类或之前从这些窗口类建立的窗口。这是从Win16环境过来的非常重要的一个变化。Windows为系统中每个不同的Win32进程保持单独的窗口类信息。要想了解Windows这方面的细节,请参阅MSDN库中"Window Classes in Win32"技术文章现在全局子类化不会影响其他进程,这成为了开发者有用的技术。在Win16中,全局子类化不被鼓励使用,因为它影响了子类化的窗口类的所有窗口-不光是执行子类化的程序,而是整个系统。这不是程序通常想要的,所以程序只能使用不方便和并不强大(less powerful)的方法来改变系统类创建的窗口的行为。在Win32中使用全局子类化变得非常的简单。

 

Superclassing超类化

子类化一个窗口类导致到窗口过程的消息被发送到子类化函数(subclass function)。子类化函数然后把该消息传递给原始窗口过程。超类化Superclassing(also known as class cloning)建立一个新的窗口类。新的窗口类使用存在的类的窗口过程,来使新的窗口类具有存在的类的功能(functionality)。超类化使基于其他的窗口类的,已经存在的类被称为base class

注意
不要超类化滚动条(scroll bar)控件类,因为Windows使用该类的名字来正确的处理滚动条的行为。

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

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

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

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

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

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

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

总结

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

 


Send feedback to Microsoft

© 2003 Microsoft Corporation. All rights reserved.

发表于 @ 2006年03月18日 20:30:00 (#)