Sunday, April 15, 2007

Callback and Subclassing





Well today I thought of writing something which could be of some use to someone. So I thought of writing on one of my faviorite subjects......Visual Basic. I have been loving VB all my college life because of its ease of use and the power with which it comes. its a really cool language which can help you in getting the most out of a Windows PC. Today i'm gonna write on Callback and subclassing using Windows VB by the means of windows API. Read on......


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_
As Long, ByVal uElapse As Long, ByVal lpTimerFunc As Long) As Long

Declare Function KillTimer Lib "user32" (ByVal hWnd As Long, _
ByVal nIDEvent As Long) As Long

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
' Create a timer that sends a notification every 500 milliseconds.
timerID = SetTimer(0, 0, 500, AddressOf Timer_CBK)

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.
KillTimer 0, timerID

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, _
ByVal idEvent As Long, ByVal SysTime As Long)
' Just display the system time in a label control.
Form1.lblTimer = SysTime
End Sub

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, _
ByVal lParam As Long) As Long

Declare Function EnumChildWindows Lib "user32" (ByVal hWndParent As Long, _
ByVal lpEnumFunc As Long, ByVal lParam As Long) 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
' Process the window's data here.
End Function

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
Dim windows() As Long
' The number of elements in the array.
Dim windowsCount As Long

' Return an array of Longs holding the handles of all the child windows
' of a given window. If hWnd = 0, return the top-level windows.
Function ChildWindows(ByVal hWnd As Long) As Long()
windowsCount = 0 ' Reset the result array.
If hWnd Then
EnumChildWindows hWnd, AddressOf EnumWindows_CBK, 1
Else
EnumWindows AddressOf EnumWindows_CBK, 1
End If
' Trim uninitialized elements and return to caller.
ReDim Preserve windows(windowsCount) As Long
ChildWindows = windows()
End Function

' The callback routine, common to both EnumWindows and EnumChildWindows
Function EnumWindows_CBK(ByVal hWnd As Long, ByVal lParam As Long) As Long
If windowsCount = 0 Then
' Create the array at the first iteration.
ReDim windows(100) As Long
ElseIf windowsCount >= UBound(windows) Then
' Make room in the array, if necessary.
ReDim Preserve windows(windowsCount + 100) As Long
End If
' Store the new item.
windowsCount = windowsCount + 1
windows(windowsCount) = hWnd
' Return 1 to continue the enumeration process.
EnumWindows_CBK = 1
End Function

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()
ShowWindows TreeView1, 0, Nothing
End Sub

Sub ShowWindows(tvw As TreeView, ByVal hWnd As Long, ParentNode As Node)
Dim winHandles() As Long
Dim i As Long, Node As MSComctlLib.Node

If ParentNode Is Nothing Then
' If no Parent node, let's add a "desktop" root node.
Set ParentNode = tvw.Nodes.Add(, , , "Desktop")
End If
' Retrieve all the child windows.
winHandles() = ChildWindows(hWnd)
For i = 1 To UBound(winHandles)
' Add a node for this child window--WindowDescription is a routine
' (not shown here) that returns a descriptive string for the window.
Set Node = tvw.Nodes.Add(ParentNode.Index, tvwChild, , _
WindowDescription(winHandles(i)))
' Recursively call this routine to show this window's children.
ShowWindows tvw, winHandles(i), Node
Next
End Sub



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, _
ByVal wParam As Long, ByVal lParam As Long) As Long
...
End Function

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
Declare Function SetWindowLong Lib "user32" Alias "SetWindowLongA" _
(ByVal hWnd As Long, ByVal ndx As Long, ByVal newValue As Long) As Long

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
Dim oldProcAddr As Long ' The address of the original window procedure

Sub StartSubclassing(ByVal hWnd As Long)
saveHWnd = hWnd
oldProcAddr = SetWindowLong(hWnd, GWL_WNDPROC, AddressOf WndProc)
End Sub

Sub StopSubclassing()
SetWindowLong saveHWnd, GWL_WNDPROC, oldProcAddr
End Sub

Function WndProc(ByVal hWnd As Long, ByVal uMsg As Long, _
ByVal wParam As Long, ByVal lParam As Long) As Long
' Process the incoming messages here.
End Function

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" _
(ByVal lpPrevWndFunc As Long, ByVal hwnd As Long, ByVal Msg As Long, _
ByVal wParam As Long, ByVal lParam As Long) As Long

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, _
ByVal wParam As Long, ByVal lParam As Long) As Long
' Send the message to the original window procedure, and then
' return to Windows the return value from the original procedure.
WndProc = CallWindowProc(oldProcAddr, hWnd, uMsg, wParam, lParam)
' See if this is the message we're waiting for.
If uMsg = WM_MOVE Then
' The window has moved.
End If
End Function

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
Event AfterMessage(ByVal hWnd As Long, ByVal uMsg As Long, _
ByVal wParam As Long, ByVal lParam As Long, retValue As Long)

Private m_hWnd As Long ' Handle of the window being subclassed

' Start the subclassing.
Sub StartSubclass(ByVal hWnd As Long)
' Terminate current subclassing, if necessary.
If m_hWnd Then StopSubclass
' Store argument in member variable.
m_hWnd = hWnd
' Add a new item to the list of subclassed windows.
If m_hWnd Then HookWindow Me, m_hWnd
End Sub

' Stop the subclassing.
Sub StopSubclass()
' Delete this item from the list of subclassed windows.
If m_hWnd Then UnhookWindow Me
End Sub

' This procedure is called when a message is sent to this window.
' (It's Friend because it's meant to be called by the BAS module only.)
Friend Function WndProc(ByVal hWnd As Long, ByVal uMsg As Long, _
ByVal wParam As Long, ByVal lParam As Long, _
ByVal oldWindowProc As Long) As Long

Dim retValue As Long, Cancel As Boolean
' Call original window procedure.
retValue = CallWindowProc(oldWindowProc, hWnd, uMsg, wParam, lParam)
' Call the application.
' The application can modify the retValue argument, if desired.
RaiseEvent AfterMessage(hWnd, uMsg, wParam, lParam, retValue)
' Return the value to Windows.
WndProc = retValue
End Function

' Stop the subclassing when the object goes out of scope.
Private Sub Class_Terminate()
If m_hWnd Then StopSubclass
End Sub

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
Type WindowInfoUDT
hWnd As Long ' Handle of the window being subclassed
oldWndProc As Long ' Address of the original window procedure
obj As MsgHook ' The MsgHook object serving this window
End Type
' This array stores data on subclassed windows.
Dim WindowInfo() As WindowInfoUDT
' This is the number of elements in the array.
Dim WindowInfoCount As Long

The HookWindow and UnhookWindow procedures are called by the MsgHook class's StartSubclass and StopSubclass methods, respectively:

' Start the subclassing of a window.
Sub HookWindow(obj As MsgHook, ByVal hWnd As Long)
' Make room in the array, if necessary.
If WindowInfoCount = 0 Then
ReDim WindowInfo(10) As WindowInfoUDT
ElseIf WindowInfoCount > UBound(WindowInfo) Then
ReDim Preserve WindowInfo(WindowInfoCount + 9) As WindowInfoUDT
End If
WindowInfoCount = WindowInfoCount + 1

' Store data in the array, and start the subclassing of this window.
With WindowInfo(WindowInfoCount)
.hWnd = hWnd
Set .obj = obj
.oldWndProc = SetWindowLong(hWnd, GWL_WNDPROC, AddressOf WndProc)
End With
End Sub

' Stop the subclassing of the window associated with an object.
Sub UnhookWindow(obj As MsgHook)
Dim i As Long, objPointer As Long
For i = 1 To WindowInfoCount
If WindowInfo(i).obj Is obj Then
' We've found the object that's associated with this window.
SetWindowLong WindowInfo(i).hWnd, GWL_WNDPROC, _
WindowInfo(i).oldWndProc
' Remove this element from the array.
WindowInfo(i) = WindowInfo(WindowInfoCount)
WindowInfoCount = WindowInfoCount - 1
Exit For
End If
Next
End Sub

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
Function WndProc(ByVal hWnd As Long, ByVal uMsg As Long, _
ByVal wParam As Long, ByVal lParam As Long) As Long
Dim i As Long, obj As MsgHook
Const WM_DESTROY = &H2

' Find this handle in the array.
For i = 1 To WindowInfoCount
If WindowInfo(i).hWnd = hWnd Then
' Notify the object that a message has arrived.
WndProc = WindowInfo(i)obj.WndProc(hWnd, uMsg, wParam, lParam,_
WindowInfo(i).oldWndProc)
' If it's a WM_DESTROY message, the window is about to close
' so there is no point in keeping this item in the array.
If uMsg = WM_DESTROY Then WindowInfo(i).obj.StopSubclass
Exit For
End If
Next
End Function

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

Private Sub Form_Load()
Set FormHook = New MsgHook
FormHook.StartSubclass Me.hWnd
End Sub

Private Sub FormHook_AfterMessage(ByVal hWnd As Long, ByVal uMsg As Long, _
ByVal wParam As Long, ByVal lParam As Long, retValue As Long)
Const WM_MOVE = &H3
If uMsg = WM_MOVE Then
lblStatus.Caption = "The window has moved."
End If
End Sub

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
X As Long
Y As Long
End Type
Type MINMAXINFO
ptReserved As POINTAPI
ptMaxSize As POINTAPI
ptMaxPosition As POINTAPI
ptMinTrackSize As POINTAPI
ptMaxTrackSize As POINTAPI
End Type

Private Sub FormHook_AfterMessage(ByVal hWnd As Long, ByVal uMsg As Long, _
ByVal wParam As Long, ByVal lParam As Long, retValue As Long)
Select Case uMsg
Case WM_GETMINMAXINFO
' Windows is querying the form for its
' minimum and maximum size and position.
Dim mmInfo As MINMAXINFO
' Read contents of structure pointed to by lParam.
CopyMemory mmInfo, ByVal lParam, Len(mmInfo)
With mmInfo
' ptMaxSize is the size of the maximized form.
.ptMaxSize.X = 600
.ptMaxSize.Y = 400
' ptMaxPosition is the position of the maximized form.
.ptMaxPosition.X = 100
.ptMaxPosition.Y = 100
' ptMinTrackSize is the minimum size of a form when
' resized with the mouse.
.ptMinTrackSize.X = 300
.ptMinTrackSize.Y = 200
' ptMinTrackSize is the maximum size of a form when
' resized with the mouse (usually equal to ptMaxSize).
.ptMaxTrackSize.X = 600
.ptMaxTrackSize.Y = 400
End With
' Copy the data back into the original structure in memory.
CopyMemory ByVal lParam, mmInfo, Len(mmInfo)
' Return 0 to say that the structure has been modified.
retValue = 0
End Select
End Sub

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.
Case WM_MENUSELECT
' The menu item identifier is in the low-order word of wParam.
' The menu handle is in lParam.
Dim mnuId As Long, mnuCaption As String, length As Long
mnuId = (wParam And &HFFFF&)
' Get the menu caption.
mnuCaption = Space$(256)
length = GetMenuString(lParam, mnuId, mnuCaption, Len(mnuCaption), 0)
mnuCaption = Left$(mnuCaption, length)
Select Case mnuCaption
Case "&New"
lblStatus.Caption = "Create a new file"
Case "&Open"
lblStatus.Caption = "Open an existing file"
Case "&Save"
lblStatus.Caption = "Save a file to disk"
Case "E&xit"
lblStatus.Caption = "Exit the program"
End Select

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.
Case WM_COMMAND
' If this is a notification from a control, lParam holds its handle.
If lParam = txtEditor.hwnd Then
' In this case, the notification message is in the
' high-order word of wParam.
Select Case (wParam \ &H10000)
Case EN_HSCROLL
' The TextBox control has been scrolled horizontally.
Case EN_VSCROLL
' The TextBox control has been scrolled vertically.
End Select
End If

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

Private Sub Form_Load()
Set TextBoxHook = New MsgHook
TextBoxHook.StartSubclass txtEditor.hWnd
End Sub

Private Sub TextBoxHook_BeforeMessage(hWnd As Long, uMsg As Long, _
wParam As Long, lParam As Long, retValue As Long, Cancel As Boolean)
If uMsg = WM_CONTEXTMENU Then
' Show a custom popup menu.
PopupMenu mnuMyCustomPopupMenu
' Cancel the default processing (i.e. the default context menu).
Cancel = True
End If
End Sub

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

Featured Post

Domain and blog for Sale