窗口是什么?大家每天在使用Windows,屏幕上的一个个方块就是一个个窗口!那么,窗口为什么是这个样子呢?窗口就是程序吗?
回想DOS时代的计算屏幕,在1990年Windows 3.0推出之前,计算机的屏幕一直使用文本模式,黑洞洞的底色上漂浮着白色的小字。对DOS程序来说,屏幕是唯一的,上面有光标表示输入字符的位置,程序运行后往屏幕输出一些信息,退出时输出的信息就留在了屏幕上,然后是第二个程序重复这个过程,当屏幕被写满的时候,整个屏幕上卷一行,最上面一行被去掉,然后程序在最底下新空出来的一行上继续输出。
对于一个单任务的操作系统,这种方式是很合理的,因为平时使用传真机或打字机就是用上卷的方式来容纳新的内容的。但是如果是多任务呢?两个程序同时往屏幕上写东西或者两个人同时往打字机上打字,那么谁都看不懂混在一起的是什么。DOS下的TSR(内存驻留)程序是多个程序同时使用一个屏幕的例子,但实质上这并不是多任务,而是TSR将别的程序暂时挂起,挂起的程序不可能在TSR执行期间再向屏幕输出内容,TSR在输出自己的内容之前必须保存屏幕上显示的内容,并在退出的时候把屏幕恢复原来的样子,否则挂起的程序并不知道屏幕已经被改变,在这个过程中,DOS不会去干预中间发生的一切。
Windows是多任务的操作系统,可以同时运行多个程序,同样,各个程序在屏幕上的显示不能互相干扰,而且,多个程序可以看成是“同时”运行的,在后台的程序也可能随时向屏幕输出内容,这中间的高度是由Windows完成的。Windows采用的方法是给程序一块矩形的屏幕空间,这就是窗口。应用程序通过Windows向属于自己的窗口显示信息,Windows判断该窗口是不是被别的窗口挡住,并把没有挡住的部分输出到屏幕上,这样屏幕上显示的东西就不会互相覆盖而乱套。对于应用程序来说,它只需认为窗口就是自己拥有的显示空间就可以了。
窗口和程序的关系
既然不同窗口的内容就是不同程序的输出,那么一个窗口就是一个程序吗?反过来,一个程序就是一个窗口吗?
答案是否定的,一个窗口不一定就是一个程序,它可能只是一个程序的一部分。一个程序可以建立多个顶层窗口,如Windows的桌面和任务栏都是顶层窗口,但它们都属于“文件管理器”进程,所以并不是一个窗口就是一个程序的代表。Windows的窗口采用层次结构,一个窗口中可以建立多个子窗口,如窗口中的状态栏,工具栏,对话框中的按钮,文本输入框与复选框等都是子窗口。子窗口中还可以建立下一级子窗口,如Word工具栏上的字体选择框。
反过来,运行的程序并非一定就是窗口,比如悄悄在后台运行的木马程序就不会显示一个窗口向用户报告它在干什么。在Windows NT下用“任务管理器”查看,进程的数量比屏幕上的窗口多得多,意味着很多的运行程序并没有显示窗口。如果一个程序不想和用户交互,它可以选择不建立窗口。
所以本章的标题“第一个窗口程序”,指学习编写第一个以标准的窗口为界面的程序,而不是泛指Windows程序。如果要写的Win32程序不是以窗口为界面的,如控制台程序等,就不一定采用本章中提及的以消息驱动的程序结构。
虽然以窗口为界面的程序并不是所有Windows程序的必须选择,但绝大部分的应用程序是以这种方式出现的,从操作系统的名称“Windows”就可以看出这一点,了解窗口程序就是相当于在了解Windows工作方式的基础。
窗口界面
大部分的窗口看上去都是大同小异。
典型的窗口,即Windows附带的写字板程序窗口。
窗口一般由屏幕上的矩形区域组成,不同的窗口可能包括一些相同的组成部分,如标题栏、菜单、工具栏、边框和状态栏等,每个部分都有自己固定的行为模式:
·窗口边框:窗口的外沿就是窗口边框,用鼠标按住边框并拖动可以调整窗口的大小。
·标题栏:窗口的最上面是标题栏,用鼠标按住标题栏拖动可以移动窗口,双击标题栏则将窗口最大化或从最大化的状态恢复。通过标题栏的颜色可以区分窗口是不是活动窗口,同时标题栏列出了应用程序的名称。
·菜单:标题栏下面是菜单,单击菜单会弹出各种功能选择。
·工具栏:菜单的下面是工具栏,工具栏上用图标的方式列出最常用的功能,相当于菜单的快捷方式。
·图标和最小化、最大化与关闭按钮:图标位于标题栏的左边,三个控制按钮则位于标题栏的右边。单击图标会弹出一个系统菜单,双击图标则相当于按下了关闭按钮。最小化、最大化按钮用来控制窗口的大小。
·状态栏:位于窗口的最下面,用来显示一些状态信息。
·客户区:窗口中间用来工作或输出的区域,叫做窗口的客户区,把窗口看做是一张白纸的话,客户区就是白纸中真正用来写东西的区域,程序在这里和用户进行交互。
·滚动条:如果客户区太小不足以显示全部内容,右边或底部可能还有滚动条,拖动它可以滚动窗口的客户区,以便看到其他的内容。
虽然大部分的窗口看上去都差不多,但并不是每个窗口都有这些东西,也许有的窗口就没有图标和最小化、最大化框,有的没有工具栏或状态栏,有的没有标题栏,而有的就干脆是个奇怪的形状,如Office帮助中助手,那些小狗小猫都些不折不扣的窗口,Windows的桌面和桌面下面的任务栏也都是窗口,就连屏幕保护的黑屏幕也是一个大小为整个屏幕、没有标题栏和边框的窗口!
一致的窗口形状和行为模式为Windows用户提供了一致的用户界面,几乎所有的窗口程序都在菜单的第一栏设置有关文件的操作和退出功能、最后一栏设置程序的帮助,相同的功能在工具栏上的图标也是大同小蒸发量的,用户可以不再像在DOS下那样,对不同的程序需要学习不同的界面,用户自从学会使用第一个软件起,就基本学会了所有Windows软件的使用模式,而且可以通过相似的菜单、工具栏等来发掘程序的新功能。窗口的菜单和客户区则是最个性化的东西,菜单随程序的不同而不同,而客户区则是窗口程序的输出区域,不同的程序在客户区内显示了不同的内容。
窗口程序是怎么工作的
窗口程序的运行模式
对程序员来说,要了解的不仅是用户可以看到的部分,还必须了解窗口底下的东西,了解用怎样的程序结构来实现窗口的行为模式。
DOS程序员熟悉的是顺序化的、按过程驱动的程序设计方法。程序有明显的开始、明显的过程和明显的结束,由程序运行的阶段来决定用户该做什么。而窗口程序是事件驱动的(再次提醒:这里是“窗口程序”,而不是“Windows程序”,因为和窗口有关的程序者事件驱动的,其他的Windows程序可能并不这样工作,如控制台程序的结构还是同DOS程序一样是顺序化的,但与窗口相关的Windows程序占了绝大多数,所以大部分的书籍中讲到Windows程序就认为是事件驱动的程序),用户可能随时发出各种消息,如操作的过程中觉得窗口不够大了,就马上会拖动边框,程序必须马上调整客户区的内容以适应新的窗口大小;用户觉得想先干别的事情,可能会把窗口最小化,关闭按钮也有可能随时被按下,这意味着程序要随时可以处理退出的请求。如果非要规定干活的时候不能移动窗口与调整大小,那么这些窗口就会呆在桌面上一动不动。
窗口程序在结构上和DOS程序有很大的不同,窗口程序实现大部分功能的代码应该呆在同一个模块——消息处理模块,这个模块可以随时应付所有类型的消息,只有这样才能随时响应用户的各种操作。
下面是地地道道的Win32汇编窗口程序:
FirstWindow源代码。
.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 '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 @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
编译运行后,窗口出来了,对于这个窗口,用户可以拖动边框去改变大小、按标题栏上的按钮来最大化和最小化,当光标到边框的时候,会自动变成箭头,总之,这个窗口包括了一个典型窗口的所有特征。
接下来开始分析源代码,看了这三页多的源代码,第一个感觉是什么?是不是想撤退了?90%的人有同样的感觉,别急,过了这一关,Win32汇编的入门就成功了一半了,所以千万要挺住!有个振奋人心的消息是,这个程序是大部分窗口程序的模板,以后要写一个新的程序,把它拷贝过来再往中间添砖加瓦就是了,功夫一点都不白费。
先静下心来分析一下程序的结构,首先是注释,模式定义,include,.data数据段,.code代码段。
程序入口是start,然后执行了一下_WinMain子程序,完成后就是程序退出的函数ExitProcess,再看_WinMain的结构,前面是顺序下来的几个API调用:
GetMoudleHandle à RelZeroMemory à LoadCursor à RegisterClassEx à CreateWindowEx à ShowWindow à UpdateWindow
从名称上就能看出它们的用途,很明显,窗口是在CreateWindowEx处建立的,ShowWindow则是把窗口显示在屏幕上,这些代码是窗口的建立过程。
接下来,就是一个由3个API组成的循环了:
GetMessage à TranslateMessage à DispatchMessage
很明显,这是和消息有关的循环,因为API名称中都带有Message字样,如果退出这个循环,程序也就结束了,这个循环叫做消息循环。
设置_WinMain子程序并不是必须的,可以把_WinMain的所有代码放到主程序中,没有任何影响,之所以这样只是为将这里使用的变量定义成局部变量,这样可以方便移植。
看了程序的流程,似乎没有什么地方涉及窗口的行为,如改变大小和移动位置的处理等。
再看源程序,除了_WinMain,还有一个子程序_ProcWinMain,但除了在WNDCLASSEX结构的赋值中提到过它,好像就没有什么地方要用到这个子程序,起码在自己编写的源代码中没有任何一个地方调用过它。
再看ProWinMain,它是一个分支结构处理的子程序,功能是把参数uMsg取出来,根据不同的uMsg执行不同的代码,完了以后就退出了,中间也没有任何东西和主程序有关联。
第一个窗口程序就是由这么两个似乎是风马牛不相及的部分组成的,但它确实能工作,对于写惯了DOS汇编的程序员来说,这似乎不可理解。下面来看看这么一个陌生而奇怪的程序是如何工作的。
窗口程序的运行过程
在屏幕上显示一个窗口的过程一般有以下步骤,这就是主程序的结构流程:
1)得到应用程序的句柄(GetMoudleHandle)。
2)注册窗口类(RegisterClassEx)。在注册之前,要先填写RegisterClassEx的参数WNDCLASSEX结构。
3)建立窗口(CreateWindowEx)。
4)显示窗口(ShowWindow)。
5)刷新窗口客户区(UpdateWindow)。
6)进入无限的消息获取和处理的循环。首先获取消息(GetMessage),如果有消息到达,则将消息分派到回调函数处理(DispatchMessage),如果消息是WM_QUIT,则退出循环。
程序的另一半_ProcWinMain子程序是用来处理消息的,它就是窗口的回调函数(CallBack),也叫做窗口过程,之所以是回调函数是因为它是由Windows而不是我们自己调用的,我们调用DispatchMessage,而DispatchMessage再回过来调用窗口过程。
所有的用户操作都是通过消息来传给应用程序的,如用户按键,鼠标移动,选择了菜单和拖动了窗口等,应用程序中由窗口过程接收消息并处理,在例子程序中就是_ProcWinMain。窗口过程构造了一个分支结构,对应不同的消息执行不同的代码,所以一个应用程序中几乎所有的功能代码都集中在窗口过程里。
Windows在系统内部有一个系统消息队列,当输入设备有所动作的时候,如用户按动了键盘、移动了鼠标,按下或放开了鼠标等,Windows都会产生相应的记录放在系统消息队列里,每个记录中包含消息的类型、发生的位置(如鼠标在什么坐标移动)和发生的时间等信息。
同时,Windows为每个程序(严格地说是每个线程)维护一个消息队列,Windows检查系统消息队列里消息的发生位置,当位置位于某个应用程序的窗口范围内的时候,就把这个消息派送到应用程序的消息队列里。
当应用程序还没有来取消息的时候,消息就暂时保留在消息队里,当程序中的消息循环执行到GetMessage的时候,控制权转移到GetMessage所在的USER32.DLL中,USER32.DLL从程序消息队列中取出一条消息,然后把这条消息返回应用程序。
应用程序可以对这条消息进行预处理,如可以用TranslateMessage把基于键盘扫描码的按键消息转换成基于ASCII码的键盘消息,以后也会用到TranslateAccelerator把键盘快捷键转换成命令消息,但这个步骤不是必需的。
然后应用程序将处理这个消息,但方法不是自己直接调用窗口过程来完成,而是通过DispatchMessage间接调用窗口过程,Dispatch的英文含义是“分派”,之所以是“分派”,是因为一个程序可能建有不止一个窗口,不同的窗口消息必须分派给相应的窗口过程。当控制权转移到USER32.DLL中的DispatchMessage时,DispatchMessage找出消息对应窗口的窗口过程,然后把消息的具体信息当做参数来调用它,窗口过程根据消息找到对应的分支去处理,然后返回,这时控制权回到DispatchMessage,最后DispatchMessage函数返回应用程序。这样,一个循环就结束了,程序又开始新一轮的GetMessage。
窗口程序 à GetMessage(USER32.DLL) –> TranslateMessage(USER32.DLL) –> DispatchMessage(USER32.DLL) à 窗口过程 à USER32.DLL à 窗口程序
为什么要由Windows来调用窗口过程,程序取了消自以后自己处理不是更简便吗?事实上并非如此,如果程序自己处理消息的“分派”,就必须自己维护本程序所属窗口的列表,当程序建立的窗口不止一个的时候,这个工作就变得复杂起来;另一个原因是:别的程序也可能用SendMessage通过Windows直接调用你的窗口过程;第三个原因:Windows并不是把所有的消息都放进消息队列,有的消息是直接调用窗口过程处理的,如WM_SETCURSOR等实时性很强的消息,所以窗口过程必须开放给Windows。
应用程序之间也可以互发消息,PostMessage是把一个消息放到其他程序的消息队列中,目标程序收到了这个条消息就把它放入该程序的消息队列去处理;而SendMessage则越过消息队列直接调用目标程序的窗口过程,窗口过程返回以后才从SendMessage返回。
窗口过程是由Windows回调的,Windows又是怎么知道往哪里回调呢?答案是我们在调用RegisterClassEx函数的时候告诉了Windows。