Sub-Classing has two definitions. In the object-oriented sense, a sub-class is derived from a base class to extend or change its functionality. In the Non-object oriented sense, Sub-classing is used to refer to a technique in which you intercept a window's default message queue. You cannot intercept every message that is sent to a window in VB, because you must have the window's handle in order to intercept the messages. You'll already have the window created before you have access to the HWND property; therefore you'll miss certain messages.
What Is It?
Windows is a message based OS. Every window has a message queue. The opperating system posts messages in this queue for the window to process. The messages range in type from the user moving the mouse, to the window being destroyed. All windows (underneath) are sitting in a countinuosly running loop. In this loop the window checks for messages, then processes them. Consider the following psudo code:
If Message = CloseWindow Then
If MessageBox("Are You Sure You Want To Quit") = Yes Then
VB handles messages a little differently:
If Message = .... Then Call The VB's Event For the Message
But not all messages are processed on VB's low-level message loop. Some messages that are rarely used have been left out of VB becuase they slow the applications generated with VB. The Key Word here is Rarely. Some developers would still like to find out when one of these messages occur, So we must sub-class. From VB 1.0 to VB 4.0 there was no built in method for accessing the messages that were left out. VB 5.0 introduced a new token called AddressOf. The AddressOf operator gives us the specific memory location to the entry point of a function. When you create an application in VB, the application is loaded in to memory and executed. The AddressOf operator gives you the Address Of the entry point to the function in memory. This memory location is required by windows in order to invoke our function for the new message processing routine. The Following Information is not yet fully verified by Microsoft: It seems that the entry point to the function can be retrieved by using the VarPtr(FunctionName). This way VB keeps info in a structure (or type in VB) on every function in the application. One of these elements is the offset to the entry point of the function. This value can be retrieved (in long functions) with Long Address = VarPtr(Function) + 56 In VB 5, and Long Address = VarPtr(Function) + 64 in VB 6. (I haven't verified these offsets in vb 3 or 4, some one let me know?): End of Non-Confirmed Info. Windows takes this address and calls our function with it every time there is a new message to be processed. Windows requires our function to have a specified number and type of paramaters in our function so it can give us info on the new message. Here's how the function should look:
Public Function WindowProc(ByVal hWnd As Long, ByVal uMsg As Long, ByVal wParam As Long, ByVal lParam As Long)
The paramaters in the function are given to us by windows. Here's what the paramaters mean:
hWnd: Handle of the window thats getting the
uMsg: The new message
wParam & lParam: additional info on the message
Different messages are handled in different ways, and there are literally thousands of messages that windows process. Hence this document is not going into detail of how to handle them all, but rather get you pointed in the right direction on how to process those messages. Messages are normally not removed from the queue by your sub-class routine, instead action is normally taken on them and then processed by the default handler. For example when we get a message that says the form is about to painted, we will call the default paint routine first, then add our graphics. If we add our graphic first, the default routine will just over-write our graphic. If we don't call the default routine, the window will become distorted. This shows the importance of the default window procedure or DefWindowProc as the API name.
How Do I Use It?
You'll need a few declerations in a code module:
Declare Function SetWindowLong Lib
"user32" Alias "SetWindowLongA" (ByVal hWnd As Long, ByVal
nIndex As Long, ByVal dwNewLong As Long) As Long
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
Public Const GWL_WNDPROC = (-4)
I've created some routines to simplify the interception process:
Dim PrevProc As Long
Public Sub HookForm(F As Form)
PrevProc = SetWindowLong(F.hWnd, GWL_WNDPROC, AddressOf WindowProc)
Public Sub UnHookForm(F As Form)
SetWindowLong F.hWnd, GWL_WNDPROC, PrevProc
Public Function WindowProc(ByVal hWnd As Long, ByVal uMsg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
WindowProc = CallWindowProc(PrevProc, hWnd, uMsg, wParam, lParam)
The PrevProc variable is declared on the Heap or in the General section of the code module. The routine HookForm needs a little attention. Here we call the SetWindowLong API. This API has several uses, on of gives us the ability to "re-route" the default windows procedure (GWL_WNDPROC) to a different procedure (AddressOf WindowProc) for a specified window (F.hWnd). This function returns us the address to the original function (PrevProc). We must save this address for use later. You must always return the default window procedure to VB before closing the form or your app will crash, so we'll save it for later. The UnHookForm routine is where we return the procedure back to VB's default for the form. This call will return us AddressOf WindowProc like to line in HookForm, but we dont need it. Our WindowProc function (for right now) is simply taking the messages and passing them on to VB's original message handler. At this point the only thing we've added to our application is wait time. The VB message handler is always going to be faster than your VB message handler. Any additional work is just going to slow our application. Messages can be sent to our form at thousands a second, so be very cautious when intercepting them. You should only sub-class when necissary, and keep it simple. Windows requires different return values for different messages, so make sure you set your function to the correct return value before exiting the function. In this example we've just set the value to whatever value VB has set for the message. In this example we are joing to sub-class a single form. So we need to create a form. Here's the code:
Private Sub Form_Load()
Private Sub Form_Unload(Cancel As Integer)
That's all there is to it (almost). Here we've successfully sub classed a window, but it doesn't do anything. Next we'll need to figure out which messages we want to catch. In this example we'll use the paint message or WM_PAINT. We're going to draw a circle on the form every time the window is painted. Here's the code:
Public Function WindowProc(ByVal hWnd As
Long, ByVal uMsg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
WindowProc = CallWindowProc(PrevProc, hWnd, uMsg, wParam, lParam)
If uMsg = WM_PAINT Then Form1.Circle (100, 100), 100, vbRed
Make sure you compile this code and run the .EXE. VB will crash (under most circumstances) when you debug this code. Microsoft has a nice utility that simplifies debugging of sub-classed windows. You can download the dbgwproc.dll from their web site. Your application will also crash if you cause recursive events. For example If we substituted Form1.Circle (100, 100), 100, vbRed With Form1.Refresh, we would be causing another WM_PAINT message to be sent to our routine, thus recalling Form1.Refresh again. At that point our message queue would fill up with WM_PAINT messages and our app would crash. Microsoft has documented every message, it's purpose, it's paramaters and it's expected return value in the Platform SDK. Refer to it for specific info on the avilible messages.
Sub-classing windows extendes functionality to an application not directly exposed from VB.
Sub-classing can cause buggy applications if not used correctly.