QQ 經典功能:視窗停靠螢幕左/右/上邊緣時自動摺疊隱藏,滑鼠移至邊緣自動展開。關鍵在於捕獲視窗外部滑鼠移動,標準 WM_MOUSEMOVE/WM_NCMOUSEMOVE 僅涵蓋視窗內部/非客戶區。解決方案:DLL 注入式全域滑鼠鉤子 (WH_MOUSE) 監控全螢幕滑鼠座標,透過 PostThreadMessage 轉發自訂訊息至主視窗執行緒,於 PreTranslateMessage 中判斷邏輯觸發摺疊/展開動畫。
| 類別 | 觸發區域 | MFC 訊息 | 能否捕獲視窗外 |
|---|---|---|---|
| 客戶區 | 視窗工作區 | WM_MOUSEMOVE | 否 |
| 非客戶區 | 標題列、邊框、捲軸 | WM_NCMOUSEMOVE | 否 |
| 視窗外部 | 其他視窗、桌面、任務列 | 無標準訊息 | 需全域鉤子 |
// 共享資料段:確保多進程共享鉤子狀態
#pragma data_seg("HOOK_DATA")
HHOOK g_hHook = NULL;
DWORD g_ThreadID = 0; // 目標視窗執行緒 ID
int g_Width = 0; // 視窗寬度
BOOL g_Left = TRUE; // 左側停靠
#pragma data_seg()
#pragma comment(linker, "/SECTION:HOOK_DATA,RWS")
HINSTANCE g_hInst = NULL;
BOOL APIENTRY DllMain(HINSTANCE hInst, DWORD reason, LPVOID)
{
if (reason == DLL_PROCESS_ATTACH) g_hInst = hInst;
return TRUE;
}
// 匯出函式:安裝鉤子
__declspec(dllexport) BOOL SetHook(DWORD dwThreadID, int nWidth, BOOL bLeft)
{
if (g_ThreadID) return FALSE; // 已安裝
g_ThreadID = dwThreadID;
g_Width = nWidth;
g_Left = bLeft;
g_hHook = SetWindowsHookEx(WH_MOUSE, MouseProc, g_hInst, 0); // 0 = 全域
return g_hHook != NULL;
}
// 鉤子處理函式
LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam)
{
if (nCode < 0) return CallNextHookEx(g_hHook, nCode, wParam, lParam);
if (g_ThreadID)
{
static POINT ptOld = {0,0};
POINT pt; GetCursorPos(&pt);
// 僅座標改變時發送,避免訊息風暴
if (pt.x != ptOld.x || pt.y != ptOld.y)
{
PostThreadMessage(g_ThreadID, WM_USER + 1000, 0, 0);
ptOld = pt;
}
}
return CallNextHookEx(g_hHook, nCode, wParam, lParam);
}
// 匯出函式:卸載鉤子
__declspec(dllexport) BOOL UnSetHook()
{
if (!g_ThreadID) return FALSE;
if (g_hHook) { UnhookWindowsHookEx(g_hHook); g_hHook = NULL; }
g_ThreadID = 0;
return TRUE;
}
typedef BOOL (WINAPI *PFN_SETHOOK)(DWORD, int, BOOL);
typedef BOOL (WINAPI *PFN_UNHOOK)();
HMODULE hHookDll = LoadLibrary(_T("HookDll.dll"));
PFN_SETHOOK pSetHook = (PFN_SETHOOK)GetProcAddress(hHookDll, "SetHook");
PFN_UNHOOK pUnHook = (PFN_UNHOOK)GetProcAddress(hHookDll, "UnSetHook");
// 視窗建立後安裝
pSetHook(GetCurrentThreadId(), 100, TRUE); // 寬度 100px,左側停靠
PreTranslateMessage 邏輯判斷BOOL CHookTestDlg::PreTranslateMessage(MSG* pMsg)
{
if (pMsg->message == WM_USER + 1000)
{
POINT pt; GetCursorPos(&pt);
CRect rc; GetWindowRect(&rc);
int screenW = GetSystemMetrics(SM_CXSCREEN);
// 滑鼠在左邊緣 1 像素內,且 Y 在視窗垂直範圍內
bool atLeftEdge = (pt.x < 1) && (pt.y > rc.top && pt.y < rc.bottom);
// 視窗目前狀態
bool isCollapsed = (rc.left < 1 && rc.Width() < 1);
bool isExpanded = (rc.left < 1 && rc.Width() > 99);
if (atLeftEdge)
{
if (isCollapsed) SliderWindow(LEFT, true); // 展開
}
else // 滑鼠離開邊緣區域
{
if (isExpanded) SliderWindow(LEFT, false); // 摺疊
}
}
return CDialog::PreTranslateMessage(pMsg);
}
SliderWindowvoid CHookTestDlg::SliderWindow(int nPos, bool bShow)
{
CRect rc; GetWindowRect(&rc);
if (nPos == LEFT)
{
if (bShow) // 展開:寬度 0 → 100
{
for (int i=0; i<=10; ++i)
{
SetWindowPos(&wndTopMost, 0, rc.top, i*10, rc.Height(), SWP_SHOWWINDOW);
Sleep(20);
}
}
else // 摺疊:寬度 100 → 0
{
for (int i=0; i<=10; ++i)
{
SetWindowPos(&wndTopMost, 0, rc.top, 100 - i*10, rc.Height(), SWP_SHOWWINDOW);
Sleep(20);
}
}
}
// RIGHT/TOP/BOTTOM 同理擴充
}

| 技術點 | 說明 |
|---|---|
| DLL 注入鉤子 | SetWindowsHookEx(WH_MOUSE, ..., hInst, 0) 最後參數 0 表示全域,需 DLL 導出安裝/卸載函式 |
| 共享資料段 | #pragma data_seg("HOOK_DATA") + 連結器 /SECTION:HOOK_DATA,RWS 確保鉤子狀態跨進程共享 |
| 訊息轉發 | PostThreadMessage 非同步發送至主執行緒,避免阻塞鉤子鏈 |
| 去抖動 | 靜態 ptOld 比較座標,僅移動時發送訊息 |
| UI 執行緒處理 | PreTranslateMessage 攔截自訂訊息,執行幾何判斷與動畫 |
WM_MOUSEMOVE。Sleep 在 UI 執行緒會凍結視窗,建議改用 SetTimer 分幀動畫或 AnimateWindow API。GetCursorPos 回傳虛擬螢幕座標,邊緣判斷需考慮多螢幕拓撲。SetWindowsHookEx 在 Win10/11 受 UIPI (User Interface Privilege Isolation) 限制,高完整性進程無法鉤子低完整性進程,反之亦然。RegisterRawInputDevices) 或 Windows Hooks (WinRT) 配合 AppService 跨進程通訊。SetWindowPos + WM_MOUSELEAVE/TrackMouseEvent 追蹤非客戶區,無需全域鉤子 (但無法檢測視窗外滑鼠)。全域滑鼠鉤子是實現「視窗外滑鼠感知」的經典技術,本文完整示範 DLL 注入、共享記憶體、訊息轉發、動畫邏輯全流程。雖現代安全機制增加部署難度,但理解其原理對維護舊專案、開發系統級工具、學習 Windows 訊息架構極有價值。
本文配套原始碼含 DLL/EXE 完整專案。