C++ Q&A

Code for this article: Feb00CQA.exe (22KB)
Paul DiLascia is the author of Windows ++: Writing Reusable Code in C++ (Addison-Wesley, 1992) and a freelance consultant and writer-at-large. He can be reached at askpd@pobox.com or http://pobox.com/~askpd.

Q Is there a system call that determines how long a machine has been idle? Basically, I'm trying to figure out how an application based on Windows® 9x could determine that there has been no keyboard or mouse input for a given length of time.
Omar Naseef

A There is a system call, but it doesn't exist on Windows 9x. Windows 2000, on the other hand, provides a new function, GetLastInputInfo, that fills a LASTINPUTINFO struct with just the information you need:


 LASTINPUTINFO lpi;
 lpi.cbSize = sizeof(lpi);
 GetLastInputInfo(&lpi);
After calling GetLastInputInfo, lpi.dwTime contains the number of milliseconds since the last input event. This is just what you need to determine how long the system has been idle. Unfortunately, as I said, it's only available only on Windows 2000—not Windows 9x or Windows NT® 4.0. So what's a programmer to do?
   Where there's a will, there's a way—and in this case the way isn't too difficult, just a pain in the keyboard. All you have to do to implement your own version of GetLastInputInfo is write a systemwide hook. A hook is a callback function that Windows calls whenever something interesting happens, such as when the user types something. There's no such thing as a generic input hook, but there are mouse and keyboard hooks, and together they cover all the input bases. The most annoying thing about writing a systemwide (as opposed to a process-specific) hook is that it has to live in a DLL. Oh well.
   I wrote a little DLL called IdleUI that does just what you want. It's pretty easy to use. There are just three functions:

 BOOL  IdleUIInit()
 void  IdleUITerm();
 DWORD IdleUIGetLastInputTime();
As you might infer, IdleUIInit is the function you must call to initialize the whole shebang. IdleUITerm is the one to call when you're done. You can call these from your MFC app's InitInstance and ExitInstance functions. Once you have initialized IdleUI, you can call the third function, IdleUIGetLastInputTime, to get the tick count of the last input event—just like GetLastInputInfo.
   To test my DLL, I wrote a little app called TestIdleUI. It calls IdleUIInit and IdleUITerm as described earlier, and it has a paint handler that displays the number of seconds since the last input.

 void CMainFrame::OnPaint()
 {
   CPaintDC dc(this);
   CString s;
   DWORD nsec = (GetTickCount()-
     IdleUIGetLastInputTime())/1000;
   s.Format(
     "No mouse or keyboard input for %d seconds.",nsec);
   CRect rc;
   GetClientRect(&rc);
   dc.DrawText(s, &rc,   
     DT_CENTER|DT_VCENTER|DT_SINGLELINE);
 }
Figure 1 shows TestIdleUI running. To provide continuous display, TestIdleUI sets a one-second timer and refreshes the client area every time the timer pops.

 void CMainFrame::OnTimer(UINT)
 {
   Invalidate();
   UpdateWindow();
 }
What could be easier? You can run TestIdleUI and watch the seconds tick 0, 1, 2, 3, and so on as your computer does nothing. If you move the mouse or press a key, the count starts all over again from zero. I should have called it Sisyphus.
Figure 1 TestIdleUI
   Figure 1 TestIdleUI

   How does IdleUI work? When you call IdleUIInit, it installs two hooks: one for mouse input and one for keyboard input.

 HHOOK g_hHookKbd;
 HHOOK g_hHookMouse;
 g_hHookKbd = SetWindowsHookEx(WH_KEYBOARD,
                               MyKbdHook, 
                               hInst, 0);
 g_hHookMouse = SetWindowsHookEx(WH_MOUSE,
                                 MyMouseHook, 
                                 hInst, 0);
Now when the user moves the mouse or presses a key, Windows calls one of these hooks and the hook function records the time:

 LRESULT CALLBACK MyMouseHook(int code, 
                              WPARAM wp, 
                              LPARAM lp)
 {
   if (code==HC_ACTION) {
     // note the tick count
     g_dwLastInputTick = GetTickCount();
   }
   return ::CallNextHookEx(g_hHookMouse,
                           code, wp, lp);
 }
Ditto for MyKbdHook. IdleUIGetLastInputTime returns g_dwLastInputTick, and IdleUITerm uninstalls both hooks. What could be simpler?
   There's only one detail that even remotely counts as a trick. Normally, when you build a DLL the linker flags static data as nonshared, which means that each process that calls the DLL gets its own copy of the data—in this case g_hHookKbd, g_hHookMouse, and g_dwLastInputTick. But that would not create a happy state in Denmark because you want one and only one instance for these items for the entire process space/system/universe. To do that, you have to make the data shared. You do that by putting them in a special segment, then flagging the segment as shared. In code, it looks like this:

 #pragma data_seg (".IdleUI") // any name you like
 HHOOK g_hHookKbd = NULL;
 HHOOK g_hHookMouse = NULL;
 DWORD g_dwLastInputTick = 0;
 #pragma data_seg ()
This tells the linker to put the three variables in a data segment called .IdleUI (you can use any name you like). Then you have to add a line to IdleUI.def to make the segment shared:

 // in IdleUI.def
 SECTIONS .IdleUI READ WRITE SHARED
Figure 2 shows the final source code for IdleUI. Use it in good health.

Have a question about programming in C or C++? Send it to Paul DiLascia at askpd@pobox.com

From the February 2000 issue of Microsoft Systems Journal.
Get it at your local newsstand, or better yet,
subscribe.