最近朋友让我写一个屏幕取词的小工具,考虑了一下主要步骤大致如下:
1.下全局鼠标钩子,Hook鼠标WM_MOUSEMOVE消息
2. WM_MOUSEMOVE消息函数中设定时器,判断鼠标停留时间
3.触发定时器事件,注DLL到目标进程(鼠标停留进程),可根据鼠标位置判断目标进程(GetWindowThreadProcessId),DLL中Hook目标进程的某些函数,如ExtTextOutW等;
4.使目标进程控件强制刷新,以此触发字符串显示函数
5.将Hook到的字符串通过(管道、共享内存、消息)等方式传回屏幕取词进程
6.卸载目标进程注入的DLL(取消函数钩子)
理论上并不复杂,但在实现时发现不同窗体的绘制函数都不一样,Hook和强制刷新存在一定难度,效率也比较低,甚至可能都不如暴力搜索内存效率高,就退而求其次先写了一个屏幕划词的小东西。
屏幕划词比屏幕取词难度就小很多,设置某个热键,触发热键按下事件时通过剪贴板过度选中的词,然后显示出来,我实现的主要步骤如下:
1.通过SetWindowsHookEx函数下全局低级键盘钩子(WH_KEYBOARD_LL),判断键盘按下(WM_KEYUP)及按下的键值(VK_F2)
注:WH_KEYBOARD_LL钩子可以在EXE中直接实现,无需通过DLL
钩子的回调函数大体如下:
1 2 3 4 5 6 7 8 9 |
LRESULT CALLBACK HookProc(int nCode, WPARAM wParam, LPARAM lParam) { KBDLLHOOKSTRUCT *ks = (KBDLLHOOKSTRUCT*)lParam; if (WM_KEYUP == wParam && ks->vkCode == VK_F2) { PostMessage(hConsoleWindows, WM_READTEXT, 0, 0); } return CallNextHookEx(0, nCode, wParam, lParam); } |
这时候有人可能会问了,为啥CallNextHookEx第一个参数可以为NULL 【其实我也是某天偶然在MSDN上查了一下这函数,写了好几年才知道第一个参数可以为空……..
2.触发事件,先保存原剪贴板中内容
注:需同时保存内容和格式,剪贴板中可能为图片等其他格式
读取剪贴板代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
BOOL CKeyBoardGetTextDlg::ReadFromClipboard(CString & CSClipBoradInfo, UINT & uiFormat) { POINT pt; GetCursorPos(&pt); CWnd* pWnd = WindowFromPoint(pt); HWND hwnd = pWnd->GetSafeHwnd(); if (!::OpenClipboard(hwnd)) { AfxMessageBox(_T("Cannot open the Clipboard!")); return FALSE; } // 选择合适的格式(读取的时候不用获得剪贴板的拥有权) uiFormat = EnumClipboardFormats(0); if (uiFormat == NULL) { MessageBox(_T("EnumClipboardFormats Error!")); return FALSE; } HANDLE hData = ::GetClipboardData(uiFormat); if (NULL == hData) { AfxMessageBox(_T("Unable to get Clipboard data!")); CloseClipboard(); return FALSE; } CStringW str; LPCTSTR lpdata = (LPCTSTR)GlobalLock(hData); CSClipBoradInfo = lpdata; GlobalUnlock(hData); CloseClipboard(); return TRUE; } |
3.模拟键盘按键Ctrl+C
注:模拟按下和模拟松开中间要加入一段时间sleep,不然会认为时间太短没有按下
1 2 3 4 5 |
keybd_event(VK_CONTROL, (BYTE)0, 0, 0); keybd_event('C', (BYTE)0, 0, 0); Sleep(100); keybd_event('C', (BYTE)0, KEYEVENTF_KEYUP, 0); keybd_event(VK_CONTROL, (BYTE)0, KEYEVENTF_KEYUP, 0); |
4.从剪贴板读出划词对应的文本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
BOOL CKeyBoardGetTextDlg::ReadFromClipboard(CString & CSClipBoradInfo) { POINT pt; GetCursorPos(&pt); CWnd* pWnd = WindowFromPoint(pt); HWND hwnd = pWnd->GetSafeHwnd(); if (!::OpenClipboard(hwnd)) { AfxMessageBox(_T("Cannot open the Clipboard!")); return FALSE; } UINT uiFormat = (sizeof(TCHAR) == sizeof(WCHAR) ? CF_UNICODETEXT : CF_TEXT); HANDLE hData = ::GetClipboardData(uiFormat); if (NULL == hData) { AfxMessageBox(_T("Unable to get Clipboard data!")); CloseClipboard(); return FALSE; } CStringW str; LPCTSTR lpdata = (LPCTSTR)GlobalLock(hData); CSClipBoradInfo = lpdata; GlobalUnlock(hData); CloseClipboard(); return TRUE; } |
我在这里并没有判断格式,直接认为是文本来做的。读出文本之后就可以按照自己的想法对文本进行操作,我就不多说了。
5.将原剪贴板数据回写到剪贴板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
BOOL CKeyBoardGetTextDlg::SetClipboard(const CString & CSClipBoardInfo,const UINT & uClipboardFormat) { // TODO: 在此添加控件通知处理程序代码 if (uClipboardFormat == NULL) { MessageBox(_T("ClipboardFormat Error!")); return FALSE; } if (!OpenClipboard()) { AfxMessageBox(_T("Cannot open the Clipboard!")); return FALSE; } // 将剪贴板内容清空,释放数据资源,然后指定当前打开剪贴板的窗口为剪贴板的所有制 EmptyClipboard(); // 取回当前控件的数据,hData开辟全局内存区域,存放数据 CString CSTempClipBoardInfo = CSClipBoardInfo; size_t cbStr = (CSTempClipBoardInfo.GetLength() + 1) * sizeof(TCHAR); HGLOBAL hData = GlobalAlloc(GMEM_MOVEABLE, cbStr); memcpy_s(GlobalLock(hData), cbStr, CSTempClipBoardInfo.LockBuffer(), cbStr); GlobalUnlock(hData); CSTempClipBoardInfo.UnlockBuffer(); if (::SetClipboardData(uClipboardFormat, hData) == NULL) { AfxMessageBox(_T("Unable to set Clipboard data")); CloseClipboard(); return FALSE; } CloseClipboard(); return TRUE; } |
至此为止,屏幕划词的功能已经实现,但在测试中发现,仍然需要开UAC并提权,否则在读某些进程的文本时会出现问题,提权的代码太常规这里就不贴了。