|
Windows deals with two types of messages: control messages and notification messages. Although sending a control message is just a matter of using the SendMessage API function, you'll see that intercepting a notification message is much more difficult and requires that you adopt an advanced programming technique known as window subclassing. But in order to understand how this technique works, you need to know what the AddressOf keyword does and how you can use it to set up a callback procedure.
Callback Techniques
Callback and subclassing capabilities are relatively new to Visual Basic, in that they weren't possible until version 5. What made these techniques available to Visual Basic programmers was the introduction of the new AddressOf keyword under Visual Basic 5. This keyword can be used as a prefix for the name of a routine defined in a BAS module, and evaluates the 32-bit address of the first statement of that routine.
System timers
To show this keyword in action I'll show you how you can create a timer without a Timer control. Such a timer might be useful, for example, when you want to periodically execute a piece of code located in a BAS module, and you don't want to add a form to the application just to get a pulse at regular intervals. Setting up a system timer requires only a couple of API functions:
Declare Function SetTimer Lib "user32" (ByVal hWnd As Long, ByVal nIDEvent_ |
For our purposes, we can ignore the first two arguments to the SetTimer function and just pass the uElapse value (which corresponds to the Interval property of a Timer control) and the lpTimerFunc value (which is the address of a routine in our Visual Basic program). This routine is known as the callback procedure because it's meant to be called from Windows and not from the code in our application. The SetTimer function returns the ID of the timer being created or 0 in case of error:
Dim timerID As Long |
You need the return value when it's time to destroy the timer, a step that you absolutely must perform before closing the application if you don't want the program to crash:
' Destroy the timer created previously. |
Let's see now how to build the Timer_CBK callback procedure. You derive the number and types of the arguments that Windows sends to it from the Windows SDK documentation or from MSDN:
Sub Timer_CBK(ByVal hWnd As Long, ByVal uMsg As Long, _ |
In this implementation, you can safely ignore the first three parameters and concentrate on the last one, which receives the number of milliseconds elapsed since the system started. This particular callback routine doesn't return a value and is therefore implemented as a procedure; you'll see later that in most cases callback routines return values to the operating system and therefore are implemented as functions. As usual, you'll find on the companion CD a complete demonstration program that contains all the routines described in this section.
Windows enumeration
Interesting and useful examples of using callback techniques are provided by the EnumWindows and EnumChildWindows API functions, which enumerate the top-level windows and the child windows of a given window, respectively. The approach used by these functions is typical of most API functions that enumerate Windows objects. Instead of loading the list of windows in an array or another structure, these functions use a callback procedure in the main application for each window found. Inside the callback function, you can do what you want with such data, including loading it into an array, a ListBox or a TreeView control. The syntax for these functions is the following:
Declare Function EnumWindows Lib "user32" (ByVal lpEnumFunc As Long, _ |
hWndParent is the handle of the parent window. lpEnumFunc is the address of the callback function. And lParam is a parameter passed to the callback function; this value can be used when the same callback routine is used for different purposes in the application. The syntax of the callback function is the same for both EnumWindows and EnumChildWindows:
Function EnumWindows_CBK(ByVal hWnd As Long, ByVal lParam As Long) As Long |
where hWnd is the handle of the window found, and lParam is the value passed as the last argument to EnumWindows or EnumChildWindows. This function should return 1 to ask the operating system to continue the enumeration or 0 to stop the enumeration.
It's easy to create a reusable procedure that builds on these API functions to return an array with the handles of all the child windows of a given window:
' An array of Longs holding the handles of all child windows |
On the companion CD, you'll find the source code of an application—also shown in Figure A-8—that displays the hierarchy of all the windows that are currently open in the system. This is the code that loads the TreeView control with the window hierarchy. Thanks to the recursion technique, the code is surprisingly compact:
Private Sub Form_Load() |
Subclassing Techniques
Now that you know what a callback procedure is, comprehending how subclassing works will be a relatively easy job.
Basic subclassing
You already know that Windows communicates with applications via messages, but you don't know yet how the mechanism actually works at a lower level. Each window is associated with a window default procedure, which is called any time a message is sent to the window. If this procedure were written in Visual Basic, it would look like this:
Function WndProc(ByVal hWnd As Long, ByVal uMsg As Long, _ |
The four parameters that a window procedure receives are exactly the arguments that you (or the operating system) pass to the SendMessage when you send a message to a given window. The purpose of the window procedure is to process all the incoming messages and react appropriately. Each class of windows—top-level windows, MDI windows, TextBox controls, ListBox controls, and so on—behave differently because their window procedures are different.
The principle of the subclassing technique is very simple: You write a custom window procedure and you ask Windows to call your window procedure instead of the standard window procedure associated with a given window. The code in your Visual Basic application traps all the messages sent to the window before the window itself (more precisely, its default window procedure) has a chance to process them, as I explain in the following illustration:
To substitute the standard window procedure with your customized procedure, you must use the SetWindowLong API function, which stores the address of the custom routine in the internal data table that is associated with each window:
Const GWL_WNDPROC = -4 |
hWnd is the handle of the window. ndx is the index of the slot in the internal data table where you want to store the value. And newValue is the 32-bit value to be stored in the internal data table at the position pointed to by nxd. This function returns the value that was previously stored in that slot of the table; you must store such a value in a variable because you must definitely restore it before the application terminates or the subclassed window is closed. If you don't restore the address of the original window procedure, you're likely to get a GPF. In summary, this is the minimal code that subclasses a window:
Dim saveHWnd As Long ' The handle of the subclassed window |
Let's focus on what the custom window procedure actually does. This procedure can't just process a few messages and forget about the others. On the contrary, it's responsible for correctly forwarding all the messages to the original window procedure; otherwise, the window wouldn't receive all the vital messages that inform it when it has to resize, close, or repaint itself. In other words, if the window procedure stops all messages from reaching the original window procedure the application won't work as expected any longer. The API function that does the message forwarding is CallWindowProc:
Declare Function CallWindowProc Lib "user32" Alias "CallWindowProcA" _ |
lpPrevWndFunc is the address of the original window procedure—the value that we saved in the oldProcAddr variable—and the other arguments are those received by the custom window procedure.
Let's see a practical example of the subclassing technique. When a top-level window—a form, in Visual Basic parlance—moves, the operating system sends it a WM_MOVE message. The Visual Basic runtime eats this message without exposing it as an event to the application's code, but you can write a custom window procedure that intercepts it before Visual Basic sees it:
Function WndProc(ByVal hWnd As Long, ByVal uMsg As Long, _ |
I've prepared a demonstration program that uses the code described in this section to trap a few messages related to forms, such as WM_MOVE, WM_RESIZING, and WM_APPACTIVATE. (See Figure A-9.) The last message is important because it lets you determine when an application loses and regains the input focus, something that you can't easily do in pure Visual Basic code. For example, the Windows hierarchy utility shown in Figure A-8 might subclass this message to automatically refresh its contents when the user switches to another application and then goes back to the utility.
You can generally process the incoming messages before or after calling the CallWindowProc API function. If you're interested only in knowing when a message is sent to the window, it's often preferable to trap it after the Visual Basic runtime has processed it because you can query updated form's properties. Remember, Windows expects that you return a value to it, and the best way to comply with this requirement is by using the value returned by the original window procedure. If you process a message before forwarding it to the original procedure, you can change the values in wParam or lParam, but this technique requires an in-depth knowledge of the inner workings of Windows. Any error in this phase is fatal because it prevents the Visual Basic application from working correctly.
CAUTION
Of all the advanced programming techniques you can employ in Visual Basic, subclassing is undoubtedly the most dangerous one. If you make a mistake in the custom window procedure, Windows won't forgive you and won't give you a chance to fix the error. For this reason, you should always save your code before running the program in the environment. Moreover, you should never stop a running program using the End button, an action which immediately stops the running program and prevents the Unload and Terminate events from executing, therefore depriving you of the opportunity to restore the original window procedure.
A class for subclassing
Although the code presented in the previous version works flawlessly, it doesn't meet the requirements of real-world applications. The reason is simple: In a complex program, you usually subclass multiple forms and controls. This practice raises a couple of interesting problems:
- You can't use simple variables to store the window's handle and the address of the original window procedure—as the previous simplified example does—but you need an array or a collection to account for multiple windows.
- The custom window procedure must reside in a BAS form, so the same procedure must serve multiple subclassed windows and you need a way to understand which window each message is bound to.
The best solution to both problems is to build a class module that manages all the subclassing chores in the program. I've prepared such a class, named MsgHook, and as usual you'll find it on the companion CD. Here's an abridged version of its source code:
' The MsgHook.cls class module |
As you see, the class communicates with its clients through the AfterMessage event, which is called immediately after the original window procedure has processed the message. From the client application's standpoint, subclassing a window has become just a matter of responding to an event, an action very familiar to all Visual Basic programmers.
Now analyze the code in the BAS module in which the subclassing actually occurs. First of all, you need an array of UDTs, where you can store information about each window being subclassed:
' The WndProc.Bas module |
The HookWindow and UnhookWindow procedures are called by the MsgHook class's StartSubclass and StopSubclass methods, respectively:
' Start the subclassing of a window. |
The last procedure left to be seen in the BAS module is the custom window procedure. This procedure has to search for the handle of the target window of the incoming message, among those stored in the WindowInfo array and notify the corresponding instance of the MsgHook class that a message has arrived:
' The custom window procedure |
NOTE
The preceding code looks for the window handle in the array using a simple linear search; when the array contains only a few items, this approach is sufficiently fast and doesn't add a significant overhead to the class. If you plan to subclass more than a dozen forms and controls, you should implement a more sophisticated search algorithm, such as a binary search or a hash table.
In general, a window is subclassed until the client application calls the StopSubclass method of the related MsgHook object or until the object itself goes out of scope. (See the code in the class's Terminate event procedure.) The code in the WndProc procedure uses an additional trick to ensure that the original window procedure is restored before the window is closed. Because it's already subclassing the window, it can trap the WM_DESTROY message, which is always the last message (or at least one of the last messages) sent to a window before it closes. When this message is detected, the code immediately stops subclassing the window.
Using the MsgHook class
Using the MsgHook class is pretty simple: You assign an instance of it to a WithEvents variable, and then you invoke its StartSubclass method to actually start the subclassing. For example, you can trap WM_MOVE messages using this code:
Dim WithEvents FormHook As MsgHook |
If you want to subclass other forms or controls, you have to create multiple instances of the MsgHook class—one for each window to be subclassed—and assign them to distinct WithEvents variables. And of course you have to write the proper code in each AfterMessage event procedure. The complete class provided on the companion CD supports some additional features, including a BeforeMessage event that fires before the original window procedure processes the message and an Enabled property that lets you temporarily disable the subclassing for a given window. Keep in mind that the MsgHook class can subclass only windows belonging to the current application; interprocess window subclassing is beyond the current capabilities of the Visual Basic and requires some C/C++ wizardry.
The MsgHook class module encapsulates most of the dangerous details of the subclassing technique. When you turn it into an ActiveX DLL component—or use the version provided on the companion CD—you can safely subclass any window created by the current application. You can even stop an interpreted program without any adverse effects because the End button doesn't prevent the Terminate event from firing if the class has been compiled in a separate component. The compiled version also solves most—but not all—of the problems that occur when an interpreted code enters break mode, during which the subclassing code can't respond to messages. In such situations, you usually get an application crash, but the MsgHook class prevents it from happening. I plan to release a more complete version of this class, which I'll make available for download from my Web site at http://www.vb2themax.com.
More subclassing examples
Now that you have a tool that implements all the nitty-gritty details of subclassing, you might finally see how subclassing can actually help you deliver better applications. The examples I show in this section are meant to be just hints of what you can really do with this powerful technique. As usual, you'll find all the code explained in this section in a sample application provided on the companion CD and shown in Figure A-10.
Windows sends Visual Basic forms a lot of messages that the Visual Basic runtime doesn't expose as events. Sometimes you don't have to manipulate incoming parameters because you're subclassing the form only to find out when the message arrives. There are many examples of such messages, including WM_MOUSEACTIVATE (the form or control is being activated with the mouse), WM_TIMECHANGE (system date and time has changed), WM_DISPLAYCHANGE (the screen resolution has changed), WM_COMPACTING (Windows is low in memory, and is asking applications to release as much memory as possible), and WM_QUERYOPEN (a form is about to be restored to normal size from an icon).
Many other messages can't be dealt with so simply, though. For example, the WM_GETMINMAXINFO message is sent to a window when the user begins to move or resize it. When this message arrives, lParam contains the address of a MINMAXINFO structure, which in turn holds information about the region to which the form can be moved and the minimum and maximum size that the window can take. You can retrieve and modify this data, thus effectively controlling a form's size and position when the user resizes or maximizes it. (If you carefully look at Figure A-10, you'll see from the buttons in the window's caption that this form is maximized, even if it doesn't take the entire screen estate.) To move this information into a local structure, you need the CopyMemory API function:
Type POINTAPI |
By subclassing the WM_MENUSELECT message, you can add a professional touch to your application. This message fires whenever the user highlights a menu item using the mouse or arrow keys, and you can employ it for displaying a short explanation of the menu item, as most commercial programs do (as shown in Figure A-10). The problem with this message is that you have to process the values stored in wParam and lParam to extract the caption of the highlighted menu item:
' Put this code inside a FormHook_AfterMessage event procedure. |
WM_COMMAND is a multipurpose message that a form receives on many occasions—for example, when a menu command has been selected or when a control sends the form a notification message. You can trap EN_HSCROLL and EN_VSCROLL notification messages that TextBox controls send their parent forms when their edit area has been scrolled:
' Put this code inside a FormHook_AfterMessage event procedure. |
Of course, you can subclass any control that exposes the hWnd property, not just forms. For example, TextBox controls receive a WM_CONTEXTMENU message when the user right-clicks on them. The default action for this message is to display the default edit pop-up menu, but you can subclass the TextBox control to suppress this action so that you might display your own pop-up menu. To achieve this result, you need to write code in the BeforeMessage event procedure and you must set the procedure's Cancel parameter to False to ask the MsgHook class not to execute the original window procedure. (This is one of the few cases when it's safe to do so.)
Dim WithEvents TextBoxHook As MsgHook |
This appendix has taken you on quite a long journey through API territory. But as I told you at the beginning, these pages only scratch the surface of the immense power that Windows API functions give you, especially if you couple them with subclassing techniques. The MsgHook class on the companion CD is a great tool for exploring these features because you don't have to worry about the implementation details, and you can concentrate on the code that produces the effects you're interested in.
If you want to learn more about this subject, I can only suggest that you get a book, such as Visual Basic Programmer's Guide to the Win32 API by Dan Appleman, specifically on this topic. You should also always have the Microsoft Developer Network at hand for the official documentation of the thousands of functions that Windows exposes. Become an expert in API programming, and you'll see that there will be very little that you can't do in Visual Basic.
No comments:
Post a Comment