New magazinenew column! I'll be bouncing between this column and the old familiar, though (re-)renamed, Q&A column as we move forward. Unlike Q&A, in this column I'll be focusing on a single solution, so I hope to be able to go into a bit more detail.
For years, I've seen questions online regarding how to kill other applications. I'm not entirely sure what brings on all this mayhem, but I'm happy to help where possible. In fact, in previous issues of Visual Basic Programmer's Journal, I presented portions of what's now turned into a fairly accurate re-creation of the Applications tab on Windows NT's Task Manager (see Resources). Missing previously was the code to go behind the End Task button, which I'll provide in this column.
Microsoft's Knowledge Base (KB) provides some good advice and general implementation ideas about shutting down another application (see Resources). However, it doesn't cover all the contingencies you should account for (you knew that was coming). It's also not entirely VB-friendly, as the majority of solutions it provides are coded in "another language."
I'll assume you have, or know how to obtain, the window handle (hWnd) of the application you want to kill. If not, numerous examples are on my Web site at
www.mvps.org/vb and in previous columns I've written (see Resources). Just as in real life, things work out most smoothly when everyone's in agreement. The easiest way to shut down another app is to ask it nicely. You can do this by sending the other app a WM_CLOSE message.
Sending WM_CLOSE to the window handle you have isn't sufficient typically because most applications comprise numerous windows. The answer: Find all the top-level windows belonging to this application and send the message to each. Start by determining the application's process ID. This is a unique 32-bit value that you can obtain using a call to GetWindowThreadProcessId. Pass the known hWnd as the first parameter, and a variable to accept the process ID as the second. GetWindowThreadProcessId returns the thread ID if the call succeeds:
Dim ThreadID As Long
Dim ProcessID As Long
ThreadID = GetWindowThreadProcessId(hWnd, ProcessID)
The EnumWindows API provides a means to iterate through the windows list. You pass EnumWindows the address of a routine and a user-defined value. The system then calls the specified routine once for each top-level window, passing it an hWnd and the value you choose. For this case, you want to pass the ProcessID so you can compare it with the same value extracted from the hWnd under consideration. When you find matching process IDs, according to the KB, it's time to send a WM_CLOSE message.
Here, you run into two difficulties. First, if the application is hung, it won't respond to a SendMessage call. Use either PostMessage or SendMessageTimeout to safely avoid hanging yourself while waiting for hung apps. The second dilemma is interesting in that the KB writers didn't even consider it. If you send WM_CLOSE to all top-level windows of a VB-authored app, and that app typically asks for confirmation at shutdown (save your work? are you sure? and so on), things lock up fast. You can avoid this second issue by adding a check in the callback routine to ensure the window under consideration is ownerless (see Listing 1).
Again, as in real life, there will be times when asking nicely doesn't achieve the results you desire. To determine if WM_CLOSE has worked, the Win32 API provides a method to wait for 32-bit Windows apps to shut down. Using the known process ID, request SYNCHRONIZE access by calling OpenProcess to obtain a handle to the process (hProc). OpenProcess might fail with some system processes, even with this minimal access request, but in most cases you can assume the process no longer exists if OpenProcess fails.
Pass this hProc to WaitForSingleObject, being sure to specify how long you're willing to wait. WaitForSingleObject returns when either an object is "signaled" or the timeout period has elapsed. A process object becomes signaled when it has terminated. Patience is a virtue, but it is limited. A timeout of three to five seconds should be sufficient in most cases to know that WM_CLOSE isn't having the intended effect (see Listing 2).
You must now decide whether to simply nuke the process with a call to TerminateProcess or ask your user to take responsibility for that call. Task Manager provides a fine precedent by telling the user that data might be lost and asking whether to wait longer or pull the final trigger (see Figure 1). I've created a dialog that replicates those shown by NT's Task Manager and exposes a number of custom properties that help it set itself up when loading. If WM_CLOSE doesn't work, the ProcessClose routine shows the FNotResponding form nonmodally, then enters a wait loop (see Listing 3).
While waiting, ProcessClose calls WaitForProcessClose repeatedly with a short timeout (10 milliseconds) and checks whether FNotResponding is still visible, exiting the loop should either of these occur. Call DoEvents on each iteration to yield the processor to the target application and allow your user to click on the buttons on FNotResponding.
Tie Up Loose Ends
You also need to be aware of the oddities you'll encounter when shutting down 16-bit apps. Under Windows 9x, 16-bit apps are "equal citizens" and the code presented here works fine. In NT, Win16 apps run as separate tasks (threads) under a 32-bit Virtual DOS Machine (VDM) process. Multiple Win16 apps can run within a single Windows on Win32 (WOW) VDM.
The cleanest way to shut down a Win16 app is to kill the entire VDM process, but this risks closing more than just the target task. Under NT, you can use functions from VDMDBG.dll to determine the extent of potential carnage should you decide to terminate the VDM (see Resources). This DLL also supplies a function to kill a single task within a WOW VDM, but there are no guarantees the resulting VDM will be stable.
Given these considerations, if your app is also a task launcher, you should always launch Win16 apps within separate processes. Calling CreateProcess rather than Shell and specifying the CREATE_SEPARATE_WOW_VDM flag achieves this separation. If you enjoy this low-level control and want to pluck individual tasks from within a VDM, take a look at the VDMDBG functions (or stay tuned to this column).
Karl E. Peterson is a GIS analyst with a regional transportation-planning agency and serves as a member of the VSM Technical Review and Editorial Advisory boards. Online, he's a Microsoft MVP and a section leader on several DevX forums. Find more of Karl's VB samples at
www.mvps.org/vb. E-mail Karl at firstname.lastname@example.org to let him know what you think of this new column.