Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

MS Spy++ style Window Finder

0.00/5 (No votes)
9 Jul 2002 1  
Ever wondered how the cool Microsoft Spy++ Window Finder Tool is created ? Here is one possible implementation.

Introduction

Have you ever wondered how the Microsoft Engineers developed that cool Window Finder Tool inside Spy++ (see diagram below) ?

This utility (symbolized by the "bulls eye" icon) can seemingly be "dragged out" (with the mouse magically turning into the "bulls eye") and be used to highlight and select windows on the desktop. After a window has been selected, Spy++ does its stuff with spying on windows messages targeted at that window as well as providing various kinds of information on that window. After studying carefully some Win32 APIs and closely watching this Spy++ Window Searching Facility, I tried my hand at re-creating a similar Window Finder Utility of my own.

After a few days of coding and testing, I came up with one imitation (see diagram above). I'd like to share my code with readers out there, especially the newbies who may be wondering just how such a window selecting utility is implemented. The demo application and its source code aims to demonstrate window highlighting and selection via mouse tracking. The Window Search dialog box will be the focal point of our discussion and the main frame window serves only as a container of the dialog box. I have developed my sample using Win32 and avoided MFC. My intension is to demonstrate the raw principles. After learning the principles, readers can easily incorporate the source codes into MFC.

Usage

  1. Run the demo application and select the "Search|Find Window" menu item. A Search Window dialog box will appear.
  2. My version of the Window Finder Utility works exactly the same way as the one in Microsoft Spy++.
  3. Move the mouse cursor inside the Finder Tool icon (the bulls eye).
  4. Press and hold down the left-mouse button on the bulls eye icon. The mouse cursor will "disappear" and be changed into the bulls eye icon. Drag the mouse and the bulls eye will follow.
  5. The main frame window of the demo app will also be hidden. This is to help in window selection.
  6. From here on, move the bulls eye across any window on the screen and the window underneath the mouse will be highlighted by having its borders highlighted with a rectangular band. The screen position, rectangular dimensions and the Window Class Name of the selected window will also be displayed on the Search Window dialog box.
  7. The demo app will not perform any action on any selected window. The principles of window finding and highlighting via mouse tracking are explained in the following sections.

Summary Of How It Works

The general principles can be summed up in the following pseudocode :

  • Capture the mouse using SetCapture.
  • Monitor all mouse movement by handling WM_MOUSEMOVE.
  • Whenever the mouse moves, get the screen position of the mouse (GetCursorPos)
  • Get the HWND of the window beneath the mouse (WindowFromPoint)
  • Highlight the window (GetWindowRect, GetWindowDC, SelectObject, Rectangle, ReleaseDC).
  • When the user lifts up the left-mouse button, stop monitoring mouse movements (WM_LBUTTONUP, ReleaseCapture).

The other parts of the program (the bulls eye cursor and the special effects) are just cosmetics. Regular, fully documented Win32 APIs are used.

Detailed Explanation Of How It Works

The Window Finder Demo Application is composed of several source and resource files but I'd like to zoom-in on 4 of the main ones :

  • main.h - the header of the main module of the program.
  • main.cpp - the main running module.
  • WindowFinder.h - the header for WindowFinder.cpp.
  • WindowFinder.cpp - module that contains functions to capture and track mouse movement, determining the window underneath the mouse and displaying information on that window.

main.cpp

This source contains the usual window startup code e.g. WinMain, MainWndProc, etc. However, I'd like to point out that InitializeApplication contains code to disallow more that one instance of the demo app from running. This is done via a Mutex object. Spy programs usually need to perform its job exclusively with exactly one instance of it running in the system. Spy++, for instance, allows only one instance of itself. This is understandable since Spy++ will hook on windows messages streaming towards one particular window. What if another Spy++ starts up and spies on the same target window ? In theory, everything should work as per normal. But it is definitely prudent to limit the running of spy programs to only one instance. I have provided the sample code to demonstrate how this can be done. If you feel that it is not necessary, simply remove it.

WindowFinder.cpp

The starting point of the Window Finder Tool is the StartSearchWindowDialog function. This function will be invoked by the handlers of the "Search|Find Window" menu item of the Main Frame Window.

long StartSearchWindowDialog (HWND hwndMain)
{
    long lRet = 0;

    lRet = (long)DialogBox
        (
        (HINSTANCE)g_hInst, // handle to application instance 

        (LPCTSTR)MAKEINTRESOURCE
            (IDD_DIALOG_SEARCH_WINDOW), // identifies dialog box template 

        (HWND)hwndMain, // handle to owner window 

        (DLGPROC)SearchWindowDialogProc // pointer to dialog box procedure 

        );

    return lRet;
}

This routine launches the "Search Window" dialog box. The dialog box is a modal dialog box that will not return until the user clicks on the "OK" or "Cancel" button. Thereafter, the SearchWindowDialogProc dialog proc takes over the message processing for the dialog box. Besides the Window Finding facility, the dialog box is very simple in nature and will leave most messages to the default dialog proc provided by the system. The actions starts only when the user presses the left mouse button on the bulls eye image at which time the WM_COMMAND message will be sent to SearchWindowDialogProc :

case WM_COMMAND :
    {
        // notification code 

        WORD wNotifyCode = HIWORD(wParam); 

        // item, control, or accelerator identifier 

        WORD wID = LOWORD(wParam);        

        // handle of control 

        HWND hwndCtl = (HWND)lParam;      

        if ((wID == IDOK) || (wID == IDCANCEL))
        {
            bRet = TRUE;
            EndDialog (hwndDlg, wID);
        }

        if (wID == IDC_STATIC_ICON_FINDER_TOOL)
        {
            // Because the IDC_STATIC_ICON_FINDER_TOOL static 

            // control is set with the SS_NOTIFY

            // flag, the Search Window's dialog box will be 

            // sent a WM_COMMAND message when this 

            // static control is clicked.

            bRet = TRUE;

            // We start the window search operation by 

            // calling the DoSearchWindow() function.

            SearchWindow(hwndDlg);

            break;
        }

        break;
    }

The "bulls eye in a window" image is really a static control (ID : IDC_STATIC_ICON_FINDER_TOOL) that is set with the SS_BITMAP and the SS_NOTIFY flags. With the SS_NOTIFY flag, the Search Window's dialog box will be sent a WM_COMMAND message when the static control is clicked. The static control will display either the IDB_BITMAP_FINDER_FILLED bitmap (bitmap that shows a tiny window with the bulls eye image) or the IDB_BITMAP_FINDER_EMPTY bitmap (bitmap that shows the same tiny window without the bulls eye image). The function SetFinderToolImage controls this. We start the window finding operation by calling on the SearchWindow function which is described next. The SearchWindow function starts the window searching operation.

long SearchWindow (HWND hwndDialog)
{
    long lRet = 0;

    // Set the global "g_bStartSearchWindow" 

    // flag to TRUE.

    g_bStartSearchWindow = TRUE;

    // Display the empty window bitmap image 

    // in the Finder Tool static control.

    SetFinderToolImage (hwndDialog, FALSE);

    MoveCursorPositionToBullsEye (hwndDialog);

    // Set the screen cursor to the BullsEye cursor.

    if (g_hCursorSearchWindow)
    {
        g_hCursorPrevious = SetCursor (g_hCursorSearchWindow);
    }
    else
    {
        g_hCursorPrevious = NULL;
    }

    // Very important : capture all mouse 

    // activities from now onwards and

    // direct all mouse messages to the "Search Window" 

    // dialog box procedure.

    SetCapture (hwndDialog);

    // Hide the main window.

    ShowWindow (g_hwndMainWnd, SW_HIDE);

    return lRet;
}

The first thing we do here is to set the global BOOL flag g_bStartSearchWindow to TRUE to indicate that we are now in the middle of a window searching operation. Next comes the software magic show that gives the illusion of the mouse grabbing the bulls eye image and moving it out of its (bulls eye's) "tiny window". This is done by first changing the image on the static control from the IDB_BITMAP_FINDER_FILLED image to the IDB_BITMAP_FINDER_EMPTY image. We do this via the SetFinderToolImage function.  Next, we call on MoveCursorPositionToBullsEye to move the cursor position from wherever it currently is to the exact screen position of the bulls eye point on the static control :

Finally, we call the SetCursor API to change the cursor to the bulls eye cursor. These three actions give the smooth illusion that the user has somehow grabbed the bulls eye image from its tiny window and has transformed it to become a mouse cursor. How's that for some software "smoke-and-mirrors" ? Next comes the very important operation of capturing all mouse messages from now onwards and re-directing them all to the "Search Window" dialog box procedure. This is done by calling the SetCapture API. This will ensure that SearchWindowDialogProc can hook the WM_MOUSEMOVE and the WM_LBUTTONUP messages which will have special processing. More on these later. Lastly, we hide the main frame window for convenience. We will later unhide it when the window searching operation has completed.

Now that we have captured all mouse messages, when the mouse is moved (with the left-button down at the same time), WM_MOUSEMOVE will be sent to SearchWindowDialogProc and the following is the handler :

case WM_MOUSEMOVE :
    {
        bRet = TRUE;

        if (g_bStartSearchWindow)
        {
            // Only when we have started the 

            // Window Searching operation will we 

            // track mouse movement.

            DoMouseMove(hwndDlg, uMsg, wParam, lParam);
        }

        break;
    }

Note that we will perform action only when the global flag g_bStartSearchWindow is TRUE. We will call on DoMouseMove to process mouse movement. Now, because the "Search Window" dialog has now captured the mouse, all mouse movement will be monitored by SearchWindowDialogProc. This is regardless of whether the mouse is inside or outside the "Search Window" dialog.

The DoMouseMove function is listed below :

long DoMouseMove 
(
 HWND hwndDialog, 
 UINT message, 
 WPARAM wParam, 
 LPARAM lParam
)
{
    POINT       screenpoint;
    HWND        hwndFoundWindow = NULL;
    char        szText[256];
    long        lRet = 0;

    // Must use GetCursorPos() instead of calculating 

    // from "lParam".

    GetCursorPos (&screenpoint);  

    // Display global positioning in the dialog box.

    wsprintf (szText, "%d", screenpoint.x);
    SetDlgItemText (hwndDialog, IDC_STATIC_X_POS, szText);

    wsprintf (szText, "%d", screenpoint.y);
    SetDlgItemText (hwndDialog, IDC_STATIC_Y_POS, szText);

    // Determine the window that lies underneath 

    // the mouse cursor.

    hwndFoundWindow = WindowFromPoint (screenpoint);

    // Check first for validity.

    if (CheckWindowValidity (hwndDialog, hwndFoundWindow))
    {
        // We have just found a new window.


        // Display some information on this found window.

        DisplayInfoOnFoundWindow (hwndDialog, hwndFoundWindow);

        // If there was a previously found window, we must 

        // instruct it to refresh itself. 

        // This is done to remove any highlighting 

        // effects drawn by us.

        if (g_hwndFoundWindow)
        {
            RefreshWindow (g_hwndFoundWindow);
        }

        // Indicate that this found window is now 

        // the current global found window.

        g_hwndFoundWindow = hwndFoundWindow;

        // We now highlight the found window.

        HighlightFoundWindow (hwndDialog, g_hwndFoundWindow);
    }

    return lRet;
}

One important note is that the horizontal and vertical positions of the mouse cannot be calculated from the lParam that is sent together with WM_MOUSEMOVE. These values can be inaccurate when the mouse is outside the dialog box. Instead, we use the GetCursorPos API to capture the screen position of the mouse. We next determine the window that lies underneath the mouse cursor. This is done by the WindowFromPoint API. Next, we check for validity of the window handle returned from WindowFromPoint. This is encapsulated by the function CheckWindowValidity. CheckWindowValidity checks a HWND to see if it is actually the "Search Window" dialog's or main window's own window or one of their child windows. If so a FALSE will be returned so that these windows will not be selected. Also, this routine checks to see if the HWND to be checked is already a currently found (highlighted) window. If so, a FALSE will also be returned to avoid repetitions.

When a HWND returned from WindowFromPoint is found to be valid, we first call DisplayInfoOnFoundWindow to display some information about the window. I have kept this function short and bland but all kinds of sophisticated data can be retrieved and displayed with a HWND. Adventurous readers can experiment with this. Next, if there was a previously highlighted window, we call on RefreshWindow on the HWND of that window. This is done to remove the rectangular band surrounding the borders of that window. RefreshWindow is not a Win32 API. I have defined it myself. It calls on three Win32 APIs InvalidateRect, UpdateWindow and RedrawWindow to perform a complete job of repainting the entire client and non-client area of a window. We then set the global HWND g_hwndFoundWindow to the new window returned from WindowFromPoint. Finally, we call the HighlightFoundWindow function on g_hwndFoundWindow to highlight the new window by drawing a rectangular band around its borders.

The code for HighlightFoundWindow is listed below.

long HighlightFoundWindow (HWND hwndDialog, HWND hwndFoundWindow)
{
    // The DC of the found window.

    HDC     hWindowDC = NULL;  

    // Handle of the existing pen in the DC of the found window.

    HGDIOBJ hPrevPen = NULL;   

    // Handle of the existing brush in the DC of the found window.

    HGDIOBJ hPrevBrush = NULL; 

    RECT        rect; // Rectangle area of the found window.

    long        lRet = 0;

    // Get the screen coordinates of the rectangle 

    // of the found window.

    GetWindowRect (hwndFoundWindow, &rect);

    // Get the window DC of the found window.

    hWindowDC = GetWindowDC (hwndFoundWindow);

    if (hWindowDC)
    {
        // Select our created pen into the DC and 

        // backup the previous pen.

        hPrevPen = SelectObject (hWindowDC, g_hRectanglePen);

        // Select a transparent brush into the DC and 

        // backup the previous brush.

        hPrevBrush = SelectObject (hWindowDC, 
            GetStockObject(HOLLOW_BRUSH));

        // Draw a rectangle in the DC covering 

        // the entire window area of the found window.

        Rectangle (hWindowDC, 0, 0, 
            rect.right - rect.left, rect.bottom - rect.top);

        // Reinsert the previous pen and brush 

        // into the found window's DC.

        SelectObject (hWindowDC, hPrevPen);

        SelectObject (hWindowDC, hPrevBrush);

        // Finally release the DC.

        ReleaseDC (hwndFoundWindow, hWindowDC);
    }

    return lRet;
}

We first get the screen dimensions of the window to be highlighted. Next we get the full window DC of the window to be highlight. We use the GetWindowDC API instead of the GetDC API because the GetWindowDC API will retrieve the device context (DC) for the entire window, including title bar, menus, and scroll bars. This is important for us because we may have to highlight top-level windows and dialogs that contain title bars. Our rectangular band must surround the entire window and not just the client area of such windows. We then select our own created pen and a transparent brush into the window DC of the target window and then draw a rectangle around the borders of the window. It is important to then restore the previous pen and brush into the window DC and also to release the DC (via ReleaseDC). Finally, when the user lifts up the left mouse button, the WM_LBUTTONUP message will be sent to SearchWindowDialogProc and the handler for this message is displayed below :

case WM_LBUTTONUP :
    {
        bRet = TRUE;

        if (g_bStartSearchWindow)
        {
            // Only when we have started the 

            // window searching operation will we

            // be interested when the user lifts 

            // up the left mouse button.

            DoMouseUp(hwndDlg, uMsg, wParam, lParam);
        }

        break;
    }

Now the DoMouseUp function is called. Its code is listed below :

long DoMouseUp
(
 HWND hwndDialog, 
 UINT message, 
 WPARAM wParam, 
 LPARAM lParam
)
{
    long lRet = 0;

    // If we had a previous cursor, set the 

    // screen cursor to the previous one.

    // The cursor is to stay exactly where it 

    // is currently located when the 

    // left mouse button is lifted.

    if (g_hCursorPrevious)
    {
        SetCursor (g_hCursorPrevious);
    }

    // If there was a found window, refresh 

    // it so that its highlighting is erased. 

    if (g_hwndFoundWindow)
    {
        RefreshWindow (g_hwndFoundWindow);
    }

    // Set the bitmap on the Finder Tool icon 

    // to be the bitmap with the bullseye bitmap.

    SetFinderToolImage (hwndDialog, TRUE);

    // Very important : must release the mouse capture.

    ReleaseCapture ();

    // Make the main window appear normally.

    ShowWindow (g_hwndMainWnd, SW_SHOWNORMAL);

    // Set the global search window flag to FALSE.

    g_bStartSearchWindow = FALSE;

    return lRet;
}

We set the screen cursor to the previous one that was in use before we changed the cursor. However, the cursor position is to remain unchanged and so the cursor itself will stay exactly where it is currently located when the left mouse button is lifted. This is the same behaviour as in Microsoft's Spy++. We must also not forget to RefreshWindow any existing found window to remove any rectangular band highlight. We then call SetFinderToolImage to restore the image on the static control to the "bulls eye in a window" bitmap. We then release the mouse capture by calling the ReleaseCapture API. We also unhide the main frame window and also set the search window flag g_bStartSearchWindow to FALSE.

Updates

  • v1 :- Monday January 7th 2002.
  • v1.1 :- Let me say a very big thanks to all the readers who have sent me very supportive comments and have given very good ratings to this article. It's very encouraging. Thanks !
  • v1.2 :- I have submitted another article : WindowFloater - A System Tray Utility To Make A Window Float To The Top that gives a good demonstration of an effective use of the WindowFinder utility. I hope it will be of benefit to all.

Conclusion

And that's it. A complete re-creation of the Microsoft Spy++ Window Finder Tool. I plan to write another article in the near future to make practical use of my Window Finder. I hope that the sample code will be of benefit to all readers. It is said that "imitation is the sincerest form of flattery". I do have great esteem for the Microsoft Engineers and I hope that my demo app is not a cheap imitation but an effective one that demonstrates how things can be done using regular and fully documented Win32 APIs.

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