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
-
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
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,
(LPCTSTR)MAKEINTRESOURCE
(IDD_DIALOG_SEARCH_WINDOW),
(HWND)hwndMain,
(DLGPROC)SearchWindowDialogProc
);
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 :
{
WORD wNotifyCode = HIWORD(wParam);
WORD wID = LOWORD(wParam);
HWND hwndCtl = (HWND)lParam;
if ((wID == IDOK) || (wID == IDCANCEL))
{
bRet = TRUE;
EndDialog (hwndDlg, wID);
}
if (wID == IDC_STATIC_ICON_FINDER_TOOL)
{
bRet = TRUE;
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;
g_bStartSearchWindow = TRUE;
SetFinderToolImage (hwndDialog, FALSE);
MoveCursorPositionToBullsEye (hwndDialog);
if (g_hCursorSearchWindow)
{
g_hCursorPrevious = SetCursor (g_hCursorSearchWindow);
}
else
{
g_hCursorPrevious = NULL;
}
SetCapture (hwndDialog);
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)
{
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;
GetCursorPos (&screenpoint);
wsprintf (szText, "%d", screenpoint.x);
SetDlgItemText (hwndDialog, IDC_STATIC_X_POS, szText);
wsprintf (szText, "%d", screenpoint.y);
SetDlgItemText (hwndDialog, IDC_STATIC_Y_POS, szText);
hwndFoundWindow = WindowFromPoint (screenpoint);
if (CheckWindowValidity (hwndDialog, hwndFoundWindow))
{
DisplayInfoOnFoundWindow (hwndDialog, hwndFoundWindow);
if (g_hwndFoundWindow)
{
RefreshWindow (g_hwndFoundWindow);
}
g_hwndFoundWindow = hwndFoundWindow;
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)
{
HDC hWindowDC = NULL;
HGDIOBJ hPrevPen = NULL;
HGDIOBJ hPrevBrush = NULL;
RECT rect;
long lRet = 0;
GetWindowRect (hwndFoundWindow, &rect);
hWindowDC = GetWindowDC (hwndFoundWindow);
if (hWindowDC)
{
hPrevPen = SelectObject (hWindowDC, g_hRectanglePen);
hPrevBrush = SelectObject (hWindowDC,
GetStockObject(HOLLOW_BRUSH));
Rectangle (hWindowDC, 0, 0,
rect.right - rect.left, rect.bottom - rect.top);
SelectObject (hWindowDC, hPrevPen);
SelectObject (hWindowDC, hPrevBrush);
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)
{
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 (g_hCursorPrevious)
{
SetCursor (g_hCursorPrevious);
}
if (g_hwndFoundWindow)
{
RefreshWindow (g_hwndFoundWindow);
}
SetFinderToolImage (hwndDialog, TRUE);
ReleaseCapture ();
ShowWindow (g_hwndMainWnd, SW_SHOWNORMAL);
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.