Implementing Tester

[Previous] [Next]

Now that you have an idea of how to use Tester, I want to go over some of the high points of its implementation. I first started to implement Tester using C++ and the Active Template Library (ATL), but then I realized that Visual Basic was a far better choice. Much of what I was going to implement in Tester is simple, and I just wanted to get the job done quickly. I ended up using Visual Basic, though as you'll see later in this section, I sometimes needed to take some odd twists and turns to make it work.

The first object I started implementing was TInput, which is responsible for all the input that you need to send to another window. Initially, I thought the keyboard input was going to be simple—I thought I'd just wrap the Visual Basic SendKeys statement. This approach worked fine when I sent some keys to Notepad, but when I tried to send keys to Microsoft Outlook 2000 I noticed that some didn't get through. I never did get the SendKeys statement to work, so I had to implement my own function, which I call PlayKeys. In the course of researching what I needed to include in my function, I noticed that Microsoft Windows 98 and Microsoft Windows 2000 have a neat new function: SendInput. For those of you who need to support Windows NT 4, Service Pack 3 and higher also implement the SendInput function. SendInput is part of Microsoft Active Accessibility (MSAA) and replaces all the previous low-level event functions, such as keybd_event. SendInput handles keyboard, mouse, and hardware events. 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. A quick test proved that SendInput also worked when sending input to Outlook 2000.

Once I knew how to send the keystrokes properly, I needed to develop the keystroke input format. Because the Visual Basic SendKeys statement already provides a nice input format, I thought I'd duplicate it for my PlayKeys function. I used everything but the repeat key code. There's nothing too thrilling about the parsing code—if you want to see it, look in the SourceCode\Tester\TInputHlp directory on the companion CD. When I started working on the TInput object, I was still intending to write Tester in C++. I wrote all the parsing code in a C++ dynamic-link library (DLL). The Visual Basic TInput PlayKeys method is just a wrapper to call the DLL.

The TWindow, TWindows, and TSystem objects are straightforward, and you should be able to understand them just by reading their source code. I implemented those three classes in Visual Basic; they're just wrappers around some Windows application programming interface (API) functions. 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 also that the window creation notifications can't be made foolproof without heroic effort.

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 just 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 nonclient 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 would be that you could receive multiple creation notifications for the same window. For example, if you set a TNotify handler for windows that contained "Notepad" anywhere in their caption, 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.

I made the decision to implement Tester in Visual Basic before I wrote the TNotify class. Earlier, I mentioned that using TNotify 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 Visual Basic 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 Visual Basic Tester DLL and, in turn, MSVBM60.DLL into each address space on the user's 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 itself couldn't call any functions in the TESTER.DLL written in Visual Basic. Consequently, my Visual Basic code would need to check the results of the hook-generated data periodically.

If you ever developed 16-bit Windows applications, you know that finding a way to get some background processing done in a single-threaded, nonpreemptive environment was the perfect job for the SetTimer API function. With SetTimer, you could get the background processing capabilities yet still keep your application single-threaded. Consequently, I set up a timer notification as part of the TNotify class to determine when 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 only almost works 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. In an attempt to make the checking automatic, I tried to set up the TSystem.Pause method to call DoEvents for the amount of time specified. Unfortunately, using DoEvents in TSystem.Pause was a major performance drag on the scripts, so I settled on just asking users to call the CheckNotification function every once in a while.

All these implementation details might seem confusing, but you'll be surprised at how little code it really takes to implement Tester. Listing 13-3 shows the hook function code from TNOTIFYHLP.CPP. On the Tester side, TNOTIFY.BAS is the module in which the timer procedure resides, and the actual class is implemented in TNOTIFY.CLS. The TNotify class has a couple of hidden methods and properties that the TNotify module 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 13-3 TNOTIFYHLP.CPP

/*---------------------------------------------------------------------- "Debugging Applications" (Microsoft Press) Copyright (c) 1997-2000 John Robbins -- All rights reserved. ------------------------------------------------------------------------ The main file for TNotifyHlp.dll ----------------------------------------------------------------------*/ #include <tchar.h> #include <windows.h> #include "TNotifyHlp.h" /*////////////////////////////////////////////////////////////////////// File Scope Defines and Constants //////////////////////////////////////////////////////////////////////*/ // The maximum number of notification slots static const int TOTAL_NOTIFY_SLOTS = 5 ; // The mutex name static const LPCTSTR k_MUTEX_NAME = _T ( "TNotifyHlp_Mutex" ) ; // The longest amount of time I'll wait on the mutex static const int k_WAITLIMIT = 5000 ; // I have my own trace here because I don't want to drag // BugslayerUtil.DLL into each address space. #ifdef _DEBUG #define TRACE ::OutputDebugString #else #define TRACE (void)0 #endif /*////////////////////////////////////////////////////////////////////// File Scope Typedefs //////////////////////////////////////////////////////////////////////*/ // The structure for an individual window to look for typedef struct tag_TNOTIFYITEM { // The PID for the process that created this item DWORD dwOwnerPID ; // The notification type int iNotifyType ; // The search parameter int iSearchType ; // The handle to the HWND being created HWND hWndCreate ; // The destroy Boolean BOOL bDestroy ; // The title string TCHAR szTitle [ MAX_PATH ] ; } TNOTIFYITEM , * PTNOTIFYITEM ; /*////////////////////////////////////////////////////////////////////// File Scope Global Variables //////////////////////////////////////////////////////////////////////*/ // This data is **NOT** shared across processes, so each process gets // its own copy. // The HINSTANCE for this module. Setting global system hooks requires // a DLL. static HINSTANCE g_hInst = NULL ; // The mutex that protects the g_NotifyData table static HANDLE g_hMutex = NULL ; // The hook handle. I don't keep this handle in the shared section because // multiple instances could set the hook when running multiple scripts. static HHOOK g_hHook = NULL ; // The number of items added by this process. This number lets me know // how to handle the hook. static int g_iThisProcessItems = 0 ; /*////////////////////////////////////////////////////////////////////// File Scope Function Prototypes //////////////////////////////////////////////////////////////////////*/ // Our happy hook LRESULT CALLBACK CallWndRetProcHook ( int nCode , WPARAM wParam , LPARAM lParam ) ; // The internal check function static LONG __stdcall CheckNotifyItem ( HANDLE hItem , BOOL bCreate ) ; /*////////////////////////////////////////////////////////////////////// Funky Shared Data Across All Hook Instances //////////////////////////////////////////////////////////////////////*/ #pragma data_seg ( ".HOOKDATA" ) // The notification items table static TNOTIFYITEM g_shared_NotifyData [ TOTAL_NOTIFY_SLOTS ] = { { 0 , 0 , 0 , NULL , 0 , '\0' } , { 0 , 0 , 0 , NULL , 0 , '\0' } , { 0 , 0 , 0 , NULL , 0 , '\0' } , { 0 , 0 , 0 , NULL , 0 , '\0' } , { 0 , 0 , 0 , NULL , 0 , '\0' } } ; // The master count static int g_shared_iUsedSlots = 0 ; #pragma data_seg ( ) /*////////////////////////////////////////////////////////////////////// EXTERNAL IMPLEMENTATION STARTS HERE //////////////////////////////////////////////////////////////////////*/ extern "C" BOOL WINAPI DllMain ( HINSTANCE hInst , DWORD dwReason , LPVOID /*lpReserved*/ ) { #ifdef _DEBUG BOOL bCHRet ; #endif BOOL bRet = TRUE ; switch ( dwReason ) { case DLL_PROCESS_ATTACH : // Set the global module instance. g_hInst = hInst ; // I don't need the thread notifications. DisableThreadLibraryCalls ( g_hInst ) ; // Create the mutex for this process. The mutex is created // here but isn't owned yet. g_hMutex = CreateMutex ( NULL , FALSE , k_MUTEX_NAME ) ; if ( NULL == g_hMutex ) { TRACE ( _T ( "Unable to create the mutex!\n" ) ) ; // If I can't create the mutex, I can't continue, so // fail the DLL load. bRet = FALSE ; } break ; case DLL_PROCESS_DETACH : // Check to see whether this process has any items in the // notification array. If it does, remove them to avoid // leaving orphaned items. if ( 0 != g_iThisProcessItems ) { DWORD dwProcID = GetCurrentProcessId ( ) ; // I don't need to grab the mutex here because only a // single thread will ever call with the // DLL_PROCESS_DETACH reason. // Loop through and take a gander. for ( int i = 0 ; i < TOTAL_NOTIFY_SLOTS ; i++ ) { if ( g_shared_NotifyData[i].dwOwnerPID == dwProcID ) { #ifdef _DEBUG TCHAR szBuff[ 50 ] ; wsprintf ( szBuff , _T( "DLL_PROCESS_DETACH removing : #%d\n" ), i ) ; TRACE ( szBuff ) ; #endif // Get rid of it. RemoveNotifyTitle ( (HANDLE)i ) ; } } } // Close the mutex handle. #ifdef _DEBUG bCHRet = #endif CloseHandle ( g_hMutex ) ; #ifdef _DEBUG if ( FALSE == bCHRet ) { TRACE ( "!!!!!!!!!!!!!!!!!!!!!!!!\n" ) ; TRACE ( "CloseHandle(g_hMutex) " "failed!!!!!!!!!!!!!!!!!!\n" ) ; TRACE ( "!!!!!!!!!!!!!!!!!!!!!!!!\n" ) ; } #endif break ; default : break ; } return ( bRet ) ; } HANDLE TNOTIFYHLP_DLLINTERFACE __stdcall AddNotifyTitle ( int iNotifyType , int iSearchType , LPCTSTR szString ) { // Ensure that the notify type range is correct. if ( ( iNotifyType < ANTN_DESTROYWINDOW ) || ( iNotifyType > ANTN_CREATEANDDESTROY ) ) { TRACE ( "AddNotify Title : iNotifyType is out of range!\n" ) ; return ( INVALID_HANDLE_VALUE ) ; } // Ensure that the search type range is correct. if ( ( iSearchType < ANTS_EXACTMATCH ) || ( iSearchType > ANTS_ANYLOCMATCH ) ) { TRACE ( "AddNotify Title : iSearchType is out of range!\n" ) ; return ( INVALID_HANDLE_VALUE ) ; } // Ensure that the string is valid. if ( TRUE == IsBadStringPtr ( szString , MAX_PATH ) ) { TRACE ( "AddNotify Title : szString is invalid!\n" ) ; return ( INVALID_HANDLE_VALUE ) ; } // Wait to acquire the mutex. DWORD dwRet = WaitForSingleObject ( g_hMutex , k_WAITLIMIT ) ; if ( WAIT_TIMEOUT == dwRet ) { TRACE ( _T( "AddNotifyTitle : Wait on mutex timed out!!\n" ) ) ; return ( INVALID_HANDLE_VALUE ) ; } // If the slots are used up, abort now. if ( TOTAL_NOTIFY_SLOTS == g_shared_iUsedSlots ) { ReleaseMutex ( g_hMutex ) ; return ( INVALID_HANDLE_VALUE ) ; } // Find the first free slot. for ( int i = 0 ; i < TOTAL_NOTIFY_SLOTS ; i++ ) { if ( _T ( '\0' ) == g_shared_NotifyData[ i ].szTitle[ 0 ] ) { break ; } } // Add this data. g_shared_NotifyData[ i ].dwOwnerPID = GetCurrentProcessId ( ) ; g_shared_NotifyData[ i ].iNotifyType = iNotifyType ; g_shared_NotifyData[ i ].iSearchType = iSearchType ; lstrcpy ( g_shared_NotifyData[ i ].szTitle , szString ) ; // Bump up the master count. g_shared_iUsedSlots++ ; // Bump up the count for this process. g_iThisProcessItems++ ; TRACE ( "AddNotifyTitle - Added a new item!\n" ) ; ReleaseMutex ( g_hMutex ) ; // If this is the first notification request, enable the hook. if ( NULL == g_hHook ) { g_hHook = SetWindowsHookEx ( WH_CALLWNDPROCRET , CallWndRetProcHook , g_hInst , 0 ) ; #ifdef _DEBUG if ( NULL == g_hHook ) { char szBuff[ 50 ] ; wsprintf ( szBuff , _T ( "SetWindowsHookEx failed!!!! (0x%08X)\n" ) , GetLastError ( ) ) ; TRACE ( szBuff ) ; } #endif } return ( (HANDLE)i ) ; } void TNOTIFYHLP_DLLINTERFACE __stdcall RemoveNotifyTitle ( HANDLE hItem ) { // Check the value. int i = (int)hItem ; if ( ( i < 0 ) || ( i > TOTAL_NOTIFY_SLOTS ) ) { TRACE ( _T ( "RemoveNotifyTitle : Invalid handle!\n" ) ) ; return ; } // Get the mutex. DWORD dwRet = WaitForSingleObject ( g_hMutex , k_WAITLIMIT ) ; if ( WAIT_TIMEOUT == dwRet ) { TRACE ( _T ( "RemoveNotifyTitle : Wait on mutex timed out!\n" ) ); return ; } if ( 0 == g_shared_iUsedSlots ) { TRACE ( _T ( "RemoveNotifyTitle : Attempting to remove when " "no notification handles are set!\n" ) ) ; ReleaseMutex ( g_hMutex ) ; return ; } // Before removing anything, make sure this index points to a // NotifyData entry that contains a valid value. If I // didn't check, you could call this function with the same value // over and over, which would mess up the used-slots counts. if ( 0 == g_shared_NotifyData[ i ].dwOwnerPID ) { TRACE ( "RemoveNotifyTitle : Attempting to double remove!\n" ) ; ReleaseMutex ( g_hMutex ) ; return ; } // Remove this item from the array. g_shared_NotifyData[ i ].dwOwnerPID = 0 ; g_shared_NotifyData[ i ].iNotifyType = 0 ; g_shared_NotifyData[ i ].hWndCreate = NULL ; g_shared_NotifyData[ i ].bDestroy = FALSE ; g_shared_NotifyData[ i ].iSearchType = 0 ; g_shared_NotifyData[ i ].szTitle[ 0 ] = _T ( '\0' ) ; // Bump down the master item count. g_shared_iUsedSlots-- ; // Bump down this process's item count. g_iThisProcessItems-- ; TRACE ( _T ( "RemoveNotifyTitle - Removed an item!\n" ) ) ; ReleaseMutex ( g_hMutex ) ; // If this is the last item for this process, unhook this process's // hook. if ( ( 0 == g_iThisProcessItems ) && ( NULL != g_hHook ) ) { if ( FALSE == UnhookWindowsHookEx ( g_hHook ) ) { TRACE ( _T ( "UnhookWindowsHookEx failed!\n" ) ) ; } g_hHook = NULL ; } } HWND TNOTIFYHLP_DLLINTERFACE __stdcall CheckNotifyCreateTitle ( HANDLE hItem ) { return ( (HWND)CheckNotifyItem ( hItem , TRUE ) ) ; } BOOL TNOTIFYHLP_DLLINTERFACE __stdcall CheckNotifyDestroyTitle ( HANDLE hItem ) { return ( (BOOL)CheckNotifyItem ( hItem , FALSE ) ) ; } /*////////////////////////////////////////////////////////////////////// INTERNAL IMPLEMENTATION STARTS HERE //////////////////////////////////////////////////////////////////////*/ static LONG __stdcall CheckNotifyItem ( HANDLE hItem , BOOL bCreate ) { // Check the value. int i = (int)hItem ; if ( ( i < 0 ) || ( i > TOTAL_NOTIFY_SLOTS ) ) { TRACE ( _T ( "CheckNotifyItem : Invalid handle!\n" ) ) ; return ( NULL ) ; } LONG lRet = 0 ; // Get the mutex. DWORD dwRet = WaitForSingleObject ( g_hMutex , k_WAITLIMIT ) ; if ( WAIT_TIMEOUT == dwRet ) { TRACE ( _T ( "CheckNotifyItem : Wait on mutex timed out!\n" ) ) ; return ( NULL ) ; } // If all slots are empty, there's nothing to do. if ( 0 == g_shared_iUsedSlots ) { ReleaseMutex ( g_hMutex ) ; return ( NULL ) ; } // Check the item requested. if ( TRUE == bCreate ) { // If the HWND value isn't NULL, return that value and NULL it // out in the table. if ( NULL != g_shared_NotifyData[ i ].hWndCreate ) { lRet = (LONG)g_shared_NotifyData[ i ].hWndCreate ; g_shared_NotifyData[ i ].hWndCreate = NULL ; } } else { if ( FALSE != g_shared_NotifyData[ i ].bDestroy ) { lRet = TRUE ; g_shared_NotifyData[ i ].bDestroy = FALSE ; } } ReleaseMutex ( g_hMutex ) ; return ( lRet ) ; } static void __stdcall CheckTableMatch ( int iNotifyType , HWND hWnd , LPCTSTR szTitle ) { // Grab the mutex. DWORD dwRet = WaitForSingleObject ( g_hMutex , k_WAITLIMIT ) ; if ( WAIT_TIMEOUT == dwRet ) { TRACE ( _T ( "CheckTableMatch : Wait on mutex timed out!\n" ) ) ; return ; } // The table shouldn't be empty, but never assume anything. if ( 0 == g_shared_iUsedSlots ) { ReleaseMutex ( g_hMutex ) ; TRACE ( _T ( "CheckTableMatch called on an empty table!\n" ) ) ; return ; } // Search through the table. for ( int i = 0 ; i < TOTAL_NOTIFY_SLOTS ; i++ ) { // Does this entry have something in it, and does the type of // notification match? if ( ( _T ( '\0' ) != g_shared_NotifyData[ i ].szTitle[ 0 ] ) && ( g_shared_NotifyData[ i ].iNotifyType & iNotifyType ) ) { BOOL bMatch = FALSE ; // Perform the match. switch ( g_shared_NotifyData[ i ].iSearchType ) { case ANTS_EXACTMATCH : // This is simple. if ( 0 == lstrcmp ( g_shared_NotifyData[i].szTitle , szTitle ) ) { bMatch = TRUE ; } break ; case ANTS_BEGINMATCH : if ( 0 == _tcsnccmp ( g_shared_NotifyData[i].szTitle , szTitle , strlen(g_shared_NotifyData[i].szTitle) ) ) { bMatch = TRUE ; } break ; case ANTS_ANYLOCMATCH : if ( NULL != _tcsstr ( szTitle , g_shared_NotifyData[i].szTitle ) ) { bMatch = TRUE ; } break ; default : TRACE ( _T ( "CheckTableMatch invalid "\ "search type!!!\n" ) ) ; ReleaseMutex ( g_hMutex ) ; return ; break ; } // Tell them, Johnny. Do we have a match? if ( TRUE == bMatch ) { // If this is a destroy notification, stick "1" in the // table. if ( ANTN_DESTROYWINDOW == iNotifyType ) { g_shared_NotifyData[ i ].bDestroy = TRUE ; } else { // Otherwise, stick the HWND in the table. g_shared_NotifyData[ i ].hWndCreate = hWnd ; } } } } ReleaseMutex ( g_hMutex ) ; } LRESULT CALLBACK CallWndRetProcHook ( int nCode , WPARAM wParam , LPARAM lParam ) { // Buffer for storing the window title TCHAR szBuff[ MAX_PATH ] ; // Always pass the message to the next hook before I do any // processing. This way I don't forget and I can do my processing // in peace. LRESULT lRet = CallNextHookEx ( g_hHook , nCode , wParam , lParam ) ; // The docs say never to mess around with a negative code, so I // don't. if ( nCode < 0 ) { return ( lRet ) ; } // Get the message structure. Why are there three (or more) // different message structures? What's wrong with consistently // using the stock ole MSG for all message/proc hooks? PCWPRETSTRUCT pMsg = (PCWPRETSTRUCT)lParam ; // No caption, no work to do LONG lStyle = GetWindowLong ( pMsg->hwnd , GWL_STYLE ) ; if ( WS_CAPTION != ( lStyle & WS_CAPTION ) ) { return ( lRet ) ; } // The WM_DESTROY messages are copacetic for both dialog boxes and // normal windows. Just get the caption and check for a match. if ( WM_DESTROY == pMsg->message ) { if ( 0 != GetWindowText ( pMsg->hwnd , szBuff , MAX_PATH ) ) { CheckTableMatch ( ANTN_DESTROYWINDOW , pMsg->hwnd , szBuff ) ; } return ( lRet ) ; } // Window creation isn't as clean as window destruction. // Get the window class. If it is a true dialog box, the // WM_INITDIALOG is all I need. if ( 0 == GetClassName ( pMsg->hwnd , szBuff , MAX_PATH ) ) { #ifdef _DEBUG TCHAR szBuff[ 50 ] ; wsprintf ( szBuff , _T ( "GetClassName failed for HWND : 0x%08X\n" ) , pMsg->hwnd ) ; TRACE ( szBuff ) ; #endif // There's not much point in going on. return ( lRet ) ; } if ( 0 == lstrcmpi ( szBuff , _T ( "#32770" ) ) ) { // The only message I need to check is WM_INITDIALOG. if ( WM_INITDIALOG == pMsg->message ) { // Get the caption of the dialog box. if ( 0 != GetWindowText ( pMsg->hwnd , szBuff , MAX_PATH ) ) { CheckTableMatch ( ANTN_CREATEWINDOW , pMsg->hwnd , szBuff ) ; } } return ( lRet ) ; } // That took care of true dialog boxes. Start figuring out what to do // for actual windows. if ( WM_CREATE == pMsg->message ) { // Very few windows set the title in WM_CREATE. // However, a few do and they don't use WM_SETTEXT, so I have // to check. if ( 0 != GetWindowText ( pMsg->hwnd , szBuff , MAX_PATH ) ) { CheckTableMatch ( ANTN_CREATEWINDOW , pMsg->hwnd , szBuff ) ; } } else if ( WM_SETTEXT == pMsg->message ) { // I always default to WM_SETTEXT because that's how captions // get set. Unfortunately, some applications, such as Internet // Explorer, seem to call WM_SETTEXT a bunch of times with the // same title. To keep this hook simple, I just report // the WM_SETTEXT instead of maintaining all sorts of weird, // hard-to-debug data structures that keep track of the windows // that called WM_SETTEXT previously. if ( NULL != pMsg->lParam ) { CheckTableMatch ( ANTN_CREATEWINDOW , pMsg->hwnd , (LPCTSTR)pMsg->lParam ) ; } } return ( lRet ) ; } 

Although the TNotify implementation was a brainteaser in some ways, I was pleased at how few troubles I experienced implementing it. If you do want to extend the hook code, be aware that debugging systemwide hooks isn't a simple endeavor. Although you can debug systemwide hooks with the Microsoft Visual C++ debugger, I've never tried it. I just use SoftICE. The other way you can debug systemwide hooks is to resort to printf-style debugging. Using DBGVIEW, you can watch all the OutputDebugString calls to see the state of your hook.

I did experience one annoying problem when developing Tester that appeared only on Windows 98. All my test code worked just fine on Windows NT 4 and Windows 2000, but on Windows 98 I couldn't get the TWindows collection filled. I was checking whether the HWND passed into the Add method was valid with IsWindow. A quick read of the documentation said that IsWindow returns a BOOL. My mistake was assuming that BOOL was TRUE for positive and FALSE for negative. I also like to use the positive form of conditionals, so I was using 1 = IsWindow(hWndT), which obviously didn't work. As you can guess, the different operating systems don't return the same value. It was a small problem, but I thought you could learn from it.



Debugging Applications
Debugging Applications for MicrosoftВ® .NET and Microsoft WindowsВ® (Pro-Developer)
ISBN: 0735615365
EAN: 2147483647
Year: 2000
Pages: 122
Authors: John Robbins

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net