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();
void IdleTrackerTerm();
DWORD IdleTrackerGetLastTickCount();
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;
HHOOK g_hHkMouse = NULL;
DWORD g_dwLastTick = 0;
LONG g_mouseLocX = -1;
LONG g_mouseLocY = -1;
#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();
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.
LRESULT CALLBACK KeyboardTracker(int code, WPARAM wParam, LPARAM lParam)
{
if (code==HC_ACTION) {
g_dwLastTick = GetTickCount();
}
return ::CallNextHookEx(g_hHkKeyboard, code, wParam, lParam);
}
LRESULT CALLBACK MouseTracker(int code, WPARAM wParam, LPARAM lParam)
{
if (code==HC_ACTION) {
MOUSEHOOKSTRUCT* pStruct = (MOUSEHOOKSTRUCT*)lParam;
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);
}
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.