Logging the foreground process as it changes


Today's Little Program simply logs all changes to the foreground window by recording the path to the application the user switched to. You might use this as part of a usability study to monitor what applications users spend most of their time in.

Most of this code is just taking things we already know and snapping them together.

  1. Using accessibility to monitor events, specifically to monitor foreground changes.

  2. Get­Window­Thread­Process­Id to get the process ID from a window.

  3. Open­Process to get a handle to a process given the process ID.

  4. Query­Full­Process­ImageName to get the path to the application from the handle. (For Windows XP, you can use Get­Process­Image­File­Name.)

Take our scratch program and make these changes:

BOOL QueryWindowFullProcessImageName(
    HWND hwnd,
    DWORD dwFlags,
    PTSTR lpExeName,
    DWORD dwSize)
{
 DWORD pid = 0;
 BOOL fRc = FALSE;
 if (GetWindowThreadProcessId(hwnd, &pid)) {
  HANDLE hProcess = OpenProcess(
          PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
  if (hProcess) {
   fRc = QueryFullProcessImageName(
          hProcess, dwFlags, lpExeName, &dwSize);
   CloseHandle(hProcess);
  }
 }
 return fRc;
}

The Query­Window­Full­Process­Image­Name function is the meat of the program, performing steps 2 through 4 above.

Now we just hook this up in our event callback function. This should look really familiar, since we did pretty much the same thing earlier this year.

BOOL
OnCreate(HWND hwnd, LPCREATESTRUCT lpcs)
{
 g_hwndChild = CreateWindow(TEXT("listbox"), NULL,
     LBS_HASSTRINGS | WS_CHILD | WS_VISIBLE | WS_VSCROLL,
     0, 0, 0, 0, hwnd, NULL, g_hinst, 0);
 if (!g_hwndChild) return FALSE;
 return TRUE;
}

void CALLBACK WinEventProc(
    HWINEVENTHOOK hWinEventHook,
    DWORD event,
    HWND hwnd,
    LONG idObject,
    LONG idChild,
    DWORD dwEventThread,
    DWORD dwmsEventTime
)
{
 if (event == EVENT_SYSTEM_FOREGROUND &
     idObject == OBJID_WINDOW &&
     idChild == CHILDID_SELF)
 {
  PCTSTR pszMsg;
  TCHAR szBuf[MAX_PATH];
  if (hwnd) {
   DWORD cch = ARRAYSIZE(szBuf);
   if (QueryWindowFullProcessImageName(hwnd, 0,
                      szBuf, ARRAYSIZE(szBuf))) {
    pszMsg = szBuf;
   } else {
    pszMsg = TEXT("<unknown>");
   }
  } else {
   pszMsg = TEXT("<none>");
  }

  ListBox_AddString(g_hwndChild, pszMsg);
 }
}

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev,
                   LPSTR lpCmdLine, int nShowCmd)
{
 ...
  ShowWindow(hwnd, nShowCmd);

 HWINEVENTHOOK hWinEventHook = SetWinEventHook(
     EVENT_SYSTEM_FOREGROUND,
     EVENT_SYSTEM_FOREGROUND,
     NULL, WinEventProc, 0, 0,
     WINEVENT_OUTOFCONTEXT);

  while (GetMessage(&msg, NULL, 0, 0)) {
   TranslateMessage(&msg);
   DispatchMessage(&msg);
  }

  if (hWinEventHook) UnhookWinEvent(hWinEventHook);
...
}

The main program installs an accessibility hook for the EVENT_SYSTEM_FOREGROUND event, and each time the event fires, it extracts the process name and logs it to the screen. Since the notification is asynchronous, the foreground window may have been destroyed by the time the notification is received, so we have to be prepared for that case.

Comments (9)
  1. Joshua says:

    Hmmm I don't think csrss.exe is all that reasonable.

  2. Neil says:

    Not sure what QueryProcessFullImageName gets you that GetProcessImageFileName doesn't, apart from being newer and therefore "obviously" better.

    [A much more convenient output format. -Raymond]
  3. Rick C says:

    Neil, GPIFN gives you the path in "native" format (e.g., "DeviceHarddisk0Partition1WindowsSystem32Ctype.nls") while QPFIN gives you the option of getting the path in Windows format (e.g., "c:windowssystem32ctype.nls").

    You have to read both function's docs, though, to find that out.

  4. Nitpicker (Corner?) says:

    > Rick C

    > "native" format

    NT format.

    > Rick C

    > Windows format

    DOS format.

  5. Rick C says:

    @Nitpicker:  Go back and read the documentation.  I used MSDN's terms.  For QPFIM, a value of 1 in dwFlags means "The name should use the native system path format"; a 0 means "The name should use the Win32 path format."

  6. Nitpicker (Corner?) says:

    The "native system" is NT (6.3 now, isn't it?). The "Win32 path format" is borrowed, for backward compatibility, from DOS (if you gloss over the detail of LFNs).

    Remember, this is the same MSDN that seems not to know that there were ANY operating systems before Windows 2000. :)

  7. Rick C says:

    Nitpicker, thank you for your valuable contribution to this topic.

  8. Nitpicker (Corner?) says:

    Anytime, Rick. :)

    (more text to evade the spam blocker)

  9. ChrisR says:

    @Nitpicker:  Your opinion does not trump the documentation, no matter how many smileys you put in your posts.  If Microsoft wants to name the underlying kernel format "native" and the Win32 format "Win32", its up to them, not you.  Historical names don't matter either, otherwise we would all be using ridiculously old names for everything.

Comments are closed.

Skip to main content