Introduction
Sunday, October 14, 2001
Subclassing isn't hard to set up, but rather difficult to encapsulate. I've worked on a component that takes care of the details, and helps avoid some traps. Basicly, the client implements an interface, and passes a reference to it to the component. The client handles or modifies the messages he's interested in, and signals whether the message is handled. The component automatically removes the hook when the window is destroyed.
You can download the source as well as the binary. The DLL is compiled against VB runtime files that were updated by my Visual Studio .NET Beta 2 install, so you (probably) need to get these dependencies if you want to use it "as is" (from the download page). I recommend, however, that you recompile the source, using the DLL as a mirror typelib for binary compatibility.
Using the DLL
To set up a form for subclassing, you need implement the ISubClass interface as follows:
Implements ISubClass Private Property Get ISubClass_Handle() As Long ISubClass_Handle = Me.hWnd End Sub Private Sub ISubClass_WndProc(ByVal message As CMessage) ' handle the messages you're interested in End Sub
You'll notice a few things here:
- The Handle property returns the window handle of the form or control whose window procedure is to be intercepted. This need not be identical to the window handle of the form instance that is called back via the ISubClass interface reference. In other words, you can subclass any control on the form, and use the form's class module for the message handling. Likewise, you can use an "ordinary" class for subclassing a form or control. However, you can only handle the messages of one form or control instance in one instance of the class that implements the ISubClass interface; every window handle must correspond to one object.
- The WndProc subroutine has a different signature than you might expect. You'll see this kind of thing in VB.NET, as well as in C# or in Delphi. The message is passed as a class instance here, which saves stack space and makes things clearer. This design also allows enhancements to the message handling procedure without breaking the interface. The CMessage class has properties for the four parameters of the window procedure, as well as "Result" and "Handled" members. If you don't handle a particular message, just leave it at that; the DLL will check this property and call the original window procedure as a fallback. If you do handle the message, just assign True to this property. In some cases, you might also modify the message, but leave the "Handled" property as it is (False), so the original window procedure will be called with modified arguments.
The DLL allows you to subclass as many windows as you like. It keeps references to the callback interfaces, as well as other information in an internal collection. All messages are routed to one procedure (this is due the limitations of the AddressOf operator), and then dispatched to the implementor according to the window handle, which is used as a key in the collection.
You add a window this way:
SubClass.GMsgSink.Recipients.Add(Me)
This is a little verbose, as GMsgSink is a global class and you don't normally need to reference the library (I show you this for clarity only).
Likewise, you can remove a window from the collection, automatically reassigning the original window procedure. If a window is closed, the subclassing hook will be removed automatically as well, so you don't need to worry about this (although it's good practice to be explicit about it).
Some internals
Here is the procedure that dispatches the messages to the handler objects. This is the first routine that will be called with a message:
Public Function SinkFunk(ByVal hWnd As Long, _ ByVal wMsg As Long, _ ByVal wParam As Long, _ Byval lParam As Long) As Long ' find recipient object in collection Dim recipient As CRecipient Set recipient = m_Recipients.ItemByHandle(hWnd) If Not recipient Is Nothing Then ' call client (will invoke old WndProc if not handled) Dim message As New CMessage message.ConstructObject hWnd, wMsg, wParam, lParam recipient.CallRecipient message ' return result SinkFunc = message.Result ' unhook on destruction If wMsg = WM_DESTROY Then m_Recipients.Remove(recipient) End If End If End Function
CRecipient objects keep pointers to the original window procedure, as well as the interface refercences to the handler objects. They take care of the hooking and unhooking, and check whether the client has handled the messages:
Friend Sub CallRecipient(ByVal message As CMessage) Call m_Client.WndProc(message) If message.Handled = False Then If m_OldWndProc <> 0 Then message.Result = CallWndProc(m_OldWndProc, _ message.hWnd, _ message.wMsg, _ message.wParam, _ message.lParam) End If End If End Sub
You can figure out the rest in the source code.
A word of caution
When debugging subclassed applications, be prepared for general page faults. Do not hit the "End" button while debugging. Also, breakpoints and message boxes can kill VB. If possible, just use Debug.Print statements, and make sure the Immediate window is open at runtime.