Safe Subclassing in Win32(Win32中的安全子类化)

Safe Subclassing in Win32(Win32中的安全子类化)
Kyle Marsh
Microsoft Developer Network Technology Group
Created: January 25, 1994
译者:BBE&BFE

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

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

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

2.基础
    创建一个窗口的第一步就是填充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.)

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

4.Win32 子类化规则
    在Win32中,有两种子类化规则适(应)用于(apply to)实例和全局子类化。
    子类化只允许在一个进程中发生,一个程序不能子类化属于另一个进程的窗口或类。
这条规则的原因很简单:Win32进程拥有独立的地址空间。一个窗口过程拥有其自己的地址。在一个不同的进程,窗口进程拥有不同的地址。作为结果,从一个进程中替换一个来自另一个进程的地址不能带来期望的结果,所以32位版本的Windows不允许这种替换发生(也就是说(that is),从一个不同的进程中进行子类化)。SetWindowLong和SetClassLong函数不允许这种类型的子类化。你不能子类化其他进程中的窗口或者类。
    然而,仍然有某些方法可以使你能对任何进程增加子类化功能。一旦你得到一个进程的地址空间内的一个函数,你可以子类化该进程的任何部分。有少数几个方法可以达到这个目的。最简单(也使最无礼的)方法(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。

5.实例子类化(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;//The application will process all keyboard inputs itself.
   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以释放内存。
     如果一个程序子类化一个已经子类化了的窗口,则移除子类化时必须以相反的顺序进行,即后子类化的先移除。

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

The following code globally subclasses and removes a subclass to an edit control:

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中使用全局子类化变得非常的简单。

7.Superclassing超类化
   Subclassing a window class causes messages meant for the window procedure to be sent to the subclass function. The subclass function then passes the message to the original window procedure. Superclassing (also known as class cloning) creates a new window class. The new window class uses the window procedure from an existing class to give the new class the functionality of the existing class. The superclass is based on some other window class, known as the base class. Frequently the base class is a Windows-supplied control class, but it can be any window class.
    子类化一个窗口类导致到窗口过程的消息被发送到子类化函数(subclass function)。子类化函数然后把该消息传递给原始窗口过程。超类化Superclassing(also known as class cloning)建立一个新的窗口类。新的窗口类使用存在的类的窗口过程,来使新的窗口类具有存在的类的功能(functionality)。超类化使基于其他的窗口类的,已经存在的类被称为base class。
   Note:Do not superclass the scroll bar control class because Windows uses the class name to produce the correct behavior for scroll bars.
    注意:不要超类化滚动条(scroll bar)控件类,因为Windows使用该类的名字来正确的处理滚动条的行为。
   The superclass has its own window procedure, the superclass procedure, which can take the same actions a subclass procedure can. The superclass procedure can take three actions with the message: (1) pass the message directly to the original window procedure; (2) modify the message before passing it to the original window procedure; (3) not pass the message. The superclass can react to the message before, after, or both before and after passing the message to the original window procedure.超类化拥有它自己的窗口过程,超类化过程(the superclass procedure).
    可以拥有与子类化过程(subclass procedure)相同的行为。超类化过程对消息可以执行三种行为:(1)直接传递消息到原始窗口过程;(2)在传递消息到原始窗口过程之前修改消息;(3)不传递消息。超类化可以在传递消息到原始窗口过程前,后,或之前并之后对消息作出响应。
   Unlike a subclass procedure, a superclass procedure receives create (WM_NCCREATE, WM_CREATE, and so on) messages from Windows. The superclass procedure can process these messages, but it must also pass these messages to the original base-class window procedure so that the base-class window procedure can initialize.
    不象子类化过程,一个超类化过程接收创建消息(create message just like WM_NCCREATE, WM_CREATE, and so on). 超类化过程可以处理这些消息,但它必须传递这些消息到基类(base-class)窗口过程以便于基类窗口过程可以进行初始化。
   The application calls GetClassInfo to base a superclass on a base class. GetClassInfo fills a WNDCLASS structure with the values from the base class's WNDCLASS structure. The application that is superclassing the base class then sets the hInstance field of the WNDCLASS structure to the instance handle of the application. The application must also set the WNDCLASS structure's lpszClassName field to the name it wants to give this superclass. If the base class has a menu, the application superclassing the base class must supply a new menu that has the same menu IDs as the base class's menu. If the superclass intends to process the WM_COMMAND message and not pass the message to the base class's window procedure, the menu does not have to have corresponding IDs. GetClassInfo does not return the lpszMenuName, lpszClassName, or hInstance field of the WNDCLASS structure.
   The last field that must be set in the superclass's WNDCLASS structure is the lpfnWndProc field. GetClassInfo fills this field with the original class window procedure. The application must save this address so that it can pass messages to the original window procedure with a call to CallWindowProc. The application must put the address of its subclass function into the WNDCLASS structure. This address is not a procedure-instance address because RegisterClass gets the procedure-instance address. The application can modify any other fields in the WNDCLASS structure to suit the application's needs.
   The application can add to both the extra class bytes and the extra window bytes because it is registering a new class. The application must follow two rules when doing this: (1) the original extra bytes for both the class and the window must not be touched by the superclass for the same reasons that an instance subclass or a global subclass should not touch these extra bytes; (2) if the application adds extra bytes to either the class or the window instance for the application's own use, it must always reference these extra bytes relative to the number of extra bytes used by the original base class. Because the number of bytes used by the base class may be different from one version of the base class to the next, the starting offset for the superclass's own extra bytes is also different from one version of the base class to the next.
   After the WNDCLASS structure is filled, the application calls RegisterClass to register the new window class. Windows of this class can now be created and used.
   Applications often used superclassing in Win16 because global subclassing was discouraged. Now that global subclassing is no longer discouraged in Win32, superclassing has lost some of its appeal. You may still find it useful to create a superclass if your application wants to change the behavior for only a subset of the windows (instead of all windows) created from a system class, which is the effect of global subclassing.
   Summing Up
   Subclassing is a powerful technique that has not changed significantly in Win32. The only major change is that you can no longer subclass a window or class that belongs to another process. Although there are workarounds for this restriction, I recommend that you move your application to OLE rather than relying on subclassing if you need this capability. 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值