Before we start, yes, this solution works but only outside Unity.
Intent
I'm trying to prevent the Windows key from interrupting my game. I wrote a DLL that registers a WH_KEYBOARD_LL Windows hook (LowLevelKeyboardProc) and in this hook I return 1 when the key pressed is the Windows key. I then expose functions to initialize and shut down this hook. Finally, I call the functions from my game.
Problem
The Windows key is intercepted but only when the game does not have the focus. When the game is in the foreground, the Windows key works as usual (opens the Start menu). This is the opposite of what I want.
Updates
- 2025-08-14: As noted by @IInspectable, it is quite possible that Unity in its great wisdom decided to return zero in its own
WH_KEYBOARD_LLWindows hook, which would explain the result. I have yet to confirm this. And if this is the case, I don't have a workaround. Suggestions?
Code
I created the following sample game and library to demonstrate this. You can download both here: https://drive.google.com/file/d/1whWoH3uWSnGJNuco1F6o4uNUQhwdwAj2/view?usp=sharing
Tested with Unity versions 6000.1.15f1 and 2023.2.1f1.
The build is documented in BUILD.md, and there's a nice build script (b.sh). Here are the central functions:
Library (C++)
static bool IsWinKey(DWORD vk)
{
return vk == VK_LWIN || vk == VK_RWIN;
}
static LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
if (nCode == HC_ACTION && (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN))
{
KBDLLHOOKSTRUCT *p = (KBDLLHOOKSTRUCT *)lParam;
DWORD vkCode = p->vkCode;
if (IsWinKey(vkCode))
{
std::cout << "Blocked Win key " << std::endl;
// Call the Unity callback if it's set
if (g_blockedKeyCallback != nullptr)
{
g_blockedKeyCallback(p->vkCode);
}
return 1; // Block the key
}
}
return CallNextHookEx(g_hKeyboardHook, nCode, wParam, lParam); //Allow key
}
static BOOL InstallKeyboardHook()
{
if (g_hKeyboardHook != NULL)
return TRUE;
g_hKeyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, g_hDllModule, 0);
return g_hKeyboardHook != NULL;
}
static void UninstallKeyboardHook()
{
if (g_hKeyboardHook)
{
UnhookWindowsHookEx(g_hKeyboardHook);
g_hKeyboardHook = NULL;
}
}
extern "C" NOWIN_API BOOL InitNoWin(BlockedKeyCallback callback)
{
static std::ofstream logFile("nowinlib.log", std::ios::app);
std::cout.rdbuf(logFile.rdbuf());
std::cout << "NoWinLib initializing." << std::endl;
g_blockedKeyCallback = callback;
return InstallKeyboardHook() ? TRUE : FALSE;
}
extern "C" NOWIN_API BOOL ShutdownNoWin()
{
UninstallKeyboardHook();
// Clear the callback on shutdown
g_blockedKeyCallback = nullptr;
std::cout << "KeybNoWinLib shut down." << std::endl;
return TRUE;
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
g_hDllModule = hModule; // Store the DLL handle
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
// No action needed for thread attach/detach
break;
case DLL_PROCESS_DETACH:
ShutdownNoWin(); // Clean up when the DLL is unloaded
break;
}
return TRUE;
}
Game (Unity, C#)
public class NoWinTest : MonoBehaviour
{
[DllImport("NoWinLib")]
private static extern bool InitNoWin(BlockedKeyCallback callback);
[DllImport("NoWinLib")]
private static extern bool ShutdownNoWin();
public delegate void BlockedKeyCallback(uint vkCode);
[MonoPInvokeCallback(typeof(BlockedKeyCallback))]
private static void OnKeyBlocked(uint vkCode)
{
Debug.Log($"Key blocked! VK Code: 0x{vkCode:X}");
}
void Start()
{
InitNoWin(OnKeyBlocked);
}
void Update()
{
// Check if Escape key is pressed and exit the game
if (Input.GetKeyDown(KeyCode.Escape))
{
Application.Quit();
}
}
void OnDestroy()
{
ShutdownNoWin();
}
I have no idea why this is not working. I swear I thought I made it work but then I made a change (added the new Input System 1.7.0) and after it stopped working. Even after I rolled back my last change, it just would not work again. What is most mysterious is that the Windows key is only blocked when the game is in the background. How is this even possible?
Questions
- Exactly why does this not work?
- How do I fix it?