personal info | online resume | project details | vb downloads | contact
Page Links
  • Overview
  • The Question
  • The Concept
  • References
  • The Code
  • Wrap Up


  •  Article Index
     
     
    Subclassing - Got Messages?
     
     
    Download a copy of the code that accompanies this article.
     
    Overview
    Windows is a world of messages. As you move your mouse around on the screen, messages are being sent to all the applications that lie under the mouse cursor. As you type keys on the keyboard, messages are being sent to the application you're typing into. At any given point in time, there are hundreds, even thousands, of messages flying all over the system. As each application gets messages, it's responsible for handling those messages appropriately. Your VB applications are no different. When the user clicks on one of your buttons, your application gets a button click message. When the user moves one of the forms in your application, your application gets a window move message. You may not have known it, but you've been responding to these messages (albeit, indirectly) ever since you started programming in VB, you just may not have known it. As I've said before, VB is great for hiding some of the complexity of programming Windows applications. In the area of message handling, this is certainly also true. The VB run-time handles the Windows messages and in turn, provides you with events that you can respond to.

    To get an idea of the messages the VB run time is handling for you, open up VB, start a new standard EXE project, and double-click the form created for you by default. Drop down the list of events for the form. You'll see events like MouseDown, MouseUp, and Paint. These events correspond to Windows messages. Sometimes, there is a one-to-one correspondence. Sometimes, there isn't. In either case, the Visual Basic run-time library is receiving Windows messages and raising events allowing you to respond to those messages. In some cases, the events you get have no parameters passed, such as the Resize event. In other cases, you get parameters passed to the event, such as the MouseDown event. In still other cases, you get parameters to events that you can respond to. An example of such an event is the QueryUnload event. If you set the Cancel parameter to True, VB will in turn prevent the form from being unloaded. VB does this by reacting to Windows message it received in such a way that prevents the window from being unloaded by Windows.

     
    The Question

    Recently, I was presented with a question in a newsgroup. The question was "how can I write some code in my form that will run when my form is being moved?". The designers of the VB run-time library didn't give us a Form_Moving event. This might lead you to believe there isn't a way to respond with code WHILE a form is being moved. If you assumed this to be correct and moved on providing less in your application than you had hoped, you'd be giving up too soon. This is the realm of subclassing. Let's go back to those Windows messages for a moment. If you search MSDN Online (http://search.microsoft.com/us/dev/default.asp) for "window messages" (without the quotes, exact phrase criteria), you'll come across a page entitled Window Messages. You'll see on this page a long list of WM_* messages used to create and manage windows. At the top of the list, you'll see one I bet you're familiar with from working with VB, the WM_ACTIVE message. As you can image, the VB run time uses this message to communicate with you that your form is being activated. It corresponds to Form_Activate event. Back to our newsgroup question: can I write code that will be run when my form is moving? If you look down the page, you'll see the WM_SIZING message. If you click on that message, you'll that the explanation of that message is as follows: The WM_SIZING message is sent to a window hat the user is resizing. Doesn't that sound like what we're after? Well in fact, it is, but this is a Windows message, not a VB event. We need a way to somehow "catch" that message when it's sent and run the code that we need to when our form is actually being moved. This, in essence, is subclassing.

     
    The Concept

    Subclassing is inserting one of our VB procedures into the Windows messaging chain so that we will see the WM_MOVING message and have a chance to respond to it. This is a very powerful capability but also comes with some very necessary rules:

    • Windows will allow you to insert yourself into the message chain. You need to make sure you pull yourself out of the chain when you're done.
    • As I mentioned previously, there are many, many messages flying around the system at any given point in time. If you enter break mode in your project and you have no way to handle the messages that continue to be sent, you will bring down Visual Basic.
    • Most messages are sent to you on a preview basis. You need to make sure that you pas along any messages you're not interested in. Failing to due so will bring down your application, Visual Basic, and possibly Windows itself.
    • Since subclassing is an advanced and sometimes complex topic, the code you create could be unstable until it is fully debugged. Remember to save your work often.

    I say all this, not to scare you off, but to ensure you understand that with the advanced capabilities allowed by subclassing, there are also advanced risks. We'll deal with each of these risks as we proceed. Let's get started!

     
    Additional References

    To begin with, there are a couple of items you need to download and install before we can get going.

    The first is the Windows API type library that originally came with the book Hardcore Visual Basic by Bruce McKinney. If you're working within Win98, download the ANSI version. If you're working in WinNT, download the Unicode version. Once you have the type library downloaded, copy it to your hard drive in a location that you'll remember. If you have a tool to register type libraries, open it up and register the type library. If you don't have any tools to register it, just let VB do it for you. Open up VB, start a new project (it doesn't matter what kind), go in to the project references dialog, click Browse', navigate to where you saved the type library, and double-click it. VB will then register it.

    The second item you'll need is a copy of the Debug Object for AddressOf Subclassing. Downloading this item may require you to register with Microsoft first. If you need to, go ahead and register and then grab a copy of the object. Once you download the object, you can install it wherever you choose and it will register the second item we'll need in our project.

     
    The Code

    Once we've obtained these two items, we're ready to get started. Open up a new standard EXE project. Go in to the project references and add a reference to the Debug Object for AddressOf Subclassing and the Windows API (Unicode if you got the Unicode version). The goal of this project is twofold: first, introduce you to subclassing so you'll understand when and why to take advantage of it and, second, to create a class you can drop into any form where it's needed allowing you to respond to some "events" that VB left out.

    First, add a class to you project and name it CFormEvents. The first thing to add will be the declarations for the events we'll be raising:

    ' the events raised by this class
    Public Event Moving(ByVal WindowRectPtr As Long)
    Public Event Sizing(ByVal WindowEdge As Long, _
                        ByVal WindowRectPtr As Long)


    The next thing to add is the procedure that will actually interpret the messages received from Windows:

    Public Function WindowProc(ByVal hWnd As Long, _
                               ByVal uMsg As Long, _
                               ByVal wParam As Long, _
                               ByVal lParam As Long) As Long
    Dim hSysMenu As Long

      ' determine what the message was and handle it accordingly
      Select Case uMsg
        Case WM_MOVING
          RaiseEvent Moving(lParam)

        Case WM_SIZING
          RaiseEvent Sizing(wParam, lParam)

        'Case ?
          '
      End Select

      ' pass the message along to Windows
      WindowProc = CallWindowProc(m_wndprcNext, hWnd, uMsg, wParam, ByVal lParam)
    End Function


    As you can see, the Select Case statement includes two messages at this point, the WM_MOVING and WM_SIZING messages. These two messages are sent to a window that is being moved or resized, respectively.

    The next code we'll add the code to respond to the events raised by the class. Go back in to the form created when you started the project. Add a declaration for the class using the WithEvents keyword:

    ' declare an instance of the class that will interpret
    ' the Windows message received WithEvents so it
    ' can notify me when a particular event occurs
    Public WithEvents m_oFormEvents As CFormEvents


    Using the WithEvents clause will allow us to "hear" the events raised by the class. In the Form_Load event, we'll instantiate the CFormEvents class object and begin subclassing (more on this in a moment):

    Private Sub Form_Load()
      ' instantiate the class
      Set m_oFormEvents = New CFormEvents
      
      ' begin subclassing
      SubClass Me
    End Sub


    In the Form_Unload event, we'll address the first rule mentioned above: once you begin subclassing, make sure you quit subclassing before your application terminates. Failure to do so will most likely cause VB to crash when you unload your form.

    Private Sub Form_Unload(Cancel As Integer)
      ' end subclassing
      UnSubClass Me
    End Sub


    While we're looking at this code, it's a good time to bring up another note about subclassing. A lot of people are in the habit of testing out their applications and when they're done testing, they click the End button on the VB toolbar to return to development mode. When working on applications that do subclassing, this is a fatal mistake. When you click the end button, VB ends your application without doing any of the normal cleanup and trash removal. The normally includes running the code contained in your Form_Unload event. Clicking the end button and bypassing the Form_Unload code in this case will be fatal to VB. VB will terminate and any code you may have entered since the last time you saved you project will be lost. In addition to losing some of your own code, some VB add-ins don't unload properly if VB shuts down suddenly. The bottom line: while working on applications that do active subclassing, NEVER push the end button.

    The last part of the form includes the code for the events raised by our class. Per the original newsgroup request, these events cover the form being moved or resized. First, the Moving event:

    ' m_oFormEvents_Moving
    '
    ' Occurs when the CFormEvents class receives a WM_MOVING
    ' message (i.e., when the form is moving).
    '
    ' Parameters: WindowRectPtr - a pointer to a RECT structure providing the
    ' location of the window at it's current position
    '
    Private Sub m_oFormEvents_Moving(ByVal WindowRectPtr As Long)
    Dim r As RECT

      ' populate the pointer to the RECT structure from the pointer
      ' received
      CopyMemory ByVal VarPtr(r), _
                 ByVal WindowRectPtr, _
                 Len(r)
      
      ' show the position of the form
      lblTop.Caption = r.Top
      lblLeft.Caption = r.Left
      lblBottom.Caption = r.bottom
      lblRight.Caption = r.Right
    End Sub


    This event is raised every time the CFormEvents class receives a WM_MOVING message. If you recall from the code in the CFormEvents.WindowProc routine, the parameter raised with the Moving event is the last parameter passed to the WindowProc routine. According to MSDN, the last parameter passed with a WM_MOVING message is a pointer to a RECT structure with the screen coordinates of the drag rectangle. If you’re not familiar with the RECT structure, it looks something like this:

    Type RECT
      Left   As Long
      Top    As Long
      Right  As Long
      Bottom As Long
    End Type

    You don't actually have to define this type yourself in your code because it's defined in the Win API type library.

    Another important note about Windows messages before we move on. The actually contents of the parameters sent with each message are different. Consult the MSDN documentation to determine the contents and usage of the two parameters for each message.

    Now, back to our code. The CFormEvents class passes the lParam value along when it raises the Moving event. According to the docs, the lParam for a WM_MOVING message contains a pointer to a RECT structure. We have to transfer this pointer to our local RECT pointer. This is done by the CopyMemory statement (also declared in the type library). Once we have the pointer transferred, all four members of the structure are available for us to use in our code. The labels on the form are all updated with the current position of the form.

    The last item in the form is the code to respond to the Sizing event:

    ' m_oFormEvents_Sizing
    '
    ' Occurs when the CFormEvents class receives a WM_SIZING
    ' message (i.e., when the form is being resized).
    '
    ' Parameters: WindowEdge - a bit flag that indicates which edge or
    ' edges of the window are moving
    ' WindowRectPtr - a pointer to a RECT structure providing the
    ' location of the window at it's current position
    '
    Private Sub m_oFormEvents_Sizing(ByVal WindowEdge As Long, ByVal WindowRectPtr As Long)
    Dim r As RECT

      ' load the correct direction cursor
      If WindowEdge = WMSZ_RIGHT _
          Or WindowEdge = WMSZ_LEFT Then
        imgDirection.Picture = LoadPicture(App.Path & "\lwe.cur")
      ElseIf WindowEdge = WMSZ_TOP _
          Or WindowEdge = WMSZ_BOTTOM Then
        imgDirection.Picture = LoadPicture(App.Path & "\lns.cur")
      ElseIf WindowEdge = WMSZ_TOPLEFT _
          Or WindowEdge = WMSZ_BOTTOMRIGHT Then
        imgDirection.Picture = LoadPicture(App.Path & "\lnwse.cur")
      ElseIf WindowEdge = WMSZ_TOPRIGHT _
          Or WindowEdge = WMSZ_BOTTOMLEFT Then
        imgDirection.Picture = LoadPicture(App.Path & "\lnesw.cur")
      Else
        imgDirection.Picture = LoadPicture()
      End If

      ' populate the pointer to the RECT structure from the pointer
      ' received
      CopyMemory ByVal VarPtr(r), _
                 ByVal WindowRectPtr, _
                 Len(r)
      
      ' show the position of the form
      lblTop.Caption = r.Top
      lblLeft.Caption = r.Left
      lblBottom.Caption = r.bottom
      lblRight.Caption = r.Right
    End Sub


    According to MSDN, the wParam value passed with a WM_SIZING message represents which edge of the window is being sized. The constants used in the code are defined in the API type library and represent the possible values. I used these to load the correct cursor to show in the image control. The other parameter is the same as the value passed to the Moving event and is handled the same way.

    So far, we've covered how to handle the new messages that we're privy to once we start subclassing. The only question remaining is how to we start subclassing? As you saw in the Form_Load and Unload events, subclassing starts and stops there. The SubClass routine appears below:

    Public Sub SubClass(ByVal TargetForm As Form)
      ' ensure there's no active subclassing already
      ' going on
      UnSubClass TargetForm
      
      ' stop here if the 32-bit value associated with
      ' the TargetForm already has a value in it
      Debug.Assert GetWindowLong(TargetForm.hWnd, GWL_USERDATA) = 0
      
      ' put a pointer to this form in the extra memory
      ' associated with the form
      SetWindowLong TargetForm.hWnd, GWL_USERDATA, ObjPtr(TargetForm)

      ' if using the Debug Object (should only be while running
      ' inside the IDE), create a WindowProcHook and use it
      ' to do the subclassing (which allow the program to
      ' enter break mode without hanging)
    #If DEBUGWINDOWPROC Then
      On Error Resume Next
      
      Set m_SCHook = CreateWindowProcHook
      
      ' make sure I'm able to create an instance of the
      ' debug object
      If Err Then
        MsgBox Err.Description
        
        Err.Clear
        
        UnSubClass TargetForm
        
        Exit Sub
      End If
      
      On Error GoTo 0
      
      With m_SCHook
        ' set the address to use to catch messages while running
        ' at full speed
        .SetMainProc AddressOf SubClassFormProc
        
        ' tell Windows to switch the message receiving proc to
        ' SubClassFormProc and give me back the previous
        ' address of the message receiving proc
        m_wndprcNext = SetWindowLong(TargetForm.hWnd, _
                                     GWL_WNDPROC, _
                                     .ProcAddress)
        
        ' tell the Debug Object the address to use when the
        ' IDE has entered break mode
        .SetDebugProc m_wndprcNext
      End With
      ' if not running inside the IDE, don't use the Debug object
      ' because there's no way to enter break mode in an EXE
    #Else
      ' switch out the address of SubClassFormProc with the
      ' old message receiving proc, thus beginning the subclass
      m_wndprcNext = SetWindowLong(TargetForm.hWnd, _
                                   GWL_WNDPROC, _
                                   AddressOf SubClassFormProc)
    #End If
    End Sub


    We pass the form to be subclassed as a parameter to the SubClass routine. First, since you can only have one active subclass in a form, we call the UnSubClass routine to make sure there's no active subclassing going on. While this project is very simple and there's no chance of that happening, in more complex projects, you'd want to make sure. Calling the UnSubClass first makes absolutely sure you don't end up in a bad situation. The next thing to do is use the Debug object's Assert method to stop the code execution if there's a value stored in the user data area of the form. To retrieve this value, the GetWindowLong function is used with the GWL_USERDATA parameter (see the MSDN docs for more info on this call). If a value OTHER than 0 is returned, code execution is stopped and VB enters break mode on the Assert line. GetWindowLong should not return anything other than 0 at this point, which is exactly why Assert was used. Assuming this is true, we go ahead and put a pointer to the form being subclassed in the 32-bit value associated with the form using the SetWindowLong call.

    Next comes the final step required to begin subclassing. This is where the Debug Object comes in that you downloaded and installed a while back. As I stated in the second rule above, messages are being created and sent to applications constantly at a very fast rate. If you were to enter break mode, essentially single stepping through the process of handling ONE message, while all these other messages are coming at you, you will immediately hang VB and your debugging session will be over, as will your entire VB session. The Debug object very cleverly continues to handle all other messages coming at VB while you focus on the one message you're trying to debug. Since this object just handles messages for you while you single stepping through your code, there's no reason for the object to be part of you compiled EXE. To enable it to be excluded from the final EXE, set the conditional compile constant, DEBUGWINDOWPROC, to False. For now though, go in to the Project Properties dialog, switch to the Make tab, and enter DEBUGWINDOWPROC = -1 in the Conditional Compilation Arguments box. When you're ready to build your EXE, go back in to the properties dialog and change the -1 to 0. If you forget to do this, the Debug Object itself will remind you (and possibly your users too) by popping up a message box when the subclassing code starts in your EXE.

    If DEBUGWINDOWPROC is True, a new instance of the CreateWindowProcHook object is created and used to insert our SubClassFormProc subroutine into the Windows messaging chain. It also gets the value of the previous messaging routine to use to process messages while you're staring in a daze at your code trying to figure out what's wrong (i.e., debugging). If DEBUGWINDOWPROC is False, the SetWindowLong API call is used to switch the address of the message receiving proc from the old one to the new one. It also gives us the address of the old one in return. We hang on to this value so we put things back the way they were when we're done (rule #1).

    The final routine to discuss is the UnSubClass routine. This routine simply puts the address of the old message receiving routine back in place of our temporary one. If you forget to do this, VB will remind you by popping a GPF as soon as soon as your application ends.

     
    Wrap Up

    There, that was wasn't too bad. If you've been following along in VB, creating code as we went along, you should be ready to run the project. If you weren't, you can just download a copy of the project. When you run the project and move the form, the current location in pixels of the form will appear in the labels. If you resize the form, the icon will match the direction of your mouse cursor (i.e., north-south, east-west, etc.) and the location of the form will also be updated. Remember, when you're done testing out the application, close the form with the X button, NOT with the End button. I would imagine if you do forget, it won't take you long to remember since you'll have to start VB over and over again until you do. Sorry, just one of the hazards of living life in the fast lane.

    Some of what we've talked about may seem a little difficult at first. It really isn't. Take your time; look over the code and the documentation on the MSDN web site. You'll see that there's really not that much to it. What you may not realize yet is the world of possibilities you've just opened up. If there's something you'd like to do with one of your forms or buttons or menus or whatever and you just don't see any event in VB that would allow you to do it, look over the messages that Windows is actually sending out for that type of control and you may just find the message you're looking for. A good place to start looking for such messages is Dan Appleman's book, Visual Basic Programmer's Guide to the Win 32 API. This book has the various messages categorized, which makes them easier to find. Another source of information about subclassing is vbAccelerator. There's a good page here that discusses subclassing and offers a component that assists in debugging subclassed applications, similar to the Debug Object discussed here. Karl Peterson also has a good subclassing code template on his Web site. Click on the Samples link and scroll down to HookMe.zip.

     
    This article has been republished with permission from EZ Programming Weekly. To subscribe, send an email to cdnelson9@hotmail.com
    personal info | online resume | project details | vb downloads | contact
    Copyright 2001 by Earl Damron