本文的知识点均来自罗云彬的《Win32下的汇编程序设计》,有兴趣的请读原书。
一个窗口就是一个程序吗?反过来,一个程序就是一个窗口吗?
答案是:一个窗口不一定是一个程序,它可能只是一个程序的一部分。一个程序可以建立多个顶层窗口,如Windows的桌面和任务栏都是顶层窗口,但它们都属于“文件管理器”进程,所有并不是一个窗口就是一个程序的代表。Windows的窗口采用层次结构,一个窗口中可以建立多个子窗口,如窗口中的状态栏,工具栏,对话框中的按钮,文本输入框与复选框等都是子窗口。子窗口中还可以再建立下一级子窗口,如Word工具栏上的字体选择框。
反过来,运行的程序并非一定就是窗口,比如悄悄在后台运行的***程序就不会显示一个窗口向用户报告它在干非法勾当。在Windows NT下用“任务管理器”查看,进程的数量比屏幕上的窗口多得多,意味着很多的运行程序并没有显示窗口。如果一个程序不想和用户交互,它可以选择不建立窗口。
Win32的消息驱动的程序结构是对有窗口的win32程序而言的,如果程序中并没有窗口,就不会采用消息驱动机制了。另外说明一点,控制台方式也是windows程序中的一种界面程序。
窗口程序如何工作?
窗口程序是事件驱动的,用户可能随时发出各种消息,如拖动边框,窗口最小化,按钮响应。这意味着程序要随时能够处理请求。提示一下:这里是“窗口程序”而不是“Windows程序”,因为和窗口有关的程序才是事件驱动的,其他的Windows可能并不这样工作,如控制台程序的结构还是同DOS程序一样是顺序化的,但与窗口相关的Windows程序占了绝大多数,所以大部分书籍中讲到Windows程序就认为是事件驱动的程序。
DOS程序与窗口程序的对比
这里以比较命令comp为例。DOS下,程序运行时先提示输入第一个文件名,然后是输入第二个文件名,程序比较后退出,同时把结果输出在屏幕上。假如有一个窗口版的comp程序,那么运行时会在屏幕上出现一个对话框,上面有两个文本框用来输入两个文件名,还会有个“比较”按钮,按下后开始比较文件,用户可以随时按下“关闭”按钮来退出程序。
两种程序的运行会有相当大的不同,如下图所示,DOS程序必须按照顺序运行,当运行到输入第二个文件名时,用户不可能回到第一步修改第一个文件名,这时候用户也不能退出(除非用户强制用Ctrl+C,但这不是程序的本意);而在窗口程序中用户可以随意选择先输入哪个文件名,同时也可以对窗口进行各种操作,当用户做任何一个操作的时候,相当于发出了一个消息,这些消息没有任何顺序关系,程序中必须随时准备处理不同的消息。
这就决定了窗口程序必定在结构上和DOS程序有很大的不同,窗口程序实现大部分功能的代码应该呆在同一个模块-----图中的“消息处理”模块中,这个模块可以随时应付所有类型的消息,只有这样才能随时响应用户的各种操作。
窗口程序的运行过程
在屏幕上显示一个窗口的过程一般有以下步骤,这就是主程序的结构流程。
1. 得到应用程序的句柄。
2. 注册窗口类。在注册之前,要先填写RegisterClassEx的参数WNDLCASSEX结构。
3. 建立窗口。
4. 显示窗口。
5. 刷新窗口客户区。
6. 进入无限的消息获取和处理的循环。首先获取消息,如果有消息到达,则将消息分派到回调函数处理,如果消息是 WM_QUIT,则退出循环。
程序中有一个函数(这里取名叫_ProcWinMain)是用来处理消息的,它就是窗口的回调函数,也叫窗口过程,之所以是回调函数是因为它是由Windows而不是我们自己调用的,我们调用DispatchMessage,而DispatchMessage在自己的内部回过来调用窗口过程。
所有的用户操作都是通过消息来传给应用程序的,如用户按键,鼠标移动,选择了菜单和拖动了窗口等,应用程序中有窗口过程接收消息并处理。由于窗口过程构造了一个分支结构,对应不同的消息执行不同的代码,所以一个应用程序中几乎所有的功能代码都集中在窗口过程里。
窗口程序运行中消息传输的流程可以由下图表示:
Windows在系统内部有一个系统消息队列,当输入设备有所动作的时候,如用户按动了键盘,移动了鼠标,按下或放开了鼠标等,Windows都会产生相应的记录放在系统消息队列里,如上图中的箭头a和b所示,每个记录中包含消息的类型,发生的位置(如鼠标在什么坐标移动)和发生的时间等信息。
同时,Windows为每个程序(严格说是每个线程)维护一个消息队列。Windows检查系统消息队列里消息的发生位置,当位置位于某个应用程序的窗口范围内的时候,就把这个消息派送到应用程序的消息队列里。如图中的箭头c所示。
当应用程序还没有来去消息的时候,消息就暂时保留在消息队列里,当程序中的消息循环执行到GetMessage的时候,控制权转移到GetMessage所在的USER32.DLL中(箭头1),USER32.DLL从程序消息队列中取出一条消息(箭头2),然后把这条消息返回应用程序(箭头3)。
应用程序可以对这条消息进行预处理,如可以用TranslateMessage把基于键盘扫描码的按键消息转换成基于ASCII码的键盘消息,以后也会用到TranslateAccelerator把键盘快捷键转换成命令消息,但这个步骤不是必需的。
然后应用程序将处理这条消息,但方法不是自己直接调用窗口过程来完成,而是通过DispatchMessage间接调用窗口过程,Dispatch的英文含义是分派,之所以是分派,是因为一个程序可能建有不止一个窗口,不同的窗口消息必须分派给相应的窗口过程。当控制权转移到USER32.DLL中的DispatchMessage时,它会找出消息对应窗口的窗口过程,然后把消息的具体信息当做参数来调用它(箭头5),窗口过程根据消息找到对应的分支去处理,然后返回(箭头6),这是控制权回到DispatchMessage,最后DispatchMessage函数返回应用程序(箭头7)。这样,一个循环就结束了,程序又开始新一轮的GetMessage。
注意:为什么要有Windows来调用窗口过程,而不是程序取了消息以后自己处理?事实上,如果程序自己处理消息的分派,就必须自己维护本程序所属窗口的列表,当程序建立的窗口不止一个的时候,这个工作就变得复杂起来;另一个原因是:别的程序也可能用SendMessage通过Windows直接调用你的窗口过程;第三个原因:Windows并不是把所有的消息都放进消息队列,有的消息是直接调用窗口过程处理的,如WM_SETCURSOR等实时性很强的消息,所以窗口过程必须开放给Windows。
应用程序之间也可以互发消息,PostMessage是把一个消息队列放到其他程序的消息队列中,如箭头d所示,目标程序收到了这条消息就把它放入该程序的消息队列去处理;而SendMessage则越过消息队列直接调用目标程序的窗口过程,如箭头I所示,窗口过程返回以后才从SendMessage返回,如箭头II所示。
窗口过程是由Windows回调的,Windows又是怎么知道往哪里回调呢?答案是我们在调用RegisterClassEx函数的时候已经把窗口过程的地址告诉了Windows。
模块的概念:一个模块代表的是一个运行中的exe文件或dll文件,用来代表这个文件中所有的代码和资源,磁盘上的文件不是模块,装入内存后运行时就叫模块。一个应用程序调用其它DLL中的API时,这些DLL文件被装入内存,就产生了不同的模块,为了区分地址空间中的不同模块,每个模块都有一个唯一的模块句柄来标记.
HINSTANCE和HMODULE是一样的,之所以有两个是因为在Win16的程序中,不同运行程序的地址空间并非是完全隔离的,一个可执行文件运行后形成“模块”,多次加载同一个可执行文件时,这个“模块”是公用的,为了区分多次加载的“拷贝”,就把每个拷贝叫做实例,每个实例均用不同的的实例句柄值来标记它们。
句柄只是一个数值而已,它的值对程序来说是没有意义的,它只是Windows用来表示各种资源的编号而已,可见只有Windows才知道怎么使用它来引用各种资源。Windows中几乎所有的东西都是用句柄来标识的,文件句柄,窗口句柄,线程句柄和模块句柄等,同样道理,不必关心它们的值究竟是多少,拿来用就是了!
一个窗口过程可以为多个窗口服务,只要这些窗口是基于同一个窗口类建立的。Windows中不同应用程序中的按钮和文本框的行为都是一样的,就是因为它们是基于相同的Windows预定义类建立的,它们背后的窗口过程其实是同一段代码。
对话框。
一个线程只有一个消息队列。但是消息循环可能不止一个。比如模态对话框,会内建一个消息循环,在这个消息循环中把消息发送给对话框管理器,对话框管理器在处理消息的过程中会调用用户定义的对话框过程,当对话框关闭的时候,Windows退出内建的消息循环,并从DialogBoxParam函数返回。而对于非模态对话框,CreateDialogParam函数在创建对话框后直接返回,对话框窗口的消息是通过用户程序中的消息循环派送的。
由于模态对话框的特征,使得用它来做小程序的主窗口非常方便,因为只需调用DialogBoxParam函数就可以搞定了,既不用注册窗口类,也不用写消息循环。但这方法的缺点就是无法使用依赖消息循环来完成的功能,很明显,加速键就不能用了。