LONG: How to Call Windows API from VB 3.0--General Guidelines

ID: Q110219


The information in this article applies to:


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