New - 25th June 2005
- Added a 'Center' button, which centers the found window on the desktop. (Useful for recording application demos etc..)
- Now remembers its last position on the desktop and starts there next time.
- Previous changes
Introduction
Lim Bio Liong's excellent CodeProject article (MS Spy++ style Window Finder) does all the hard work relating to picking a window to work with for this utility. I claim no credit whatever for this.
However, Lim Bio Liong's sample project launches the window finder dialog from an SDI application and is provided principally to assist in demonstrating the window finder code.
As a developer, I wanted a simple, free tool that would do all of the following:
- 'Scrape' the text off a dialog box control or message box, so that it can be pasted as text into an email or whatever. (How often have I dismissed the error box without accurately noting down its contents?)
- Capture images of controls or windows from my application for documentation purposes etc..
- Resize my application or web browser windows to a specific size for testing alternate screen resolutions or consistent documentation of my application.
That's what the tool presented in this article sets out to do. As a full time developer I expect to make a lot of use of it from now on!
Along the way, the source code also demonstrates 'stay on top' functionality, and the dialog box expands and contracts to show more or less detail.
Architecture
The first thing I wanted to do was to turn the original SDI app + dialog box into a dialog box application, as the SDI bit was inappropriate to my design.
As an MFC developer, the easiest way was:
- Create an MFC dialog box application.
- Remove the app wizard created dialog box cpp and h files from the project.
- Remove the reference to the dialog box h file in the application cpp file.
- Remove the standard dialog box launch code from the application's
InitInstance()
and replace it with the code needed to launch the Win32 dialog, i.e.:
InitialiseResources();
long lRet = (long)DialogBox
(
(HINSTANCE)m_hInstance,
(LPCTSTR)MAKEINTRESOURCE
(IDD_DIALOG_SEARCH_WINDOW),
(HWND)NULL,
(DLGPROC)SearchWindowDialogProc
);
It was also necessary to move some of the global declarations out of the SDI app file and into the windowfinder.cpp file. The result is that the application's InitApp()
initializes the resources required for the dialog and then launches the dialog.
Scraping Text from Dialog and Message Boxes
The code in DisplayInfoOnFoundWindow()
in windowfinder.cpp receives a handle to the selected window. With this handle, it is simple to get the window's text:
char winTxt[5000];
memset(winTxt, 0, 5000);
SendMessage(hwndFoundWindow, WM_GETTEXT, 5000, (LPARAM)winTxt);
(The MSDN documentation states that this method is preferable to GetWindowText()
when getting text from a window outside your application.)
However, some controls do not yield their contents in this way. So, for example, to get the text from a listbox control, we use the following:
if(strcmp(szClassName, "ListBox") == 0)
{
CListBox lb;
lb.Attach(hwndFoundWindow);
nItems=lb.GetCount();
for(count=0; count < nItems; count++)
{
lb.GetText(count, strItem);
strItem+="\r\n";
strcat(winTxt, strItem);
}
lb.Detach();
}
(It would be nice to capture the text from more complex controls, such as tree controls, using a similar approach but to date such an approach eludes me. Even myTreeCtrl.GetItemText(myTreeCtrl.GetRootItem()
returns an empty string, for some reason.)
Capturing the Window to the Windows Clipboard
This is processed in SearchWindowDialogProc()
- (search windowfinder.cpp for "if(wID == IDC_CAPTURE_BUTTON)
").
A global is already set to the handle of the target window before SearchWindowDialogProc()
is called, so we already have a handle to the window to capture.
We need to temporarily hide our application in case it overlaps the capture target, then we capture the screen area to a compatible bitmap and insert it into the clipboard, finally restoring our application's window.
The biggest challenge here was that I initially thought you should use the device context of the window you want to capture to create the compatible bitmap - however this led to the 'wrong' area being captured. Using "hdc=GetDC(HWND_DESKTOP)
" gets things capturing correctly.
Moving and Resizing the Target Window
The code in DisplayInfoOnFoundWindow()
in windowfinder.cpp receives a handle to the selected window. With this handle, it is simple to resize the target window (where h
and w
are the desired height and width of the target window):
WINDOWPLACEMENT wp;
GetWindowPlacement(g_hwndFoundWindow, &wp);
wp.rcNormalPosition.right=wp.rcNormalPosition.left+w;
wp.rcNormalPosition.bottom=wp.rcNormalPosition.top+h;
SetWindowPlacement(g_hwndFoundWindow, &wp);
Similarly, moving the window to a screen location is easily accomplished via the Get/SetWindowPlacement()
Win32 functions.
Stay on Top
Making the dialog stay on top of all other windows is easy, but not documented in a sufficiently clear enough way for my simple mind to understand in the MS docs:
To set the stay on top property:
SetWindowPos( hwndDlg, HWND_TOPMOST, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW );
To remove it:
SetWindowPos( hwndDlg, HWND_NOTOPMOST, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW );
More / Less Detail on Dialog
We can make the dialog show more or less detail by resizing it using the position of a visible (or hidden) control on the dialog as a marker.
The following code block handles expanding / contracting the dialog:
WINDOWPLACEMENT dlgwp;
GetWindowPlacement(hwndDlg, &dlgwp);
CRect rect;
UINT nBtnState=IsDlgButtonChecked(hwndDlg, IDC_EXPANDVIEW_CHECK);
if(nBtnState == BST_CHECKED)
{
GetWindowRect(GetDlgItem(hwndDlg, IDC_DETAILS_STATIC), &rect);
dlgwp.rcNormalPosition.bottom=rect.bottom+8;
SetWindowPlacement(hwndDlg, &dlgwp);
}
else
{
GetWindowRect(GetDlgItem(hwndDlg, IDC_WINTXT_STATIC), &rect);
dlgwp.rcNormalPosition.bottom=rect.top+2;
SetWindowPlacement(hwndDlg, &dlgwp);
}
The IDC_DETAILS_STATIC
ID refers to the static groupbox labeled "Details:" at the bottom of the expanded application window.
The IDC_WINTXT_STATIC
ID refers to the static groupbox labeled "Window Text" on the dialog.
As you can see, we get the initial dimensions of the dialog and marker statics using GetWindowPlacement()
and GetWindowRect()
and adjust the WINDOWPLACEMENT
's rcNormalPosition
members. Finally we use the adjusted WINDOWPLACEMENT
structure to set the new size of the dialog.
Conclusion
Hopefully what we have here is a reasonably useful little tool for software / web site developers. Along the way, I learned a little more about Win32 (but think I will stick with MFC given the choice!). I hope you find the tool useful, even if the source code is a mixture of Lim Bio Liong's original work and my quick and dirty (but hell, they seem to work!) hacks.
Finally, if you do find the tool useful, I'd really appreciate it if you could kindly take the time to click on the link to http://www.powerprogrammer.co.uk/ at the top right of the tool's dialog. Oh, and also, how about giving this article a nice rating so that others can be alerted to the benefits too! Thanks!
- Updated source and exe on 5 January 2004
- Updated source and exe on 25 June 2005