解剖窗口程序
在我们充分了解了消息驱动体系的工作流程以后,让我们接下来分析如何用 Win32汇编实现这一切,本章小甲鱼将带大家详细分析FirstWindow 源程序的各个组成部分。
Windows编程理论上不难,因为原理上只不过是调用Windows为我们设计好的一系列API函数(接口)来实现相应的功能。
但是难点就在于对其中参数的理解和正确使用,稍有不慎,就可能导致调用失败
句柄是什么
随着分析的深入,句柄(handle)一词也出现得频繁了起来,那么”句柄”到底是什么呢?
句柄只是一个数值而已,它的值对程序来说是没有意义的,它只是Windows用来表示各种资源的编号而已,所以只有Windows才知道怎么使用它来引用各种资源。
如果没有句柄,Windows就不知道如何来控制所有的东西。。。
举例说明,屏幕上已经有10个窗口,Windows把它们从1到10编号,应用程序又建立了一个窗口,现在Windows把它编号为11,然后把11当做窗口句柄返回给应用程序,应用程序并不知道11代表的是什么,但在操作窗口的时候,把11当做句柄直接传给Windows,Windows自然可以根据这个数值查出是哪个窗口。
当该窗口关闭的时候,11这个编号作废。第二次运行的时候,如果屏幕上现有5个窗口,那么现在句柄可能就是6了,所以,应用程序并不用关心句柄的具体数值是多少。
打个比方,可以把句柄当做是商场中寄放书包时营业员给的纸条,纸条上的标记用户并不知道是什么意思,但把它交还给营业员的时候,她自然会找到正确的书包。
Windows中几乎所有的东西都是用句柄来标识的,文件句柄、窗口句柄、线程句柄和模块句柄等,同样道理,不必关心它们的值究竟是多少,拿来用就是了!
模块和句柄
一个模块代表的是一个运行中的EXE文件或DLL文件,用来代表这个文件中所有的代码和资源,磁盘上的文件不是模块,装入内存后运行时就叫做模块。
一个应用程序调用其他DLL中的API函数时,这些DLL文件被装入内存,就产生了不同的模块,为了区分地址空间中的不同模块,每个模块都有一个惟一的模块句柄来标识。
很多API函数中都要用到程序的模块句柄,以便利用程序中的各种资源,所以在程序的一开始就先取得模块句柄并存放到一个全局变量中可以省去很多的麻烦。
在Win32中,模块句柄在数值上等于程序在内存中装入的起始地址。体验一下,顺便介绍IDA的动态调试方法。
取模块句柄使用的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
【Question】为什么我们命名接受句柄的变量习惯叫 hInstance 而不是 hModule 呢?
(h是handle”句柄”的缩写)
Instance 的中文意思是”实例”,它的概念来自于Win16。
Win16中不同运行程序的地址空间并非是完全隔离的,一个可执行文件运行后形成”模块”,多次加载同一个可执行文件时,然而这个”模块”是公用的。
为了区分多次加载的”拷贝”,就把每个”拷贝”叫做实例,每个实例均用不同的”实例句柄”(hInstance)值来标识它们。
但在Win32中,程序运行时是隔离的,每个实例都使用自己私有的4 GB空间,都认为自己是惟一的,不存在一个模块的多个实例的问题。
实际上在Win32中,实例句柄就是模块句柄,但很多API原型中用到模块句柄的时候使用的名称还是沿用 hInstance,所以我们还是把变量名称取为 hInstance。
创建窗口
在创建窗口之前,我们先要理解“类”。
“类”的也是一个封装的概念,主要是为了把一组物体的相同属性归纳整理起来封装在一起,以便重复使用。
在“类”已定义的属性基础上加上其他个性化的属性,就形成了各式各样的个体。
这可以结合OO编程来理解,思维都是那么回事。当然如果不懂得OO编程,通过学习以下内容将会使你对XXOO又有一个新的认识。
Windows中创建窗口同样使用这样的层次结构。
首先定义一个窗口类,然后在窗口类的基础上添加其他的属性建立窗口。
不用一步到位的办法是因为很多窗口的基本属性和行为都是一样的,如按钮、文本输入框和选择框等,对这些东西Windows都预定义了对应的类,使用时直接使用对应的类名建立窗口就可以了。
只有用户自定义的窗口才需要先定义自己的类,再建立窗口。这样可以节省资源。
注册窗口类
创建窗口
注册窗口类(模板)
建立窗口
建立窗口类的方法是在系统中注册,注册窗口类的API函数是 RegisterClassEx,最后的“Ex”是扩展的意思,因为它是 Win16 中RegisterClass的扩展。
一个窗口类定义了窗口的一些主要属性,如:图标、光标、背景色、菜单和负责处理该窗口所属消息的函数。
这些属性并不是分成多个参数传递过去的(这种做法不聪明),而是定义在一个WNDCLASSEX结构中,再把结构的地址当参数一次性传递给 RegisterClassEx,WNDCLASSEX 是WNDCLASS 结构的扩展。
靠猫, WNDCLASS结构
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程序中,注册窗口类的代码是:
FirstWindows
注意两点:
程序定义了一个WNDCLASSEX结构的变量@stWndClass,用 RtlZeroMemory 将它先填为全零,再填写结构的各个字段,这样,没有赋值的部分就保持为0。
push hInstance
pop @stWndClass.hInstance
WNDCLASSEX结构各字段的含义
给大家唠叨一下结构各字段的含义:
hIcon —— 图标句柄,指定显示在窗口标题栏左上角的图标。(程序可以使用在资源文件中定义的图标,这些图标的句柄可以用LoadIcon函数获得。例子程序没有用到图标,所以Windows给窗口显示了一个默认的图标。)
hCursor —— 光标句柄,指定了鼠标在窗口中的光标形状。可以用LoadCursor获取它们的句柄,IDC_ARROW是Windows预定义的箭头光标,如果想使用自定义的光标,也可以自己定义。
lpszMenuName —— 指定窗口上显示的默认菜单
hInstance —— 指定要注册的窗口类属于哪个模块,模块句柄在程序开始的地方已经用GetModuleHandle 函数获得。
cbSize —— 指定 WNDCLASSEX 结构的长度,用 sizeof 伪操作来获取。
注:很多Win32 API参数中的结构都有cbSize字段,它主要是用来区分结构的版本,因为新的结构会增加一些新的字段。
style —— 窗口风格。
注:CS_HREDRAW 和 CS_VREDRAW 表示窗口的宽度或高度改变时是否重画窗口。比较重要的是 CS_DBLCLKS 风格,指定了它,Windows才会把在窗口中快速两次单击鼠标的行为翻译成双击消息 WM_LBUTTONDBLCLK 发给窗口过程。
lpszClassName —— 指定程序员要建立的类命名,以便以后用这个名称来引用它。
hbrBackground —— 窗口客户区的背景色。
前面的 hbr 表示它是一个刷子(Brush)的句柄,Windows预定义了一些刷子,如 BLACK_BRUSH 和WHITE_BRUSH 等,可以用下列语句来得到它们的句柄:
invoke GetObjectStock, WHITE_BRUSH
但在这里也可以使用颜色值,如COLOR_BACKGROUND, COLOR_HIGHLIGHT,COLOR_MENU,COLOR_WINDOW 等,使用颜色值的时候,Windows规定必须在颜色值上加1。
cbWndExtra 和 cbClsExtra —— 分别是在Windows 内部保存的窗口结构和类结构中给程序员预留的空间大小,用来存放自定义数据,它们的单位是字节。不使用自定义数据的话,这两个字段就是0。
lpfnWndProc —— 最重要的参数,它指定了基于这个类建立的窗口的窗口过程地址。通过这个参数,Windows 就知道了在 DispatchMessage 函数中把窗口消息发到哪里去。
最后一个是结构中的 style 表示窗口的风格,Windows 已经有一些预定义的值,它们是以 CS(Class Style的缩写)开始的标识符。
可以看到,这些预定义值实际上在使用不重复的数据位,所以可以组合起来使用,同时使用不同的预定义值并不会引起混淆。
对于不同二进制位组合的计算,“加”和“或”的结果是一样的,但强烈建议使用or,因为如果不小心指定了两个同样的风格时就有 BUG 产生了,因为 1 or 1 = 1,而 1+1 = 2。
建立窗口
接下来的步骤是在已经注册的窗口类的基础上建立窗口,使用“类”的原因是定义窗口的“共性”,建立窗口时肯定还要指定窗口的很多“个性化”的参数。
和注册窗口类时用一个结构传递所有参数不同,建立窗口时所有的属性都是用单个参数的方式传递的,建立窗口的函数是CreateWindowEx。
它是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”类中指定的窗口过程中去。
lpWindowName(第三个参数)
指向表示窗口名称的字符串。
该名称会显示在标题栏上。
hMenu(第十个参数)
窗口上要出现的菜单的句柄。
在注册窗口类的时候也定义了一个菜单,那是窗口的默认菜单,意思是如果这里没有定义菜单(用参数NULL)而注册窗口类时定义了菜单,则使用窗口类中定义的菜单;如果这里指定了菜单句柄,则不管窗口类中有没有定义都将使用这里定义的菜单
两个地方都没有定义菜单句柄,则窗口上没有菜单
lpParam(最后一个参数)
一般情况下用不到这个字段
hInstance(第十一个参数)
模块句柄,和注册窗口类时一样,指定了窗口所属的程序模块。
hWndParent(第九个参数)
窗口所属的父窗口,这里的“父子”关系只是从属关系,主要用来在父窗口销毁时一同将其“子”窗口销毁,并不会把窗口位置限制在父窗口的客户区范围内。
x,y(第五、六个参数)
指定窗口左上角位置,单位是像素(px)。默认时可指定为 CW_USEDEFAULT,这样Windows会自动为窗口指定最合适的位置,当建立子窗口时,位置是以父窗口的左上角为基准的,否则,以屏幕左上角为基准。
nWidth,nHeight(第七、八个参数)
窗口的宽度和高度,也就是窗口的大小,同样是以像素为单位的。默认时可指定为 CW_USEDEFAULT
dwStyle(第四个参数)
窗口的两个参数 dwStyle 和 dwExStyle 决定了窗口的外形和行为,dwStyle 是从 Win16 开始就有的属性,我们用一个表列出了常见的 dwStyle 定义,它们是一些以WS(Windows Style的缩写)为开头的预定义值。
窗口风格的预定义值
为了容易理解,Windows也为一些定义取了一些别名,同时,由于窗口的风格往往是几种风格的组合,所以Windows也预定义了一些组合值:
dwExStyle(第一个参数)
dwExStyle 是Win32中扩展的,它们是一些以WS_EX_开头的预定义值,主要定义了一些特殊的风格,下表给出了一些最常用的特殊风格。
窗口扩展风格的预定义值
ShowWindow
CreateWindowEx 建立窗口以后,eax 中传回来的是窗口句柄,注意要把它先保存起来,因为这时候,窗口虽已建立,但还没有在屏幕上显示出来。
要用 ShowWindow 把它显示出来,主要用来控制窗口的显示状态(显示或隐藏),大小控制(最大化、最小化或原始大小)和是否激活(当前窗口还是背后的窗口),它用窗口句柄做第一个参数,第二个参数则是显示的方式。
ShowWindow 函数显示方式(第二个参数)的定义:Follow me!
窗口显示以后,用 UpdateWindow 绘制客户区,它实际上就是向窗口发送了一条 WM_PAINT 消息。到此为止,一个顶层窗口才算正常建立并显示!
另外,CreateWindowEx 也可以用来建立子窗口,Windows中有很多预定义的子窗口类,如按钮和文本框的类名分别是”Button”和”Edit”。建立一个按钮,只要把 lpClassName 指向”Button”字符串就可以。