上過QQ的朋友們都知道,當QQ窗口位於桌面的左邊界、右邊界或頂部的時候,QQ會自動隱藏起來;而一旦鼠標再次接觸到上述邊界的時候,QQ窗口又會自動展開。QQ的這種特效在一定程度上大大的節約了桌面資源,給使用者帶來的方便。
QQ懸掛窗口主要特點就是結合窗口以及鼠標的位置,並通過鼠標事件來調整窗口的顯示方式。其中,窗口以及鼠標的位置可以通過GetWindowRect和GetCursorPos這兩個函數來獲取,故如何獲取鼠標事件成為QQ懸掛窗口實現的關鍵。
對於一個窗口來說,按鼠標事件的觸發位置,鼠標事件可以分為三類:
1. 客戶區鼠標消息:鼠標在窗口的客戶區移動時產生的消息,此消息是標准的鼠標消息,MFC中通過WM_MOUSEMOVE這個事件解決了這個問題。
2. 非客戶區鼠標消息:鼠標在非客戶區以外(標題欄、框架等)移動時產生的消息,此消息是標准的鼠標消息,MFC中通過WM_NCMOUSEMOVE這個事件解決了這個問題。
3. 窗口以外的鼠標消息:鼠標不在本窗口移動時產生的消息,此消息不是標准的鼠標消息,在MFC中也找不到這樣的事件。那該如何捕獲這樣的鼠標消息呢?
窗口以外的鼠標消息必然是發生在其他窗口上的,此鼠標消息是發往其他窗口的消息隊列中,由其他窗口的消息隊列所維護。
不過,我們可以通過設置全局鼠標鉤子來監視鼠標的位置,並觸發鼠標消息。如果將鼠標鉤子設置在窗口內部設置的話,那此鼠標鉤子僅能夠監視到上述鼠標事件的前兩類事件,而不能夠監視到本窗口以外的鼠標消息,並不是真正的全局鼠標鉤子。如果將鼠標鉤子設置在DLL中,那麼鼠標在整個屏幕上所發生的事件都會被這個鼠標過程所監察到,即可以捕獲其他窗口的鼠標消息並將此鼠標消息發往本窗口的所屬線程的消息隊列中。在本窗口中,必須將本窗口的線程ID傳到DLL中,使DLL能夠將其他鼠標事件發到指定線程的消息隊列中。具體實現如下:
//------------------------------------------------------------------------------------
// Function: SetHook - Creates mouse hook (Exported), called by CAppBarMngr
// Arguments: _id - Calling thread ID, used to send message to it
// _width - Width of window
// _left - True if window is left side docked, false if not
// Returns: False if it is already hooked
// True if hook has been created
//------------------------------------------------------------------------------------
BOOL SetHook(DWORD _id, int _width, BOOL _left)
{
if (s_ThreadID)
return FALSE; // Already hooked!
s_Width = _width;
s_Left = _left;
g_Hook = ::SetWindowsHookEx(WH_MOUSE, (HOOKPROC)MouseProc, g_Instance, 0);
s_ThreadID = _id;
return TRUE; // Hook has been created correctly
}
//-------------------------------------------------------------------------------------
// Function: MouseProc - Callback function for mouse hook
// Arguments: nCode - action code, according to MS documentation, must return
// inmediatly if less than 0
// wParam - not used
// lParam - not used
// Returns: result from next hook in chain
//-------------------------------------------------------------------------------------
static LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam)
{
static LRESULT lResult; // Made static to accelerate processing
static POINT pt; // idem
if (nCode<0 && g_Hook)
{
::CallNextHookEx(g_Hook, nCode, wParam, lParam); // Call next hook in chain
return 0;
}
if (s_ThreadID)
{
// Obtain absolute screen coordinates
::GetCursorPos(&pt);
static POINT ptOld;
//只有當鼠標發生移動時候發生鼠標事件,沒有想到鼠標不移動也會產生此鼠標過程,
//真讓我大吃一驚,必須得防止鼠標消息亂發。
if(ptOld.x!=pt.x && ptOld.y!=pt.y)
{
::PostThreadMessage(s_ThreadID, WM_USER+1000, 0, 0);
ptOld.x = pt.x;
ptOld.y = pt.y;
}
}
return ::CallNextHookEx(g_Hook, nCode, wParam, lParam); // Call next hook in chain
}
//-------------------------------------------------------------------------------------
// Function: UnSetHook - Removes hook from chain
// Arguments: none
// Returns: False if not hook pending to delete (no thread ID defined)
// True if hook has been removed. Also returns true if there is not hook
// handler, this can occur if Init failed when called in second instance
//-------------------------------------------------------------------------------------
BOOL UnSetHook()
{
if (!s_ThreadID) {
return FALSE; // There is no hook pending to close
}
if (g_Hook) { // Check if hook handler is valid
::UnhookWindowsHookEx(g_Hook); // Unhook is done here
s_ThreadID = 0; // Remove thread id to avoid continue sending
g_Hook = NULL; // Remove hook handler to avoid to use it again
}
return TRUE; // Hook has been removed
}
鼠標消息一旦發到本窗口線程的消息隊列後,本窗口過程在鼠標消息未被翻譯之前從消息隊列中取出消息,並進行處理。故得重載PreTranslateMessage這個虛函數。邏輯判斷過程如下:
BOOL CHookTestDlg::PreTranslateMessage(MSG* pMsg)
{
// TODO: Add your specialized code here and/or call the base class
static int i=0;
int nSrcWidth = ::GetSystemMetrics(SM_CXSCREEN);
int nSrcHeight = ::GetSystemMetrics(SM_CYSCREEN);
switch(pMsg->message)
{
case WM_USER+1000:
{
POINT pt;
CRect rcWindow;
::GetCursorPos(&pt);
GetWindowRect(&rcWindow);
if(pt.x<1 && (pt.y>rcWindow.top && pt.y<rcWindow.bottom))
{
if(rcWindow.left<1 && rcWindow.Width()<1)
{
SliderWindow(LEFT, true);
}
else if(rcWindow.left<1 && rcWindow.Width()>99)
{
}
else if(rcWindow.left>1 && rcWindow.Width()>99)
{
}
else
{
}
}
else if(pt.y<rcWindow.top || pt.y>rcWindow.bottom)
{
if(rcWindow.left<1 && rcWindow.Width()<1)
{
}
else if(rcWindow.left<1 && rcWindow.Width()>99)
{
SliderWindow(LEFT, false);
}
else if(rcWindow.left>1 && rcWindow.Width()>99)
{
}
else
{
}
}
else if(pt.x>0 && pt.x<100)
{
if(rcWindow.left<1 && rcWindow.Width()<1 && (pt.y>rcWindow.top && pt.y<rcWindow.bottom))
{
}
else if(rcWindow.left<1 && rcWindow.Width()>99)
{
//SliderWindow(LEFT, true);
}
else if(rcWindow.left>1 && rcWindow.Width()>99)
{
}
else
{
}
}
else
{
if(rcWindow.left<1 && rcWindow.Width()<1)
{
}
else if(rcWindow.left<1 && rcWindow.Width()>99 && (pt.y>rcWindow.top && pt.y<rcWindow.bottom))
{
SliderWindow(LEFT, false);
}
else if(rcWindow.left>1 && rcWindow.Width()>99)
{
}
else
{
}
}
}
break;
default:
break;
}
return CDialog::PreTranslateMessage(pMsg);
}
void CHookTestDlg::SliderWindow(int nPos, bool bShow)
{
CRect rc;
GetWindowRect(rc);
int nSrcWidth = ::GetSystemMetrics(SM_CXSCREEN);
switch(nPos)
{
case LEFT:
if(bShow)
{
for(int i=0; i<=10; i++)
{
SetWindowPos(&CWnd::wndTopMost, 0, rc.top, i*10, rc.Height(), SWP_SHOWWINDOW);
Sleep(20);
}
}
else
{
for(int i=0; i<=10; i++)
{
SetWindowPos(&CWnd::wndTopMost, 0, rc.top, 100-10*i, rc.Height(), SWP_SHOWWINDOW);
Sleep(20);
}
}
break;
case RIGHT:
break;
case TOP:
break;
case BOTTOM:
break;
default:
break;
}
}
最後運行結果如下:
朋友們,以後若想捕獲其他窗口的鼠標事件的時候可以采用這個方法,大家也可以明白MFC中的標准鼠標消息的底層是怎麼實現的,大家是否有眼前一亮的感覺呢?最後提出幾個問題:
1、 在安裝完上述鼠標鉤子後,MFC的標准鼠標消息WM_MOUSEMOVE、WM_LBUTTONDOWN等還有作用嗎?
2、 通過MFC的ON_MESSAGE將WM_USER+1000這個與指定的處理過程相關聯來處理鼠標消息(當然此時不需要重載PreTranslateMessage),這樣做可以嗎?如:ON_MESSAGE(WM_USER+1000, MouseProc)。
希望大家給我發郵件進行討論,祝大家編程愉快!
QQ:181484408
本文配套源碼