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
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.
- Run the demo application and select the "Search|Find Window" menu item. A Search Window dialog box will appear.
- My version of the Window Finder Utility works exactly the same way as the one in Microsoft Spy++.
- Move the mouse cursor inside the Finder Tool icon (the bulls eye).
- 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.
- The main frame window of the demo app will also be hidden.
This is to help in window selection.
- 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.
- 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
- Monitor all mouse movement by handling
- Whenever the mouse moves, get the screen position of the mouse (
- Get the
HWND of the window beneath the mouse (
- Highlight the window (
- When the user lifts up the left-mouse button, stop monitoring
mouse movements (
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.
This source contains the usual window startup code e.g.
MainWndProc, etc. However, I'd like to point out that
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.
The starting point of the Window Finder Tool is the
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
(IDD_DIALOG_SEARCH_WINDOW), // identifies dialog box template
(HWND)hwndMain, // handle to owner window
(DLGPROC)SearchWindowDialogProc // pointer to dialog box procedure
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
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
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.
The "bulls eye in a window" image is really a static
control (ID : IDC_STATIC_ICON_FINDER_TOOL) that is set with 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
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);
// Set the screen cursor to the BullsEye cursor.
g_hCursorPrevious = SetCursor (g_hCursorSearchWindow);
g_hCursorPrevious = NULL;
// Very important : capture all mouse
// activities from now onwards and
// direct all mouse messages to the "Search Window"
// dialog box procedure.
// Hide the main window.
ShowWindow (g_hwndMainWnd, SW_HIDE);
The first thing we do here is to set the global
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
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
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;
// Only when we have started the
// Window Searching operation will we
// track mouse movement.
DoMouseMove(hwndDlg, uMsg, wParam, lParam);
Note that we will perform action only when the global flag
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.
DoMouseMove function is listed below :
HWND hwndFoundWindow = NULL;
long lRet = 0;
// Must use GetCursorPos() instead of calculating
// from "lParam".
// 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.
// 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);
One important note is that the horizontal and vertical positions of the mouse
cannot be calculated from the
that is sent together with
WM_MOUSEMOVE. These values can be inaccurate
when the mouse is outside the dialog box. Instead, we use the
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
Next, we check for validity of the window handle returned from
This is encapsulated by the function
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,
FALSE will also be returned to avoid repetitions.
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
. Adventurous readers can experiment with this.
Next, if there was a previously highlighted window, we call on
of that window. This is done to remove the rectangular band surrounding the
borders of that window.
is not a Win32 API.
I have defined it myself. It calls on
three Win32 APIs
to perform a complete job of repainting the entire client and non-client area of a window.
We then set the global
to the new window returned from
Finally, we call the
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);
// 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,
// 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);
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
Finally, when the user lifts up the left mouse button, the
will be sent to
SearchWindowDialogProc and the handler for this message is displayed below :
case WM_LBUTTONUP :
bRet = TRUE;
// 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);
DoMouseUp function is called. Its code is listed below :
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 there was a found window, refresh
// it so that its highlighting is erased.
// 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.
// Make the main window appear normally.
ShowWindow (g_hwndMainWnd, SW_SHOWNORMAL);
// Set the global search window flag to FALSE.
g_bStartSearchWindow = FALSE;
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
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
API. We also unhide the main frame window and also set the search window flag
- 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 :
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.
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.