模块和句柄
1. 模块的概念
一个模块代表的是一个运行中的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 GetModuleHandle,NULL
mov hInstance,eax
可以注意到,把返回的句柄放到了hInstance变量里而并不是放在hModule中,为什么是 hInstance呢? Instance是“实例”,它的概念来自于Win16,Win16中不同运行程序的地址空间并非是完全隔离的,一个可执行文件运行后形成“模块”,多次加载同一个可执行文件时,这个“模块”是公用的,为了区分多次加载的“拷贝”,就把每个“拷贝”叫做实例,每个实例均用不同的“实例句柄”(hInstance)值来标识它们。
但在Win32中,程序运行时是隔离的,每个实例都使用自己私有的4 GB空间,都认为自己是惟一的,不存在一个模块的多个实例的问题,实际上在Win32中,实例句柄就是模块句柄,但很多API原型中用到模块句柄的时候使用的名称还是沿用hInstance,所以我们还是把变量名称取为hInstance。
2.句柄是什么
随着分析的深入,句柄(handle)一词也出现得频繁了起来,“句柄”是什么呢?句柄只是一个数值而已,它的值对程序来说是没有意义的,它只是Windows用来表示各种资源的编号而已,所以只有Windows才知道怎么使用它来引用各种资源。
举例说明,屏幕上已经有10个窗口,Windows把它们从1到10编号,应用程序又建立了一个窗口,现在Windows把它编号为11,然后把11当做窗口句柄返回给应用程序,应用程序并不知道11代表的是什么,但在操作窗口的时候,把11当做句柄传给Windows,Windows自然可以根据这个数值查出是哪个窗口。当该窗口关闭的时候,11这个编号作废。第二次运行的时候,如果屏幕上现有5个窗口,那么现在句柄可能就是6了,所以,应用程序并不用关心句柄的具体数值是多少。打个比方,可以把句柄当做是商场中寄放书包时营业员给的纸条,纸条上的标记用户并不知道是什么意思,但把它交还给营业员的时候,她自然会找到正确的书包。
Windows中几乎所有的东西都是用句柄来标识的,文件句柄、窗口句柄、线程句柄和模块句柄等,同样道理,不必关心它们的值究竟是多少,拿来用就是了!
3.创建窗口
因为考虑到一般窗口的共性,比如:每个窗口都有图标、光标、背景色、菜单和负责处理该窗口所属消息的函数这些属性并不是分成多个参数传递过去的,而是定义在一个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
在FirstWindow程序中,注册窗口类的代码是:
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将它填为全零(局部变量初始化的重要性在第3章中已经强调过),再填写结构的各个字段,这样,没有赋值的部分就保持为0,结构各字段的含义如下:
● hIcon——图标句柄,指定显示在窗口标题栏左上角的图标。Windows已经预定义了一些图标,同样,程序也可以使用在资源文件中定义的图标,这些图标的句柄可以用LoadIcon函数获得。例子程序没有用到图标,所以Windows给窗口显示了一个默认的图标。
● hCursor——光标句柄,指定了鼠标在窗口中的光标形状。同样,Windows也预定义了一些光标,可以用LoadCursor获取它们的句柄,IDC_ARROW是Windows预定义的箭头光标,如果想使用自定义的光标,也可以自己在资源文件中定义。
● lpszMenuName——指定窗口上显示的默认菜单,它指向一个字符串,描述资源文件中菜单的名称,如果资源文件中菜单是用数值定义的,那么这里使用菜单资源的数值。窗口中的菜单也可以在建立窗口函数CreateWindowEx的参数中指定。如果在两个地方都没有指定,那么建立的窗口上就没有菜单。
● 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 GetObjectStock, WHITE_BRUSH
但在这里也可以使用颜色值,Windows已经预定义了一些颜色值,分别对应窗口各部分的颜色,如COLOR_BACKGROUND,COLOR_HIGHLIGHT,COLOR_MENU和COLOR_WINDOW等,使用颜色值的时候,Windows规定必须在颜色值上加1,所以程序中的指令是:
mov @stWndClass.hbrBackground,COLOR_WINDOW + 1
● lpszClassName——指定程序员要建立的类命名,以便以后用这个名称来引用它。这个字段是一个字符串指针,在程序里,它指向“MyClass”字符串。
● cbWndExtra和cbClsExtra——分别是在Windows内部保存的窗口结构和类结构中给程序员预留的空间大小,用来存放自定义数据,它们的单位是字节。不使用自定义数据的话,这两个字段就是0。
● lpfnWndProc——最重要的参数,它指定了基于这个类建立的窗口的窗口过程地址。通过这个参数,Windows就知道了在DispatchMessage函数中把窗口消息发到哪里去,一个窗口过程可以为多个窗口服务,只要这些窗口是基于同一个窗口类建立的。Windows中不同应用程序中的按钮和文本框的行为都是一样的,就是因为它们是基于相同的Windows预定义类建立的,它们背后的窗口过程其实是同一段代码。
结构中的style表示窗口的风格,Windows已经有一些预定义的值,它们是以CS(Class Style的缩写)开始的标识符,如表4.1所示。
表4.1 一些窗口类的style预定义值
预定义值 |
16进制值 |
对应二进制位 |
CS_VREDRAW |
00000001h |
0 |
CS_HREDRAW |
00000002h |
1 |
CS_KEYCVTWINDOW |
00000004h |
2 |
CS_DBLCLKS |
00000008h |
3 |
CS_OWNDC |
00000020h |
5 |
CS_CLASSDC |
00000040h |
6 |
… |
|
|
可以看到,这些预定义值实际上在使用不重复的数据位,所以可以组合起来使用,同时使用不同的预定义值并不会引起混淆。
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”中指定的窗口过程中去,当然,这里也可以是Windows预定义的类名。
● lpWindowName——指向表示窗口名称的字符串,该名称会显示在标题栏上。如果该参数空白,则标题栏上什么都没有。
● hMenu——窗口上要出现的菜单的句柄。在注册窗口类的时候也定义了一个菜单,那是窗口的默认菜单,意思是如果这里没有定义菜单(用参数NULL)而注册窗口类时定义了菜单,则使用窗口类中定义的菜单;如果这里指定了菜单句柄,则不管窗口类中有没有定义都将使用这里定义的菜单;两个地方都没有定义菜单句柄,则窗口上没有菜单。另外,当建立的窗口是子窗口时(dwStyle中指定了WS_CHILD),这个参数是另一个含义,这时hMenu参数指定的是子窗口的ID号,这样可以节省一个参数的位置,因为反正子窗口不会有菜单。
● lpParam——这是一个指针,指向一个欲传给窗口的参数,这个参数在WM_CREATE消息中可以被获取,一般情况下用不到这个字段。
● hInstance——模块句柄,和注册窗口类时一样,指定了窗口所属的程序模块。
● hWndParent——窗口所属的父窗口,对于普通窗口(相对于子窗口),这里的“父子”关系只是从属关系,主要用来在父窗口销毁时一同将其“子”窗口销毁,并不会把窗口位置限制在父窗口的客户区范围内,但如果要建立的是真正的子窗口(dwStyle中指定了WS_CHILD的时候),这时窗口位置会被限制在父窗口的客户区范围内,同时窗口的坐标(x,y)也是以父窗口的左上角为基准的。
● x,y——指定窗口左上角位置,单位是像素。默认时可指定为 CW_USEDEFAULT,这样Windows会自动为窗口指定最合适的位置,当建立子窗口时,位置是以父窗口的左上角为基准的,否则,以屏幕左上角为基准。
● nWidth,nHeight—窗口的宽度和高度,也就是窗口的大小,同样是以像素为单位的。默认时可指定为 CW_USEDEFAULT,这样Windows会自动为窗口指定最合适的大小。
窗口的两个参数dwStyle和dwExStyle决定了窗口的外形和行为,dwStyle是从Win16开始就有的属性,表4.2列出了一些常见的dwStyle定义,它们是一些以WS(Windows Style的缩写)为开头的预定义值。
表4.2 窗口风格的预定义值
预定义值 |
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 |
可以拖动调整大小的边框 |
WS_MINIMIZEBOX |
00020000h |
有最小化按钮 |
WS_MAXIMIZEBOX |
00010000h |
有最大化按钮 |
为了容易理解,Windows也为一些定义取了一些别名,同时,由于窗口的风格往往是几种风格的组合,所以Windows也预定义了一些组合值,如表4.3所示。
表4.3 等效的窗口风格预定义值
预定义值 |
等 效 值 |
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_MAXIMIZEBOX |
WS_TILEDWINDOW |
WS_OVERLAPPEDWINDOW |
WS_POPUPWINDOW |
WS_POPUP or WS_BORDER or WS_SYSMENU |
dwExStyle是Win32中扩展的,它们是一些以WS_EX_开头的预定义值,主要定义了一些特殊的风格,表4.4给出了一些最常用的特殊风格。
表4.4 窗口扩展风格的预定义值
预定义值 |
16进制值 |
含 义 |
WS_EX_TOPMOST |
00000008h |
总在顶层的窗口 |
WS_EX_ACCEPTFILES |
00000010h |
允许窗口进行鼠标拖放操作 |
WS_EX_TOOLWINDOW |
00000080h |
工具窗口(很窄的标题栏) |
WS_EX_WINDOWEDGE |
00000100h |
立体感的边框 |
WS_EX_CLIENTEDGE |
00000200h |
客户区立体边框 |
预定义值 |
16进制值 |
含 义 |
WS_EX_OVERLAPPEDWINDOW |
|
WS_EX_WINDOWEDGE or WS_EX_CLIENTEDGE |
WS_EX_PALETTEWINDOW |
|
WS_EX_WINDOWEDGE or WS_EX_TOOLWINDOW or WS_EX_TOPMOST |
用预定义的组合值WS_EX_PALETTEWINDOW可以很方便地构成浮在其他窗口前面的工具栏。
下面看几种不同的窗口外形,如图4.5所示,窗口1是WS_OVERLAPPED类型的窗口,只有一个边框,没有控制按钮和图标;窗口2同时指定WS_MAXIMIZEBOX,WS_MINIMIZEBOX和WS_SYSMENU,在窗口1的基础上多了控制按钮和图标;窗口3是WS_POPUPWINDOW风格的,这是一个没有标题和控制按钮的弹出式窗口,常见的软件装入时的版权窗口就是这种风格;前面3个窗口都不能通过拖动边框改变大小,而窗口4指定了WS_THICKFRAME风格,可以改变大小,它的边框显得厚了一点;窗口5的风格是WS_OVERLAPPEDWINDOW,是最常见的属性组合;窗口6在窗口5的基础上指定了WS_EX_CLIENTEDGE,它的客户区显得有立体感;窗口7是个工具栏,指定的是WS_EX_TOOLWINDOW风格,可以看到它的标题栏要小得多;窗口8指定了WS_HSCROLL和WS_VSCROLL风格,窗口中多了垂直和水平滚动条。
图4.5 不同风格的窗口
FirstWindow程序中建立窗口的相关代码是这样的:
invoke CreateWindowEx,WS_EX_CLIENTEDGE,\
offset szClassName,offset szCaptionMain,\
WS_OVERLAPPEDWINDOW,\
100,100,600,400,\
NULL,NULL,hInstance,NULL
mov hWinMain,eax
invoke ShowWindow,hWinMain,SW_SHOWNORMAL
invoke UpdateWindow,hWinMain
…
建立窗口以后,eax中传回来的是窗口句柄,要把它先保存起来,这时候,窗口虽已建立,但还没有在屏幕上显示出来,要用ShowWindow把它显示出来,ShowWindow也可以用在别的地方,主要用来控制窗口的显示状态(显示或隐藏),大小控制(最大化、最小化或原始大小)和是否激活(当前窗口还是背后的窗口),它用窗口句柄做第一个参数,第二个参数则是显示的方式。表4.5给出了显示方式预定义值。
表4.5 ShowWindow函数显示方式的定义
预定义值 |
等 效 值 |
SW_HIDE |
隐藏窗口,大小不变,激活状态不变 |
SW_MAXIMIZE |
最大化窗口,显示状态不变,激活状态不变 |
SW_MINIMIZE |
最小化窗口,显示状态不变,激活状态不变 |
SW_RESTORE |
从最大化或最小化恢复正常大小,显示状态不变,激活状态不变 |
SW_SHOW |
显示并激活窗口,大小状态不变 |
SW_SHOWMAXIMIZED |
显示并激活窗口,以最大化显示 |
SW_SHOWMINIMIZED |
显示并激活窗口,以最小化显示 |
SW_SHOWMINNOACTIVE |
显示窗口并最小化,激活状态不变 |
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消息中加入建立按钮的代码,执行一下,窗口中就出现了一个按钮,如图4.6所示。建立按钮的时候,lpWindowName参数就是按钮上的文字,风格则一定要指定WS_CHILD,建立的按钮才会在我们的主窗口上,WS_VISIBLE也要同时指定,否则按钮不会显示出来,hMenu参数在这里用做表示子窗口ID,将它设置为1,在建立多个子窗口的时候,ID应该有所区别。