第22课 超类化
在这一讲我们将学习什么是超类化以及它有什么作用;同时你还会学到怎样在自己的窗口中用Tab键在控件中切换这一技巧。
理论:
在你的程序生涯中你肯定遇到过这样的情况,你需要一系列的控件,但它们之间却只有一点点的不同。例如,你可能需要10个只接受数字的 Edit 控件,当然你可以通过多种方法来达到这个目的。
创建自己的类并用它实例化为那些控件
创建那些 Edit 控件并把它们全部子类化
超类化Edit 控件
第一种方法太乏味了,因为你必须自己实现Edit 控件的每个功能,但这项工作不是轻松就能完成的。第二种方法好于第一种,但仍然要做许多工作,子类化几个Edit 控件还可以接受,但若要子类化十几二十个,这项工作简直就是一场恶梦。在这种情况下就应该使用超类化这个技巧,它是用于控制某一个特定窗口类的特殊方法。通过这种控制就可以修改窗口类的特性使之符合你的要求,然后再创建那一堆控件就可以了。
超类化有如下几个步骤:
通过调用 GetClassInfoEx 来获得想要进行超类化操作的窗口类的信息。函数GetClassInfoEx 需要一个指向 WNDCLASSEX 结构的指针,用于当成功返回时填入窗口类的信息。
按需要修改 WNDCLASSEX 结构的成员,其中有两个成员必须修改:
hInstance 存放程序的实例句柄
lpszClassName 指向一个新类名的指针
不必修改成员 lpfnWndProc,但大多数情况下还是需要的。但要记住如果要使用函数 CallWindowProc 调用老窗口的过程,那就必须保存成员 lpfnWndProc 的原值。
注册修改完的 WNDCLASSEX 结构,得到一个具有旧窗口类某些特性的新窗口类。
用新窗口类创建窗口
如果要创建具有相同特性的多个控件,超类化就比子类化要好。
举例:
.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 的处理过程和这正好相反。