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.

1 Comment

Comments have been disabled for this content.