ITEEDU

第二十课 窗口子类化

在这一讲,我们将学习什么是窗口子类化和怎样按你所想要的方式方便地使用它。

理论:

如果你曾经在 Windows 环境下编过程序,有时候就会发现:有一个现成的窗口,几乎有你所需要的全部功能,但还不完全一样(否则就没有必要讲这一节了)。你曾遇到过这样的处境吗,如果你需要一个具有过滤特殊字符功能的 Edit 控件。当然最直接的方法就是自己用代码来实现,但这的确是一个费时又很困难的任务,而窗口子类化就可以用来做这种事情。

窗口子类化允许你接管被子类化的窗口,使你对它有绝对的控制权。举个例子了来阐明一下:例如你需要一个只接受十六进制数字输入的文本编辑框,如果使用一个简单的 Edit控件,当用户输入十六进制以外的字符时,你既不知道也无计可施。也就是说,当用户进文本框中输入字符串 "zb+q*" 时,如果除了拒绝接受整个字符串以外几乎什么也不能做,至少这显得特别不专业。重要的是,你需要具有输入检测的能力,即每当用户输入一个字符到编辑框中时要能检测这个字符。

现在来解释实现细节:当用户往文本框中输入字符时,Windows 会给Edit控件的窗口函数发送 WM_CHAR 消息。这个窗口函数本身寄生于 Windows 中,因此不能直接修改它。但是我们可以重定向这个消息使之发送到我们自己编写的窗口处理函数。如果自定义窗口要处理这个消息那就可以处理它,如果不处理就可以把这个消息转发到它原来窗口处理函数。通过这种方式,自定义的窗口处理函数就把它自己插入到 Windows 系统和 Edit 控件之间。

看下面的流程:
窗口子类化之前
Windows ==>Edit 控件的窗口处理函数。

子类化之后
Windows ==>自定义的窗口处理函数==> Edit 控件的窗口处理函数。

注意子类化并不局限于控件,可以子类化任何窗口,现在我们要把精力集中到怎样实现子类化一个窗口上。让我们想想Windows 怎样知道 Edit 控件的窗口处理函数放在什么地方。猜的?…肯定不是。原来 WNDCLASSEX 结构的成员 lpfnWndProc 指出了窗口函数地址。如果能用自己编写的窗口函数的地址来替换这个成员变量,那 Windows 不就把消息发到自定义的窗口函数了吗! 我们通过调用函数SetWindowLong 来实现这个任务,此函数的原型为:

SetWindowLong PROTO hWnd:DWORD, nIndex:DWORD, dwNewLong:DWORD
  • hWnd = 将要实施子类化的窗口的句柄
  • nIndex = 函数了功能索引
    GWL_EXSTYLE 设置窗口的扩展风格.
    GWL_STYLE 设置新的窗口风格
    GWL_WNDPROC 设置新的窗口处理函数地址
    GWL_HINSTANCE 设置新的应用程序句柄
    GWL_ID 设置新的窗口标识
    GWL_USERDATA 设置一个与这个窗口相关的给用户使用的32位的数据
  • dwNewLong = 用来更新的数据
  • 我们的工作还是比较简单的:

    1. 写一个窗口函数用于处理发给 Edit 控件的消息。
    2. 用参数GWL_WNDPROC调用SetWindowLong 函数,如果调用成功那么返回值就是与调用功能相联系的一个32位的整数

    在我们的程序中,返回值就是原先窗口函数的地址。我们要保存这个值以便以后使用。 记住:有一些我们不处理的消息,需要把它们派遣给原来的窗口函数来处理,这就用到另外一个函数 CallWindowProc, 函数原型为:

     CallWindowProc PROTO lpPrevWndFunc:DWORD, hWnd:DWORD, Msg:DWORD,wParam:DWORD, lParam:DWORD 

    lpPrevWndFunc = 窗口原来函数的地址. 剩下的四个参数就是发给自定义函数的参数,直接把它们传给函数 CallWindowProc 就行了。

    代码举例:

    .386
    .MODEL        FLAT,STDCALL
    option casemap:none 
         INCLUDE  \MASM32\INCLUDE\WINDOWS.INC
         INCLUDE  \MASM32\INCLUDE\USER32.INC
         INCLUDE  \MASM32\INCLUDE\KERNEL32.INC
         INCLUDE  \MASM32\INCLUDE\COMCTL32.INC
      INCLUDELIB  \MASM32\LIB\COMCTL32.LIB
      INCLUDELIB  \MASM32\LIB\USER32.LIB
      INCLUDELIB  \MASM32\LIB\KERNEL32.LIB
    WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD 
    EditWndProc PROTO :DWORD,:DWORD,:DWORD,:DWORD 
    
    .DATA
       CLASSNAME  DB        "SUBCLASSWINCLASS",0
         APPNAME  DB        "SUBCLASSING DEMO",0
       EDITCLASS  DB        "EDIT",0
         MESSAGE  DB        "YOU PRESSED ENTER IN THE TEXT BOX!",0
    
    .DATA?
    hInstance  HINSTANCE ? 
        HWNDEDIT  DD        ?
      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,ADDR CLASSNAME,ADDR APPNAME,\
     WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE,CW_USEDEFAULT,\ 
               CW_USEDEFAULT,350,200,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      HWND:HWND, UMSG:UINT, WPARAM:WPARAM, LPARAM:LPARAM
    .IF           UMSG==WM_CREATE
                  INVOKE    CREATEWINDOWEX,WS_EX_CLIENTEDGE,ADDR EDITCLASS,NULL,\
                WS_CHILD+WS_VISIBLE+WS_BORDER,20,\ 
                20,300,25,hWnd,NULL,\ 
                hInstance,NULL 
                  MOV       HWNDEDIT,EAX
                  INVOKE    SETFOCUS,EAX
            ;----------------------------------------- 
            ; Subclass it! 
            ;----------------------------------------- 
                  INVOKE    SETWINDOWLONG,HWNDEDIT,GWL_WNDPROC,ADDR EDITWNDPROC
                  MOV       OLDWNDPROC,EAX
    .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
    .IF           AL>="A" && AL<="F"
                  SUB       AL,20H
    .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
    .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
    

    分析:

    invoke SetWindowLong,hwndEdit,GWL_WNDPROC,addr EditWndProc
    mov OldWndProc,eax 

    在创建 Edit 控件后,通过调用 SetWindowLong 把原来的窗口函数地址替换为自定义函数的地址,从而对它实施了窗口子类化,要注意 为了调用函数 CallWindowProc,我们存储了原窗口函数地址,自已编写的EditWndProc 仅仅是个普普通通的窗口函数。当然也可以再调用一次 SetWindowLong 函数来存储这个32位的值,

    invoke SetWindowLong ,hwndEdit,GWL_USERDATA,eax 。

    当然用的时候就要调用GetWindowLong 来取回这个值。

      .if uMsg==WM_CHAR 
            mov eax,wParam 
            .if (al>="0" && al<="9") || (al>="A" && al<="F") || (al>="a" && al<="f") || al==VK_BACK 
                .if al>="a" && al<="f" 
                    sub al,20h 
                .endif 
                invoke CallWindowProc,OldWndProc,hEdit,uMsg,eax,lParam 
                ret 
            .endif 
    
    在函数 EditWndProc 中,我们自己处理了WM_CHAR消息: 如果输入的字符是'0'--'9'、'A'-'F'或者是'a'--'f'就接受,并且把此消息转发给原窗口函数,其中若输入的是小写的'a'--'f'就把它变为大写。如果输入的不是十六进制字符,就丢掉它,并且也不转发此消息。因此当输入是非十六进制字符时,这个字符就不会显示在 Edit 控件中。
        .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 
            .else 
                invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam 
                ret 
            .end 
    
    在这里我们通过处理 回车(Enter) 键进一步示范了子类化的能力。EditWndProc 通过检查 WM_KEYDONW 消息来判断是否是 回车键,若是显示提示消息框,否则转发此消息。 你可以用窗口子类化来控制另外的窗口,这是必须掌握的十分有用的技术之一。