日前在开发一个多桌面系统,即利用CreateDesktop创建多个桌面,并自由切换,类似Sysinternal上的Desktops。开发过程中遇到一个问题,当在新创建的桌面上按下某个Win组合快捷键后,当前桌面并不会响应这个消息,而是默认桌面响应了这个消息。为了能在新创建的桌面上响应热键消息,同事反编译了Desktops软件,发现它使用了低级键盘钩子(WH_KEYBOARD_LL),于是仿照这个方法,我们也挂上了低级键盘钩子,当发现按键组合属于热键映射表中的某一项时,向当前桌面的Shell_TrayWnd窗口发送WM_HOTKEY消息并且不在传递此HOOK消息。这个方法用起来还行,但是由于自身写的代码处理不够好,或者说逆向的不够,功能上有个bug。当用户按下热键后,例如Win+R,输入(按下)t 时会继续触发Win+T热键消息。代码显示按下t是win键也处于按下状态,我分析了代码也没找到原因,想彻底的反汇编Desktops的钩子函数又能力不够,只能另想其它方法解决了。
既然默认桌面的Shell_TrayWnd窗口处理了热键消息,那么我是不是可以利用消息钩子截获它呢?于是我利用WH_GETMESSAGE钩子截获了发送到这个窗口线程的消息
HWND hWnd = ::FindWindow(_T("Shell_TrayWnd"),0);
DWORD ProcessID;
DWORD tid=GetWindowThreadProcessId(hWnd,&ProcessID);
fnInstallHooklib(tid,hWnd,m_hWnd);
为新桌面创建的线程上等待消息,转发热键消息
DWORD WINAPI CHookDemoDlg::ThreadFun(LPVOID lp)
{
CHookDemoDlg* pThis = (CHookDemoDlg*)lp;
HDESK hDesk = OpenDesktop(_T("Topdesk"),0,FALSE,GENERIC_ALL);
if (hDesk!=NULL)
{
if(!SetThreadDesktop(hDesk))
{
/*The SetThreadDesktop function will fail if the calling thread has any windows
or hooks on its current desktop (unless the hDesktop parameter is a handle to the current desktop).*/
/*so SetThreadDesktop failed.*/
pThis->PopErrorMessage(_T("SetThreadDesktop"));
return 0;
}
}
else
{
pThis->PopErrorMessage(_T("OpenDesktop"));
return 0;
}
pThis->ms_newTrayWnd = ::FindWindow(_T("Shell_TrayWnd"), NULL);
MSG msg;
CString strMsg;
while(GetMessage(&msg,NULL,0,0))
{
::SetForegroundWindow(pThis->ms_newTrayWnd);
::OutputDebugString(_T("Receive Thread Message."));
if(::PostMessage(pThis->ms_newTrayWnd,WM_HOTKEY,msg.wParam,msg.lParam)==FALSE)
{
strMsg.Format(_T("PostMessage failed. code = %d"), GetLastError());
::OutputDebugString(strMsg);
}
}
::OutputDebugString(_T("Assist Thread Exit"));
return 0;
}
在钩子函数(DLL模块中)处理热键消息
#pragma data_seg("SharedDataName")
HHOOK ms_hHook=NULL;
HWND ms_hSourceWnd=NULL;
HWND ms_hDirectWnd=NULL;
#pragma data_seg()
extern HMODULE ms_hModule;
#pragma comment(linker,"/section:SharedDataName,rws")
LRESULT CALLBACK GetMsgProc(
_In_ int code,
_In_ WPARAM wParam,
_In_ LPARAM lParam
)
{
MSG* pMsg = (MSG*)lParam;
if (pMsg->message==WM_HOTKEY)
{
CString strMsg;
strMsg.Format(_T("%d %d %d %d"), ms_hHook, ms_hSourceWnd, ms_hDirectWnd, pMsg->hwnd);
::OutputDebugString(strMsg);
if (pMsg->hwnd == ms_hSourceWnd)
{
::OutputDebugString(_T("Receive WM_HOTKEY message."));
if(!PostMessage(ms_hDirectWnd,WM_USER + 1000,pMsg->wParam,pMsg->lParam))
{
strMsg.Format(_T("PostMessage failed. code = %d"), GetLastError());
::OutputDebugString(strMsg);
}
pMsg->message = WM_NULL;
return 1;
}
}
return CallNextHookEx(ms_hHook,code,wParam,lParam);
}
本来我是打算利用PostThreadMessage向新桌面关联的线程发送消息,但是发送失败,因为钩子函数运行在默认桌面下,而新桌面关联的线程关联的新桌面,一个线程不能向一个属于另外桌面的线程发送线程消息(如果这两个线程属于同一个进程则例外,下面会有这个情形用法),于是我只好向主窗口发送消息。
主窗口线程收到消息后,它可以向新桌面关联的线程发送消息,如下
if (message == (WM_USER + 1000))
{
::OutputDebugString(_T("Receive Message."));
if(!PostThreadMessage(ms_tid,WM_HOTKEY,wParam,lParam))
{
CString strMsg;
strMsg.Format(_T("PostThreadMessage failed. code=%d"),GetLastError());
::OutputDebugString((LPCTSTR)strMsg);
}
return 0;
}
这样子新创建的桌面就可以截获热键消息了。
注意:
1. SetWindowsHookEx只能截获当前桌面的消息,msdn有说明
The SetWindowsHookEx function installs an application-defined hook procedure into a hook chain. You would install a hook procedure to monitor the system for certain types of events. These events are associated either with a specific thread or with all threads in the same desktop as the calling thread.
2. FindWindow函数会在调用线程关联的桌面上查找窗口
3. 若程序以管理员身份启动,为了让钩子函数(在explorer进程中)向程序发送消息,越过UIPI,需要调用函数
ChangeWindowMessageFilter(WM_USER+1000,MSGFLT_ADD);