|Products Purchase Publishing Articles Support Company Contact|
> COM > Detect New Window
Detect and Automatically Respond to User Input Requests from Running Applications
By Franky Wong
When running long operations in an application, you may prefer to run them overnight or while you are away on a break. But if the application encounters errors or unexpected results, it may display a message box or form requesting your input on how to proceed. Sometimes these interruptions can be annoying when the choice on how to proceed is obvious. Instead of returning to see the results of the long awaited operation, you return to a silly or obvious question that must be answered before resuming the operation, and then have to continue waiting for the operation to complete. If you had the source code to the application, you would probably change it to select the obvious choice whenever these situations arise. Unfortunately, most applications do not provide source code, so this article explores different ways to automatically respond to these requests.
There are three parts that you need to consider when designing a solution to this problem. The first is to detect when the application is interrupted - that is when it is requesting additional instructions. The second is to retrieve the information the application displays, such as the text explaining the error, instructions on how to proceed, or the choice of buttons you can select. The last part is to respond to the request.
Different applications may request user input using different methods. Some may display a Windows form with many different options and not proceed until you make a selection, some may display a smart Windows form that times out after a short period selecting the default option, and some may log an entry into the Windows Registry or Event Log. One of the most common methods is simply to display a standard Windows Message box containing multiple buttons to represent different choices. This article will focus on detecting for the creation of a Windows Message box that belongs to the application running the long operation.
The first step is to create an external application that can detect when a new Windows Message box is created. One way to do this is to periodically enumerate all of the existing windows on a system. You can create an application that uses a timer - on each timer event, it enumerates all the windows. You can keep track of the existing windows in a list and compare it to a list of the currently enumerated windows to determine which windows are newly created. The drawback with this method is that it is not very efficient, as it continuously wastes CPU cycles unnecessarily enumerating windows. Another method would be to use a Windows Hook such as the one included with SpyWorks. Set the SpyWorks hook to Monitor messages just for the specified application (TaskParam), select the CallWndProc HookType, and detect the WM_CREATE message. An event will be triggered whenever a new window is created belonging to the specified application.
For our simple sample, we will use the FindWindow API function to find the main window of the application we want to detect for. FindWindow is declared as follows:
Declare Function FindWindow Lib "User32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As String) As Long
lpClassName contains the classname of the window to find, lpWindowName contains the window title/caption of the window to find. Note that FindWindow only searches top-level windows. You can also use different variations of FindWindow if you want to perform a search based only on the class name or the window title/caption. Refer to Dan Appleman's Visual Basic Programmer's Guide to the Win32 API book or similar reference for more information. We search for our window as follows:
hwnd = FindWindow(app_main_window_class_name, app_main_window_title)
The window handle matching the window class name and title is returned on success. Next, we use that window handle to retrieve the process id of the application so we can set the SpyWorks hook to detect messages just for that application. The GetWindowThreadProcessId function does that and is defined as follows:
Declare Function GetWindowThreadProcessId Lib "User32" (ByVal hwnd As Long, lpdwProcessId As Long) As Long
hwnd is the window handle for which to retrieve the process id and thread ID. lpdwProcessId is a long parameter passed by reference which will contain the process ID of the specified window on success. GetWindowThreadProcessId returns the thread id for the specified window on success. On successfully retrieving the process ID, set the SpyWorks hook control to start detecting messages for that particular process.
threadid = GetWindowThreadProcessId(hwnd, processid)
If processid <> 0 Then
The WinHook1_WndMessage event will trigger when a new window is created within the specified application. The event will contain the window handle of the new window. Note that for our example, we set the Notify property to ?Posted?, this defers our event to trigger after the WM_CREATE message has been processed by the system. This allows the system time to create the window and child windows so that when we process the event, the window is fully created.
Once you detect the new window, how do you extract the message text and types of buttons from the message box? The Win32 API provides a number of functions to extract information from any Window given the window handle. You can find all of the child windows (static text and buttons) of the message box, as well as retrieve the window text for them. With this information, you can extract the actual message displayed in the message box and the determine choice of buttons it presents.
The first Win32 API function to call when we have found a new window is the GetClassName function. It is declared as follows:
Declare Function GetClassName Lib "User32" Alias "GetClassNameA" (ByVal hwnd As Long, ByVal classnamebuffer As String, ByVal bufferlength As Long) As Long
hwnd is the window handle for which to retrieve the class name. Classnamebuffer is a pre-defined string in which to return the class name, and bufferlength holds the pre-defined length of classnamebuffer. On success, GetClassName returns the number of characters it copied into the classnamebuffer parameter that will contain the class name of the specified window. A standard Windows Message Box has the class name ?#32770?. Before calling GetClassName, you must initialize the classnamebuffer string thus:
' Set aside enough room in the string for the window class text
windClass = String$(260, 0)
' Get window class name, up to 259 characters (terminating null)
ret = GetClassName(hwnd, windClass, 259)
' Trim the class name.
windClass = LCase$(Left$(windClass, ret))
After verifying that this is a message box, you may want to retrieve the window text (or caption) of the message box. You can do that by using the GetWindowText API function. It is declared as follows:
Declare Function GetWindowText Lib "User32" Alias "GetWindowTextA" (ByVal hwnd As Long, ByVal textbuffer As String, ByVal bufferlength As Long) As Long
hwnd is the window handle for which to retrieve the window text, textbuffer is a pre-defined string, and bufferlength holds the pre-defined length of textbuffer. GetWindowText returns the number of characters it copied into the textbuffer parameter that will contain the window text of the specified window. It is called similarly to GetClassName.
windName = String$(260, 0)
ret = GetWindowText(hwnd, windName, 259)
windName = Left$(windName, ret)
The next thing to do is to retrieve all of the child windows of the message box. The child windows contain information such as the button text and message box text. The preferred way to do this is by calling the EnumChildWindows API function. It is declared as follows:
Declare Function EnumChildWindows Lib "User32" (ByVal hWndParent As Long, ByVal lpEnumFunc As Long, ByVal lParam As Long) As Long
hWndParent is the window handle of the parent window whose child windows you are enumerating, lpEnumFunc is a function address of a function to call (commonly referred to as the callback function) ? once for each child window found, and lParam contains additional information to pass to the callback function. The designated callback function will be called, once for each child window found. EnumChildWindows returns a non-zero value on success, after the callback function has been called for each of the child windows.
The EnumChildWindows function is called as follows:
ret = EnumChildWindows(hwndparent, AddressOf Callback1_EnumWindows, 0)
The callback function is defined as follows:
Public Function Callback1_EnumWindows(ByVal hwnd As Long, ByVal lpData As Long) As Long
Setting the return value of the callback function to a non-zero value instructs Windows to continue enumerating the next child window.
You retrieve the child window information in the Callback1_EnumWindows function. Call the GetClassName API function again to determine whether you are working with a button or static text (equivalent of Labels) child window. A button window has the class name ?button?, and a static text window has the class name ?static?. You then call the GetWindowText API function to retrieve the text for the static window, or the button control. You can keep each child window in a collection so that after all the child windows have been enumerated, you have a clear picture of the message box.
Finally, after you retrieve the message box text and determine the buttons that exist in the message box, you can decide how to proceed by calling the PostMessage API function to ?click? on one of the buttons. PostMessage is defined as follows:
Declare Function PostMessage Lib "User32" Alias "PostMessageA" (ByVal hwnd As Long, ByVal wMsg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
hwnd is the window handle to post the message to, in this case the window handle of the message box (not the button). wMsg is the Windows message to post to the window, when simulating a button click, it would be WM_COMMAND (&H111), wParam is additional information passed depending on the Windows message posted, in the case of the WM_COMMAND message, it contains the child window's control id in the low 16 bits. The different buttons in the standard Windows Message box are normally assigned the following control ids: ok=1, cancel=2, abort=3, retry=4, ignore=5, yes=6, no=7. But under some circumstances, the ok button is assigned to 2 - refer to the sample project on how we handle that. The lParam can be set to 0 for our case. If you want to respond by selecting the ?Retry? button (after you verified that it exists), you can use the following code:
ret = PostMessage(hwnd, WM_COMMAND, 4, 0)
ret = PostMessage(hwnd, WM_COMMAND, VbMsgBoxResult.vbRetry, 0)
The full sample can be downloaded at Sample , it uses SpyWorks to detect newly created Windows. Included in the download is another project that displays different types of windows message boxes along with the return value for the message box that can be used for testing. Detecting for other types of window prompts should also work. The key thing for detecting other types of windows is to retrieve some sort of unique information for that particular window that clearly identifies it. Use the SpyWin Windows browser sample included with SpyWorks to retrieve information for the windows during testing.
Further information on Desaware's SpyWorks can be found at http://www.desaware.com.
For notification when new articles are available, sign up for Desaware's Newsletter.
|Products Purchase Articles Support Company Contact