[SubClass]

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 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.