4.1 开始了解窗口
4.1.1 窗口是什么窗口是什么?
大家每天在使用Windows,屏幕上的一个个方块就是一个个窗口!那么,窗口为什么是这个样子呢?窗口就是程序吗?
1.使用窗口的原因
回想一下DOS时代的计算机屏幕,在1990年Windows 3.0推出之前,计算机的屏幕一直使用文本模式,黑洞洞的底色上漂浮着白色的小字,性能不高的图形模式只用于简单的游戏和一些图形软件。对DOS程序来说,屏幕是唯一的,上面有个光标表示输入字符的位置,程序运行后往屏幕输出一些信息,退出时输出的信息就留在了屏幕上,然后是第二个程序重复这个过程,当屏幕被写满的时候,整个屏幕上卷一行,最上面一行被去掉,然后程序在最底下新空出来的一行上继续输出。
对于一个单任务的操作系统来说,这种方式是很合理的,因为平时使用的传真机或打字机就是用上卷的方式来容纳新的内容的。但是如果是多任务呢?两个程序同时往屏幕上输出字符或者两个人同时往打字机上打字,那么谁都看不懂混在一起的是什么。DOS下的TSR(内存驻留)程序是多个程序同时使用一个屏幕的例子,但实质上这并不是多任务,而是TSR将别的程序暂时挂起,挂起的程序不可能在TSR执行期间再向屏幕输出内容,TSR在输出自己的内容之前必须保存屏幕上显示的内容,并在退出的时候把屏幕恢复为原来的样子,否则挂起的程序并不知道屏幕已经被改变,在这个过程中,DOS不会去干预中间发生的一切。
Windows是多任务的操作系统,可以同时运行多个程序,同样,各个程序在屏幕上的显示不能互相干扰,而且,多个程序可以看成是"同时"运行的,在后台的程序也可能随时向屏幕输出内容,这中间的调度是由Windows完成的。Windows采用的方法是给程序一块矩形的屏幕空间,这就是窗口。应用程序通过Windows向属于自己的窗口显示信息,Windows判断该窗口是不是被别的窗口挡住,并把没有挡住的部分输出到屏幕上,这样屏幕上显示的窗口就不会互相覆盖而乱套。对于应用程序来说,它只需认为窗口就是自己拥有的显示空间就可以了。
2.窗口和程序的关系
既然不同窗口的内容就是不同程序的输出,那么一个窗口就是一个程序吗?反过来,一个程序就是一个窗口吗?
答案是否定的,一个窗口不一定就是一个程序,它可能只是一个程序的一部分。一个程序可以建立多个顶层窗口,如Windows的桌面和任务栏都是顶层窗口,但它们都属于"文件管理器"进程,所以并不是一个窗口就是一个程序的代表。Windows的窗口采用层次结构,一个窗口中可以建立多个子窗口,如窗口中的状态栏、工具栏,对话框中的按钮、文本输入框与复选框等都是子窗口。子窗口中还可以再建立下一级子窗口,如Word工具栏上的字体选择框。
反过来,运行的程序并非一定就是窗口,比如悄悄在后台运行的木马程序就不会显示一个窗口向用户报告它在干非法勾当。在Windows NT下用"任务管理器"查看,进程的数量比屏幕上的窗口多得多,意味着很多的运行程序并没有显示窗口。如果一个程序不想和用户交互,它可以选择不建立窗口。
所以本章的标题"第一个窗口程序"指学习编写第一个以标准的窗口为界面的程序,而不是泛指Windows程序。如果要写的Win32程序不是以窗口为界面的(如控制台程序等),就不一定采用本章中提及的以消息驱动的程序结构。
虽然以窗口为界面的程序并不是所有Windows程序的必然选择,但绝大部分的应用程序是以这种方式出现的,从操作系统的名称"Windows"就可以看出这一点,了解窗口程序就是相当于在了解Windows工作方式的基础。
控制台方式也是Windows程序的另一种常用界面,考虑到初学者刚刚接触Windows程序的体系架构,将控制台界面编程的内容插在本章或者后续章节中容易引起初学者对窗口程序架构理解上的混淆,所以本书将控制台编程单独放在附录A中(以电子版方式放在随书光盘中),有兴趣的读者可以在学完资源、图形编程、界面编程等内容后再单独阅读这个章节。
4.1.2 窗口界面
大部分的窗口看上去都是大同小异的,先来看一个典型的窗口------Windows附带的写字板,该程序的界面如图4.1所示,我们将用它来说明窗口的各个组成部分。

图4.1 一个典型的窗口
窗口一般由屏幕上的矩形区域组成,不同的窗口可能包括一些相同的组成部分,如标题栏、菜单、工具栏、边框和状态栏等,每个部分都有自己固定的行为模式:
● 窗口边框------窗口的外沿就是窗口边框,用鼠标按住边框并拖动可以调整窗口的大小。
● 标题栏------窗口的最上面是标题栏,用鼠标按住标题栏拖动可移动窗口,双击标题栏则将窗口最大化或从最大化的状态恢复。通过标题栏的颜色可以区分窗口是不是活动窗口,同时标题栏列出了应用程序的名称。
● 菜单------标题栏下面是菜单,单击菜单会弹出各种功能选择。
● 工具栏------菜单的下面是工具栏,工具栏上用图标的方式列出最常用的功能,相当于菜单的快捷方式。
● 图标和"最小化"、"最大化"与"关闭"按钮------图标位于标题栏的左边,三个控制按钮则位于标题栏的右边。单击图标会弹出一个系统菜单,双击图标则相当于按下了"关闭"按钮。"最小化"、"最大化"按钮用来控制窗口的大小。
● 状态栏------状态栏位于窗口的最下面,用来显示一些状态信息。
● 客户区------窗口中间用来工作或输出的区域叫做窗口的客户区,把窗口看做是一张白纸的话,客户区就是白纸中真正用来书写的部分,程序在这里和用户进行交互。
● 滚动条------如果客户区太小不足于显示全部内容,则右边或底部可能还有滚动条,拖动它可以滚动窗口的客户区,以便看到其他的内容。
虽然大部分窗口看上去都差不多,但并不是每个窗口都有这些组成部分,也许有的窗口就没有图标和最小化、最大化框,有的没有工具栏或状态栏,有的没有标题栏,而有的就干脆是个奇怪的形状,如Office帮助中的助手,那些小狗小猫都是些不折不扣的窗口,本书第7章中的BmpClock例子就是类似的不规则窗口的例子,另外,Windows的桌面和桌面下面的任务栏也都是窗口,就连屏幕保护程序的黑屏幕也是一个大小为整个屏幕、没有标题栏和边框的窗口!
一致的窗口形状和行为模式为Windows用户提供了一致的用户界面,几乎所有的窗口程序都在菜单的第一栏设置有关文件的操作和退出功能、最后一栏设置程序的帮助,相同的功能在工具栏上的图标也是大同小异的,用户可以不再像在DOS下那样,对不同的程序需要学习不同的界面,用户自从学会使用第一个软件起,就基本学会了所有Windows软件的使用模式,而且可以通过相似的菜单、工具栏等来发掘程序的新功能。窗口的菜单和客户区是最个性化的部分,菜单随程序功能的不同而不同,而客户区则是窗口程序的输出区域,不同的程序在客户区内显示了不同的内容。
4.1.3 窗口程序是怎么工作的
1.窗口程序的运行模式
对程序员来说,不仅要了解用户可以看到的部分,还必须了解隐藏在窗口底下的细节,了解用怎样的程序结构来实现窗口的行为模式。
DOS程序员熟悉的是顺序化的、按过程驱动的程序设计方法,这种程序有明显的开始、明显的过程和明显的结束,由程序运行的阶段来决定用户该做什么。
而窗口程序是事件驱动的,用户可能随时发出各种消息,如操作的过程中觉得窗口不够大了,就马上拖动边框,程序必须马上调整客户区的内容以适应新的窗口大小;用户觉得想先干别的事情,可能会把窗口最小化,"关闭"按钮也有可能随时被按下,这意味着程序要随时可以处理退出的请求。如果非要规定干活的时候不能移动窗口与调整大小,那么这些窗口就会呆在桌面上一动不动。
再次提醒:这里是"窗口程序"而不是"Windows程序",因为和窗口有关的程序才是事件驱动的,其他的Windows可能并不这样工作,如控制台程序的结构还是同DOS程序一样是顺序化的,但与窗口相关的Windows程序占了绝大多数,所以大部分书籍中讲到Windows程序就认为是事件驱动的程序。
先通过一个简单的例子来说明两种程序设计方式的不同,以DOS下的文件比较命令comp为例,程序运行时先提示输入第一个文件名,然后是输入第二个文件名,程序比较后退出,同时把结果输出在屏幕上。假如有一个窗口版的comp程序,那么运行时会在屏幕上出现一个对话框,上面有两个文本框用来输入两个文件名,还会有个"比较"按钮,按下后开始比较文件,用户可以随时按下"关闭"按钮来退出程序。
两种程序的运行会有相当大的不同,如图4.2所示,DOS程序必须按照顺序运行,当运行到输入第二个文件名时,用户不可能回到第一步修改第一个文件名,这时候用户也不能退出(除非用户强制用Ctrl+C键,但这不是程序的本意);而在窗口程序中用户可以随意选择先输入哪个文件名,同时也可以对窗口进行各种操作,当用户做任何一个操作的时候,相当于发出了一个消息,这些消息没有任何顺序关系,程序中必须随时准备处理不同的消息。

图4.2 不同的程序结构模式
这就决定了窗口程序必定在结构上和DOS程序有很大的不同,窗口程序实现大部分功能的代码应该呆在同一个模块------图中的"消息处理"模块中,这个模块可以随时应付所有类型的消息,只有这样才能随时响应用户的各种操作。
下面先来看一个地地道道的Win32汇编窗口程序。
2.FirstWindow源代码
读者可以在所带光盘的Chapter04\FirstWindow目录中找到源代码,目录里面有两个文件,它们是汇编源文件FirstWindow.asm和nmake工具使用的makefile,汇编源程序如下:
【学习笔记】
;FirstWindow.asm 窗口程序的模板代码
; 使用 nmake 或下列命令进行编译和链接:
; ml /c /coff FirstWindow.asm
; Link /subsystem:windows FirstWindow.obj
.386
.model flat,stdcall
option casemap:none
;include 文件定义
;-------------------------------------------------------------
include windows.inc
include gdi32.inc
includelib gdi32.lib
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
;--------------------------------------------------------------
;数据段
;-------------------------------------------------------------
.data? ;未初始化
hInstance dword ?
hWinMain dword ?
.const ;常量数据
szClassName byte 'MyClass',0
szCaptionMain byte 'My first Window!', 0
szText byte 'Win32 Assembly, Simple and powerful!', 0
;-----------------------------------------------------------------
;代码段
;-------------------------------------------------------------
.code
;窗口过程
;-------------------------------------------------------------
_ProcWinMain proc uses ebx edi esi, hWnd, uMsg, wParam, lParam
local @stPs:PAINTSTRUCT
local @stRect:RECT
local @hDc
mov eax, uMsg
;-----------------------------------------------------------
.if eax == WM_PAINT
invoke BeginPaint, hWnd, addr @stRect
mov @hDc, eax
invoke GetClientRect, hWnd, addr @stRect
invoke DrawText, @hDc, addr szText, -1, \
addr @stRect, \
DT_SINGLELINE or DT_CENTER or DT_VCENTER
invoke EndPaint, hWnd, addr @stPs
;-------------------------------------------------------------
.elseif eax == WM_CLOSE
invoke DestroyWindow, hWinMain
invoke PostQuitMessage, NULL
;--------------------------------------------------------------
.else
invoke DefWindowProc, hWnd, uMsg, wParam, lParam
ret
.endif
;--------------------------------------------------------------
xor eax, eax
ret
_ProcWinMain endp
;-------------------------------------------------------------------
_WinMain proc
local @stWndClass:WNDCLASSEX
local @stMsg:MSG
invoke GetModuleHandle, NULL
mov hInstance, eax
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
;建立并显示窗口
;--------------------------------------------------------------------
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
;消息循环
;------------------------------------------------------------------------
.while TRUE
invoke GetMessage, addr @stMsg, NULL, 0, 0
.break .if eax == 0
invoke TranslateMessage, addr @stMsg
invoke DispatchMessage, addr @stMsg
.endw
ret
_WinMain endp
;----------------------------------------------------------------------------
start:
call _WinMain
invoke ExitProcess, 0
end start
让我们打开一个DOS窗口,切换到FirstWindow所在的目录,运行环境设置的批处理文件var.bat,再键入nmake编译出FirstWindow.exe,这个程序只有2560字节,运行后窗口出来了,如图4.3所示。对于这个窗口,用户可以拖动边框去改变大小、按标题栏上的按钮来最大化和最小化,当光标移到边框的时候,会自动变成双箭头......总之,这个窗口包括了一个典型窗口的所有特征。
windws XP 环境下【编译:ml /c /coff FirstWindow.asm 链接:Link /subsystem:windows FirstWindow.obj】

运行如下:

图4.3 FirstWindow的运行结果
接下来开始分析源代码,看了这近三页的源代码,第一个感觉是什么?是不是想撤退了?笔者刚开始编Win32程序的时候就是这种感觉,可能90%的人有同样的感觉,别急,过了这一关,Win32汇编的入门就成功了一半,所以千万要挺住!有个振奋人心的消息是,这个程序是大部分窗口程序的模板,以后要写一个新的程序,把它复制过来再往中间添砖加瓦就是了,工夫一点都不白费。
先静下心来分析一下程序的结构,还看得懂,很好!其实源程序的结构在第3章里已经了解过了,首先是注释......模式定义......include...... .data数据段,都没有问题,这些已经占去了近40行了,好了,终于是关键的代码段了,统计一下,只剩80行代码了。
分析一下程序的结构,发现入口是start,然后执行了一个_WinMain子程序,完成后就是程序退出的函数ExitProcess,再看_WinMain的结构,前面是顺序下来的几个API:
GetModuleHandle → RtlZeroMemory → LoadCursor → RegisterClassEx → CreateWindowEx → ShowWindow → UpdateWindow
从名称上就能看出它们的用途,很明显,窗口是在CreateWindowEx处建立的,ShowWindow则是把窗口显示在屏幕上,这些代码是窗口的建立过程。
接下来,就是一个由3个API组成的循环了:
GetMessage → TranslateMessage → DispatchMessage
很明显,这是与消息有关的循环,因为API名称中都带有Message字样,如果退出这个循环,程序也就结束了,这个循环叫做消息循环。设置_WinMain子程序并不是必需的,可以把_WinMain的所有代码放到主程序中,没有任何影响,之所以这样,只是为了将这里使用的变量定义成局部变量,这样可以方便移植。
看了程序的流程,似乎没有什么地方涉及窗口的行为,如改变大小和移动位置的处理等。再看源程序,除了_WinMain,还有一个子程序_ProcWinMain,但除了在WNDCLASSEX结构的赋值中提到过它,好像就没有什么地方要用到这个子程序,起码在自己编写的源代码中没有任何一个地方调用过它。
再看_ProcWinMain,它是一个分支结构处理的子程序,功能是把参数uMsg取出来,根据不同的uMsg执行不同的代码,完了以后就退出了,中间也没有任何代码和主程序有关联。
第一个窗口程序就是由这么两个似乎是风马牛不相及的部分组成的,但它确实能工作,对于写惯了DOS汇编的程序员来说,这似乎不可理解。下面来看看这么一个陌生而奇怪的程序是如何工作的。
3.窗口程序的运行过程
在屏幕上显示一个窗口的过程一般有以下步骤,这就是主程序的结构流程:
(1)得到应用程序的句柄(GetModuleHandle)。
(2)注册窗口类(RegisterClassEx)。在注册之前,要先填写RegisterClassEx的参数WNDCLASSEX结构。
(3)建立窗口(CreateWindowEx)。
(4)显示窗口(ShowWindow)。
(5)刷新窗口客户区(UpdateWindow)。
(6)进入无限的消息获取和处理的循环。首先获取消息(GetMessage),如果有消息到达,则将消息分派到回调函数处理(DispatchMessage),如果消息是WM_QUIT,则退出循环。
程序的另一半_ProcWinMain子程序是用来处理消息的,它就是窗口的回调函数(Callback),也叫做窗口过程,之所以是回调函数,是因为它是由Windows而不是我们自己调用的,我们调用DispatchMessage,而DispatchMessage在自己的内部回过来调用窗口过程。
所有的用户操作都是通过消息来传给应用程序的,如用户按键、鼠标移动、选择了菜单和拖动了窗口等,应用程序中由窗口过程接收消息并处理,在例子程序中就是_ProcWinMain。由于窗口过程构造了一个分支结构,对应不同的消息执行不同的代码,所以一个应用程序中几乎所有的功能代码都集中在窗口过程里。
窗口程序运行中消息传输的流程可以由图4.4来表示。
先来看看Windows对消息的处理。Windows在系统内部有一个系统消息队列,当输入设备有所动作的时候,如用户按动了键盘、移动了鼠标、按下或放开了鼠标等,Windows都会产生相应的记录放在系统消息队列里,如图4.4中的箭头a和b所示,每个记录中包含消息的类型、发生的位置(如鼠标在什么坐标移动)和发生的时间等信息。

图4.4 窗口程序的运行过程
同时,Windows为每个程序(严格地说是每个线程)维护一个消息队列,Windows检查系统消息队列里消息的发生位置,当位置位于某个应用程序的窗口范围内的时候,就把这个消息派送到应用程序的消息队列里,如图4.4中的箭头c所示。
当应用程序还没有来取消息的时候,消息就暂时保留在消息队列里,当程序中的消息循环执行到GetMessage的时候,控制权转移到GetMessage所在的USER32.DLL中(箭头1),USER32.DLL从程序消息队列中取出一条消息(箭头2),然后把这条消息返回应用程序(箭头3)。
应用程序可以对这条消息进行预处理,如可以用TranslateMessage把基于键盘扫描码的按键消息转换成基于ASCII码的键盘消息,以后也会用到TranslateAccelerator把键盘快捷键转换成命令消息,但这个步骤不是必需的。
然后应用程序将处理这条消息,但方法不是自己直接调用窗口过程来完成的,而是通过DispatchMessage间接调用窗口过程,Dispatch的英文含义是"分派",之所以是"分派",是因为一个程序可能建有不止一个窗口,不同的窗口消息必须分派给相应的窗口过程。当控制权转移到USER32.DLL中的DispatchMessage时,DispatchMessage找出消息对应窗口的窗口过程,然后把消息的具体信息当做参数来调用它(箭头5),窗口过程根据消息找到对应的分支去处理,然后返回(箭头6),这时控制权回到DispatchMessage,最后DispatchMessage函数返回应用程序(箭头7)。这样,一个循环就结束了,程序又开始新一轮的GetMessage。
有个很常见的问题:为什么要由Windows来调用窗口过程,程序取了消息以后自己处理不是更简便吗?事实上并非如此,如果程序自己处理消息的"分派",就必须自己维护本程序所属窗口的列表,当程序建立的窗口不止一个的时候,这个工作就变得复杂起来;另一个原因是:别的程序也可能用SendMessage通过Windows直接调用你的窗口过程;第三个原因:Windows并不是把所有的消息都放进消息队列,有的消息是直接调用窗口过程处理的,如WM_SETCURSOR等实时性很强的消息,所以窗口过程必须开放给Windows。
应用程序之间也可以互发消息,PostMessage是把一个消息放到其他程序的消息队列中,如图4.4中箭头d所示,目标程序收到了这条消息就把它放入该程序的消息队列去处理;而SendMessage则越过消息队列直接调用目标程序的窗口过程(如图4.4中箭头I所示),窗口过程返回以后才从SendMessage返回(如图4.4中箭头II所示)。
窗口过程是由Windows回调的,Windows又是怎么知道往哪里回调的呢?答案是我们在调用RegisterClassEx函数的时候已经把窗口过程的地址告诉了Windows。
4.2 分析窗口程序
了解了消息驱动体系的工作流程以后,让我们来分析如何用Win32汇编实现这一切,本节和下一节将详细分析FirstWindow源程序。
4.2.1 模块和句柄
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。
在C语言的编程中,hInstance通过WinMain由系统传入,WinMain的原型是:
WinMain(hInstance, hPrevInstance, lpzCmdParam, nCmdShow),程序不用自己去获得hInstance,这个过程由C的初始化代码代劳了,但在Win32汇编中hInstance必须自己获取,如果不了解hModule就是hInstance的话,就无法得知如何得到hInstance,因为并没有一个GetInstanceHandle之类的API函数。
2.句柄是什么
随着分析的深入,句柄(handle)一词也出现得频繁起来,"句柄"是什么呢?句柄只是一个数值而已,它的值对程序来说是没有意义的,它只是Windows用来表示各种资源的编号而已,可见只有Windows才知道怎么使用它来引用各种资源。
下面举例说明。屏幕上已经有10个窗口,Windows把它们从1到10编号,应用程序又建立了一个窗口,现在Windows把它编号为11,然后把11当做窗口句柄返回给应用程序,应用程序并不知道11代表的是什么,但在操作窗口的时候,把11当做句柄传给Windows,Windows自然可以根据这个数值查出是哪个窗口。当该窗口关闭的时候,11这个编号作废。第二次运行的时候,如果屏幕上现有5个窗口,那么现在句柄可能就是6了,所以,应用程序并不用关心句柄的具体数值是多少。打个比方,可以把句柄当做是商场中寄放书包时营业员给的纸条,纸条上的标记用户并不知道是什么意思,但把它交还给营业员的时候,她自然会找到正确的书包。
Windows中几乎所有的东西都是用句柄来标识的,文件句柄、窗口句柄、线程句柄和模块句柄等,同样道理,不必关心它们的值究竟是多少,拿来用就是了!
4.2.2 创建窗口
在创建窗口之前,先要谈到"类"。"类"的概念读者都不陌生,主要是为了把一组物体的相同属性归纳整理起来封装在一起,以便重复使用,在"类"已定义的属性基础上加上其他个性化的属性,就形成了各式各样的个体。
Windows中创建窗口同样使用这样的层次结构。首先定义一个窗口类,然后在窗口类的基础上添加其他的属性建立窗口。不用一步到位的办法是因为很多窗口的基本属性和行为都是一样的,如按钮、文本输入框和选择框等,对这些特殊的窗口Windows都预定义了对应的类,使用时直接使用对应的类名建立窗口就可以了。只有用户自定义的窗口才需要先定义自己的类,再建立窗口。这样可以节省资源。
1.注册窗口类
建立窗口类的方法是在系统中注册,注册窗口类的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
在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伪操作来获取。很多Win32API参数中的结构都有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_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预定义值

可以看到,这些预定义值实际上是在使用不重复的数据位,所以可以组合起来使用,同时使用不同的预定义值并不会引起混淆。
对于不同二进制位组合的计算,"加"和"或"的结果是一样的,在FirstWindow程序中用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了。
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预定义的类名,如编辑框就是"EDIT"。
● 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 窗口风格的预定义值

为了容易理解,Windows也为一些定义取了一些别名,同时,由于窗口的风格往往是几种风格的组合,所以Windows也预定义了一些组合值,如表4.3所示。
表4.3 等效的窗口风格预定义值

dwExStyle是Win32中扩展的,它们是一些以WS_EX_开头的预定义值,主要定义了一些特殊的风格,表4.4给出了一些最常用的特殊风格。
表4.4 窗口扩展风格的预定义值

用预定义的组合值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函数显示方式的定义

窗口显示以后,用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应该有所区别。这个例子的源程序可以在所附带光盘的Chapter04\FirstWindow-1目录中找到。

图4.6 用CreateWindowEx建立的按钮
【学习笔记】
;FirstWindow1.asm 在窗口模板程序上添加一个按钮
; 使用 nmake 或下列命令进行编译和链接:
; ml /c /coff FirstWindow1.asm
; Link /subsystem:windows FirstWindow1.obj
.386
.model flat,stdcall
option casemap:none
;include 文件定义
;-------------------------------------------------------------
include windows.inc
include gdi32.inc
includelib gdi32.lib
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
;--------------------------------------------------------------
;数据段
;-------------------------------------------------------------
.data? ;未初始化
hInstance dword ?
hWinMain dword ?
.const ;常量数据
szClassName byte 'MyClass',0
szCaptionMain byte 'My first Window!', 0
szText byte 'Win32 Assembly, Simple and powerful!', 0
szButton byte 'button', 0
szButtonText byte '&OK', 0
;-----------------------------------------------------------------
;代码段
;-------------------------------------------------------------
.code
;窗口过程
;-------------------------------------------------------------
_ProcWinMain proc uses ebx edi esi, hWnd, uMsg, wParam, lParam
local @stPs:PAINTSTRUCT
local @stRect:RECT
local @hDc
mov eax, uMsg
;-----------------------------------------------------------
.if eax == WM_PAINT
invoke BeginPaint, hWnd, addr @stRect
mov @hDc, eax
invoke GetClientRect, hWnd, addr @stRect
invoke DrawText, @hDc, addr szText, -1, \
addr @stRect, \
DT_SINGLELINE or DT_CENTER or DT_VCENTER
invoke EndPaint, hWnd, addr @stPs
;-------------------------------------------------------------
;添加一个按钮
.elseif eax == WM_CREATE
invoke CreateWindowEx, NULL, \
offset szButton, offset szButtonText, \
WS_CHILD or WS_VISIBLE, \
10, 10, 65, 22, \
hWnd, 1, hInstance, NULL
;-------------------------------------------------------------
.elseif eax == WM_CLOSE
invoke DestroyWindow, hWinMain
invoke PostQuitMessage, NULL
;--------------------------------------------------------------
.else
invoke DefWindowProc, hWnd, uMsg, wParam, lParam
ret
.endif
;--------------------------------------------------------------
xor eax, eax
ret
_ProcWinMain endp
;-------------------------------------------------------------------
_WinMain proc
local @stWndClass:WNDCLASSEX
local @stMsg:MSG
invoke GetModuleHandle, NULL
mov hInstance, eax
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
;建立并显示窗口
;--------------------------------------------------------------------
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
;消息循环
;------------------------------------------------------------------------
.while TRUE
invoke GetMessage, addr @stMsg, NULL, 0, 0
.break .if eax == 0
invoke TranslateMessage, addr @stMsg
invoke DispatchMessage, addr @stMsg
.endw
ret
_WinMain endp
;----------------------------------------------------------------------------
start:
call _WinMain
invoke ExitProcess, 0
end start
Window XP环境下编译运行如下:

4.2.3 消息循环
1.消息循环的一般形式
程序中的以下代码就是通常的消息循环:
.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,wMsgFilterMin,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函数才返回,然后开始新一轮消息循环。
2.其他形式的消息循环
GetMessage函数是程序空闲的时候主动将控制权交还给Windows的一种方式,Windows是一个抢占式的多任务系统,任务之间每20 ms切换一次,试想一下,如果窗口程序在主窗口中采用死循环等待,消息由Windows直接发送到窗口过程,那么程序会是下列这种样子:
invoke CreateWindow,...
invoke ShowWindow,...
invoke UpdateWindow,...
.while dwQuitFlag == 0 ;要退出时在窗口过程中设置dwQuitFlag
.endw
invoke ExitProcess,...
但这样一来,即使程序在空闲状态,轮到自己的20 ms时间片的时候,CPU时间就会全部消耗在 .while循环中,使用GetMessage的时候,轮到应用程序时间片的时候,如果消息队列里还没有消息,那么程序还是停留在GetMessage内部,这时就可以由Windows当家做主没收这20 ms的时间片,这样保证了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,是则结束消息循环,不是则用标准流程处理消息;返回零的时候,表示是空闲时间,程序就可以做其他工作了,但插入做其他工作的代码执行时间不能过长,以不超过10 ms为好,否则会影响正常的消息处理,使窗口的反应看起来很迟钝。如果必须处理很长时间的工作,那么应该将它分成很多小部分处理,以便有足够的频率用PeekMessage来检查消息。
PeekMessage的前面4个参数和GetMessage是相同的,增加的最后一个参数表示在取回消息以后,对消息队列中的消息是否保留。当这个参数是PM_REMOVE时,消息被取回的同时也被从消息队列里删除,而用PM_NOREMOVE的时候,被取回的消息不会从消息队列中删除,函数相当于"偷看"了这条消息。例子程序中用了PM_REMOVE,否则每次看到的都是队列中的第一条消息。
4.2.4 窗口过程
窗口过程是给Windows回调用的,它必须遵循规定的格式。对窗口过程的子程序名并没有规定,对Windows来说,窗口过程的地址才是唯一需要的,例子程序中的子程序名是_ProcWinMain,读者可以改用任何名称。窗口过程子程序的参数格式为:
WindowProc proc hwnd,uMsg,wParam,lParam
第一个参数是窗口句柄,由于一个窗口过程可能为多个基于同一个窗口类的窗口服务,所以Windows回调的时候必须指出要操作的窗口,否则窗口过程不知道要去处理哪个窗口,FirstWindow程序只建立了一个窗口,所以每次传递过来的hwnd和用CreateWindowEx函数返回的窗口句柄是一样的;第二个参数是消息标识,后面两个参数是消息的两个参数。这4个参数和消息循环中MSG结构中的前4个字段是一样的。
1.窗口过程的结构
窗口过程一般有如下的结构:
WindowProc 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
xor 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之间,用户可以自定义一些消息,通过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清0后返回,如果在处理某个消息的时候需要返回不同的值,可以在分支中将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,程序就会莫名其妙地退出了。
2.收到消息的顺序
窗口过程收到消息是有一定顺序的,收到第一条消息并不是从消息循环开始以后,而是在CreateWindowEx中就开始了,显示和刷新窗口的函数ShowWindow和UpdateWindow也向窗口过程发送消息,这一点并不奇怪,因为Windows在CreateWindowEx前调用RegisterClassEx的时候就已经得到窗口过程的地址了。并且在建立窗口的过程中需要窗口过程的配合。表4.6和表4.7分别列出了调用CreateWindowEx和ShowWindow的时候窗口过程收到的消息
|------------------|--------------|
| 表4.6 调用CreateWindowEx时窗口过程收到的消息 ||
| 消息发生 | 说明 |
| WM_GETMINMAXINFO | 获取窗口大小,以便初始化 |
| WM_NCCREATE | 非客户区开始建立 |
| WM_NCCALCSIZE | 计算客户区大小 |
| WM_CREATE | 窗口建立 |
|----------------------|---------------|
| 表4.7 调用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为止。表4.8列出了DestroyWindow向窗口过程发送的消息。
|----------------|--------------------|
| 表4.8 调用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消息也不用处理。
3.消息的默认处理------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后面直接使用的这句ret非常重要,如果丢失了这一句,那么相当于处理大多数消息时没有返回正确的值,窗口将不会正常工作。
表4.9中列出了DefWindowProc中对一些消息的处理方法,如果与用户期望的不同,就必须在窗口过程中自己处理。
|--------------------|-------------------------------------------------------------------------------|
| 表4.9 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以结束消息循环。
附录B(以电子版方式放在随书光盘中)中的内容以几个实验的方式,演示了窗口从建立、进行各种操作到摧毁的过程中收到的各种消息,读者可以自行阅读并进行实验,以加深对窗口程序工作原理的认识。
4.3 窗口间的通信
4.3.1 窗口间的消息互发
在介绍消息循环的时候,已经知道在不同应用程序之间的窗口中可以互发消息(如图4.4所示),方法是通过SendMessage或者PostMessage函数,这两个函数的使用语法是相同的:
invoke PostMessage,hWnd,Msg,wParam,lParam
invoke SendMessage,hWnd,Msg,wParam,lParam。
对于不同的Msg,wParam和lParam的含义是不同的,如对于WM_SETTEXT是:
wParam = 0; ;未定义,必须为0
lParam = (LPARAM)(LPCTSTR)lpsz ;要设置的字符串地址
想一想就会发现一个问题:Windows中不同应用程序的地址空间是隔离的(如图1.6所示),假设程序1要用SendMessage调用程序2所属窗口的窗口过程,但程序2窗口过程的代码并不在程序1的地址空间中,那么SendMessage如何调用它呢?其实很简单,当程序1调用SendMessage函数的时候,Windows会先保存wParam和lParam参数并等待,等轮到程序2的时间片的时候再去调用它的窗口过程,并把保存的wParam和lParam参数发给它,等窗口过程返回的时候,Windows记下返回值并等待,再等轮到程序1的时间片的时候把返回值当做SendMessage的返回值传给程序1,这样程序1看上去就像自己直接在调用程序2的窗口过程一样。
但又一个问题出现了:Windows在做"牵线红娘"的时候传递了wParam和lParam,以及返回值,如果参数指向一个字符串呢,比如说上面的WM_SETTEXT消息中的lParam指向一个字符串,假设程序1中lParam指向字符串的地址为xxxxxxxx,把这个地址传给程序2的时候,程序2不可能访问到程序1的地址空间,在程序2中xxxxxxxx指向的可能是其他内容,也可能是不可访问的,这又该如何处理呢?
写一个源程序实验一下,用一个程序向另一个程序的窗口发送WM_SETTEXT消息,然后在另一个程序中将接收到的WM_SETTEXT消息的参数显示出来。先来打造接收程序,首先复制一份FirstWindow的代码,然后在窗口过程的分支中加上以下代码:
.elseif eax == WM_SETTEXT
invoke wsprintf,addr szBuffer,addr szReceive,lParam,lParam
invoke MessageBox,hWnd,offset szBuffer,addr szCaptionMain,MB_OK
同时在数据段中加上下列定义:
szCaptionMain db 'Receive Message',0
szReceive db 'Receive WM_SETTEXT message',0dh,0ah
db 'param: %08x',0dh,0ah
db 'text: "%s"',0dh,0ah,0
在这里,要提及Win32 API中一个很常用的函数wsprintf,这是一个字符串格式化函数,可以将数值按指定格式翻译成字符串,类似于C语言中的printf函数,它的原型是这样的:
int wsprintf(
LPTSTR lpOut, // 输出缓冲区地址
LPCTSTR lpFmt, // 格式化串地址
... // 变量列表
);
变量列表的数目由格式化字符串规定,wsprintf处理格式化字符串,遇到普通的字符则直接复制到输出,遇到%字符则代表有一个变量,%后面不同的字母表示不同的输出格式,如%d表示输出为整数,%x表示输出为十六进制,%s表示输出字符串等。
%符号和表示格式的d,x和s等字母间可以用数字来指定输出时占用的位长,这时输出的位长不够时函数会用空格填齐。另外,表示位长的数字前可以加0来表示填齐时用"0"而非空格,如%08x表示输出为8位前面用0填齐的十六进制数。wsprintf是Win32API中唯一一个参数数量不定的函数,使用wsprintf函数的时候,参数的数量取决于格式化字符串中用%号指定的数量,变量列表的数目和格式化串中的%格式一定要一一对应,比如,例子中szReceive中有两个%号定义,那么后面就要额外跟两个参数:
invoke wsprintf,addr szBuffer,addr szReceive,lParam,lParam
这条语句将lParam的数值,以及lParam的字符串按照szReceive格式化串定义的格式转换,并将结果存放到szBuffer中,然后程序将szBuffer中的内容在一个消息框中显示出来:
invoke MessageBox,hWnd,offset szBuffer,addr szCaptionMain,MB_OK
【学习笔记】
完整的接收程序如下:
;Receive.asm 从一个程序向另一个窗口程序发送消息 之 [消息接收程序]
; 使用 nmake 或下列命令进行编译和链接:
; ml /c /coff Receive.asm
; Link /subsystem:windows Receive.obj
.386
.model flat,stdcall
option casemap:none
;include 文件定义
include C:/masm32/include/windows.inc
include C:/masm32/include/gdi32.inc
includelib C:/masm32/lib/gdi32.lib
include C:/masm32/include/user32.inc
includelib C:/masm32/lib/user32.lib
include C:/masm32/include/kernel32.inc
includelib C:/masm32/lib/kernel32.lib
;数据段
.data? ;未初始化的数据
hInstance dword ?
hWinMain dword ?
szBuffer byte 512 dup(?)
.const ;常量数据
szClassName byte 'MyClass', 0
szCaptionMain byte 'Receive Message', 0
szReceive byte 'Receive WM_SETTEXT message', 0dh, 0ah
byte 'param:%08x', 0dh, 0ah
byte 'text: "%s"', 0dh, 0ah,0
; 代码段
.code
;窗口过程
_ProcWinMain proc uses ebx edi esi, hWnd, uMsg, wParam, lParam
mov eax, uMsg
.if eax == WM_CLOSE
invoke DestroyWindow, hWinMain
invoke PostQuitMessage, NULL
;收到 WM_SETTEXT 消息则将消息字符串和字符串地址显示出来
.elseif eax == WM_SETTEXT
invoke wsprintf, addr szBuffer, addr szReceive, lParam, lParam
invoke MessageBox, hWnd, offset szBuffer, addr szCaptionMain, MB_OK
.else
invoke DefWindowProc, hWnd, uMsg, wParam, lParam
ret
.endif
xor eax, eax
ret
_ProcWinMain endp
_WinMain proc
local @stWndClass:WNDCLASSEX
local @stMsg:MSG
invoke GetModuleHandle, NULL
mov hInstance, eax
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
;建立并显示窗口
invoke CreateWindowEx, WS_EX_CLIENTEDGE, offset szClassName, offset szCaptionMain, \
WS_OVERLAPPEDWINDOW, \
50, 50, 200, 150, \
NULL, NULL, hInstance, NULL
mov hWinMain, eax
invoke ShowWindow, hWinMain, SW_SHOWNORMAL
invoke UpdateWindow, hWinMain
;消息循环
.while TRUE
invoke GetMessage, addr @stMsg, NULL, 0, 0
.break .if eax == 0
invoke TranslateMessage, addr @stMsg
invoke DispatchMessage, addr @stMsg
.endw
ret
_WinMain endp
main proc
call _WinMain
invoke ExitProcess, 0
main endp
end main
接收程序写好了,现在来写一个发送程序,如下所示:
;Send.asm 从一个程序向另一个窗口程序发送消息 之 [发送程序]
; 使用 nmake 或下列命令进行编译和链接:
; ml /c /coff Send.asm
; Link /subsystem:windows Send.obj
.386
.model flat,stdcall
option casemap:none
;include 文件定义
include C:/masm32/include/windows.inc
include C:/masm32/include/gdi32.inc
includelib C:/masm32/lib/gdi32.lib
include C:/masm32/include/user32.inc
includelib C:/masm32/lib/user32.lib
include C:/masm32/include/kernel32.inc
includelib C:/masm32/lib/kernel32.lib
;数据段
.data? ;未初始化的数据
hWnd dword ?
szBuffer byte 256 dup(?)
.const ;常量数据
szCaption byte 'SendMessage', 0
szStart byte 'Press OK to start SendMessage, param: %08x!', 0
szReturn byte 'SendMessage returned!', 0
szDestClass byte 'MyClass', 0
szText byte 'Text send to other windows', 0
szNotFound byte 'Receive Message Window not found!', 0
; 代码段
.code
main proc
invoke FindWindow, addr szDestClass, NULL
.if eax
mov hWnd, eax
invoke wsprintf, addr szBuffer, addr szStart, addr szText
invoke MessageBox, NULL, offset szBuffer, \
offset szCaption, MB_OK
invoke SendMessage, hWnd, WM_SETTEXT, 0, addr szText
invoke MessageBox, NULL, offset szReturn, \
offset szCaption, MB_OK
.else
invoke MessageBox, NULL, offset szNotFound, \
offset szCaption, MB_OK
.endif
invoke ExitProcess, 0
main endp
end main
分别编译两个程序,生成Receive.exe和Send.exe.
在这个程序中首先用FindWindow函数找到接收窗口的窗口句柄,FindWindow函数的使用方法是:
invoke FindWindow,lpClassName,lpWindowName
.if eax
mov hWin,eax
.endif
两个参数都指向字符串,lpClassName指向需要寻找的窗口的窗口类,lpWindowName指向需要寻找窗口的窗口标题,如果目标窗口存在的话,函数的返回值是找到的窗口句柄,否则函数返回0。
用接收窗口的窗口类当做参数寻找窗口,如果没有找到,显示"Receive Message Window not found",找到的话则把"Text send to other windows"字符串的地址当做WM_SETTEXT消息的参数用SendMessage发送给接收窗口。两个程序的源代码可以在所附带光盘的Chapter04\SendMessage目录中找到。
好!现在发送开始,首先执行Receive.exe,窗口出来了,然后执行Send.exe,屏幕上出现一个对话框:Press OK to start SendMessage, param: 00402072,表示在Send程序中字符串的地址是00402072h,现在单击"确定"按钮执行SendMessage函数,单击后对话框消失,但接收程序显示出了一个对话框,内容为:
Receive WM_SETTEXT message
param: 0012ff1c (注:该地址在具体执行的时候可能有所不同)
text: "Text send to other windows"

点击【确定】按钮

可见字符串是正确地传了过来,但地址却不是发送程序的00402072h,这是怎么回事呢?
其实Windows在处理SendMessage的时候要检查消息的类型,并对不同的消息做不同的处理,当消息的参数是一个普通的32位数时,仅仅将该数值传递给目标窗口过程;而当消息的参数是一个指针的时候,Windows对指针指向的内容进行了一些处理,以便数据能够正常地传递到目标进程中。
Windows首先创建一块共享内存,并将WM_SETTEXT消息lParam指向的字符串复制到该内存中,然后再发送消息到其他进程,并将共享内存在目标进程中的地址发送给目标窗口过程,目标窗口过程处理完消息后,函数返回,共享内存被释放。共享内存使用的是第10章介绍的内存映射文件技术,相当于用图1.6的方法将同一块物理内存映射到不同进程的不同线性地址上去。
虽然当消息传递到目标窗口过程的时候lParam的取值会有所变化,但在WM_SETTEXT消息中,lParam的数值是多少并不重要,重要的是它指向的字符串是否正确。
最后,单击Receive程序中的"确定"按钮,Send程序马上会弹出一个消息框并显示:SendMessage returned,这是SendMessage函数告诉我们:我回来了!

在用户自定义的消息中(WM_USER等),不要在消息参数中传递指针,这只会引发非法访问内存,因为Windows不知道用户的意图,它只会把lParam和wParam当两个普通的数值传递,而不会帮用户把指针指向的内容复制到一块共享内存中。
4.3.2 在窗口间传递数据
在WM_SETTEXT这一类的消息中,Windows可以将参数所指的字符串传递到目标窗口过程中,但是这些消息都有它们的本职工作,并且传递的数据也只限于以0结尾的字符串。为了能够在不同进程的窗口间自由地拷贝任意类型的数据,Windows提供了一个特殊的窗口消息------WM_COPYDATA。
WM_COPYDATA消息用一个COPYDATASTRUCT结构来描述要拷贝的数据的长度和位置:
COPYDATASTRUCT STRUCT
dwData DWORD ? ; 附加字段
cbData DWORD ? ; 数据长度
lpData DWORD ? ; 数据位置指针
COPYDATASTRUCT ENDS
其中的dwData字段是一个备用的字段,可以存放任何值,例如,读者有可能向另外的进程发送数据的同时用一个数字来说明数据的类型,那么就可以把这个字段用上去;cbData字段规定了发送的字节数,lpData字段是指向待发送数据的指针。填充好数据结构后,用SendMessage函数就可以将数据发送给目标窗口过程:
.data
stCopyData COPYDATASTRUCT <>
.code
...
invoke SendMessage,hDestWnd,WM_COPYDATA,hWnd,addr stCopyData
例句中的hDestWnd为目标窗口句柄;wParam指定为hWnd,是当前窗口的句柄;lParam指向已经填充完毕的COPYDATASTRUCT结构。
Windows收到WM_COPYDATA消息后,会根据cbData字段的长度创建一块共享内存,并把lpData所指的数据拷贝到共享内存中,然后定位该共享内存在目标进程中的地址,把该地址作为新的地址添加到COPYDATASTRUCT结构的lpData字段中,最后将经过处理的COPYDATASTRUCT结构发送给目标窗口过程,目标窗口过程就可以根据结构中的字段来定位数据了。目标窗口过程返回后,Windows释放掉共享内存,SendMessage函数返回。
光盘Chapter04\SendMessage-1目录中的源代码演示了WM_COPYDATA消息的使用方法,读者可自行对比该例子和上一个例子的区别。
【学习笔记】
;Receive.asm 从一个程序向另一个窗口程序发送消息 [之消息接收程序]
; 使用 nmake 或下列命令进行编译和链接:
; ml /c /coff Receive.asm
; Link /subsystem:windows Receive.obj
.386
.model flat,stdcall
option casemap:none
;include 文件定义
include C:/masm32/include/windows.inc
include C:/masm32/include/gdi32.inc
includelib C:/masm32/lib/gdi32.lib
include C:/masm32/include/user32.inc
includelib C:/masm32/lib/user32.lib
include C:/masm32/include/kernel32.inc
includelib C:/masm32/lib/kernel32.lib
;数据段
.data?
hInstance dword ?
hWinMain dword ?
szBuffer byte 512 dup(?)
.const
szClassName byte 'MyClass', 0
szCaptionMain byte 'Receive Message', 0
szReceive byte 'Receive WM_COPYDATA message', 0dh, 0ah
byte 'length:%08x', 0dh, 0ah
byte 'text address: %08x', 0dh, 0ah
byte 'text: "%s"', 0dh, 0ah, 0
;代码段
.code
;窗口过程
_ProcWinMain proc uses ebx edi esi, hWnd, uMsg, wParam, lParam
mov eax, uMsg
.if eax == WM_CLOSE
invoke DestroyWindow, hWinMain
invoke PostQuitMessage, NULL
;收到 WM_COPYDATA 消息将消息附带的数据长度和字符串数据显示出来
.elseif eax == WM_COPYDATA
mov eax, lParam
assume eax:ptr COPYDATASTRUCT
invoke wsprintf, addr szBuffer, addr szReceive, \
[eax].cbData, [eax].lpData, [eax].lpData
invoke MessageBox, hWnd, offset szBuffer, addr szCaptionMain, MB_OK
assume eax:nothing
.else
invoke DefWindowProc, hWnd, uMsg, wParam, lParam
ret
.endif
xor eax, eax
ret
_ProcWinMain endp
_WinMain proc
local @stWndClass:WNDCLASSEX
local @stMsg:MSG
invoke GetModuleHandle, NULL
mov hInstance, eax
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
;建立并显示窗口
invoke CreateWindowEx, WS_EX_CLIENTEDGE or WS_EX_TOPMOST, offset szClassName, offset szCaptionMain, \
WS_OVERLAPPEDWINDOW, \
50, 50, 200, 150, \
NULL, NULL, hInstance, NULL
mov hWinMain, eax
invoke ShowWindow, hWinMain, SW_SHOWNORMAL
invoke UpdateWindow, hWinMain
;消息循环
.while TRUE
invoke GetMessage, addr @stMsg, NULL, 0, 0
.break .if eax == 0
invoke TranslateMessage, addr @stMsg
invoke DispatchMessage, addr @stMsg
.endw
ret
_WinMain endp
main proc
call _WinMain
invoke ExitProcess, 0
main endp
end main
发送方源码:
;Send.asm 从一个程序向另一个窗口程序发送消息 之 [发送程序]
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;使用 nmake 或下列命令进行编译和链接:
;ml /c /coff Send.asm
;Link /subsystem:windows Send.obj
.386
.model flat,stdcall
option casemap:none
;include 文件定义
include C:/masm32/include/windows.inc
include C:/masm32/include/user32.inc
includelib C:/masm32/lib/user32.lib
include C:/masm32/include/kernel32.inc
includelib C:/masm32/lib/kernel32.lib
;数据段
.data
hWnd dword ?
szBuffer byte 512 dup(?)
stCopyData COPYDATASTRUCT <>
szCaption byte 'SendMessage', 0
szStart byte 'Press OK to start SendMessage, text address:%08x!', 0
szReturn byte 'SendMessage returned!', 0
szDestClass byte 'MyClass', 0 ;目标窗口的窗口类
szText byte 'Text send to other windows', 0
szNotFound byte 'Receive Message Window not found!', 0
;代码段
.code
main proc
invoke FindWindow, addr szDestClass, NULL
.if eax
mov hWnd, eax ;找到目标窗口则发送消息
invoke wsprintf, addr szBuffer, addr szStart, addr szText
invoke MessageBox, NULL, offset szBuffer, offset szCaption, MB_OK
mov stCopyData.cbData, sizeof szText
mov stCopyData.lpData, offset szText
invoke SendMessage, hWnd, WM_COPYDATA, 0, addr stCopyData
invoke MessageBox, NULL, offset szReturn, offset szCaption, MB_OK
.else
invoke MessageBox, NULL, offset szNotFound, offset szCaption, MB_OK
.endif
invoke ExitProcess, 0
main endp
end main
编译运行,先运行Receive.exe,再运行Send.exe

点击【确定】按钮

4.3.3 SendMessage和PostMessage函数的区别
从逻辑上看,SendMessage函数相当于直接调用其他窗口的窗口过程来处理某个消息,并等待窗口过程的返回,在函数返回后,目标窗口过程必定已经处理了该消息。PostMessage函数则将消息放入目标窗口的消息队列中并直接返回,函数返回后,目标窗口过程可能还没有处理到该消息。
对于普通的消息来说,两个函数除了在处理速度上有所区别外,其他的表现都一模一样,但是对于WM_SETTEXT,WM_COPYDATA等在参数中用到指针的消息来说,两者就有所不同了。读者可以尝试将前面例子中的SendMessage函数改为PostMessage函数,就会发现Receive程序根本不会接收到WM_SETTEXT或者WM_COPYDATA消息。事实上,当消息参数中用到指针时,用PostMessage函数来发送消息都不会成功,PostMessage的参考文档中明确地说明,该函数不能用于任何参数中用到指针的消息。