windows窗口创建流程及window消息机制详解

windows窗口创建流程及消息机制详解

绪论

本文通过对windows窗口创建的流程来分析在windows系统中消息的产生、获取、处理的方法,详细介绍windows的消息机制,方便Windows开发者对windows的消息机制进行理解。

一、窗口类(WNDCLASS)

定义:窗口类是一个属性集,是Windows编程中用于创建窗口的模板。窗口类包含了窗口的各种信息的数据结构,每个窗口都具有窗口类,每个窗口都是基于自己的窗口类来进行创建窗口的。每一个窗口类都有一个名称,使用窗口类时必须注册到操作系统中去。
分类:窗口类可以分为系统窗口类、应用程序全局窗口类、应用程序局部窗口类。
系统窗口类——系统中已经定义好的窗口类,所有的的应用程序都可以使用 如:BUTTON EDIT。
应用程序全局窗口类——由用户自己定义的,当前应用程序所有模块都可以使用。
程序局部窗口类——由用户自己定义,当前应用程序中本模块中可以使用
结构:

 typedef struct_WNDCLASS{
  UINT style;                 //窗口类的缓冲大小
  WNDPROC lpfnWndProc;        //窗口的消息处理函数的指针
  int cbClsExtra;             //窗口类的buff的缓冲大小
  int cbWndExtra;             //窗口的buff的缓冲大小
  HANDLE hInstance;           //当前窗口的实例
  HICON hIcon;                //窗口图标句柄
  HCURSOR hCursor;            //窗口鼠标句柄
  HBRUSH hbrBackground;       //绘制窗口背景的画刷句柄
  LPCTSTR lpszMenuName;       //窗口菜单资源ID的字符串
  LPCTSTR lpszClassName;      //窗口类的名称
  }WNDCLASS;
注意:1. 不建议使用全局窗口类,容易发生冲突,全局窗口实现的功能,局部窗口类都可以实现
     2. 用户在创建窗口时,首先需要对窗口进行注册,即通过ATOM RegisterClassEx(CONST WNDCLASSEX *Ipwcx):将窗口类的结构写入操作系统中。

二、窗口程序的创建流程

  1. 注册窗口:将窗口类的信息写入操作系统的内核中
  2. 创建窗口:在内存中开辟一片空间,将窗口的数据(包括CreateWindows传入的数据和窗口类的数据)写入内存,并返回该内存的地址(窗口句柄)
  3. 显示窗口:按照内存中的窗口数据绘制窗口
  4. 消息循环:获取、翻译、分发窗口中的消息
  5. 窗口的消息处理函数进行消息处理:处理窗口中的消息
    以上对窗口的创建流程进行了概括,接下来对窗口创建过程进行详细的分析。

三、窗口创建的原理

通过以上分析,我们存在两个个问题。第一,在窗口的创建之前为什么要进行注册?第二,窗口创建和注册之间有什么关系?

在窗口的创建之前为什么要进行注册?

想创建什么样的窗口呢?那么,创建什么样的窗口呢?创建前,Window系统可不知道你要的是什么类型的窗口啊(比如标题栏上显示什么图标,鼠标形状是什么,窗口背景颜色等等)。这些类型信息应在你创建前事先告诉Window系统。可以采用这种方法:就是我们事先写一份要创建窗口的类型申请表,提交(注册)给Window系统。然后在创建时,可以让Windows按这个申请表来创建你所要的窗口了。也就是说我们还应该先提交一个申请表,申请成功后再根据这个表创建一个窗口。
  我们在使用microsoft平台SDK或者MFC编程时,在创建窗口类后都要先用RegisterClass函数来注册窗口类,这个函数需要一个指向窗口类结构的指针。那么RegisterClass这个windows API函数到底做了什么呢,关于这个函数的源码微软是不会给出来的,因为它只是提供一个系统编程接口,网上也找不到相关说明,只是粗略介绍需要将类注册给系统,但从msdn的atom table说明中我们发现这样一段说明。The system uses atom tables that are not directly accessible to applications. However, the application uses these atoms when calling a variety of functions. For example, registered clipboard formats are stored in an internal atom table used by the system. An application adds atoms to this atom table using。答案有了,在我们构造一个窗口类结构后,我们需要将这个类结构指针加入到system atom table 即SAT中,这样系统就可以通过查找这张表来找到用户自定义的窗口类,window预定义的窗口类指针也在SAT中。SAT实际上实现了一种用于查询的映射,atom实际类型是short,即16位数据。只有系统才可直接访问这张表,但在调用某些api函数时,如Registerclass,可以告知系统来存取这张表。当然,还有本地原子表和全局原子表,这些表应用程序是可以直接访问的。
转自: https://blog.csdn.net/zxj2018/article/details/6636067

窗口创建和注册之间有什么关系?

通过分析,我们惊奇的发现在创建窗口和构造WNDCLASS的过程中有两个相同参数lpszClassName(窗口名称)和hInstance(窗口实例),创建窗口和注册窗口通过这两个参数联系起来的。
CreateWindow函数的执行过程:

  1. CreateWindow首先会根据窗口类的名称在应用程序局部窗口类中查找窗口类,如果找到执行2,若未找到执行3。
  2. 比较局部窗口类与创建窗口类传入的hinstance是否相同,如果相同在同一模块下创建该窗口。否则执行3。
  3. 在应用程序全局窗口类中查找,若查找到执行4,否则执行5。
  4. 使用窗口信息创建窗口并返回。因为要创建的是全局窗口所以不需要比对hInstance。
  5. 在系统窗口类中查找,若找到创建窗口,否则创建窗口失败,返回NULL。

CreateWindow找到相应的窗口类后会在内存中开辟一个空间来存储窗口类的信息并返回该内存地址的指针(句柄)。

四、消息简介

概念:
消息(Message)指的就是Windows 操作系统发给应用程序的一个通告,它告诉应用程序某个特定的事件发生了。比如,用户单击鼠标或按键都会引发Windows 系统发送相应的消息。最终处理消息的是应用程序的窗口函数,如果程序不负责处理的话系统将会作出默认处理。
在windows的平台下消息由窗口句柄、消息ID、消息两个附带参数(wParam和lParam)、消息的时间和消息产生时鼠标的位置五个部分组成。
消息的组成和MSG的成员对应。

typedef struct  tagMSG {
HWND hwnd;      //窗口句柄
UINT message;  //消息ID
WPARAM wParam;  
LPARAM lParam;  //两个参数
DWORD time;     //消息产生的时间
POINT pt;      //消息产生时鼠标的位置
}MSG, *PMSG,NERA*  NPMSG, FAR* LPMSG;

作用:
当系统通知窗口工作时,就采用消息的方式通过DispatchMessage(&msg);派发给各个窗口的消息的处理函数。
** 通过消息的作用我们可以知道每一窗口都需要一个消息处理函数.**
分类:
消息可以分为系统消息和用户自定义消息
系统消息: ID范围为 0 —— 0X03FF
系统消息是由系统定义好的消息,可以在系统中直接使用(负责消息处理或消息发送)
用户自定义消息:ID范围为 0X0400—— 0X7FFF
用户消息是由用户自定义的消息,满足用户自己的需求,由用户自己发出,并响应处理。
自定义消息宏 #define WM_USER 0X0400

五、消息循环

MSG msg = { 0 };
while (GetMessage(&msg,0,0,0))
{
 TranslateMessage(&msg);
  DispatchMessage(&msg);
}

接下来对消息循环的中的各个函数进行详细的解释。

DispatchMessage与窗口消息处理函数的关系

在消息的作用中我们知道DispatchMessage是将消息分发个不同窗口的消息处理函数,那么它时如何找的窗口的消息处理函数的。
接下我们使用伪代码分析一波。

DispatchMessage(msg) {
 通过窗口句柄获取保存窗口信息的内存,
 然后通过WNDCLASS的lpfnWndProc找到窗口消息处理函数。
 最后调用窗口消息处理函数。
}

为了能够使系统能够识别和调用窗口消息处理函数,我们遵循windows的规定来定义每个窗口的消息处理函数。
消息处理函数的原型为:

LRESULT CALLBACK WindowProc(
HWND hwnd;       //窗口句柄
UINT uMsg;       //消息ID
WPARAM wParam;   //消息参数
LPARAM lParam;   //消息参数
);

解释:
1. 当系统通知窗口时,会调用窗口消息处理函数,同时将消息ID和消息参数传递给窗口消息处理函数。
2. 在窗口消息处理函数中,若不需要处理消息,可以使用缺省的窗口消息处理函数 DefWindowProc();
3. 窗口处理函数的除了返回值和参数列表,其他都可以和windows的原型不一样.

因此窗口消息处理函数就变为了

  LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
  {
	 switch(message)
	 {
		    case 需要处理的消息 : 
		    {
		          处理的内容;
		     }break;
		    case WM_DESTROY: //窗口销毁 该消息和消息的处理内容后面介绍
		    {
		         PostQuitMessage( 0 );
		    }break;
		    default :
		    {
		        return DefWindowProc(hWnd, message, wParam, lParam); //处理其他消息
		    }
      }
      return 0;
  }

GetMessage的简单解释

GetMessage的作用时获取本窗口消息队列中的消息,但是的功能不仅仅如此,具体功能将在后面的消息队列中进行详细的解释。
函数原型:

BOOL WINAPI GetMessage(
   _Out_ LPMSG lpmsg,         //存放消息的BUFF即MSG
   _In_Opt_ HWND hWnd, //窗口句柄 只获取该窗口句柄指定窗口的消息,NULL表示获取所有窗口的消息。
   _In_ UINT wMsgFilterMin, //获取消息的最小ID
   _In_ UINT wMsgFiterMax //获取消息的最大ID  这两个参数规定了GetMessage获取消息的ID范围,当这两个参数全部为零时,表示获取所有的消息
);

通过消息循环的代码可以知道GetMessage的返回值尤为重要,它直接在影响到程序是否能够结束。
那么GetMessage何时返回true,何时返回false?
官方解释:当GetMessage在消息队列中获取到WM_QUIT消息时,GetMessage就会返回false,否则返回true。
因此我们可以通过在PostQuitMessage( 0 );向消息队列中放置WM_QUIT消息,这样程序就可以结束。

TranslateMessage

TranslateMessage的作用是将按键消息翻译成字符消息。但是它只能翻译像24字母和数字这样的可见按键,不可以翻译像F1,F2这样的带有功能的不可见按键。
函数原型:

BOOL WINAPI TranslateMessage(
 _In_ CONST MSG* lpMSG; 需要翻译的消息的地址。
);

常见的消息

通过对常见消息的了解为后面消息循环的原理做铺垫。
当我们见到一个陌生消息时,我们要从消息的产生时间、消息两个附带的参数(wParam和lParam)和消息的一般用法(功能)。

(1)WM_DESTROY

产生时间:窗口被销毁时的消息。
附带消息:wParam: 0 lParam:0
一般用法:常用于窗口被销毁之前做一下善后的处理,例如资源、内存等。
补个小坑:在上文的窗口处理函数中,我们捕捉了WM_DESTROY消息,然后使用PostQuitMessage( 0 )将WM_QUIT消息放置到消息队列中,让GetMessage获取后退出程序。

(2) WM_SYSCOMMAND

产生时间:当点击窗口最大化、最小化、关闭时。
附带消息:wParam: 鼠标具体点击位置 例如:SC_CLOSE lParam:LOWORD(低八位) 水平位置 HIWORD(高八位) 垂直位置
一般用法:常在窗口关闭时,提示用户处理。

(3) WM_CREATE

产生时间:窗口创建后但未显示时。
附带消息:wParam:0 lParam:是CREATESTRUCT结构的指针,该指针包含了窗口的所有的信息。
一般用法:常用于初始化窗口的参数、资源等。

(4) WM_SIZE

产生时间:窗口大小发生变化后。
附带消息:wParam:wParam: 窗口大小变化的原因 lParam:窗口变化后的大小 LOWORD 宽度 HIWORD 高度
一般用法:常用于窗口大小变化后,调整窗口内部各个元素的布局。

消息循环的原理

消息信息的阻塞

GetMessage ——GetMessage不仅具有从消息队列中获取消息的能力,还能将在获取的同时将消息移除,当消息队列中没有消息时,该函数还会将程序阻塞(线程挂起,系统无法进行调度)等待下一条消息。因此使用GetMessage获取消息的效率低。
PeekMessage——PeekMessage具有和GetMessage相同的功能,但是以查看的方式获取消息队列中的消息,可以不将消息移除,如果系统中没有消息,PeekMessage会返回false,不会阻塞程序。
PeekMessage的原型:

BOOL WINAPI PeekMessage(
   _Out_ LPMSG lpmsg,         //存放消息的BUFF即MSG
   _In_Opt_ HWND hWnd,        //窗口句柄 只获取该窗口句柄指定窗口的消息,NULL表示获取所有窗口的消息。
   _In_ UINT wMsgFilterMin,   //获取消息的最小ID
   _In_ UINT wMsgFiterMax     //获取消息的最大ID  这两个参数规定了GetMessage获取消息的ID范围,当这两个参数全部为零时,表示获取所有的消息
   _In_ UINT wRemoveMsg       //移除标识 值为: PM_REMOVE(获取消息并移除 ,不建议使用)/ PM_NOREMOVE(获取但不移除消息)  
);

因此为了提高运行效率,建议使用这样的消息循环代码

while(true){
 if (PeekMessage(&msg,NULL,0,0,PM_NOREMOVE)){
     if(GetMessage(&msg,NULL,0,0)){
         TranslateMessage(&msg);
         DispatchMessage(&msg);
      }else {
          return 0;
      }
 }else {
    //空闲处理
  }
}
发送消息

发送消息是将消息发送到消息队列中或者将消息直接发送给窗口的消息处理函数。接下来介绍发送消息的两个函数。
PostMessage将消息发送到消息队列中,不等待消息结果直接返回。
函数原型:

BOOL WINAPI PostMessage(
   _In_opt_  HWND hwnd,
   _In_  UINT Msg,
   _In_ WPARAM wParam,
   _In_ LPARAM lParam
);

SendMessage将消息直接发送到窗口的消息处理函数(调用消息处理函数),等待消息处理函数处理消息,在这个过程中该函数会阻塞程序的执行,直到获取到消息的处理结果。
函数原型:

BOOL WINAPI SendMessage(
   _In_opt_  HWND hwnd,
   _In_  UINT Msg,
   _In_ WPARAM wParam,
   _In_ LPARAM lParam
   );

消息队列

消息队列就是用与存放消息的队列,消息在队列中是先近先出的,所有的窗口都有自己的消息队列。
只有GetMessage会从消息对列中获取消息,且GetMessage只能在消息队列中获取消息。
分类
消息队列分为系统消息队列和程序消息队列。
程序消息队列是由系统维护的消息队列。存放系统产生的消息。如键盘消息、鼠标消息等。其实window系统中产生的消息首先都需要存放到系统消息队列中,系统消息队列每隔一段时间将消息分发到不同窗口(线程)的程序消息队列中。
程序消息队列是由每个程序进行维护的,每个程序都有属于自己的程序消息对列。

消息的传递过程
当消息产生时,系统会将消息放到系统消息队列中,系统消息队列每隔一段时间会通过hInstance(实例句柄)将消息分发到不同窗口(线程)的程序消息队列中。然后每个程序的GetMessage在程序消息队列中获取消息。

消息与队列的关系

按照消息与队列关系可以分为队列消息和非队列消息。
队列消息:队列消息的发送和获取都是通过队列完成的。
队列消息需要通过PostMessage将发送后首先会进入队列,然后通过消息循环,再通过GetMessage从消息队列中获取。
常见的队列消息:WM_QUIT、键盘、鼠标、定时器等。
非对列消息:非队列消息的发送和获取直接通过调用SendMessage和窗口消息处理函数来完成。
非队列消息通过SendMessage发送后,会通过窗口句柄找到窗口的消息处理函数,然后调用窗口处理函数处理消息。
常见的非队列消息:WM_CREATE、WM_SIZE等。

深谈GetMessage的执行过程

通过以上分析,我们可以知道GetMessage在消息循环起着非常重要的作用。接下来将深入探讨一下GetMessage的执行过程。

  1. GetMessage会在程序消息队列中获取消息,如果有消息,GetMessage就会检查消息是否满足指定条件,如果不满足就不会取出消息,如果满足就会取出消息并返回。
  2. 如果程序消息队列中没有消息,GetMessage就会向系统消息队列发出向本程序的消息队列中发送属于本程序的消息的申请。如果系统消息队列中有属于本程序的消息,就会转发到该程序的程序的消息队列中。(此时GetMessage会打断系统消息队列每隔一段时间分发消息的时序。)
  3. 系统消息队列中没有属于该程序的消息,GetMessage会检查当前程序窗口中需要重新绘制的区域,如果有GetMessage就会调用PostMessage向系统队列发送WM_PAINT的消息,然后执行2获取WM_PAINT消息。
  4. 如果没有需要重新绘制的区域,GetMessage会检查定时器,有没有到时间的,如果有GetMessage就会和步骤3一样通过PostMessage向系统消息队列发送WM_TIME的消息,然后执行2获取WM_TIME消息。(其实步骤3和步骤4是同时执行的)
  5. 如果没有到时的定时器,GetMessage就会整理资源和内存。
  6. 最后GetMessage就会阻塞程序等待下一条消息。PeekMessage也会执行上述步骤,但是PeekMessage执行到此步骤时会返回false,交出程序的控制权,不会阻塞程序。
  7. 若GetMessage获得WM_QUIT,GetMessage就会返回false。

结语

通过以上描述,我想大家一定会对window的整个消息机制有了比较清晰认识,如果本文中有错误的理解,恳请各位指正。

  • 3
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值