Introduction
This article demonstrates yet another effective use of the WindowFinder utility that I presented in my CodeProject article early in January this year
It also incorporates the screen capture routine presented by Joseph M. Newcomer in his CodeProject article
The demo app is a Sys Tray utility that activates a context menu when doubled-clicked. Using the demo app, you are able to capture any window or control anywhere on the screen and save the screen-captured bitmap into the clipboard. After that, you'll be able to paste the bitmap from the clipboard onto any bitmap editing tool of your choice.
Although my main intention is to give a demonstration of one possible use of the Window Finder utility, such a screen capture utility is pretty useful all on its own. Although you can always use the "PrintScreen" button or the "Alt+PrintScreen" buttons, this utility allows you to precisely select windows and controls. Thus saving you the trouble of having to perform further bitmap area selection and cutting and pasting.
Usage
- Start the demo app and the Window Snapshot icon will appear in the System Tray area :
- Right-click on the icon and a context menu will appear. There will be 3 menu items :
- Take A Snapshot - this option will invoke the "Search Window" dialog box which will allow you to select a window or control to take a screenshot of.
- About - displays an about message box.
- Shutdown - shuts down the entire Sys Tray app. [Note that if the Search Window Dialog Box is already running, right-mouse-clicking on the icon will not bring up the context menu.]
- You can also double-click on the icon to directly invoke the Search Window Dialog box. If the dialog box is already running, it is brought to the foreground.
- Once a window is selected, its information will be displayed in the Search Window Dialog Box and once you click on the "OK" button, a screenshot of the selected window or control will be taken and saved into the system clipboard.
- Thereafter, use your favourite bitmap editing tool to further edit or save the bitmap.
Summary Of How It Works
Please read through my "MS Spy++ Window Finder" article to understand how the window finder part of this demo app works. I have made extensive re-use of the source codes of the WindowFinder app. The following is a summary of the enhancements and transformations that were made to the original WindowFinder source codes :
- From Standard Window App to System Tray App. This requires :
- Adding a Sys Tray icon.
- Hiding the main window of the app.
- Defining a custom Window message for processing Sys Tray activities.
- Defining a context menu to be activated when the user right-clicks on the Sys Tray icon.
- Defining handlers for the context menu items.
- Screen Capturing the selected Window/Control.
- After a window has been selected and the user selects the "OK" button of the Search Window Dialog Box, we proceed to take a snapshot of the window or control.
- A slightly modified version of Joseph Newcomer's
toClipboard()
routine is used to perform the screen capture.
- I made the modifications to Joseph's
toClipboard()
routine to handle some possible resource leakage detected by Numega's Bounds Checker while I was doing debugging.
- I have informed Joseph of the possible leakage and Joseph has kindly informed me he will look into this.
Detailed Explanation Of How It Works
In order to make a System Tray app, I have removed the
WS_VISIBLE
style from the main window of the application and explicitly hidden it after creation :
dwStyle = WS_OVERLAPPEDWINDOW;
g_hwndMainWnd = CreateWindow
(
szMainWindowClassName,
WINDOW_SNAPSHOT_MAIN_WINDOW_TITLE,
dwStyle,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
NULL,
NULL,
hInstance,
NULL
);
...
...
...
ShowWindow(g_hwndMainWnd, SW_HIDE);
UpdateWindow(g_hwndMainWnd);
The main window will still be required to process messages for the Sys Tray icon.
I have also defined a new function InitialiseShellModules()
which is called within WinMain()
. This function will perform initializations for the Sys Tray icon. This function is listed below :
BOOL InitialiseShellModules()
{
NOTIFYICONDATA nid;
BOOL bRetTemp = FALSE;
BOOL bRet = TRUE;
g_hIconSysTray = (HICON)LoadImage
(
(HINSTANCE)g_hInst,
(LPCTSTR)MAKEINTRESOURCE(IDI_ICON_SYS_TRAY),
(UINT)IMAGE_ICON,
(int)16,
(int)16,
(UINT)0
);
memset (&nid, 0, sizeof(NOTIFYICONDATA));
nid.cbSize = sizeof(NOTIFYICONDATA);
nid.hWnd = g_hwndMainWnd;
nid.uID = ICON_ID;
nid.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP;
nid.hIcon = g_hIconSysTray;
nid.uCallbackMessage = WM_SYS_TRAY_MESSAGE;
strcpy (nid.szTip, WINDOW_SNAPSHOT_TOOL_TIP);
bRetTemp = Shell_NotifyIcon
(
(DWORD)NIM_ADD,
(PNOTIFYICONDATA)&nid
);
g_dwLastError = GetLastError ();
return bRet;
}
The function contains basic sys tray initialization and startup code. The counterpart of InitialiseShellModules()
is UninitialiseShellModules()
which will basically unregister our icon from the System Tray.
As can be seen in InitialiseShellModules()
, we have indicated that the main application window g_hwndMainWnd
is to receive the WM_SYS_TRAY_MESSAGE
message meant for our icon. Hence, we provide a WM_SYS_TRAY_MESSAGE
message handler in MainWndProc()
:
case WM_SYS_TRAY_MESSAGE:
{
lRet = 0;
bCallDefWndProc = TRUE;
if (wParam == ICON_ID)
{
if (lParam == WM_RBUTTONDOWN)
{
if (g_bAllowContextMenu)
{
DisplayContextMenu (hwnd);
}
}
if (lParam == WM_LBUTTONDBLCLK)
{
if (g_hwndSearchDialog)
{
SetForegroundWindow (g_hwndSearchDialog);
}
else
{
PostMessage (hwnd, WM_START_SEARCH_WINDOW, 0, 0);
}
}
}
break;
}
When the user right-clicks on the icon, we first check the BOOL
flag g_bAllowContextMenu
to see if we allow the display of the context menu. This is important because we do not want to allow the user the possibility of having two Search Window Dialogs running at the same time. The g_bAllowContextMenu
flag is set to TRUE
when the Search Window Dialog Box is displayed and will be set to FALSE
when it is closed (see the WM_INITDIALOG
and the WM_COMMAND
(with wID
== IDOK
) message handlers in SearchWindowDialogProc()
for more details).
We may, of course, opt to disable the "Take A Snapshot" menu item. This is a good idea, especially if there are any other options in the context menu not related directly to window selection. This will certainly complicate the code further but it can be done. When the user double-clicks on the sys tray icon, to directly start up the Search Window Dialog Box, we first check the global variable g_hwndSearchDialog
to see if it contains any value. This g_hwndSearchDialog
global variable is set to the HWND
of the Search Window Dialog box when the dialog box is started up (see WM_INITDIALOG
handler in SearchWindowDialogProc()
) and is set to NULL
when the dialog is closed (see WM_COMMAND
, wID == IDOK
, in SearchWindowDialogProc()
).
If the Search Window Dialog Box is not running at the moment (null value in g_hwndSearchDialog
), we start up the dialog by posting the WM_START_SEARCH_WINDOW
message to the Main Window. If the Search Window dialog box is already running at the moment (non-null value in g_hwndSearchDialog
), we bring the Search Window to the foreground immediately. This provides greater convenience for the user especially when the Search Window is hidden by another window. By bringing the Search Window dialog to the foreground when the user double-clicks the Sys Tray icon, the user can avoid minimizing windows in order to get to it.
The various handlers for the context menu items are listed below. They are part of the WM_COMMAND
handler of the MainWndProc()
:
case WM_COMMAND:
{
WORD wNotifyCode = HIWORD(wParam);
WORD wID = LOWORD(wParam);
HWND hwndCtl = (HWND)lParam;
if (wNotifyCode == 0)
{
if (wID == IDM_CONTEXT_TAKE_SNAPSHOT)
{
PostMessage (hwnd, WM_START_SEARCH_WINDOW, 0, 0);
}
if (wID == IDM_CONTEXT_ABOUT)
{
MessageBox (NULL, ABOUT_WINDOW_FLOATER,
WINDOW_SNAPSHOT_MAIN_WINDOW_TITLE, MB_OK);
}
if (wID == IDM_CONTEXT_SHUTDOWN)
{
PostMessage (hwnd, WM_CLOSE, 0, 0);
}
lRet = 0;
bCallDefWndProc = FALSE;
}
else
{
bCallDefWndProc = TRUE;
}
break;
}
Let us examine the handler for IDM_CONTEXT_TAKE_SNAPSHOT
. It will post the WM_START_SEARCH_WINDOW
message back to the main window which will eventually call on StartSearchWindowDialog()
to start the Search Window dialog box :
case WM_START_SEARCH_WINDOW :
{
lRet = 0;
bCallDefWndProc = FALSE;
StartSearchWindowDialog (hwnd);
break;
}
The Window Search Dialog Box is managed by the SearchWindowDialogProc()
dialog box procedure. This has been covered in my "MS Spy++ Style Window Finder" article. Please refer to the article for more details.
The important modification to the original SearchWindowDialogProc()
is the handler for the IDOK
button :
if ((wID == IDOK) && (g_hwndFoundWindow))
{
ShowWindow (hwndDlg, SW_HIDE);
Sleep(500);
CaptureWindowToClipboard (g_hwndFoundWindow);
}
Here, when the user clicks on the "OK" button, and a window or control has been selected, we perform 3 important steps to capture the screenshot of the selected window/control :
- We hide the Search Window dialog box.
- We halt our application for about half a second.
- We proceed to perform the screen-capture which is encapsulated by the
CaptureWindowToClipboard()
function.
The reasons for taking the 3 steps are explained next. In general, before one captures the screen shot of a window or control, that window or control must be entirely visible. It must not be obscured partially or entirely by another window. Please refer to the discussion thread in Joseph Newcomer's "Screen Capture to the Clipboard" article in which this is discussed.
As such, please ensure that the selected window/control is totally visible when you click on the "OK" button of the Search Window dialog box. Note that this is most evident when you are performing debugging because you will be brought to the Visual Studio IDE and the selected window may be obscured. However, anticipating that the Search Window Dialog Box itself could be obscuring the selected window/control, I have made sure that I first hide the dialog box before performing the screen capture.
Furthermore, while I was doing testing, I noticed that sometimes, even after I have commanded the dialog to hide itself, I still get an image of the dialog box on top of the selected window in the screenshot. The speed of the hiding of any window really depends on the OS itself. To get round this problem, I have added in the Sleep()
API to make our application halt for just half-a-second (enough time to make sure the dialog box is really hidden) before taking the screenshot. If you find that you need more time for this, simply increase the parameter for Sleep()
to a greater value.
I would also like to touch a little on the CaptureWindowToClipboard()
function and the modifications that I made to Joseph's toClipboard()
function.
BOOL CaptureWindowToClipboard (HWND hwndToCapture)
{
BOOL bRet = FALSE;
if((hwndToCapture) && (::IsWindow (hwndToCapture)))
{
bRet = TRUE;
toClipboard_Bio((CWnd *)CWnd::FromHandle (hwndToCapture), TRUE);
}
return bRet;
}
I basically used the CWnd::FromHandle()
method to dynamically convert the selected window to a CWnd
pointer.
void toClipboard_Bio(CWnd * wnd, BOOL FullWnd)
{
CDC *dc;
if(FullWnd)
{
dc = new CWindowDC(wnd);
}
else
{
dc = new CClientDC(wnd);
}
I have created a new function toClipboard_Bio()
which is essentially a copy of Joseph's toClipboard()
routine albeit I changed the "dc" variable from a CDC object to a pointer to a CDC. Via polymorphism, I have later created a pointer to a CWindowDC
or a CClientDC
based on the FullWnd BOOL
parameter. All references to "dc" have been modified in the light of the fact that "dc" is now a pointer. I have also made sure that "dc" is deleted at the end of the routine. The problem discovered with Joseph's code lies in the way that CDC objects are destroyed. Please refer to my discussion thread in Joseph's article for more details.
In Conclusion
- I would like to thank Joseph Newcomer for his Screen Capture code. I hope readers of Joseph's article will find this utility useful as well.
- I would also like to thank the many readers who have read this article and have given me very supportive comments and suggestions. Through their suggestions and bug reports, WindowSnapshot has been upgraded and made more robust. This spirit is truly what makes CodeProject a great web site for developers.
- Note that I have also submitted another article last week that demonstrates another effective use of the WindowFinder :
- My best wishes to all.
Updates, Enhancements And Bug Fixes
- Monday January 14th 2002. Usability Enhancement.
- An enhancement has been added to WindowSnapshot in response to Andrew Peace's suggestion (please refer to the discussion thread below) to directly invoke the Search Window Dialog box when the user double-clicks on the Sys Tray icon.
- I also noticed that while trying to capture a few screens, the context menu generally got in the way of fast deployment.
- The code changes are in main.cpp and WindowFinder.cpp. These are explained below.
- Changes in main.cpp :
case WM_SYS_TRAY_MESSAGE:
{
lRet = 0;
bCallDefWndProc = TRUE;
if (wParam == ICON_ID)
{
if (lParam == WM_RBUTTONDOWN)
{
if (g_bAllowContextMenu)
{
DisplayContextMenu (hwnd);
}
}
if (lParam == WM_LBUTTONDBLCLK)
{
if (g_hwndSearchDialog)
{
SetForegroundWindow (g_hwndSearchDialog);
}
else
{
PostMessage (hwnd, WM_START_SEARCH_WINDOW, 0, 0);
}
}
}
break;
}
A handler for WM_RBUTTONDOWN
is added to process right-mouse-clicks and the context menu is displayed this way. Note that usual check is made to see if a context menu is allowed to be displayed in the first place. This depends solely on whether the Search Window Dialog is currently already running. If so, the context menu is not allowed to be invoked. In the WM_LBUTTONDBLCLK
message handler, no context menu is displayed. Instead, the Search Window is either directly started up, or, if it is already running (most likely obscured behind other windows), it is set to be the foreground window.
- Changes to WindowFinder.cpp :
case WM_INITDIALOG :
{
g_hwndSearchDialog = hwndDlg;
g_hwndFoundWindow = NULL;
g_bAllowContextMenu = FALSE;
SetForegroundWindow (hwndDlg);
bRet = TRUE;
break;
}
Because Search Window can now be invoked directly, we must set this dialog to be the foreground whenever it is first displayed.
- Thanks, Andrew, for your suggestion :-)
- Monday January 14th 2002. Bug Fix.
- A bug has been spotted by Ahmed Ismaiel Zakaria (please refer to discussion thread below) in the
toClipboard_Bio()
function which causes a crash when a user selects a top-level (main frame) window or any window which does not have any parent window (e.g. a parent-less dialog box).
- This is a good catch by Ahmed. Ahmed also gave some suggestions for resolving the bug and I've modified them a little. These are explained next.
- Changes to function
toClipboard_Bio()
:
wnd->OpenClipboard();
Instead of calling GetParent()
from "wnd" and then using the parent window's CWnd
pointer to call OpenClipboard()
(which is the cause of the crash if "wnd" does not have any parent), I've decided to call OpenClipboard()
from "wnd" directly.
- Changes to function
CaptureWindowToClipboard()
: BOOL CaptureWindowToClipboard (HWND hwndToCapture)
{
BOOL bRet = FALSE;
if((hwndToCapture) && (::IsWindow (hwndToCapture)))
{
bRet = TRUE;
toClipboard_Bio((CWnd *)CWnd::FromHandle (hwndToCapture),
TRUE);
}
return bRet;
}
I've made more stringent checks on the parameter hwndToCapture
so that only valid hwnd
s are passed to the toClipboard_Bio()
function.
- Thanks, Ahmed, for spotting this for us :-)