第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核心编程 第8章201页旋转锁的代码在新版Visual Studio运行问题

    // 全局变量,用于指示共享的资源是否在使用 BOOL g_fResourceInUse = FALSE; void Func1() { //等待访问资源 while(InterlockedExcha ...

  2. windows核心编程 第5章job lab示例程序 解决小技巧

    看到windows核心编程 第5章的最后一节,发现job lab例子程序不能在我的系统(win8下)正常运行,总是提示“进程在一个作业里”         用process explorer程序查看 ...

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

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

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

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

  5. windows核心编程---第七章 用户模式下的线程同步

    用户模式下的线程同步 系统中的线程必须访问系统资源,如堆.串口.文件.窗口以及其他资源.如果一个线程独占了对某个资源的访问,其他线程就无法完成工作.我们也必须限制线程在任何时刻都能访问任何资源.比如在 ...

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

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

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

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

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

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

  9. windows核心编程---第六章 线程的调度

    每个线程都有一个CONTEXT结构,保存在线程内核对象中.大约每隔20ms windows就会查看所有当前存在的线程内核对象.并在可调度的线程内核对象中选择一个,将其保存在CONTEXT结构的值载入c ...

随机推荐

  1. CVE-2017-12149-JBoss 5.x/6.x 反序列化

    漏洞分析 https://www.freebuf.com/vuls/165060.html 漏洞原理 该漏洞位于JBoss的HttpInvoker组件中的 ReadOnlyAccessFilter 过 ...

  2. shell脚本,mysql数据库的备份,并压缩

    db_user="root"db_passwd="xxx"db_host="localhost" databases="xxx-p ...

  3. 死磕生菜 -- lettuce 间歇性发生 RedisCommandTimeoutException 的深层原理及解决方案

    0x00 起源 项目的一些微服务集成了 Spring Data Redis,而底层的 Redis 客户端是 lettuce,这也是默认的客户端.微服务在某些环境中运行很正常,但在另一些环境中运行就会间 ...

  4. java 集合 的理解

    1.对象的存储:①数组(基本数据类型 & 引用数据类型) ②集合(引用数据类型) >数组存储数据的弊端:长度一旦初始化以后,就不可变:真正给数组元素赋值的个数没有现成的方法可用. 2.集 ...

  5. 实验: survivor放不下的对象进入老年代

    实验一: 存活对象包含 小于survivor大小的对象 + 大于survivor的对象 private static final Integer _1MB = 1024 * 1024; /** * - ...

  6. PTA 找出不是两个数组共有的元素

    7-2 找出不是两个数组共有的元素 (20 分)   给定两个整型数组,本题要求找出不是两者共有的元素. 输入格式: 输入分别在两行中给出两个整型数组,每行先给出正整数N(≤),随后是N个整数,其间以 ...

  7. 【java框架】SpringBoot(5)--SpringBoot整合分布式Dubbo+Zookeeper

    1.理论概述 1.1.分布式 分布式系统是若干独立计算机的集合,这些计算机对于用户来讲就像单个系统. 由多个系统集成成一个整体,提供多个功能,组合成一个板块,用户在使用上看起来是一个服务.(比如淘宝网 ...

  8. 学习笔记-git 上传

    0.git add * (如果你需要修改源码需要在 1 之前使用,然后再回到 1) 1.git commit -m '提交文字描述' 2.git push -u origin master (上传到主 ...

  9. Dynamics CRM报表提示rsProcessingAborted解决方法

    有时候CRM用的好好的突然报表提示了一个错误,rsProcessingAborted如下图: 开始以为是权限问题,在数据库捣鼓了很长时间,服务也重启了很多遍都没效果.后来试了一下重新安装一下报表服务器 ...

  10. 这样介绍Ribbon,从此任何问题也难不住你

    Springcloud的核心组件之Ribbon 上篇文章详细介绍了springcloud的注册中心Eureka,那么这篇文章则会介绍springcloud的另外一个组件Spring Cloud Rib ...