28.1 原始输入线程(RIT)
(1)图解硬件输入模型
①当操作系统初始化时会创建一个原始输入线程(RIT)和系统硬件消息队列(SHIQ),这两者是系统硬件输入模型的核心。当SHIQ队列有硬件(如鼠标或键盘)消息时,RIT被唤醒,并将事件添加到用户线程的VIQ队列。
②任何时刻,只能有一个用户线程与RIT连接,该线程被称为前景线程。相对于其他线程创建的窗口,前景线程创建的窗口主要当前正在与用户交互的窗口。
③RIT如何知道要往哪个线程增加硬件输入消息?
A.如果是鼠标消息,RIT会调用检查当前鼠标所在的窗口,并调用GetWidowThreadProcessId找出创建该窗口的线程,然后向该线程添加消息。
B.如果是键盘事件,RIT只向前景线程添加键盘消息
④RIT如何切换与不同线程的连接?
A.当进程创建一个子进程,而子进程的线程里创建了一个窗口时,这个子进程的线程成为前景线程。
B.RIT负责Alt+Tab、Alt+Esc和Ctrl+Alt+Del键的处理,用户可以通过这些按键的组合来激活窗口并将创建该窗口的线程连接到RIT。(注意这些按键组合在RIT内部处理,所以我们无法拦截或丢弃它们)。Windows提供了一些激活窗口的函数(见后面)
(2)硬件输入模型的健壮性
①假设当前前景线程为线程B,此时RIT将消息分派给线程B的VIQ队列。如果此时线程B遇到一个死循环或死锁。由于线程B仍与RIT相连,所以系统的硬件消息仍会被添加到线程B的队列中。
②当用户按住Alt+Tab键时,由于Alt+Tab组合键由RIT线程内部处理,所以用户可以激活其他线程的窗口(如窗口A1),这里线程A将连接到RIT。因此,保证了当线程B出了问题时,不会影响到其他线程的执行。
28.2 局部输入状态
每个线程都有自己的THREADINFO结构,通过使用THREADINFO结构可以控制虚拟输入队列和一组状态信息,从而可以让线程使用虚拟输入队列和状态信息变量来实现对“窗口焦点、鼠标捕获”的管理信息有各自不同的处理,以防止对其他线程的影响。主要有两类:
①管理与键盘输入及焦点窗口有关的信息:如哪个窗口拥有键盘焦点、哪个窗口是活动的、键盘上哪些键被按下。
②管理与鼠标和光标有关的信息:哪个窗口捕获鼠标、鼠标的形状、鼠标的可见性
28.2.1 键盘输入与焦点
(1)焦点窗口与活动窗口的区别
线程内部会维护当前自己的活动窗口(Active Window)和焦点窗口(Focus Window),焦点窗口其实只是窗口的一个属性,其实就是“焦点状态”是窗口的一个属性,而焦点窗口的顶层窗口就是活动窗口。比如,一个对话框中有一个按钮,当按钮获得焦点的时候,那此按钮就是焦点窗口,则包含此按钮的对话框就是活动窗口,若出现窗口嵌套的情况,则最根的那个窗口才是活动窗口。
焦点窗口只是一个局部的概念,并不是所有的焦点窗口都可以获得键盘事件。只有前景线程的焦点窗口才能从系统队列中得到键盘事件(所以要SetFocus()),而前景线程中的活动窗口是前景窗口。在任何时刻系统中都只可能有一个被激活的窗口,这就是前景窗口。
(2)窗口间的焦点切换
①RIT是将键盘输入放到线程的虚拟输入队列(VIQ),而不是某个窗口。当线程GetMessage时,键盘事件被从VIQ中删除,并被分派到当前的焦点窗口。
②要让不同窗口接受键盘输入,须做两件事:一是让将接受线程与RIT连接。二是在该线程局部输入状态变量中记录要成为焦点的窗口。而SetFocus不能同时完成这两件事,只能做后面的一件。
③假设当前线程1与RIT连接,当调用SetFocus,然后传入参数hwndA、hwndB、hwndC则焦点会在这三个窗口之间切换。但如果传入的是hwndD、hwndE、hwndF中的任何一个都会失败,实际上SetFocus什么事情都不做。因为当前线程B还没与RIT相连接。
④仍然假设线程1与RIT正连接着,如果线程2调用SetFocus,然后传入hwndE。此时,线程2的局部输入状态变量会被更新以反映这一变化,在以后线程2连接到RIT时,键盘事件会被分派到窗口E中。但要注意的是,虽然在线程2还未连接RIT之前,窗口E还不能接收到键盘消息,但它会收到WM_SETFOCUS消息,如果是按钮,其表面会绘出虚线框。
⑤当焦点切换时,失去焦点的窗口会收到WM_KILLFOCUS消息。如果接收焦点的窗口与失去窗口的焦点属于不同线程创建的,那么失去焦点的窗口会更新局部输入状态变量,以表明该线程现在没有焦点窗口(调用GetFocus会返回NULL)。
(3)设置焦点窗口函数的比较
①SetForegroundWindow 和 SetActiveWindow的区别
SetActiveWindow(hWnd)函数,改变的是一个线程的局部状态变量,将活动窗口置为hWnd。如果当前线程是背景线程,则只改变局部状态变量。如果是前景线程,则该窗口会被置为前景窗口。但要注意的是这个函数不能够跨线程调用(也就是说不能够改变另外一个线程的局部变量),即如果hWnd为其他线程的窗口,则调用线程的局部状态变量将被设为NULL,所以GetActivWindow会返回NULL。
SetWindowPos、BringWindowToTop(该函数内部调用了SetWindowPos)可以跨线程或进程调用,函数会改变窗口的Z序,激活状态和焦点。如果调用线程未连接到RIT则什么也不做。如果调用线程己连接到RIT,则系统会激活hWnd窗口(其他线程也可以)。这也就意味着如果hWnd是其他线程创建的窗口,则会同时将这个线程连接到RIT,并改变其局部输入状态来反应激活窗口的变化。
SetForegroundWindow将窗口设为前景窗口, Windows为了防止突然的一个窗口跳至屏幕的Foreground,所以如果调用线程是背景线程,则产生的将是任务栏闪烁效果,表示不当前前景进程(正连接RIT的进程)不允许该窗口置于它的前面而成为前景窗口,我们可以手动到任务栏去激活这个窗口。而BringWindowToTop和SetWindowPos (TOP)在没有连接到RIT的时候则干脆不起效果。但是需要注意的是SetWindowPos(BOTTOM)还是有效果的(因为不违反Windows的这个约束)。
②但我们调用AllowSetForegroundWindow(dwProcessId),表示允许dwProcessId进程弹出的窗口置于调用线程的窗口之上。当传入参数ASFW_ANY表示允许任何进程,如果这时调用线程为RIT,而其他线程要通过SetForegroundWindow来置顶窗口时,只会在任务栏中闪烁提示,而不会真正被置顶。
③LockSetForegroundWindow函数,如果调用线程调用该函数并传入LSFW_LOCK参数,则当该线程为前景线程时,任何其他线程调用SetForegroundWindow函数都将失败。这可以防止当前线程的前景窗口被其他线程的窗口给挡住。传入参数LSFW_UNLOCK时则解锁这种阻止。当用户按Alt键或用户显式将一个窗口变为前景窗口时,系统会自动解锁这种阻止,以防止一个应用程序总是霸占桌面。
(4)键盘状态——比较GetKeyState和GetAsyncKeyState函数
函数 |
描述 |
GetKeyState(int nVirtKey) |
①线程局部输入状态包含一个同步键状态数组,这个数组为线程私有。 ②获得最近那次从消息队列中删除键盘消息时的按键状态,是从线程私有的同步键状态数组中获取到的。 ③nVirtKey指出要检查键的虚键代码,结果的高位为1时表示按下,0为释放 |
GetAsyncKeyState(int nVirtKey) |
①该函数从异步键状态数组中获取按键的状态,这个数组是所有线程所共享的,函数检查当前实时的键盘状态。 ②nVirtKey指出要检查键的虚键代码。结果的高位为1时表示按下,0为释放。 ③如果调用线程不是当前焦点窗口的创建线程,则函数总是返回0. |
28.2.2 鼠标光标管理
(1)ShowCursor(BOOL bShow):
①只影响调用线程的光标状态。
②调用ShowCursor(FALSE)多少次来隐藏,就要ShowCursor(TRUE)多少次才能显示出来。
(2)ClipCursor(CONST RECT* prc);
将鼠标限制在prc指定的范围内。但当异步激活事件发生(如用户激活另一个窗口)、调用SetForegroundWindow或用户按了Ctrl+Esc时,系统会停止鼠标剪裁。
(3)SetCapture(HWND hWnd)
①当调用SetCapture时,RIT会将所有消息发送给调用线程的VIQ队列,并把所有消息都发送给hWnd窗口。同时设置局部输入状态,以反映是哪个窗口被捕获。调用ReleaseCapture将释放捕获
②当鼠标按住时调用SetCapture,这里进行的是系统范围内的捕获。不管鼠标移到桌面上的哪个位置,鼠标消息都被发往hWnd窗口。如果此时释放鼠标按键时,RIT会检测到这个动作,此时如果鼠标位于其他线程创建的窗口时,RIT会将鼠标消息发给鼠标光标之下的窗口,而不是hWnd。如果鼠标位于调用线程创建的任何窗口时,这里系统会认为鼠标捕获仍然有效,所以只要鼠标位于调用线程所创建的任何一个窗口中,鼠标消息都会被发往hWnd窗口。换一句话讲,如果用户释放鼠标时,鼠标捕获不再是系统全局的捕获,而是线程局部的一种捕获。
③如果用户试图去激活另一个线程的窗口时,系统会自动向调用SetCapture线程发送鼠标按下和释放的消息。然后系统会更新调用线程的局部输入状态,以将捕获窗口设为NULL。
(4)SetCursor(HCURSOR hCursor);——设置光标的形状
①改变光标形状,并设置线程局部输入状态变量以更新鼠标的形状信息
②当鼠标在窗口中移动时(前提是未设置鼠标捕获),窗口会收到WM_SETFOCUS消息,这时可以调用SetCursor函数来设置鼠标的形状。
28.3 让多个线程共享某个线程的虚拟输入队列和局部输入状态变量
(1) AttachingThreadInput(IdAttach,IdAttachTo,fAttach);
参数 |
说明 |
IdAttach |
参数为不再使用虚拟输入队列和局部输入变量的线程Id |
IdAttachTo |
参数要共享虚拟输入队列和局部输入变量的线程Id |
fAttach |
TRUE时表示要挂接线程以共享,FALSE表示分离线程的VIQ和局部输入变量。 |
备注:可多次调用,以让多个线程共享同一个VIQ和局部输入状态变量 |
(2)举例说明:AttachThreadInput(idThreadA,idThreadB,TRUE);
①所有将输入窗口A1、B1、B2的硬件输入事件都将被添加到线程B的虚拟输入队列。在分离之前,线程A的虚拟输入队列不再接收输入事件。
②当两个线程共享虚拟输入队列时,也会共享同一套局部输入状态变量。但是会使用各自的posted-message、send-message、reply-message队列及唤醒标志位。
③当线程共享单一个VIQ队列时,会严重影响程序的健壮性。当一个线程接收一个击键消息并挂起,另一个线程就不能接收任何的输入。
(3)使用AttachThreadInput的场合
①在少数场合下,系统会显式地将两个线程挂起在一起。如线程安装了日志记录或日志回放钩子。在钩子卸载时会自动将两个线程分离。如果线程安装日志记录钩子,它等于告诉系统当发生硬件输入事件时,它都应该被通知。由于用户的输入必须被按相同的顺序记录下来,所以系统会共享一个VIQ以让所有的输入处理都能被同步起来。
②当应用程序创建了两个线程,第1个线程创建了一个对话框,当创建完成后。第2个线程调用CreateWindow,使用WS_CHILD并将对话框的句柄传给函数以便创建一个对话框的子窗口。系统会调用AttachThreadInput让第1个线程与第2个线程共对话框线程的VIQ,这个动作可以强制所有对话框所有的子窗口(包括第1个线程创建和其他线程创建的窗口)输入都可以同步起来。