Jangogo : 

子类化(Subclass)的实现的深入讨论
作者:阿国哥  发布于2006-4-26 17:10(星期三)
导读
 
什么是子类化窗口过程
实现子类化的一般方法
另类技术实现子类化(Thunk)
继续扩展 Thunk 的应用
使用 ComCtl32.dll version 6 实现窗口子类化
点此下载 VB 版的实现源码
一、什么是子类化(Subclass)
 
         视窗系统是基于消息驱动的。因此每一个窗口都有一个函数来处理这些消息,系统管理的窗口结构中有一域记录着这个函数的地址(使用GetWindowLong函数传入索引标识GWL_WNDPROC可获取此值)。所有系统发送到这个窗口的消息都会交由此函数来处理。所以改变窗口结构中这个指针值到自定义的某个函数(使用SetWindowLong函数带GWL_WNDPROC索引标识修改此值),就能接管窗口绝大部分的消息。实际中我们只处理自己感兴趣的某些消息,之后可以有选择的继续Call原来那个窗口函数(使用CallWindowProc函数调用)或直接返回一个值。
 
二、实现子类化的一般方法
 
  从上面可以看出,子类化主要使用几个API函数,这几乎是所有实现子类化相同的方法。相关API说明如下:
 1、 WINUSERAPI LONG WINAPI GetWindowLongA(HWND hWnd, int nIndex);
     WINUSERAPI LONG WINAPI GetWindowLongW(HWND hWnd,int nIndex);
 
       VB声明:Declare Function GetWindowLong Lib "user32" Alias "GetWindowLongW" (ByVal hWnd As Long, ByVal nIndex As Long) As Long
         注:建议使用UNICODE版的API,无特别原因毕竟不用再考虑9X平台下的应用。如非特别说明,我所有原创文章中使用的API均为UNICDOE版。
 
        说明:    hWnd  窗口句柄
                         nIndex 结构索引
                        索引微软如下定义:
                        /*
                        * Window field offsets for GetWindowLong()
                        */
                       #define GWL_WNDPROC         (-4) //这个就是我们要用到的
                       #define GWL_HINSTANCE       (-6)
                       #define GWL_HWNDPARENT      (-8)
                       #define GWL_STYLE           (-16)
                       #define GWL_EXSTYLE         (-20)
                       #define GWL_USERDATA        (-21)
                       #define GWL_ID              (-12)
 2、 SetWindowLong 与 CallWindowProc 函数这不再列出来了,都是粉容易的!
 
       实际应用中,在维护原窗口函数指针及新的窗口函数时,最原始的方法是使用单独的变量保存原指针及单独的窗口函数服务某一个窗口。但这样很不方便。编写和维护代码都造成很大的混乱。于是我们将新的窗口函数直接定义到相应的类的实例中去(如VB的表单,VC的类),这样在编写及维护程序是很方便。自然而然的,我们就要维护一个链表,即窗口句柄与对应的实例指针。目前也有使用 GWL_USERDATA 域保存实例指针,也有使用 SetProp 及 GetProp 函数来记录窗口句柄与实例指针的对应关系的。这些都各有优劣,前者不够稳定,后者速度不尽人意。相关的实例网上也大把资料的,在此本文就不再多此一举了。本文讨论的主题是如何利用特殊的代码来维护这种关系,最后发布VB的实现代码,帮助众多的VB程序员方便使用此技术。
 
三、另类技术实现子类化(Thunk)
 在ATL模板中,微软使用一段奇妙的代码实现将窗口的子类化,称为 Thunk 技术。且看如下代码
 
// ===================================================================
 // 摘自 ATLWIN.h    已去除条件编译中的_M_ALPHA声明代码,仅讨论Intel系统
 // ===================================================================
 // WindowProc thunks
 
struct _WndProcThunk
 {
   DWORD m_mov; // mov dword ptr [esp+0x4], pThis (esp+0x4 is hWnd)
   DWORD m_this; //
   BYTE m_jmp; // jmp WndProc
   DWORD m_relproc; // relative jmp
 };
 
class CWndProcThunk
 {
   public:
     union
     {
         _AtlCreateWndData cd;
         _WndProcThunk thunk;
     };
     void Init(WNDPROC proc, void *pThis)
     {
         thunk.m_mov = 0x042444C7; //C7 44 24 04(原文为0C,是微软的错误)
         thunk.m_this = (DWORD)pThis;
         thunk.m_jmp = 0xe9;
         thunk.m_relproc = (int)proc - ((int)this + sizeof(_WndProcThunk));
 
        FlushInstructionCache(GetCurrentProcess(), &thunk, sizeof(thunk));
     }
 };
 
其实也不是什么高深的代码,实际上就是替换栈中原 hWnd ([esp+0x4]) 参数为 This 指针,然后有了实例的 This 指针,相就的数据如 hWnd 的访问就非常容易,非常方便了。对应的汇编代码如下:
 mov dword ptr [esp+04],实例 This 指针
jmp 新的窗口函数
 然后在新的窗口函数中,第一参数已不再是 hWnd ,而是 This。
 

template <class TBase, class TWinTraits> LRESULT CALLBACK
   CWindowImplBaseT<TBase, TWinTraits>::WindowProc(HWND hWnd, UINT uMsg, WPARAM
   wParam, LPARAM lParam)
 {
     CWindowImplBaseT<TBase, TWinTraits>  *pThis = (CWindowImplBaseT<TBase,
       TWinTraits> *)hWnd;
     ...//省略
 

可以看出,函数一开始就把 hWnd 参数 cast 到一个类的实例指针 pThis。看到这里,相信你应该了解了,就是这一点点小技巧,成功的处理好了整个 subclass 过程,如果你是VB程序员,请看下面的代码:
 '在模块中:
 
Option Explicit
 '================================================
 ' Thunk 实现子类化之第一版。扩展应用请使用第二版
 ' 作者:阿国哥 (hackor)
 ' 网站:javascript:lnkchk('%2fmoc.egouga.www%2f%2f%3aptth');
 '       hackor@yeah.net
 ' 注意:转载请保留此声明
 '================================================
 
'第一版 汇编实现源码
 'C74424 04 01000000    mov dword ptr ss:[esp+4],1       ;替换 hWnd 参数为 实例指针
 '90                    nop
 '90                    nop
 '90                    nop
 'B8 01000000           mov eax,1
 'FFE0                  jmp eax                          ;跳到自定义的窗口函数
 '90                    nop
 '90                    nop
 

Private Const GWL_WNDPROC = (-4)
 Private Declare Function SetWindowLong Lib "user32" Alias "SetWindowLongA" (ByVal hwnd As Long, _
         ByVal nIndex As Long, ByVal dwNewLong As Long) As Long
 Private Declare Function IsWindow Lib "user32" (ByVal hwnd As Long) As Long
 
'Thunk 代码及数据类型
 Public Type SubclassThunk
     AsmMov1     As Long   '&H042444C7        C7 44 24 04
     pThis       As Long   'ObjPtr(对象)
     AsmMov2     As Long   '&HB8909090        90 90 90 B8
     pWndProc    As Long   'pWndProc
     AsmJmp      As Long   '&H9090E0FF        FF E0 90 90
     '-----------------
     hwnd        As Long
     WndProcNext As Long
 End Type
 '相应的汇编实现源码,nop 指令仅仅是对齐数据,另外注意字节顺序;
 '--------------------------------------------------------------------------------
 '机器码                汇编指令                          注释
 '--------------------------------------------------------------------------------
 'C74424 04 01000000    mov dword ptr ss:[esp+4],pThis   ;替换 hWnd 参数为 实例指针
 '90                    nop                              ;
 '90                    nop
 '90                    nop
 'B8 01000000           mov eax,pWndProc
 'FFE0                  jmp eax                          ;跳到自定义的窗口函数
 '90                    nop
 '90                    nop
 
'-----------------------------
 ' 创建子类化
 '-----------------------------
 Public Function CreateSubclass(Thunk As SubclassThunk, ByVal hwnd As Long, _
                             ByVal pThis As Long, ByVal pWndProc As Long) As Long
     Dim pThunk As Long
 
    Debug.Assert IsWindow(hwnd)
 
    pThunk = VarPtr(Thunk)
     With Thunk
         .AsmMov1 = &H42444C7
         .pThis = pThis
         .AsmMov2 = &HB8909090
         .pWndProc = pWndProc
         .AsmJmp = &H9090E0FF
         '-------------------
         .hwnd = hwnd
         .WndProcNext = SetWindowLong(hwnd, GWL_WNDPROC, pThunk)
         CreateSubclass = .WndProcNext
     End With
 End Function
 
'----------------------------
 ' 卸载子类化
 '----------------------------
 Public Sub DestroySubclass(Thunk As SubclassThunk)
     Debug.Assert Thunk.WndProcNext
     Call SetWindowLong(Thunk.hwnd, GWL_WNDPROC, Thunk.WndProcNext)
     Thunk.WndProcNext = 0
 End Sub
 
'-------------------------------------------
 ' 窗口转发函数,实际应用中,应修改第一个参数
 '-------------------------------------------
 Public Function gWndProc(ByVal frmTest As Form1, ByVal uMsg As Long, _
                         ByVal wParam As Long, ByVal lParam As Long) As Long
     gWndProc = frmTest.WndProc(uMsg, wParam, lParam)
 End Function
 

'在表单中 Option Explicit
 Private Const WM_MBUTTONUP = &H208
 Private Declare Function CallWindowProc Lib "user32" Alias "CallWindowProcA" (ByVal lpPrevWndFunc As Long, _
         ByVal hwnd As Long, ByVal MSG As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
 Dim mWndProcThunkEx1 As SubclassThunkEx
 Dim mWndProcThunkEx2 As SubclassThunkEx
 Dim mWndProcThunkEx3 As SubclassThunkEx
 
Private mWndProcThunk As SubclassThunk
 
Private Sub Form_Load()
     Call CreateSubclass(mWndProcThunk, Me.hwnd, ObjPtr(Me), AddressOf gWndProc)
 End Sub
 

Friend Function WndProc(ByVal uMsg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
     If uMsg = WM_MBUTTONUP Then Debug.Print "WM_MBUTTONUP"
     WndProc = CallWindowProc(mWndProcThunk.WndProcNext, mWndProcThunk.hwnd, uMsg, wParam, lParam)
 End Function
 
Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer)
     DestroySubclass mWndProcThunk
 End Sub

文档中心
Copyright © 2000-2016 粤ICP05021785号
地址:广州市天河区员村二横路8号全丰商业大厦808室 邮编:510600