ITEEDU

第22课 超类化


在这一讲我们将学习什么是超类化以及它有什么作用;同时你还会学到怎样在自己的窗口中用Tab键在控件中切换这一技巧。

理论:

在你的程序生涯中你肯定遇到过这样的情况,你需要一系列的控件,但它们之间却只有一点点的不同。例如,你可能需要10个只接受数字的 Edit 控件,当然你可以通过多种方法来达到这个目的。

  1. 创建自己的类并用它实例化为那些控件
  2. 创建那些 Edit 控件并把它们全部子类化
  3. 超类化Edit 控件

第一种方法太乏味了,因为你必须自己实现Edit 控件的每个功能,但这项工作不是轻松就能完成的。第二种方法好于第一种,但仍然要做许多工作,子类化几个Edit 控件还可以接受,但若要子类化十几二十个,这项工作简直就是一场恶梦。在这种情况下就应该使用超类化这个技巧,它是用于控制某一个特定窗口类的特殊方法。通过这种控制就可以修改窗口类的特性使之符合你的要求,然后再创建那一堆控件就可以了。

超类化有如下几个步骤:

  1. 通过调用 GetClassInfoEx 来获得想要进行超类化操作的窗口类的信息。函数GetClassInfoEx 需要一个指向 WNDCLASSEX 结构的指针,用于当成功返回时填入窗口类的信息。
  2. 按需要修改 WNDCLASSEX 结构的成员,其中有两个成员必须修改:
    hInstance 存放程序的实例句柄
    lpszClassName 指向一个新类名的指针
    不必修改成员 lpfnWndProc,但大多数情况下还是需要的。但要记住如果要使用函数 CallWindowProc 调用老窗口的过程,那就必须保存成员 lpfnWndProc 的原值。
  3. 注册修改完的 WNDCLASSEX 结构,得到一个具有旧窗口类某些特性的新窗口类。
  4. 用新窗口类创建窗口

如果要创建具有相同特性的多个控件,超类化就比子类化要好。

举例:

.386
.MODEL        FLAT,STDCALL
option casemap:none 
     INCLUDE  \MASM32\INCLUDE\WINDOWS.INC
     INCLUDE  \MASM32\INCLUDE\USER32.INC
     INCLUDE  \MASM32\INCLUDE\KERNEL32.INC
  INCLUDELIB  \MASM32\LIB\USER32.LIB
  INCLUDELIB  \MASM32\LIB\KERNEL32.LIB
           WM_SUPERCLASS  EQU       WM_USER+5
WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD 
EditWndProc PROTO :DWORD,:DWORD,:DWORD,:DWORD 

.DATA
   CLASSNAME  DB        "SUPERCLASSWINCLASS",0
     APPNAME  DB        "SUPERCLASSING DEMO",0
   EDITCLASS  DB        "EDIT",0
    OURCLASS  DB        "SUPEREDITCLASS",0
     MESSAGE  DB        "YOU PRESSED THE ENTER KEY IN THE TEXT BOX!",0

.DATA?
   HINSTANCE  DD        ?
    HWNDEDIT  DD        6 DUP(?) ;存放6个窗口句柄的数组
  OLDWNDPROC  DD        ? ;原来的窗口过程

.CODE
      START:
              INVOKE    GETMODULEHANDLE, NULL
              MOV       HINSTANCE,EAX
              INVOKE    WINMAIN, HINSTANCE,NULL,NULL, SW_SHOWDEFAULT
              INVOKE    EXITPROCESS,EAX

     WINMAIN  PROC      HINST:HINSTANCE,HPREVINST:HINSTANCE,CMDLINE:LPSTR,CMDSHOW:DWORD
              LOCAL     WC:WNDCLASSEX
              LOCAL     MSG:MSG
              LOCAL     HWND:HWND

              MOV       WC.CBSIZE,SIZEOF WNDCLASSEX
              MOV       WC.STYLE, CS_HREDRAW OR CS_VREDRAW
              MOV       WC.LPFNWNDPROC, OFFSET WNDPROC
              MOV       WC.CBCLSEXTRA,NULL
              MOV       WC.CBWNDEXTRA,NULL
              PUSH      HINST
              POP       WC.HINSTANCE
              MOV       WC.HBRBACKGROUND,COLOR_APPWORKSPACE
              MOV       WC.LPSZMENUNAME,NULL
              MOV       WC.LPSZCLASSNAME,OFFSET CLASSNAME
              INVOKE    LOADICON,NULL,IDI_APPLICATION
              MOV       WC.HICON,EAX
              MOV       WC.HICONSM,EAX
              INVOKE    LOADCURSOR,NULL,IDC_ARROW
              MOV       WC.HCURSOR,EAX
              INVOKE    REGISTERCLASSEX, ADDR WC
              INVOKE    CREATEWINDOWEX,WS_EX_CLIENTEDGE+WS_EX_CONTROLPARENT,ADDR CLASSNAME,ADDR APPNAME,\
WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE,CW_USEDEFAULT,\ 
CW_USEDEFAULT,350,220,NULL,NULL,\ 
hInst,NULL 
              MOV       HWND,EAX

.WHILE        TRUE
              INVOKE    GETMESSAGE, ADDR MSG,NULL,0,0
.BREAK        .IF (!EAX)
              INVOKE    TRANSLATEMESSAGE, ADDR MSG
              INVOKE    DISPATCHMESSAGE, ADDR MSG
.ENDW
              MOV       EAX,MSG.WPARAM
              RET
     WINMAIN  ENDP

     WNDPROC  PROC      USES EBX EDI HWND:HWND, UMSG:UINT, WPARAM:WPARAM, LPARAM:LPARAM
              LOCAL     WC:WNDCLASSEX
.IF           UMSG==WM_CREATE
              MOV       WC.CBSIZE,SIZEOF WNDCLASSEX
              INVOKE    GETCLASSINFOEX,NULL,ADDR EDITCLASS,ADDR WC
              PUSH      WC.LPFNWNDPROC
              POP       OLDWNDPROC
              MOV       WC.LPFNWNDPROC, OFFSET EDITWNDPROC
              PUSH      HINSTANCE
              POP       WC.HINSTANCE
              MOV       WC.LPSZCLASSNAME,OFFSET OURCLASS
              INVOKE    REGISTERCLASSEX, ADDR WC
              XOR       EBX,EBX
              MOV       EDI,20
.WHILE        EBX<6
              INVOKE    CREATEWINDOWEX,WS_EX_CLIENTEDGE,ADDR OURCLASS,NULL,\
WS_CHILD+WS_VISIBLE+WS_BORDER,20,\ 
edi,300,25,hWnd,ebx,\ 
hInstance,NULL 
              MOV       DWORD PTR [HWNDEDIT+4*EBX],EAX
              ADD       EDI,25
              INC       EBX
.ENDW
              INVOKE    SETFOCUS,HWNDEDIT
.ELSEIF       UMSG==WM_DESTROY
              INVOKE    POSTQUITMESSAGE,NULL
.ELSE
              INVOKE    DEFWINDOWPROC,HWND,UMSG,WPARAM,LPARAM
              RET
.ENDIF
              XOR       EAX,EAX
              RET
     WNDPROC  ENDP

 EDITWNDPROC  PROC      HEDIT:DWORD,UMSG:DWORD,WPARAM:DWORD,LPARAM:DWORD
.IF           UMSG==WM_CHAR
              MOV       EAX,WPARAM
.IF           (AL>="0" && AL<="9") || (AL>="A" && AL<="F") || (AL>="A" && AL<="F") || AL==VK_BACK
;处理字符0~9,A~F,a~f,这几个十六进制数
.IF           AL>="A" && AL<="F"
              SUB       AL,20H
如果是字符a~f,则把它们变为大写
.ENDIF
              INVOKE    CALLWINDOWPROC,OLDWNDPROC,HEDIT,UMSG,EAX,LPARAM
              RET
.ENDIF
.ELSEIF       UMSG==WM_KEYDOWN
              MOV       EAX,WPARAM
.IF           AL==VK_RETURN
              INVOKE    MESSAGEBOX,HEDIT,ADDR MESSAGE,ADDR APPNAME,MB_OK+MB_ICONINFORMATION
              INVOKE    SETFOCUS,HEDIT
.ELSEIF       AL==VK_TAB
              INVOKE    GETKEYSTATE,VK_SHIFT
              TEST      EAX,80000000
.IF           ZERO?
              INVOKE    GETWINDOW,HEDIT,GW_HWNDNEXT
.IF           EAX==NULL
              INVOKE    GETWINDOW,HEDIT,GW_HWNDFIRST
.ENDIF
.ELSE
              INVOKE    GETWINDOW,HEDIT,GW_HWNDPREV
.IF           EAX==NULL
              INVOKE    GETWINDOW,HEDIT,GW_HWNDLAST
.ENDIF
.ENDIF
              INVOKE    SETFOCUS,EAX
              XOR       EAX,EAX
              RET
.ELSE
              INVOKE    CALLWINDOWPROC,OLDWNDPROC,HEDIT,UMSG,WPARAM,LPARAM
              RET
.ENDIF
.ELSE
              INVOKE    CALLWINDOWPROC,OLDWNDPROC,HEDIT,UMSG,WPARAM,LPARAM
              RET
.ENDIF
              XOR       EAX,EAX
              RET
 EDITWNDPROC  ENDP
              END       START

分析

这个程序创建了一个在其客户区有六个被修改的 Edit 控件的简单窗口,这些 Edit控件只接受十六进制的数字。实际上,这个例子是通过修改窗口了类化的例子得来的。这个程序开始和其它程序一样,有趣的部分出现在主窗口被创建的时候:

.IF           UMSG==WM_CREATE
              MOV       WC.CBSIZE,SIZEOF WNDCLASSEX
              INVOKE    GETCLASSINFOEX,NULL,ADDR EDITCLASS,ADDR WC

必须用想进行超类化操作的类数据填充 WNDCLASSEX 结构,在我们的例子中就是类 Edit ,记住在调用函数 GetClassInfoEx 之前必须填写成员 cbSize,否则函数调用 GetClassInfoEx不会在 WNDCLASSEX 结构中填入正确的返回值。成功返回后,变量 wc中保存的就是想要创建一个新类所需要的所有信息。

              PUSH      WC.LPFNWNDPROC
              POP       OLDWNDPROC
              MOV       WC.LPFNWNDPROC, OFFSET EDITWNDPROC
              PUSH      HINSTANCE
              POP       WC.HINSTANCE
              MOV       WC.LPSZCLASSNAME,OFFSET OURCLASS

现在必须修改变量 wc 的一些属性:第一个要修改的就是指向窗口过程的指针。因为在新窗口过程中函数 CallWindowProx 要用到老窗口过程,因此得把它保存到一个变量中以便使用。这个技巧和在子类化中用到的一样,只不过不是调用 SetWindowLong 而是直接修改 WNDCLASSEX 结构罢了。接下来必须得为这个新类取个名字。

 invoke RegisterClassEx, addr wc 

当所有这些都完成时,注册这个新类就会得到一个具有旧类某些特征的新类了。

              XOR       EBX,EBX
              MOV       EDI,20
.WHILE        EBX<6
              INVOKE    CREATEWINDOWEX,WS_EX_CLIENTEDGE,ADDR OURCLASS,NULL,\
WS_CHILD+WS_VISIBLE+WS_BORDER,20,\ 
edi,300,25,hWnd,ebx,\ 
hInstance,NULL 
              MOV       DWORD PTR [HWNDEDIT+4*EBX],EAX
              ADD       EDI,25
              INC       EBX
.ENDW
              INVOKE    SETFOCUS,HWNDEDIT

注册完新类就可以创建基于它的窗口了:
在上面的程序片断中,用寄存器 ebx 来保存已创建的窗口数目,用寄存器 edi 来保存窗口左上角的 y 坐标。创建一个新窗口时,把它的句柄保存在一个双字的数组中,当创建完所有的窗口后,设定输入焦点为所创建的第一个窗口。

这时已经有6个只能接受十六进制数字的 edit 窗口控件了,替换的窗口过程处理了字符过滤,这实际上和在子类化中的例子是一样的。但不必做子类化那些窗口的额外工作了。

在此程序中,通过使用 Tabs 键来在各个 Edit 控件中切换来使得这个程序更加有趣。一般来说,如果使用对话框,对话框管理器会处理好所有这些问题,即:
按下 Tabs 输入焦点切换到下一个控件窗口中,按下 Shift-Tabs 输入焦点切换到上一个控件窗口中;但一个简单的窗口不具有这个功能,必须子类化它们以处理 Tabs 键。在这个例子中,不必一个一个去子类化已经进行过超类化操作的这些控件,可以使用一种集中控制切换策略。

.ELSEIF       AL==VK_TAB
              INVOKE    GETKEYSTATE,VK_SHIFT
              TEST      EAX,80000000
.IF           ZERO?
              INVOKE    GETWINDOW,HEDIT,GW_HWNDNEXT
.IF           EAX==NULL
              INVOKE    GETWINDOW,HEDIT,GW_HWNDFIRST
.ENDIF
.ELSE
              INVOKE    GETWINDOW,HEDIT,GW_HWNDPREV
.IF           EAX==NULL
              INVOKE    GETWINDOW,HEDIT,GW_HWNDLAST
.ENDIF
.ENDIF
              INVOKE    SETFOCUS,EAX
              XOR       EAX,EAX
              RET

上面是摘自于 EditWndClass 过程的程序片断,它检查用户是否按下了 Tabs 键,若是就调用函数 GetKeyState 来检查 SHIFT 键是否也被同时按下了。函数 GetKeyState 在寄存器 eax 中设立一个返回值,用于判断某个特定的键是否被按下了,若按下了,则把 eax 的的最高位置1,否则把最高位清0。所以只要用 80000000h 来测试返回值就行了,若最高位是1则说明用户按下了 SHIFT-Tabs,这需要单独处理;否则说明只按下 Tabs 键,调用函数 GetWindow 来获得 hEdit 所指向窗口的下一个窗口句柄,若该函数返回 NULL ,说明这是当前窗口是窗口链中最后一个窗口了,应该通过以参数 GW_HWNDFIRST 调用函数 GetWindow 来卷回到窗口链中的第一个窗口控件。SHIFT-Tabs 的处理过程和这正好相反。