C# 編寫一個小而快的 Windows 動態桌面(一)之桌面交互功能


DreamScene2 現在支持鼠標和桌面交互了,這個功能基本不占用 CPU。桌面交互功能讓我對 Windows 消息機制有了更深入的理解,在這篇博客中我會介紹實現方式。

歡迎 Star 和 Fork https://github.com/he55/DreamScene2

實現原理

使用 WIN32 API SetWindowsHookEx 函數 Hook 鼠標鍵盤消息,在鈎子處理函數中處理捕獲鼠標鍵盤消息然后調用 PostMessage 函數向動態桌面窗口發送轉發消息。

設置鈎子函數

SetWindowsHookEx 函數的第一個參數是鈎子類型,Hook 鼠標消息可以傳 WH_MOUSE_LL,Hook 鍵盤消息可以傳 WH_KEYBOARD_LL。第二個參數是自定義的鈎子消息處理函數地址。函數的第三個參數是鈎子函數所在的模塊句柄,當鈎子類型是 WH_MOUSE_LL 或者 WH_KEYBOARD_LL 時,可以直接傳當前模塊句柄。函數的第四個參數是線程 Id,傳 NULL 捕獲所有消息。

設置鈎子函數代碼。

HHOOK g_hLowLevelMouseHook = NULL;
HHOOK g_hLowLevelKeyboardHook = NULL;

BOOL WINAPI DS2_StartForwardMouseKeyboardMessage(HWND hWnd) {
    g_hWnd = hWnd;

    HMODULE hm = GetModuleHandle(NULL);
    g_hLowLevelMouseHook = SetWindowsHookEx(WH_MOUSE_LL, LowLevelMouseProc, hm, NULL);
    if (!g_hLowLevelMouseHook) {
        return FALSE;
    }

    g_hLowLevelKeyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, hm, NULL);
    return TRUE;
}

卸載鈎子函數代碼。

void WINAPI DS2_EndForwardMouseKeyboardMessage(void) {
    if (g_hLowLevelMouseHook) {
        UnhookWindowsHookEx(g_hLowLevelMouseHook);
        g_hLowLevelMouseHook = NULL;
    }

    if (g_hLowLevelKeyboardHook) {
        UnhookWindowsHookEx(g_hLowLevelKeyboardHook);
        g_hLowLevelKeyboardHook = NULL;
    }
}

編寫鈎子處理函數

WH_MOUSE_LL 和 WH_KEYBOARD_LL 的鈎子處理函數簽名相同,wParam 參數是消息類型,lParam 參數是一個指針和鈎子函數的類型有關。當鈎子類型為 WH_MOUSE_LL 時 lParam 參數是 MSLLHOOKSTRUCT 結構體指針。當鈎子類型為 WH_KEYBOARD_LL 時 lParam 參數是 KBDLLHOOKSTRUCT 結構體指針。

鈎子處理函數簽名

LRESULT CALLBACK xxxProc(
  _In_ int    nCode,
  _In_ WPARAM wParam,
  _In_ LPARAM lParam
);

鼠標鈎子處理函數

LRESULT CALLBACK LowLevelMouseProc(int    nCode, WPARAM wParam, LPARAM lParam) {
    return CallNextHookEx(NULL, nCode, wParam, lParam);
}

處理 WM_LBUTTONDOWN 鼠標按下消息

鼠標鈎子處理函數的 wParam 參數就是鼠標消息類型,lParam 參數需要轉換成 MSLLHOOKSTRUCT 結構體指針,MSLLHOOKSTRUCT 結構體的 pt 字段鼠標相對於屏幕的坐標。想轉發鼠標按下消息,需要看 WM_LBUTTONDOWN 消息的定義:WM_LBUTTONDOWN 消息的 wParam 參數為按鍵的狀態,lParam 參數的低字節為光標的 x 坐標、高字節為光標的 y 坐標。需要注意鼠標鈎子處理函數和 PostMessage 函數的 wParam 參數、lParam 參數含義不同,需要轉換成 PostMessage 函數需要的參數。

WM_LBUTTONDOWN 處理方法

LRESULT CALLBACK LowLevelMouseProc(int    nCode, WPARAM wParam, LPARAM lParam) {
    MSLLHOOKSTRUCT* p = (MSLLHOOKSTRUCT*)lParam;
    LONG lp = MAKELONG(p->pt.x, p->pt.y); // 低字節 x 坐標、高字節 y 坐標

    if (wParam == WM_LBUTTONDOWN) {
        PostMessage(g_hWnd, (UINT)wParam, MK_LBUTTON, lp); // 向動態桌面窗口發送鼠標按下消息
    }
    return CallNextHookEx(NULL, nCode, wParam, lParam);
}

WM_LBUTTONUP 和 WM_MOUSEMOVE 處理方法一樣

LRESULT CALLBACK LowLevelMouseProc(int    nCode, WPARAM wParam, LPARAM lParam) {
    MSLLHOOKSTRUCT* p = (MSLLHOOKSTRUCT*)lParam;
    LONG lp = MAKELONG(p->pt.x, p->pt.y);

    if (wParam == WM_MOUSEMOVE) {
        PostMessage(g_hWnd, (UINT)wParam, MK_XBUTTON1, lp);
    }
    else  if (wParam == WM_LBUTTONDOWN || wParam == WM_LBUTTONUP) {
        PostMessage(g_hWnd, (UINT)wParam, MK_LBUTTON, lp);
    }
    return CallNextHookEx(NULL, nCode, wParam, lParam);
}

優化鼠標消息轉發

上面的代碼會轉發所有的鼠標消息,實際上並不想轉發所有的鼠標消息。對鼠標按下和松開的消息,只轉發焦點在桌面上的鼠標消息。

判斷前台窗口是不是桌面

BOOL DS2_IsDesktop(void) {
    HWND hProgman = FindWindow("Progman", "Program Manager");
    HWND hWorkerW = NULL;

    HWND   hShellViewWin = FindWindowEx(hProgman, NULL, "SHELLDLL_DefView", NULL);
    if (!hShellViewWin)
    {
        HWND hDesktopWnd = GetDesktopWindow();
        do
        {
            hWorkerW = FindWindowEx(hDesktopWnd, hWorkerW, "WorkerW", NULL);
            hShellViewWin = FindWindowEx(hWorkerW, NULL, "SHELLDLL_DefView", NULL);
        } while (!hShellViewWin && hWorkerW);
    }

    HWND hForegroundWindow = GetForegroundWindow();
    return hForegroundWindow == hWorkerW || hForegroundWindow == hProgman;
}

對鼠標移動的消息,轉發鼠標在桌面上的鼠標移動消息。

LRESULT CALLBACK LowLevelMouseProc(int    nCode, WPARAM wParam, LPARAM lParam) {
    MSLLHOOKSTRUCT* p = (MSLLHOOKSTRUCT*)lParam;
    LONG lp = MAKELONG(p->pt.x, p->pt.y);

    if (wParam == WM_MOUSEMOVE) {
        RECT rect;
        GetWindowRect(GetForegroundWindow(), &rect);

        if (!PtInRect(&rect, p->pt)) {
            PostMessage(g_hWnd, (UINT)wParam, MK_XBUTTON1, lp);
        }
    }
    return CallNextHookEx(NULL, nCode, wParam, lParam);
}

完整的鼠標鈎子處理函數代碼

LRESULT CALLBACK LowLevelMouseProc(int    nCode, WPARAM wParam, LPARAM lParam) {
    MSLLHOOKSTRUCT* p = (MSLLHOOKSTRUCT*)lParam;
    LONG lp = MAKELONG(p->pt.x, p->pt.y);

    if (DS2_IsDesktop()) {
        if (wParam == WM_MOUSEMOVE) {
            PostMessage(g_hWnd, (UINT)wParam, MK_XBUTTON1, lp);
        }
        else  if (wParam == WM_LBUTTONDOWN || wParam == WM_LBUTTONUP) {
            PostMessage(g_hWnd, (UINT)wParam, MK_LBUTTON, lp);
        }
        else  if (wParam == WM_MOUSEWHEEL) {
            // TODO:
        }
    }
    else  if (wParam == WM_MOUSEMOVE) {
        RECT rect;
        GetWindowRect(GetForegroundWindow(), &rect);

        if (!PtInRect(&rect, p->pt)) {
            PostMessage(g_hWnd, (UINT)wParam, MK_XBUTTON1, lp);
        }
    }

    return CallNextHookEx(NULL, nCode, wParam, lParam);
}

鍵盤鈎子處理函數

鍵盤鈎子處理函數的 wParam 參數就是鍵盤消息類型,lParam 參數需要轉換成 KBDLLHOOKSTRUCT 結構體指針。KBDLLHOOKSTRUCT 結構體中用到的有 scanCode 字段和 vkCode 字段。鍵盤消息 WM_KEYDOWNWM_KEYUP 消息的 wParam 參數為 vkCode,lParam 參數的含義比較復雜。

WM_KEYDOWN 消息的 lParam 參數 bit 位說明

Bits 說明
0-15 當前消息的重復計數。
16-23 掃描代碼
24 指示該鍵是擴展鍵。如果它是擴展鍵則值為 1,否則為 0。
25-28 保留,不使用。
29 上下文代碼。對於 WM_KEYDOWN 消息該值始終為 0。
30 之前的鍵狀態。如果在發送消息之前鍵關閉則值為 1,如果鍵已啟動則值為 0。
31 轉換狀態。對於 WM_KEYDOWN 消息該值始終為 0。

WM_KEYUP 消息的 lParam 參數 bit 位說明

Bits 說明
0-15 當前消息的重復計數。對於 WM_KEYUP 消息,重復計數始終為1。
16-23 掃描代碼
24 指示該鍵是擴展鍵。如果它是擴展鍵則值為 1,否則為 0。
25-28 保留,不使用。
29 上下文代碼。對於 WM_KEYUP 消息該值始終為 0。
30 之前的鍵狀態。對於 WM_KEYUP 消息該值始終為 1。
31 轉換狀態。對於 WM_KEYUP 消息該值始終為 1。

鍵盤鈎子處理函數代碼

LRESULT CALLBACK LowLevelKeyboardProc(int    nCode, WPARAM wParam, LPARAM lParam) {
    if (DS2_IsDesktop()) {
        KBDLLHOOKSTRUCT* p = (KBDLLHOOKSTRUCT*)lParam;

        if (wParam == WM_KEYDOWN) {
            int lp = 1 | (p->scanCode << 16) | (1 << 24) | (0 << 29) | (0 << 30) | (0 << 31);
            PostMessage(g_hWnd, (UINT)wParam, p->vkCode, lp);
        }
        else if (wParam == WM_KEYUP) {
            int lp = 1 | (p->scanCode << 16) | (1 << 24) | (0 << 29) | (1 << 30) | (1 << 31);
            PostMessage(g_hWnd, (UINT)wParam, p->vkCode, lp);
        }
    }

    return CallNextHookEx(NULL, nCode, wParam, lParam);
}

所有代碼 https://github.com/he55/DreamScene2


看板娘使用方法 https://www.cnblogs.com/he55/p/15705047.html

寫在最后

下一步會增加 ffmpeg 視頻播放引擎


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM