barcode source code 16: Automated Testing in C#

Creation Code 39 Extended in C# 16: Automated Testing

16: Automated Testing
Painting Code 3 Of 9 In Visual C#.NET
Using Barcode creator for VS .NET Control to generate, create Code 39 Extended image in .NET applications.
Scanning Code-39 In Visual C#.NET
Using Barcode scanner for Visual Studio .NET Control to read, scan read, scan image in VS .NET applications.
Overview In 3, I talked about unit tests and explained why unit testing is such a vital part of producing high-quality code. For those of us who work mostly on an application's internal logic, unit tests can be fairly simple. Look at this book's sample files in BugslayerUtil\Tests, and you'll see all the unit tests I used to develop BUGSLAYERUTIL.DLL. Almost all those tests are console applications that do their jobs admirably. Unfortunately, testing user interface (UI) code is more difficult, no matter whether the application is a Microsoft .NET fat client or is browser based. In this chapter, I'll present a utility I wrote, Tester, that will help you automate your UI testing. Compared with the version of Tester included in the first edition of this book, the new Tester tool now borders on the capabilities of a full-blown commercial regression-testing tool. In fact, I've been extremely pleased to see how many development shops are using Tester. Not only is it easier to deal with than many commercial systems, it's quite a bit less expensive, too. The Bane of Unit Testing: User Interfaces I'm firmly convinced that Microsoft Windows developers get their carpal tunnel syndrome not from typing their source code but from hitting the same keystroke combinations over and over to test their applications. After the 5,000th time you type Alt+F, O, your wrists are locked tighter than rebar in concrete. Without a tool to automate the tasks involved in accessing the different features of your applications, you generally have to follow some sort of script to ensure that you're doing sufficient unit testing. Testing with manual input scripts is totally boring and the boredom that results leaves plenty of room for human error. Automating your unit tests will mean that you don't have to type so much and that you can quickly verify the state of your code. Unfortunately, the Recorder application that used to ship with Microsoft Windows 3.0 and 3.1 doesn't come with any of the 32-bit operating systems. For those of you new to Windows, Recorder wrote your mouse and keyboard interactions to a file so that you could play them back as if they were physical mouse and keyboard events. Although several third-party products are available that will automate your application and a whole lot more (such as completely validate every pixel in a screen comparison and maintain databases of which tests ran when), I wanted something that was lightweight and geared toward development engineers. Thus, the idea for my Tester application was born. When I started thinking about creating an automation utility, I spent some time considering exactly what I'd expect from such a tool. At first, I thought about developing a utility akin to the old Recorder application. Way back in the days of Windows 3.0, I had a complete set of REC files to drive my tests. However, the big problem with Recorder was that it didn't provide a way to do conditional tests. If my application signaled an error during the test, Recorder just went along its merry way, playing back the recorded keystrokes and mouse clicks, completely oblivious to my application's distress. One time I wiped out half my operating system with Recorder because I was testing a WINFILE.EXE extension, and when my extension had a problem, Recorder played the delete files sequence all over the System directory. My new automation tool definitely had to have an if...then...else construct. To incorporate conditional constructs into my tests, I obviously needed to use some sort of language. Developing my own testing language would've been an intriguing intellectual exercise, but I soon concluded that I was more interested in writing a useful debugging tool than in designing a language and dealing with YACC and FLEX. It took all of two seconds 573
Bar Code Printer In C#
Using Barcode maker for Visual Studio .NET Control to generate, create bar code image in .NET applications.
Decoding Bar Code In C#
Using Barcode recognizer for Visual Studio .NET Control to read, scan read, scan image in Visual Studio .NET applications.
to realize that I should write Tester as a COM object that way, developers could use my utility and still write tests in their language of choice, and I could concentrate on programming the utility's regression-testing features instead of designing a new language. My personal testing languages of choice are scripting languages, such as Microsoft Visual Basic Scripting Edition (VBScript) and Microsoft JScript, because the testing scripts don't require compiling. The different Microsoft Windows Scripting Host (WSH) scripting engine implementations have a few limitations, however, which I'll point out later in the chapter. For now, let's talk about the requirements that guided my creation of Tester. Tester Requirements I wanted to keep Tester focused on doing two things very well: recording your keystrokes and mouse usage, and playing those inputs back to your application so that you can unit test faster. If you've ever explored commercial regression-testing tools, you've undoubtedly seen what a wild ride they can involve, from simply controlling a window on the screen to validating all sorts of complicated and weird data of the most obscure window properties possible. I wanted to concentrate on the developer's needs during unit testing and keep Tester simple to use. Here are the main requirements for Tester: 1. Tester can be controlled through any language that supports COM. 2. Given an input string of keystrokes, in the same format used by the System.Windows.Forms.SendKeys class, Tester can play the keystrokes to the active window. 3. Tester can find any top-level or child window by its caption or class. 4. Given any arbitrary HWND, Tester can get all the window's properties. 5. Tester must notify the user's script of specific window creation or destruction so that the script can handle potential error conditions or do advanced window handling. Tester doesn't limit developers from extending the code in any direction they need for their shops' requirements. 6. Tester must be able to record keystrokes and place them into a string that is compatible with the Tester playback code. 7. When generating a Tester script, the script is self-contained so that the saved script is ready to run. 8. The user can edit the automatically generated script before saving it. 9. Tester can properly set focus to a specific window, including any child control, to ensure that playback goes to the correct window. While quite complete, Tester probably isn't a general solution for your 20-person QA department. My intention was to build a tool that you and I, as development engineers, could use to automate our unit testing. To that end, I think Tester fits the bill. I used Tester quite a bit to help develop WDBG, the graphical user interface (GUI) debugger I developed as part of 4. The best part of using Tester with WDBG was that I saved myself thousands of keystrokes this far into the book, I can still move my wrists! Using Tester Using Tester is relatively simple. I'll start out by discussing the actual Tester object and its use in scripts before I move on to discussing how to use the TESTREC.EXE program to record scripts. By understanding the object Tester presents for your scripts, you'll be able to record them more effectively. Tester Scripts In your scripts, the basic idea is to just create a couple of Tester objects, either start or find your application's main window, play some keystrokes to your application, check the results, and end. Listing 16-1 shows a sample VBScript test that starts NOTEPAD.EXE, 574
Code 39 Creation In .NET
Using Barcode encoder for ASP.NET Control to generate, create Code 3/9 image in ASP.NET applications.
Encoding Code-39 In Visual Studio .NET
Using Barcode generator for .NET Control to generate, create ANSI/AIM Code 39 image in .NET applications.
enters a few lines of text, and closes Notepad. (All the example scripts shown in this chapter are included with the sample files for this book.) Listing 16-1: MINIMAL.VBS showing how to use common Tester objects ' A minimal VBScript Tester example. It just starts Notepad, enters a ' few lines of text, and closes Notepad. ' Get the system and input objects. Dim tSystem Dim tInput Dim tWin Set tSystem = WScript.CreateObject ( "Tester.TSystem" ) Set tInput = WScript.CreateObject ( "Tester.TInput" ) ' Start Notepad. tSystem.Execute "NOTEPAD.EXE" ' Wait a few seconds. tSystem.Sleep 3.0 ' Try to find Notepad's main window. Set tWin = tSystem.FindTopTWindowByTitle ( "Untitled - Notepad" ) If ( tWin Is Nothing ) Then MsgBox "Unable to find Notepad!" WScript.Quit End If ' Ensure that Notepad is in the foreground. tWin.SetForegroundTWindow ' Type something. tInput.PlayInput "Be all you can be!~~~" ' Play it again, Sam. tInput.PlayInput "Put on your boots and parachutes....~~~" ' Third time's the charm. tInput.PlayInput "Silver wings upon their chests.....~~~" ' Wait 3 seconds. tSystem.Sleep 3.0 ' End Notepad. tInput.PlayInput "%FX" 575
Code 3/9 Generator In Visual Basic .NET
Using Barcode generator for Visual Studio .NET Control to generate, create Code 3/9 image in .NET applications.
UPC Code Printer In C#
Using Barcode creation for VS .NET Control to generate, create UCC - 12 image in Visual Studio .NET applications.
tSystem.Sleep 2.0 tInput.PlayInput "{TAB}~" ' Script is done!
Encode ECC200 In C#
Using Barcode encoder for .NET framework Control to generate, create Data Matrix image in VS .NET applications.
Code 128 Code Set B Generator In Visual C#.NET
Using Barcode printer for .NET Control to generate, create USS Code 128 image in VS .NET applications.
Listing 16-1 shows the three objects Tester uses most often. The TSystem object allows you to find top-level windows, start applications, and pause the testing. The TWindow object, which is returned by FindTopTWindowByTitle in Listing 16-1, is the main workhorse. It is a wrapper around an HWND and has all sorts of properties that tell you everything about the window. Additionally, TWindow allows you to enumerate all the child windows that belong to a particular parent. The last object in Listing 16-1 is the TInput object, which supports the single method PlayInput to funnel keystrokes to the window that has the focus. Listing 16-2 shows the TNotify object used in a VBScript test. When developing automation scripts, one of the most difficult cases you'll need to handle is when an unexpected window, such as an ASSERT message box, pops up. The TNotify object makes it a snap to provide an emergency handler for such events. The simple script in Listing 16-2 just watches for any windows with "Notepad" in their captions. Although you might not use the TNotify class much, when you do need it, you really need it. Listing 16-2: HANDLERS.VBS showing how to use the TNotify object ' A VBScript test to show the window notification handlers ' Constants for the TNotify.AddNotification routine. Const antDestroyWindow Const antCreateWindow = 1 = 2
PDF-417 2d Barcode Creation In C#
Using Barcode drawer for VS .NET Control to generate, create PDF 417 image in Visual Studio .NET applications.
Create Planet In Visual C#.NET
Using Barcode creation for VS .NET Control to generate, create Planet image in Visual Studio .NET applications.
Const antCreateAndDestroy = 3 Const ansExactMatch Const ansBeginMatch Const ansAnyLocMatch = 0 = 1 = 2
Generating USS Code 39 In Java
Using Barcode encoder for Android Control to generate, create Code 39 image in Android applications.
Generating EAN-13 In Visual Studio .NET
Using Barcode maker for Reporting Service Control to generate, create EAN-13 image in Reporting Service applications.
' Get the system and input objects. Dim tSystem Dim tInput Set tSystem = WScript.CreateObject ( "Tester.TSystem" ) Set tInput = WScript.CreateObject ( "Tester.TInput" ) ' The TNotify object variable Dim Notifier ' Create the TNotify object. 576
GS1 - 12 Recognizer In VS .NET
Using Barcode recognizer for .NET framework Control to read, scan read, scan image in .NET framework applications.
Creating USS-128 In Visual Basic .NET
Using Barcode printer for Visual Studio .NET Control to generate, create EAN / UCC - 14 image in Visual Studio .NET applications.
Set Notifier = _ WScript.CreateObject ( "Tester.TNotify" "NotepadNotification" , _ )
Reading QR-Code In Visual C#
Using Barcode scanner for .NET framework Control to read, scan read, scan image in VS .NET applications.
Decoding Data Matrix In None
Using Barcode reader for Software Control to read, scan read, scan image in Software applications.
' Add the notifications that I want. For this demonstration, I want both ' window destroy and window create notifications. See the TNotify source ' code for all the possible notification combinations. Notifier.AddNotification antCreateAndDestroy , _ ansAnyLocMatch "Notepad" ' Start Notepad. tSystem.Execute "NOTEPAD.EXE" ' Pause for one seconds. tSystem.Sleep 1.0 ' Because the apartment model isn't thread-safe, I set up the ' notification scheme to use a timer. However, the message can get ' blocked because all the processing is forced to a single thread. This ' function lets you manually check the window create and window destroy ' conditions. Notifier.CheckNotification ' The message box in the NotepadNotification_CreateWindow event ' procedure blocks, so the code to end Notepad won't execute until the ' message box is cleared. tInput.PlayInput "%FX" tSystem.Sleep 1.0 ' Again, check notifications. Notifier.CheckNotification ' Give TNotify a chance to intercept the window destroy message. tSystem.Sleep 1.0 , _
UCC - 12 Encoder In Java
Using Barcode creation for Android Control to generate, create GS1 128 image in Android applications.
QR Code 2d Barcode Reader In Java
Using Barcode recognizer for Java Control to read, scan read, scan image in Java applications.
' Disconnect the notifications. If you don't do this in WSH, the class ' terminate never gets called so the notification is still active in the ' notification table. WScript.DisconnectObject Notifier Set Notifier = Nothing WScript.Quit
Sub NotepadNotificationCreateWindow ( tWin ) MsgBox ( "Notepad was created!!" ) End Sub Sub NotepadNotificationDestroyWindow ( ) MsgBox ( "Notepad has gone away...." ) End Sub
You need to call the TNotify CheckNotification method every once in a while. (I'll explain the reasons you need to call this method in the section "Implementing Tester" later in the chapter.) Periodically calling the CheckNotification method ensures that the notification messages can get through even though you might not have a message loop in your language of choice. The code in Listing 16-2 shows how to use a message box in the notification event procedures, though you probably don't want to use message boxes in your real scripts because they can cause problems by unexpectedly changing the window with the focus. Also keep in mind that I allow you to set only a limited number of notifications five so you shouldn't use TNotify for general scripting tasks such as waiting for the File Save dialog box to appear. TNotify should be used only for unexpected windows. Depending on how you set up your notification handlers and how they search for the specified text in the window caption, you can easily receive notifications for windows that you might not be interested in. You're most likely to receive unwanted notifications when you use a generic string such as "Notepad" and you specify that the string can appear anywhere in the window caption. To avoid notifications you don't want, you should be as specific as possible with your notifications when calling the TNotify AddNotification method. Your CreateWindow event procedures should also look at the TWindow passed in so that you can verify that it's the window you're interested in. For DestroyWindow event procedures that handle generic notifications, you should search the open windows to ensure that the window you're interested in no longer exists. Included with the source code that accompanies this book are other samples that you might want to look at to see how you can use Tester. NPAD_TEST.VBS is a more complete VBScript test and has some reusable routines. PAINTBRUSH.JS shows using the mouse playback capabilities in a resolution-independent manner. It takes a bit to run, but the 578
outcome is worth it. TesterTester is the main unit test for the Tester COM object. TesterTester a C# application in the Tester\Tester\Tests\TesterTester directory, should give you an idea of how to use Tester with .NET. Additionally, the sample TesterTester shows the TWindows object, which is a collection that contains TWindow objects. Although I'm partial to using JScript and VBScript for my unit tests, I realize that getting them to work correctly can be a challenge. Scripting variables are untyped and there's no magic IntelliSense editor, such as the C# editor in Visual Studio .NET, so you're back to the old run-and-crash style of debugging. The main reason I like using scripting languages is that I don't need to rely on compiling my tests. If you have a flexible build environment one in which you can easily build other binaries in addition to your main application you might want to consider using .NET so that you can build your tests as you build your application. Of course, Tester doesn't limit you to the easiest-to-use testing languages. If you're more comfortable using C or the Microsoft Macro Assembler (MASM), you're welcome to use those languages instead. Although using the objects in Tester is fairly simple, the real work is planning your tests. You should keep your tests as focused and simple as possible. When I first started automating my unit tests in my early development days, I tried to force my tests to do too much. Now I write each script to test just a single operation. A good example of a singleoperation test is to limit the script just to sending the keystrokes to open a file. You can chain the scripts together in various ways to maximize script reuse. Once you have the script to open a file, you can use it in three different tests: a test to see whether you can open a valid file, a test to open an invalid file, and a test to open a corrupt file. As in normal development, you should avoid any hard-coded strings if possible. Not only will this make internationalizing your script a piece of cake, but it will also help when you change your menu system and accelerators for the hundredth time. Another detail to consider when you're designing your Tester scripts is how to verify that the script actually worked. If you're bored and have the time, I guess you could just sit there and watch the scripts run to see whether you get the same results on each run. Probably a better idea is to log states and key points in your script so that you can compare the output to previous runs automatically. If you use the CSCRIPT.EXE WSH executable, you can use WScript.Echo and redirect the output to a file. After the script finishes, you can run a difference utility (such as WinDiff) on the output; if the utility turns up any differences, you can check to see that the script executed correctly. Keep in mind that you'll want to normalize the information you log and keep the information free of run-specific details. For example, if you're writing an application that downloads stock quotes, you won't want to include the last price-update time in the logging output. What about debugging your Tester scripts Tester doesn't have its own integrated debugger, so you'll need to use whatever debugging tools are available for the language that the Tester script is written in. If you're using a debugger, you need to be careful that you don't stop on a TInput PlayInput method call. If the debugger does stop there, the keystrokes will obviously go to the wrong window. To work around this potential problem, I generally force the window to which I'm sending keystrokes to the top of the z-order by calling the TWindow SetForegroundTWindow method before each PlayInput call. This way, I can break on the SetForegroundTWindow call, check the state of the application, and still get the keystrokes to the correct window. Recording Scripts Now that you're armed with an understanding of the Tester objects and how to call them from your own scripts, I can turn to the TESTREC.EXE program that you'll use to record the interaction with your applications. When you first fire up TESTREC.EXE, you'll notice 579
that it's a text editor that has a little bit of code already written for you. By default, the language is JScript, but I'll show you how to change the default to VBScript in a moment. Starting the recording is as simple as pressing the Record button on the toolbar or pressing Ctrl+R. When recording, TESTREC.EXE automatically minimizes itself and changes its caption to "RECORDING!," so you'll know what it's doing. You can stop recording in several different ways. The easiest is to set focus to TESTREC.EXE by either pressing Alt+Tab or clicking on the application. Recording will also stop if you press Ctrl+Break or Ctrl+Alt+Delete; the former is suggested in the hook documentation, and the latter is appropriate when you want the system to forcibly cancel any active journaling hooks (which is how TESTREC.EXE does its magic). Before you go off and start recording a million scripts, you'll need to do a little planning to take complete advantage of Tester. Although Tester handles mouse recording and playback, your scripts will be much more robust if you can do as much work as possible with keystrokes. One nice feature is that when recording, Tester works hard to keep track of the window with the focus. By default, for single mouse clicks and double-clicks, Tester will generate code to set the focus to the top-level window before processing the click or clicks. In addition, when recoding with keystrokes, Tester monitors Alt+Tab combinations to set the focus when you finish shifting focus. Since mouse recording can generate a million statements in a script, TESTREC.EXE, by default, records only single mouse clicks, double-clicks, and drag actions every 50 pixels. Of course, I did the right thing and allowed you to specify exactly how you want your scripts recorded. Figure 16-1 shows TESTREC.EXE's Script Recording Options dialog box, which is accessible by pressing Ctrl+T or selecting Script Options from the Scripts menu. All items shown are the default values.
Figure 16-1: Script recording options for Tester The first thing you'll notice at the top of the Script Recording Options dialog box is the option to choose JScript or VBScript for new scripts. The Record For Multiple Monitor Playback check box, which is checked by default, will insert calls to TSystem.CheckVirtualResolution to ensure the screen size for the rest of the recording. If you uncheck this option, any point that falls outside the primary monitor for mouse clicks or windows locations will abort recording. You might want to turn off multiple monitor recording if you plan to use recorded scripts on different machines. However, if you're recording scripts that only you will be running, leave the multiple monitor recording turned on so that you can take advantage of multiple monitors. For scripts in which you will be doing a lot of click-and-drags and you want to record all the mouse movements between the mouse down and release actions, set the Minimum Pixels To Drag Before Generating A MOVETO value to 0. If your recording will be doing a lot of clicks in the application without shifting focus to other applications, you'll want to uncheck Record Focus Changes With Mouse Clicks And Double Clicks. That will keep TESTREC.EXE from generating the code to force the focus each time the mouse is clicked and will make your script much smaller. The Do Child Focus Attempt In Scripts option will add code that attempts to set the focus to a specific control or child window you click on. I left this option off by default because I was already generating the statements to set the focus to the top-level window. Although applications like Notepad have only a single child window, other applications have deeply nested window hierarchies, and it can be difficult to track down child windows when all the parents don't have titles and unique classes. For an example, use Spy++ to look at the Visual Studio .NET editor window hierarchy. I found that setting the top window level focus before generating the click code usually worked perfectly fine. 581
Finally, the Seconds To Wait Before Inserting SLEEP Statements option automatically inserts pauses in the script longer than a specific value in seconds. Most of the time, you'll want to let your scripts run as quickly as possible, but to help keep scripts coordinated, the extra pause time can help. The recording and playback for Tester support the same format as the .NET System.Windows.Forms.SendKeys class, except for the repeating keys option. To handle the mouse, I extended the format to allow for it as well as the format modifiers necessary to allow Ctrl, Alt, and Shift key utilization in conjunction with it. Table 16-1 shows the format of the mouse commands to TInput.PlayInput.
Table 16-1: The Mouse Commands to TInput.PlayInput Command MOVETO BTNDOWN BTNUP CLICK DBLCLICK SHIFT DOWN SHIFT UP CTRL DOWN CTRL UP ALT DOWN ALT UP Usage {MOVETO x , y} {BTNDOWN btn , x , y} {BTNUP btn , x , y} {CLICK btn , x , y} {DBLCLICK btn , x , y} {SHIFT DOWN} {SHIFT UP} {CTRL DOWN} {CTRL UP} {ALT DOWN} {ALT UP} btn: LEFT, RIGHT, MIDDLE x: X screen coordinate value y: Y screen coordinate value There were a few items I wasn't able to add to the mouse recording. The first was mouse wheel processing. I used a journal hook to capture keystrokes and mouse operations, and the mouse wheel message came through. Unfortunately, a bug in the journal hook reporting doesn't pass the mouse wheel direction, so there's no way to know whether you're scrolling up or down. The second item I couldn't process was the new X1 and X2 buttons found on the newer Microsoft Explorer mouse. These WM_XBUTTON* messages pass which button was pressed in the high order word of the wParam. Since the WM_MOUSEWHEEL message does the same thing with the direction, but the journal record hook doesn't receive it, I doubt the X button would come through either. Implementing Tester Now that you have an idea of how to use both sides of Tester to record and play back your automation scripts, I want to go over some of the high points of the implementation. If you add up the source and build sizes of the Tester source code and binaries, which include both TESTER.DLL and TESTREC.EXE, you'll see that Tester is the biggest utility in 582
this book. Not only is it the biggest, but it's easily the most complicated because of COM, parsing recursion, and background timers. The TESTER.DLL Notification and Playback Implementation In the first version of this book, I implemented TESTER.DLL in Visual Basic 6, because that was the hot COM programming language and environment de jour. However, requiring you to keep Visual Basic 6 installed just to compile a single COM DLL didn't seem like a great idea. My first inclination was to move the TESTER.DLL code over to .NET. Since some of the core code, specifically the portion that played back keystrokes, was in C++, I thought it'd be easier to re-implement the Visual Basic 6 portion of Tester in C++ and take advantage of the new attributed COM programming. In all, attributed COM is quite nice, but it did take me a while to find the idl_quote attribute to get my forward interface declarations to work. One very pleasant surprise with the attributed COM was how clean everything felt when combining the IDL/ODL and the C++ code. Additionally, the hugely improved wizards made it a snap to add interfaces and methods and properties to those interfaces. I certainly remember my fair share of times when the wizards broke in prior releases of Visual Studio. Back when I first started thinking about doing an automated playback utility, I thought I could use the original SendKeys statement from Visual Basic 6. After a bit of testing, I found that that implementation didn't suffice, because it did not correctly send keystrokes to programs such as Microsoft Outlook. That meant I needed to implement my own version that would properly send the keystrokes and allow mouse input in the future. Fortunately, I ran across the SendInput function, which is part of Microsoft Active Accessibility (MSAA) and replaces all the previous low-level event functions, such as keybd_event. It also places all the input information in the keyboard or mouse input stream as a contiguous unit, ensuring that your input isn't interspersed with any extraneous user input. This functionality was especially attractive for Tester. Once I knew how to send the keystrokes properly, I needed to develop the keystroke input format. Because the Visual Basic 6 SendKeys statement or .NET System.Windows.Forms.SendKeys class already provides a nice input format, I thought I'd duplicate it for my PlayInput function. I used everything but the repeat key code, though as I mentioned earlier, I extended the format to support mouse playback as well. There's nothing too thrilling about the parsing code if you want to see it, look in the Tester\Tester\ParsePlayInputString.CPP file that accompanies the sample files for this book. Additionally, if you want to see the code in action, you might want to debug through the ParsePlayKeysTest program in the Tester\Tester\Tests\ParsePlayKeysTest directory. As you can tell by the name, this program is one of the unit tests for the Tester DLL. The TWindow, TWindows, and TSystem objects are straightforward, and you should be able to understand them just by reading their source code. These three classes are essentially wrappers around the appropriate Windows API functions. The only slightly interesting portion of the implementation was writing the code to ensure the TWindow.SetFocusTWindow and TSystem.SetSpecificFocus methods could bring a window to the foreground. Those functions entailed attaching to the input thread by using the AttachThreadInput API before being able to set the focus. I ran into some interesting obstacles in the TNotify class. When I first started thinking about what it would take to determine whether a window with a specific caption was created or destroyed, I didn't expect that creating such a class would be too hard. I discovered that not only was the job moderately difficult, but the window creation notifications can't be made foolproof without heroic effort. 583
My first idea was to implement a systemwide computer-based training (CBT) hook. The SDK documentation seemed to say that a CBT hook was the best method for determining when windows are created and destroyed. I whipped up a quick sample but soon hit a snag. When my hook got the HCBT_CREATEWND notification, I couldn't retrieve the window caption consistently. After I thought about the problem a bit, it started to make sense; the CBT hook is probably called as part of the WM_CREATE processing, and very few windows have set their captions at that point. The only windows I could get reliably with the HCBT_CREATEWND notification were dialog boxes. The window destruction surveillance always worked with the CBT hook. After looking through all the other types of hooks, I extended my quick sample to try them all. As I suspected, just watching WM_CREATE wasn't going to tell me the caption reliably. A friend suggested that I watch only the WM_SETTEXT messages. Eventually, to set the caption in a title bar, almost every window will use a WM_SETTEXT message. Of course, if you're doing your own non-client painting and bit blitting, you won't use the WM_SETTEXT message. One interesting behavior I did notice was that some programs, Microsoft Internet Explorer in particular, post WM_SETTEXT messages with the same text many times consecutively. Having figured out that I needed to watch WM_SETTEXT messages, I took a harder look at the different hooks I could use. In the end, the call window procedure hook (WH_CALLWNDPROCRET) was the best choice. It allows me to watch WM_CREATE and WM_SETTEXT messages easily. I can also watch WM_DESTROY messages. At first, I expected to have some trouble with WM_DESTROY because I thought that the window caption might have been deallocated by the time this message showed up. Fortunately, the window caption is valid until the WM_NCDESTROY message is received. After considering the pros and cons of handling WM_SETTEXT messages only for windows that didn't yet have a caption, I decided to just go ahead and process all WM_SETTEXT messages. The alternative would've involved writing a state machine to keep track of created windows and the times they get their captions set, and this solution sounded error prone and difficult to implement. The drawback to handling all WM_SETTEXT messages is that you can receive multiple creation notifications for the same window. For example, if you set a TNotify handler for windows that contained "Notepad" anywhere in their captions, you'd get a notification when NOTEPAD.EXE launched, but you'd also get a notification every time NOTEPAD.EXE opened a new file. In the end, I felt it was better to accept a less-than-optimal implementation rather than spend days and days debugging the "correct" solution. Also, writing the hook was only about a quarter of the implementation of the final TNotify class; the other three-quarters addressed the problem of how to let the user know that the window was created or destroyed. Earlier, I mentioned that using the TNotify object isn't completely hands-off and that you have to call the CheckNotification method every once in a while. The reason you have to call CheckNotification periodically is that Tester supports only the apartment threading model that can't be multithreaded, and I needed a way to check whether a window was created or destroyed and still use the same thread in which the rest of Tester was running. After sketching out some ideas about the notification mechanisms, I narrowed down the implementation needs to the following basic requirements: The WH_CALLWNDPROCRET hook has to be systemwide, so it must be implemented in its own DLL. The Tester DLL obviously can't be that DLL because I don't want to drag the entire Tester DLL and, in turn, all the COM code into each address space on the user's 584
computer. This condition means that the hook DLL probably has to set a flag or something that the Tester DLL can read to know that a condition is met. Tester can't be multithreaded, so I need to do all the processing in the same thread. The first ramification of the basic requirements is that the hook function had to be written in C. Because the hook function is loaded into all address spaces, the hook DLL couldn't call TESTER.DLL that were written in apartment-threaded COM. any functions in the Consequently, my code would need to check the results of the hook-generated data periodically. If you've ever developed 16-bit Windows applications, you know that getting some background processing done in a single-threaded, non-preemptive environment seems like the perfect job for the SetTimer API function. With SetTimer, you can get the background-processing capabilities yet still keep your application single-threaded. Consequently, I set up a timer notification as part of the TNotify object to determine when the windows I needed to monitor were created or destroyed. What made the TNotify background processing interesting was that the timer procedure solution seemed like the answer, but in reality, it usually works only in the TNotify case. Depending on the length of the script and on whether your language of choice implements a message loop, the WM_TIMER message might not get through, so you'll need to call the CheckNotification method, which checks the hook data as well. All these implementation details might seem confusing, but you'll be surprised at how little code it really takes to implement Tester. Listing 16-3 shows the hook function code from TNOTIFYHLP.CPP. On the Tester side, TNOTIFY.CPP is the module in which the timer procedure resides along with the COM code necessary for the object. The TNotify class has a couple of C++ methods that the TNotify object can access to get the events fired and to determine what types of notifications the user wants. The interesting part of the hook code is the globally shared data segment, .HOOKDATA, which holds the array of notification data. When looking at the code, keep in mind that the notification data is global but all the rest of the data is on a per-process basis. Listing 16-3: TNOTIFYHLP.CPP /*--------------------------------------------------------------------Debugging Applications for Microsoft .NET and Microsoft Windows Copyright 1997-2003 John Robbins -- All rights reserved. ---------------------------------------------------------------------*/ #include "stdafx.h" /*///////////////////////////////////////////////////////////////// ///// File Scope Defines and Constants /////////////////////////////////////////////////////////////////// ///*/ // The maximum number of notification slots static const int TOTAL_NOTIFY_SLOTS = 5 ; // The mutex name 585
Copyright © . All rights reserved.