by Francesco Balena
Reprinted with permission from Visual Basic Programmer's Journal, September 1999, Volume 9, Issue 9, 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.
Most VB built-in controls are merely wrappers around Windows native controls. For example, VB's TextBox control borrows its core functionality from the Windows Edit control. VB's CheckBox, OptionButton, and CommandButton controls are actually different styles of the Windows Button control.
|
Working at the API level poses a lot of problems, including the possibility of causing fatal errors and application crashes if you pass the wrong argument to a routine. Unlike VB properties and methods, API routines don't check the validity of their arguments. When something goes wrong, you'll most likely encounter the dreaded General Protection Fault (GPF), which forces the application to terminate. When you compare this with VB's usual smooth way of dealing with errors, you can understand why many C and C++ programmers prefer to build their apps' user interfaces in VB.
Microsoft engineers did a great job encapsulating Windows controls in their VB counterparts, but they left some things out. For example, the TextBox controls' properties and methods don't account for all the capabilities of the underlying Windows Edit control, especially in its multiline variant. Similarly, the Windows ListBox control supports a few featuressuch as a horizontal scrollbar and programmable tab stops for building columnsnot included in the corresponding VB control.
Fortunately, VB offers all you need to call API routines directly. So if a VB control doesn't do what you need, you can bypass it and exploit the features of the underlying Windows control. This capability is made possible by one property most controls expose: the hWnd property. This read-only property returns the control's handle, a Long value that uniquely identifies the control in Windows. You must pass this magic number to any API routine expected to work on the control.
Note that not all built-in controls expose the hWnd property. For example, the Label, Image, Line, and Shape controls don't expose it because instead of wrappers around Windows controls, they're shapes drawn by the VB runtime on their parent form. VB controls that aren't backed up by real Windows controls are called lightweight controls, and when possible you should use them instead of regular controls because they take fewer system resources. For example, use an Image instead of a PictureBoxunless you need PictureBox-specific features, such as support for graphic methodsbecause Image controls load faster and take less memory.
Figure 1 Using the API Viewer Utility.
Click here.
|
Private Declare Function SendMessage _
Lib "user32" Alias "SendMessageA" _
(ByVal hwnd As Long, ByVal wMsg _
As Long, ByVal wParam As Long, _
lParam As Any) As Long
Before I explain what the SendMessage function does, here's the meaning of each portion of the statement: A Declare statement can be Public or Private, depending on whether you want to use it from the entire project or only from inside the module where the Declare itself appears. Other VB entities, such as variables or Type clauses, have a similar scope rule.
Gather the Declare Statements
The rule is simple: You can use the Public attribute only if the Declare appears in a BAS module, whereas all other module types accept only Private Declare statements. I recommend gathering all the Declare statements used by an application in a BAS module so you don't have to duplicate them in each module. You can create a BAS module that contains the Declare statements for your favorite API functions, and import that module into every project you write. Declaring an API function without using it is perfectly legal, and doing so doesn't add any noticeable overhead to the project.
The Lib clause inside the Declare statement tells VB where the function is locatedthe User32.dll system file, in this case. The DLL is probably located in the current directory, or, more frequently, in one of the system directories: c:\Windows or c:\Windows\System. You can use the Declare statement to execute routines located either in the system DLLs or in third-party commercial and shareware DLLs. But remember one point: A DLL extension doesn't ensure, by itself, that you can use the Declare statement to invoke the code contained in the file. In fact, the DLL extension is used for different types of files, such as ActiveX components or even files that contain only resourcessuch as bitmaps or iconsbut no code. Many programmers use the phrase "true DLLs" to describe DLLs that contain code callable through the Declare statement. If you specify the wrong name or if the DLL can't be found, you'll receive a runtime error when VB tries to locate the declared function.
The Alias clause is also important; it indicates the actual name of the routine as it appears in the DLL. In fact, the name you use to refer to the routine in your VB code doesn't necessarily match the routine's true name as registered in the DLL. A different name might be necessary for many reasons: The name of the routine might be an invalid name under VB, or it might coincide with the name of a method, property, or event, which can often confuse the compiler. For example, you should refer to the Windows API Beep function using an aliased name to avoid confusion with the VB command of the same name:
Private Declare Function BeepAPI _
Lib "kernel32" Alias "Beep" _
(ByVal dwFreq As Long, _
ByVal dwDuration As Long) As Long
Finally, the Declare statement includes the list of arguments you can pass to an API routine. The argument name isn't important; what matters is its type and the method used to pass it. You can pass an argument by value (using the ByVal keyword) or by address (using the ByRef keyword or simply omitting any keyword before the argument's name). As happens with regular VB procedures, the called code can modify an argument passed by address, but it can't modify an argument passed by value. When you pass a string to an API routine, the story is more complex, but I won't tackle the details here.
Another point about arguments: The Declare statement supports the As Any clause, which means you can pass nearly everything to that argument. For example, the SendMessage's lParam argument is declared with As Any, because you are allowed to pass it either a Long or a String value. I'll return to this point shortly, because As Any arguments weigh in among the most frequent sources of errors when working with API calls.
Among the hundreds of available API routines, I picked the SendMessage API function because it's probably the most useful API function and it lets you access nearly all the controls' features VB has hidden from you. To understand why a single function can be so powerful, you need to grasp how controls are managed at a lower level by Windows or the VB runtime.
The Windows operating system is heavily based on messages. Whenever the user clicks on a control or presses a key, the operating system sends a message to the control under the mouse cursor or the control that currently has the input focus, depending on the user's action. Each message corresponds to a unique numeric value. For example, the WM_ LBUTTONDBLCLK message corresponds to a double-click with the left mouse button, and the WM_CHAR message fires when the user types a printable key. In most cases, a message carries some additional information with it, such as the numeric code of the key being pressed. Windows also uses messages when it needs to query or set a control's characteristic, such as its background color or the text it contains.
Use the SendMessage Function
As a VB programmer, you usually don't have to get involved with messages, because you can use a control's Caption or Text property to read or modify its contents, while the VB runtime automatically converts into events the notification messages Windows sends to a control. Alas, most VB controls potentially support features for which the language provides no property or method. This isn't a real problem, however, because you can use SendMessage to send a message to the control to affect its behavior or retrieve the value of one of its internal properties.
The first argument of the SendMessage function is the handle of the window the message is sent tousually the value returned by a form or a control's hWnd property. The next argument is the message's numeric value, for which you typically pass one of the symbolic constants the API Viewer makes available. The third and fourth arguments carry any additional information the message requests. The exact meaning of these two arguments varies, depending on the particular message; in the simplest cases, the message doesn't need any additional values, and you can pass zero in both arguments.
For example, the TextBox control internally supports undo capabilities, and allows you to programmatically restore the control's content as it was before the user edited it. To do so, send the control the EM_UNDO message and pass zero in wParam and lParam because no further values are needed:
Const EM_UNDO = &HC7
SendMessage Text1.hWnd, EM_UNDO, 0, _
ByVal CLng(0)
The last argument must be a 32-bit null value, and you must pass it by value. If you make a mistake in this phase, you can compromise your entire application. It's easy to forget the ByVal or pass a value that hasn't been converted to Long, and it's highly unfortunate that tiny errors like these produce such catastrophic consequences. To reduce the risk, many developers prefer to stay clear of statements that contain As Any clauses and instead resort to aliased, type-safe Declares. For example, you can eliminate ambiguity by using these statements:
Declare Function SendMessageByVal _
Lib "user32" Alias "SendMessageA" _
(ByVal hwnd As Long, ByVal wMsg As _
Long, ByVal wParam As Long, _
ByVal lParam As Long) As Long
Declare Function SendMessageString _
Lib "user32" Alias "SendMessageA" _
(ByVal hwnd As Long, ByVal wMsg As _
Long, ByVal wParam As Long, _
ByVal lParam As String) As Long
Then you can safely undo any change to a TextBox control's contents with this statement:
SendMessageByVal Text1.hWnd, EM_UNDO, _
0, 0
In this case, you're using SendMessage as a procedure instead of a function, because you aren't interested in the return value from Windows. In most other cases, however, you exploit the fact that the SendMessage API routine has a return value. For example, you can query a TextBox to see whether it can undo its current value. This information is typically used to enable or disable the state of the Edit | Undo menu command:
Const EM_CANUNDO = &HC6
If SendMessageByVal(Text1.hWnd, _
EM_CANUNDO, 0, 0) Then
mnuEditUndo.Enabled = True
Else
mnuEditUndo.Enabled = False
End If
This code can be shortened to:
Const EM_CANUNDO = &HC6
mnuEditUndo.Enabled = SendMessageByVal _
(Text1.hWnd, EM_CANUNDO, 0, 0)
Build Better TextBox Controls
The SendMessage function proves particularly useful with multiline TextBox controls, because many of their intrinsic features aren't exposed as properties or methods. For example, VB offers no way to determine quickly how many lines of text appear in a TextBox control.
Remember that TextBox controls have two variants. When the ScrollBars property is set to 2 - Vertical, the TextBox control behaves like a word processor, and longer lines are automatically wrapped around. When the ScrollBars property is set to 3 - Both, the control behaves like a programmer's editor; longer lines must be scrolled to uncover their right-most portion. In the latter case, you can determine the number of lines by retrieving the Text property and counting the number of carriage return/line feed character pairs (these correspond to vbCrLf symbolic constants). But deriving this information in the former case isn't that easy because you can't know where each line has been wrapped around. When you turn to API programming, all you need is a single call, which works equally well for both types of multiline controls:
Const EM_GETLINECOUNT = &HBA
LineCount = SendMessageByVal _
(Text1.hWnd, EM_GETLINECOUNT, 0, 0)
Another piece of information hidden by VB is the index of the first visible line. Again, one call to the SendMessage function solves the problem:
Const EM_GETFIRSTVISIBLELINE = &HCE
' The first line is line 0
TopLine = SendMessageByVal(Text1.hWnd, _
EM_GETFIRSTVISIBLELINE, 0, 0)
You have no direct method to set a value for the first visible line, but the EM_LINESCROLL message lets you scroll the contents of a TextBox control both horizontally and vertically:
' Positive values scroll left and up,
' negative values scroll right and
' down.
Const EM_SCROLL = &HB5
SendMessageByVal Text1.hWnd, _
EM_LINESCROLL, Columns, Lines
Scrolling the contents of the control to make the caret visible is even simpler, because you can count on the EM_SCROLLCARET message:
Const EM_SCROLLCARET = &HB7
SendMessageByVal Text1.hWnd, _
EM_SCROLLCARET, 0, 0
For instance, this code ensures the control shows the initial portion of its contents:
Text1.SelStart = 0
SendMessageByVal Text1.hWnd, _
EM_SCROLLCARET, 0, 0
To extract individual lines out of a multiline TextBox control, you need a way to determine where each line starts, and its length. You can achieve these values with two separate messages. The EM_LINEINDEX message's result is the offset of the first character in a given line; you can then pass this value to the EM_LINELENGTH message to get the line's length:
Const EM_LINEINDEX = &HBB
Const EM_LINELENGTH = &HC1
Index = SendMessageByVal(Text1.hWnd, _
EM_LINEINDEX, lineNum, 0)
Length = SendMessageByVal(Text1.hWnd, _
EM_LINELENGTH, Index, 0)
' You can now extract the line
' (Index is zero-based)
GetLine = Mid$(Text1.Text, Index + 1, _
Length)
To retrieve all the lines in one operation, however, there is a faster way. By default, a TextBox control's Text property returns all the lines of text, separated by vbCrLf characters in the points where the user has pressed the Enter key. These are the so-called hard line breaks. You can change an internal flag in the control to have the Text property include the so-called soft line breaks, which are sequences of CR-CR-LF characters inserted in the points where the control has split lines longer than the control's width.
This capability makes it easy to create a function that returns all the control's individual lines in a String array. If you use VB6, you can take advantage of the new Split function to have VB automatically extract all the portions delimited by a CR-LF sequence. This process finds both hard and soft line breaks, and you need only to get rid of any extra CR character left at the end of a line after the deletion of a soft line break (see Listing 1).
The last TextBox control feature I'll describe is the ability to set tab stop positions. When you press the Tab key in a multiline TextBox control that has the input focusand no other control on the form can get the focusthe caret moves to the next tab stop, which by default is set every eight characters on average. This value can vary if the control is displaying a nonfixed font. If you feel uncomfortable with this tab distance, you can change it to any number you prefer, using the EM_SETTABSTOPS message. You must use dialog units to express the value you pass in lParam, where each dialog unit corresponds to one fourth of the average character width. For example, execute this command to set tab stop positions every five characters:
Const EM_SETTABSTOPS = &HCB
SendMessage Text1.hWnd, _
EM_SETTABSTOPS, 1, 20
The EM_SETTABSTOPS message is flexible in that it allows you to set the position of each individual tab stop. To exploit this capability, you must store all the positions in an array of Longs, then pass its first element in lParam and the number of elements in wParam:
' Set tab stops at the 5th, 8th, and
' 10th character positions
Dim tabs(1 To 3) As Long
tabs(1) = 20 ' = 5 * 4
tabs(2) = 32 ' = 8 * 4
tabs(3) = 40 ' = 10 * 4
SendMessage Text1.hWnd, _
EM_SETTABSTOPS, 3, tabs(1)
Wrap it in a Class Module
As you've seen, once you know which messageor which API routinesolves your problem, calling it from VB isn't difficult. However, you'll probably agree that what you've seen so far isn't the kind of code the average VB programmer writes. VB developers are accustomed to properties and methods, and the concept of sending messages to do the job doesn't sound natural to them.
Fortunately, you can exploit the capabilities API programming gives you and keep the code syntax simple and natural at the same time. VB gives you two ways to do this: by creating an ActiveX control or by creating a class module that wraps around the control and adds the missing features. The two approaches demand different programming skills; in general, creating an ActiveX control proves more complex than building a wrapping class module. For this reason, I'll show only the latter technique.
The sample CTextBoxML class module exposes a property named Ctrl. You assign this property a reference to the multiline TextBox control you want to encapsulate in the class. Suppose you have a control named txtEditor, and you want to extend it using the class:
' in the client code
Dim txtEditorX As New CTextBoxML
Set txtEditorX.Ctrl = txtEditor
The CTextBoxML class exposes other properties and methods, one for each extended feature you can reach through API calls (see Listing 2). The class includes the necessary Declare and Const statements, making it self-sufficient and usable in any VB project, without requiring you to import a separate BAS file to hold the API declarations. Using the CTextBoxML class is similar to using an actual control. For example, you can get the line count using this intuitive syntax:
MsgBox "Lines = " & txtEditorX.LineCount
Similarly, this code builds on the GetLine method to print the lines in a TextBox exactly as they appear to the end user:
Dim i As Long
For i = 0 To UBound(res)
Printer.Print txtEditorX.GetLine(i)
Next
Printer.EndDoc
Figure 2 Dig Out the Hidden Capabilities of a Multiline TextBox Control.
Click here.
|
Once you become familiar with using APIs, nothing can stop you from building similar class modules that encapsulate and extend other controls. For example, the ListBox and the ComboBox controls include hidden features you can exploit simply by sending them a message. Or you can extend the PictureBox control with support for additional graphic methods.
To safely explore this promising territory, however, you need a guide explaining the do's and don'ts of API programming. The best book on this topic is undoubtedly Dan Appleman's Visual Basic Programmer's Guide to the Win32 API, which contains the reference to hundreds of useful API calls (see Resources). API programmers should also have the Microsoft Developer Network CDs that contain descriptions of the thousands of routines that make up Windows itself. (MSDN doesn't include the syntax for VB's Declare statements, though.)
With so many different API functions, you can easily get disoriented. Focus on a small subset at a time, and always remember to save your work before running a program. This reduces the damage of the virtually unavoidable system crashes that occur so frequently when you work without the safety net offered by VB.
Francesco Balena is publisher and editor-in-chief of Visual Basic Journal, Italian licensee of VBPJ, this magazine's sister publication. He is coauthor of Platinum Edition Using Visual Basic 5 (Que), author of Programming Microsoft Visual Basic 6.0 (Microsoft Press)the source of the VB code in this articleand a frequent speaker at VBITS conferences. Contact Francesco at fbalena@infomedia.it or visit his Web site at www.vb2themax.com.