需求:截获桌面窗口鼠标单击事件,解析所选中的桌面
Item,并将解析后的 item 信息发送给主调程序,并将信息显示在一个窗口上面。如下图:
思路:
1. 确定HOOK的类型。很明显,这一个进程外的HOOK,我们的应用程序DesktopCaptor2.exe
需要捕获 Explorer.exe 这个进程的桌面窗口所在的线程的消息。因此,需要将HOOK过程放在一个独立的DLL
中去,然后使用 SetWindowsHookEx 将HOOK过程安装到HOOK链中去。
2. 如何解析点击的桌面 Item 信息?这个其实也比较好做,由于桌面窗口本身是一个
listview 控件(不解释),因此,我们可以通过 listview 拿到桌面窗口的简单信息。
3. 如何将解析后的桌面 Item 信息发送给主调程序,让它弹出一个窗口,并显示桌面
Item 信息?我们这里采用WM_COPYDATA 将从桌面进程获取到信息发送到我们的应用程序。
工程目录如下:
在主调程序(DesktopCaptor2.exe)是一个简单Win32
Dialog的程序。在这个程序中,我们干了两件事情:
1. 调用 DekstopHook 工程中的 DesktopHook.h
中的两个导出函数,通过这两个函数,对 HOOK 过程安装和卸载。
2. 接收来自于Explorer.exe 进程发送的 WM_COPYDATA
消息,还原桌面 Item 数据,并将在点击桌面 Item 的位置弹出一个窗口出来,显示 Item 信息。至于这个窗口如何制作,我就不再描述了。
以下为部分代码:
#include "CommonDef.h" #include "DesktopHook.h" #include "FloatWin.h" const UINT WM_DESKTOP_CLICKED_ITEM = RegisterWindowMessage(L"WM_DESKTOP_CLICKED_ITEM"); BOOL g_isCaptured = FALSE; CFloatWin* g_floatWin = NULL; INT_PTR WINAPI DlgProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) ; int WINAPI _tWinMain(HINSTANCE hinstExe, HINSTANCE, PTSTR pszCmdLine, int) { HWND hwnd = FindWindow(TEXT("#32770"), TEXT("DesktopCaptor2")); if (IsWindow(hwnd)) { // An instance is already running, show a messagebox MessageBox(GetForegroundWindow(), L"An instance is already running", L"Error", MB_ICONERROR); } else { DialogBox(hinstExe, MAKEINTRESOURCE(IDD_DESKTOP_CAPTOR), NULL, DlgProc); } return(0); } INT_PTR CALLBACK DlgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) { UNREFERENCED_PARAMETER(lParam); switch (message) { case WM_INITDIALOG: { // Set icon for the application SendMessage(hDlg, WM_SETICON, ICON_BIG, (LPARAM) LoadIcon((HINSTANCE) GetWindowLongPtr(hDlg, GWLP_HINSTANCE), MAKEINTRESOURCE(IDI_DESKTOPCAPTOR2))); // Set dialog's position int nScreenWidth = ::GetSystemMetrics(SM_CXSCREEN); int nScreenHeight = ::GetSystemMetrics(SM_CYSCREEN); RECT rect = { 0 }; GetWindowRect(hDlg, &rect); SetWindowPos( hDlg, HWND_TOP, nScreenWidth - (rect.right - rect.left), 0, 0, 0, SWP_NOSIZE); g_floatWin = CFloatWin::getInstance(); } return (INT_PTR)TRUE; case WM_COMMAND: { UINT wmId = LOWORD(wParam); UINT wmEvent = HIWORD(wParam); switch (wmId) { case IDOK: case IDCANCEL: EndDialog(hDlg, LOWORD(wParam)); return (INT_PTR)TRUE; case IDC_START_CAPTOR: if (FALSE == g_isCaptured) { <strong> </strong>g_isCaptured = CreateDesktopEventCaptor(hDlg); } break; case IDC_STOP_CAPTOR: if (TRUE == g_isCaptured) { CloseDesktopEventCaptor(); g_isCaptured = FALSE; } break; default: return DefWindowProc(hDlg, message, wParam, lParam); } } break; case WM_COPYDATA: { COPYDATASTRUCT* pCopyData = (COPYDATASTRUCT*)lParam; if (pCopyData->dwData == WM_DESKTOP_CLICKED_ITEM) { DesktopItemData itemData(*(DesktopItemData*)pCopyData->lpData); g_floatWin->ShowWindow(TRUE, &itemData); } } break; } return (INT_PTR)FALSE; } |
我们重点说明一下DekstopHookDLL中的内容。
DesktopHook.h 中定义的是一些导出接口,代码如下:
#ifndef _DESKTOPHOOK_H_ #define _DESKTOPHOOK_H_ //#ifdef __cplusplus //extern "C" { //#endif #ifdef DESKTOPHOOK_EXPORTS #define DESKTOPHOOK_API __declspec(dllexport) #else #define DESKTOPHOOK_API __declspec(dllimport) #endif typedef struct DESKTOPHOOK_API _DesktopItemData { POINT point; WCHAR szName[MAX_PATH]; // The desktop item's name. int nIndex; // The desktop item's index on desktop. _DesktopItemData() { ZeroMemory(szName, MAX_PATH); point.x = point.y = 0; nIndex = -1; } _DesktopItemData(POINT pt, WCHAR* pszName, int nIx) { if (NULL != pszName) { point.x = pt.x; point.y = pt.y; ZeroMemory(szName, MAX_PATH); _tcscpy_s(szName, MAX_PATH, pszName); nIndex = nIx; } } _DesktopItemData(const _DesktopItemData& itemDataRef) { point.x = itemDataRef.point.x; point.y = itemDataRef.point.y; ZeroMemory(szName, MAX_PATH); _tcscpy_s(szName, MAX_PATH, itemDataRef.szName); nIndex = itemDataRef.nIndex; } _DesktopItemData& operator = (const _DesktopItemData& itemDataRef) { point.x = itemDataRef.point.x; point.y = itemDataRef.point.y; ZeroMemory(szName, MAX_PATH); _tcscpy_s(szName, MAX_PATH, itemDataRef.szName); nIndex = itemDataRef.nIndex; return *this; } } DesktopItemData, *LPDesktopItemData; EXTERN_C DESKTOPHOOK_API BOOL CreateDesktopEventCaptor(HWND hNotifierhWnd); EXTERN_C DESKTOPHOOK_API void CloseDesktopEventCaptor(); //#ifdef __cplusplus //} //#endif #endif // _DESKTOPHOOK_H_ |
其中:
1. DesktopItemData 结构体是用来存放解析桌面 Item
的数据。
2. CreateDesktopEventCaptor 函数是用来安装
HOOK;
3. CloseDesktopEventCaptor 函数是用来卸载 HOOK;
以下是 DesktopHook.cpp 中的实现代码:
// DesktopHook.cpp : Defines the exported functions for the DLL application. // #include "stdafx.h" #include "DesktopHook.h" #include "DesktopItem.h" #pragma data_seg("SHARED_DATA") HWND g_hNotifierWnd = NULL; HHOOK g_hPostMsgHook = NULL; WCHAR g_szBuf[MAX_PATH] = {0}; #pragma data_seg() #pragma comment(linker, "/SECTION:SHARED_DATA,RWS") // Global data const UINT WM_DESKTOP_CLICKED_ITEM = RegisterWindowMessage(L"WM_DESKTOP_CLICKED_ITEM"); HMODULE g_hModule; HWND g_hDesktopWnd = NULL; // The low-order word specifies the x-coordinate of the cursor. // The high-order word specifies the y-coordinate of the cursor. BOOL g_bDoubleClick = FALSE; UINT_PTR g_timerID = 0; POINT g_clickPt; CDesktopItem g_singleClickDesktopItem = CDesktopItem::Empty; DesktopItemData g_desktopItemData; // Declaration of methods. static HWND FindShellWindow(); static LRESULT CALLBACK GetMsgProc(int code,WPARAM wParam,LPARAM lParam); static VOID CALLBACK TimerProc(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime); EXTERN_C DESKTOPHOOK_API BOOL CreateDesktopEventCaptor(HWND hNotifierhWnd) { g_hDesktopWnd = FindShellWindow(); if (NULL == g_hDesktopWnd) { OutputDebugString(L"Can not find desktop window handle"); return FALSE; } g_hNotifierWnd = hNotifierhWnd; // Get desktop handle's thread id. DWORD targetThreadid = ::GetWindowThreadProcessId(g_hDesktopWnd, NULL); // Hook post message. g_hPostMsgHook = ::SetWindowsHookEx(WH_GETMESSAGE, &GetMsgProc, g_hModule, targetThreadid); if (g_hPostMsgHook != NULL) { return TRUE; } return FALSE; } EXTERN_C DESKTOPHOOK_API void CloseDesktopEventCaptor() { if (g_hPostMsgHook != NULL) { ::UnhookWindowsHookEx(g_hPostMsgHook); } } HWND FindShellWindow() { // Sometimes, we can't find the desktop window when we use this function, but we must // find it's handle, so we do a loop to find it, but at most we find for 10 times. UINT uFindCount = 0; HWND hSysListView32Wnd = NULL; while (NULL == hSysListView32Wnd && uFindCount < 10) { HWND hParentWnd = ::GetShellWindow(); HWND hSHELLDLL_DefViewWnd = ::FindWindowEx(hParentWnd, NULL, L"SHELLDLL_DefView", NULL); hSysListView32Wnd = ::FindWindowEx(hSHELLDLL_DefViewWnd, NULL, L"SysListView32", L"FolderView"); if (NULL == hSysListView32Wnd) { hParentWnd = ::FindWindowEx(NULL, NULL, L"WorkerW", L""); while((!hSHELLDLL_DefViewWnd) && hParentWnd) { hSHELLDLL_DefViewWnd = ::FindWindowEx(hParentWnd, NULL, L"SHELLDLL_DefView", NULL); hParentWnd = FindWindowEx(NULL, hParentWnd, L"WorkerW", L""); } hSysListView32Wnd = ::FindWindowEx(hSHELLDLL_DefViewWnd, 0, L"SysListView32", L"FolderView"); } if (NULL == hSysListView32Wnd) { Sleep(1000); uFindCount++; } else { break; } } return hSysListView32Wnd; } // The message which is "Post" type can be hook in this hook procedure. LRESULT CALLBACK GetMsgProc(int code, WPARAM wParam, LPARAM lParam) { MSG* pMsg = (MSG*)lParam; if( NULL != pMsg && pMsg->hwnd != NULL ) { switch(pMsg->message) { case WM_MOUSEMOVE: { //OutputDebugStringW(L"Mouse Move"); } break; case WM_LBUTTONUP: { //OutputDebugStringW(L"WM_LBUTTONUP"); if (g_bDoubleClick) { OutputDebugString(L"g_bDoubleClick == TRUE"); g_singleClickDesktopItem = CDesktopItem::Empty; g_bDoubleClick = FALSE; ::KillTimer(NULL, g_timerID); } else { OutputDebugString(L"g_bDoubleClick == FALSE"); ::KillTimer(NULL, g_timerID); g_clickPt.x = pMsg->pt.x; g_clickPt.y = pMsg->pt.y; g_singleClickDesktopItem = pMsg; g_timerID = ::SetTimer(NULL, 1, ::GetDoubleClickTime(), TimerProc); } } break; case WM_LBUTTONDBLCLK: g_bDoubleClick = TRUE; break; } } return ::CallNextHookEx(g_hPostMsgHook, code, wParam, lParam); } VOID CALLBACK TimerProc(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime) { UNREFERENCED_PARAMETER(hwnd); UNREFERENCED_PARAMETER(uMsg); UNREFERENCED_PARAMETER(idEvent); UNREFERENCED_PARAMETER(dwTime); if (g_timerID == idEvent && g_bDoubleClick == FALSE && g_singleClickDesktopItem != CDesktopItem::Empty) { OutputDebugString(L"SendMessageTimeout Process begin"); wstring strName = g_singleClickDesktopItem.GetItemName(); //_tcscpy_s(g_szBuf, MAX_PATH, strName.c_str()); //::PostMessage(g_hNotifierWnd, WM_DESKTOP_CLICKED_ITEM, NULL, (LPARAM)g_szBuf); ZeroMemory(&g_desktopItemData, sizeof(DesktopItemData)); g_desktopItemData.point.x = g_clickPt.x; g_desktopItemData.point.y = g_clickPt.y; _tcscpy_s(g_desktopItemData.szName, MAX_PATH, strName.c_str()); g_desktopItemData.nIndex = g_singleClickDesktopItem.GetItemIndex(); COPYDATASTRUCT copyData; copyData.dwData = (UINT_PTR)WM_DESKTOP_CLICKED_ITEM; copyData.cbData = sizeof(DesktopItemData); copyData.lpData = &g_desktopItemData; DWORD_PTR rt = -1; SetActiveWindow(g_hNotifierWnd); SetForegroundWindow(g_hNotifierWnd); //SendMessageTimeout(g_hNotifierWnd, WM_COPYDATA,
(WPARAM)g_hDesktopWnd, (LPARAM)?Data, SMTO_NORMAL, 1000, &rt); SendMessage(g_hNotifierWnd, WM_COPYDATA, (WPARAM)g_hDesktopWnd, (LPARAM)?Data); OutputDebugString(L"SendMessageTimeout Process end"); } ::KillTimer(NULL, g_timerID); } |
说明:在该实现中,CreateDesktopEventCaptor 函数与
CloseDesktopEventCaptor 函数被 DesktopCaptor2.exe 进程调用,此时,DesktopHook.dll
会被加载到 DesktopCaptor2.exe 所在进程,当使用 CreateDesktopEventCaptor
函数后,HOOK 过程函数 GetMsgProc 会被安装到 Explorer.exe 进程中去,此时,DesktopHook.dll
会被加载到 Explorer.exe 中去。当用鼠标单击桌面 Item 时,消息将被传递到 GetMsgProc
中去,然后,在这个函数中发送一个 WM_COPYDATA 消息给DesktopCaptor2 的窗口,实际上,这相当于是
Explorer.exe 进程发送的 WM_COPYDATA 消息到 DesktopCaptor2.exe
进程 。需要注意一点的是,在 GetMsgProc 中发送消息的时候,g_hNotifierWnd 必须要设置成为共享数据,因为它是DesktopCaptor2.exe
进程在调用 DesktopHook.dll 的 CreateDesktopEventCaptor 函数的时候被设置的,要想在Explorer.exe进程中继续有效,需要将之设置为
DLL共享数据。
另外,这里还有另外两个问题:
1、如何找到 Desktop 窗口句柄? 通过Spy++,我们得到:
其中,蓝色部分就是我们的桌面窗口句柄。我们的 FindShellWindow
函数就是为了干这个事情,但是有时候,这个函数会失败,因此,我们最多循环10次去查找桌面窗口句柄。
2、WH_GETMESSAGE 类型的消息 HOOK 只能钩住使用 PostMessage
方式发送的消息,这点很重要,否则,如果是以SendMessage发送的方式,则需要使用 WH_CALLWNDPROC
或者 WH_CALLWNDPROCRET 的 HOOK 方式。
3、如何解析点击桌面的 Item 信息?
实际上,由于桌面窗口本身是一个 ListView 控件,通过ListView
的控件的API,我们便能够拿到相关的信息(当然这里只是一个简单的信息,深层次的信息还需要深掘)。
首先,通过下面的函数,能够拿到选中的 Item 的 ID,其中 hwnd
就是桌面窗口句柄。
UINT ListView_GetSelectedCount( HWND hwnd ); |
其次,通过下面的函数,传入选中的 Item 的 ID,我们就能拿到 Item 对应的文本。
void ListView_GetItemText( HWND hwnd, int iItem, int iSubItem, LPTSTR pszText, int cchTextMax ); |
在这里,我使用了一个 CDesktopItem 类来专门干这个事情。
至此,简单的代码讲解就结束了。
下面说一下如何调试。
因为本例子是进程外的HOOK,因此调试起来有很多不方便的地方。调试的难处在于如何调试HOOK过程函数。先说一个简单的例子,可能大家经常会遇到这种情况:假如一个Solution下有ProjectA和
Project B,它们都是exe,但涉及到相互发消息,有人就会打开2个Visual Studio,同时进行调试,这样的确可以做到,但总感觉不太方便。实际上,在同一个Visual
Studio中,是可以同时调试多个程序的。对于刚刚这种情况,只需要在每个Project A 和 ProjectB
上面分别右击->Debug->Start new instance 即可。
然而,对于本例,这样做是不行的,因为 DesktopHook 是一个DLL工程,本身是无法进行独立调试的。因此,要想同时调试
DesktopCaptor2 工程和 DesktopHook 工程,需要按如下操作:
1、将DesktopHook工程设置为默认启动的工程。如图:
2、将 DesktopCaptor2 工程通过 右击->Debug->Start
new instance 启动起来,点击 Start Desktop Captor 按钮启动HOOK,将DesktopHook
.dll 注入到进程Explorer.exe中去。
3、将 DesktopHook 工程到附加(Attach)到Explorer.exe进程中去。如图:
点击 Tools->Attachto Process...
然后找到Explorer.exe,点击Attach按钮即可。
通过上面的这种方式,我们就能够很简单的在一个工程中,调试两个不同的进程的程序。这时,我们将断点打在DesktopHook
工程的函数 GetMsgProc中,并在DesktopCaptor2 工程 WM_COPYDATA 内部打上断点,发现点击桌面图标时,断点会走到GetMsgProc内部,当发送完消息后,就能走到DesktopCaptor2
工程 WM_COPYDATA 内部。。
另外,你调试的时候,要注意一下当前用户的权限以及Visual Studio的权限。如果当前用户是管理员组用户,而你的VisualStudio是以管理员启动进来的,那么DesktopCaptor2.exe将也是管理员权限,但此时,Explorer.exe却是普通权限,此时,在GetMsgProc内部发送WM_COPYDATA是会出现问题的,因为不能向高权限进程发送消息。
|