[.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;
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:

          //Dispose of our client idle handler
          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;
using System.Runtime.InteropServices;

namespace MouseAndKbdHookSample
{
    
/// <summary>
    /// Write something here sometime...
    /// </summary>
    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)
                {
                    Close();
                    
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.

No Comments