分析窗口程序
了解了消息驱动体系的工作流程以后,让我们来分析如何用Win32汇编实现这一切。
模块和句柄
模块的概念
一个模块代表的是一个运行中的exe文件或dll文件,用来代表这个文件中所有的代码和资源,磁盘上的文件不是模块,装入内存后运行时就叫做模块。
一个应用程序调用其他DLL中的API时,这些DLL文件被装入内存,就产生了不同的模块,为了区分地址空间中的不同模块,每个模块都有一个唯一的模块句柄来标识。
很多API函数中都要用到程序的模块句柄,以便利用程序中的各种资源,所以在程序中一开始就先取得模块句柄并存放到一个全局变量中可以省去很多的麻烦,在Win32中,模块句柄在数值上等于程序在内存中装入的起始地址。
取模块句柄使用的API函数是GetModuleHandle,它的使用方法是:
invoke GetModuleHandle, lpModuleName
lpModuleName参数是一个指向含有模块名称字符串的指针,可以用这个函数取得程序地址空间中各个模块的句柄,例如,如果想得到User32.dll的句柄以便使用其中包含的图标资源,那么可以如下使用:
szUserDll db ‘User32.dll’,0
invoke GetModuleHandle, addr szUserDll
.if eax
mov hUserDllHandle,eax
.endif
如果使用参数NULL,调用GetModuleHandle,那么得到的是调用者本模块的句柄,如下所示:
invoke GetModuleHandl,NULL
mov hInstance,eax
可以注意到,把返回的句柄放到了hInstance变量里而并不是放在hModule中,为什么是hInstance呢?Instance是“实例”,它的概念来自于Win16,Win16中不同运行程序的地址空间并不是完全隔离的,一个可执行文件运行后形成“模块”,多次加载同一个可执行文件时,这个“模块”是公用的,为了区分多次加载的“拷贝”,就把每个“拷贝”叫做实例,每个实例均用不同的“实例句柄”(hInstance)值来标识它们。
但在Win32中,程序运行时是隔离的,每个实例都使用自己私有的4GB空间,都认为自己是唯一的,不存在一个模块的多个实例的问题,实际上在Win32中,实例句柄就是模块句柄,但很多API原型中用到模块句柄的时候使用的名称还是沿用hInstance,所以我们还是把变量名称取为hInstance。
在C++语言的编程中,hInstance通过WinMain由系统传入,WinMain的原型是:
WinMain(hInstance, hPrevInstance, lpszCmdParam, nCmdShow)
程序不用自己去获得hInstance,但在Win32汇编中必须自己获取,如果不了解hModule就是hInstance的话,就无法得知如何得到hInstance,因为并没有一个类似于GetInstanceHandle之类的API函数。
句柄是什么
随着分析的深入,句柄(handle)一词也出现得频繁了起来,“句柄”是什么呢?句柄只是一个数值而已,它的值对程序来说是没有意义的,它只是Windows用来表示各种资源的编号而已,所以只有Windows才知道怎么使用它来引用各种资源。
举例说明,屏幕上已经有10窗口,Windows把它们从1到10编号,应用程序又建立了一个窗口,现在Windows把它编号为11,然后把11当做窗口句柄返回给应用程序,应用程序并不知道11代表的是什么,但在操作窗口的时候,把11当做句柄传给Windows,Windows自然可以根据这个数值查出是哪个窗口。当该窗口关闭的时候,11这个编号作废。第二次运行的时候,如果屏幕上现有5个窗口,那么现在句柄可能就是6了,所以,应用程序并不用关心句柄的具体数值是多少。打个比方,可以把句柄当做是商场中寄放书包时营业员给的纸条,纸条上的标记用户并不知道是什么意思,但把它交还给营业员的时候,她自然会找到正确的书包。
Windows中几乎所有的东西都是用句柄来标识的,文件句柄、窗口句柄、线程句柄和模块句柄等,同样道理,不必关心它们的值究竟是多少,拿来用就是了!
创建窗口
在创建窗口之前,先要谈到“类”。“类”的概念读者都不陌生,主要是为了把一组物体的相同属性归纳整理起来封装在一起,以便重复使用,在“类”已定义的属性基础上加上其他个性化的属性,就形成了各式各样的个体。
Windows中创建窗口同样使用这样的层次结构。首先定义一个窗口类,然后在窗口类的基础上添加其他的属性建立窗口。不同一步到位的办法是因为很多窗口的基本属性和行为都是一样的,如按钮、文本输入框和选择框等,对这些东西Windows都预定义了对应的类,使用时直接使用对应的类名建立窗口就可以了。只有用户自定义的窗口才需要先定义自己的类,再建立窗口。这样可以节省资源。
注册窗口类
建立窗口类的方法是在系统中注册,注册窗口类的API函数是RegisterClassEx,最后的Ex是扩展的意思,因为它是Win16中RegisterClass的扩展。一个窗口类定义了窗口的一些主要属性,如:图标、光标、背景色、菜单和负责处理该窗口所属消息的函数。这些属性并不是分成多个参数传递过去的,而是定义在一个WNDCLASSEX结构中,再把结构的地址当参数一次性传递给RegisterClassEx,WNDCLASSEX是WNDCLASS结构的扩展。
WNDCLASSEX的结构定义为:
WNDCLASSEX STRUCT
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
在Win32汇编源程序中,注册窗口类的代码如下:
local @stWndClass:WNDCLASSEX ;定义一个WNDCLASSEX结构
invoke RtlZeroMemory, addr @stWndClass, sizeof @stWndClass
invoke LoadCursor, 0, IDC_ARROW
mov @stWndClass.hCursor, eax
push hInstance
pop @stWndClass.hInstance
mov @stWndClass.cbSize, sizeof WNDCLASSEX
mov @stWndClass.style, CS_HREDRAW or CS_VREDRAW
mov @stWndClass.lpfnWndProc, offset _ProcWinMain
mov @stWndClass.hbrBackground, COLOR_WINDOW + 1
mov @stWndClass.lpszClassName, offset szClassName
invoke RegisterClassEx, addr @stWndClass
程序定义了一个WNDCLASSEX结构的变量@stWndClass,用RtlZeroMemory将它填为全零,再填写结构的各个字段,这样,没有赋值的部分就保持为0,结构各字段的含义如下:
hIcon 图标句柄,指定显示在窗口标题栏左上角的图标。Windows已经预定课外作业了一些图标,同样,程序也可以使用在资源文件中定义的图标,这些图标的句柄可以用LoadIcon函数获得。例子程序没有用到图标,所以Windows给窗口显示了一个默认的图标。
hCursor 光标句柄,指定了鼠标在窗口中光标形状。同样,Windows也预定义了一些光标,可以用LoadCursor获取它们的句柄,IDC_ARROW是Windows预定义的箭头光标,如果想使用自定义的光标,也可以自己在资源文件中定义。
lpszMenuName 指定窗口上显示的默认菜单,它指向一个字符串,描述资源文件中菜单的名称,如果资源文件中菜单是用数值定义的,那么这里使用菜单资源的数值。窗口中的菜单也可以在建立窗口函数CreateWindowsEx的参数中指定。如果在两个地方都没有指定,那么建立的窗口上就没有菜单。
hInstance 指定要注册的窗口类属于哪个模块,模块句柄在程序开始的地方已经用GetModuleHandle函数获得。
cbSize 指定WNDCLASSEX结构的长度,用sizeof伪操作符来获取。很多Win32 API参数中的结构都有cbSize字段,它主要是用来区分结构的版本,当以后新增了一个字段时,cbSize就相应增大,如果调用的时候cbSize还是老的长度,表示运行的是基于旧结构的程序,这样可以防止使用无效的字段。
style 窗口风格。CS_HREDRAW和CS_VREDRAW表示窗口的宽度或高度改变时是否重画窗口。比较重要的是CS_DBLCLKS风格,指定了它,Windows才会把在窗口中快速两次单击鼠标的行为翻译为双击消息WM_LBUTTONDBLCLK发给窗口过程。
hbrBackground 窗口客户区的背景色。前面的hbr表示它是一个刷子(Brush)的句柄,刷子一词形象地表示了填充一个区域的着色模式。Windows预定义了一些刷子,如BLACK_BRUSH和WHITE_BRUSH等,可以用下列语句来得到它们的句柄:
invoke GetStockObject, WHITE_BRUSH
但在这里也可以使用颜色值,Windows已经预定义了一些颜色值,分别对应窗口各部分的颜色,如COLOR_BACKGROUND,COLOR_HIGHLIGHT,COLOR_MENU和COLOR_WINDOWS等,使用颜色值的时候,Windows规定必须在颜色值上加1,所以在程序中的指令是:
mov @stWndClass.hbrBackground, COLOR_WINDOWS + 1
lpszClassName指定程序员要建立的类命名,以便以后用这个名称来引用它。这个字段是一个字符串指针,在程序里,它指向MyClass字符串中。
cbWndExtra和cbClsExtra分别是在Windows内部保存的窗口结构和类结构中给程序员预留的空间大小,用来存放自定义的数据,它们的单位是字节。不使用自定义数据的话,这两个字段就是0。
lpfnWndProc 是最重要的参数,它指定了基于这个类建立的窗口的窗口过程地址。通过这个参数,Windows就知道了在DispatchMessage函数中把窗口消息发到哪里去,一个窗口过程可以为多个窗口服务,只要这些窗口是基于同一个窗口类建立的。Windows中不同应用程序中的按钮和文本框的行为都是一样的,就是因为它们是基于相同的Windows预定义类建立的,它们背后的窗口过程其实是同一段代码。
结构中的style表示窗口的风格,Windows已经有一些预定义的值,它们是以CS_(Class Style的缩写)开始的标识符,如下所示:
CS_VREDRAW 00000001H
CS_HREDRAW 00000002H
CS_KEYCVTWINDOWS 00000004H
CS_DBLCLKS 00000008H
CS_OWNDC 00000020H
CS_CLASSDC 00000040H
可以看到,这些预定义值实际上在使用不重复的数据位,所以可以组合起来使用,同时使用不同的预定义值并不会引起混淆。
注意:对于不同二进制位组合的计算,“加”和“或”的结果是一样的,在FirstWindows程序中用CS_HREDRAW or CS_VREDRAW来代表两个组合,若用CS_HREDRAW + CS_VREDRAW也并没有什么不同,但强烈建议使用or,因为如果不小心指定了两个同样的风格时:CS_HREDRAW or CS_VREDRAW or CS_VREDRAW和原来的数值是一样的,而CS_HREDRAW + CS_VREDRAW + CS_VREDRAW就不对了,因为1 or 1 = 1,而1 + 1就等于2了。
建立窗口
接下来的步骤是在已经注册的窗口类的基础上建立窗口,使用“类”的原因是定义窗口的“共性”,建立窗口时肯定还要指定窗口的很多“个性化”的参数,如WNDCLASSEX结构中没有定义的外观、标题、位置、大小和边框类型等属性,这些属性是在建立窗口时才指定的。
和注册窗口类时用一个结构传递所有参数不同,建立窗口时所有的属性都是用单个参数的方式传递的,建立窗口的函数是CreateWindowEx(注意不要写成CreateWindowsEx),同样,它是Win16中的CreateWindow函数的扩展,主要表现在多了一个dwExStyle(扩展风格)参数,原因是Win32比Win16中多了很多种窗口风格,原来的一个风格参数已经不够用了。
CreateWindowEx函数的使用方法是:
invoke CreateWindowEx, dwExStyle, lpClassName, lpWindowName, dwStyle, x, y, nWidth, nHeight, hWndParent, hMenu, hInstance, lpParam
12个参数很好理解:
lpClassName 建立窗口使用的类名字符串指针,在FirstWindow程序中指向MyClass字符串,表示使用MyClass类建立窗口,这正是我们自己注册的类,这样一来,这个窗口就有“MyClass”的所有属性,并且消息将被发到“MyClass”中指定的窗口过程中去,当然,这里也可以是Window预定义的类名。
lpWindowName 指向表示窗口名称的字符串,该名称会显示在标题栏上。如果该参数空白,则标题栏上什么都没有。
hMenu 窗口上要出现的菜单的句柄。在注册窗口类的时候也定义了一个菜单,那是窗口的默认菜单,意思是如果这里没有定义菜单(用参数NULL)而注册窗口类时定义了菜单,则使用窗口类中定义的菜单;如果这里指定了菜单句柄,则不管窗口类中有没有定义都将使用这里定义的菜单;两个地方都没有定义菜单句柄,则窗口上没有菜单。另外,当建立的窗口是子窗口时(dwStyle中指定了WS_CHILD),这个参数是另一个含义,这时hMenu参数指定的是子窗口的ID号,这样可以节省一个参数的位置,因为反正子窗口不会有菜单。
hpParam 这是一个指针,指向一个欲传给窗口的参数,这个参数在WM_CREATE消息中可以被获取,一般情况下用不到这个字段。
hInstance 模块句柄,和注册窗口类时一样,指定了窗口所属的程序模块。
hWndParent 窗口所属的父窗口,对于普通窗口(相对于子窗口),这里的“父子”关系只是从属关系,主要用来在父窗口销毁时一同将其“子”窗口销毁,并不会把窗口位置限制在父窗口的客户区范围内,但如果要建立的是真正的子窗口(dwStyle中指定了WS_CHILD的时候),这时窗口位置会被限制在父窗口的客户区范围内,同时窗口的坐标(x,y)也是以父窗口的左上角为基准的。
x,y 指定窗口左上角位置,单位是像素。默认时可指定为CW_USEDEFAULT,这样Windows会自动为窗口指定最合适的位置,当建立子窗口时,位置是以父窗口的左上角为基准的,否则,以屏幕左上角为基准。
nWidth,hHeight 窗口的宽度和高度,也就是窗口的大小,同样是以像素为单位的。默认时可指定为CW_USEDEFAULT,这样Windows会自动为窗口指定最合适的大小。
窗口的两个参数dwStyle和dwExStyle决定了窗口的外形和行为,dwStyle是从Win16开始就有的属性,下表列出了一些常见的dwStyle定义,它们是一些以WS(Windows Style的缩写)为开头的预定义值。
预定义值
|
16进制值
|
含义
|
WS_OVERLAPPED
|
00000000h
|
普通的重叠式窗口
|
WS_POPUP
|
80000000h
|
弹出式窗口(没有标题栏)
|
WS_CHILD
|
40000000h
|
子窗口
|
WS_MINIMIZE
|
20000000h
|
初始状态是最小化的
|
WS_VISIBLE
|
10000000h
|
初始状态是可见的
|
WS_DISABLED
|
08000000h
|
初始状态是被禁止的
|
WS_MAXIMIZE
|
01000000h
|
初始状态是最大化的
|
WS_BORDER
|
00800000h
|
单线条边框
|
WS_DLGFRAME
|
00400000h
|
对话框类型的边框
|
WS_VSCROLL
|
00200000h
|
带垂直滚动条
|
WS_HSCROLL
|
00100000h
|
带水平滚动条
|
WS_SYSMENU
|
00080000h
|
带系统菜单(即带标题栏左上角的图标)
|
WS_THICKFRAME
|
00040000h
|
可以拖动调整大小的边框
|
为了容易理解,Windows也为一些定义取了一些别名,同时,由于窗口的风格往往是几种风格的组合,所以Windows也预定义了一些组合值,如下表所示:
预定义值
|
等效值
|
WS_CHILDWINDOW
|
WS_CHILD
|
WS_TILED
|
WS_OVERLAPPED
|
WS_ICONIC
|
WS_MINIMIZE
|
WS_SIZEBOX
|
WS_THICKFRAME
|
WS_OVERLAPPEDWINDOW
|
WS_OVERLAPPED or WS_CAPTION or WS_SYSMENU or WS_THICKFRAME or WS_MINIMIZEBOX or WS_MAXINIZEBOX
|
WS_TILEDWINDOW
|
WS_OVERLAPPEDWINDOW
|
WS_POPUPWINDOW
|
WS_POPUP or WS_BORDER or WS_SYSMENU
|
dwExStyle是Win32中扩展的,它们是一些以WS_EX_开头的预定义值,主要定义了一些特殊的风格,如下表所示:
预定义值
|
16进制值
|
含义
|
WS_EX_POPMOST
|
00000008h
|
总在顶层的窗口
|
WS_EX_ACCEPTFILES
|
00000010h
|
允许窗口进行鼠标拖放操作
|
WS_EX_TOOLWINDOW
|
00000080h
|
工具窗口(很窄的标题栏)
|
WS_EX_WINDOWEDGE
|
00000100h
|
立体体的边框
|
WS_EX_CLIENTEDGE
|
00000200h
|
客户区立体边框
|
WS_EX_OVERLAPPDWINDOW
|
|
WS_EX_WINDOWEDGE or WS_EX_CLIENTEDGE
|
WS_EX_PALETTEWINDOW
|
|
WS_EX_POPMOST
|
用预定义的组合值WS_EX_PALETTEWINDOW可以很方便地构成浮在其他窗口前面的工具栏。
建立窗口的相关代码如下:
invoke CreateWindowEx, WS_EX_CLIENTEDGE, \
offset szClassName, offset szCaptionMain, \
WS_OVERLAPPPEDWINDOW, \
100, 100, 600, 400, \
NULL, NULL, hInstance, NULL
mov hWinMain,eax
invoke ShowWindow, hWinMain, SW_SHWONORMAL
invoke UpdateWindow, hWinMain
建立窗口以后,eax中传回来的是窗口句柄,要把它保存起来,这时候,窗口虽已建立,但还没有在屏幕上显示出来,要用ShowWindow把它显示出来,ShowWindow也可以用在另的地方,主要用来控制窗口的显示状态(显示或隐藏),大小控制(最大化、最小化或原始大小)和是否激活(当前窗口还是背后的窗口),它用窗口句柄第一个参数,第二个参数则是显示的方式。显示方式有如下的预定义值:
预定义值
|
等效值
|
SW_HIDE
|
隐藏窗口,大小不变,激活状态不变
|
SW_MAXIMIZE
|
最大化窗口,显示状态不变,激活状态不变
|
SW_MINIMIZE
|
最小化窗口,显示状态不变,激活状态不变
|
SW_RESTORE
|
从最大化或最小化恢复正常大小,显示状态不变,激活状态不变
|
SW_SHOW
|
显示并激活窗口,大小状态不变
|
SW_SHOWMAXIMIZED
|
显示并激活窗口,以最大化显示
|
SW_SHOWMINIMIZED
|
显示并激活窗口,以最小化显示
|
SW_SHOWMINOACTIVE
|
显示窗口并最小化,激活状态不变
|
SW_SHOWNA
|
显示窗口,大小状态不变,激活状态不变
|
SW_SHOWNOACTIVATE
|
显示并从最大化或最小化恢复正常大小,激活状态不变
|
SW_SHOWNORMAL
|
显示并激活窗口,恢复正常大小(初始化时用这个参数)
|
窗口显示以后,用UpdateWindow绘制客户区,它实际上就是向窗口发送了一条WM_PAINT消息。到此为止,一个顶层窗口就正常建立并显示了。
CreateWindowEx也可以用来建立子窗口,Windows中有很多预定义的子窗口类,如按钮和文本框的类名分别是Button和Edit。要建立一个按钮,只要把lpClassName指向Button字符串就可以了。例如:
.data
szButton db ‘button’,0
szButtonText db ‘&OK’,0
invoke CreateWindowEx, NULL, \
offset szButton, offset szButtonText, \
WS_CHILD or WS_VISIBLE, \
10, 10, 65, 22, \
hWnd, 1, hInstance, NULL
在FirstWindow的源程序中加入按钮类的定义字符串“szButton”和按钮文字字符串“szButtonText”,然后在窗口过程的WM_CREATE消息中加入建立按钮的代码,执行一下,窗口中就出现了一个按钮。建立按钮的时候,lpWindowName参数就是按钮上的文字,风格则一定要指定WS_CHILD,建立的按钮才会在我们的主窗口上,WS_VISIBLE也要同时指定,否则按钮不会显示出来,hMenu参数在这里用做表示子窗口ID,将它设置为1,在建立多个子窗口的时候,ID应该有所区别。
消息循环
消息循环的一般形式:
.while TRUE
invoke GetMessage, addr @stMsg, NULL, 0, 0
.break .if eax == 0
invoke TranslateMessage, addr @stMsg
invoke DispatchMessage, addr @stMsg
.endw
消息循环中的几个函数要用到一个MSG结构,用来做消息传递:
MSG STRUCT
Hwnd DWORD ?
Message DWORD ?
WParam DWORD ?
LParam DWORD ?
Time DWORD ?
Pt POINT <>
MSG ENDS
各个字段的含义是:
hwnd 消息要发向的窗口句柄。
message 消息标识符,在头文件中以WM_开头的预定义值(意思为Windows Message)。
wParam 消息的参数之一。
lParam 消息的参数之二。
time 消息放入消息队列的时间。
pt 这是一个POINT的数据结构,表示消息放入消息队列时的鼠标坐标。
这个结构定义了消息的所有属性,GetMessage函数就是从消息队列中取出这样一条消息来的:
invoke GetMessage, lpMsg, hWnd, wMsgPilterMin, wMsgFilterMax
函数的lpMsg指向一个MSG结构,函数会在这里返回取到的消息,hWnd参数指定要获取哪个窗口的消息,例子中指定为NULL,表示获取的是所有本程序所属窗口的消息,wMsgFilterMin和wMsgFilterMax为0表示获取所有编号的消息。
GetMessage函数从消息队列里取得消息,填写好MSG结构并返回,如果获取的消息是WM_QUIT消息,那么eax中的返回值是0,否则eax返回非零值,所以用.break .if eax == 0来检查返回值,如果消息队列中有WM_QUIT则退出消息循环。
TranslateMessage将MSG结构传给Windows进行一些键盘消息的转换,当有键盘按下和放开时,Windows产生WM_KEYDOWN和WM_KEYUP或WM_SYSKEYDOWN和WM_SYSKEYUP消息,但这些消息的参数中包含的是按键的扫描码,转换成常用的ASCII码要经过查表,很不方便,TranslateMessage遇到键盘消息则将扫描码转换成ASCII码并在消息队列中插入WM_CHAR或WM_SYSCHAR消息,参数就是转换好的ASCII码,如此一来,要处理键盘消息的话只要处理WM_CHAR消息就好了。遇到别的消息则TranslateMessage不做处理。
最后,由DispatchMessage将消息发送到窗口对应的窗口过程去处理。窗口过程返回后DispatchMessage函数才返回,然后开始新一轮消息循环。
其它形式的消息循环:
GetMessage函数是程序空闲的时候主动将控制权交还给Windows的一种方式,Windows是一个抢占式的多任务系统,任务之间每20ms切换一次,试想一下,如果窗口程序在主窗口中采用死循环等待,消息由Windows直接发送到窗口过程,那么程序会是下列这种样子:
invoke CreateWindow,
invoke ShowWindow,
invoke UpdateWindow,
.while dwQuitFlag == 0 ;要退出时在窗口过程中设置dwQuitFlag
.endw
invoke ExitProcess,
但这样一来,即使程序在空闲状态,轮到自己的20ms时间片的时候,CPU时间就会全部消耗在.while循环中,使用GetMessage的时候,轮到应用程序时间片的时候,如果消息队列里还没有消息,那么程序还是停留在GetMessage内部,这时就可以由Windows当家作主没收这20ms的时间片,如此保证了CPU资源的合理应用。
如果应用程序想把所有时间充分用回来,消息队列里没有消息的时候不让GetMessage在Windows内部等待,拱手交出属于自己的CPU时间,那么消息循环可以是下列这种样子:
.while TRUE
invoke PeekMessage, addr @stMsg, NULL, 0, 0, PM)REMOVE
.if eax
.break .if @stMsg.message == WM_QUIT
invoke TranslateMessage, addr @stMsg
invoke DispatchMessage, addr @stMsg
.else
<做其他工作>
.endif
.endw
PeekMessage是一个类似于GetMessage的函数,区别在于当消息队列里有消息的时候,PeekMessage取回消息,并在eax中返回非零值,没有消息的时候它会直接返回,并在eax中返回零。所以在返回非零值的时候,程序检查消息是否是WM_QUIT,是则结束消息循环,不是则用标准流程处理消息;返回零的时候,表示是空闲时间,程序就可以做其他工作了,但插入做其他工作的代码执行时间不能过长,以不越过10ms为好,否则会影响正常的消息处理,使窗口的反应看起来很迟钝。如果必须处理很长时间的工作,那么应该将它分成很多小部分处理,以便有足够的频率来用PeekMessage来检查消息。PeekMessage的前面4个参数和GetMessage是相同的,增加了最后一个参数,PM_REMOVE表示取回消息的同时从消息队列里删除,否则用PM_NOREMOVE。
窗口过程
窗口过程是给Windows回调用的,它必须遵循规定的格式。对窗口过程的子程序名并没有规定,对Windows来说,窗口过程的地址才是唯一需要的,例子程序中的子程序名是_ProcWinMain,读者可以改用任何名称。窗口过程子程序的参数格式为:
WindowProc proc hwnd, uMsg, wParam, lParam
第一个参数是窗口句柄,一个窗口过程可能为多个基于同一个窗口类的窗口服务,所以Windows回调的时候必须指出要操作的窗口,否则窗口过程不知道要去处理哪个窗口,FirstWindow程序只建立了一个窗口,所以每次传递过来的hwnd和用CreateWindowEx函数返回的窗口句柄是一样的;
第二个参数是消息标识,后面两个参数是消息的两个参数。这4个参数和消息循环中MSG结构中的前4个字段是一样的。
窗口过程的结构
窗口过程一般有如下结构:
WindowsProc proc uses ebx edi esi, hWnd, uMsg, wParam, lParam
mov eax,uMsg
.if eax == WM_XXX
<处理WM_XXX消息>
.elseif eax == WM_YYY
<处理WM_YYY消息>
.elseif eax == WM_CLOSE
invoke DestroyWindow, hWinMain
invoke PostQuitMessage, NULL
.else
invoke DefWindowProc, hWnd, uMsg, wParam, lParam
ret
.endif
mov eax,eax
ret
WindowProc endp
该过程主要是对uMsg参数中的消息编号构成一个分支结构,对于需要处理的消息分别处理。不感兴趣的消息则交给DefWindowProc来处理。
要注意的是窗口过程中要注意保存ebx,edi,esi和ebp寄存器,高级语言中不用自己操心这一点,汇编中就要注意了,Windows内部将这4个寄存器当指针使用,如果返回时改变了它们的值,程序会马上崩溃。proc后面的uses伪操作在子程序进入和退出时自动按插上push和pop寄存器指令,来保护这些寄存器的值。其实不仅是在窗口过程中是这样,所有由应用程序提供给Windows的回调函数都必须遵循这个规定,如定时器回调函数等,所有Win32 API也遵循这个规定,所以调用API后,ebx,edi,esi和ebp寄存器的值总是不会被改变的,但ecx和edx的值就不一定了。
uMsg参数指定的消息有一定的范围,Windows标准窗口中已经预定义的值在0~03ffh之间(1024个),用户可以自定义一些消息,通过SendMessage等函数传给窗口过程做自定义的处理工作,这时可以使用的值是从0400h开始的,WM_USER就定义为00000400h,当程序员定义多个用户消息的时候,一般使用WM_USER+1,WM_USER+2,之类的定义方法。
wParam和lParam参数是消息所附带的参数,它随消息的不同而不同,对于不同的消息,它们的含义必须分别从手册中查明:如WM_MOUSEMOVE消息中,wParam是标志,lParam是鼠标位置;而在WM_GETTEXT消息中,wParam是要获取的字符数,lParam是缓冲地址;而对于WM_COPY消息来说,它不需要额外的信息,所以两个参数都没有定义。
处理了不同的消息,必须返回规定的值给Windows,返回值也需要分别从手册中查明,比如处理WM_CREATE消息的时候,如果返回0表示成功;如果程序无法初始化,如申请内存失败,那么可以返回-1,Windows就不会继续窗口的创建过程。一些消息的返回值则没有定义,但大部分的消息处理以后都以返回0表示成功,所以程序中把默认的返回语句放在最后,将eax清零后返回,如果在处理某个消息的时候需要返回不同的值,可以以分支中将eax赋值后直接用ret指令返回。对于DefWindowProc的返回值,我们不对它进行干涉,所以直接将eax不做修改地用ret返回。
WM_CLOSE消息是按下了窗口右上角的“关闭”按钮后收到的,程序可以在这里处理和关闭窗口相关的事情,一般是相关资源的释放工作,如释放内存、保存工作和提示用户是否保存工作等,如记事本程序在未保存的时候单击“关闭”按钮,会有提示框提示是否先保存文件,单击“取消”按钮的话,记事本不会关闭,这个步骤就是在WM_CLOSE消息处理中完成的。如果处理WM_CLOSE消息时直接返回,那么窗口不会关闭,因为这个消息只是Windows通知窗口用户刚才单击了“关闭”按钮而已,窗口采用什么样的行为是窗口的事。当窗口决定关闭的时候,需要程序自己调用DestroyWindow来摧毁窗口,并用PostQuitMessage向消息循环发送WM_QUIT消息来退出消息循环。调用PostQuitMessage时的参数是退出码,就是GetMessage收到的WM_QUIT后MSG结构wParam字段中的东西,在这里使用NULL。
PostQuitMessage是初学者容易遗漏的函数,如果没有这条语句,外观上窗口是从屏幕上消失了,但主程序中的消息循环却没有收到WM_QUIT,结果还在那里打转。常有人调试的时候丢了这条语句,结果再一次编译的时候就收到错误:LINK fatal error LNK1104:cannot open file “xxx.exe”,表示exe文件现在不可写。
Windows为什么不在窗口摧毁的时候自动发送一个WM_QUIT消息,而必须由用户程序自己通过PostQuitMessage函数发送呢?其实很好理解:因为屏幕上可能不止一个窗口,Windows无法确定哪个窗口的关闭代表着程序结束。试想一下,用户打开了一个输入参数的小窗口,单击“确定”按钮后关闭并回到主窗口,Windows却不分三七二十一自动发送了一个WM_QUIT,程序就会莫名其妙地退出了。
收到消息的顺序
窗口过程收到消息是有一定顺序的,收到第一条消息并不是从消息循环开始以后,而是在CreateWindowEx中就开始了,显示和刷新窗口的函数ShowWindow和UpdateWindow也向窗口过程发送消息,这一点并不奇怪,因为Windows在CreateWindowEx前调用RegisterClassEx的时候就已经得到窗口过程的地址了。并且在建立窗口的过程中需要窗口过程的配合。
下面分别列出调用CreateWindowEx和ShowWindow的时候窗口过程收到的消息。
调用CreateWindowEx时窗口过程收到的消息
消息发生
|
说明
|
WM_GETMINMAXINFO
|
获取窗口大小,以便初始化
|
WM_NCCREATE
|
非客户区开始建立
|
WM_NCCALCSIZE
|
计算客户区大小
|
WM_CREATE
|
窗口建立
|
调用ShowWindow时窗口过程收到的消息
消息发生
|
说明
|
WM_SHOWWINDOW
|
显示窗口
|
WM_WINDOWPOSCHANGING
|
窗口位置准备改变
|
WM_ACTIVATEAPP
|
窗口准备激活
|
WM_NCACTIVATE
|
激活状态改变
|
WM_GETTEXT
|
取窗口名称(显示标题栏用)
|
WM_ACTIVATE
|
窗口准备激活
|
WM_SETFOCUS
|
窗口获得焦点
|
WM_NCPAINT
|
需要绘画窗口边框
|
WM_ERASEBKGND
|
需要擦除背景
|
WM_WINDOWPOSCHANGED
|
窗口益已经改变
|
WM_SIZE
|
窗口大小已经改变
|
WM_MOVE
|
窗口位置已经移动
|
然后程序执行UpdateWindow,这个函数向窗口过程发送一条WM_PAINT消息,接着,主程序开始进入消息循环,Windows根据各种因素给窗口过程发送相应的消息,一直到调用DestroyWindow为止。
调用DestroyWindow时窗口过程收到的消息
消息发生
|
说明
|
WM_NCACTIVATE
|
窗口激活状态改变
|
WM_ACTIVATE
|
窗口准备非激活
|
WM_ACTIVATEAPP
|
窗口准备非激活
|
WM_KILLFOCUS
|
失去焦点
|
WM_DESTROY
|
窗口即将被摧毁
|
WM_NCDESTROY
|
窗口的非客户区及所有子窗口已经被摧毁
|
在所有这些阶段的消息中,大部分的消息都不需要程序自己关心,Windows只是尽义务通知窗口过程而已,窗口过程转手就交给DefWindowProc去处理了。程序需要关心的消息有下面这些,可以根据需要选择使用:
WM_CREATE 放置窗口初始化代码,如建立各种子窗口(状态栏和工具栏等)。
WM_SIZE 放置位置安排的代码,因为建立的子窗口可能需要随窗口大小的改变而移动位置。
WM_PAINT 如果需要自己绘制客户区,则在这里安排代码。
WM_CLOSE 向用户确认是否退出,如果退出则摧毁窗口并发送WM_QUIT消息。
WM_DESTROY 窗口摧毁,在这里放置释放资源等扫尾代码。
在例子程序中,我们处理了WM_PAINT消息来绘制客户区,功能就是在窗口中的中间写上一行字:Win32 Assembly, Simple and powerful! 过程是先通过BeginPaint获取窗口客户区的“设备环境”句柄,然后通过GetClientRect获取客户区的大小,最后通过DrawText函数将字符串按照取得的屏幕大小居中写到“设备环境”中,也就是窗口上。如果不需要显示这个字符串,则连WM_PAINT消息也不用处理。
消息的默认处理:DefWindowProc
Windows预定义的消息范围是0~03ffh,共1024个消息,查看一下头文件Windows.inc,可以发现实际已定义的消息数目有几百个,这些消息中的大部分对于窗口的运行来说都是必需的,如果窗口过程要处理每一种消息,那么窗口过程中的elseif语句就会绵延数千行,但是窗口的行为就是由处理这些消息的方法来表现的,不处理又不行,怎么办呢?
实际上大部分窗口的行为都是差不多的,这意味着如果要窗口过程处理全部的消息,不同窗口的窗口过程代码应该是大同上异的,那么可以用一个模块来以默认的方式处理消息,Win32中的DefWindowProc函数实现的就是这个功能。
不要小看了这个DefWindowProc,正是它用默认的方式处理了几百种消息,才使用户能用区区百来行代码写出一个全功能的窗口。也正是所有的窗口都用DefWindowProc默认处理程序自己不处理的消息,才使它们的行为看上去大同小异,因为它们背后实际上是同一块代码在处理。
在窗口过程的分支语句中,用户处理所有需要个性化处理的消息,对于表现行为是默认行为的消息,则在else分支中用DefWindowProc来处理,对于Windows来说,它并不关心消息在窗口过程中是程序用自己的代码处理的还是用DefWindowProc处理的,它只看eax中的返回值来了解处理结果,所以不管消息是谁处理的,都必须在eax中返回正确的值。DefWindowProc返回时eax中就是它对消息的处理结果,程序只要直接把eax传回给Windows就行了,所以在例子程序中,DefWindowProc后面直接用一句ret指令返回。
DefWindowProc中对一些消息的处理方法,如果和用户期望的不同,就必须在窗口过程中自己处理。
DefWindowProc对一些消息的默认处理方式
消息
|
DefWindowProc的处理方式
|
WM_PAINT
|
发送WM_ERASEBKGND消息来擦除背景
|
WM_ERASEBKGND
|
用窗口类结构中的hbrBackground刷子来绘画窗口背景
|
WM_CLOSE
|
调用DestroyWindow来摧毁窗口
|
WM_NCLBUTTONDBLCLK
|
这是非客户区(如标题栏)鼠标双击消息,DefWindowProc测试鼠标的位置,然后再采取相应的措施,如标题栏双击将最大化和恢复窗口
|
WM_NCLBUTTONUP
|
这非客户区鼠标标题释放消息,同样,DefWindowProc测试鼠标的位置然后再采取相应的措施,如鼠标在“关闭”按钮的位置释放将导致发送WM_CLOSE消息
|
WM_NCPAINT
|
非客户区绘制消息,DefWindowProc将绘制边框和客户区
|
从这些默认的处理方法可以看出,想要一个窗口和别的窗口看起来不一样,比如想要窗口看起来像苹果机的窗口一样,并且把关闭按钮移到标题栏最左边去,那么可以自己处理WM_NCPAINT消息,把非客户区画成苹果机窗口的样子,并把关闭按钮画到标题栏左边去,并且自己处理WM_NCLBUTTONUP消息,当检测到鼠标按下的位置在自己的关闭按钮上的时候,则发送WM_CLOSE消息。对别的消息的处理思路也可以按这种方法类推。
另外,可以发现DefWindowProc对WM_CLOSE的默认处理是调用DestroyWindow摧毁窗口,DestroyWindow会引发一个WM_DESTROY消息,WM_CLOSE和WM_DESTROY的不同之处是:WM_CLOSE代表用户有关闭的意向,窗口过程有权不“服从”,但收到WM_DESTROY的时候窗口已经在关闭过程中了,不管窗口过程愿不愿意,窗口的关闭已经是不可挽回的事了。
对于这两个消息,窗口过程必须处理其中的一个,因为必须有个地方发送WM_QUIT消息来结束消息循环,例子程序中处理WM_CLOSE消息,在其中用DestroyWindow摧毁窗口,再调用PostQuitMessage结束消息循环;程序也可以不处理WM_CLOSE消息,让DefWindowProc以默认处理的方式摧毁窗口,但这时候必须处理WM_DESTROY消息,在其中调用PostQuitMessage发送WM_QUIT以结束消息循环。