Windows 程序中,在写图形用户界面时需要调用大量的标准 Windows Gui 函数。其实这对用户和程序员来说都有好处,对于用户,面对的是同一套标准的窗口,对这些窗口的操作都是一样的,所以使用不同的应用程序时无须重新学习操作。对程序员来说,这些 Gui 源代码都是经过了微软的严格测试,随时拿来就可以用的。当然至于具体地写程序对于程序员来说还是有难度的。为了创建基于窗口的应用程序,必须严格遵守规范。作到这一点并不难,只要用模块化或面向对象的编程方法即可。
下面我就列出在桌面显示一个窗口的几个步骤:
下面列出简单的窗口程序的源代码:
1: .386
2: .model flat,stdcall
3: option casemap:none
4:
5: WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
6:
7: include /masm32/include/windows.inc
8: include /masm32/include/user32.inc
9: includelib /masm32/lib/user32.lib
10: include /masm32/include/kernel32.inc
11: includelib /masm32/lib/kernel32.lib
12:
13: .DATA
14: ClassName db "SimpleWinClass",0
15: AppName db "Our First Window",0
16: OurText db "Win32 assembly is great and easy!",0
17:
18: .DATA?
19: hInstance HINSTANCE ?
20: CommandLine LPSTR ?
21:
22: .CODE
23: start:
24: invoke GetModuleHandle, NULL
25: mov hInstance,eax
26: invoke GetCommandLine
27: mov CommandLine,eax
28: invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
29: invoke ExitProcess,eax
30:
31: WinMain proc hInst:HINSTANCE, hPrevInst:HINSTANCE, CmdLine:LPSTR, CmdShow:DWORD
32: LOCAL wc:WNDCLASSEX
33: LOCAL msg:MSG
34: LOCAL hwnd:HWND
35: mov wc.cbSize,SIZEOF WNDCLASSEX
36: mov wc.style, CS_HREDRAW or CS_VREDRAW
37: mov wc.lpfnWndProc, OFFSET WndProc
38: mov wc.cbClsExtra,NULL
39: mov wc.cbWndExtra,NULL
40: push hInst
41: pop wc.hInstance
42: mov wc.hbrBackground,COLOR_WINDOW+1
43: mov wc.lpszMenuName,NULL
44: mov wc.lpszClassName,OFFSET ClassName
45: invoke LoadIcon,NULL,IDI_APPLICATION
46: mov wc.hIcon,eax
47: mov wc.hIconSm,eax
48: invoke LoadCursor,NULL,IDC_ARROW
49: mov wc.hCursor,eax
50: invoke RegisterClassEx, addr wc
51: invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,/
52: WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,/
53: CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,/
54: hInst,NULL
55: mov hwnd,eax
56: invoke ShowWindow, hwnd,SW_SHOWNORMAL
57: invoke UpdateWindow, hwnd
58: .WHILE TRUE
59: invoke GetMessage, ADDR msg,NULL,0,0
60: .BREAK .IF (!eax)
61: invoke TranslateMessage, ADDR msg
62: invoke DispatchMessage, ADDR msg
63: .ENDW
64: mov eax,msg.wParam
65: ret
66: WinMain endp
67:
68: WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
69: LOCAL hdc:HDC
70: LOCAL ps:PAINTSTRUCT
71: LOCAL rect:RECT
72: .IF uMsg==WM_DESTROY
73: invoke PostQuitMessage,NULL
74: .ELSEIF uMsg==WM_PAINT
75: invoke BeginPaint,hWnd, ADDR ps
76: mov hdc,eax
77: invoke GetClientRect,hWnd, ADDR rect
78: invoke DrawText, hdc,ADDR OurText,-1, ADDR rect, /
79: DT_SINGLELINE or DT_CENTER or DT_VCENTER
80: invoke EndPaint,hWnd, ADDR ps
81: .ELSE
82: invoke DefWindowProc,hWnd,uMsg,wParam,lParam
83: ret
84: .ENDIF
85: xor eax, eax
86: ret
87: WndProc endp
88: end start
分析:
看到一个简单的 Windows 程序有这么多行,您是不是有点想撤? 但是您必须要知道的是上面的大多数代码都是模板而已,模板的意思即是指这些代码对差不多所有标准 Windows 程序来说都是相同的。在写 Windows 程序时您可以把这些代码拷来拷去,当然把这些重复的代码写到一个库中也挺好。其实真正要写的代码集中在 WinMain 中。这和一些 C 编译器一样,无须要关心其它杂务,集中精力于 WinMain 函数。唯一不同的是 C 编译器要求您的源代码有必须有一个函数叫 WinMain。否则 C 无法知道将哪个函数和有关的前后代码链接。相对C,汇编语言提供了较大的灵活性,它不强行要求一个叫 WinMain 的函数。
.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include /masm32/include/windows.inc
include /masm32/include/user32.inc
includelib /masm32/lib/user32.lib
include /masm32/include/kernel32.inc
includelib /masm32/lib/kernel32.lib
前三行是必须的,接下来的一行是函数WinMain的原型声明,因为我们稍后要用到该函数,故必须先声明。window.inc 文件包含大量要用到的常量和结构的定义。
用 includelib 指令,包含您的程序要引用的库文件,譬如:若您的程序要调用 "MessageBox", 您就应当在源文件中加入如下一行: includelib user32.lib 这条语句告诉 MASM 您的程序将要用到一些引入库。如果您不止引用一个库,只要简单地加入 includelib 语句,不要担心链接器如何处理这么多的库,只要在链接时用链接开关 /LIBPATH 指明库所在的路径即可。我们的程序调用驻扎在 user32.dll (譬如:CreateWindowEx, RegisterWindowClassEx) 和 kernel32.dll (ExitProcess)中的函数,所以必须链接这两个库。
.DATA
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
.DATA?
hInstance HINSTANCE ?
CommandLine LPSTR ?
接下来是DATA"分段"。 在 .DATA 中我们定义了两个以 NULL 结尾的字符串 (ASCIIZ):其中 ClassName 是 Windows 类名,AppName 是我们窗口的名字。这两个变量都是初始化了的。未进行初始化的两个边量放在 .DATA? "分段"中,其中 hInstance 代表应用程序的句柄,CommandLine 保存从命令行传入的参数。HINSTACE 和 LPSTR 是两个数据类型名,它们在头文件中定义,可以看做是 DWORD 的别名,之所以要这么重新定仅是为了易记。在.DATA?中的的变量都是未初始化的。
.CODE
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax
.....
end start
.CODE"分段"包含您应用程序的所有代码。程序的第一条语句是调用GetModuleHandle去查找我们应用程序的句柄。在Win32下应用程序的句柄和模块的句柄是一样的。您可以把实例句柄看成是你的应用程序的ID号。我们在调用几个函数时都把它作为参数来传递。
特别注意:WIN32下的实例句柄实际上是您应用程序在内存中的线性地址。
WIN32 中函数的函数如果有返回值,那它是通过 eax 寄存器来传递的。其他的值可以通过传递进来的参数地址进行返回。
如果您的应用程序不处理命令行那么就无须调用 GetCommandLine,这里只是告诉您如果要调用应该怎么做。
下面则是调用WinMain了。该函数共有4个参数:应用程序的实例句柄,该应用程序的前一实例句柄,命令行参数串指针和窗口如何显示。Win32 没有前一实例句柄的概念,所以第二个参数总为0。之所以保留它是为了和 Win16 兼容的考虑,在 Win16下,如果 hPrevInst 是 NULL,则该函数是第一次运行。特别注意:您不用必须申明一个名为 WinMain 函数,事实上在这方面您可以完全作主,您甚至无须有一个和 WinMain 等同的函数。您只要把 WinMain 中的代码拷到GetCommandLine 之后,其所实现的功能完全相同。在 WinMain 返回时,把返回码放到 eax 中。然后在应用程序结束时通过 ExitProcess 函数把该返回码传递给 Windows 。
WinMain proc Inst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
上面是WinMain的定义。注意跟在 proc 指令后的parameter:type形式的参数,它们是由调用者传给 WinMain 的,我们引用是直接用参数名即可。至于压栈和退栈时的平衡堆栈工作由 MASM 在编译时加入相关的前序和后序汇编指令来进行 LOCAL wc:W
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
LOCAL 伪指令为局部变量在栈中分配内存空间,所有的 LOCAL 指令必须紧跟在 PROC 之后。LOCAL 后跟声明的变量,其形式是 变量名:变量类型。譬如 LOCAL wc:WNDCLASSEX 即是告诉 MASM 为名字叫 wc 的局部边量在栈中分配长度为 WNDCLASSEX 结构体长度的内存空间,然后我们在用该局部变量是无须考虑堆栈的问题,这样申明的局部变量在函数结束时释放栈空间。
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 hInstance
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
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 w
上面几行从概念上说确实是非常地简单。只要几行指令就可以实现。其中的主要概念就是窗口类(window class),一个窗口类就是一个有关窗口的规范,这个规范定义了几个主要的窗口的元素,如:图标、光标、背景色、和负责处理该窗口的函数。您产生一个窗口时就必须要有这样的一个窗口类。如果您要产生不止一个同种类型的窗口时,最好的方法就是把这个窗口类存储起来,这种方法可以节约许多的内存空间。如果您要定义自己的创建窗口类就必须:在一个 WINDCLASS 或 WINDOWCLASSEXE 结构体中指明您窗口的组成元素,然后调用 RegisterClass 或 RegisterClassEx ,再根据该窗口类产生窗口。对不同特色的窗口必须定义不同的窗口类。 WINDOWS有几个预定义的窗口类,譬如:按钮、编辑框等。要产生该种风格的窗口无须预先再定义窗口类了,只要包预定义类的类名作为参数调用 CreateWindowEx 即可。
WNDCLASSEX 中最重要的成员莫过于lpfnWndProc了。前缀 lpfn 表示该成员是一个指向函数的长指针。在 Win32中由于内存模式是 FLAT 型,所以没有 near 或 far 的区别。每一个窗口类必须有一个窗口过程,当 Windows 把属于特定窗口的消息发送给该窗口时,该窗口的窗口类负责处理所有的消息,如键盘消息或鼠标消息。由于窗口过程差不多智能地处理了所有的窗口消息循环,所以您只要在其中加入消息处理过程即可。下面讲解 WNDCLASSEX 的每一个成员:
WNDCLASSEX STRUCT DWORD
cbSize DWORD ?
style DWORD ?
lpfnWndProc DWORD ?
cbClsExtra DWORD ?
cbWndExtra DWORD ?
hInstance DWORD ?
hIcon DWORD ?
hCursor DWORD ?
hbrBackground DWORD ?
lpszMenuName DWORD ?
lpszClassName DWORD ?
hIconSm DWORD ?
WNDCLASSEX ENDS
invoke CreateWindowEx, NULL,/
ADDR ClassName,/
ADDR AppName,/
WS_OVERLAPPEDWINDOW,/
CW_USEDEFAULT,/
CW_USEDEFAULT,/
CW_USEDEFAULT,/
CW_USEDEFAULT,/
NULL,/
NULL,/
hInst,/
NULL
注册窗口类后,我们将调用CreateWindowEx来产生实际的窗口。请注意该函数有12个参数。
CreateWindowExA proto dwExStyle:DWORD,/
lpClassName:DWORD,/
lpWindowName:DWORD,/
dwStyle:DWORD,/
X:DWORD,/
Y:DWORD,/
nWidth:DWORD,/
nHeight:DWORD,/
hWndParent:DWORD ,/
hMenu:DWORD,/
hInstance:DWORD,/
lpParam:DWORD
下面是参数的解释:
mov hwnd,eax
invoke ShowWindow, hwnd,CmdShow
invoke UpdateWindow, hwnd
调用CreateWindowEx成功后,窗口句柄在eax中。我们必须保存该值以备后用。我们刚刚产生的窗口不会自动显示,所以必须调用 ShowWindow 来按照我们希望的方式来显示该窗口。接下来调用 UpdateWindow 来更新客户区。
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
这时候我们的窗口已显示在屏幕上了。但是它还不能从外界接收消息。所以我们必须给它提供相关的消息。我们是通过一个消息循环来完成该项工作的。每一个模块仅有一个消息循环,我们不断地调用 GetMessage 从 Windows 中获得消息。GetMessage 传递一个 MSG 结构体给 Windows ,然后 Windows 在该函数中填充有关的消息,一直到 Windows 找到并填充好消息后 GetMessage 才会返回。在这段时间内系统控制权可能会转移给其他的应用程序。这样就构成了Win16 下的多任务结构。如果 GetMessage 接收到 WM_QUIT 消息后就会返回 FALSE,使循环结束并退出应用程序。TranslateMessage 函数是一个是实用函数,它从键盘接受原始按键消息,然后解释成 WM_CHAR,在把 WM_CHAR 放入消息队列,由于经过解释后的消息中含有按键的 ASCII 码,这比原始的扫描码好理解得多。如果您的应用程序不处理按键消息的话,可以不调用该函数。DispatchMessage 会把消息发送给负责该窗口过程的函数。
mov eax,msg.wParam
ret
WinMain endp
如果消息循环结束了,退出码存放在 MSG 中的 wParam中,您可以通过把它放到 eax 寄存器中传给 Windows目前 Windows 没有利用到这个结束码,但我们最好还是遵从 Windows 规范已防意外。
WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
WndProc是我们的窗口处理函数。您可以随便给该函数命名。其中第一个参数 hWnd 是接收消息的窗口的句柄。uMsg 是接收的消息。注意 uMsg 不是一个 MSG 结构,其实上只是一个 DWORD 类型数。Windows 定义了成百上千个消息,大多数您的应用程序不会处理到。当有该窗口的消息发生时,Windows 会发送一个相关消息给该窗口。其窗口过程处理函数会智能的处理这些消息。wParam 和 lParam 只是附加参数,以方便传递更多的和该消息有关的数据。
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
上面可以说是关键部分。这也是我们写 Windows 程序时需要改写的主要部分。此处您的程序检查 Windows 传递过来的消息,如果是我们感兴趣的消息则加以处理,处理完后,在 eax 寄存器中传递 0,否则必须调用 DefWindowProc,把该窗口过程接收到的参数传递给缺省的窗口处理函数。所有消息中您必须处理的是 WM_DESTROY,当您的应用程序结束时 Windows 把这个消息传递进来,当您的应用程序解说到该消息时它已经在屏幕上消失了,这仅是通知您的应用程序窗口已销毁,您必须自己准备返回 Windows 。在此消息中您可以做一些清理工作,但无法阻止退出应用程序。如果您要那样做的话,可以处理 WM_CLOSE 消息。在处理完清理工作后,您必须调用 PostQuitMessage,该函数会把 WM_QUIT 消息传回您的应用程序,而该消息会使得 GetMessage 返回,并在 eax 寄存器中放入 0,然后会结束消息循环并退回 WINDOWS。您可以在您的程序中调用 DestroyWindow 函数,它会发送一个 WM_DESTROY 消息给您自己的应用程序,从而迫使它退出。