LONG: How to Call Windows API from VB 3.0--General Guidelines
ID: Q110219
|
The information in this article applies to:
-
Microsoft Visual Basic programming system for Windows, versions 2.0, 3.0
SUMMARY
This article gives general guidelines and examples to introduce you to the
process of calling Windows API functions from a Visual Basic application.
The examples used in this article are discussed individually in other
articles in the Microsoft Knowledge Base. They are repeated here as
examples for those new to the process of calling Windows API functions.
One of the most powerful features of Microsoft Visual Basic is the Declare
statement, which allows you, the Visual Basic programmer, to call the
routines in any Dynamic Link Library (DLL). Microsoft Windows is itself a
collection of DLLs, so Visual Basic can call almost any of the functions in
the Microsoft Windows Application Programming Interface (API). By calling
these routines you can perform tricks that are impossible in Visual Basic
alone.
MORE INFORMATION
The Windows API can appear daunting at first. You need to approach it with
a sense of adventure: for a Visual Basic programmer, the Windows API is a
huge unexplored jungle of over five hundred functions. Fortunately, Visual
Basic takes care of so many details for you that you will never have to
learn anything about most of these functions. But some of them do things
that are very hard to do in Visual Basic alone. And a few of them allow you
to do things in your Visual Basic application that you can't do any other
way. This article is your guide to the API jungle.
Backups Are Crucial
As with any good adventure, there are risks as well. When calling the
Windows API, you may declare a function incorrectly or pass it the wrong
values. As a result, you may get a general protection (GP) fault or an
Unexpected Application Error (UAE). Fortunately, insurance for this
adventure is cheap: always save your work before you run it. Keep backups
for every version.
Additional Resources You Might Want
While reading this article and trying out the examples, you may find it
helpful to keep the Visual Basic Programmer's Guide handy. Chapter 24,
"Calling Procedures in DLLs" explains details only touched on here.
To go beyond this article, you need to get documentation for the Windows
API. The Professional Edition of Visual Basic includes this information in
two Help files (WIN31WH.HLP and WIN31API.HLP) and a text file (WINAPI.TXT)
the complete list of Visual Basic DLL procedure, constant, and user-defined
type declarations for the Windows API. You can search for the declaration
you want in the WIN31API.HLP Help file; then copy and paste them into your
code. Alternatively, you can copy the declarations from the WINAPI.TXT
file. You'll find all three files in the \VB\WINAPI directory
Two-Step Process
There are two steps to using a DLL procedure in Visual Basic. First you
declare it once. Then you call it as many times as it is needed. The
remainder of this article provides a number of examples you can use to test
this two-step process.
Declaring DLL Routines
Most DLL routines, including those in the Windows API, are documented using
notation from the C programming language. This is only natural, as most
DLLs are written in C. However, this poses something of a challenge for the
intrepid Visual Basic programmer who wants to call these routines.
In order to translate the syntax of a typical API routine into a Visual
Basic Declare statement, you have to understand something about how both C
and Visual Basic pass their arguments. The usual way for C to pass numeric
arguments is "by value": a copy of the value of the argument is passed to
the routine.
Sometimes C arguments are pointers, and these arguments are said to be
passed "by reference." Passing an argument by reference allows the called
routine to modify the argument and return it. C strings and arrays are
always passed by reference.
Visual Basic, on the other hand, usually passes all of its arguments by
reference. In effect, when you pass arguments to a Visual Basic procedure
you are actually passing "far" (32 bit) pointers to those values. In order
to pass arguments to a C routine that expects its arguments to be passed by
value, you have to use the ByVal keyword with the argument in the
Declaration statement.
Obviously, if a DLL routine is expecting an argument to be passed by value
and you pass a pointer instead, it is not going to behave as you expect.
Likewise, if the routine is expecting a pointer to a value and you pass the
value itself, the routine is going to attempt to access the wrong memory
location and probably cause a GP fault or UAE. So be careful.
One added wrinkle to this is that Visual Basic strings do not use the same
format as C strings. Visual Basic has overloaded the ByVal keyword to mean
"pass a C string" when it is used with a string argument in a declare
statement.
C argument types and their equivalent declarations in Visual Basic:
If the argument is Declare it as
----------------------------------------------------------------------------
standard C string (LPSTR, char far *) ByVal S$
Visual Basic string (see note) S$
integer (WORD, HANDLE, int) ByVal I%
pointer to an integer (LPINT, int far *) I%
long (DWORD, unsigned long) ByVal L&
pointer to a long (LPDWORD, LPLONG, DWORD far *) L&
standard C array (A[ ]) base type array (no ByVal)
Visual Basic array (see note) A()
struct (typedef) S As Struct
NOTE: You will never pass a Visual Basic string or array to a DLL routine
unless the DLL was written specifically for use with Visual Basic. Visual
Basic strings and arrays are represented in memory by "descriptors" (not
pointers), which are useless to DLL routines that were not written with
Visual Basic in mind.
There is one more complication to this, however. Some Windows functions
take a 32 bit argument that sometimes is a "far" (32 bit) pointer to
something and sometimes is just a 32 bit value. The fourth argument in the
SendMessage function is like this. If you are going to call it with just a
pointer or with just a value, you can declare it appropriately. For
example, you could declare SendMessage to take a pointer to a string:
' Enter the following Declare statement as one, single line:
Declare Function SendMessage Lib "user"
(ByVal hWnd%, ByVal msg%, ByVal wp%, ByVal lp$) As Long
Or you could declare it to take a 32 bit value:
' Enter the following Declare statement as one, single line:
Declare Function SendMessage Lib "user"
(ByVal hWnd%, ByVal msg%, ByVal wp%, ByVal lp&) As Long
Notice the fourth argument is declared ByVal lp$ in the first example and
ByVal lp& in the second.
However, what if you want to call it with both kinds of arguments in the
same program? If you declare it one way and call it another, you will get
an error message from Visual Basic. The solution is to declare the argument
As Any:
' Enter the following Declare statement as one, single line:
Declare Function SendMessage Lib "user"
(ByVal hWnd%, ByVal msg%, ByVal wp%, lp As Any) As Long
The Any "data type" tells Visual Basic not to do any type checking for that
argument. So now you can pass it anything as long as it is what the
function is expecting. If an argument is declared As Any, you must specify
whether the argument is passed by value or not -- when you actually call
the function. You do this by using ByVal for strings and for arguments that
should be passed by value, and omitting ByVal for arguments that should be
passed by reference. Use the appropriate entry from the second column in
the table shown above as the argument when you call the function.
For example, if you have declared the fourth argument in SendMessage As
Any, you can pass a string in that argument:
buflen& = SendMessage(txthWnd, EM_GETLINE, lineNum%, ByVal buf$)
Notice you use the ByVal keyword in the call. This tells Visual Basic that
you want to pass a standard C string. If you don't include the ByVal, you
will pass a Visual Basic string descriptor, which is not something
SendMessage knows how to handle.
You can also pass an array:
dummy& = SendMessage(any_hWnd%, EM_SETTABSTOPS, NumCol%, ColSizes(1))
Notice you do not use ByVal in this case because you want to pass a pointer
(specifically a pointer to the indicated element in the array -- all
subsequent array elements are packed into memory after it).
You can pass a long integer value as the fourth argument:
dummy& = SendMessage(txthWnd, EM_LINESCROLL, 0, ByVal ScrollAmount&)
Note the use of ByVal here. You want to pass the value itself, rather than
a pointer to the value. It's very important that you pass a Long integer
for this argument. If you pass a normal Integer Visual Basic will not
convert it into Long.
If you're careful to match up what you're passing with what the routine
expects, you should have no trouble calling the Windows API to get Visual
Basic to do what you want as demonstrated in the examples that comprise the
remainder of this article.
Scoping Out the System
One of the nice things about Windows is that it insulates you from a lot of
the details of the system. You can print to the printer without knowing
what kind it is; you can display things on the screen without knowing its
resolution. However, there may be times when your application needs to know
key information about the system. For example, you may want your
application to perform different calculations depending on whether the
system has a math coprocessor or not.
Fortunately, Windows provides several functions that you can use to obtain
this kind of information. For example the GetWinFlags API function can give
you a lot of information.
Place this declaration in the declarations section of a form or module, or
in the global module:
Declare Function GetWinFlags Lib "kernel" () As Long
As functions in the Windows API go, this one is very simple. It is found in
the Windows "kernel" DLL. It takes no arguments (hence the empty
parentheses in the declaration) and returns a Long integer. This Long will
have bits, or flags, set to indicate certain facts about the system. Here
are some of the flags:
Const WF_CPU286 = &H2&
Const WF_CPU386 = &H4&
Const WF_CPU486 = &H8&
Const WF_STANDARD = &H10&
Const WF_ENHANCED = &H20&
Const WF_80x87 = &H400&
Place the constants in the Declarations section of the form or module where
you declare the GetWinFlags function.
Now you can call GetWinFlags and use the And operator with these constants
to test the value returned. For example:
Dim WinFlags As Long
WinFlags = GetWinFlags()
If WinFlags And WF_ENHANCED Then
Print "Windows Enhanced Mode ";
Else
Print "Windows Standard Mode ";
End If
If WinFlags And WF_CPU486 Then Print "on a 486"
If WinFlags And WF_CPU386 Then Print "on a 386"
If WinFlags And WF_CPU286 Then Print "on a 286"
If WinFlags And WF_80x87 Then Print "Math coprocessor available"
There's one important fact about the system that this function does not
provide: the version of Windows. You can obtain that information with the
GetVersion function:
Declare Function GetVersion Lib "Kernel" () as Long
This returns a Long integer containing the version numbers of MS-DOS and
Windows. Here's the code that extracts the version information:
Dim Ver As Long, WinVer As Long, DosVer As Long
Ver = GetVersion()
WinVer = Ver And &HFFFF
Print "Windows " + Format$(WinVer Mod 256) + "." + Format$(WinVer \ 256)
DosVer = Ver \ &H10000
Print "MS-DOS " + Format$(DosVer \ 256) + "." + Format$(DosVer Mod 256)
GetSystemMetrics is another Windows function that provides useful system
information. You declare it like this:
Declare Function GetSystemMetrics Lib "User" (ByVal nIndex%) As Integer
This function is located in the "User" DLL. It takes one argument: an
integer indicating which item of system information you want it to return.
This argument, like most arguments to Windows API functions, is passed by
value. Because Visual Basic usually passes arguments by reference, you have
to include the ByVal keyword to specify the argument should be passed by
value. This is very important. Forgetting ByVal when it is needed or
including it when it isn't often leads to problems.
GetSystemMetrics provides a potpourri of information. For example, you can
use it to find out if a mouse is installed in the system with code like
this:
Const SM_MOUSEPRESENT = 19
If GetSystemMetrics(SM_MOUSEPRESENT) Then Print "Mouse installed"
Some other useful information provided by GetSystemMetrics is the size of
the arrow bitmaps used by standard horizontal and vertical scroll bars.
This is important because the size of these bitmaps varies with the
resolution of the display and the display driver installed. When you create
an application that uses a horizontal scroll bar control, you usually give
it a fixed height; likewise, when you create a form with a vertical scroll
bar control, you usually give it a fixed width. You fix these sizes based
on what looks good on your system. Unfortunately, what looks good on your
display can look strange on a display that has a different resolution. If
you are writing applications that need to look good on a variety of display
resolutions, you need to write code that can determine the standard size of
scroll bars on the current display and dynamically resize your scroll bar
controls to match. You need to write code like this:
Const SM_CXVSCROLL = 2
Const SM_CYHSCROLL = 3
ScaleMode = 3 'Pixels
VScroll1.Width = GetSystemMetrics(SM_CXVSCROLL)
HScroll1.Height = GetSystemMetrics(SM_CYHSCROLL)
Notice that the values returned by the GetSystemMetrics function are always
in pixels, so you need to set the ScaleMode of the form to 3 (pixels)
before setting the sizes of the scroll bars.
There are a lot of other system values you can obtain using
GetSystemMetrics, but not all of them are useful to Visual Basic
programmers. Here are a few of the interesting ones:
Const SM_CXSCREEN = 0 'Width of screen in pixels
Const SM_CYSCREEN = 1 'Height of screen in pixels
Const SM_CYCAPTION = 4 'Height of form titlebar in pixels
Const SM_CXICON = 11 'Width of icon in pixels
Const SM_CYICON = 12 'Height of icon in pixels
Const SM_CXCURSOR = 13 'Width of mousepointer in pixels
Const SM_CYCURSOR = 14 'Height of mousepointer in pixels
Const SM_CYMENU = 15 'Height of top menu bar in pixels
Yet another function that provides system information is GetDeviceCaps.
This function returns information about a particular device in the system,
such as the printer or the display. Like many of the functions you will see
in this article, the declaration for GetDeviceCaps is too long to fit on
one line, but you must type it all on one, single line:
' Enter the following Declare statement as one, single line:
Declare Function GetDeviceCaps Lib "GDI" (ByVal hDC%, ByVal nIndex%)
As Integer
GetDeviceCaps is found in the "GDI" DLL and it takes two arguments. The
first allows you to specify the device for which you want information. When
calling the function from Visual Basic, supply either the hDC property of a
form or the hDC property of the Printer object. The second argument
specifies the device information you want to get. There are a lot of
possible values for this second argument, but only a couple of them are
very interesting to the Visual Basic programmer. For example, you can find
out how many colors the screen or printer supports:
Const PLANES = 14
Const BITSPIXEL = 12
Dim Cols As Long
Cols = GetDeviceCaps(hDC, PLANES) * 2 ^ GetDeviceCaps(hDC, BITSPIXEL)
The number of colors a device supports is the product of the number of
color planes it has and the number of bits per pixel in each plane. Because
each bit can represent two colors, you have to raise 2 to the power of the
number of bits per pixel, and then multiply that by the number of color
planes, to get the total number of colors that the device can display.
Some Useful Tricks
That's enough poking about in the system. Here are some useful tricks.
If you have used the Shell function in Visual Basic, you have probably
discovered that it will only run files that have the extension .EXE, .COM,
.PIF, or .BAT. But you can double-click almost any file in the File
Manager, and Windows does the right thing. For example, if it is a .TXT
file, Windows starts Notepad. How would you add this kind of functionality
to your own applications?
Windows stores the association between data files and their related
application (such as the association between a .TXT file and the NOTEPAD
application) in the WIN.INI file in the Extensions section. Windows also
provides a function called GetProfileString that reads the WIN.INI file for
you. Here is the Declare for GetProfileString:
' Enter the following Declare statement on one, single line:
Declare Function GetProfileString Lib "Kernel"
(ByVal Sname$, ByVal Kname$, ByVal Def$, ByVal Ret$, ByVal Size%)
As Integer
GetProfileString searches the WIN.INI section specified in the first
argument for the key specified in the second argument. It will then return
the value for the key specified in the fourth argument unless the specified
key in the specifed section was not located. In this event, the value
returned in the fouth argument will be equal to that in the third default
argument. The fifth argument provides the length of the string passed as
the fourth argument.
Therefore, once you know the extension of a file, you can use
GetProfileString to find the parent application for files with that
extension. Here's a function that does that:
Function FindApp (Ext As String) As String
' Find the parent app for a file with the given extension
Dim Sname As String, Ret As String, Default As String
Ret = String$(255, 0)
Default = Ret
Sname = "Extensions"
nSize = GetProfileString(Sname, Ext, Default, Ret, Len(Ret))
If Left$(Ret, 1) <> Chr$(0) Then
FindApp = Mid$(Ret, 1, InStr(Ret, "^") - 1)
End If
End Function
GetProfileString is an example of a Windows function that returns a string
by modifying one of its arguments. To use these kinds of functions, you
must create a string and fill it with something (character code 0 in the
example above) before you call the function. This is because Windows cannot
enlarge strings the way Visual Basic can, so whenever you pass a string to
Windows you must ensure that it is long enough to hold the largest possible
string that Windows might return.
You can add new values to WIN.INI using the WriteProfileString function:
' Enter the following Declare statement on one, single line:
Declare Function WriteProfileString Lib "Kernel"
(ByVal Sname$, ByVal Kname$, ByVal Set$) As Integer
This function searches the WIN.INI file for the section specified in the
first argument and the key specified in the second argument. Then it
replaces the key value with the value specified in the third argument. If
the key is not found, it adds the key and its value to the specified
section. If it does not find the section, it adds that to WIN.INI as well.
Some applications use their own private .INI files rather than using
WIN.INI -- Visual Basic has its own VB.INI, for example. You can use the
functions GetPrivateProfileString and WritePrivateProfileString to
manipulate other .INI files:
' Enter each of the following Declare statements on one, single line:
Declare Function GetPrivateProfileString Lib "Kernel"
(ByVal Sname$, ByVal Kname$, ByVal Def$, ByVal Ret$, ByVal Size%,
ByVal Fname$) As Integer
Declare Function WritePrivateProfileString Lib "Kernel"
(ByVal Sname$, ByVal Kname$, ByVal Set$, ByVal Fname$) As Integer
These work exactly like GetProfileString and WriteProfileString, except
they have one additional argument that specifies the path and filename of
the .INI file.
Now, where should you put that custom .INI file? One obvious place is the
Windows directory. But people name that directory all sorts of things:
\WINDOWS or \WIN3 or who knows what. It might not even be at the root
level. How can you find it? Well, Windows knows where this directory is,
and it provides a function to tell you:
' Enter the following Declare statement on one, single line:
Declare Function GetWindowsDirectory Lib "Kernel"
(ByVal P$, ByVal S%) As Integer
The first argument is a string that the function will fill with the path to
the Windows directory; the second argument is the length of this string.
Again, because you are passing a string to be filled by Windows, you must
make sure it is large enough to accommodate whatever string Windows might
provide. In this case, the Windows Reference warns that it should be at
least 144 characters. On the other hand, 144 characters is the worst case
so in most cases there will be a lot of unused characters that you will
need to trim off. The GetWindowsDirectory function returns an integer value
that indicates the actual length of the returned string. So here's some
fancy code that calls the function and trims the returned string all in one
line:
Dim WinPath As String
WinPath = String$(145, Chr$(0))
WinPath = Left$(WinPath, GetWindowsDirectory(WinPath, Len(WinPath)))
In addition, there is usually a \SYSTEM directory within the windows
directory. Once again, that could be called anything, and once again
Windows provides a function to find it: GetSystemDirectory.
This function is declared in exactly the same way as GetWindowsDirectory
and can be called in the same way, so substitute it in the declaration and
code above and try it out.
Another sensible place to put that .INI file is in the same directory as
the application. It should be trivial to figure out what directory a
running Visual Basic application is stored in, but it's not. This
information isn't provided through the Command$ system variable,
unfortunately. And even CurDir$ isn't reliable because the application
could have been run using a full path without changing the current
directory to the application's directory. Windows API calls to the
GetModuleHandle and GetModuleFileName functions give you what you need.
Here are the declarations:
' Enter each of the following Declare statement on one, single line:
Declare Function GetModuleHandle Lib "Kernel"
(ByVal FileName$) As Integer
Declare Function GetModuleFileName Lib "Kernel"
(ByVal hModule%, ByVal FileName$, ByVal nSize%) As Integer
GetModuleHandle takes the filename of a running program and returns a
"module handle." All you need to know about the module handle is that it is
an integer and you pass it to the GetModuleFileName function.
GetModuleFilename takes three arguments; the first is the module handle
returned by GetModuleHandle. The second is a string that the function fills
with the complete path and filename of the program specified by the module
handle. The third is the size of this string. The value returned by
GetModuleFilename is the length of the path that it placed in the string
you passed.
Using these two functions, obtaining the path to a running program is easy:
Dim hMod As Integer, Path As String
hMod = GetModuleHandle%("MyApp.EXE")
Path = String$(145, Chr$(0))
Path = Left$(Path, GetModuleFileName%(hMod, Path, Len(Path))
Notice that you are again passing a large string and using the value
returned by the function to trim the string down to size with Left$.
Another trick you can perform with GetModuleHandle is limiting your
application to a single instance. Normally, Windows allows you to run
multiple instances (copies) of the same program. Most of the time this is a
handy feature, but sometimes it can cause problems. If your program uses
data files, having more than one instance of the program accessing those files
at the same time can leave the files in an inconsistent state. You could
write the program so that it works correctly even if there are multiple
copies running, but that's a lot of work and sometimes it's not even
possible. An easier method is to just ensure that only one copy of the
program can be run. Thanks to GetModuleHandle and another Windows function,
GetModuleUsage, this is easy:
Declare Function GetModuleUsage Lib "Kernel" (ByVal hModule%) As Integer
GetModuleUsage returns how many instances of the specified program exist.
The program is specified by passing GetModuleUsage a module handle, which
is what GetModuleHandle returns. Putting code like this in the Form_Load
event for your startup form (or in your Sub Main if you don't have a
startup form) ensures that only a single instance of your application can
be run:
If GetModuleUsage(GetModuleHandle("YOURAPP.EXE")) > 1 Then
MsgBox "This program is already loaded!", 16
End
End If
GetModuleHandle and GetModuleUsage work for DLLs as well as ordinary
executable files, so you could use this technique to find out how many
Visual Basic executables are running by using GetModuleUsage with the
module handle for VBRUN100.DLL.
Sending Messages to Controls
Windows is built around messages. The Windows system sends messages to
applications. You see these messages in your Visual Basic program as
events. In addition, applications send messages to each other (this is the
basis for DDE), and applications even send messages to themselves.
You can get in on the action. By sending messages to controls, you can get
them to do things that would otherwise be impossible, such as setting
tab stops in list boxes and getting a single line of text from a multi-line
text box. You can also do things by sending a single message that would
take a lot more Basic code to accomplish, such as emptying a list box.
You send messages with SendMessage function:
' Enter the following Declare statement as one, single line:
Declare Function SendMessage Lib "User"
(ByVal hWnd%, ByVal msg%, ByVal wp%, lp As Any) As Long
The first argument identifies the recipient of the message. Windows uses
"handles" to keeps track of everything it uses. Handles are integer ID
numbers that Windows assigns to things -- like the module handles used
earlier to refer to programs.
Controls are just another kind of window as far as Windows is concerned.
Controls are identified by their window handle, or hWnd. To send a message
to a control you need its hWnd.
Visual Basic provides the hWnd for a form through the hWnd property.
Unfortunately, there is no such property for any of the controls. The
controls are windows and do have hWnds, but Visual Basic doesn't provide
them for you. So you have to resort to some subterfuge. Windows has a
function called GetFocus that will return the hWnd for the window that has
the focus:
Declare Function GetFocus Lib "user" () As Integer
And we can give the focus to any control using the Visual Basic SetFocus
method. So to get the hWnd for a control, use code similar to this:
AnyControl.SetFocus
control_hWnd = GetFocus()
The second argument in the SendMessage function is the message number. All
of the message numbers are some offset from the WM_USER message, which has
the value 1024 (&H400 in hexadecimal notation). The complete list of
message numbers is included in the WINAPI.TXT file.
The last two arguments in SendMessage supply additional information for a
particular message. What they contain varies from message to message.
Notice that the last argument was declared As Any. This is different from
the way the SendMessage function is declared in the WINAPI.TXT file. It
allows you to pass any data type as the fourth argument.
The Long integer value that SendMessage returns depends on what message you
sent. Sometimes you send a message to tell a control to do something, and
the return value is zero if the control could perform the action and non-
zero if it could not. Sometimes you send a message to a control to find out
something about that control, and in those cases the return value is the
information you requested. And sometimes the return value means nothing at
all.
Try out SendMessage by starting with something simple: emptying list boxes.
If you've spent much time programming with list boxes, you are probably
annoyed that there is no simple way to empty a list. Instead of telling the
list box to simply empty itself, you have to loop through all the entries
and use the RemoveItem method on each one. But there's a better way.
Windows provides a message (LB_RESETCONTENT) that you can send to a list
box to make it empty itself in one step.
Const WM_USER = &H400
Const LB_RESETCONTENT = WM_USER + 5
Here is a procedure that uses this message to empty any list box:
Sub ClearListBox(Ctrl As Control)
Ctrl.SetFocus
dummy& = SendMessage(GetFocus(), LB_RESETCONTENT, 0, ByVal 0&)
End Sub
The RESETCONTENT message needs no additional information, so the last two
arguments to SendMessage are zero. Notice that the last argument is ByVal
0& (zero followed by an ampersand character). The ampersand is very
important; it ensures that a long (32 bit) zero is passed. There is no
useful information returned when this message is sent, so the SendMessage
function is assigned to a dummy variable.
You can also empty combo box lists in the same way; just use this constant
for the message:
Const CB_RESETCONTENT = WM_USER+11
While on the topic of lists, there's an easy way to find strings in a list.
You can loop through all the items in the list, but why bother when the
LB_FINDSTRING message allows you to find a string in a list box with a
single function call? When you send the LB_FINDSTRING message, the
SendMessage function returns the index of the first item in the list that
matches the string you specified (so obviously you should only use this
message with sorted list boxes).
Const LB_FINDSTRING = WM_USER + 16
Dim itemNum As Long
itemNum = SendMessage(GetFocus(), LB_FINDSTRING, -1, ByVal "Visual")
Print "Windows is item: "; Format$(itemNum)
This finds the first list item that begins with "Visual" and returns its
index in the list. It will match even if there are additional characters
following the specified string, so the example above would match "Visual
Basic" if it was the first string in the list beginning with "Visual."
Again, this technique works as well with combo boxes as it works with list
boxes. Just use the message:
Const CB_FINDSTRING = WM_USER + 12
And, speaking of combo boxes, here's a neat trick for a dropdown list combo
box (a combo box with the Style property set to 2). This code drops the
list automatically when the combo box gets the focus:
Sub Combo1_GotFocus ()
Const CB_SHOWDROPDOWN = WM_USER + 15
Dummy& = SendMessage(GetFocus(), CB_SHOWDROPDOWN, 1, ByVal 0&)
End Sub
Sending messages in the GotFocus event for a control is a good technique
because it allows you to avoid explicitly setting the focus to a control to
get its hWnd.
Another useful message is LB_GETTOPINDEX. When you send this message to a
list box, the SendMessage function returns the index of the first visible
item in the list. This is valuable if the list has been scrolled and you
want to determine which items are actually visible in the list (in a
DragDrop event, for example).
Const LB_GETTOPINDEX = WM_USER+15
FirstItem& = SendMessage(GetFocus(), LB_GETTOPINDEX, 0, ByVal 0&)
You can also send a message to scroll a list box to make any item the first
visible item in the list:
Const LB_SETTOPINDEX = WM_USER+24
Success& = SendMessage(GetFocus(), LB_SETTOPINDEX, item%, ByVal 0&)
You could combine this message with the LB_FINDSTRING message to scroll the
list box so that the list item found by LB_FINDSTRING is at the top of the
list.
One especially valuable use of SendMessage is to set the tabstops in a list
or text box. You have probably discovered that list boxes and multi-line text
boxes handle tabs automatically, so if you assign text that contains tabs
(character code 9) the columns line up automatically. Unfortunately, Visual
Basic gives you no way to adjust where the columns fall -- except by
sending a message. For a list box, the message is:
Const LB_SETTABSTOPS = WM_USER + 19
When you send this message, you must supply an array of integers that
specify the new tab positions. (These positions are specified in terms of
characters; when the list box or text box contains a proportional font,
these are "average" characters.) This array is the fourth argument to the
SendMessage function; the number of elements in the array is the third
argument. Notice that to pass an array to a Windows function, you actually
pass the first element of the array:
ReDim tabs(3) As Integer
tabs(1) = 10
tabs(2) = 50
tabs(3) = 90
List.SetFocus
dummy& = SendMessage(GetFocus(), LB_SETTABSTOPS, 0, ByVal 0&)
dummy& = SendMessage(GetFocus(), LB_SETTABSTOPS, 3, tabs(1))
The first call to SendMessage clears any existing tabstops; the second sets
three tabstops as specified in the array.
You can set the tabstops in a multi-line text box as well; just send the
message EM_SETTABSTOPS:
Const EM_SETTABSTOPS = WM_USER + 27
This won't work for a single-line text box. Speaking of multi-line text
boxes, there are several useful messages you can send to a multi-line text
box to get information that Visual Basic does not provide. For example, you
can send the EM_GETLINECOUNT message to get the number of lines text in a
multi-line text box:
Const EM_GETLINECOUNT = WM_USER+10
lineCount& = SendMessage(GetFocus(), EM_GETLINECOUNT, 0, ByVal 0&)
You can obtain the text contained in any line of a multi-line text box with
the message:
Const EM_GETLINE = WM_USER + 20
When sending this message, you have to provide the number of the line you
want to retrieve in the third argument to SendMessage, and the string to be
filled with the contents of the line as the fourth argument. There's one
odd thing about this string. Normally, when you pass a string to a Windows
function you also supply the size of the string as an argument. However,
the usual place for that information is in the third argument, and that is
already being used to specify which line you want retrieved. So you have to
place the length of the string in the first two bytes of the string, using
code like this:
Dim LineNum As Integer, linelength As Integer, buf As String
'Set linelength to some reasonable value
buf = String$(linelength, chr$(0))
buf = Chr$(linelength Mod 256) + Chr$(linelength \ 256) + Buf
' Enter the following two lines as one, single line:
buf = Left$(buf,
SendMessage(GetFocus(), EM_GETLINE, lineNum, ByVal buf))
Another handy message for multi-line text boxes is EM_LINESCROLL, which
allows you to scroll them horizontally and vertically. You specify the
amount to scroll in the fourth argument of the SendMessage function: place
the number of characters to scroll horizontally in the high word (by
multiplying by 65536) and the number of lines to scroll vertically in the
low order word. For example:
Sub ScrollIt (ctl As Control, chars As Integer, lines As Integer)
Const EM_LINESCROLL = WM_USER + 6
Dim scroll As Long
scroll = chars * 65536 + lines
ctl.SetFocus
dummy& = SendMessage(GetFocus(), EM_LINESCROLL, 0, ByVal scroll)
End Sub
This is a relative scroll: if you use the value 2 the text box will scroll
down by two lines; if you use the value -65536 the text box will scroll
left by one character.
Another feature that Visual Basic does not directly support is a way to
restrict the number of characters that can be entered in a text box. You
can do this by responding to the various Key events, but there is an easier
way: send the EM_LIMITTEXT message to the text box:
Sub Text1_GotFocus()
Const EM_LIMITTEXT = WM_USER+21
dummy& = SendMessage(GetFocus(), EM_LIMITTEXT, numChars, ByVal 0&)
End Sub
Here the third argument specifies the maximum number of characters the text
box will accept. If you want to set it back to normal, send EM_LIMITTEXT
with that argument set to zero. You can also restrict the number of
characters accepted by a combo box by sending the combo box the message
CB_LIMITTEXT:
Const CB_LIMITTEXT = WM_USER+1
One last trick: turn a text box into a password control. Windows provides
automatic support for text boxes that display asterisks (or some other
character) instead of the actual characters the user types. To take
advantage of this support, set a style bit in the text box. Normally you
set style bits when you create a control, but you can't do that because
Visual Basic creates the control for you.
Fortunately, Windows allows you to set that bit after the control is
created (this is one of the few style bits that you can change after a
control is created). The functions that get and set the style information
for a window are as follows:
' Enter each of the following Declare statements as one, single line:
Declare Function GetWindowLong Lib "User"
(ByVal hWnd%, ByVal nIndex%) As Long
Declare Function SetWindowLong Lib "User"
(ByVal hWnd%, ByVal nIndex%, ByVal NewLong&) As Long
To set the password style bit, call GetWindowLong to get the style
information, use the Or operator to set the bit, and then call
SetWindowLong to store the new style. Once again, do this in the GotFocus
event so you don't have to worry about using SetFocus to get the hWnd:
Sub Text1_GotFocus ()
Const ES_PASSWORD = &H20
Const EM_SETPASSWORD = 1052
Const GWL_STYLE = -16
Const Asterisk = 42
Dim TxthWnd As Integer, WindowLong As Long
TxthWnd = GetFocus()
WindowLong = GetWindowLong(TxthWnd, GWL_STYLE)
WindowLong = WindowLong Or ES_PASSWORD
WindowLong = SetWindowLong(TxthWnd, GWL_STYLE, WindowLong)
WindowLong = SendMessage(TxthWnd, EM_SETPASSWORD, Asterisk, ByVal 0&)
End Sub
You can define the character you want displayed in place of the actual
characters the user types by sending the EM_SETPASSWORD message to the
control. This example sets the password character to asterisks. If you want
to use a different character, supply a different ANSI character code when
you send the EM_SETPASSWORD message.
There are some limitations to this password functionality. For one thing,
the characters in the text box are not stored as asterisks; they are just
displayed that way. This is good, because it allows your code to easily
check what the user typed. But it is also bad, because any user can select
the contents of the text box, copy it, and paste it somewhere else and see
the actual characters that were typed in the text box. Whether you consider
this a flaw depends on whether you expect the text box containing a
password to sit around on a screen where some malicious users can copy it.
Usually, this is not a problem. But it is something to keep in mind.
Exploring on Your Own
The Windows API is like an enormous hidden world that is just waiting to be
explored. The API documentation is the treasure map, and like the maps in
all good adventure stories, it requires some translation to be useful. And
you aren't limited to just the Windows API. Almost any DLL contains
functions that you might find useful. For example, many spell checkers are
implemented as DLLs; if you know how to declare and call the functions in
one of these DLLs, you can add spell-checking to your Visual Basic programs
(assuming that you obey the copyright restrictions for the DLL, of course).
Finding out how to declare and call the DLL functions can be tough
sometimes, however. You'll learn to keep your eyes open for anything that
looks like API documentation. Who knows what treasure you'll discover
inside some obscure DLL? And as new versions of Windows appear, the
treasure will only increase. The Multimedia Extensions for Microsoft
Windows are just a collection of DLLs, after all. With the right hardware,
think of how much fun you'll have calling those from your Visual Basic
programs.
Additional query words:
2.00 3.00
Keywords :
Version :
Platform :
Issue type :
Last Reviewed: May 13, 1999