C# 編寫 Windows 動態桌面軟體實現(一)之桌面互動功能

he55發表於2021-12-28

DreamScene2 1.3 版本已經發布了,現在支援滑鼠和桌面互動功能。這個功能不會影響效能,基本不佔用 CPU。這個功能讓我對 Windows 訊息機制有了更深入的理解,在這篇部落格中我會詳細介紹實現方式。

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

實現原理

使用 WIN32 API SetWindowsHookEx 函式 Hook 滑鼠鍵盤訊息,在鉤子處理函式中處理捕獲滑鼠鍵盤訊息然後呼叫 PostMessage 函式向動態桌面視窗傳送轉發訊息。

設定滑鼠和鍵盤鉤子

函式的第一個引數是鉤子型別,Hook 滑鼠訊息可以傳 WH_MOUSE_LL,Hook 鍵盤訊息可以傳 WH_KEYBOARD_LL。第二個引數是自定義的鉤子訊息處理函式地址。函式的第三個引數是鉤子函式所在的模組控制程式碼,當鉤子型別是 WH_MOUSE_LL 或者 WH_KEYBOARD_LL 時,可以直接傳當前模組控制程式碼。函式的第四個引數是執行緒 Id,傳 NULL 捕獲所有訊息。

設定 Hook 程式碼。儲存 SetWindowsHookEx 函式返回值,解除安裝 Hook 時需要

HHOOK g_hLowLevelMouseHook = NULL;
HHOOK g_hLowLevelKeyboardHook = NULL;

HMODULE hModule = GetModuleHandle(NULL);
g_hLowLevelMouseHook = SetWindowsHookEx(WH_MOUSE_LL, LowLevelMouseProc, hModule, NULL);
g_hLowLevelKeyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, hModule, NULL);

解除安裝 Hook 程式碼

UnhookWindowsHookEx(g_hLowLevelMouseHook);
UnhookWindowsHookEx(g_hLowLevelKeyboardHook);

編寫鉤子處理函式

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 視訊播放引擎

相關文章