• 第28章 硬件输入模型和局部输入状态(全书完)


    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个线程创建和其他线程创建的窗口)输入都可以同步起来。

    【28_LISLab程序】检查线程局部输入状态

      ①GetFocus、GetActiveWindow、GetForegroundWindow、GetCapture是从调用线程自己的局部输入状态中获取信息的

      ②当激活记事本时(属不同线程间的切换),LISLab会收到WM_KILLFOCUS,从而会认为自己没有“焦点窗口”及“激活窗口”

      ③SetFocus、SetActiveWindow会返回之前的“焦点窗口”或“激活窗口”的句柄,并显示在“先前窗口”标签中。

      ④可以选中列表框的“记事本”,然后在“AttachToNotepad”和“DetachFromNotepad”下分别观察SetFocus等几个函数的操作结果。

      ⑤鼠标捕获实验:观察鼠标按住和释放鼠标时的不同

    //LISLab.cpp

    /************************************************************************
    Module: LISLab.cpp
    Notices:Copyright(c) 2008 Jeffrey Richter & Christophe Nasarre
    ************************************************************************/
    
    #include "../../CommonFiles/CmnHdr.h"
    #include "resource.h"
    #include <tchar.h>
    
    //////////////////////////////////////////////////////////////////////////
    #define TIMER_DELAY  (500)  //半秒
    UINT_PTR  g_uTimerId = 1;
    int       g_nEventId = 0; //所选择的函数(如SetFocus:0,SetActiveWindow:1等)
    DWORD     g_dwEventTime = 0;//事件发生以来的毫秒数
    HWND      g_hwndSubject = NULL; //当前窗口列表中选中的窗口
    HWND      g_hwndNotepad = NULL; //记事本的句柄
    
    //////////////////////////////////////////////////////////////////////////
    void CalcWndText(HWND hwnd, PTSTR szBuf, int nLen){
        TCHAR szClass[50], szCaption[50], szBufT[150];
    
        if (hwnd == (HWND)NULL){
            _tcscpy_s(szBuf, nLen,TEXT("(无窗口)"));
            return;
        }
    
        if (!IsWindow(hwnd)){
            _tcscpy_s(szBuf,nLen, TEXT("(无效窗口)"));
            return;
        }
    
        GetClassName(hwnd, szClass, chDIMOF(szClass));
        GetWindowText(hwnd, szCaption, chDIMOF(szCaption));
    
        wsprintf(szBufT, TEXT("[%s] %s"), (PTSTR)szClass,
                 (*szCaption ==0)?TEXT("(无标题)"):(PTSTR)szCaption);
    
        _tcsncpy_s(szBuf,nLen, szBufT, nLen - 1);
        szBuf[nLen - 1] = 0; //强制0字符串结束
    }
    
    //////////////////////////////////////////////////////////////////////////
    //为了最大限度为减少对栈的使用,这里创建一个局部变量WALKWINDOWTREEDATA,并
    //将其指针传给WalkWindowTreeRecurse递归函数。
    
    //在WalkWindowTreeRecurse递归函数中使用的数据
    typedef struct{
        HWND hwndLB;       //要输出信息到指定的列表框
        HWND hwndParent;   //父句柄
        int nLevel;        //递归深度
        int nIndex;        //列表框项的索引
        TCHAR szBuf[100];  //输出缓冲区
        int  iBuf;         //szBuf的索引值
    }WALKWINDOWTREEDATA,*PWALKWINDOWTREEDATA;
    
    //////////////////////////////////////////////////////////////////////////
    void WalkWindowTreeRecurse(PWALKWINDOWTREEDATA pWWT){
        if (!IsWindow(pWWT->hwndParent)) //父窗口是否存在
            return;
    
        pWWT->nLevel++;
        const int nIndexAmount = 2;
    
        //格式化输出,为了直观,每级比上一级缩进2个字符
        for (pWWT->iBuf = 0; pWWT->iBuf < pWWT->nLevel*nIndexAmount; pWWT->iBuf++)
            pWWT->szBuf[pWWT->iBuf] = TEXT(' ');
    
        //获取窗体类名及标题
        CalcWndText(pWWT->hwndParent, &pWWT->szBuf[pWWT->iBuf],
                    chDIMOF(pWWT->szBuf)-pWWT->iBuf);
    
        pWWT->nIndex = ListBox_AddString(pWWT->hwndLB, pWWT->szBuf);
        ListBox_SetItemData(pWWT->hwndLB, pWWT->nIndex, pWWT->hwndParent);
    
        HWND hwndChild = GetFirstChild(pWWT->hwndParent);
        while (hwndChild != NULL){
            pWWT->hwndParent = hwndChild;
            WalkWindowTreeRecurse(pWWT);
            hwndChild = GetNextSibling(hwndChild);
        }
        pWWT->nLevel--;
    }
    //////////////////////////////////////////////////////////////////////////
    void WalkWindowTree(HWND hwndLB, HWND hwndParent){
        WALKWINDOWTREEDATA WWT;
        WWT.hwndLB = hwndLB;
        WWT.hwndParent = hwndParent;
        WWT.nLevel = -1;
        WalkWindowTreeRecurse(&WWT);
    }
    
    //////////////////////////////////////////////////////////////////////////
    BOOL Dlg_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam){
    
        chSETDLGICONS(hwnd, IDI_LISLAB);
    
        //关联一个向上箭头的鼠标到对话框的客户区
        SetClassLongPtr(hwnd, GCLP_HCURSOR, (LONG_PTR)LoadCursor(NULL,IDC_UPARROW));
    
        g_uTimerId = SetTimer(hwnd, g_uTimerId, TIMER_DELAY, NULL);
    
        HWND hWndT = GetDlgItem(hwnd, IDC_WNDFUNC);
        ComboBox_AddString(hWndT, TEXT("SetFocus"));
        ComboBox_AddString(hWndT, TEXT("SetActiveWindow"));
        ComboBox_AddString(hWndT, TEXT("SetForegroundWnd"));
        ComboBox_AddString(hWndT, TEXT("BringWindowToTop"));
        ComboBox_AddString(hWndT, TEXT("SetWindowPos-TOP"));
        ComboBox_AddString(hWndT, TEXT("SetWindowPos-BTM"));
        ComboBox_SetCurSel(hWndT, 0);
        
        //为列表填充所有的窗口信息(本窗口及记事本下的所有窗口)
        hWndT = GetDlgItem(hwnd, IDC_WNDS);
        ListBox_AddString(hWndT, TEXT("--->本对话框<---"));
        ListBox_SetItemData(hWndT, 0, hwnd);
        ListBox_SetCurSel(hWndT, 0);
    
        //添加记事本下的窗口
        g_hwndNotepad = FindWindow(TEXT("NotePad"), NULL);
        if (g_hwndNotepad == NULL){
            //记事本程序还没启动;启动它
            STARTUPINFO si = { sizeof(si)};
            PROCESS_INFORMATION  pi;
            TCHAR szCommandLine[] = TEXT("Notepad");
            if (CreateProcess(NULL,szCommandLine,NULL,NULL,FALSE,0,
                NULL,NULL,&si,&pi)){
    
                //等待记事本创建它所有的窗口
                WaitForInputIdle(pi.hProcess, INFINITE);
                CloseHandle(pi.hProcess);
                CloseHandle(pi.hThread);
                g_hwndNotepad = FindWindow(TEXT("Notepad"), NULL);
            }
        }
    
        WalkWindowTree(hWndT, g_hwndNotepad);
    
        return (TRUE);
    }
    
    //////////////////////////////////////////////////////////////////////////
    void Dlg_OnCommand(HWND hwnd, int id, HWND hwndCtrl, UINT codeNotity){
        HWND hWndT;
        switch (id)
        {
        case IDCANCEL:
            if (g_uTimerId != 0)
                KillTimer(hwnd, g_uTimerId);
            EndDialog(hwnd, id);
            break;
    
        case IDC_HIDECURSOR:
            ShowCursor(FALSE);
            break;
    
        case IDC_SHOWCURSOR:
            ShowCursor(TRUE);
            break;
    
        case IDC_INFINITELOOP:
            SetCursor(LoadCursor(NULL, IDC_NO));
            for (;;);
            break;
    
        case IDC_FUNCSTART: //“延时”按钮
            g_dwEventTime = GetTickCount() + 1000 * GetDlgItemInt(hwnd, IDC_DELAY, NULL, FALSE); //IDC_DELAY指定了秒数
            hWndT = GetDlgItem(hwnd, IDC_WNDS);//在“窗口”列表框
            g_hwndSubject = (HWND)//选中的、要操作的窗口
                ListBox_GetItemData(hWndT, ListBox_GetCurSel(hWndT));
            g_nEventId = ComboBox_GetCurSel(GetDlgItem(hwnd, IDC_WNDFUNC));
            SetWindowText(GetDlgItem(hwnd, IDC_EVENTPENDING), TEXT("Pending"));        
            break;
    
        case IDC_REMOVECLIPRECT:
            ClipCursor(NULL);
            break;
    
        case IDC_SETCLIPRECT:
            RECT rc;
            SetRect(&rc, 0, 0, GetSystemMetrics(SM_CXSCREEN) / 2,
                    GetSystemMetrics(SM_CYSCREEN) / 2);
            ClipCursor(&rc);
            break;
    
        case IDC_THREADATTACH:
            AttachThreadInput(GetWindowThreadProcessId(g_hwndNotepad, NULL), 
                GetCurrentThreadId(),TRUE);
            break;
    
        case IDC_THREADDETACH:
            AttachThreadInput(GetWindowThreadProcessId(g_hwndNotepad, NULL),
                              GetCurrentThreadId(), FALSE);
            break;
        }
    }
    
    //////////////////////////////////////////////////////////////////////////
    void AddStr(HWND hwndLB, PCTSTR szBuf){
        int nIndex;
        do{
            nIndex = ListBox_AddString(hwndLB, szBuf);
            if (nIndex == LB_ERR)
                ListBox_DeleteString(hwndLB, 0);
    
        } while (nIndex == LB_ERR);
    
        ListBox_SetCurSel(hwndLB, nIndex);
    }
    
    //////////////////////////////////////////////////////////////////////////
    
    void Dlg_OnMouseMove(HWND hwnd, int x, int y, UINT KeyFlags){
        TCHAR szBuf[100];
        wsprintf(szBuf, TEXT("捕获=%s,消息=MouseMove, x=%5d,y=%5d"),
                 (GetCapture() == NULL) ? TEXT("") : TEXT(""), x, y);
        AddStr(GetDlgItem(hwnd, IDC_MOUSEMSGS), szBuf);
    }
    
    //////////////////////////////////////////////////////////////////////////
    int Dlg_OnLButtonDown(HWND hwnd, BOOL fDoubleClick,int x,int y,UINT KeyFlags){
        TCHAR szBuf[100];
        wsprintf(szBuf, TEXT("捕获=%s,消息=LButtonDown, DblClk=%s,x=%5d,y=%5d"),
                 (GetCapture() == NULL) ? TEXT("") : TEXT(""),
                 fDoubleClick ? TEXT(""):TEXT(""), x, y);
        AddStr(GetDlgItem(hwnd, IDC_MOUSEMSGS), szBuf);
    
        return (0);
    }
    
    //////////////////////////////////////////////////////////////////////////
    void Dlg_OnLButtonUp(HWND hwnd, int x, int y, UINT KeyFlags){
        TCHAR szBuf[100];
        wsprintf(szBuf, TEXT("捕获=%s,消息=LButtonUp,x=%5d,y=%5d"),
                 (GetCapture() == NULL) ? TEXT("") : TEXT(""), x, y);
        AddStr(GetDlgItem(hwnd, IDC_MOUSEMSGS), szBuf);
    }
    
    //////////////////////////////////////////////////////////////////////////
    int Dlg_OnRButtonDown(HWND hwnd, BOOL fDoubleClick, int x, int y, UINT KeyFlags){
        TCHAR szBuf[100];
        wsprintf(szBuf, TEXT("捕获=%s,消息=RButtonDown, DblClk=%s,x=%5d,y=%5d"),
                 (GetCapture() == NULL) ? TEXT("") : TEXT(""),
                 fDoubleClick ? TEXT("") : TEXT(""), x, y);
        AddStr(GetDlgItem(hwnd, IDC_MOUSEMSGS), szBuf);
    
        if (!fDoubleClick)
            SetCapture(hwnd);
        else ReleaseCapture();
    
        return (0);
    }
    
    //////////////////////////////////////////////////////////////////////////
    void Dlg_OnRButtonUp(HWND hwnd, int x, int y, UINT KeyFlags){
        TCHAR szBuf[100];
        wsprintf(szBuf, TEXT("捕获=%s,消息=RButtonUp,x=%5d,y=%5d"),
                 (GetCapture() == NULL) ? TEXT("") : TEXT(""), x, y);
        AddStr(GetDlgItem(hwnd, IDC_MOUSEMSGS), szBuf);
    }
    
    //////////////////////////////////////////////////////////////////////////
    void Dlg_OnTimer(HWND hwnd, UINT id){ //每秒刷新2次
        TCHAR szBuf[100];
        CalcWndText(GetFocus(), szBuf, chDIMOF(szBuf));
        SetWindowText(GetDlgItem(hwnd, IDC_WNDFOCUS), szBuf);
    
        CalcWndText(GetCapture(), szBuf, chDIMOF(szBuf));
        SetWindowText(GetDlgItem(hwnd, IDC_WNDCAPTURE), szBuf);
    
        CalcWndText(GetActiveWindow(), szBuf, chDIMOF(szBuf));
        SetWindowText(GetDlgItem(hwnd, IDC_WNDACTIVE), szBuf);
    
        CalcWndText(GetForegroundWindow(), szBuf, chDIMOF(szBuf));
        SetWindowText(GetDlgItem(hwnd, IDC_WNDFOREGROUND), szBuf);
    
        RECT rc;
        GetClipCursor(&rc);
        wsprintf(szBuf, TEXT("左=%d,顶=%d,右=%d,底=%d"), 
                 rc.left,rc.top,rc.right,rc.bottom);
        SetWindowText(GetDlgItem(hwnd, IDC_CLIPCURSOR), szBuf);
    
        //延时的时间未到或未输入延时数(秒),则直接返回
        if ((g_dwEventTime == 0) || (GetTickCount() < g_dwEventTime))
            return;
    
        HWND hWndT;
        switch (g_nEventId)
        {
        case 0: //SetFocus
            g_hwndSubject = SetFocus(g_hwndSubject);
            break;
    
        case 1: //SetActiveWindow
            g_hwndSubject = SetActiveWindow(g_hwndSubject);
            break;
    
        case 2: //SetForegroundWindow
            hWndT = GetForegroundWindow();
            SetForegroundWindow(g_hwndSubject);
            g_hwndSubject = hWndT;
            break;
    
        case 3: //BringWindowToTopWindow
            BringWindowToTop(g_hwndSubject);
            break;
    
        case 4: //SetWindowPos w/HWND_TOP
            SetWindowPos(g_hwndSubject, HWND_TOP, 0, 0, 0, 0, 
                         SWP_NOMOVE | SWP_NOSIZE);
            g_hwndSubject = (HWND)1;
            break;
    
        case 5: //SetWindowPos w/HWND_BOTTOM
            SetWindowPos(g_hwndSubject, HWND_BOTTOM, 0, 0, 0, 0,
                         SWP_NOMOVE | SWP_NOSIZE);
            g_hwndSubject = (HWND)1;
            break;
        }
    
        if (g_hwndSubject == (HWND)1){
            SetWindowText(GetDlgItem(hwnd, IDC_PREVWND), TEXT("不能判别."));
        } else{
            CalcWndText(g_hwndSubject, szBuf, chDIMOF(szBuf));
            SetWindowText(GetDlgItem(hwnd, IDC_PREVWND), szBuf);
        }
        g_hwndSubject = NULL; g_nEventId = 0; g_dwEventTime = 0;
        SetWindowText(GetDlgItem(hwnd, IDC_EVENTPENDING), TEXT("执行"));
    }
    
    //////////////////////////////////////////////////////////////////////////
    INT_PTR WINAPI Dlg_Proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam){
        switch (uMsg)
        {
            chHANDLE_DLGMSG(hWnd, WM_INITDIALOG,   Dlg_OnInitDialog);
            chHANDLE_DLGMSG(hWnd, WM_COMMAND,       Dlg_OnCommand);
    
            chHANDLE_DLGMSG(hWnd, WM_MOUSEMOVE,    Dlg_OnMouseMove);
    
            chHANDLE_DLGMSG(hWnd, WM_LBUTTONDOWN,   Dlg_OnLButtonDown);
            chHANDLE_DLGMSG(hWnd, WM_LBUTTONDBLCLK, Dlg_OnLButtonDown);
            chHANDLE_DLGMSG(hWnd, WM_LBUTTONUP,     Dlg_OnLButtonUp);
    
            chHANDLE_DLGMSG(hWnd, WM_RBUTTONDOWN,   Dlg_OnRButtonDown);
            chHANDLE_DLGMSG(hWnd, WM_RBUTTONDBLCLK, Dlg_OnRButtonDown);
            chHANDLE_DLGMSG(hWnd, WM_RBUTTONUP,     Dlg_OnRButtonUp);
    
            chHANDLE_DLGMSG(hWnd, WM_TIMER,         Dlg_OnTimer);
        }
        
        return (FALSE);
    }
    
    //////////////////////////////////////////////////////////////////////////
    int WINAPI _tWinMain(HINSTANCE hInstExe, HINSTANCE, PTSTR pszCmdLine, int){
        DialogBox(hInstExe, MAKEINTRESOURCE(IDD_LISLAB), NULL, Dlg_Proc);
        return 0;
    }
    /////////////////////////////文件结束/////////////////////////////////////

    //resouce.h

    //{{NO_DEPENDENCIES}}
    // Microsoft Visual C++ 生成的包含文件。
    // 供 28_LISLab.rc 使用
    //
    #define IDD_LISLAB                      1
    #define IDC_WNDFOCUS                    100
    #define IDC_WNDACTIVE                   101
    #define IDC_WNDFOREGROUND               102
    #define IDI_LISLAB                      102
    #define IDC_WNDCAPTURE                  103
    #define IDC_CLIPCURSOR                  104
    #define IDC_WNDFUNC                     105
    #define IDC_FUNCSTART                   106
    #define IDC_DELAY                       107
    #define IDC_WNDS                        110
    #define IDC_THREADATTACH                111
    #define IDC_THREADDETACH                112
    #define IDC_MOUSEMSGS                   113
    #define IDC_SETCLIPRECT                 114
    #define IDC_REMOVECLIPRECT              115
    #define IDC_HIDECURSOR                  116
    #define IDC_SHOWCURSOR                  117
    #define IDC_EVENTPENDING                118
    #define IDC_PREVWND                     119
    #define IDC_INFINITELOOP                120
    
    // Next default values for new objects
    // 
    #ifdef APSTUDIO_INVOKED
    #ifndef APSTUDIO_READONLY_SYMBOLS
    #define _APS_NEXT_RESOURCE_VALUE        104
    #define _APS_NEXT_COMMAND_VALUE         40001
    #define _APS_NEXT_CONTROL_VALUE         1001
    #define _APS_NEXT_SYMED_VALUE           101
    #endif
    #endif
    View Code

    //28_LISLab.rc

    // Microsoft Visual C++ generated resource script.
    //
    #include "resource.h"
    
    #define APSTUDIO_READONLY_SYMBOLS
    /////////////////////////////////////////////////////////////////////////////
    //
    // Generated from the TEXTINCLUDE 2 resource.
    //
    #include "winres.h"
    
    /////////////////////////////////////////////////////////////////////////////
    #undef APSTUDIO_READONLY_SYMBOLS
    
    /////////////////////////////////////////////////////////////////////////////
    // 中文(简体,中国) resources
    
    #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_CHS)
    LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED
    
    #ifdef APSTUDIO_INVOKED
    /////////////////////////////////////////////////////////////////////////////
    //
    // TEXTINCLUDE
    //
    
    1 TEXTINCLUDE 
    BEGIN
        "resource.h"
    END
    
    2 TEXTINCLUDE 
    BEGIN
        "#include ""winres.h""
    "
        ""
    END
    
    3 TEXTINCLUDE 
    BEGIN
        "
    "
        ""
    END
    
    #endif    // APSTUDIO_INVOKED
    
    
    /////////////////////////////////////////////////////////////////////////////
    //
    // Dialog
    //
    
    IDD_LISLAB DIALOGEX 12, 38, 286, 178
    STYLE DS_SETFONT | DS_CENTER | WS_MINIMIZEBOX | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
    CAPTION "局部输入状态模拟实验"
    FONT 10, "宋体", 400, 0, 0x86
    BEGIN
        GROUPBOX        "各类窗口信息",IDC_STATIC,4,0,192,56
        LTEXT           "焦点:",IDC_STATIC,8,12,24,8
        LTEXT           "焦点窗口信息",IDC_WNDFOCUS,52,12,140,8
        LTEXT           "激活:",IDC_STATIC,8,20,25,8
        LTEXT           "激活窗口信息",IDC_WNDACTIVE,52,20,140,8
        LTEXT           "前台:",IDC_STATIC,8,28,40,8
        LTEXT           "前台窗口信息",IDC_WNDFOREGROUND,52,28,140,8
        LTEXT           "鼠标捕获:",IDC_STATIC,8,36,40,8
        LTEXT           "捕获窗口信息",IDC_WNDCAPTURE,52,36,140,8
        LTEXT           "光标剪裁:",IDC_STATIC,8,44,41,8
        LTEXT           "光标被限制指定的矩形区内",IDC_CLIPCURSOR,52,44,140,8
        LTEXT           "函数:",IDC_STATIC,200,4,32,8
        COMBOBOX        IDC_WNDFUNC,200,14,82,54,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP
        PUSHBUTTON      "延时:",IDC_FUNCSTART,201,30,25,12
        EDITTEXT        IDC_DELAY,228,30,21,12,ES_AUTOHSCROLL
        LTEXT           "执行",IDC_EVENTPENDING,253,32,29,10
        LTEXT           "先前窗口:",IDC_STATIC,200,46,34,8
        LTEXT           "先前窗口信息",IDC_PREVWND,208,54,76,18
        LTEXT           "记事本里的所有窗口及本窗口",IDC_STATIC,4,58,120,8
        LISTBOX         IDC_WNDS,3,69,192,50,WS_VSCROLL | WS_TABSTOP
        PUSHBUTTON      "&Attach to Notepad",IDC_THREADATTACH,200,72,80,12
        PUSHBUTTON      "&Detach from Notepad",IDC_THREADDETACH,200,88,80,12
        LTEXT           "接收到的鼠标消息:",IDC_STATIC,4,102,89,8
        LISTBOX         IDC_MOUSEMSGS,4,112,192,42,WS_VSCROLL | WS_TABSTOP
        LTEXT           "单击鼠标右键以便开始捕获.
    
    双击鼠标右键释放鼠标捕获.",IDC_STATIC,200,110,80,40
        LTEXT           "光标剪裁区域:",IDC_STATIC,4,148,60,8
        PUSHBUTTON      "顶,左",IDC_SETCLIPRECT,56,146,56,12
        PUSHBUTTON      "删除",IDC_REMOVECLIPRECT,116,146,56,12
        LTEXT           "鼠标可见性:",IDC_STATIC,4,164,48,8
        PUSHBUTTON      "隐藏",IDC_HIDECURSOR,56,162,56,12
        PUSHBUTTON      "显示",IDC_SHOWCURSOR,116,162,56,12
        PUSHBUTTON      "无限循环",IDC_INFINITELOOP,200,162,80,12,WS_GROUP | NOT WS_TABSTOP
    END
    
    
    /////////////////////////////////////////////////////////////////////////////
    //
    // DESIGNINFO
    //
    
    #ifdef APSTUDIO_INVOKED
    GUIDELINES DESIGNINFO
    BEGIN
        IDD_LISLAB, DIALOG
        BEGIN
        END
    END
    #endif    // APSTUDIO_INVOKED
    
    
    /////////////////////////////////////////////////////////////////////////////
    //
    // Icon
    //
    
    // Icon with lowest ID value placed first to ensure application icon
    // remains consistent on all systems.
    IDI_LISLAB              ICON                    "LISLab.ico"
    #endif    // 中文(简体,中国) resources
    /////////////////////////////////////////////////////////////////////////////
    
    
    
    #ifndef APSTUDIO_INVOKED
    /////////////////////////////////////////////////////////////////////////////
    //
    // Generated from the TEXTINCLUDE 3 resource.
    //
    
    
    /////////////////////////////////////////////////////////////////////////////
    #endif    // not APSTUDIO_INVOKED
    View Code

    【28_LISWatch程序】用来监视“焦点窗口”、“活动窗口”和“鼠标捕获窗口”信息

      ①主线程会被Attach到前景线程或指定的线程。如果Attact到前景线程则监视所有线程,否则监视指定的线程。

      ②当监视指定线程时,如“计算器”程序,则“线程”标签上会为显示计算器的主线程ID。如果此时去激活另一个程序,如“记事本”,则将无法获取记事本的“焦点窗口”等信息,因为现在仍被Attach到“计算器”的线程时,所以无法获得非“计算器”线程的局部输入状态变量的信息。

            

    //LISWatch.cpp

    /************************************************************************
    Module: LISWatch.cpp
    Notices:Copyright(c) 2008 Jeffrey Richter
    ************************************************************************/
    
    #include "../../CommonFiles/CmnHdr.h"
    #include "resource.h"
    #include <tchar.h>
    
    
    //////////////////////////////////////////////////////////////////////////
    #define TIMER_DELAY (500)  //半秒
    UINT_PTR  g_uTimerId = 1;
    DWORD  g_dwThreadIdAttchTo = 0; //0:System-wide;非0:指定的线程
    
    
    //////////////////////////////////////////////////////////////////////////
    static void CalcWndText(HWND hwnd, PTSTR szBuf, int nLen){
        if (hwnd == (HWND)NULL) {
            lstrcpy(szBuf, TEXT("(无窗口)"));
            return;
        }
    
        if (!IsWindow(hwnd)){
            lstrcpy(szBuf, TEXT("(无效窗口)"));
            return;
        }
    
        TCHAR szClass[50], szCaption[50], szBufT[150];
        GetClassName(hwnd, szClass, chDIMOF(szClass));
        GetWindowText(hwnd, szCaption, chDIMOF(szCaption));
        wsprintf(szBufT, TEXT("[%s] %s"), (PTSTR)szClass, 
                 (szCaption[0]==0)? (PTSTR)TEXT("(无标题)"):
                 (PTSTR)szCaption);
        _tcsncpy_s(szBuf, nLen, szBufT, nLen - 1);
        szBuf[nLen - 1] = 0; //强制0字符串结束
    }
    
    //////////////////////////////////////////////////////////////////////////
    BOOL Dlg_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam){
    
        chSETDLGICONS(hwnd, IDI_LISWATCH);
    
        //周期性的更新内容
        g_uTimerId = SetTimer(hwnd, g_uTimerId, TIMER_DELAY, NULL);
    
        //使我们的窗口置顶
        SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
    
        return (TRUE);
    }
    
    //////////////////////////////////////////////////////////////////////////
    void Dlg_OnCommand(HWND hwnd, int id, HWND hwndCtrl, UINT codeNotity){
        switch (id)
        {
        case IDCANCEL:
            KillTimer(hwnd, g_uTimerId);
            EndDialog(hwnd, id);
            break;
        }
    }
    
    
    
    //////////////////////////////////////////////////////////////////////////
    void Dlg_OnTimer(HWND hwnd, UINT id){
        TCHAR szBuf[100] = TEXT("全系统范围"); //System-wide
        HWND  hwndForeground = GetForegroundWindow(); //获取前景窗口
        DWORD  dwThreadIdAttachTo = g_dwThreadIdAttchTo;
    
        if (dwThreadIdAttachTo == 0){ //全系统范围
            //如果监视全系统范围的局部输入状态,则将我们的线程
            //Attatch到前景线程
            dwThreadIdAttachTo = GetWindowThreadProcessId(hwndForeground, NULL);
            AttachThreadInput(GetCurrentThreadId(), dwThreadIdAttachTo, TRUE);
        } else {
            wsprintf(szBuf, TEXT("0x%08x"), dwThreadIdAttachTo);
        }
    
        SetWindowText(GetDlgItem(hwnd, IDC_THREADID), szBuf);
    
        CalcWndText(GetFocus(), szBuf, chDIMOF(szBuf));
        SetWindowText(GetDlgItem(hwnd, IDC_WNDFOCUS), szBuf);
    
        CalcWndText(GetActiveWindow(), szBuf, chDIMOF(szBuf));
        SetWindowText(GetDlgItem(hwnd, IDC_WNDACTIVE), szBuf);
    
        CalcWndText(GetCapture(), szBuf, chDIMOF(szBuf));
        SetWindowText(GetDlgItem(hwnd, IDC_WNDCAPTURE), szBuf);
    
        CalcWndText(GetForegroundWindow(), szBuf, chDIMOF(szBuf));
        SetWindowText(GetDlgItem(hwnd, IDC_WNDFOREGRND), szBuf);
    
        if (g_dwThreadIdAttchTo == 0){
            //如果监视全系统范围的局部输入状态,则将本线程与前景线程分离
            AttachThreadInput(GetCurrentThreadId(), dwThreadIdAttachTo, FALSE);
        }
    }
    
    //////////////////////////////////////////////////////////////////////////
    void Dlg_OnRButtonDown(HWND hwnd, BOOL fDoubleClick, int x, int y, UINT KeyFlags){
        chMB("要监视指定线程,可在主窗口中单击鼠标左键,并在指定的窗口中释放左键.
    "
             "要监视所有线程,可在主窗口中双击鼠标左键.");
    }
    
    //////////////////////////////////////////////////////////////////////////
    void Dlg_OnLButtonDown(HWND hwnd, BOOL fDoubleClick, int x, int y, UINT KeyFlags){
        //如果我们己Attatch到某个线程,则分离它
        if (g_dwThreadIdAttchTo !=0){
            AttachThreadInput(GetCurrentThreadId(), g_dwThreadIdAttchTo, FALSE);
        }
    
        //设置鼠标捕获,并改变鼠标形状
        SetCapture(hwnd);
        //HCURSOR hCur = LoadCursor(GetModuleHandle(NULL), MAKEINTRESOURCE(IDC_EYES));
        SetCursor(LoadCursor(GetModuleHandle(NULL), MAKEINTRESOURCE(IDC_EYES)));
    }
    
    //////////////////////////////////////////////////////////////////////////
    void Dlg_OnLButtonUp(HWND hwnd, int x, int y, UINT KeyFlags){
        if (GetCapture() == hwnd){
            //如果我们设置了鼠标捕获,则获取鼠标下面的窗口的线程Id
            POINT pt;
            pt.x = LOWORD(GetMessagePos());
            pt.y = HIWORD(GetMessagePos());
            ReleaseCapture();
    
            g_dwThreadIdAttchTo = GetWindowThreadProcessId(
                ChildWindowFromPointEx(GetDesktopWindow(),pt,CWP_SKIPINVISIBLE),NULL);
    
            if (g_dwThreadIdAttchTo == GetCurrentThreadId()){
                //如果鼠标是在我们的窗口上释放的,则仍监视所有的线程
                g_dwThreadIdAttchTo = 0;
            } else{
                //鼠标是在其他线程创建的窗口上释放,则监视该窗口所属的线程
                AttachThreadInput(GetCurrentThreadId(), g_dwThreadIdAttchTo, TRUE);
            }
        }
    }
    
    //////////////////////////////////////////////////////////////////////////
    INT_PTR  WINAPI  Dlg_Proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam){
        
        switch (uMsg)
        {
            chHANDLE_DLGMSG(hWnd, WM_INITDIALOG,    Dlg_OnInitDialog);
            chHANDLE_DLGMSG(hWnd, WM_COMMAND,        Dlg_OnCommand);
            chHANDLE_DLGMSG(hWnd, WM_TIMER,            Dlg_OnTimer);
            chHANDLE_DLGMSG(hWnd, WM_RBUTTONDOWN,    Dlg_OnRButtonDown);
            chHANDLE_DLGMSG(hWnd, WM_LBUTTONDOWN,   Dlg_OnLButtonDown);
            chHANDLE_DLGMSG(hWnd, WM_LBUTTONUP,     Dlg_OnLButtonUp);
        }
        
        return FALSE;
    }
    
    //////////////////////////////////////////////////////////////////////////
    int WINAPI _tWinMain(HINSTANCE hInstExe, HINSTANCE, PTSTR pszCmdLine, int){
        DialogBox(hInstExe, MAKEINTRESOURCE(IDD_LISWATCH), NULL, Dlg_Proc);
        return (0);
    }

    //resource.h

    //{{NO_DEPENDENCIES}}
    // Microsoft Visual C++ 生成的包含文件。
    // 供 28_LISWatch.rc 使用
    //
    #define IDD_LISWATCH                    101
    #define IDC_EYES                        101
    #define IDI_LISWATCH                    102
    #define IDC_WNDFOCUS                    1000
    #define IDC_WNDACTIVE                   1001
    #define IDC_WNDFOREGRND                 1002
    #define IDC_THREADID                    1003
    #define IDC_WNDCAPTURE                  1004
    
    // Next default values for new objects
    // 
    #ifdef APSTUDIO_INVOKED
    #ifndef APSTUDIO_READONLY_SYMBOLS
    #define _APS_NEXT_RESOURCE_VALUE        104
    #define _APS_NEXT_COMMAND_VALUE         40001
    #define _APS_NEXT_CONTROL_VALUE         1001
    #define _APS_NEXT_SYMED_VALUE           101
    #endif
    #endif
    View Code

    //28_LISWatch.rc

    // Microsoft Visual C++ generated resource script.
    //
    #include "resource.h"
    
    #define APSTUDIO_READONLY_SYMBOLS
    /////////////////////////////////////////////////////////////////////////////
    //
    // Generated from the TEXTINCLUDE 2 resource.
    //
    #include "winres.h"
    
    /////////////////////////////////////////////////////////////////////////////
    #undef APSTUDIO_READONLY_SYMBOLS
    
    /////////////////////////////////////////////////////////////////////////////
    // 中文(简体,中国) resources
    
    #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_CHS)
    LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED
    
    #ifdef APSTUDIO_INVOKED
    /////////////////////////////////////////////////////////////////////////////
    //
    // TEXTINCLUDE
    //
    
    1 TEXTINCLUDE 
    BEGIN
        "resource.h"
    END
    
    2 TEXTINCLUDE 
    BEGIN
        "#include ""winres.h""
    "
        ""
    END
    
    3 TEXTINCLUDE 
    BEGIN
        "
    "
        ""
    END
    
    #endif    // APSTUDIO_INVOKED
    
    
    /////////////////////////////////////////////////////////////////////////////
    //
    // Dialog
    //
    
    IDD_LISWATCH DIALOGEX 0, 0, 210, 58
    STYLE DS_SETFONT | DS_CENTER | WS_MINIMIZEBOX | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
    CAPTION "LISWatch"
    FONT 10, "宋体", 400, 0, 0x0
    BEGIN
        LTEXT           "线程:",IDC_STATIC,4,3,31,8
        LTEXT           "线程Id",IDC_THREADID,37,3,91,8
        LTEXT           "焦点:",IDC_STATIC,4,14,31,8
        LTEXT           "焦点窗口",IDC_WNDFOCUS,37,14,88,8
        LTEXT           "激活:",IDC_STATIC,4,25,31,8
        LTEXT           "活动窗口",IDC_WNDACTIVE,37,25,135,8
        LTEXT           "捕获:",IDC_STATIC,4,36,31,8
        LTEXT           "捕获窗口",IDC_WNDCAPTURE,37,36,138,8
        LTEXT           "前景窗口:",IDC_STATIC,4,47,44,8
        LTEXT           "前景窗口",IDC_WNDFOREGRND,64,47,115,8
        LTEXT           "帮助:单击鼠标右键",IDC_STATIC,136,1,69,8
    END
    
    
    /////////////////////////////////////////////////////////////////////////////
    //
    // DESIGNINFO
    //
    
    #ifdef APSTUDIO_INVOKED
    GUIDELINES DESIGNINFO
    BEGIN
        IDD_LISWATCH, DIALOG
        BEGIN
            RIGHTMARGIN, 209
            BOTTOMMARGIN, 41
        END
    END
    #endif    // APSTUDIO_INVOKED
    
    
    /////////////////////////////////////////////////////////////////////////////
    //
    // Icon
    //
    
    // Icon with lowest ID value placed first to ensure application icon
    // remains consistent on all systems.
    IDI_LISWATCH            ICON                    "LISWatch.ico"
    
    /////////////////////////////////////////////////////////////////////////////
    //
    // Cursor
    //
    
    IDC_EYES             CURSOR                  "Eyes.cur"
    #endif    // 中文(简体,中国) resources
    /////////////////////////////////////////////////////////////////////////////
    
    
    
    #ifndef APSTUDIO_INVOKED
    /////////////////////////////////////////////////////////////////////////////
    //
    // Generated from the TEXTINCLUDE 3 resource.
    //
    
    
    /////////////////////////////////////////////////////////////////////////////
    #endif    // not APSTUDIO_INVOKED
    View Code
  • 相关阅读:
    Rigidbody和Collider
    Unity官方实例教程 Roll-a-Ball
    unity还原three之旋转
    unity还原three——顶点,三角面,uv
    unity还原three导出的json——基本模型,位移,旋转,缩放
    【struts2基础】配置详解
    【深入Struts2】获取ServletAPI的三种方式
    JDBC事务与事务隔离级别详解
    【GOF23设计模式】--工厂模式
    【GOF23设计模式】--单例模式
  • 原文地址:https://www.cnblogs.com/5iedu/p/5296007.html
Copyright © 2020-2023  润新知