Detecting Idle Time with Global Mouse and Keyboard Hooks in WPF

Posted by jdanforth on ASP.net Weblogs See other posts from ASP.net Weblogs or by jdanforth
Published on Sat, 19 Mar 2011 10:29:47 GMT Indexed on 2011/03/19 16:10 UTC
Read the original article Hit count: 575

Filed under:
|

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.

© ASP.net Weblogs or respective owner

Related posts about wpf

Related posts about .NET 4.0