最近朋友让我写一个屏幕取词的小工具,考虑了一下主要步骤大致如下:
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并提权,否则在读某些进程的文本时会出现问题,提权的代码太常规这里就不贴了。