Archives

Archives / 2011 / March
  • Detecting Idle Time with Global Mouse and Keyboard Hooks in WPF

    Years and years ago I wrote this blog post about detecting if the user was idle or active at the keyboard (and mouse) using a global hook. Well that code was for .NET 2.0 and Windows Forms and for some reason I wanted to try the same in WPF and noticed that a few things around the keyboard and mouse hooks didn’t work as expected in the WPF environment. So I had to change a few things and here’s the code for it, working in .NET 4.

    I took the liberty and refactored a few things while at it and here’s the code now. I’m sure I will need it in the far future as well.

    using System;
    using System.Diagnostics;
    using System.Runtime.InteropServices;
    
    namespace Irm.Tim.Snapper.Util
    {
        public class ClientIdleHandler : IDisposable
        {
            public bool IsActive { get; set; }
    
            int _hHookKbd;
            int _hHookMouse;
    
            public delegate int HookProc(int nCode, IntPtr wParam, IntPtr lParam);
            public event HookProc MouseHookProcedure;
            public event HookProc KbdHookProcedure;
    
            //Use this function to install thread-specific hook.
            [DllImport("user32.dll", CharSet = CharSet.Auto,
                 CallingConvention = CallingConvention.StdCall)]
            public static extern int SetWindowsHookEx(int idHook, HookProc lpfn,
                IntPtr hInstance, int threadId);
    
            //Call this function to uninstall the hook.
            [DllImport("user32.dll", CharSet = CharSet.Auto,
                 CallingConvention = CallingConvention.StdCall)]
            public static extern bool UnhookWindowsHookEx(int idHook);
    
            //Use this function to pass the hook information to next hook procedure in chain.
            [DllImport("user32.dll", CharSet = CharSet.Auto,
                 CallingConvention = CallingConvention.StdCall)]
            public static extern int CallNextHookEx(int idHook, int nCode,
                IntPtr wParam, IntPtr lParam);
    
            //Use this hook to get the module handle, needed for WPF environment
            [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
            public static extern IntPtr GetModuleHandle(string lpModuleName);
    
            public enum HookType : int
            {
                GlobalKeyboard = 13,
                GlobalMouse = 14
            }
    
            public int MouseHookProc(int nCode, IntPtr wParam, IntPtr lParam)
            {
                //user is active, at least with the mouse
                IsActive = true;
                Debug.Print("Mouse active");
    
                //just return the next hook
                return CallNextHookEx(_hHookMouse, nCode, wParam, lParam);
            }
    
            public int KbdHookProc(int nCode, IntPtr wParam, IntPtr lParam)
            {
                //user is active, at least with the keyboard
                IsActive = true;
                Debug.Print("Keyboard active");
    
                //just return the next hook
                return CallNextHookEx(_hHookKbd, nCode, wParam, lParam);
            }
    
            public void Start()
            {
                using (var currentProcess = Process.GetCurrentProcess())
                using (var mainModule = currentProcess.MainModule)
                {
    
                    if (_hHookMouse == 0)
                    {
                        // Create an instance of HookProc.
                        MouseHookProcedure = new HookProc(MouseHookProc);
                        // Create an instance of HookProc.
                        KbdHookProcedure = new HookProc(KbdHookProc);
    
                        //register a global hook
                        _hHookMouse = SetWindowsHookEx((int)HookType.GlobalMouse,
                                                      MouseHookProcedure,
                                                      GetModuleHandle(mainModule.ModuleName),
                                                      0);
                        if (_hHookMouse == 0)
                        {
                            Close();
                            throw new ApplicationException("SetWindowsHookEx() failed for the mouse");
                        }
                    }
    
                    if (_hHookKbd == 0)
                    {
                        //register a global hook
                        _hHookKbd = SetWindowsHookEx((int)HookType.GlobalKeyboard,
                                                    KbdHookProcedure,
                                                    GetModuleHandle(mainModule.ModuleName),
                                                    0);
                        if (_hHookKbd == 0)
                        {
                            Close();
                            throw new ApplicationException("SetWindowsHookEx() failed for the keyboard");
                        }
                    }
                }
            }
    
            public void Close()
            {
                if (_hHookMouse != 0)
                {
                    bool ret = UnhookWindowsHookEx(_hHookMouse);
                    if (ret == false)
                    {
                        throw new ApplicationException("UnhookWindowsHookEx() failed for the mouse");
                    }
                    _hHookMouse = 0;
                }
    
                if (_hHookKbd != 0)
                {
                    bool ret = UnhookWindowsHookEx(_hHookKbd);
                    if (ret == false)
                    {
                        throw new ApplicationException("UnhookWindowsHookEx() failed for the keyboard");
                    }
                    _hHookKbd = 0;
                }
            }
    
            #region IDisposable Members
    
            public void Dispose()
            {
                if (_hHookMouse != 0 || _hHookKbd != 0)
                    Close();
            }
    
            #endregion
        }
    }

    The way you use it is quite simple, for example in a WPF application with a simple Window and a TextBlock:

    <Window x:Class="WpfApplication2.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">
        <Grid>
            <TextBlock Name="IdleTextBox"/>
        </Grid>
    </Window>
    
    

    And in the code behind we wire up the ClientIdleHandler and a DispatcherTimer that ticks every second:

    public partial class MainWindow : Window
    {
        private DispatcherTimer _dispatcherTimer;
        private ClientIdleHandler _clientIdleHandler;
    
        public MainWindow()
        {
            InitializeComponent();
        }
    
        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            //start client idle hook
            _clientIdleHandler = new ClientIdleHandler();
            _clientIdleHandler.Start();
            
            //start timer
            _dispatcherTimer = new DispatcherTimer();
            _dispatcherTimer.Tick += TimerTick;
            _dispatcherTimer.Interval = new TimeSpan(0, 0, 0, 1);
            _dispatcherTimer.Start();
        }
    
        private void TimerTick(object sender, EventArgs e)
        {
            if (_clientIdleHandler.IsActive)
            {
                IdleTextBox.Text = "Active";
                //reset IsActive flag
                _clientIdleHandler.IsActive = false;    
            }
            else IdleTextBox.Text = "Idle";
        }
    }

    Remember to reset the ClientIdleHandle IsActive flag after a check.