介绍

老板键是一种热键或热键组合,用于快速隐藏游戏或其他无关工作的程式,并让显示器呈现正常工作时的画面,藉以欺瞒老板和同事等,达到保护您隐私的目的,使之以为上班时间进行娱乐的员工在做自己份内的工作。

需求

Unity Windows 页游需要增加老板键,基本需求:

  • 全局快捷键显示隐藏游戏窗口
  • 支持自定义快捷键

环境

  • Unity 5.6.6f2
  • Windows 7
  • Visual Studio 2019

编译 DLL

参考 Unity 官方文档以及网上质量较高的博客文章,了解如何编译 DLL。

窗口操作

显示隐藏窗口

通过下面两个方法就可以获得 Unity 窗口并进行显示与隐藏。

设置窗口焦点

在从后台恢复窗口后,必须让游戏窗口获得焦点,这样游戏才能响应鼠标键盘操作。

兼容不同情况

使用下面这一整套调用来兼容各种奇怪的情况

1
2
3
4
5
6
7
// restore the minimize window
SendMessage(hwnd, WM_SYSCOMMAND, SC_RESTORE, 0);
SetForegroundWindow(hwnd); 
SetActiveWindow(hwnd); 
SetWindowPos(hwnd, HWND_TOP, 0, 0, 0, 0, SWP_SHOWWINDOW | SWP_NOMOVE  | SWP_NOSIZE);
// redraw to prevent the window blank.
RedrawWindow(hwnd, NULL, 0, RDW_FRAME | RDW_INVALIDATE | RDW_ALLCHILDREN );

全局快捷键

原理

找到以下介绍全局快捷键的文章,了解了大概的原理

核心原理非常简单,只需要注册快捷键消息,然后在消息循环中处理快捷键消息就可以了。

例子

Visual Studio 2019 打开、升级、编译、运行,当焦点不在窗口时也可以正确响应全局快捷键。

核心代码 GlobalHotkeyTest/main.cpp at master · efevans/GlobalHotkeyTest · GitHubRegisterHotKey function (winuser.h) - Win32 apps | Microsoft Docs 的例子代码一样。

消息注入

由于必须要使用消息循环来处理快捷键事件,第一时间想到的方法是使用类似钩子的方法注入到 Unity 窗口的消息循环中,截获快捷键 WM_HOTKEY 消息进行处理。

Installs an application-defined hook procedure into a hook chain. You would install a hook procedure to monitor the system for certain types of events. These events are associated either with a specific thread or with all threads in the same desktop as the calling thread.

参考这个回答编写钩子

必须使用 GetWindowLongPtr 与 GWLP_HINSTANCE 来同时支持 x86 与 x86_64

1
2
3
HWND unityWindow = GetActiveWindow();
HINSTANCE hInstance = (HINSTANCE)GetWindowLongPtr(unityWindow, GWLP_HINSTANCE);
hGetMsgHook = SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc, hInstance, 0);

SetWindowsHookEx 注册时提示以下错误,需要使用 DLL Attach 时传入的 HMODULE

ERROR_HOOK_NEEDS_HMOD 1428 (0x594) Cannot set nonlocal hook without a module handle.

但实际测试退出后发现 SetWindowsHookEx 会将 DLL 注入到所有的进程中,并不是需要的结果,需要尝试其他方案。

自定义消息循环

尝试开启线程,在线程中创建窗口并执行消息循环,捕获消息处理。

使用下面博客文章的方案成功

错误(活动) E0167 “const wchar_t *” 类型的实参与 “wchar_t *” 类型的形参不兼容

调用方法 RegisterDLLWindowClass((wchar_t* )L"XXXX"); 时对参数进行强制类型转换

自定义按键

这里可以使用 UGUI 的 InputField 在获得焦点时在 Update 方法中处理自定义按键显示。

按键映射

需要将 Unity 的 KeyCode 转换为 win32 的 Virtual Key Codes 与显示用的字符串,这里需要使用查找替换以及文本编辑器的列编辑生成最后需要的字典

有几个坑:

  1. Unity 中 Ctrl Alt Shift 分左右两个按键,但是注册全局快捷键时并不区分
  2. 编辑器下已占用 Ctrl+Shift+数字键Ctrl+Alt+数字键 等一堆快捷键,测试时需要注意是否冲突
  3. Input.GetKeyDown 只能获取按下那一帧,需要改用 Input.GetKey
  4. Input.anyKeyDown 会响应鼠标按键,因此编写逻辑时需要额外处理

注册按键

直接调用 RegisterHotKey 方法只会将事件注册当调用的线程,通过 GetLastError 拿到错误码查到错误:

ERROR_WINDOW_OF_OTHER_THREAD 1408 (0x580) Invalid window; it belongs to other thread.

这是因为 Unity 主线程与 DLL 创建的事件线程不是同一个,因此需要将快捷键绑定通过事件的方式传给线程,在线程事件响应函数中再注册快捷键。

使用给线程发消息的 API 无效

PostThreadMessage(bossKeyThreadId, WM_USER, fsModifiers, keyUnique);

最终使用给窗口发消息的 API 成功

PostMessage(bossKeyHWND, WM_USER, fsModifiers, keyUnique);

检测冲突

要想调用,必须在 Unity 主线程才可以,否则可能会崩溃:

It is technically possible but your C++ plugin must run in the engines thread, otherwise it will crash if the callback you try to call accesses anything from the unity engine. Unity is not thread safe and you must not access its “space” from another thread.

这里可以改为 Unity 侧主动查询 C++ 侧。

核心代码

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
#include "pch.h"
#include "winuser.h"

#define WM_REGISTER_HOTKEY WM_USER
#define WM_UNREGISTER_HOTKEY WM_USER + 1

HINSTANCE inj_hModule; //Injected Modules Handle
HWND prnt_hWnd; //Parent Window Handle
HWND bossKeyHWND = 0;
BOOL isVisible = TRUE;
BOOL hasHotKey = FALSE;
int iKeyID = 0x53C8;

__declspec(dllexport) void __stdcall TriggerDLLAttachToInitialize()
{
    // Call this function to trigger DLL_PROCESS_ATTACH in dllmain.cpp
    // and Initialize thread window & event loop
}

extern "C" __declspec(dllexport) BOOL __stdcall RegisterGlobalHotKey(int keyCtrl, int keyAlt, int keyShift, int keyUnique)
{
    UINT fsModifiers = MOD_NOREPEAT;
    fsModifiers |= keyCtrl > 0 ? MOD_CONTROL : 0;
    fsModifiers |= keyAlt > 0 ? MOD_ALT : 0;
    fsModifiers |= keyShift > 0 ? MOD_SHIFT : 0;

    PostMessage(bossKeyHWND, WM_REGISTER_HOTKEY, fsModifiers, keyUnique);

    return FALSE;
}

extern "C" __declspec(dllexport) BOOL __stdcall UnregisterGlobalHotKey()
{
    PostMessage(bossKeyHWND, WM_UNREGISTER_HOTKEY, 0, 0);

    return FALSE;
}

DWORD WINAPI ThreadProc(LPVOID lpParam);

void Initialize(HMODULE hModule)
{
    inj_hModule = hModule;
    prnt_hWnd = GetActiveWindow();
    CreateThread(0, NULL, ThreadProc, (LPVOID)L"Game HotKey Window", NULL, NULL);
}

void Terminate()
{
    if (bossKeyHWND == NULL || !hasHotKey)
    {
        return;
    }

    UnregisterGlobalHotKey();
}

//WndProc for the new window
LRESULT CALLBACK DLLWindowProc(HWND, UINT, WPARAM, LPARAM);

//Register our windows Class
BOOL RegisterDLLWindowClass(wchar_t szClassName[])
{
    WNDCLASSEX wc;
    wc.hInstance = inj_hModule;
    wc.lpszClassName = (LPCWSTR)L"HotKeyDLLWindowClass";
    wc.lpszClassName = (LPCWSTR)szClassName;
    wc.lpfnWndProc = DLLWindowProc;
    wc.style = CS_DBLCLKS;
    wc.cbSize = sizeof(WNDCLASSEX);
    wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.lpszMenuName = NULL;
    wc.cbClsExtra = 0;
    wc.cbWndExtra = 0;
    wc.hbrBackground = (HBRUSH)COLOR_BACKGROUND;
    if (!RegisterClassEx(&wc))
        return 0;

    return 1;
}

//The new thread
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
    MSG messages;
    wchar_t* pString = reinterpret_cast<wchar_t*> (lpParam);
    RegisterDLLWindowClass((wchar_t* )L"HotKeyDLLWindowClass");
    bossKeyHWND = CreateWindowEx(0, L"HotKeyDLLWindowClass", pString, WS_EX_PALETTEWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 400, 300, prnt_hWnd, NULL, inj_hModule, NULL);
    ShowWindow(bossKeyHWND, SW_HIDE);

    while (GetMessage(&messages, NULL, 0, 0))
    {
        TranslateMessage(&messages);
        DispatchMessage(&messages);
    }

    return 1;
}

//Our new windows proc
LRESULT CALLBACK DLLWindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_REGISTER_HOTKEY:
        if (RegisterHotKey(bossKeyHWND, iKeyID, (UINT)wParam, (int)lParam))
        {
            hasHotKey = TRUE;
        }
        else
        {
            hasHotKey = FALSE;
        }
        break;
    case WM_UNREGISTER_HOTKEY:
        UnregisterHotKey(bossKeyHWND, iKeyID);
        hasHotKey = FALSE;
        break;
    case WM_HOTKEY:
        if (isVisible)
        {
            isVisible = FALSE;
            ShowWindow(prnt_hWnd, SW_HIDE);
        }
        else
        {
            isVisible = TRUE;
            ShowWindow(prnt_hWnd, SW_SHOW);
            SetForegroundWindow(prnt_hWnd);
            SetActiveWindow(prnt_hWnd);
            SetWindowPos(prnt_hWnd, HWND_TOP, 0, 0, 0, 0, SWP_SHOWWINDOW | SWP_NOMOVE | SWP_NOSIZE);
            //redraw to prevent the window blank.
            RedrawWindow(prnt_hWnd, NULL, 0, RDW_FRAME | RDW_INVALIDATE | RDW_ALLCHILDREN);
        }
        break;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hwnd, message, wParam, lParam);
    }
    return 0;
}

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        Initialize(hModule);
        break;
    case DLL_THREAD_ATTACH:
        break;
    case DLL_THREAD_DETACH:
        break;
    case DLL_PROCESS_DETACH:
        Terminate();
        break;
    }
    return TRUE;
}

参考资料