[.NET 2.0] Detecting Idle Time with Mouse and Keyboard Hooks
I thought I could share some piece of code I wrote a couple of years ago to detect if a user is idle or not. I used it on a chat client I developed because we were not allowed to use Messenger at the site. So the way to do it is to set up a coupld of low-level Windows hooks to detect mouse and keyboard activity. Obviously this involves a couple of DLL Import statements and you have to be careful to UnHook everything when the application exits as we're dealing with unmanaged code here. That's why I implemented the IDispose pattern for it.
This code is actually a piece of cake to implement if you use the ClientIdleHandler class (code below). First a look at the sample Windows Form which is made up of a lable indicating if the user is idle or active, and a timer set to 1 second. Each time the timer fires, it checks the bActive value in the ClientIdleHandler to see if the user has been active since the last check. I also added a progress bar on it (0 to 10) to give a visual indication on the idle seconds count.
Note: This code has not been tested in production systems and if you decide to use it, do that at your own risk. This code has been running pretty fine for the last 2 years, but on .NET 1.1. I don't know how well it behaves on .NET 2.0 even though it seems to work well. Also note that this code is not optimized in anyway and you may want to implement the static bActive variable different, perhaps raise events from the ClientIdleHandler class instead of using a timer like I do. Anyway, you have been warned :)
Form1.cs
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
namespace MouseAndKbdHookSample
{
public partial class Form1 : Form
{
private ClientIdleHandler clientIdleHandler = null;
private int iIdleCount = 0; //keeps track of how many seconds the user has been idle
private const int IDLE_THRESHOLD = 10; //how many seconds should pass before the app goes into idle mode
public Form1()
{
InitializeComponent();
//start client idle hook
clientIdleHandler = new ClientIdleHandler();
clientIdleHandler.Start();
}
private void timer1_Tick(object sender, EventArgs e)
{
if (ClientIdleHandler.bActive) //indicates user is active
{
//zero the idle counters
ClientIdleHandler.bActive = false;
iIdleCount = 0;
//change user status
if (IdleLabel.Text == "IDLE")
IdleLabel.Text = "ACTIVE";
}
else //user was idle the last second
{
//check if threshold was reached
if (iIdleCount >= IDLE_THRESHOLD)
{
//change to IDLE and stop counting seconds
if (IdleLabel.Text == "ACTIVE")
IdleLabel.Text = "IDLE";
}
else //increase secs counter
iIdleCount++;
}
//some visual goo added
progressBar1.Value = iIdleCount;
}
}
}
I added some code into the Dispose() method of the form, to help close the hooks, like this:
if (clientIdleHandler != null)
clientIdleHandler.Close();
Then there is this ClientIdleHandler class which does it all. I added enough comments in there I hope to explain what it does. If you want to read more on these hooks and how to use them from .NET there is always Google out there and a few KB articles on MSDN which helped me out. Like this one for example. Some people may recognize parts of the code in this class from a bunch of code sample that can be found on the Internet, still this kind of functionality is still asked for by people in forums.
ClientIdleHandler.cs
using System.Runtime.InteropServices;
namespace MouseAndKbdHookSample
{
/// <summary>
/// Write something here sometime...
public class ClientIdleHandler : IDisposable
{
//idle counter
public static bool bActive = false;
// hook active or not
static int hHookKbd = 0;
static int hHookMouse = 0;
// the Hook delegate
public delegate int HookProc(int nCode, IntPtr wParam, IntPtr lParam);
//Declare MouseHookProcedure as HookProc type.
public event HookProc MouseHookProcedure;
//Declare KbdHookProcedure as HookProc type.
public event HookProc KbdHookProcedure;
//Import for SetWindowsHookEx function.
//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);
//Import for UnhookWindowsHookEx.
//Call this function to uninstall the hook.
[DllImport("user32.dll", CharSet = CharSet.Auto,
CallingConvention = CallingConvention.StdCall)]
public static extern bool UnhookWindowsHookEx(int idHook);
//Import for CallNextHookEx
//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);
//Added all the hook types here just for reference
public enum HookType : int
{
WH_JOURNALRECORD = 0,
WH_JOURNALPLAYBACK = 1,
WH_KEYBOARD = 2,
WH_GETMESSAGE = 3,
WH_CALLWNDPROC = 4,
WH_CBT = 5,
WH_SYSMSGFILTER = 6,
WH_MOUSE = 7,
WH_HARDWARE = 8,
WH_DEBUG = 9,
WH_SHELL = 10,
WH_FOREGROUNDIDLE = 11,
WH_CALLWNDPROCRET = 12,
WH_KEYBOARD_LL = 13,
WH_MOUSE_LL = 14
}
static public int MouseHookProc(int nCode, IntPtr wParam, IntPtr lParam)
{
//user is active, at least with the mouse
bActive = true;
//just return the next hook
return CallNextHookEx(hHookMouse, nCode, wParam, lParam);
}
static public int KbdHookProc(int nCode, IntPtr wParam, IntPtr lParam)
{
//user is active, at least with the mouse
bActive = true;
//just return the next hook
return CallNextHookEx(hHookKbd, nCode, wParam, lParam);
}
public void Start()
{
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.WH_MOUSE_LL,
MouseHookProcedure,
(System.IntPtr)Marshal.GetHINSTANCE(
System.Reflection.Assembly.GetExecutingAssembly().GetModules()[0]),
0);
//If SetWindowsHookEx fails.
if (hHookMouse == 0)
{
Close();
throw new ApplicationException("SetWindowsHookEx() failed");
}
}
if (hHookKbd == 0)
{
//register a global hook
hHookKbd = SetWindowsHookEx((int)HookType.WH_KEYBOARD_LL,
KbdHookProcedure,
(System.IntPtr)Marshal.GetHINSTANCE(
System.Reflection.Assembly.GetExecutingAssembly().GetModules()[0]),
0);
//If SetWindowsHookEx fails.
if (hHookKbd == 0)
{
throw new ApplicationException("SetWindowsHookEx() failed");
}
}
}
public void Close()
{
if (hHookMouse != 0)
{
bool ret = UnhookWindowsHookEx(hHookMouse);
//If UnhookWindowsHookEx fails.
if (ret == false)
{
throw new ApplicationException("UnhookWindowsHookEx() failed");
}
hHookMouse = 0;
}
if (hHookKbd != 0)
{
bool ret = UnhookWindowsHookEx(hHookKbd);
//If UnhookWindowsHookEx fails.
if (ret == false)
{
throw new ApplicationException("UnhookWindowsHookEx() failed");
}
hHookKbd = 0;
}
}
public ClientIdleHandler()
{
//
// TODO: Add constructor logic here
//
}
#region IDisposable Members
public void Dispose()
{
if (hHookMouse != 0 || hHookKbd != 0)
Close();
}
#endregion
}
}
That's it folks! I hope it helps someone.