Windows 程序中,在写图形用户界面时需要调用大量的标准 Windows Gui 函数。其实这对用户和程序员来说都有好处,对于用户,面对的是同一套标准的窗口,对这些窗口的操作都是一样的,所以使用不同的应用程序时无须重新学习操作。对程序员来说,这些 Gui 源代码都是经过了微软的严格测试,随时拿来就可以用的。当然至于具体地写程序对于程序员来说还是有难度的。为了创建基于窗口的应用程序,必须严格遵守规范。作到这一点并不难,只要用模块化或面向对象的编程方法即可。
下面我就列出在桌面显示一个窗口的几个步骤:
得到您应用程序的句柄(必需); 得到命令行参数(如果您想从命令行得到参数,可选); 注册窗口类(必需,除非您使用 Windows 预定义的窗口类,如 MessageBox 或 dialog box; 产生窗口(必需); 在桌面显示窗口(必需,除非您不想立即显示它); 刷新窗口客户区; 进入无限的获取窗口消息的循环; 如果有消息到达,由负责该窗口的窗口回调函数处理; 如果用户关闭窗口,进行退出处理。 相对于单用户的 DOS 下的编程来说,Windows 下的程序框架结构是相当复杂的。但是 Windows 和 DOS 在系统架构上是截然不同的。Windows 是一个多任务的操作系统,故系统中同时有多个应用程序彼此协同运行。这就要求 Windows 程序员必须严格遵守编程规范,并养成良好的编程风格。
内容:
下面是我们简单的窗口程序的源代码。在进入复杂的细节前,我将提纲挈领地指出几点要点:
您应当把程序中要用到的所有常量和结构体的声明放到一个头文件中,并且在源程序的开始处包含这个头文件。这么做将会节省您大量的时间,也免得一次又一次的敲键盘。目前,最完善的头文件是 hutch 写的,您可以到 hutch 或我的网站下载。您也可以定义您自己的常量和结构体,但最好把它们放到独立的头文件中 用 includelib 指令,包含您的程序要引用的库文件,譬如:若您的程序要调用 "MessageBox",您就应当在源文件中加入如下一行: includelib user32.lib 这条语句告诉 MASM 您的程序将要用到一些引入库。如果您不止引用一个库,只要简单地加入 includelib 语句,不要担心链接器如何处理这么多的库,只要在链接时用链接开关 /LIBPATH 指明库所在的路径即可。 在其它地方运用头文件中定义函数原型,常数和结构体时,要严格保持和头文件中的定义一致,包括大小写。在查询函数定义时,这将节约您大量的时间; 在编译,链接时用makefile文件,免去重复敲键。 .386 .model flat,stdcall option casemap:none include \masm32\include\windows.inc include \masm32\include\user32.inc includelib \masm32\lib\user32.lib ; calls to functions in user32.lib and kernel32.lib include \masm32\include\kernel32.inc includelib \masm32\lib\kernel32.lib
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
.DATA ; initialized data ClassName db "SimpleWinClass",0 ; the name of our window class AppName db "Our First Window",0 ; the name of our window
.DATA? ; Uninitialized data hInstance HINSTANCE ? ; Instance handle of our program CommandLine LPSTR ? .CODE ; Here begins our code start: invoke GetModuleHandle, NULL ; get the instance handle of our program. ; Under Win32, hmodule==hinstance mov hInstance,eax mov hInstance,eax invoke GetCommandLine ; get the command line. You don't have to call this function IF ; your program doesn't process the command line. mov CommandLine,eax invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT ; call the main function invoke ExitProcess, eax ; quit our program. The exit code is returned in eax from WinMain.
WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD LOCAL wc:WNDCLASSEX ; create local variables on stack LOCAL msg:MSG LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX ; fill values in members of wc 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 wc ; register our window class invoke CreateWindowEx,NULL,\ ADDR ClassName,\ ADDR AppName,\ WS_OVERLAPPEDWINDOW,\ CW_USEDEFAULT,\ CW_USEDEFAULT,\ CW_USEDEFAULT,\ CW_USEDEFAULT,\ NULL,\ NULL,\ hInst,\ NULL mov hwnd,eax invoke ShowWindow, hwnd,CmdShow ; display our window on desktop invoke UpdateWindow, hwnd ; refresh the client area
WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM .IF uMsg==WM_DESTROY ; if the user closes our window invoke PostQuitMessage,NULL ; quit our application .ELSE invoke DefWindowProc,hWnd,uMsg,wParam,lParam ; Default message processing ret .ENDIF xor eax,eax ret WndProc endp
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 include \masm32\include\kernel32.inc includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib
您可以把前三行看成是"必须"的.
.386告诉MASN我们要用80386指令集。 . model flat,stdcall告诉MASM 我们用的内存寻址模式,此处也可以加入stdcall告诉MASM我们所用的参数传递约定。
.DATA "分段"包含了您应用程序的所有代码,这些代码必须都在 .code 和 end 之间。至于 label 的命名只要遵从 Windows 规范而且保证唯一则具体叫什么倒是无所谓。我们程序的第一条语句是调用 GetModuleHandle 去查找我们应用程序的句柄。在Win32下,应用程序的句柄和模块的句柄是一样的。您可以把实例句柄看成是您的应用程序的 ID 号。我们在调用几个函数是都把它作为参数来进行传递,所以在一开始便得到并保存它就可以省许多的事。
特别注意:WIN32下的实例句柄实际上是您应用程序在内存中的线性地址。
WIN32 中函数的函数如果有返回值,那它是通过 eax 寄存器来传递的。其他的值可以通过传递进来的参数地址进行返回。一个 WIN32 函数被调用时总会保存好段寄存器和 ebx,edi,esi和ebp 寄存器,而 ecx和edx 中的值总是不定的,不能在返回是应用。特别注意:从 Windows API 函数中返回后,eax,ecx,edx 中的值和调用前不一定相同。当函数返回时,返回值放在eax中。如果您应用程序中的函数提供给 Windows 调用时,也必须尊守这一点,即在函数入口处保存段寄存器和 ebx,esp,esi,edi 的值并在函数返回时恢复。如果不这样一来的话,您的应用程序很快会崩溃。从您的程序中提供给 Windows 调用的函数大体上有两种:Windows 窗口过程和 Callback 函数。
上面是WinMain的定义。注意跟在 proc 指令后的parameter:type形式的参数,它们是由调用者传给 WinMain 的,我们引用是直接用参数名即可。至于压栈和退栈时的平衡堆栈工作由 MASM 在编译时加入相关的前序和后序汇编指令来进行。 LOCAL wc:WNDCLASSEX LOCAL msg:MSG LOCAL hwnd:HWND LOCAL 伪指令为局部变量在栈中分配内存空间,所有的 LOCAL 指令必须紧跟在 PROC 之后。LOCAL 后跟声明的变量,其形式是 变量名:变量类型。譬如 LOCAL wc:WNDCLASSEX 即是告诉 MASM 为名字叫 wc 的局部边量在栈中分配长度为 WNDCLASSEX 结构体长度的内存空间,然后我们在用该局部变量是无须考虑堆栈的问题,考虑到 DOS 下的汇编,这不能不说是一种恩赐。不过这就要求这样申明的局部变量在函数结束时释放栈空间,(也即不能在函数体外被引用),另一个缺点是您因不能初始化您的局部变量,不得不在稍后另外再对其赋值。
WNDCLASSEX 中最重要的成员莫过于lpfnWndProc了。前缀 lpfn 表示该成员是一个指向函数的长指针。在 Win32中由于内存模式是 FLAT 型,所以没有 near 或 far 的区别。每一个窗口类必须有一个窗口过程,当 Windows 把属于特定窗口的消息发送给该窗口时,该窗口的窗口类负责处理所有的消息,如键盘消息或鼠标消息。由于窗口过程差不多智能地处理了所有的窗口消息循环,所以您只要在其中加入消息处理过程即可。下面我将要讲解 WNDCLASSEX 的每一个成员