Go Back  Xtreme Visual Basic Talk > Legacy Visual Basic (VB 4/5/6) > Knowledge Base > Tutors' Corner > Subclassing as a Method of Problem Solving


Reply
 
Thread Tools Display Modes
  #1  
Old 05-12-2003, 09:30 PM
John's Avatar
John John is offline
Bit Flipper
 
Join Date: Feb 2002
Location: The Inner Loop
Posts: 5,550
Default Subclassing as a Method of Problem Solving (Part 1)


Assumptions
This tutorial is written with the assumption that the reader has at least heard of the term subclassing, and has some experience with using the windows API.

Requirements
You should be using Visual Basic 6.0, and have access to a Spy++ like program. I no longer use Spy++ after finding a better alternative on the internet. I will, however, be using it for this article since that is what most people using Visual Basic 6.0 have. If you are interested in using a different program or don't have access to Spy++ for whatever reason you can download the one I use Winspector, or one of many other alternatives that can be found on the web. Here are a couple others: MiniSpy, WinSpy++.

Introduction
Subclassing is a way of gaining access to the most important part of any window, the window procedure. This procedure is the part of the window that deals with all the messages windows sends to it as things happen.

When using something like SendMessage, or PostMessage what you are doing is sending the message to the window procedure of a window. It is up to the window procedure to deal with each message in a way that it needs to, to accomplish its tasks. For instance when a user of a program clicks their mouse in a given window there are a series of messages sent to it by the operating system. It is up to the program, specifically the window procedure of the program, to process the messages.

Every window, as far as I know, that has an .hWnd property also has a window procedure that processes the messages for the window. Subclassing is a way of replacing this original window procedure with one of your own. By doing this, the operating system will then send all the messages to the procedure you have written, thereby allowing you to see and deal with any of the messages you want. After dealing with all the messages you are concerned with you should then pass each message along to the default window procedure, the one you replaced with your own, so it can handle the default processing of the message.

I know this can sound a little confusing to someone who is new to subclassing, but hopefully once you work through this article with me it will become clear.

Background
When writing a little notepad like program, I decided to switch to a RichTextBox instead of the standard TextBox control for a few reasons. First, I wanted to be able to work with large files, and the intrinsic TextBox control has a limit of somewhere around 32k. Second I thought that in the future I might want to make my text editor into a programming editor with syntax highlighting and to do that I needed to be able to change the colors of certain words which you can't do with the TextBox control.

Problem
While developing this editor I ran into a problem when pasting text into the RTB (RichTextBox). I originally had a menu item for the user to select when they wanted to paste some text and, of course, I assigned it the shortcut of "Ctrl+V" to be consistent with the windows shortcut conventions. The problem here was that when using the shortcut, it would not only paste the text in the RTB twice, but if the text on the clipboard was in RTF (Rich Text Format), then one copy that was pasted would show up with the formatting.

The first part of the problem can be summed up as follows. I programmed the shortcut into my program that took care of the paste action from the clipboard into the RTB. The problem is that the RTB already has this shortcut "Built in" and therefore it was being done once by my program and once by the RTB itself. This is a relatively straight forward problem to solve, by just taking out the shortcut of the menu item and changing that menu items .Caption property in the form load. Now my code would only execute when the menu item is actually clicked, and the RTB would take care of the shortcut. The menu item still looks like it has a shortcut to the user and they have no idea, nor do they need to, that there are two different pieces of code being executed depending on whether they use the shortcut or click the menu item.

Even though the first part of the problem is solved, there still remains the trouble of the RTF formatted text showing up in my RTB. This problem is a little more difficult to overcome. I tried everything I could possibly think of to take care of this problem with the built in functionality of Visual Basic but nothing I came up with fit in as cleanly as I wanted. I then decided to search the internet, thinking that someone must have come across this problem before me and hopefully they posted some code somewhere that I could use. I found a few solutions, but once again nothing that seemed "right" to me. One of these solutions was bound to cause problems with the program at some point, because it was doing quite a bit of work inside the KeyUp event. It would copy all the text from the RTB to a temporary string and then back into the RTB's .Text property. This, as you could imagine, would cause quite a bit of slowdown in my program when working with large files. I then decided it was time to take the leap, subclass the RTB and take care of the problem before it even occurs.

Time to Subclass
The first thing I did was setup the basic subclassing code in a blank project with just a RichTextBox (named "rtbMain") and a menu item with "Paste" under the top level item "Edit." Then I installed the subclassing code. To do that, create the blank project I mentioned and add a new standard module to the program by clicking the project menu and then "Add Module", and call it "modSubclass." Now in the module just copy and paste the following code:
Code:
Option Explicit Public 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 Declare Function SetWindowLong Lib "user32" Alias "SetWindowLongA" _ (ByVal hwnd As Long, _ ByVal nIndex As Long, _ ByVal dwNewLong As Long) As Long ' A pointer to the old window procedure Public pOldWindPoc As Long Public Const GWL_WNDPROC& = (-4) ' Our new window procedure Public Function WndProc(ByVal hwnd As Long, _ ByVal uMsg As Long, _ ByVal wParam As Long, _ ByVal lParam As Long) As Long WndProc = CallWindowProc(pOldWindPoc, hwnd, uMsg, wParam, lParam) End Function
This is the new window procedure, which will be put in place in a minute, as well as the API declares we need to get it running. There's really not much going on here, all the new window procedure does is pass all the messages it receives on to the default window procedure by using the API CallWindowProc. The reason I do it like this is to be sure that everything is working correctly before I start to deal with any messages. This doesn't actually do any of that yet though because the window procedure needs to be installed first. To do that we need to add some code to the Form. The code that needs to be added goes into the Form_Load event and the Form_Unload, or the Form_QueryUnload whichever you prefer. Here is the code for that:
Code:
Option Explicit Private Sub Form_Load() ' Redirect messages to our new handler in the module and ' save the pointer to the old handler pOldWindPoc = SetWindowLong(rtbMain.hwnd, GWL_WNDPROC, AddressOf WndProc) End Sub Private Sub Form_Unload(Cancel As Integer) ' Restore the previously saved message handler SetWindowLong rtbMain.hwnd, GWL_WNDPROC, pOldWindPoc End Sub
It is very important to restore the old window procedure before the program terminates, and by doing it this way you don't have to worry about it later. Now you should save the program before running it because of the instability subclassing can add to your program. Once you have done this much you should always save your program before running it, and remember to never press the end button in the IDE. By saving your program every time you run it you won't lose anything if the program crashes, and by not pressing end you make sure that everything gets cleaned up properly in your Form_Unload or Form_QueryUnload event by restoring the old window procedure. Just to test that everything still works the way it should, go ahead and save it now and run the program. Test everything out, and paste some RTF text into the RTB so you can see an example of the problem we are about to solve.

Once I decided to subclass the window (RTB), it was time to figure out what message or messages I was interested in getting at. I remember seeing, somewhere on the web, that the message was WM_PASTE. So I turned to google using the search terms, in quotes, "Visual basic" and, not in quotes, WM_PASTE. Don't bother going through the trouble of trying this because the RTB doesn't get the WM_PASTE message when the user presses "Ctrl+V."

Continued in next post
__________________
Subclassing|Magnetic Forms|Operator Overloading (VB2K5)|QuickSnip.NET

"These Patriot playoff wins are like Ray Charles songs, Nantucket sunsets, and hot fudge sundaes. Each one is better than the last." - Dan Shaughnessy

Last edited by Iceplug; 08-16-2005 at 07:34 AM. Reason: adjusted code formatting to reduce side scrolling ; I fixed the WinSpy++ link
Reply With Quote
  #2  
Old 05-12-2003, 09:32 PM
John's Avatar
John John is offline
Bit Flipper
 
Join Date: Feb 2002
Location: The Inner Loop
Posts: 5,550
Default Subclassing as a Method of Problem Solving (Part 2)

Time to Spy
Hopefully you are still with me here. Now it is time to figure out what message(s) needs to handled. Remember that we have total control over how the messages are handled now that we have the subclassing code installed. So go ahead and fire up Spy++, as well as the basic program that we have been working on here. Once they are both up and running go into Spy++ and Click the menu item named "Spy" followed by "Find Window" or just press "Ctrl+F." Once the little window shows up bring your RichTextBox program to the foreground by clicking it in the taskbar and place it just outside the Spy++ window but still visible, even just partially. Now click and drag the little Bulls-Eye looking icon in the Spy++ Find Window dialog over to your program and place the mouse over the RTB itself, not just the main window. When you do this you will see a rectangle appear around the RTB and then you know it is ok to let go of the mouse. Now back in Spy++ go all the way to the bottom of the Find Window dialog and click the Messages option button. Click ok. Now Spy++ has all the info it needs and should be spying on the window. You can see it doing its work by bring your window back to the foreground and just moving your mouse around inside the RTB.

At this point Spy++ is showing the information for every message the window procedure is receiving, which, undoubtedly, includes many messages we are not interested in. If you look at Spy++ now you will notice that the window procedure is getting messages like WM_SETCURSOR, WM_KILLFOCUS, WM_MOUSEMOVE, and WM_NCHITTEST. Now we know, since we haven't actually done anything yet, that these messages have nothing to do with what we are after so we can go back into Spy++ and turn off the notification of these messages. Just click "Messages" then "Options" and switch to the "Messages" tab. Here you will see all the messages that Spy++ is reporting. Scroll down the list and deselect the four messages mentioned above, by holding down the Ctrl key and clicking each of them in the list. Then click ok and go back to your RTB program and move the mouse around again. Notice that there are still a bunch of messages coming through but we did cut down on a significant number of the messages.

Now clear the message log in Spy++ by clicking the menu item "Messages" and then "Clear" or by pressing delete. With your RTB program still running and Spy++ still setup to spy on the RTB go back into the RTB program and quickly press "Ctrl+V." Right after doing this jump back to Spy++ and click the little traffic light toolbar button to stop the messages from being reported by Spy++. We now have all the information we need. If you scroll up the messages log you will see exactly what happened when we pressed the keyboard shortcut "Ctrl+V." There were a series of messages sent to the window including WM_KEYDOWN, WM_CHAR, WM_KEYUP, and probably a WM_PAINT or two. We can see that we probably need to watch for the WM_KEYDOWN message and specifically the one that corresponds to the 'v' key as reported by Spy++. So go ahead and close the RTB program (by clicking the "x" (not the end button in the IDE!), and leave Spy++ running for now.

Now that we know what message we are looking for we need to find the declaration for it. Back to google we go, again in quotes, "visual basic" and, not in quotes, WM_KEYDOWN. Many sites will popup but it doesn't really matter all that much which one we choose, just click one of them and get the declaration for the message. You should find that the declaration to be "Private Const WM_KEYDOWN = &H100". Just place that in the general declarations section of the modSubclass module. It is time now to open MSDN and search for "WM_KEYDOWN" so we can find out what parameter is filled with the key code when the window procedure receives this message. Doing so will tell us that the wParam parameter is filled with the key code, so comparing the wParam to vbKeyV will tell us if it was the WM_KEYDOWN message received because of the user pressing the 'v' key.

The trick now, is that when this message is received we want it to be ignored by the original window procedure. So we have to modify our new window procedure in a way that can get this job done, and at the same time won't slow things down too much. The way I have done it is to declare a long called handled inside the window procedure and initialize it to 'False'. Since longs actually operate faster than booleans, for reasons beyond the scope of this article, it is better to use a long as if it were a boolean, especially (as you have seen) in a function, which gets called as much as this one does. This variable will be toggled to 'True' when we detect that the user pressed "Ctrl+V". Then I setup a Select Case structure for the uMsg parameter comparing it to WM_KEYDOWN. The reason for using a Select Case here is simple. If there comes a time in the future that we need, or want, to deal with other messages it will be quite simple to just add another case for it. I then did the same thing within this by adding another select case for the wParam. Again, for future use it will be easy to add other cases and thereby ignore other keys if need be. Now if we enclose the "CallWindowProc" call, which passes the message along to the default window procedure, inside an if statement that checks the value of the handled variable we can effectively ignore any messages we want. Here is the modified window procedure:
Code:
Public Function WndProc(ByVal hwnd As Long, _ ByVal uMsg As Long, _ ByVal wParam As Long, _ ByVal lParam As Long) As Long Dim handled As Long handled = False Select Case uMsg Case WM_KEYDOWN Select Case wParam Case vbKeyV 'just ignore this message handled = True End Select End Select If Not handled Then WndProc = CallWindowProc(pOldWindPoc, hwnd, uMsg, wParam, lParam) End If End Function
Paste Code
Now that we have taken care of the RTB handling the shortcut and thereby pasteing RTF text into the RTB, we can go ahead and code the paste menu item as well as assign it the typical "Ctrl+V" shortcut.
Code:
Private Sub mnuPaste_Click() rtbMain.SelText = Clipboard.GetText(vbCFText) End Sub
Conclusion
Now that you have seen a complete walkthrough you should be able to figure out how to solve some other problems you might come across in your Visual Basic 6.0 programming. While I still have your attention (hopefully I do) let me just mention a couple other things about working on programs involving subclassing.

As I mentioned before, introducing subclassing to your programs can also introduce instability. To overcome this challenge you need to remember to save your program before you run it, and remember to close your program in a normal way and not press the end button in the IDE (and never use End in your programs). I have been bitten by this problem myself and I'm sure many other people have as well. Always trying to keep it in the back of your mind though should be helpful.

Lets see an example of the instability I'm talking about. In the RTB program, go into the window procedure and just place the word text in there without declaring it. Save the program and then run it, you will see not only the program disappear but also the IDE. Your program has crashed and where VB would usually halt the program in mid-stream to tell you about it, since you are using Option Explicit, the program instead crashes and brings VB down with it. If you should run into this problem in the future, a nice and easy way to find out what caused the problem is to go into the part of your form's code where you replace the old window procedure with your own and comment out that line as well as the other line in the Form_Unload or the Form_QueryUnload. This will temporarily remove the instability and you can track down the error that crashed VB before. After fixing that error you can just un-comment those two lines and be off and running again.

I am attaching an example program of the final results of subclassing the RTB to prevent its default processing of the keyboard shortcut "Ctrl+V". Hopefully you stuck with me through this and learned something. If you have any feedback please submit it via PM. This is my first attempt at a tutorial, so any and all feedback is welcomed.

Orbity
Attached Files
File Type: zip RTB-Paste.zip (2.3 KB, 342 views)
__________________
Subclassing|Magnetic Forms|Operator Overloading (VB2K5)|QuickSnip.NET

"These Patriot playoff wins are like Ray Charles songs, Nantucket sunsets, and hot fudge sundaes. Each one is better than the last." - Dan Shaughnessy

Last edited by Orbity; 09-20-2003 at 08:54 AM. Reason: adjusted code formatting to reduce side scrolling
Reply With Quote
Reply


Currently Active Users Viewing This Thread: 1 (0 members and 1 guests)
 
Thread Tools
Display Modes

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

BB code is On
Smilies are On
[IMG] code is On
HTML code is Off

Forum Jump

Similar Threads
Thread Thread Starter Forum Replies Last Post
Problem with ADO parameters.refresh method gallicus General 5 01-03-2002 09:04 AM
TreeView Add Method Problem George General 6 11-11-2001 02:09 AM
HUGE performance problem on Find method sethindeed Database and Reporting 4 09-04-2001 01:30 PM
Filtering unique items MarkG General 24 08-18-2001 01:44 AM

Advertisement:





Free Publications
The ASP.NET 2.0 Anthology
101 Essential Tips, Tricks & Hacks - Free 156 Page Preview. Learn the most practical features and best approaches for ASP.NET.
subscribe
Programmers Heaven C# School Book -Free 338 Page eBook
The Programmers Heaven C# School book covers the .NET framework and the C# language.
subscribe
Build Your Own ASP.NET 3.5 Web Site Using C# & VB, 3rd Edition - Free 219 Page Preview!
This comprehensive step-by-step guide will help get your database-driven ASP.NET web site up and running in no time..
subscribe
 
 
-->