![]() |
Desktop Development »
Dialogs and Windows »
General
Intermediate
License: The Code Project Open License (CPOL)
MS Spy++ style Window FinderBy Lim Bio LiongEver wondered how the cool Microsoft Spy++ Window Finder Tool is created ? Here is one possible implementation. |
VC6, MFC, Dev
|
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||

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.
![]()
The general principles can be summed up in the following pseudocode :
SetCapture.
WM_MOUSEMOVE.
GetCursorPos)HWND of the window beneath the mouse (WindowFromPoint)GetWindowRect, GetWindowDC,
SelectObject, Rectangle, ReleaseDC).
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.
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 :
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.
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.
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.
| You must Sign In to use this message board. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
General
News
Question
Answer
Joke
Rant
Admin
Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+PgUp/PgDown to switch pages.
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 9 Jul 2002 Editor: Nishant Sivakumar |
Copyright 2001 by Lim Bio Liong Everything else Copyright © CodeProject, 1999-2010 Web09 | Advertise on the Code Project |