Detecting Idle Time with Global Mouse and Keyboard Hooks in WPF
- by jdanforth
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.