首页 理论教育 操作系统实现:GUI原始输入线程

操作系统实现:GUI原始输入线程

时间:2023-10-21 理论教育 版权反馈
【摘要】:消息被DIM对象送到RAWIT线程之后,操作系统内核对消息的处理就算结束了,接下来RAWIT线程要大显身手了。在前面关于DIM的描述中,RAWIT线程是在GUI模块初始化过程中被创建的,创建完毕,被设置成当前输入焦点线程。这样只要从RAWIT线程的入口代码开始分析,逐层深入,就很容易把消息在RAWIT线程内的处理机制搞清楚。这里看LBUTTONDOWN消息,在收到该消息后,RAWIT线程会调用DoLButtonDown函数,对其进一步进行处理。

操作系统实现:GUI原始输入线程

消息被DIM对象送到RAWIT线程之后,操作系统内核对消息的处理就算结束了,接下来RAWIT线程要大显身手了。RAWIT(Raw Input Thread),翻译过来就是原始输入线程,这里“原始”的意思,是指消息还未被操作系统或应用程序处理过,是由设备驱动程序“原汁原味”地发过来的。同时还有另外一层意思,那就是消息的归属还不明确,到底归属哪个线程、哪个应用程序,还未被最终确定,因此用Raw来形容这个线程。

在前面关于DIM的描述中,RAWIT线程是在GUI模块初始化过程中被创建的,创建完毕,被设置成当前输入焦点线程。这样只要从RAWIT线程的入口代码开始分析,逐层深入,就很容易把消息在RAWIT线程内的处理机制搞清楚。RAWIT线程处理很多主动输入消息,比如鼠标的七个消息、键盘的若干个消息、其他类似输入设备的输入消息等,但对每个消息的处理机制是相同的,因此为了简便起见,以鼠标左键被按下(LBUTTONDOWN)这个消息为例,来说明RAWIT对消息的处理。

先从RAWIT线程的入口函数开始,下列代码是该线程的入口函数(即该线程被调度执行的起始点):

978-7-111-41444-5-Chapter11-75.jpg

978-7-111-41444-5-Chapter11-76.jpg

978-7-111-41444-5-Chapter11-77.jpg

代码比较简单,就是一个无限循环,调用GetMessage从其线程队列内获取消息。注意GetMessage函数是阻塞操作,如果线程的消息队列内无任何消息,则该线程进入阻塞等待状态,等待消息的来临。一旦有消息被送入消息队列(通过SendMessage函数),则线程会被唤醒,进而对消息进行处理。如果线程消息队列内有消息,则GetMessage会把线程消息队列内的第一个消息(队列头消息),复制到其参数Msg内,然后从消息队列中删除该消息。

接下来就是分析Msg的消息类型了。Hello China V1.75版本定义的消息类型见表11-7。

表11-7 Hello China定义的消息类型

978-7-111-41444-5-Chapter11-78.jpg

(续)

978-7-111-41444-5-Chapter11-79.jpg

针对每个消息类型,RAWIT会进一步调用不同的函数进行处理。这里看LBUTTONDOWN消息,在收到该消息后,RAWIT线程会调用DoLButtonDown函数,对其进一步进行处理。需要记住的是,鼠标驱动程序使用消息对象的dwParam记录了鼠标消息的位置(低16比特记录了x分量的位置,高16比特记录了y分量的位置),因此在进一步调用DoLButtonDown函数前,需要把鼠标位置的x和y分量分离出来。

978-7-111-41444-5-Chapter11-80.jpg(www.xing528.com)

978-7-111-41444-5-Chapter11-81.jpg

在介绍上述代码之前,先引入一个概念:窗口消息。之前的设备消息和内核消息(这两者的定义是一样的),都是与图形界面的窗口无关的。但到了GUI之后,一个消息需要与窗口关联起来,比如一个鼠标点击,最终是落在某个窗口之内的,这样这个点击消息就与所在窗口建立了关联。因此这些与窗口进行关联的鼠标点击等消息,就叫做窗口消息。RAWIT线程的重要工作之一,就是把原始的内核(或设备)消息,转换为窗口消息。

与设备消息和内核消息不同的是,窗口消息的定义中增加了一个窗口句柄,下面是其定义:

978-7-111-41444-5-Chapter11-82.jpg

这里的hWnd,就是消息归属窗口的窗口句柄,message则是消息类型,与内核消息的wCommand含义一样。后面的wParam和lParam,分别用于存放与message相关的进一步信息。在GUI模式下,鼠标消息、键盘消息、触摸屏消息等,都是窗口消息。

下面正式分析DoLButtonDown函数,针对代码中标注的(1)、(2)、(3)、(4),分别介绍如下:

(1)显然,鼠标左键按下消息是一个窗口消息,RAWIT需要把原始消息转换为窗口消息,然后把窗口消息发送给目标应用程序。因为窗口消息对象是发送给另外的核心线程的,因此窗口消息不能从堆栈中分配内存,必须从核心内存池中分配。(1)部分代码,就是从核心内存池中申请一个窗口消息对象。需要注意的是,RAWIT只负责创建窗口消息,然后把消息发送给目标线程。窗口消息的销毁,是由目标线程完成的。目标线程处理完窗口消息后,必须调用KMemFree函数释放对应内存。这个过程无需应用程序编程人员考虑,操作系统相关调用已完成了内存的释放工作。

(2)从设备输入管理器(DIM)送到RAWIT线程的鼠标消息,其坐标位置是原始的,范围在0~255之间。但切换到图形模式后,屏幕分辨率是变化的(V1.75的缺省分辨率是1024×768像素)。这样必须把鼠标的原始位置,转换为在屏幕上的具体位置,这就是MouseToScreen函数的工作。这个函数通过读取Video对象的相关参数,获得屏幕分辨率,然后把原始鼠标坐标转换为屏幕坐标,并通过xpos和ypos返回。

(3)找到鼠标在屏幕上的坐标后,接下来就是RAWIT线程最核心、最重要的工作了:确定鼠标消息的归属窗口。要知道在图形模式下,屏幕上是有很多窗口的,而MouseToScreen函数返回的是鼠标消息在整个屏幕上的位置,而不是某个具体窗口的位置。因此,RAWIT线程就需要确定窗口左键按下消息,到底是落在哪个窗口内。找到归属窗口后,归属线程就好找了,因为在窗口对象中记录了窗口的归属线程。显然,GetFallWindow就是用户获取鼠标消息的归属窗口的。该函数查询窗口树,按照某个优先级对系统中的所有窗口进行匹配,最终找到目标窗口。需要注意的是,找到一个归属窗口后,首先判断该窗口是不是当前焦点窗口(注意与当前输入焦点线程的区别)。对于当前焦点窗口,窗口管理器会单独记录(WindowManager.pCurrWindow),这是为了处理键盘消息的目的。所有键盘消息(除了系统组合键),都会被发送到当前焦点窗口所在线程,这是操作系统GUI功能的通用处理方式。如果RAWIT发现鼠标左键按下的目标窗口不是当前焦点窗口,则需要修改当前焦点窗口为左击消息落入的窗口。在这之前,需要通知原焦点窗口已失去输入焦点,这就是UnfocusWindow函数的作用。同样地,也要通知鼠标消息落入窗口已经获得输入焦点。这就是FocusWindow的用途。

(4)找到鼠标左键按下消息落入的窗口对象后,剩下的事情就好办了,就是把鼠标在屏幕上的位置保存在窗口消息对象的lParam参数内(最低两个字节保存x坐标,最高两个字节保存y坐标),同时也把目标窗口的句柄保存在窗口消息内,然后把这个窗口消息发送给目标窗口所在线程。注意,这里在发送的时候,由于消息目标还是一个线程,因此必须以核心线程消息对象进行发送。于是,上述代码首先把窗口消息的地址,保存在内核消息的dwParam内,然后设置内核消息的消息类型为KERNEL_MESSAGE_WINDOW,调用SendMessage,把消息发送给目标线程。这里实际上是用一个内核消息携带了窗口消息。

好了,这个鼠标左键按下的消息就处理完了。其他消息,比如鼠标双击、鼠标右键按下等,与此处理方式类似,读者可通过阅读代码做进一步理解。相关代码,都在[gui/kthread/rawit.cpp]文件内。

接下来,就需要应用程序来完成消息的最终处理了。后面的过程,对Windows应用程序员来说,应该是驾轻就熟。但本书还是要详细讲解一下。因为这部分内容是消息驱动机制的精华所在,而且本书在讲解的时候,不会太注重应用程序层面的处理,而是深入内核机制,考察一下消息循环到底是怎么工作的。

免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。

我要反馈