第27章 硬件输入模型和局部输入状态

这章说的是按键和鼠标事件是如何进入系统并发送给适当的窗口过程的。微软设计输入模型的一个主要目标就是为了保证一个线程的动作不要对其他线程的动作产生不好的影响。

27.1 原始输入线程

当系统初始化时,要建立一个特殊的线程,即原始输入线程(raw input thread,R I T)。此外,系统还要建立一个队列,称为系统硬件输入队列(System hardware input queue, SHIQ)。R I T和S H I Q构成系统硬件输入模型的核心。

R I T怎么才能知道要向哪一个线程的虚拟输入队列里增加硬件输入消息?对鼠标消息,R I T只是确定是哪一个窗口在鼠标光标之下。利用这个窗口, R I T调用G e t Wi n d o w T h r e a dP r o c e s s I d来确定是哪个线程建立了这个窗口。返回的线程 I D指出哪一个线程应该得到这个鼠标消息。

对按键硬件事件的处理稍有不同。在任何给定的时刻,只有一个线程同 R I T“连接”。这个线程称为前景线程(foreground thread),因为它建立了正在与用户交互的窗口,并且这个线程的窗口相对于其他线程所建立的窗口来说处在画面中的前景。

当一个用户在系统上登录时, Windows Explorer进程让一个线程建立相应的任务栏(t a s k b a r)和桌面。这个线程连接到R I T。如果你又要产生C a l c u l a t o r,那么就又有一个线程来建立一个窗口,并且这个线程变成连接到 R I T的线程。注意现在Windows Explorer的线程不再与R I T连接,因为在一个时刻只能有一个线程同R I T连接。当一个按键消息进入S H I Q时,R I T就被唤醒,将这个事件转换成适当的按键消息,并将消息放入与R I T连接的线程的虚拟输入队列。

不同的线程是如何连接到R I T的呢?我们已经说过,当产生一个进程时,这个进程的线程可以建立一个窗口。这个窗口处于前景,其建立窗口的线程同 R I T相连接。另外,R I T还要负责处理特殊的键组合,如A l t + Ta b、A l t + E s c和C t r l + A l t + D e l等。因为R I T在内部处理这些键组合,就可以保证用户总能用键盘激活窗口。应用程序不能够拦截和废弃这些键组合。当用户按动了某个特殊的键组合时,R I T激活选定的窗口,并将窗口的线程连接到R I T。Wi n d o w s也提供激活窗口的功能,使窗口的线程连接到R I T。

从上面的图中可以看到如何保护线程,避免相互影响的。如果 R I T向窗口 B 1 或窗口 B 2 发送一个消息,消息到达线程 B的虚拟输入队列。在处理消息时,线程 B在与五个内核对象同步时可能会进入死循环或死锁。如果发生这种情况,线程仍然同 R I T连接在一起,并且可能有更多的消息要增加到线程的虚拟输入队列中。

这种情况下,用户会发现窗口 B 1和B 2都没有反应,可能想切换到窗口 A 1 。为了做这种切换,用户按A l t + Ta b。因为是R I T处理A l t + Ta b按键组合,所以用户总能切换到另外的窗口,不会有什么问题。在选定窗口 A 1 之后,线程 A就连接到R I T。这个时候,用户就可以对窗口 A 1 进入输入,尽管线程及其窗口都没有响应。

27.2 局部输入状态

• 哪一个窗口有鼠标捕获。

• 鼠标光标的形状。

• 鼠标光标的可见性。

由于每个线程都有自己的输入状态变量,每个线程都有不同的焦点窗口、鼠标捕获窗口等概念。从一个线程的角度来看,或者它的某个窗口拥有键盘焦点,或者系统中没有窗口拥有键盘焦点;或者它的某个窗口拥有鼠标捕获,或者系统中没有窗口拥有鼠标捕获,等等。

27.2.1 键盘输入与焦点

R I T使用户的键盘输入流向一个线程的虚拟输入队列,而不是流向一个窗口。R I T将键盘事件放入线程的虚拟输入队列时不用涉及具体的窗口。当这个线程调用G e t M e s s a g e时,键盘事件从队列中移出并分派给当前有输入焦点的窗口。(由该线程所建立)。下图说明了这个处理过程。

线程1当前正在从R I T接收输入,用窗口A、窗口B或窗口C的句柄作参数调用S e t F o c u s会引起焦点改变。失去焦点的窗口除去它的焦点矩形或隐藏它的插入符号,获得焦点的窗口画出焦点矩形或显示它的插入符号。

假定线程1仍然从R I T接收输入,并用窗口 E的句柄作为参数调用 S e t F o c u s。这种情况下,系统阻止执行这个调用,因为想要设置焦点的窗口不使用当前连接 R I T的虚拟输入队列。在线的线程不一样,那么,对于建立失去焦点窗口的线程,要更新它的局部输入状态变量,说明它没有窗口拥有焦点。这时调用G e t F o c u s将返回N U L L,这会使线程知道当前没有窗口拥有焦点。
    函数S e t A c t i v e Wi n d o w激活系统中一个最高层( t o p - l e v e l)的窗口,并对这个窗口设定焦点:

HWND WINAPI SetActiveWindow(__in HWND hWnd);

同S e t F o c u s函数一样,如果调用线程没有创建作为函数参数的窗口,则这个函数什么也不做。

与S e t A c t i v e Wi n d o w相配合的函数是G e t A c t i v e Wi n d o w函数:

HANDLE GetActiveWindow();

这个函数的功能同G e t F o c u s函数差不多,不同之处是它返回由调用线程的局部输入状态变量所指出的活动窗口的句柄。当活动窗口属于另外的线程时, G e t A c t i v e Wi n d o w返回N U L L。

其他可以改变窗口的 Z序(Z - o r d e r)、活动状态和焦点状态的函数还包括 B r i n g Wi n d o w ToTo p和S e t Wi n d o w P o s:

BOOL WINAPI BringWindowToTop(__in HWND hWnd);

BOOL WINAPI SetWindowPos(

_In_ HWND hWnd,

_In_opt_ HWND hWndInsertAfter,

_In_ int X,

_In_ int Y,

_In_ int cx,

_In_ int cy,

_In_ UINT uFlags);

这两个函数功能相同(实际上, B r i n g Wi n d o w To To p函数在内部调用 S e t Wi n d o w P o s,以H W N D _ TO P作为第二个参数)。如果调用这两个函数的线程没有连接到 R I T,则函数什么也不做。如果调用这些函数的线程同 R I T相连接,系统就会激活相应的窗口。注意即使调用线程不是建立这个窗口的线程,也同样有效。这意味着,这个窗口变成活动的,并且建立这个窗口的线程被连接到R I T。这也引起调用线程和新连接到R I T的线程的局部输入状态变量被更新。

有时候,一个线程想让它的窗口成为屏幕的前景。例如,有可能会利用 Microsoft Qutlook

安排一个会议。在会议开始前的半小时, O u t l o o k弹出一个对话框提醒用户会议将要开始。如果Q u t l o o k的线程没有连接到R I T,这个对话框就会藏在其他窗口的后面,有可能看不见它。

为了制止这种现象,微软对 S e t F o r e g r o u n d Wi n d o w函数增加了更多的智能。特别规定,仅当调用一个函数的线程已经连接到 R I T或者当前与R I T相连接的线程在一定的时间内(这个时间量由S y s t e m P a r a m e t e r s I n f o函数和S P I _ S E T F O R E G R O U N D _ L O C K T I M E O U T值来控制)没有收到任何输入,这个函数才有效。另外,如果有一个菜单是活动的,这个函数就失效。

如果不允许S e t F o r e g r o u n d Wi n d o w将窗口移到前景,它会闪烁该窗口的标题栏和任务条上该窗口的按钮。用户看到任务条按钮闪烁,就知道该窗口想得到用户的注意。用户应该手工激活这个窗口,看一看要报告什么信息。还可以用S y s t e m P a r a m e t e r s I n f o函数和S P I _ S E T F O R E G R O U N D -F L A S H C O U N T值来控制闪烁。

由于这些新的内容,系统又提供了另外一些函数。如果调用 A l l o w S e t F o r e g r o u n d Wi n d o w的线程能够成功调用S e t F o r e g r o u n d Wi n d o w,第一个函数(见下面所列)可使指定进程的一个线程成功调    用S e t F o r e g r o u n d Wi n d o w。为了使任何进程都可以在你的线程的窗口上弹出一个窗口,指定A S F W _ A N Y (定义为-1 )作为d w P r o c e s s I d参数:

此外,线程可以锁定 S e t F o r e g r o u n d Wi n d o w函数,使它总是失效的。方法是调用 L o c kS e t F o r e g r o u n d Wi n d o w。

BOOL LockSetForegroundWindow(UINT uLockCode);

对u L o c k C o d e参数可以指定L S F W _ L O C K或者L S F W _ U N L O C K。当一个菜单被激活时,系统在内部调用这个函数,这样一个试图跳到前景的窗口就不能关闭这个菜单。 Wi n d o w sE x p l o r e r在显示S t a r t菜单时,需要明确地调用这些函数,因为 S t a r t菜单不是一个内置菜单。当用户按了A l t键或者将一个窗口拉到前景时,系统自动解锁 S e t F o r e g r o u n d Wi n d o w函数。这可以防止一个程序一直对S e t F o r e g r o u n d Wi n d o w函数封锁。

关于键盘管理和局部输入状态,其他的内容是同步键状态数组。每个线程的局部输入状态变量都包含一个同步键状态数组,但所有的线程要共享一个同步键状态数组。这些数组反映了在任何给定时刻键盘所有键的状态。利用 G e t A s y n c K e y S t a t e函数可以确定用户当前是否按下了键盘上的一个键:

SHORT WINAPI GetAsyncKeyState(__in int vKey);

参数n Vi r t K e y指出要检查键的虚键代码。结果的高位指出该键当前是否被按下(是为 1,否为0)。笔者在处理一个消息时,常用这个函数来检查用户是否释放了鼠标主按钮。为函数参数赋一个虚键值V K _ L B U T TO N,并等待返回值的高位成为0。注意,如果调用函数的线程不是建立的窗口上,鼠标光标就可见了。

鼠标光标管理的另一个方面是使用C l i p C u r s o r函数将鼠标光标剪贴到一个矩形区域。

BOOL ClipCursor(CONST RECT *prc);

这个函数使鼠标被限制在一个由p r c参数指定的矩形区域内。当一个程序调用 C l i p C u r s o r函数时,系统该做些什么呢?允许剪贴鼠标光标可能会对其他线程产生不利影响,而不允许剪贴鼠标光标又会影响调用线程。微软实现了一种折衷的方案。当一个线程调用这个函数时,系统将鼠标光标剪贴到指定的矩形区域。但是,如果同步激活事件发生(当用户点击了其他程序的窗口,调用了S e t F o r e g r o u n d Wi n d o w,或按了C t r l + E s c组合键),系统停止剪贴鼠标光标的移动,允许鼠标光标在整个屏幕上自由移动。

27.3 将虚拟输入队列同局部输入状态挂接在一起

从上面的讨论我们可以看出这个输入模型是强壮的,因为每个线程都有自己的局部输入状态环境,并且在必要时每个线程可以连接到 R I T或从R I T断开。有时候,我们可能想让两个或多个线程共享一组局部输入状态变量及一个虚拟输入队列。

可以利用A t t a c h T h r e a d I n p u t函数来强制两个或多个线程共享同一个虚拟输入队列和一组局部输入状态变量:

BOOL WINAPI AttachThreadInput(

__in DWORD idAttach,

__in DWORD idAttachT);

函数的第一个参数i d A t t a c h,是一个线程的I D,该线程所包含的虚拟输入队列(以及局部输入状态变量)是你不想再使用的。第二个参数 i d A t t a c h To,是另一个线程的I D,这个线程所包含的虚拟输入队列(和局部输入状态变量)是想让两个线程共享的。第三个参数 f A t t a c h,当想让共享发生时,被设置为 T R U E,当想把两个线程的虚拟输入队列和局部输入状态变量分开时,设定为FA L S E。可以通过多次调用A t t a c h T h r e a d I n p u t函数让多个线程共享同一个虚拟输入队列和局部输入状态变量。

我们再考虑前面的例子,假定线程 A调用A t t a c h T h r e a d I n p u t,传递线程 A的I D作为第一个参数,线程B的I D作为第二个参数,T R U E作为最后一个参数:

线程 A的虚拟输入队列将不再接收输入事件,除非再一次调用A t t a c h T h r e a d I n p u t并传递FA L S E作为最后一个参数,将两个线程的输入队列分开。

当将两个线程的输入都挂接在一起时,就使线程共享单一的虚拟输入队列和同一组局部输入状态变量。但线程仍然使用自己的登记消息队列、发送消息队列、应答消息队列和唤醒标志(见第2 6章的讨论)。

如果让所有的线程都共享一个输入队列,就会严重削弱系统的强壮性。如果某一个线程接收一个按键消息并且挂起,其他的线程就不能接收任何输入了。所以应该尽量避免使用A t t a c h T h r e a d I n p u t函数。在某些情况下,系统隐式地将两个线程挂接在一起。第一种情况是当一个线程安装一个日志记录挂钩(journal record hook)或日志播放挂钩(journal playback hook)的时候。当挂钩被卸载时,系统自动恢复所有线程,这样线程就可以使用挂钩安装前它们所使用的相同输入队列。

当一个线程安装一个日志记录挂钩时,它是让系统将用户输入的所有硬件事件都通知它。这个线程通常将这些信息保存或记录在一个文件上。因用户的输入必须按进入的次序来记录,所以系统中每个线程要共享一个虚拟输入队列,使所有的输入处理同步。

还有一些情况,系统会代替你隐式地调用 A t t a c h T h r e a d I n p u t。假定你的程序建立了两个线程。第一个线程建立了一个对话框。在这个对话框建立之后,第二个线程调用 G r e a t Wi n d o w,使用W S _ C H I L D风格,并向这个子窗口的双亲传递对话框的句柄。系统用子窗口的线程调用A t t a c h T h r e a d I n p u t,让子窗口的线程使用对话框线程所使用的输入队列。这样就使对话框的所有子窗口之间对输入强制同步。

Windows核心编程 第27章 硬件输入模型和局部输入状态相关推荐

  1. 《Windows 核心编程》27章:硬件输入模型和局部输入状态

    内容概括 本章主要讨论系统的硬件输入模型 重点考察:按键和鼠标事件进入系统.发送给适当的窗口 设计输入模型的目标:保证一个线程的动作不影响其他线程 16位 Windows 中的案例: 若一个任务引起死 ...

  2. Windows核心编程笔记(二十一) 硬件输入模型和局部输入状态

    28.1 原始输入线程(RIT) (1)图解硬件输入模型 ①当操作系统初始化时会创建一个原始输入线程(RIT)和系统硬件消息队列(SHIQ),这两者是系统硬件输入模型的核心.当SHIQ队列有硬件(如鼠 ...

  3. Windows核心编程 第26章 窗口消 息

    窗 口 消 息 Wi n d o w s允许一个进程至多建立10 000个不同类型的用户对象(User object):图符.光标.窗口类.菜单.加速键表等等.当一个线程调用一个函数来建立某个对象时, ...

  4. Windows核心编程 第七章 线程的调度、优先级和亲缘性(下)

    7.6 运用结构环境 现在应该懂得环境结构在线程调度中所起的重要作用了.环境结构使得系统能够记住线程的状态,这样,当下次线程拥有可以运行的C P U时,它就能够找到它上次中断运行的地方. 知道这样低层 ...

  5. Windows核心编程 第十七章 -内存映射文件(上)

    第1 7章 内存映射文件 对文件进行操作几乎是所有应用程序都必须进行的,并且这常常是人们争论的一个问题.应用程序究竟是应该打开文件,读取文件并关闭文件,还是打开文件,然后使用一种缓冲算法,从文件的各个 ...

  6. Windows核心编程 第六章 线程基础知识 (上)

    第6章 线程的基础知识 理解线程是非常关键的,因为每个进程至少需要一个线程.本章将更加详细地介绍线程的知识.尤其是要讲述进程与线程之间存在多大的差别,它们各自具有什么作用.还要介绍系统如何使用线程内核 ...

  7. Windows核心编程 第四章 进程(上)

    第4章 进 程     本章介绍系统如何管理所有正在运行的应用程序.首先讲述什么是进程,以及系统如何创建进程内核对象,以便管理每个进程.然后将说明如何使用相关的内核对象来对进程进行操作.接着,要介绍进 ...

  8. Windows核心编程 第十七章 -内存映射文件(下)

    17.3 使用内存映射文件 若要使用内存映射文件,必须执行下列操作步骤: 1) 创建或打开一个文件内核对象,该对象用于标识磁盘上你想用作内存映射文件的文件. 2) 创建一个文件映射内核对象,告诉系统该 ...

  9. Windows核心编程 第十一章 线程池的使用

    第11章 线程池的使用 第8章讲述了如何使用让线程保持用户方式的机制来实现线程同步的方法.用户方式的同步机制的出色之处在于它的同步速度很快.如果关心线程的运行速度,那么应该了解一下用户方式的同步机制是 ...

最新文章

  1. 北斗时钟在国内各行业的应用前景
  2. 不看后悔 如何删除WIN7的100M隐藏分区
  3. 金融数据分析(四)-------矩,偏度,峰度
  4. 粒子物理学有了新的基础数学理论
  5. php 验证微信token_微信token认证程序
  6. springcloud的config
  7. 上传文件显示进度条_【技巧 】iOSamp;Windows互传文件?透过「文件」轻松解决~...
  8. Springboot整合zookeeper
  9. 关于泊松过程的参数估计
  10. memcpy的两种实现(转)
  11. Java开发 明华usbkey_明华驱动官方版下载-明华usbkey数字证书驱动下载v3.0.2420.9 最新版-当易网...
  12. Karen与测试 奇迹淫巧+快速幂
  13. 云计算具有哪些特点,主要分为哪几大类型?
  14. nodejs完成从163邮箱发送邮件到qq邮箱
  15. 强化学习(RLAI)读书笔记第六章差分学习(TD-learning)
  16. 如何将html转换成avi,MP4如何转换AVI格式 如何将MP4转换成AVI
  17. IOS_音视频不同步问题
  18. ZBrush Braid Brush 辫子笔刷——缎沫晗自制
  19. 2022安全员-A证考试题模拟考试平台操作
  20. 天下手游服务器维修,《天下》手游3月2日更新公告 亡命副本全服开启

热门文章

  1. Git 工作流的正确打开方式
  2. 动态规划之 0-1背包问题及改进
  3. Flask与Ajax
  4. jQuery源码dom ready分析
  5. 在 Mac OSX 版的 LispBox 环境上安装配置 SBCL 详细过程
  6. Object_Type列表
  7. Tip #6 用ASP.NET AJAX判断当前浏览器类型
  8. Linux下grep显示前后几行信息
  9. MongoDB · 引擎特性 · MongoDB索引原理
  10. Delphi开发的数据库程序在C:\PDOXUSRS.NET生成文件,拒绝访问及读写权限