4

My Question

How do I implement and use a low-level keyboard hook in Unity to disable windows shortcuts?

I would like to prevent the user being able to lose focus of my game through accidental use of the windows key. This is because my application is designed for toddlers who can press the keyboard quite randomly.

From searching stack overflow it seems that I need to implement a a low-level keyboard hook.

What I have tried

The below has been implemented in Unity. When pressing the print screen button it should turn the background color of my application black proving that I have implemented it correctly. However, when testing to see if I can capture keyboard inputs using this I have found that this block of code

        Debug.Log("Print Screen");
        Camera cam = FindObjectOfType<Camera>();
        cam.backgroundColor = Color.black; 

does not get called and the background colour does not change to black.

The code

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace SnagFree.TrayApp.Core
{
    class GlobalKeyboardHookEventArgs : HandledEventArgs
    {
        public GlobalKeyboardHook.KeyboardState KeyboardState { get; private set; }
        public GlobalKeyboardHook.LowLevelKeyboardInputEvent KeyboardData { get; private set; }

        public GlobalKeyboardHookEventArgs(
            GlobalKeyboardHook.LowLevelKeyboardInputEvent keyboardData,
            GlobalKeyboardHook.KeyboardState keyboardState)
        {
            KeyboardData = keyboardData;
            KeyboardState = keyboardState;
        }
    }

    //Based on https://gist.github.com/Stasonix
    class GlobalKeyboardHook : IDisposable
    {
        public event EventHandler<GlobalKeyboardHookEventArgs> KeyboardPressed;

        public GlobalKeyboardHook()
        {
            _windowsHookHandle = IntPtr.Zero;
            _user32LibraryHandle = IntPtr.Zero;
            _hookProc = LowLevelKeyboardProc; // we must keep alive _hookProc, because GC is not aware about SetWindowsHookEx behaviour.

            _user32LibraryHandle = LoadLibrary("User32");
            if (_user32LibraryHandle == IntPtr.Zero)
            {
                int errorCode = Marshal.GetLastWin32Error();
                throw new Win32Exception(errorCode, $"Failed to load library 'User32.dll'. Error {errorCode}: {new Win32Exception(Marshal.GetLastWin32Error()).Message}.");
            }



            _windowsHookHandle = SetWindowsHookEx(WH_KEYBOARD_LL, _hookProc, _user32LibraryHandle, 0);
            if (_windowsHookHandle == IntPtr.Zero)
            {
                int errorCode = Marshal.GetLastWin32Error();
                throw new Win32Exception(errorCode, $"Failed to adjust keyboard hooks for '{Process.GetCurrentProcess().ProcessName}'. Error {errorCode}: {new Win32Exception(Marshal.GetLastWin32Error()).Message}.");
            }
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                // because we can unhook only in the same thread, not in garbage collector thread
                if (_windowsHookHandle != IntPtr.Zero)
                {
                    if (!UnhookWindowsHookEx(_windowsHookHandle))
                    {
                        int errorCode = Marshal.GetLastWin32Error();
                        throw new Win32Exception(errorCode, $"Failed to remove keyboard hooks for '{Process.GetCurrentProcess().ProcessName}'. Error {errorCode}: {new Win32Exception(Marshal.GetLastWin32Error()).Message}.");
                    }
                    _windowsHookHandle = IntPtr.Zero;

                    // ReSharper disable once DelegateSubtraction
                    _hookProc -= LowLevelKeyboardProc;
                }
            }

            if (_user32LibraryHandle != IntPtr.Zero)
            {
                if (!FreeLibrary(_user32LibraryHandle)) // reduces reference to library by 1.
                {
                    int errorCode = Marshal.GetLastWin32Error();
                    throw new Win32Exception(errorCode, $"Failed to unload library 'User32.dll'. Error {errorCode}: {new Win32Exception(Marshal.GetLastWin32Error()).Message}.");
                }
                _user32LibraryHandle = IntPtr.Zero;
            }
        }

        ~GlobalKeyboardHook()
        {
            Dispose(false);
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        private IntPtr _windowsHookHandle;
        private IntPtr _user32LibraryHandle;
        private HookProc _hookProc;

        delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);

        [DllImport("kernel32.dll")]
        private static extern IntPtr LoadLibrary(string lpFileName);

        [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
        private static extern bool FreeLibrary(IntPtr hModule);

        /// <summary>
        /// The SetWindowsHookEx function 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.
        /// </summary>
        /// <param name="idHook">hook type</param>
        /// <param name="lpfn">hook procedure</param>
        /// <param name="hMod">handle to application instance</param>
        /// <param name="dwThreadId">thread identifier</param>
        /// <returns>If the function succeeds, the return value is the handle to the hook procedure.</returns>
        [DllImport("USER32", SetLastError = true)]
        static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, int dwThreadId);

        /// <summary>
        /// The UnhookWindowsHookEx function removes a hook procedure installed in a hook chain by the SetWindowsHookEx function.
        /// </summary>
        /// <param name="hhk">handle to hook procedure</param>
        /// <returns>If the function succeeds, the return value is true.</returns>
        [DllImport("USER32", SetLastError = true)]
        public static extern bool UnhookWindowsHookEx(IntPtr hHook);

        /// <summary>
        /// The CallNextHookEx function passes the hook information to the next hook procedure in the current hook chain.
        /// A hook procedure can call this function either before or after processing the hook information.
        /// </summary>
        /// <param name="hHook">handle to current hook</param>
        /// <param name="code">hook code passed to hook procedure</param>
        /// <param name="wParam">value passed to hook procedure</param>
        /// <param name="lParam">value passed to hook procedure</param>
        /// <returns>If the function succeeds, the return value is true.</returns>
        [DllImport("USER32", SetLastError = true)]
        static extern IntPtr CallNextHookEx(IntPtr hHook, int code, IntPtr wParam, IntPtr lParam);

        [StructLayout(LayoutKind.Sequential)]
        public struct LowLevelKeyboardInputEvent
        {
            /// <summary>
            /// A virtual-key code. The code must be a value in the range 1 to 254.
            /// </summary>
            public int VirtualCode;

            /// <summary>
            /// A hardware scan code for the key. 
            /// </summary>
            public int HardwareScanCode;

            /// <summary>
            /// The extended-key flag, event-injected Flags, context code, and transition-state flag. This member is specified as follows. An application can use the following values to test the keystroke Flags. Testing LLKHF_INJECTED (bit 4) will tell you whether the event was injected. If it was, then testing LLKHF_LOWER_IL_INJECTED (bit 1) will tell you whether or not the event was injected from a process running at lower integrity level.
            /// </summary>
            public int Flags;

            /// <summary>
            /// The time stamp stamp for this message, equivalent to what GetMessageTime would return for this message.
            /// </summary>
            public int TimeStamp;

            /// <summary>
            /// Additional information associated with the message. 
            /// </summary>
            public IntPtr AdditionalInformation;
        }

        public const int WH_KEYBOARD_LL = 13;
        //const int HC_ACTION = 0;

        public enum KeyboardState
        {
            KeyDown = 0x0100,
            KeyUp = 0x0101,
            SysKeyDown = 0x0104,
            SysKeyUp = 0x0105
        }

        public const int VkSnapshot = 0x2c;
        //const int VkLwin = 0x5b;
        //const int VkRwin = 0x5c;
        //const int VkTab = 0x09;
        //const int VkEscape = 0x18;
        //const int VkControl = 0x11;
        const int KfAltdown = 0x2000;
        public const int LlkhfAltdown = (KfAltdown >> 8);

        public IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam)
        {
            bool fEatKeyStroke = false;

            var wparamTyped = wParam.ToInt32();
            if (Enum.IsDefined(typeof(KeyboardState), wparamTyped))
            {
                object o = Marshal.PtrToStructure(lParam, typeof(LowLevelKeyboardInputEvent));
                LowLevelKeyboardInputEvent p = (LowLevelKeyboardInputEvent)o;

                var eventArguments = new GlobalKeyboardHookEventArgs(p, (KeyboardState)wparamTyped);

                EventHandler<GlobalKeyboardHookEventArgs> handler = KeyboardPressed;
                handler?.Invoke(this, eventArguments);

                fEatKeyStroke = eventArguments.Handled;
            }

            return fEatKeyStroke ? (IntPtr)1 : CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam);
        }
    }
}

Usage:

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;


namespace SnagFree.TrayApp.Core

{

internal class Controller : MonoBehaviour
{
    public bool printScreen = false;

    private GlobalKeyboardHook _globalKeyboardHook;

    public void SetupKeyboardHooks()
    {
        _globalKeyboardHook = new GlobalKeyboardHook();
        _globalKeyboardHook.KeyboardPressed += OnKeyPressed;
    }

    private void OnKeyPressed(object sender, GlobalKeyboardHookEventArgs e)
    {

        //Debug.WriteLine(e.KeyboardData.VirtualCode);

        if (e.KeyboardData.VirtualCode != GlobalKeyboardHook.VkSnapshot)
            return;

        // seems, not needed in the life.
        //if (e.KeyboardState == GlobalKeyboardHook.KeyboardState.SysKeyDown &&
        //    e.KeyboardData.Flags == GlobalKeyboardHook.LlkhfAltdown)
        //{
        //    MessageBox.Show("Alt + Print Screen");
        //    e.Handled = true;
        //}
        //else

        if (e.KeyboardState == GlobalKeyboardHook.KeyboardState.KeyDown)
        {

            e.Handled = true;
            Debug.Log("Print Screen");
            Camera cam = FindObjectOfType<Camera>();
            cam.backgroundColor = Color.black;
        }
    }

    public void Dispose()
    {
        _globalKeyboardHook?.Dispose();
    }
}
}
Jim
  • 761
  • 3
  • 10
  • 32

1 Answers1

6

to disable/discard or ignore low level Input you have to use Raw Input

Details here: https://learn.microsoft.com/en-us/windows/desktop/inputdev/raw-input

but that's not a good idea to use this in Unity!

Explanation why it is not a good Idea (Asked in comment):

With unity "onboard" tools you cannot disable/ignore key events like Windows key or other combinations because your OS will first process these events and AFTER this, it will pass those to your program like Unity / stand-alone player. To work with unsafe code (import W32Libs) in Unity you have to take care of what you are doing. No one prevents you from creating memory leaks. Also Unity is not designed to go this way, this can lead to unstable and very weird behavior on runtime (even in the Editor)

Also you will start annoying your users when you remove working behaviors from your OS/Environment that they use normally e.g. when the functionality "Open Task Manager" is inaccessible due to the keys were removed how would you close the App when it crashed? how can I switch to my Desktop.

This is why I wrote "but that's not a good idea to use this in Unity!"

You can but if you have to ask for a way to do that, you may not have the experience to use it without harm to your system or your users :)

PonWer
  • 87
  • 1
  • 7
Vampirasu
  • 202
  • 1
  • 12
  • 2
    `thats not a good idea to use this in Unity!` why not? And this an answer to the question than? – derHugo Jan 15 '19 at 06:31
  • due to the fact that my comment was to long i put it into the Answer – Vampirasu Jan 15 '19 at 07:33
  • I am aware of the issues during crashes, but on balance, less harm would be caused by disabling the keys – Jim Jan 15 '19 at 15:41
  • than you should go the way :) you could have a look at https://easyhook.github.io/ its a wrapper for raw input and makes stuff a little bit easier. The only downside is to add an extra DLL to your Unity Project/Plugin Folder ;) hope this helps – Vampirasu Jan 15 '19 at 16:48