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