目录
1. Windows系统 基本原理
2. 需要什么函数库(.LIB)
2.1 C Runtimes:
2.2 Windows API
3. 需要什么头文件(.H)
4. Windows 程序运行的本质
5. 窗口类的注册与窗口的诞生
6.消息
6.1 消息分类:
7. 消息队列
8. 消息循环
9. 窗口的生命中枢:窗口函数(窗口过程)
10. 对话框窗口的运作
11. 资源描述文件(.RC)
12. Windows 程序的生与死
13. 问答
参考:侯杰的书,感谢JJ深入浅出的介绍!
第1章 Win32 基本程序观念 | 深入浅出MFC (gitbooks.io)
1. Windows系统 基本原理
Windows 操作系统本身,主要是 Windows 三大模块 GDI32.DLL 和 USER32.DLL 和 KERNEL32.DLL提供的。
下图是Windows SDK程序(Win32)的开发流程图:
我们基于Windows编程,肯定不是从零开始搭建,是基于Windows函数库,以及微软封装的其他功能函数库开发的。
2. 需要什么函数库(.LIB)
编译器驱动程序读取源程序文件.c,并把它翻译成一个可执行目标文件exe。这个翻译过程分为四个阶段:预处理(Preprocessing)、编译(Compilation)、汇编(Assembly)、链接(Linking)。执行这四个阶段的程序(预处理器、编译器、汇编器、和链接器)一起构成了编译系统。
参考: 程序详细编译过程(预处理、编译、汇编、链接) - 知乎 (zhihu.com)
在链接时期,链接器仍需先为调用者(应用程序本身)准备一些适当的资讯,才能够在运行时顺利「跳」到 DLL 执行。
Windows支持静态链接库lib(链接时期),和动态链接库dll(运行时期)。
文件名为 .dll 者是动态链接函数库(DLL,Dynamic Link Library),事实上.exe、.dll、.fon、.mod、.drv、.ocx 都是所谓的动态链接函数库。
Windows 程序调用的函数可分为 C Runtimes 以及 Windows API 两大部分。
2.1 C Runtimes:
-
LIBC.LIB - 这是C Runtime 函数库的静态链接版本。
-
MSVCRT.LIB - 这是C Runtime 函数库动态链接版本(MSVCRT40.DLL)的 import 函数库。如果链接此一函数库,你的程序执行时必须有 MSVCRT40.DLL 在场。
2.2 Windows API
-
Windows 的三大模块所对应的 import 函数库分别为 GDI32.LIB 和 USER32.LIB 和 KERNEL32.LIB,动态库对应的是 GDI32.DLL 和 USER32.DLL 和 KERNEL32.DLL。
3. 需要什么头文件(.H)
所有Windows程序都必须含入WINDOWS.H,不过,WINDOWS.H 只照顾三大模块所提供的API 函数。如果你用到其它 system DLLs,就得含入对应的头文件。
4. Windows 程序运行的本质
Windows 程序运行的本质是以消息为基础,以事件为驱动。Windows程序的事件驱动特性,包括消息的产生、获得、分派、判断、处理。
Windows 程序的进行系依靠外部发生的事件来驱动。换句话说,程序不断等待(利用一个 while 循环),等待任何可能的输入,然后做判断,然后再做适当的处理。上述的「输入」是由操作系统捕捉到之后,以消息形式(一种数据结构)进入程序之中。操作系统如何捕捉外围设备(如键盘和鼠标)所发生的事件呢?噢,USER 模块掌管各个外围的驱动程序,它们各有侦测循环。
如果把应用程序获得的各种「输入」分类,可以分为由硬件装置所产生的消息(如鼠标移动或键盘被按下),放在系统队列(system queue)中,以及由 Windows 系统或其它Windows 程序传送过来的消息,放在程序队列(application queue)中。以应用程序的眼光来看,消息就是消息,来自哪里或放在哪里其实并没有太大区别,反正程序调用GetMessage API就取得一个消息,程序的生命靠它来推动。所有的GUI系统,包括UNIX的X Window 以及OS/2的Presentation Manager,都像这样,是以消息为基础的事件驱动系统。
Windows 程序的本体与操作系统之间的关系图:
WinMain 则是Windows 程序的进入点:
nt CALLBACK WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow)
{
...
} // 在 Win32 中 CALLBACK 被定义为 __stdcall,是一种函数调用习惯,关系到参数挤压到堆栈的次序,以及处理堆栈的责任归属。其它的函数调用习惯还有_pascal和_cdecl
当Windows的「外壳」(shell,例如 Windows 3.1的程序管理员或Windows 95的文件总管)侦测到使用者意欲执行一个 Windows 程序,于是调用加载器把该程序加载,然后调用C startup code,后者再调用WinMain,开始执进程序。
5. 窗口类的注册与窗口的诞生
一开始,Windows 程序必须做些初始化工作,为的是产生应用程序的工作舞台:窗口。这没有什么困难,因为API函数CreateWindow完全包办了整个巨大的工程。但是窗口产生之前,其属性必须先设定好。所谓属性包括窗口的「外貌」和「行为」,一个窗口的边框、颜色、标题、位置等等就是其外貌,而窗口接收消息后的反应就是其行为(具体地说就是指窗口函数本身)。程序必须在产生窗口之前先利用 API 函数 RegisterClass设定属性(我们称此动作为注册窗口类)。RegisterClass 需要一个大型数据结构WNDCLASS 做为参数,CreateWindow 则另需要11个参数。
RegisterClass与CreateWindow关系图
可以清楚看出一个窗口类牵扯的范围多么广泛,其中wc.lpfnWndProc所指定的函数就是窗口的行为中枢,也就是所谓的窗口函数。注意,CreateWindow只产生窗口,并不显示窗口,所以稍后我们必须再利用 ShowWindow 将之显示在屏幕上。又,我们希望先传送个WM_PAINT给窗口,以驱动窗口的绘图动作,所以调用UpdateWindow。
6.消息
消息,就是指Windows发出的一个通知,告诉应用程序某个事情发生了。例如,单击鼠标、改变窗口尺寸、按下键盘上的一个键都会使Windows发送一个消息给应用程序,它被定义为:
typedef struct {
HWND hwnd; //窗口句柄, 发生在哪个窗口上
UINT message; //消息标识号 ( WM_MOUSEMOVE, WM_LBUTTONDOWN, ... )
WPARAM wParam; //消息参数1
LPARAM lParam; //消息参数2
DWORD time;
POINT pt;
} MSG, *PMSG;
其中的成员变量 message 是 WM_SIZE 、 WM_COMMAND 、 WM_QUIT 等等消息标识。hwnd 中是这个消息所在的窗口句柄。time和pt由系统生成这个消息时填充。
一个消息结构体包含了该事件 所有完备信息,当应用程序收到该消息时,就可以做出相应处理了。
6.1 消息分类:
-
队列消息和非队列消息
从消息的发送途径上看,消息分两种:队列消息和非队列消息。队列消息送到系统消息队列,然后到线程消息队列;非队列消息直接送给目的窗口过程。
除了键盘、鼠标消息以外,队列消息还有WM_PAINT、WM_TIMER和WM_QUIT。这些队列消息以外的绝大多数消息是非队列消息。
-
系统消息和应用程序消息
从消息的来源来看,可以分为:系统定义的消息和应用程序定义的消息。
系统消息ID的范围是从0到WM_USER-1,或0X80000到0XBFFFF;应用程序消息从WM_USER(0X0400)到0X7FFF,或0XC000到0XFFFF;WM_USER到0X7FFF范围的消息
由应用程序自己使用;0XC000到0XFFFF范围的消息用来和其他应用程序通信,为了ID的唯一性,使用::RegisterWindowMessage来得到该范围的消息ID。
-
窗口消息,命令消息,控件通知消息,根据处理过程的不同,可以分为三类:窗口消息,命令消息,控件通知消息。
(1) 窗口消息
一般以WM_开头,如WM_CREATE, WM_SIZE, WM_MOUSEMOVE等标准的Windows消息, 用于窗口相关的事件通知,窗口消息将由系统分配到该窗口的窗口过程处理。
(2) 命令消息 (WM_COMMAND)
一种特殊的窗口消息,它从一个窗口发送到另一个窗口以处理来自用户的请求,通常是从子窗口发送到父窗口,例如,点击按钮时,按钮的父窗口会收到WM_COMMAND消息,用以通知父窗口按钮被点击,经测试:子窗口向父窗口发送WM_COMMAND消息,或者称为父窗口会收到WM_COMMAND消息,操作系统并不是通过将WM_COMMAND消息放入到父窗口的消息队列中去,而是直接调用了父窗口的窗口过程,以 WM_COMMAND 为消息标识参数(UINT uMsg),实现这个功能的API函数正是: LRESULT DispatchMessage(const MSG *lpmsg);
(3) 控件通知消息
WM_NOTIFY消息,当用户与控件交互(Edit, Button...)时,通知消息会从控件窗口发送到父窗口,这种消息的目的不是为了处理用户命令,而是为了让父窗口能够适时的改变控件。
7. 消息队列
一个进程至少拥有一个线程,称为主线程,如果一个线程创建了窗口,拥有GUI资源,那么也称该线程为GUI线程,否则就为工作线程。窗口是由线程创建的,创建窗口的线程就拥有该窗口。这种线程拥有关系的概念对窗口有重要的意义:建立窗口的线程必须是为窗口处理所有消息的线程。
在Windows操作系统中,不是每一个线程都有自己的消息队列。只有那些创建了窗口的线程或者明确调用了PeekMessage或GetMessage函数的线程才会有消息队列。这些消息队列用于存储各种来自系统、应用程序或其他线程的消息,以便线程能够按照一定的顺序处理它们。如果一个线程没有消息队列,那么它就无法接收和处理用户输入、窗口更新等消息,这在大多数情况下是不可取的。因此,通常我们会看到至少有一个拥有消息队列的线程在运行,以处理各种系统消息和用户交互。但是,也有一些线程是不需要消息队列的,比如那些只负责后台计算或者数据处理的线程。这些线程通常会避免创建消息队列,以减少资源消耗和提高效率。总的来说,是否为一个线程创建消息队列,取决于这个线程的具体任务和需求。
另外,系统还维护一个全局的系统消息队列,用来接受鼠标,键盘等外设触发的消息。并根据消息的窗口句柄,窗口所属的线程,不断移除系统队列的消息,放到对应的线程队列中,线程内又有一个小希循环,不断的获取消息,并由DispatchMessage 分派到对应窗口的窗口过程中处理。每个消息都应该得到妥善的处理,如果应用程序不处理的消息,应该交由系统默认的窗口过程DefWindowProc处理。
每个线程,如果它至少建立了一个窗口,都由系统(一般是在线程调用GetMessage或者PeekMessage时)对它分配一个消息队列。这个队列用于窗口消息的派送(dispatch)。为了使窗口接收这些消息,线程必须有它自己的消息循环。
Windows维护一个系统消息队列(System message queue),每个GUI线程有一个线程消息队列(Thread message queue)。鼠标、键盘事件由鼠标或键盘驱动程序转换成输入消息并把消息放进系统消息队列,例如WM_MOUSEMOVE、WM_LBUTTONUP、WM_KEYDOWN、WM_CHAR等等。Windows每次从系统消息队列移走一个消息,确定它是送给哪个窗口的和这个窗口是由哪个线程创建的,然后,把它放进创建窗口的线程消息队列。线程消息队列接收送给该线程所创建窗口的消息。线程从消息队列取出消息,通过Windows把它送给适当的窗口过程来处理。
8. 消息循环
窗口初始化工作完成后,WinMain 进入所谓的消息循环:
BOOL bRet;
while( (bRet = GetMessage( &msg, hWnd, 0, 0 )) != 0) // 不断的尝试到消息队列获取消息
{
if (bRet == -1)
{
// handle the error and possibly exit
}
else
{
TranslateMessage(&msg); // 转换键盘消息
DispatchMessage(&msg); // 经过USER模块的协助,把消息交到窗口函数
}
}
windows 消息处理机制:
首先 Windows OS 把来自硬件 ( 鼠标 ,键盘等消息 ) 和来自应用程序的消息放到一个 OS 系统消息队列中去 ,Windows 系统会不断的处理系统消息队列内的消息,根据消息中附带的窗口句柄,把消息放到对应的应用程序的线程消息队列中。然后应用程序的消息循环每次取出一条消息发送到指定窗口过程 , 直到程序退出。这个循环就是靠消息环实现的 GetMessage() 从线程消息中取出一条消息 , 而 DispatchMessage则把取出的消息发送到目的窗口。如果收到 WM_QIUT 消息则结束循环,发送 PostQuitMessage(0),处理WM_DESTROY 销毁窗口。
9. 窗口的生命中枢:窗口函数(窗口过程)
窗口过程就是一个拥有有固定签名的 C函数回调,具体格式如下:
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
窗口函数通常利用switch/case方式判断消息种类,以决定处置方式。由于它是被Windows系统所调用的(我们并没有在应用程序任何地方调用此函数),所以这是一种call back函数,意思是指「在你的程序中,被 Windows 系统调用」的函数。这些函数虽然由你设计,但是永远不会也不该被你调用,它们是为 Windows 系统准备的。程序进行过程中,消息由输入装置,经由消息循环的抓取,源源传送给窗口并进而送到窗口函数去。不论什么消息,都必须被处理,所以switch/case 指令中的default: 处必须调用DefWindowProc,这是 Windows 内部预设的消息处理函数。
10. 对话框窗口的运作
Windows 的对话框依其与父窗口的关系,分为两类:
1.「令其父窗口除能,直到对话框结束」,这种称为modal 对话框。
2.「父窗口与对话框共同运行」,这种称为modeless 对话框。
为了做出一个对话框,程序员必须准备两样东西:
1. 对话框模板(dialog template)。这是在 RC 文件中定义的一个对话框外貌,以各种方式决定对话框的大小、字形、内部有哪些控制组件、各在什么位置...等等。
2. 对话框函数(dialog procedure)。其类型非常类似窗口函数,但是它通常只处理WM_INITDIALOG和WM_COMMAND两个消息。对话框中的各个控制组件也都是小小窗口,各有自己的窗口函数,它们以消息与其管理者(父窗口,也就是对话框)沟通。而所有的控制组件传来的消息都是 WM_COMMAND,再由其参数分辨哪一种控制组件以及哪一种通告(notification)。
Modal 对话框的启动与结束,靠的是DialogBox 和EndDialog 两个API 函数。
对话框的诞生、运作、结束:
对话框处理过消息之后,应该传回TRUE;如果未处理消息,则应该传回FALSE。这是因为你的对话框函数之上层还有一个系统提供的预设对话框函数。如果你传回FALSE,该预设对话框函数就会接手处理。
11. 资源描述文件(.RC)
RC文件是一个以文字描述资源的地方。常用的资源分别是 ICON、CURSOR、 BITMAP、FONT、DIALOG、MENU、ACCELERATOR、STRING、VERSIONINFO。这些文字描述需经过RC编译器,才产生可使用的二进位代码。
12. Windows 程序的生与死
我们已经了解Windows程序的架构以及它与Windows系统之间的关系。对Windows 消息种类以及发生时机的透彻了解,正是程序设计的关键。现在我以窗口的诞生和死亡,说明消息的发生与传递,以及应用程序的兴起与结束。
窗口的生命周期:
1. 程序初始化过程中调用CreateWindow,为程序建立了一个窗口,做为程序的屏幕舞台。CreateWindow 产生窗口之后会送出WM_CREATE直接给窗口函数,后者于是可以在此时机做些初始化动作(例如配置内存、开文件、读初始数据...)。
2. 程序活着的过程中,不断以GetMessage从消息贮列中抓取消息。如果这个消息是WM_QUIT,GetMessage 会传回0而结束while循环,进而结束整个程序。
3. DispatchMessage 通过Windows USER 模块的协助与监督,把消息分派至窗口函数。消息将在该处被判别并处理。
4. 程序不断进行2.和3.的动作。
5. 当使用者按下系统菜单中的Close命令项,系统送出WM_CLOSE。通常程序的窗口函数不栏截此消息,于是DefWindowProc 处理它。
6. DefWindowProc收到WM_CLOSE后 ,调用DestroyWindow 把窗口清除 。DestroyWindow 本身又会送出WM_DESTROY。
7. 程序对WM_DESTROY的标准反应是调用PostQuitMessage。
8. PostQuitMessage 没什么其它动作,就只送出WM_QUIT 消息,准备让消息循环中的GetMessage 取得,如步骤2,结束消息循环。
因为操作系统与应用程序职责不同,二者是互相合作的关系,所以必需各做各的份内事,并互以消息通知对方。
-
GetMessage 会过门不入,于是操作系统再去照顾其它人。
-
PeekMessage 会取回控制权,使程序得以执行一段时间。于是上述消息循环进入OnIdle 函数中。
13. 问答
1 每个窗体是一个 UI thread ,还是只有一个 UI thread?
答: 所有的窗体都用这一个 UI thread。当然可以有两个UI 线程,如果除了主线程外,另一个线程也创建了窗口,并调用了GetMessage或者PeekMessage时,系统也会给它创建消息队列。具体看微软文档。
2 Message queue: 每个窗体都有一个 message queue ,还是共用一个 message queue?
答:同一个线程的窗体共用一个消息队列。
3 message 处理:是同步还是异步。是每次处理一个消息等这个消息处理完后再处理另一个消息还是每次取一个不等这个消息处理完就处理下一个,也就是 dispatchmessage 什么时候返回?
答:要等这个窗口过程处理函数处理完这个消息以后才返回,否则会造成消息处理的混乱
4 Windows 如何知道消息应该送到哪一个线程,
答:这里我们要分为两种情况 , 消息是不是队列消息 , 比如在一个窗体空白处点击左键 , 首先 OS 会根据当前的context 来生成 MSG , MSG 中会包括要发送到的窗口的 Handle, 这就是一个队列消息 , 首先 OS 会将这个消息放到 OS 的系统消息队列中 , 而后 OS 会有专门的进程根据 MSG 中的窗口的 Handle 找到创建该窗口的线程,而后将该 MSG 送到该线程的消息队列,而后由该消息循环来处理这个消息, 最终由DispatchMessage 函数来将这个消息送到相应的窗口处理函数。
如果你在一个窗体上点击了一个 button 呢,消息的路径是怎样的呢?当你点击了一个 button 后, OS 产生三个MSG。 WM_LBUTTONDOWN和WM_LBUTTONUP,这两个消息的窗口Handle为button的handle。一个WM_command 或者 wm_notify 消息, OS 会将这个消息直接送给包含 button 的 window processdure 来处理,而不会将这个送到消息队列。