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

内容概括

  • 本章主要讨论系统的硬件输入模型
  • 重点考察:按键和鼠标事件进入系统、发送给适当的窗口
  • 设计输入模型的目标:保证一个线程的动作不影响其他线程

16位 Windows 中的案例:

  • 若一个任务引起死循环,所有任务都被挂起,不再响应用户
  • 若一个任务死循环后,只能通过重启机器解决,则单个任务的执行影响太大

在 Windows 2000 和 Windows 98 中,一个挂起的线程不会妨碍其他线程接受硬件输入

1. 原始输入线程

当系统初始化时会创建一个原始输入线程和一个系统硬件输入队列,两者构成系统硬件输入模型的核心

  • 原始输入线程(raw input thread RIT),简称 RIT
  • 系统硬件输入队列(System hardware input queue SHIQ),简称 SHIQ
    在这里插入图片描述

RIT 处理 Input 消息

Windows 输入消息大多数只有鼠标和键盘两种:

  • 对于键盘消息,RIT 会把消息放入跟 RIT 连接的线程的虚拟输入队列
  • 对于鼠标消息,RIT 可以确定哪个窗口在鼠标光标之下,通过这个窗口调用 GetWindowThreadProcessId 来确定哪个线程建立了这个窗口,返回该线程的 ID,指出这个 ID 的线程得到鼠标消息

与按键硬件事件的处理不同,在任何时刻下,只有一个线程与 RIT 连接,这个线程称为 “前景线程”(因为它建立与用户交互的窗口,且线程中的窗口相对与其他窗口来说,处于画面的前景)

【例】
在任何时刻下,只有一个线程与 RIT 连接 :
在这里插入图片描述

按键消息进入虚拟输入队列:
在这里插入图片描述

不同的线程如何连接到 RIT

  • 连接前景线程:当创建一个进程时,进程的线程可以建立一个窗口,使创建窗口的线程同 RIT 连接(鼠标放在上面点一下)
  • 用键盘激活的窗口:RIT 要负责处理特殊的键组合:Alt + TabAlt + EscCtrl + Alt + Del 等,处理这些键组合时,可以保证总能用键盘激活窗口。 因为应用程序不能废弃这些键组合功能,当用户按下某个特殊组合时, RIT 激活选定的窗口,并将窗口的(创建)线程连接到 RIT
  • Windows 也提供激活窗口的功能,使窗口的线程连接到 RIT

注意:

  • 若当前的激活窗口无反应,可以通过按下 Alt + Tab 切换到其他窗口,不会有任何问题,除非电脑由于某种原因死机
  • 切换到其他窗口后,即使线程、窗口都没有响应,用户也可以对窗口一直输入(输入,但无响应的状态!就是那样!)

2. 局部输入状态

每个线程都有自己的输入状态变量,每个线程都有不同的焦点窗口、鼠标捕获窗口等概念

2.1 键盘输入与焦点

  • RIT 使用户的键盘输入流向一个线程的虚拟输入队列,而不是流向一个窗口(不涉及具体窗口)
  • 当线程调用 GetMessage 时,键盘事件从队列中移出,并分派给当前有输入焦点的窗口(调用接口的线程建立的窗口 )

在这里插入图片描述

2.2 激活窗口函数

2.2.1 SetActiveWindowGetActiveWindow

  • 激活系统中一个最高层(top-level)的窗口,并对这个窗口设定焦点
  • SetFocus 函数一样,若调用线程没有创建(作为函数参数的)窗口,则这个函数什么也不做
HWND SetActiveWindow(HWND hwnd);
  • SetActiveWindow 配合的函数 : GetActiveWindow
  • GetFocus 函数差不多,不同在于,此函数返回(由调用线程的局部输入状态变量 所指出的) 活动窗口的句柄
HWND GetActiveWindow();

2.2.2 BringWindowToTopSetWindowPos

  • 两个都是可以改变窗口的 Z 序(Z-order)、活动状态、焦点状态的函数
  • BringWindowToTop 函数在内部调用 SetWindowPos 实现,以 HWND_TOP 作为第二个参数
  • 若调用这两个函数的线程没有连接到 RIT,则函数什么也不做(无法操控状态变量值)
  • 若调用这两个函数的线程与 RIT 连接,即使指定的窗口不是调用线程的,系统也会激活该窗口,且创建窗口的线程被连接到 RIT (会引起调用线程、新连接到 RIT 的线程的局部输入状态变量的更新)
BOOL BringWindowToTop(HWWD hwnd);

BOOL SetWindowPos(
	HWND hwnd,
	HWND hwndInsertAfter,
	int x,
	int y,
	int cx,
	int cy,
	UINI fuFlags);

2.2.3 SetForegroundWindow

  • 仅当调用一个函数的线程,已经连接到 RIT 或者当前与 RIT 连接的线程在一定时间内(时间由 SystemParametersInfo 函数和 SPI_SETFOREGROUND_LOCKTIMEOUT 值来控制)没有接收到任何输入时,此函数才有效,若有一个菜单(GUI 的基础部分)是活动的,此函数失效
  • 若不允许 SetForegroundWindow 将窗口移到前景,它会闪烁窗口标题栏和任务条的窗口按钮,提示用户应该手工激活,有报告信息输出,可以用 SystemParametersInfo 函数和 SPI_SETFOREGROUNDFLASHCOUNT 值来控制闪烁

为了使这个函数内容更完整,系统提供了另外的一些函数:

AllowSetForegroundWindow

  • 调用 AllowSetForegroundWindow 函数可使指定进程的一个线程成功调用 SetForegroundWindow
  • 为了使任何进程都可以在线程的窗口上,弹出一个窗口,指定 ASFW_ANY(定义为-1)作为 dwProcessId 参数
BOOL AllowSetForegroundWindow(DWORD dwProcessId);

LockSetForegroundWindow

  • 锁定 SetForegroundWindow 函数,使它总是失效的
  • 对于 uLockCode 参数可以指定 LSFW_LOCKLSFW_UNLOCK
BOOL LockSetForegroundWindow(UINI uLockCode);
  • 当一个菜单被激活时,系统在内部调用这个函数,这样试图跳到前景的窗口,不能关闭这个的菜单

【例】

  • Windows 在显示 Start 菜单时,需要明确地调用这些函数,因为 Start 菜单不是一个内置菜单
  • 当用户按下 Alt 或将一个窗口拉到前景时,系统自动解锁 SetForegroundWindow 函数,这样可以防止一个程序一直对 SetForegroundWindow 函数封锁

2.2.4 同步键状态

  • 每个线程的局部输入状态变量,都包含一个同步键状态数组
  • 所有的线程共享一个同步键状态数组,这些数组反应了:在任何给定时刻键盘所有键的状态

GetAsyncKeyState 函数,确定用户当前是否按下了键盘的一个键:

SHORT GetAsyncKeyState(int nVirtKey);
  • nVirtKey 指出要检查键的虚键代码
  • 返回的结果高位指出该键当前是否被按下:是为1,否为0
  • 可以用来检查用户是否释放了某些按键

2.3 鼠标光标管理

2.3.1 ClipCursor

  • ClipCursor 函数将鼠标光标剪贴到一个矩形区域
  • 使鼠标被限制在一个由 prc 参数指定的矩形区域内
BOOL ClipCursor(CONST RECT *prc);

允许剪贴鼠标光标可能会对其他线程产生不利的影响,而不允许剪贴鼠标光标会影响调用线程,系统实现了一种折中的方案:

  • 当一个线程调用这个函数时,程序将光标剪贴到指定的矩形区域
  • 但若同步激活事件(用户点击其他程序的窗口、调用了 SetForgroundWindow,或按下了 Ctrl + Esc 组合键)发生,系统会停止剪贴鼠标光标的移动,允许鼠标在整个屏幕上自由移动

2.3.2 鼠标捕获

捕获调用 SetCapture,释放调用 ReleaseCapture

  • 当一个窗口捕获鼠标时,它要求所有的鼠标消息从 RIT 发到调用线程的虚拟输入队列,并且所有的鼠标消息从虚拟输入队
    列发到设置捕获的窗口,在调用 ReleaseCapture 之前,要一直持续这种鼠标消息的捕捉

鼠标的捕获必须与系统的强壮性折衷:

  • 当一个程序调用 SetCapture 时,RIT 将所有鼠标信息放入线程的虚拟输入队列
  • SetCapture 要为调用 SetCapture 的线程设置局部输入状态变量

做和不做鼠标捕获时,消息传递情况:

  • 一个程序在用户按一个鼠标按钮时调用 SetCapture
  • 鼠标没有按下时,RIT 不再将鼠标信息发往线程的虚拟输入队列,而是发往与鼠标光标所在的窗口相联系的输入队列
  • 也可以说,当用户释放了所有鼠标按钮时,鼠标捕获不再 在全系统范围内执行,而是在一个线程的局部范围内执行

若用户想激活一个其他线程所建立的窗口,系统自动向设置捕获的线程发送:鼠标按钮按下、鼠标按钮放开的消息,然后更新线程的局部输入状态变量,指出该线程不再有鼠标捕获

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

  • 由上述讨论得:输入模型是强壮的,每个线程都有自己的局部输入状态环境,在必要时,每个线程还可以连接到 RIT 或从 RIT 断开

3.1 AttachThreadInput 函数

  • 可以通过多次调试 AttachThreadInput 函数让多个线程共享一个虚拟输入队列和局部输入状态变量
  • 可以利用 AttachThreadInput 函数来强制两个或多个线程共享同一个虚拟输入队列和一组局部输入状态变量
BOOL AttachThreadInput(
	DWORD idAttach,		//线程的ID,该线程所包含的虚拟输入队列是你不想再使用的
	DWORD idAttachTo,	//另一个线程的ID,线程包含的虚拟输入队列,是想让两个线程共享的
	BOOL fAttach;		//共享设为TRUE, 分开设为 FALSE
)

3.2 案例

线程 A 调用 AttachThreadInput,传递线程 A 的 ID 作为第一个参数,线程 B 的 ID 作为第二个参数, TRUE 作为最后一个参数:

AttachThreadInput(AThreadID, BThreadID, TRUE);
  • 发往 A1、B1、B2 的硬件输入事件,都将添加到线程 B 的虚拟输入队列中
  • 线程 A 的虚拟输入队列不再接收输入事件,除非再调用一次 AttachThreadInput 最后一个参数设为 FALSE
    在这里插入图片描述

3.3 总结

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

使用 AttachThreadInput 函数存在的问题:

  • 若让所有的线程都共享一个输入队列,就会严重削弱系统的强壮性
  • 若某一个线程接收一个按键消息并且挂起,其他的线程就不能接收任何输入

综上所述,应该尽量避免使用 AttachThreadInput 函数,但也不是绝对的,以下列举两种使用 AttachThreadInput 的情况

安装日志记录挂钩(journal record hook)或日志播放挂钩(journal playback hook)时

  • 当挂钩被卸载时,系统自动恢复所有线程,这样线程就可以使用 (挂钩安装前它们所使用的相同)输入队列
  • 当某个线程安装日志记录挂钩时,这个线程通常将 “硬件事件的通知信息” 保存或记录在一个文件上,因输入必须按进入的次序来记录,所以系统中每个线程要共享一个虚拟输入队列,使所有的输入处理同步

设某程序建立了两个线程:

  • 第一个线程建立了一个对话框,在这个对话框建立之后,第二个线程调用 GreatWindow,使用 WS_CHILD 风格,并向这个子窗口的双亲传递对话框的句柄
  • 系统用子窗口的线程调用 AttachThreadInput,让子窗口的线程使用(对话框线程所使用的)输入队列,这样就使对话框的 所有子窗口之间,对输入强制同步

3.4 两个示例程序(略)

猜你喜欢

转载自blog.csdn.net/qq_36804363/article/details/125884255
今日推荐