窗口程序的运行模式
DOS程序员熟悉的是顺序化的、按过程驱动的程序设计方法。程序有明显的开始、明显的过程和明显的结束,由程序运行的阶段来决定用户该做什么。
但是到了Windows我们不能这么干,思维必须要“升级”一下,这也就是为什么面对过程的程序员要转向面对对象显得特别困难的原因。
现在请升级:窗口程序是事件驱动的,用户可能随时发号各种不同命令,如觉得尺寸太小。。。
窗口程序在结构上和DOS程序有很大的不同,窗口程序实现大部分功能的代码应该呆在同一个模块 —— 消息处理模块,这个模块可以随时应付所有类型的消息,只有这样才能随时响应用户的各种操作。
这么一看来,小甲鱼可以给大家断言:如果都是男人,那么窗口程序是三好男人,因为他时刻准备着,而DOS程序是充满大男人主义的家伙,什么事都要按照他自己的流程来做。
框架分析
注释……
模式定义……
include……
.data 数据段……
.code 代码段
读代码首先要找入口,跟着入口的思路,按照调用顺序跟着走就容易读懂。
start -> _WinMain -> ExitProcess -> 剧终!
那我们发觉大结构变简单了,接着我们有开始蛋疼。因为 _WinMain 看上去一点也不单纯,上边还多了个貌似没有用到的 _ProcWinMain 。。。
那我们接着来细化分析,大家跟着小甲鱼的思路走就不会乱了:从调用的 API函数入手。
GetModuleHandle -> RtlZeroMemory -> LoadCursor -> RegisterClassEx -> CreateWindowEx -> ShowWindow -> UpdateWindow
接下来,就是一个由3个API组成的循环了:
GetMessage -> TranslateMessage -> DispatchMessage
很明显,这是和消息有关的循环,因为名称中都带有Message字样,如果退出这个循环,程序也就结束了,这个循环叫做消息循环。
关于消息机制,在前边的前置知识中已经给大家做了介绍,不过那时候我们没有结合实例,所以大家估计也忘记了。。。咱回顾下!
分析到这里,我们大致了解了程序的流程,似乎没有什么地方涉及窗口的行为,如改变大小和移动位置的处理等。
再看回源程序,除了_WinMain,还有一个子程序_ProcWinMain,但除了在WNDCLASSEX结构的赋值中提到过它,好像就没有什么地方要用到这个子程序。。。
而细看 _ProcWinMain 的代码,发觉它的功能是把参数uMsg取出来,根据不同的uMsg执行不同的代码,完了以后就退出了。
第一个Windows 32位汇编语言窗口程序就是由这么两个似乎是风马牛不相及的部分组成的,但它确实能工作,对于写惯了DOS汇编的程序员来说,这似乎不可理解。
下节课我们进一步来了解 Windows 窗口程序的运行过程。
窗口程序的运行过程
在屏幕上显示一个窗口的过程一般有以下步骤,这就是主程序的结构流程:
得到应用程序的句柄(GetModuleHandle)
注册窗口类(RegisterClassEx)
建立窗口(CreateWindowEx)
显示窗口(ShowWindows)
刷新窗口客户区(UpdateWindow)
进入无限的消息获取和处理的循环
正是我们之前分析的几个阶段(IDA图形分解)
程序的另一半 _ProcWinMain 子程序是用来处理消息的,它就是窗口的回调函数(Callback),也叫做窗口过程,之所以是回调函数是因为它是由 Windows 而不是我们自己调用的。
Windows 接收到属于该窗口的消息后就会调用_ProcWinMain 子程序,并把消息当参数uMsg传递进去接受处理。
我们调用DispatchMessage,而DispatchMessage 再回过来调用窗口过程。
所有的用户操作都是通过消息来传给应用程序。
如用户按键,鼠标移动,选择了菜单和拖动了窗口等,应用程序中由窗口过程接收消息并处理,在例子程序中就是_ProcWinMain。
窗口过程构造了一个分支结构,对应不同的消息执行不同的代码,所以一个应用程序中几乎所有的功能代码都集中在窗口过程里。
以下内容请在高清组图的配合下探讨:
Windows在系统内部有一个系统消息队列,当输入设备有所动作的时候,如用户按动了键盘、移动了鼠标,按下或放开了鼠标等,Windows都会产生相应的记录放在系统消息队列里,如图中的箭头a和b所示,每个记录中包含消息的类型、发生的位置(如鼠标在什么坐标移动)和发生的时间等信息。
同时,Windows为每个程序(严格地说是每个线程)维护一个消息队列,Windows检查系统消息队列里消息的发生位置,当位置位于某个应用程序的窗口范围内的时候,就把这个消息派送到应用程序的消息队列里,如图中的箭头c所示。
当程序中的消息循环执行到GetMessage的时候,控制权转移到 GetMessage 所在的USER32.DLL中(箭头1),USER32.DLL从程序消息队列中取出一条消息(箭头2),然后把这条消息返回应用程序(箭头3)。
应用程序可以对这条消息进行预处理,如可以用TranslateMessage 把基于键盘扫描码的按键消息转换成基于 ASCII码的键盘消息,以后也会用到TranslateAccelerator 把键盘快捷键转换成命令消息,但这个步骤不是必需的。
然后应用程序将处理这条消息,但方法不是自己直接调用窗口过程来完成,而是通过DispatchMessage间接调用窗口过程。
Dispatch的英文含义是“分派”,之所以是“分派”,是因为一个程序可能建有不止一个窗口,不同的窗口消息必须分派给相应的窗口过程。
当控制权转移到USER32.DLL中的DispatchMessage时,DispatchMessage找出消息对应窗口的窗口过程,然后把消息的具体信息当做参数来调用它(箭头5),窗口过程根据消息找到对应的分支去处理,然后返回(箭头6),这时控制权回到DispatchMessage,最后DispatchMessage函数返回应用程序(箭头7)。
PostMessage 和 SendMessage
应用程序之间也可以互发消息,PostMessage 是把一个消息放到其他程序的消息队列中,如图中箭头d所示,目标程序收到了这条消息就把它放入该程序的消息队列去处理。
而 SendMessage 则越过消息队列直接调用目标程序的窗口过程(如图中箭头I所示),窗口过程返回以后才从 SendMessage 返回(如图中箭头II所示)。
最后补充
常见问题一:为什么要由Windows来调用窗口过程,程序取了消息以后自己处理不是更简便吗?
事实上并非如此,如果程序自己处理消息的“分派”,就必须自己维护本程序所属窗口的列表,当程序建立的窗口不止一个的时候,这个工作就变得复杂起来;
另一个原因是:别的程序也可能用SendMessage通过Windows直接调用你的窗口过程;
第三个原因:Windows并不是把所有的消息都放进消息队列,有的消息是直接调用窗口过程处理的,如WM_SETCURSOR等实时性很强的消息,所以窗口过程必须开放给Windows。
说白了就是你经营一个帝国,你必须自己掌权!
常见问题二:窗口过程是由Windows回调的,Windows又是怎么知道往哪里回调呢?答案是我们在调用RegisterClassEx函数的时候告诉了Windows。
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Sample code for < Win32ASM Programming 2nd Edition>
; by 罗云彬, http://asm.yeah.net
; change by 小甲鱼, http://www.fishc.com
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 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 dd ?
hWinMain dd ?
.const
szClassName db 'MyClass',0
szCaptionMain db 'My first Window !',0
szText db 'Welcome to fishc.com!',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 @stPs
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,NULL
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
end start