Click here to Skip to main content
15,892,737 members
Articles / Desktop Programming / MFC
Article

HOWTO track a user's idle time

Rate me:
Please Sign up or sign in to vote.
4.70/5 (23 votes)
12 Nov 20014 min read 394.8K   4.1K   109   76
Track a user's idle time using global keyboard and mouse hooks.

Introduction

In general, when we talk about tracking a user's idle time, we are really after the time duration since the user last touched the mouse or keyboard of the system. Unfortunately, the Windows API does not provide us with an easy way of getting this value. However, we can roll our own using the Win32 hooks API.

The approach used here is really a simple one. We intercept the mouse and keyboard activities of the user by hooking into the OS's mouse and keyboard events using the API SetWindowsHookEx(). It is important to note that the hooks we are installing are system-wide. i.e. we receive notification even when our application does not have the focus. This is necessary since we are interested in system-wide user activities, not just in our own application. In these notifications (both keyboard and mouse), we update a common variable that stores the time when the event occurred. Therefore, to get the duration since the last user input, we simply compare the current time against this value.

The accompanying zip file contains the VC++ 6.0 project files and source code that implements this feature in a compact DLL. Also included are .lib and .dll files, which you may use directly in your applications.

The focus of this article is on how to track a user's input idle time using global hooks and how to use the accompanying DLL. If you want to find out more about the issues regarding the use and implementation of system-wide hooks and dlls, check out Joseph M. Newcomer's article.

DLL Usage

The DLL exports the following three functions:

BOOL IdleTrackerInit();    //start the monitoring process
void IdleTrackerTerm(); //stop the monitoring process
DWORD IdleTrackerGetLastTickCount(); //get the tick count of last user input

To start the monitoring process, call the function IdleTrackerInit(). The return value indicates if the mouse and keyboard hooks are installed successfully.

To stop the monitoring process, call the function IdleTrackerTerm(). This function will uninstall the mouse and keyboard hooks from the system.

To get the time duration since the last user input, just use the following piece of code. (Note that the times used are measured in milliseconds.)

UINT timeDuration = (UINT)(GetTickCount() - IdleTrackerGetLastTickCount());

And that is all to it!

DLL Innards Dissected

Data Variables

We maintain a set of variables in a shared data segment so that we have only one instance of each variable in all processes. The most important variable here is g_dwLastTick, which stores the time when the last user input event occurred. We are also storing the last known position of the mouse to filter off spurious mouse events. See Mouse Woes for more details.

#pragma data_seg(".IdleTracker")
HHOOK     g_hHkKeyboard = NULL;    // handle to the keyboard hook
HHOOK     g_hHkMouse = NULL;    // handle to the mouse hook
DWORD    g_dwLastTick = 0;    // tick time of last input event
LONG    g_mouseLocX = -1;    // x-location of mouse position
LONG    g_mouseLocY = -1;    // y-location of mouse position
#pragma data_seg()
#pragma comment(linker, "/section:.IdleTrac,rws")

DLL Initialization

The function IdleTrackerInit() simply initializes the variable g_dwLastTick to the current time and installs the global keyboard and mouse hooks to start the monitoring process.

__declspec(dllexport) BOOL IdleTrackerInit()
{
    if (g_hHkKeyboard == NULL) {
        g_hHkKeyboard = SetWindowsHookEx(WH_KEYBOARD, 
           KeyboardTracker, g_hInstance, 0);
    }
    if (g_hHkMouse == NULL) {
        g_hHkMouse = SetWindowsHookEx(WH_MOUSE, 
           MouseTracker, g_hInstance, 0);
    }

    _ASSERT(g_hHkKeyboard);
    _ASSERT(g_hHkMouse);

    g_dwLastTick = GetTickCount(); // init count

    if (!g_hHkKeyboard || !g_hHkMouse)
        return FALSE;
    else
        return TRUE;
}

DLL Termination

The function IdleTrackerTerm() does nothing more than just uninstalling the mouse and keyboard hooks to stop the monitoring process.

__declspec(dllexport) void IdleTrackerTerm()
{
    BOOL bResult;
    if (g_hHkKeyboard)
    {
        bResult = UnhookWindowsHookEx(g_hHkKeyboard);
        _ASSERT(bResult);
        g_hHkKeyboard = NULL;
    }
    if (g_hHkMouse)
    {
        bResult = UnhookWindowsHookEx(g_hHkMouse);
        _ASSERT(bResult);
        g_hHkMouse = NULL;
    }
}

Callback Functions

In the mouse and keyboard callbacks, we update the global variable g_dwLastTick with the latest tick count. But notice that in the mouse hook MouseTracker(), we update the tick count only if the mouse location has changed since the last time this method was called. This is really a hack solution to a problem that occurs on some systems. See Mouse Woes for more details on this problem.

/**
 * Keyboard hook: record tick count
 **/
LRESULT CALLBACK KeyboardTracker(int code, WPARAM wParam, LPARAM lParam)
{
    if (code==HC_ACTION) {
        g_dwLastTick = GetTickCount();
    }
    return ::CallNextHookEx(g_hHkKeyboard, code, wParam, lParam);
}

/**
 * Mouse hook: record tick count
 **/
LRESULT CALLBACK MouseTracker(int code, WPARAM wParam, LPARAM lParam)
{
    if (code==HC_ACTION) {
        MOUSEHOOKSTRUCT* pStruct = (MOUSEHOOKSTRUCT*)lParam;
        //we will assume that any mouse msg with 
        //the same locations as spurious
        if (pStruct->pt.x != g_mouseLocX || pStruct->pt.y != g_mouseLocY)
        {
            g_mouseLocX = pStruct->pt.x;
            g_mouseLocY = pStruct->pt.y;
            g_dwLastTick = GetTickCount();
        }
    }
    return ::CallNextHookEx(g_hHkMouse, code, wParam, lParam);
}

Mouse Woes

This DLL was used in an internet application that I developed some time back. It was used to trigger multimedia shows/movies whenever the user has been idle for X minutes. While beta-testing on some 30 odd PCs, we found a handful of them not kicking in after the stipulated X minutes. On further investigation, I found out that on these systems, the mouse callback mysteriously get triggered periodically even when the mouse was left untouched. It may have been triggered by the mouse (too sensitive? faulty?), the OS (9x and NT both had this problem) itself or some third-party software, I do not know.

In any case, it was unrealistic to expect my users to change their mouse, reinstall the OS, or uninstall the conflicting third-party software to fix this problem; It has to be fixed within my application. Hence I made the assumption that any subsequent mouse event that has the same location as the previous is spurious. Note that this assumption is rather overbearing as we are ignoring scenarios where the user is simply clicking the buttons on the mouse without moving it (should be rather seldom but nonetheless possible). Therefore, you will have to come up with your own fix if this assumption is not acceptable to you.

Conclusion

Well, that's it folks. Hope at least some of you out there will find this DLL useful. Please send feedback, bug reports or suggestions here.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
Singapore Singapore
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralRe: Mysterious periodically triggering!? Pin
wheregone22-Jun-03 21:57
wheregone22-Jun-03 21:57 
GeneralRe: Mysterious periodically triggering!? Pin
Lars-Inge Tønnessen16-Aug-03 10:17
Lars-Inge Tønnessen16-Aug-03 10:17 
GeneralChanges in code for Perl Win32::API to work properly with IdleTracker. Pin
Brian Gregg21-Jan-03 6:00
Brian Gregg21-Jan-03 6:00 
GeneralRe: Changes in code for Perl Win32::API to work properly with IdleTracker. Pin
Xhosa9995-Sep-03 1:07
Xhosa9995-Sep-03 1:07 
GeneralRe: Changes in code for Perl Win32::API to work properly with IdleTracker. Pin
Anonymous23-Mar-04 20:39
Anonymous23-Mar-04 20:39 
Generalmore troubles. Pin
Anonymous12-Dec-02 11:17
Anonymous12-Dec-02 11:17 
GeneralDirectInput Sample Code Header Pin
Anonymous28-Oct-02 6:38
Anonymous28-Oct-02 6:38 
GeneralDirect Input Sample Code Pin
Anonymous28-Oct-02 6:37
Anonymous28-Oct-02 6:37 
#include <windows.h>
#include <windowsx.h>
#include <assert.h>
#include "idleinputdll.h"


#define PRINTDEBUG 0

#if PRINTDEBUG
#include <stdio.h>
#endif

#define DIRECTINPUT_VERSION 0x0300 /* Remain DX3-compatible */
#include <dinput.h>

#define DINPUT_BUFFERSIZE 16


LPDIRECTINPUT g_pdi;
LPDIRECTINPUTDEVICE g_pMouse;
HANDLE g_hevtMouse;
LPDIRECTINPUTDEVICE g_pKeyboard;
HANDLE g_hevtKeyboard;
HANDLE g_hevts[2];


static void CDECL Complain(HWND hwndOwner, HRESULT hr, LPCSTR pszFormat, ...)
{
va_list ap;
char szBuf[1024];
char *pszBuf;

va_start(ap, pszFormat);
pszBuf = szBuf + wsprintf(szBuf, pszFormat, ap);
va_end(ap);
wsprintf(pszBuf, "\n\nError = %08x\n", hr);
#if PRINTDEBUG
printf("%s\n", pszBuf);
#else
OutputDebugString(pszBuf);
#endif
}



#define COOPERATIVE_LEVEL (DISCL_BACKGROUND | DISCL_NONEXCLUSIVE)


static BOOL setupMouseDevice(HWND hwnd)
{
HRESULT hr;

// Obtain an interface to the system mouse device.
hr = g_pdi->CreateDevice(GUID_SysMouse, &g_pMouse, NULL);
if (FAILED(hr)) {
Complain(hwnd, hr, "CreateDevice(SysMouse)");
return FALSE;
}

// Set data format. Must be done before mouse can be acquired.
// Note: c_dfDIMouse is an external DIDATAFORMAT structure supplied
// by DirectInput. Default values are:
// dwSize 24
// dwObjSize 16
// dwFlags 2 = DIDF_RELAXIS
// dwDataSize 16
// dwNumObjs 7 (3 axes and 4 buttons)
hr = g_pMouse->SetDataFormat(&c_dfDIMouse);
if (FAILED(hr)) {
Complain(hwnd, hr, "SetDataFormat(SysMouse, dfDIMouse)");
return FALSE;
}

hr = g_pMouse->SetCooperativeLevel(hwnd, COOPERATIVE_LEVEL);
if (FAILED(hr)) {
Complain(hwnd, hr, "SetCooperativeLevel(SysMouse)");
return FALSE;
}

// Create the handle that tells us new data is available.
g_hevtMouse = CreateEvent(0, 0, 0, 0);
if (g_hevtMouse == NULL) {
Complain(hwnd, GetLastError(), "CreateEvent");
return FALSE;
}

// Associate the event with the device.
hr = g_pMouse->SetEventNotification(g_hevtMouse);
if (FAILED(hr)) {
Complain(hwnd, hr, "SetEventNotification(SysMouse)");
return FALSE;
}

// Set the buffer size to DINPUT_BUFFERSIZE elements.
// The buffer size is a DWORD property associated with the device.
DIPROPDWORD dipdw =
{
{
sizeof(DIPROPDWORD), // diph.dwSize
sizeof(DIPROPHEADER), // diph.dwHeaderSize
0, // diph.dwObj
DIPH_DEVICE, // diph.dwHow
},
DINPUT_BUFFERSIZE, // dwData
};
hr = g_pMouse->SetProperty(DIPROP_BUFFERSIZE, &dipdw.diph);
if (FAILED(hr)) {
Complain(hwnd, hr, "Set buffer size(SysMouse)");
return FALSE;
}

hr = g_pMouse->Acquire();
if (FAILED(hr)) {
Complain(hwnd, hr, "g_pMouse->Acquire()");
return FALSE;
}

return TRUE;
}


static BOOL setupKeyboardDevice(HWND hwnd)
{
HRESULT hr;

// Obtain an interface to the system keyboard ndevice.
hr = g_pdi->CreateDevice(GUID_SysKeyboard, &g_pKeyboard, NULL);
if (FAILED(hr)) {
Complain(hwnd, hr, "CreateDevice(SysKeyboard)");
return FALSE;
}

// Set the data format to "keyboard format".
hr = g_pKeyboard->SetDataFormat(&c_dfDIKeyboard);
if (FAILED(hr)) {
Complain(hwnd, hr, "SetDataFormat(SysKeyboard, dfDIKeyboard)");
return FALSE;
}

hr = g_pKeyboard->SetCooperativeLevel(hwnd, COOPERATIVE_LEVEL);
if (FAILED(hr)) {
Complain(hwnd, hr, "SetCooperativeLevel(SysKeyboard)");
return FALSE;
}

// Create the handle that tells us new data is available.
g_hevtKeyboard = CreateEvent(0, 0, 0, 0);
if (g_hevtKeyboard == NULL) {
Complain(hwnd, GetLastError(), "CreateEvent");
return FALSE;
}

// Associate the event with the device.
hr = g_pKeyboard->SetEventNotification(g_hevtKeyboard);
if (FAILED(hr)) {
Complain(hwnd, hr, "SetEventNotification(SysKeyboard)");
return FALSE;
}

// Set the buffer size to DINPUT_BUFFERSIZE elements.
// The buffer size is a DWORD property associated with the device.
DIPROPDWORD dipdw =
{
{
sizeof(DIPROPDWORD), // diph.dwSize
sizeof(DIPROPHEADER), // diph.dwHeaderSize
0, // diph.dwObj
DIPH_DEVICE, // diph.dwHow
},
DINPUT_BUFFERSIZE, // dwData
};

hr = g_pKeyboard->SetProperty(DIPROP_BUFFERSIZE, &dipdw.diph);
if (FAILED(hr)) {
Complain(hwnd, hr, "Set buffer size(SysKeyboard)");
return FALSE;
}

hr = g_pKeyboard->Acquire();
if (FAILED(hr)) {
Complain(hwnd, hr, "g_pKeyboard->Acquire()");
return FALSE;
}

return TRUE;
}


IDLEINPUTDLL_API BOOL initDirectInput(HWND hwnd, HINSTANCE hinst)
{
HRESULT hr;

// Register with DirectInput and get an IDirectInput to play with.
hr = DirectInputCreate(hinst, DIRECTINPUT_VERSION, &g_pdi, NULL);

if (FAILED(hr)) {
Complain(hwnd, hr, "DirectInputCreate");
return FALSE;
}

if (!setupMouseDevice(hwnd))
return FALSE;

if (!setupKeyboardDevice(hwnd))
return FALSE;

g_hevts[0] = g_hevtMouse;
g_hevts[1] = g_hevtKeyboard;
return TRUE;
}


IDLEINPUTDLL_API void terminateDirectInput(void)
{

if (g_pdi) {
g_pdi->Release();
g_pdi = NULL;
}

/*
The IDirectInputDevice::Unacquire method releases access to the device.

HRESULT Unacquire();
The return value is DI_OK is the device was unacquired, or DI_NOEFFECT
if the device was not in an acquired state to begin with.
*/
if (g_pMouse) {
g_pMouse->Unacquire();
g_pMouse->Release();
g_pMouse = NULL;
}
if (g_hevtMouse) {
CloseHandle(g_hevtMouse);
g_hevtMouse = NULL;
}
if (g_pKeyboard) {
g_pKeyboard->Unacquire();
g_pKeyboard->Release();
g_pKeyboard = NULL;
}
if (g_hevtKeyboard) {
CloseHandle(g_hevtKeyboard);
g_hevtKeyboard = NULL;
}
}


IDLEINPUTDLL_API BOOL waitForInputActivity(DWORD dwMilliseconds)
{
DWORD dw = WaitForMultipleObjects(2, g_hevts, FALSE, dwMilliseconds);
// returns WAIT_OBJECT_0 + 0, WAIT_OBJECT_0 + 1, or WAIT_TIMEOUT
// printf("WAIT_OBJECT_0 + %d\n", dw - WAIT_OBJECT_0);
if (dw == WAIT_TIMEOUT) {
// this was a timeout
return false;
}
if (dw == WAIT_OBJECT_0 + 0) {
static int counter = 0;
// OutputDebugString("Input 0\n");
#if PRINTDEBUG
printf("# %d\n", counter++);
#endif
return true;
}
else {
assert(dw == WAIT_OBJECT_0 + 1);
static int counter = 0;
// OutputDebugString("Input 1\n");
#if PRINTDEBUG
printf("* %d\n", counter++);
#endif
return true;
}
}



IDLEINPUTDLL_API void waitForIdle(DWORD dwMilliseconds)
{
// dwMilliseconds should be between 1 second and 60 minutes
assert(500 <= dwMilliseconds && dwMilliseconds <= 60 * 60 * 1000);
while (1) {
// return if there was no activity for dwMilliseconds
if (waitForInputActivity(dwMilliseconds) == FALSE)
return;
}
}

GeneralRe: Direct Input Sample Code Pin
crystalus25-Feb-03 18:03
crystalus25-Feb-03 18:03 
GeneralRe: Direct Input Sample Code Pin
Michael B. Hansen12-Feb-04 8:09
Michael B. Hansen12-Feb-04 8:09 
Generalhook only works for my window :( Pin
Anonymous25-Oct-02 14:12
Anonymous25-Oct-02 14:12 
GeneralRe: hook only works for my window :( Pin
Jinhyuck Jung28-Oct-02 22:34
Jinhyuck Jung28-Oct-02 22:34 
GeneralRe: hook only works for my window :( Pin
Member 2681967-Nov-02 0:11
Member 2681967-Nov-02 0:11 
GeneralRe: hook only works for my window :( Pin
Yonatan23-Dec-02 21:43
Yonatan23-Dec-02 21:43 
GeneralHook appears to stop seeing activity with c# Pin
Member 26819620-Oct-02 22:09
Member 26819620-Oct-02 22:09 
GeneralRe: Hook appears to stop seeing activity with c# Pin
Anonymous22-Oct-02 4:23
Anonymous22-Oct-02 4:23 
GeneralRe: Hook appears to stop seeing activity with c# Pin
Beer2-Jan-03 9:54
Beer2-Jan-03 9:54 
GeneralRe: Hook appears to stop seeing activity with c# Pin
tom_waters5-Feb-03 12:42
tom_waters5-Feb-03 12:42 
GeneralRe: Hook appears to stop seeing activity with c# Pin
Beer5-Feb-03 12:46
Beer5-Feb-03 12:46 
GeneralRe: Hook appears to stop seeing activity with c# Pin
tom_waters5-Feb-03 13:00
tom_waters5-Feb-03 13:00 
GeneralRe: Hook appears to stop seeing activity with c# Pin
Will4922-Feb-04 19:39
Will4922-Feb-04 19:39 
GeneralRe: Hook appears to stop seeing activity with c# Pin
RickyC15-Jul-03 2:05
RickyC15-Jul-03 2:05 
GeneralRe: Hook appears to stop seeing activity with c# Pin
Anonymous28-Jul-03 10:08
Anonymous28-Jul-03 10:08 
GeneralDetect and log off idle user in visual basic application Pin
Ashishjan_in1-Sep-02 23:14
Ashishjan_in1-Sep-02 23:14 
QuestionIsn't there a bug ? Pin
13-Jun-02 21:10
suss13-Jun-02 21:10 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.