January 1999
|
|
Uncover Secret Control Properties
Use your knowledge of Windows internals to access secret control properties hidden by VB.
by Hank Marquis
Reprinted with permission from Visual Basic Programmer's Journal, Jan 1999, Volume 9, Issue 1, Copyright 1999, Fawcette Technical Publications, Palo Alto, CA, USA. To subscribe, call 1-800-848-5523, 650-833-7100, visit www.vbpj.com, or visit The Development Exchange.
VB's plethora of features comes at a price. Microsoft implemented only the properties, methods, and events it thought we'd need in VB's intrinsic controls (TextBox, ListBox, ComboBox, and so on).
These built-in properties of the intrinsic controls are often enough. Sometimes, however, it might take a lot of code to implement a simple featuresuch as managing text in a multiline textbox, or performing data validation. You can quickly find yourself swimming in public variables and fairly complex (read "potentially buggy") code. Worse, all this time and energy is spent working only on a feature, not the core application itself.
What you need:
VB4/32, VB5, or VB6 |
|
Take heart! VB uses standard Windows control classes for most of its intrinsic controls. Therefore, you can access, change, and enhance these VB controls using low-level Windows programming techniques: the Windows Application Programming Interface (API).
VB's intrinsic controls generally contain a subset of the features available in similar, stock Windows controls. For example, the VB TextBox control, while providing many valuable features, omits several contained in the similar Windows control, such as undo and redo; row, column, and line location information; changed status; top line number; and line count.
You'll learn how VB's intrinsic controls are really extensions of standard Windows control classes. You'll then modify an existing control (such as a textbox) to give it new properties, as well as access "hidden" features VB does not implement. As always when using the Windows API, you need to take extra care (see the sidebar, "5 Steps to Safe Windows API Usage" for a few simple steps that can make your Windows API usage much more enjoyable).
Windows Windows Everywhere
VB's built-in controls, technically called windows, are classes of Windows-created windows with definable, standard behavior. The API CreateWindow is used to create virtually all the objects in a Windows program. Windows can create buttons, textboxes, listboxes, combo boxes, and static windows, among other things. These are all "windows," similar in many aspects to the familiar Windows dialog box or VB form. Most VB developers refer to these windows as forms or controls.
When a program creates a window of a given class, Windows provides the new window with certain properties by default. For example, a command button window class supports a simulated up and down graphical animationa textbox does not. A listbox may contain a list of sorted strings, but an option button does not.
All Windows applications are composed of these windows. These applications are event-drivenjust waiting for Windows or a user to pass input to them. For example, the VB TextBox control just sits there until the user enters text. As text is entered, VB fires the changed event, but it does not fire until a key is pressed.
Windows handles the passing of input to a window (form or control). Every window has a function called its "window procedure" that Windows calls when it has input for that window. The window (or control) processes the input and carries out whatever that input commands and returns control to Windows so that it can pass input to the next window.
VB masks windows' procedures from us, but they exist nonetheless. And these "hidden" window procedures most intrinsic controls possess are your ticket to unlocking a treasure trove of secret features VB does not provide on its own.
Regardless of the type of window, or its default properties and capabilities, Windows uses messages to manipulate all the windows it creates, including VB controls and forms. Messages are generated for all input events that occur during the life of a Windows application. For example, when you type text into a textbox, or when you move the mouse over a form, the action generates messages Windows sends to the affected window or control.
You can create and send messages to windows (or controls) within a VB application or to windows and forms in another application. (Here you'll work only with VB controls.) You can use Windows' SendMessage API for this communication:
Declare Function SendMessage Lib "user32" _
Alias "SendMessageA" (ByVal hwnd _
As Long, ByVal wMsg As Long, ByVal _
wParam As Long, lParam As Any) As Long
When SendMessage sends a message to a window or control, it actually calls the window procedure for the designated window, which passes information, commands, and/or data to the designated window. The return value of SendMessage indicates the result of the message processing and depends on the message sent. To better understand how to use SendMessage, you need to understand each of its arguments.
The first argument, hWnd, identifies the window to which the message will be sent. hWnd, the windows handle, is the long integer value returned from the hWnd property of many controls, such as Text1.hWnd. You can send messages to any window in the systemproviding you know its hWnd.
The second argument, wMsg, indicates which message to send. Windows has thousands of defined messages, each of which has a unique identifier and a defined constant (see Table 1). These constants are named in such a way that the message hints at the function or purpose of the message. For example, the EM_UNDO constant directs a textbox to "undo" any changes made to its text. Windows messages cover a huge area and return information or control the action of virtually everything that occurs under Windows.
The third and fourth arguments, wParam and lParam, provide additional information the receiver of the message should use in conjunction with the message. For example, the EM_LINEFROMCHAR message requires you to pass the character number in wParam, and returns the line number in the textbox that contains that character index.
You use SendMessage to send a message to direct a window procedure to perform the task the message indicates. SendMessage waits for the window to complete processing the message before returning a result. (Note that there is another API called SendMessageCallback that returns immediately and passes the result of the message processing to a user-defined callback function. See the Win32 SDK for information on this API if you want to include it in your VB5/VB6 programs.)
Using SendMessage With a VB TextBox
There are thousands of messages, dozens of window permutations, and an almost limitless number of features you can expose, query, and direct using SendMessage. Trying SendMessage techniques on a single VB intrinsic control, however, will give you a good introduction to using these techniques on all the VB intrinsic controls, third-party controls, and VB-authored ActiveX controls that provide a window handle property (usually as an hWnd property).
The VB TextBox control is based on the Windows "edit control" class. An edit control lets a user input text. While the VB implementation does quite a bit for you, there are many other features VB hides. However, using SendMessage, you can access dozens of useful, additional properties.
To use SendMessage with a VB TextBox control (or any other control), all you need is the control's window handle (hWnd), the message constant of the task you want the control to perform, and the arguments that message requires.
You can find most of these constants in the VB API Viewer (see Figure 1). (To use the VB API Viewer, choose it from the VB Add-Ins menu.) However, not all the possible message declarations are included in the VB API Viewer. For a complete listing of declarations, view the Visual C++ header files (.h files) found in Visual Studio. For example, you can find some of the TextBox control message declarations missing from the VB API Viewer in the RICHEDIT.h header file that comes with Visual Studio. Edit control messages (EM_XXX) are used when the SendMessage target is a textbox (see Table 2). A tip on locating declarations not in the VB API Viewer: Use the Windows Find command to search through all files with an extension of .h for the constant you want (see Figure 2).
Using SendMessage is a snap once you have the window handle, message constant, and message arguments. For example, did you ever wish you could add an "undo" ability to a textbox? To do this using VB alone requires a variable to store changes, and code and logic to restore the original text. Unfortunately, not only is this complex, but it's also prone to bugs. Also, using VB alone you cannot roll back to saved points: The only indication of changes to the text you get is the Changed event, which fires for each and every key press the user makes. The problem then becomes tracking all the changes the user makes, knowing which changes to keep and which to discard. You can see how this simple-sounding task can quickly become a programming nightmare.
Time to Send a Message
Using SendMessage, you can quickly and easily add powerful undo, redo, and save point functionality to an ordinary VB textbox. The secret here is knowing these messages are available:
Const EM_CANUNDO = 198
Const EM_EMPTYUNDOBUFFER = 205
Const EM_GETMODIFY = 184
Const EM_SETMODIFY =
Const EM_UNDO = 199
These five messages provide total programmatic control over the undo and redo features of a textbox. For example, undoing changes to a textbox is as simple as this:
Private Sub Command1_Click()
' make control undo edits
SendMessage Text1.hwnd, EM_UNDO, _
0, 0
End Sub
This code sends the EM_UNDO message to the VB textbox named Text1. EM_UNDO requires that the wParam and lParam arguments be set to zero. The SendMessage call in the example discards any changes made to the text, restoring the text to a previous value stored in the control's undo buffer. The undo buffer is a hidden copy of the text in a textbox. In the code I implemented SendMessage as a method, but you can also implement SendMessage as a function.
You can also add redo functionality by simply using the same code again. EM_UNDO toggles from the edit buffer (the text you can modify) to the undo buffer (a copy of the text you cannot see).
Expand your control further using EM_CANUNDO to determine whether
a textbox can undo any changes. You can extend the previous example with this additional code:
Private Sub Command1_Click()
' if can undo
If SendMessage(Text1.hWnd, _
EM_CANUNDO, 0, 0) <> 0 Then
' make control undo edits
SendMessage Text1.hwnd, _
EM_UNDO, 0, 0
End If
End Sub
This code checks the undo capability of the TextBox control. If the TextBox control can undo the changes made, EM_CANUNDO returns True through SendMessage. Note that the code uses <> 0 to check for the "truth" of the SendMessage return value. Unlike VB, Windows API "truth" means not equal to zero (<> 0), not necessarily -1 as in VB, so be sure to code accordingly.
To add more functionality, you can also implement a save point feature for your textboxes using the EM_EMPTYUNDOBUFFER message. Send the EM_EMPTYUNDOBUFFER message to reset the undo flag of the textboxeffectively copying the current contents of the textbox to the undo buffer. After you use this message, any changes made to the text that are undone using EM_UNDO will revert to the value of the textbox when the EM_EMPTYUNDOBUFFER was sent. This code shows how to implement a save point function in a multiline textbox:
Private Sub Command2_Click()
' make the controls current text
' be its undo text
SendMessage Text1.hwnd, _
EM_EMPTYUNDOBUFFER, 0, 0
End Sub
This code empties the textbox's current undo buffer, which makes the current text the undo text. This code replaces dozens of lines of potentially buggy VB code and variables with two lines of code! You can also use these techniques to redo and save (see Listing 1).
But don't stop there. VB only gives you the Changed event of a textbox to determine whether the contents have changed. Usually you would use some sort of public or module-level variable to track changes made. However, when you rollback or undo an edit, keeping track of this change state becomes tedious and complex. Instead, why not use the EM_GETMODIFY message? EM_GETMODIFY returns True (<> 0) if the textbox contents have been modified or False (0) otherwise:
Public Function Changed(ByVal _
ctlTextBox As TextBox) As Boolean
' get change state of control
Changed = _
SendMessage(ctlTextBox.hwnd, _
EM_GETMODIFY, 0, 0) <> 0
End Function
This code directs the VB textbox passed in ctlTextBox to return the value of its edit flag, which is set whenever the text in the textbox changes. You might think this isn't much better than the VB TextBox control's Changed event, but you can use EM_GETMODIFY's partner message EM_SETMODIFY to set the controls edit flag as needed. When you use EM_GETMODIFY and EM_SETMODIFY together, you can take control over a textbox's change flag.
Using the previous example, perhaps you want to reset the change flag after using the EM_EMPTYUNDOBUFFER message. This code extends the previous example to reset the change flag after a save point:
Private Sub Command2_Click()
' make the controls current text
' be its undo text
SendMessage Text1.hwnd, _
EM_EMPTYUNDOBUFFER, 0, 0
' reset edit flag
SendMessage Text1.hwnd, _
EM_SETMODIFY, False, 0
End Sub
This code empties the undo buffer and sets the edit flag to False. After this procedure, the Changed() function presented earlier would return False (0). You can also programmatically set the edit flag using EM_SETMODIFYfor example, you might want certain default information to be saved in a VB textbox named Text1. After setting the textbox to a value, you can mark it as changed using EM_SETMODIFY with a wParam of 1:
Text1.Text = "hi there"
' mark text1 as changed
SendMessage Text1.hwnd, _
EM_SETMODIFY, 1, 0
If you want to mark text as unchanged, use EM_SETMODIFY again, with a wParam of 0:
Text1.Text = "hi there"
' mark text1 as unchanged
SendMessage Text1.hwnd, _
EM_SETMODIFY, 0, 0
Then, when you're ready to commit or process the data, you can loop through all the textboxes on a form and process or save the data in the changed textboxes only:
Public Sub SaveData()
Dim iTextBoxCount As Integer
For iTextBoxCount = 0 To _
Controls.Count - 1
If TypeOf Controls( _
iTextBoxCount) _
Is TextBox Then
If Changed(Controls( _
iTextBoxCount)) Then
' process data
End If
End If
Next
End Sub
This code completely replaces perhaps dozens of public or module-level variables and many lines of logic and code. This is an excellent example of how you can tap into the Windows API to accomplish common tasks more quickly and more reliably than using your own VB code.
Remember, these simple examples are representative of other Windows API capabilities exposed through SendMessage.
You can also manipulate text in a multiline textbox as if it were an array using these messages: EM_GETFIRSTVISIBLELINE, EM_GETLINECOUNT, EM_GETLINE, EM_LINEFROMCHAR, and EM_LINEINDEX. You can search a textbox using EM_FINDTEXT. You can control tab stops using EM_SETTABSTOPS, and you can scroll a textbox using EM_LINESCROLL and EM_SCROLLCARET.
Caution: You declare and use Windows APIs without VB's safety net. VB usually handles errors and other bad things for us. However, when using the Windows API, VB cannot provide the same amount of protection as it does for internal VB keywords and methods. So whenever you use the Windows API, make sure to follow my "5 Steps to Safe Windows API Usage" (see the sidebar).
Don't forget that most of VB's intrinsic controls and many third-party controls have these "secret" capabilities hidden just under the skin. You can search listboxes and combo boxes; modify option boxes and checkboxes; and manipulate picture boxes, menus, and more. You can even work SendMessage magic on forms.
Hank Marquis is coauthor of the Visual Basic 6.0 Bible (ISBN 0-76453-227-8) and founder of Modern Software, creators of data security and trial-ware tools for developers. Reach him through http://www.modernsoftware.com.
|