1

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_LL Windows 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?
20
  • 1
    What you ask for is an intrusive, privileged operation and thus requires privileged access. It can be done through Windows settings or registry changes. Are you sure the keyboard hook was installed? Did you get the UAC prompt? The code itself doesn't log whether the hook was installed or not, and writing to the console isn't enough - there may not be any console attached. Write to a log file at least. Although frankly, let the user decide whether they want to disable Win or not. Commented Aug 12 at 7:23
  • 1
    I don't like games blocking my Windows key, and if I really need this feature, I'll use a keyboard that has it. Besides, any form of hooking will hurt system performance. Commented Aug 12 at 7:37
  • 1
    While I broke a keyboard once for a game that crashed when i accidentally hit the windows key.. I too would find this behaviour intrusive and certainly a game would be removed for such behaviour - I can understand a more commercial sense making a replacement front end.. but still.. TBH i can see people flagging it as malicious if you do this Commented Aug 12 at 8:31
  • 2
    @PanagiotisKanavos That's entirely made up. Installing a low-level keyboard hook doesn't require any particular permissions. It is by no means "privileged". Any user is allowed to spy on themselves. The behavior reported sounds more like the game also (successfully) installs a low-level keyboard hook that either bypasses calling previous hooks or ignores their return values (and thus their request to filter). Commented Aug 12 at 10:39
  • 1
    It was meant as a note that you don't have to compile the hook procedure into a DLL (unlike other global hooks). The DLL doesn't get injected into other processes, so a simple EXE suffices. Commented Aug 12 at 16:06

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.