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