Sunday, December 12, 2010

Global Hotkeys and User Activity Detection

Back in July I posted about a job where I have to be able to detect user input (or absence of it) through keyboard or mouse.  In addition, how do you go about setting up global OS level hot keys?

Detecting Keyboard and Mouse Input
The main ingredient is the Win32 function

[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr SetWindowsHookEx(
        int idHook,
        LowLevelInputProc lpfn,
        IntPtr hMod,
        uint dwThreadId);

This function is used to hook into many Windows events.  The first parameter is the type of hook you would like to "hook" into. The term hook is used basically to describe subscribing to a Windows event.
The second parameter Lpfn is the callback delegate Windows should invoke when an event occurs. The third is your application's module handle, and finally the last is the thread id in your application Windows should use to invoke the callback.

To set the hook, is reasonably straight forward. I've written a wrapper method to simplify the invocation of the Win32 call:


private static IntPtr SetHook(LowLevelInputProc proc, int hookTypeConstant)
        {
            using (Process curProcess = Process.GetCurrentProcess())
            {
                using (ProcessModule curModule = curProcess.MainModule)
                {
                    if (curModule == null)
                    {
                        return IntPtr.Zero;
                    }

                    return SetWindowsHookEx(
                        hookTypeConstant,
                        proc,
                        GetModuleHandle(curModule.ModuleName),
                        0);
                }
            }
        }

The two hook types I am interested in are Keyboard keypress events (int WH_KEYBOARD_LL = 13) and Mouse keypress events (int WH_MOUSE_LL = 14).  The callback has the signature:


private delegate IntPtr LowLevelInputProc(int nCode, IntPtr wParam, IntPtr lParam);

The three parameters are then used to give you information about the event and vary between different hook types.

My end goal is to be able to wrap these two hooks into a reusable class. The idea is to make a static class that has standard .NET events that are easier to subscribe to and deal with.


namespace KeyboardHookConsole
{
    using System;
    using System.Diagnostics;
    using System.Diagnostics.CodeAnalysis;
    using System.Runtime.InteropServices;
    using System.Windows.Forms;

    public static class InputDeviceInterceptor
    {
        [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Reviewed. Maps to Win32.")]
        private const int WH_KEYBOARD_LL = 13;

        [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Reviewed. Win32.")]
        private const int WH_MOUSE_LL = 14;

        [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1310:FieldNamesMustNotContainUnderscore", Justification = "Reviewed. Maps to Win32.")]
        private const int WM_KEYDOWN = 0x0100;

        private static IntPtr keyboardHookId = IntPtr.Zero;

        private static IntPtr mouseHookId = IntPtr.Zero;

        private delegate IntPtr LowLevelInputProc(int nCode, IntPtr wParam, IntPtr lParam);

        private static readonly LowLevelInputProc KeyboardCallback = KeyboardHookCallback;

        private static readonly LowLevelInputProc MouseCallback = MouseHookCallback;

        public static event EventHandler<InterceptedKeyEventArgs> KeyboardInput;

        public static event EventHandler<InterceptedMouseEventArgs> MouseInput;

        private enum MouseMessages
        {
            WM_LBUTTONDOWN = 0x0201,
            WM_LBUTTONUP = 0x0202,
            WM_MOUSEMOVE = 0x0200,
            WM_MOUSEWHEEL = 0x020A,
            WM_RBUTTONDOWN = 0x0204,
            WM_RBUTTONUP = 0x0205
        }

        public static void SetHook()
        {
            keyboardHookId = SetHook(KeyboardCallback, WH_KEYBOARD_LL);
            mouseHookId = SetHook(MouseCallback, WH_MOUSE_LL);
        }

        public static void Unhook()
        {
            UnhookWindowsHookEx(keyboardHookId);
            UnhookWindowsHookEx(mouseHookId);
        }

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr CallNextHookEx(
            IntPtr hhk,
            int nCode,
            IntPtr wParam,
            IntPtr lParam);

        [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr GetModuleHandle(string lpModuleName);

        private static IntPtr KeyboardHookCallback(int nCode, IntPtr wParam, IntPtr lParam)
        {
            if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN)
            {
                int code = Marshal.ReadInt32(lParam);
                var handler = KeyboardInput;
                if (handler != null)
                {
                    handler(null, new InterceptedKeyEventArgs((Keys)code));
                    //// handler(null, new InterceptedKeyEventArgs());
                }
            }

            return CallNextHookEx(keyboardHookId, nCode, wParam, lParam);
        }

        private static IntPtr MouseHookCallback(int nCode, IntPtr wParam, IntPtr lParam)
        {
            // MouseButtonState left = MouseButtonState.Released, right = MouseButtonState.Released;
            // var point = new Point();

            bool raiseEvent = false;
            // var hookStruct = (MSLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(MSLLHOOKSTRUCT));

            if (nCode >= 0)
            {
                if (MouseMessages.WM_LBUTTONDOWN == (MouseMessages)wParam)
                {
                    // left = MouseButtonState.Pressed;
                    raiseEvent = true;
                }

                if (MouseMessages.WM_RBUTTONDOWN == (MouseMessages)wParam)
                {
                    // right = MouseButtonState.Pressed;
                    raiseEvent = true;
                }

                if (MouseMessages.WM_MOUSEMOVE == (MouseMessages)wParam)
                {
                    raiseEvent = true;
                    // point.X = hookStruct.pt.x;
                    // point.Y = hookStruct.pt.y;
                }

                if (raiseEvent)
                {
                    var handler = MouseInput;
                    if (handler != null)
                    {
                        // handler(null, new InterceptedMouseEventArgs(left, right, point));
                        handler(null, new InterceptedMouseEventArgs());
                    }
                }
            }

            return CallNextHookEx(mouseHookId, nCode, wParam, lParam);
        }

        private static IntPtr SetHook(LowLevelInputProc proc, int hookTypeConstant)
        {
            using (Process curProcess = Process.GetCurrentProcess())
            {
                using (ProcessModule curModule = curProcess.MainModule)
                {
                    if (curModule == null)
                    {
                        return IntPtr.Zero;
                    }

                    return SetWindowsHookEx(
                        hookTypeConstant,
                        proc,
                        GetModuleHandle(curModule.ModuleName),
                        0);
                }
            }
        }

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr SetWindowsHookEx(
            int idHook,
            LowLevelInputProc lpfn,
            IntPtr hMod,
            uint dwThreadId);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool UnhookWindowsHookEx(IntPtr hhk);

        [StructLayout(LayoutKind.Sequential)]
        private struct POINT
        {
            public int x;
            public int y;
        }
        [StructLayout(LayoutKind.Sequential)]
        private struct MSLLHOOKSTRUCT
        {
            public POINT pt;
            public uint mouseData;
            public uint flags;
            public uint time;
            public IntPtr dwExtraInfo;
        }
    }
}

Here's a sample console application that tests this class.

namespace ConsoleApplication1
{
    using System;
    using System.Windows.Forms;
    using KeyboardHookConsole;

    public static class Program
    {
        public static void Main()
        {
            InputDeviceInterceptor.SetHook();
            InputDeviceInterceptor.KeyboardInput += (s, e) => Console.WriteLine(e.Key.ToString());

            InputDeviceInterceptor.MouseInput += (s, e) => Console.WriteLine("Mouse moved");

            Application.Run();

            InputDeviceInterceptor.Unhook();
        }
    }
}



Finally more close to my ultimate end goal, here's a WPF application with a check box to indicate if the user has recently typed input, or moved their mouse:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <StackPanel>
        <StackPanel Orientation="Horizontal">
            <TextBlock Text="Are you present?" />
            <CheckBox IsChecked="{Binding Present}" />
        </StackPanel>
    </StackPanel>
</Window>

namespace WpfApplication1
{
    using System;
    using System.ComponentModel;
    using System.Threading.Tasks;
    using System.Windows;
    using System.Windows.Threading;
    using KeyboardHookConsole;

    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : INotifyPropertyChanged
    {
        private readonly DispatcherTimer awayTimer = new DispatcherTimer();
        private bool present;

        public MainWindow()
        {
            InitializeComponent();
            DataContext = this;
            Loaded += OnLoaded;
            Closing += OnClosing;
        }

        public event PropertyChangedEventHandler PropertyChanged;

        public bool Present
        {
            get
            {
                return this.present;
            }

            set 
            { 
                if (value)
                {
                    this.awayTimer.Start();
                }

                this.present = value;
                PropertyChanged(this, new PropertyChangedEventArgs("Present"));
            }
        }

        private void OnAwayTimerTick(object sender, EventArgs e)
        {
            Present = false;
            this.awayTimer.Stop();
        }

        private void OnClosing(object sender, CancelEventArgs e)
        {
            InputDeviceInterceptor.KeyboardInput -= OnInterceptorKeyboardInput;
            InputDeviceInterceptor.MouseInput -= OnInterceptorMouseInput;
            InputDeviceInterceptor.Unhook();
        }

        private void OnInterceptorKeyboardInput(object sender, InterceptedKeyEventArgs e)
        {
            // Toggle the user presence property
            Task.Factory.StartNew(() =>
                                  {
                                      if (!Present)
                                      {
                                          Dispatcher.BeginInvoke(new Action(() => Present = true));
                                      }
                                  });
        }

        private void OnInterceptorMouseInput(object sender, InterceptedMouseEventArgs e)
        {
            Task.Factory.StartNew(() =>
            {
                if (!Present)
                {
                    Dispatcher.BeginInvoke(new Action(() => Present = true));
                }
            });
        }

        private void OnLoaded(object sender, RoutedEventArgs e)
        {
            InputDeviceInterceptor.SetHook();
            InputDeviceInterceptor.KeyboardInput += OnInterceptorKeyboardInput;
            InputDeviceInterceptor.MouseInput += OnInterceptorMouseInput;
            this.awayTimer.Interval = new TimeSpan(0, 0, 5);
            this.awayTimer.Tick += OnAwayTimerTick;
            this.awayTimer.Start();
        }
    }
}

Setting Hot Keys will follow.

No comments:

Post a Comment