首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> VC >> 關於VC++

仿 QQ 懸掛視窗:全域滑鼠鉤子實現邊緣自動隱藏/展開

     編輯:關於VC++

前言

QQ 經典功能:視窗停靠螢幕左/右/上邊緣時自動摺疊隱藏,滑鼠移至邊緣自動展開。關鍵在於捕獲視窗外部滑鼠移動,標準 WM_MOUSEMOVE/WM_NCMOUSEMOVE 僅涵蓋視窗內部/非客戶區。解決方案:DLL 注入式全域滑鼠鉤子 (WH_MOUSE) 監控全螢幕滑鼠座標,透過 PostThreadMessage 轉發自訂訊息至主視窗執行緒,於 PreTranslateMessage 中判斷邏輯觸發摺疊/展開動畫。

三類滑鼠訊息範圍

類別觸發區域MFC 訊息能否捕獲視窗外
客戶區視窗工作區WM_MOUSEMOVE否
非客戶區標題列、邊框、捲軸WM_NCMOUSEMOVE否
視窗外部其他視窗、桌面、任務列無標準訊息需全域鉤子

DLL 端:全域滑鼠鉤子實作

// 共享資料段:確保多進程共享鉤子狀態
#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;
}

EXE 端:視窗類別整合

1. 載入 DLL 並安裝鉤子

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,左側停靠

2. 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);
}

3. 滑動動畫 SliderWindow

void 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 同理擴充
}

效果展示

QQ 懸掛視窗效果圖

關鍵技術點總結

技術點說明
DLL 注入鉤子SetWindowsHookEx(WH_MOUSE, ..., hInst, 0) 最後參數 0 表示全域,需 DLL 導出安裝/卸載函式
共享資料段#pragma data_seg("HOOK_DATA") + 連結器 /SECTION:HOOK_DATA,RWS 確保鉤子狀態跨進程共享
訊息轉發PostThreadMessage 非同步發送至主執行緒,避免阻塞鉤子鏈
去抖動靜態 ptOld 比較座標,僅移動時發送訊息
UI 執行緒處理PreTranslateMessage 攔截自訂訊息,執行幾何判斷與動畫

常見問題與避坑

  1. 安裝鉤子失敗:DLL 未置於系統路徑/應用程式目錄、位元數不匹配 (32/64bit)、殺毒軟件攔截。
  2. 訊息風暴導致卡頓:未加座標比較去抖動,滑鼠靜止時仍產生大量 WM_MOUSEMOVE。
  3. 動畫阻塞 UI:Sleep 在 UI 執行緒會凍結視窗,建議改用 SetTimer 分幀動畫或 AnimateWindow API。
  4. 多螢幕座標:GetCursorPos 回傳虛擬螢幕座標,邊緣判斷需考慮多螢幕拓撲。

現代替代方案

  • SetWindowsHookEx 在 Win10/11 受 UIPI (User Interface Privilege Isolation) 限制,高完整性進程無法鉤子低完整性進程,反之亦然。
  • 現代應用建議改用 Raw Input (RegisterRawInputDevices) 或 Windows Hooks (WinRT) 配合 AppService 跨進程通訊。
  • 邊緣隱藏視窗可直接使用 SetWindowPos + WM_MOUSELEAVE/TrackMouseEvent 追蹤非客戶區,無需全域鉤子 (但無法檢測視窗外滑鼠)。

結語

全域滑鼠鉤子是實現「視窗外滑鼠感知」的經典技術,本文完整示範 DLL 注入、共享記憶體、訊息轉發、動畫邏輯全流程。雖現代安全機制增加部署難度,但理解其原理對維護舊專案、開發系統級工具、學習 Windows 訊息架構極有價值。

本文配套原始碼含 DLL/EXE 完整專案。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved