真正理解微软Windows程序运行机制——什么是消息

我是荔园微风,作为一名在IT界整整25年的老兵,今天说说Windows程序的运行机制。经常被问到MFC到底是一个什么技术,为了解释这个我之前还写过帖子,但是很多人还是不理解。其实这没什么,我在学生时代也被这个问题困绕过。而且那个时间学习资料没有那么丰富,网上也没有什么资料,周围也没有懂的人,那个时候理解MFC更困难。甚至在我看来,理解这个比理解人工神经网络更难。

我认为造成这种现象的根本原因就是没有搞清楚Windows程序的运行机制,因为不理解Windows程序的运行机制,所以给理解MFC带来了很大的困难。我决定带所有微软开发技术的初学者一起攻破这个问题,但是一篇文章肯定是讲不清楚的,我们要分好几章来说。需要你有足够的耐心,一起来吧。我们这次来搞清楚什么是Windows程序的消息。

消息?难道是你路上遇到一个熟人然后朝熟人喊一声就叫消息?这个概念让很多人产生严重误解。

要了解什么是消息,要先了解什么是事件。事件可分为几种,一是由输入设备触发的,比如鼠标键盘等等。二是由窗体控件触发的,比如Button控件,File菜单等。最后是由来自Windows内部的事件。这三种称为事件,而事件产生消息。

消息,就是指Windows发出的一个通知,告诉应用程序某个事件发生了。例如你单击鼠标、改变窗口尺寸、按下键盘上的一个键都会使Windows发送一个消息给应用程序。消息本身是作为一个记录传递给应用程序的,这个记录(一般在 C语言中称为“结构体”)中包含了消息的类型以及其他信息。例如,对单击鼠标所产生的消息来说,这个结构体中包含了单击鼠标的消息号(WM_LBUTTONDOWN)、单击鼠标时的坐标(由X,Y值连接而成的一个32位整数)。这个结构体类型叫做MSG。

在 Windows程序中,消息是由MSG结构体来表示的。MSG结构体的定义如下:

typedef struct tagMSG{
  HWND hwnd;
  UINT message;
  WPARAM wParam;
  LPARAM lParam;
  DWORD time;
  POINT pt;
}MSG;

typedef struct POINT
{
   int x;
   int y;
}POINT;

该结构体中各成员变量的含义如下:

第一个成员变量hwnd表示消息所属的窗口。在windows上通常开发的程序都是窗口应用程序,一个消息一般都是与某个窗口相关联的。例如,在某个活动窗口中按下鼠标左键,产生的按键消息就是发给该窗口的。在Windows程序中,用HWND类型的变量来标识窗口。说到消息就不能不说窗口句柄,系统通过窗口句柄来在整个系统中唯一标识一个窗口,发送一个消息时必须指定一个窗口句柄表明该消息由那个窗口接收。而每个窗口都会有自己的窗口过程,所以用户的输入就会被正确的处理。例如有两个窗口共用一个窗口过程代码,你在窗口1上按下鼠标时消息就会通过窗口1的句柄被发送到窗口1而不是窗口2。如果对HWND不明白,马上看我的另一篇文章:

真正理解微软Windows程序运行机制——什么是句柄

第二个成员变量 message指定了消息的标识符。在Windows中,消息是由一个数值来表示的,不同的消息对应不同的数值。但是由于数值不便于记忆,所以Windows将消息对应的数值定义为“WM_名称”的宏的形式,其中的名称对应某种消息的英文拼写的大写形式。例如,鼠标左键按下消息是WM_LBUTTONDOWN,键盘按下消息是WM_KEYDOWN,字符消息是WM_CHAR等。在程序中我们通常都是以这种宏的形式来使用消息的。如果想知道“WM_名称”消息对应的具体数值,可以在 Visual Studio 2022的代码编辑窗口中选中“WM_名称”,然后单击鼠标右键,在弹出菜单中选择“转到定义”,即可看到该宏的具体定义。跟踪或查看某个变量的定义,都可以使用这个方法。

第三个和第四个成员变量wParam和IParam,这两个变量我当年学了很久很久才搞清楚是什么意思。其实这两个变量就是用于指定消息的附加信息。例如,当我们收到一个字符消息的时候,message成员变量的值就是WM_CHAR,但用户到底输入的是什么字符,那么就由wParam和IParam来说明。wParam、IParam表示的信息随消息的不同而不同。如果想知道这两个成员变量具体表示的信息,可以在MSDN中关于某个具体消息的说明文档查看到。读者可以在Visual Studio 2022的开发环境中通过“转到定义”查看一下WPARAM和 LPARAM这两种类型的定义,可以发现这两种类型实际上就是unsigned int和 long。

第五个变量time表示消息投递到消息队列中的时间。第六个变量pt表示消息投递到消息队列中鼠标的当前位置,可以看看上面的代码,并不复杂。

好,我们总结一下,消息的组成情况:一个消息由一个消息名称(UINT),和两个参数(WPARAM,LPARAM)。当用户进行了输入或是窗口的状态发生改变时系统都会发送消息到某一个窗口。例如当菜单选中之后会有WM_COMMAND消息发送,WPARAM的高字中(HIWORD(wParam))是命令的ID号,对菜单来讲就是菜单ID。当然用户也可以定义自己的消息名称,也可以利用自定义消息来发送通知和传送数据。

在上世纪末本世纪初,我还在用C99写程序的时候,当调用函数打开文件,这个库函数最终调用操作系统提供的函数来打开文件。而在Windows中,用户程序可以调用系统的API函数,系统也会调用用户程序,这个调用是通过消息来进行的。与基于DOS的应用程序不同,Windows的应用程序是事件消息驱动的。Windows的程序不会显式地调用函数来获取输入,而是等待windows操作系统向它们传递输入。所以说,Windows程序设计是一种完全不同于传统的DOS方式的程序设计方法。它是一种事件驱动方式的程序设计模式,主要是基于消息的。

也就是说每个在windows操作系统上的应用程序都会在启动后(比如被你双击桌面图标)开始接收操作系统抛过来的各种各样的数值,或者叫各种各样的参数(比如你点了应用程序窗口上的什么按钮)。然后你的应用程序根据这些参数来进行相应的操作和运行。

例如,当用户在窗口中输入文本的时候,按下鼠标左键,此时,操作系统会感知到这一事件,于是将这个事件包装成一个消息,放入到应用程序的消息队列中,就好像大家在排队一样,如下图。然后应用程序从消息队列中取出消息并进行响应,也就是响应这个鼠标的操作。在这个处理过程中,操作系统也会给应用程序“发送消息”,实际上是操作系统调用程序中一个专门负责处理消息的函数,这个函数称为窗口过程。

每一个Windows应用程序在开始执行后,系统都会为该程序创建一个消息队列,这个消息队列用来存放该程序创建的窗口的消息。例如,当我们按下鼠标左键的时候,将会产生WM_LBUTTONDOWN消息,系统会将这个消息放到窗口所属的应用程序的消息队列中,等待应用程序的处理。Windows将产生的消息依次放到消息队列中,而应用程序则通过一个消息循环不断地从消息队列中取出消息,并进行响应。这种消息机制就是Windows程序运行的机制。

这里我们思考一下,谁将收到消息??一个消息必须由一个窗口接收。在窗口的过程(WNDPROC)中可以对消息进行分析,对自己感兴趣的消息进行处理。例如你希望对菜单选择进行处理那么你可以定义对WM_COMMAND进行处理的代码,如果希望在窗口中进行图形输出就必须对WM_PAINT进行处理。谁来处理这个事,程序员来负责,不然窗口怎么响应这些消息?

Windows系统把应用程序的输入事件传递给各个窗口,每个窗口有一个函数,称为窗口消息处理函数。窗口消息处理函数处理各种用户输入,处理完成后再将控制权交还给系统。窗口消息处理函数一般是在注册一个窗口的时候指定的。你可以从典型的SDK程序中窗口消息处理函数是怎么声明和实现的。

Windows通过消息的形式向窗口传递用户输入。消息可以由系统和应用程序生成。该系统会为每个输入事件产生相应的消息,用户点击鼠标、移动鼠标或滚动条,或是应用程序改变了系统的某些属性,比如说系统更改了字体资源,改变了某个窗口的大小。应用程序可以生成消息,通告发送消息指定它的窗体去执行某些任务或者是与其他的应用程序交互。windows系统将消息发送到一个窗口消息处理函数时传递四个参数:窗口句柄,消息标识符,两个DWORD值(消息参数)。窗口句柄标识了该消息的目的窗口。Windows使用它来确定是哪个窗口的的窗口消息处理函数收到该消息。

一个消息标识符是一个有名字的常量,用来表明消息的意义。当一个窗口处理函数收到一条消息,它根据判断消息标识符来决定如何处理该消息,例如,消息标识符WM_PAINT消息告诉窗口程序窗口的客户区已发生变化,必须重绘。 消息参数(DWORD值)指定传递的数据或是数据的地址。消息参数可以是一个整型值,一个指针值。也可以为NULL。一个窗口过程必须根据消息标识符来确定如何解释消息参数。

windows 消息类型主要分为系统定义的消息和应用程序定义的消息。产生的消息首先由Windows系统捕获,放在系统消息队列,再拷贝到对应的应用程序消息队列。32或64位系统为每一个应用程序维护一个消息队列。系统为每个应用程序维护一个消息循环,消息循环会不断检索自身的消息队列。每有一个消息,就用GetMessage()取出消息。

while(GetMessage (&msg, NULL, 0, 0))//Windows消息循环。
 
{
    TranslateMessage (&msg) ;//翻译消息,如按键消息,翻译为WM_CHAR

    DispatchMessage (&msg) ;//分发消息到对应窗口

}

GetMessage具有阻塞机制。当消息队列中没有消息时,程序非忙等,而是让权等待。当收到WM_QUIT时,GetMessage返回false,循环停止,同时应用程序终止。在消息处理中:DispatchMessage()把取出来的消息分配给相应的窗口或线程,由窗口过程处理函数DefWindowProc()处理。

Windows的应用程序靠消息驱动来实现功能。而消息驱动靠消息机制来处理。消息机制就是由消息队列,消息循环,消息处理构成的。那么,消息机制是如何运作的呢?

当用户运行一个应用程序,通过对鼠标的点击或键盘按键,产生一些特定事件。由于Windows一直监控着I/O设备,该事件首先会被翻译成消息,由系统捕获,存放于系统消息队列。经分析,Windows知道该消息应由那个应用程序处理,则拷贝到相应的应用程序消息队列。由于消息循环不断检索自身的消息队列,当发现应用程序消息队列里有消息,就用GetMessage()取出消息,封装成Msg()结构。如果该消息是由键盘按键产生的,用TranslateMessage()翻译为WM_CHAR消息,否则,用DisPatchMessage()将取出的消息分发到相应的应用程序窗口,交由窗口处理程序处理。Windows为每个窗体预留了过程窗口函数,该函数是一个回掉函数,由系统调用,应用程序不能调用。程序员可以通过重载该函数处理我们”感兴趣”的消息。对于不感兴趣的消息,则由系统默认的窗口过程处理程序做出处理。一定仔细看下图,这个图是无意中在百度图片里看到的,不知道作者是谁。这幅图是我认为画的最好的一幅:

消息机制的整个完整过程就是:当运行程序->事件操作引发消息->消息先存在系统消息队列->再存入到应用程序消息队列->用消息循环提取消息->处理消息->再返回消息队列。

同样,我们可以通过自定义用户消息,用SendMessage()函数向窗口模拟发送消息,再重载窗口过程处理函数DefWndProc()来接收用户自定义消息,并作出处理。因为SendMessage()是系统的一个API函数。

int a1 = 0x340;
int a2 = 0x350; //用户自定义消息。

[DllImport("User32.dll",EntryPoint="SendMessage")]
//EntryPoint 表示要调用的函数入口点是dll文件里的SendMessage()函数。

private static extern int SendMessage(
  ntPtr hwnd,   //IntPtr是平台特定的int类型。就是在32跟64位系统上分别为32位和64位
  int Msg,    //表明要发送的消息。
   int wParam,  //32位的附加信息。
   int IPrarm  //32位的附加信息。(随消息的改变而改变)
);

// 此处由于SendMessage发送到本线程的窗口。所以发送的消息不会被加入到消息队列中,
//所以通过PeekMessage()或GetMessage()不能获取到由SendMessage发送的消息。
1 protected override void DefWndProc(ref Message m)//重载接收处理函数
2 {
3     switch (m.Msg)
4   {
5     case a1: label1.Text = "做第一个动作"; break;
6     case a2: label1.Text = "做第二个动作"; break; 
7     default: base.DefWndProc(ref m); break;
8   }
9 }

总结

我们把上面这些过程再总结一下,以窗体的创建为例。Windows应用程序实际上具有相同的程序结构和执行控制流程。执行流程如下:程序入口点(WinMain())->注册窗口类(RegisterClass)->创建窗口(CreateWindow())->显示窗口(ShowWindow(hwnd,nCmdShow)->(UpdateWindow(hwnd))->消息循环(等待用户操作窗口产生消息)->放入消息队列->消息循环往复读取并执行相应代码->窗口函数(决定在窗口那里显示些什么,或者如何响应用户输入(如上面的SendMessage()函数))->消息处理(用来确定窗口函数接收的是什么消息以及如何处理,如DefWndProc()函数。

系统定义的消息是指操作系统向应用程序发送消息来和应用程序通讯。操作系统通过消息控制应用程序的运行,向应用程序传递用户输入以及一些其他有用的信息。应用程序也可以发送系统定义的消息,应用程序通过这些消息去控制使用注册窗口类创建的控件的窗口的运行。每个系统定义的消息都有一个唯一的消息标识符和相应的符号常量。符号常量通常会表明系统定义的消息所属的类别。不同的前缀表明不同的类别。应用程序定义的消息是指应用程序可以通过创建自定义的消息,用来和自己的窗口和其他进程通讯。如果应用程序创建了自己的消息,窗口处理函数可以解析这些信息并作出相应的处理。

Windows程序中的消息可以分为“进队消息”和“不进队消息”,分出两种队,如下图。进队的消息将由系统放入应用程序的消息队列中,由应用程序取出并发送。不进队的消息在系统调用窗口过程时直接发送给窗口。不管是进队消息还是不进队消息,最终都由系统调用窗口过程函数对消息进行处理。也就是说,windows使用两种方法将消息派发到一个窗口消息处理函数:一是将消息放到消息队列(先进先出队列),二是不放到消息队列,直接发送到窗口消息处理函数,让窗口处理函数来处理消息。派发到消息队列的消息被称为排队消息。它们主要是用户输入事件,比如说鼠标或键盘消息盘,有WM_MOUSEMOVE消息,WM_LBUTTONDOWN,WM_KEYDOWN,和WM_CHAR消息。还有一些其他的,包括WM_TIMER,WM_PAINT,以及WM_QUIT。大多数其他的消息息,这是直接发送到窗口过程,被称为非队列消息(non queued messages)。

再思考一下,未处理的消息到那里去了?操作系统为窗口编写了默认的窗口过程,这个窗口过程将负责处理那些你不处理消息。正因为有了这个默认窗口过程我们才可以利用Windows的窗口进行开发而不必过多关注窗口各种消息的处理。例如窗口在被拖动时会有很多消息发送,而我们都可以让系统自己去处理。

最后我们用一段代码演示如何在窗口过程中处理消息

LONG yourWndProc(HWND hWnd,UINT uMessageType,WPARAM wP,LPARAM)
{
         switch(uMessageType)
               {
                 //使用SWITCH语句将各种消息分开
                 case(WM_PAINT):
                      doWindow(...);//在窗口需要重新绘制时进行输出
                 break;
                 case(WM_LBUTTONDOWN):
                      doWork(...);//在鼠标左键被按下时进行处理
                 break;
                 default:
                      callDefaultWndProc(...);//对于其它情况就让系统自己处理
                 break;
         }
}

好,总结一下,消息机制就是系统维护了一个或多个消息队列,所有产生的消息都回被放入或是插入队列中。系统会在队列中取出每一条消息,根据消息的接收句柄而将该消息发送给拥有该窗口的程序的消息循环。每一个运行的程序都有自己的消息循环,在循环中得到属于自己的消息并根据接收窗口的句柄调用相应的窗口过程。而在没有消息时消息循环就将控制权交给系统所以Windows可以同时进行多个任务。

各位小伙伴,这次我们就说到这里,下次我们再深入研究windows程序运行机制。

作者简介:荔园微风,1981年生,高级工程师,浙大工学硕士,软件工程项目主管,做过程序员、软件设计师、系统架构师,早期的Windows程序员,Visual Studio忠实用户,C/C++使用者,是一位在计算机界学习、拼搏、奋斗了25年的老将,经历了UNIX时代、桌面WIN32时代、Web应用时代、云计算时代、手机安卓时代、大数据时代、ICT时代、AI深度学习时代、智能机器时代,我不知道未来还会有什么时代,只记得这一路走来,充满着艰辛与收获,愿同大家一起走下去,充满希望的走下去。

  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
理解Microsoft专家认证程序 理解Microsoft认证的不同等级和类型 选择成为MCP(Microsft认证专家)的考试科目 选择成为MCSD的考试科目 选择成为MCT的考试科目 MCSE认证考试的科目 选择合适的MCSE课程组合 核心课程考试 选修课程考试 考试编号的识别 课程内容和考试内容的对照 理解微软的MCSE长远考虑 理解微软出题的方式 使用本书帮助备考 在Internet上寻找对考试有帮助的信息 寻求微软认可的课程指导 寻找高质量的和三方帮助 寻找可利用的评估软件拷贝 报名参加考试 考试的费用问题 考前的自我调整 使用考试中心提供的考试工具 参加模拟测试 熟悉使用计算机进行考试 充分利用考试时间 考题的形式 理解多重选择题型 理解对错题题型 理解多重选择多重答案题型 理解基于解决方案型的问题 理解“建议方法”类型的考题 分析考试结果 准备重新考试 合理安排考试课程的顺序 熟悉Windows系列产品 比较Windows NT Server和NT Workstation 比较Windows NT Workstaton和Windows 95 在Windowx 95和Windows NT Workstation之间作出选择 关于Microsoft Windows NT的70-069号考试:实现和支持Microsoft Windows NT Server 4.0 70-069号考试(实现和支持Microsoft Windows NT Server 4.0)覆盖的内容 Windows NT 4.0界面简介 Windows NT 4.0任务栏(taskbar)的使用 Windows NT回收站简介 Windows NT帐号简介 理解单域模型支持和帐号数量 安全认证号简介 使用管理向导(Administrative Wizards)创建帐号 使用Server Manager(服务器管理器)程序创建计算机帐号 Userver Manager for Domains(域的用户管理器)简介 使用User Mnager for Domains创建用户帐号 刷新用户帐号列表 用户帐号列表的排序 事件查看器(Event View)程序简介 筛选Event Viewer中的事件 授予用户在本地登录的权利 使用Windows NT诊断程序查看系统配置 激活“Windows NT Security(Windows NT安全)”对话框 理解登录验证过程 理解访问令牌(Access Token) Windows NT目录服务简介 理解Windows NT如何构造用户帐号数据库 使用Windows NT中的Ctrl+Alt+Del组合键 把Windows NT计算机设置成自动登录 改变Windows NT口令 用拨号网络登录 复制用户帐号 为简化多个帐号的创建工作而建立用户帐号模板 删除和重新命名用户帐号 理解保护缺省的Administrator帐号的重要性 重新命名管理员帐号 理解缺省的Guest帐户 Windows NT在哪里创建帐号 设置口令限制条件 设置用户登录地点 创建宿主文件夹 设置用户登录时间 创建临时用户帐号 重新设置用户帐号口令 修改多个用户帐号 自动注销有时间限制的用户 要求用户在下次登录时改变口令 设置帐号规则 设置用户口令永不过期 停用用户帐号 解开登录失败后的用户帐号 Windows NT组简介 理解用户权限和组的访问权限 理解用户和组的权利 分清权限(permission)和权利(right) 设置组成成员关系 理解全局帐号 理解本地帐号 定义Everyone组 Network组的详细说明 Inteactive组的详细说明 Administrators组的详细说明 Guest组的详细说明 Users组的详细说明 Print Operators组的详细说明 Backup Operators(帐户操作员)组的详细说明 Replicator(复制员)组的详细说明 Domain Guests(域客户)组的详细说明 Domain Users(域用户)组的详细说明 Domain Admins(域管理员)组的详细说明 赋予拨号进入权限 理解用户配置文件(User Profile) 为Windows用户创建并使用登录脚本文件(Logon Script) 创建漫游式用户配置文件(Roaming User Profile) 创建强制性用户配置文件(Mandatory User Profile) 为用户帐号分配一个配置文件 创建帐户时变量的使用 创建随机初始化口令 理解内建组(Built-in Group) 理解组和策略 设置主组(Primary Group) 理解删除一个组的影响 域控制器(Domain Controller)简介 成员服

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值