Using A DLL Procedure in Your Application: Dlls or Automation?
Using A DLL Procedure in Your Application: Dlls or Automation?
with Visual Basic, you can make direct calls to procedures contained in dynamic-link
libraries (DLLs). By calling procedures in DLLs, you can access the thousands of
procedures that form the backbone of the Microsoft Windows operating system, as
well as routines written in other languages.
As their name suggests, DLLs are libraries of procedures that applications can link to
and use at run time rather than link to statically at compile time. This means that the
libraries can be updated independently of the application, and many applications can
share a single DLL. Microsoft Windows itself is comprised of DLLs, and other
applications call the procedures within these libraries to display windows and
graphics, manage memory, or perform other tasks. These procedures are sometimes
referred to as the Windows API, or application programming interface.
Note If you are using the Control Creation Edition of Visual Basic, some of the material
covered in this document may not be applicable. With the full editions of Visual Basic you have
the ability to create applications, ActiveX documents, and other types of components. Although
some of the terminology may relate to application specific objects such as forms, in most
cases the underlying concepts also apply to ActiveX control creation.
DLLs or Automation?
Another way to bring more power into Visual Basic is through Automation (formerly
called OLE Automation). Using Automation is simpler than calling routines in a
DLL, and it doesn't create the same level of risk that you'll hit when going straight to
the Windows API. By using Automation, you can get programmatic access to a wide
range of objects exposed by external applications.
Contents
Using a DLL Procedure in Your Application
Accessing the Microsoft Windows API
Declaring a DLL Procedure
Passing Strings to a DLL Procedure
Passing Arrays to a DLL Procedure
Passing User-Defined Types to a DLL Procedure
Passing Function Pointers to DLL Procedures and Type Libraries
Passing Other Types of Information to a DLL Procedure
Converting C Declarations to Visual Basic
1
In the following example, we'll show how to call a procedure from the Windows API. The
function we'll call, SetWindowText, changes the caption on a form. While in practice, you
would always change a caption by using Visual Basic's Caption property, this example offers
a simple model of declaring and calling a procedure.
2
3
To load the Win32api.txt file, choose Load Text File from the File menu. After the file
is loaded, you can view entries in the Available Items list box by selecting Declares,
Constants, or Types in the API Type drop-down list box. To search for specific items
To load a Jet database API file, choose Load Database File from the File menu. You
can then search for specific items in the database by typing the first letter of the item
you want to find.
4
Argument Description
/T API Viewer will load the file as a text file. /T must be uppercase.
/D API Viewer will load the file as a database file. /D must be uppercase.
filename The path of the file you want to open.
5
For other DLLs, the Lib clause is a file specification that can include a path:
Declare Function lzCopy Lib "c:\windows\lzexpand.dll" _
(ByVal S As Integer, ByVal D As Integer) As Long
If you do not specify a path for libname, Visual Basic will search for the file in the
following order:
1. Directory containing the .exe file
2. Current directory
3. Windows 32-bit system directory (often but not necessarily \Windows\System32)
4. Windows 16-bit system directory (often but not necessarily \Windows\System)
5. Windows directory (not necessarily \Windows)
6. Path environment variable
6
7
Note that the string that follows the Alias clause must be the true, case-sensitive name
of the procedure.
Important For API functions you use in Visual Basic, you should specify the ANSI version of
a function, because Unicode versions are only supported by Windows NT — not Windows 95.
Use the Unicode versions only if you can be certain that your applications will be run only on
Windows NT-based systems.
String arguments are a special case. Passing a string by value means you are passing
the address of the first data byte in the string; passing a string by reference means you
are passing the memory address where another address is stored; the second address
actually refers to the first data byte of the string. How you determine which approach
to use is explained in the topic "Passing Strings to a DLL Procedure" later in this
chapter.
Nonstandard Names
Occasionally, a DLL procedure has a name that is not a legal identifier. It might have
an invalid character (such as a hyphen), or the name might be the same as a Visual
Basic keyword (such as GetObject). When this is the case, use the Alias keyword to
specify the illegal procedure name.
For example, some procedures in the operating environment DLLs begin with an
underscore character. While you can use an underscore in a Visual Basic identifier,
8
To declare a DLL procedure by ordinal number, use the Alias clause with a string
containing the number sign character ( #) and the ordinal number of the procedure.
For example, the ordinal number of the GetWindowsDirectory function has the value
432 in the Windows kernel; you can declare the DLL procedure as follows:
Declare Function GetWindowsDirectory Lib "kernel32" _
Alias "#432" (ByVal lpBuffer As String, _
ByVal nSize As Long) As Long
Notice that you could specify any valid name for the procedure in this case, because
Visual Basic is using the ordinal number to find the procedure in the DLL.
To obtain the ordinal number of a procedure you want to declare, you can use a utility
application, such as Dumpbin.exe, to examine the .dll file. (Dumpbin.exe is a utility
included with Microsoft Visual C++.) By running Dumpbin on a .dll file, you can
extract information such as a list of functions contained within the DLL, their ordinal
numbers, and other information about the code.
For More Information For more information on running the Dumpbin utility, refer to
the Microsoft Visual C++ documentation.
9
The procedures in most DLLs (and in all procedures in the Windows API) recognize
LPSTR types, which are pointers to standard null-terminated C strings (also called
10
11
A safe way to call this procedure is to first use the String function to set the returned
argument to at least 255 characters by filling it with null (binary zero) characters:
Path = String(255, vbNullChar)
ReturnLength = GetWindowsDirectory(Path, Len(Path))
Path = Left(Path, ReturnLength)
Both of these processes have the same result: They create a fixed-length string that
can contain the longest possible string the procedure might return.
Note Windows API DLL procedures generally do not expect string buffers longer than 255
characters. While this is true for many other libraries, always consult the documentation for the
procedure.
When the DLL procedure calls for a memory buffer, you can either use the
appropriate data type, or use an array of the byte data type.
12
Sometimes you may want to pass an entire array to a DLL procedure. If the DLL
procedure was written especially for Automation, then you may be able to pass an
array to the procedure the same way you pass an array to a Visual Basic procedure:
with empty parentheses. Because Visual Basic uses Automation data types, including
SAFEARRAYs, the DLL must be written to accommodate Automation for it to accept
Visual Basic array arguments. For further information, consult the documentation for
the specific DLL.
If the DLL procedure doesn't accept Automation SAFEARRAYs directly, you can still
pass an entire array if it is a numeric array. You pass an entire numeric array by
passing the first element of the array by reference. This works because numeric array
data is always laid out sequentially in memory. If you pass the first element of an
array to a DLL procedure, that DLL then has access to all of the array's elements.
As an example, consider how you can use an API call to set tab stops within a text
box There are internal tab stops in multiple-line (but not single-line) text box
controls: If the text in the text box contains tab characters (character code 9), the text
following the tab character is aligned at the next tab stop. You can set the position of
these tab stops by calling the SendMessage function in the Windows API and passing
an array that contains the new tab stop settings.
Private Declare Function SendMessageSetTabs Lib _
"user32" Alias "SendMessageA" (ByVal hwnd As Long, _
ByVal wMsg As Long, ByVal wParam As Long, _
lParam As Any) As Long
Const EM_SETTABSTOPS = &HCB
When you call this procedure, you specify the name of the text box and the number of
tab stops you want to use for the indent. For example:
13
This approach will also work for string arrays. A DLL procedure written in C treats a
string array as an array of pointers to string data, which is the same way Visual Basic
defines a string array.
For More Information For more information on SAFEARRAYs and other
Automation data types, see the Microsoft Press book, OLE 2 Programmer's
Reference.
Two of the procedures that accept a rectangle are DrawFocusRect, which draws a
dotted outline around the specified rectangle, and InvertRect, which inverts the colors
of the specified rectangle. To use the procedures, place these declarations in the
Declarations section of a form or standard module:
Declare Function DrawFocusRect Lib "User32" Alias _
"DrawFocusRect" (ByVal hdc As Long, _
lpRect As RECT) As Long
Now you can use the following Sub procedures to call the DLLs:
14
User-defined types can contain objects, arrays, and BSTR strings, although most DLL
procedures that accept user-defined types do not expect them to contain string data. If
the string elements are fixed-length strings, they look like null-terminated strings to
the DLL and are stored in memory like any other value. Variable-length strings are
incorporated in a user-defined type as pointers to string data. Four bytes are required
for each variable-length string element.
Note When passing a user-defined type that contains binary data to a DLL procedure, store
the binary data in a variable of an array of the Byte data type, instead of a String variable.
Strings are assumed to contain characters, and binary data may not be properly read in
external procedures if passed as a String variable.
15
EnumWindows is an enumeration function, which means that it can list the handle of
every open window on your system. EnumWindows works by repeatedly calling the
function you pass to its first argument (lpEnumFunc). Each time EnumWindows calls
the function, EnumWindows passes it the handle of an open window.
When you call EnumWindows from your code, you pass a user-defined function to
this first argument to handle the stream of values. For example, you might write a
function to add the values to a list box, convert the hWnd values to window names, or
take whatever action you choose.
To specify that you're passing a user-defined function as an argument, you precede
the name of the function with the AddressOf keyword. Any suitable value can be
passed to the second argument. For example, to pass the function MyProc as an
argument, you might call the EnumWindows procedure as follows:
x = EnumWindows(AddressOf MyProc, 5)
The user-defined function you specify when you call the procedure is referred to as
the callback function. Callback functions (or "callbacks," as they are commonly
called) can perform any action you specify with the data supplied by the procedure.
A callback function must have a specific set of arguments, as determined by the API
from which the callback is referenced. Refer to your API documentation for
information on the necessary arguments and how to call them.
16
To use the function, you first declare the type, then call FnPtrToLong. You pass
AddressOf plus your callback function name for the second argument.
Dim mt as MyType
mt.MyPtr = FnPtrToLong(AddressOf MyCallBackFunction)
Subclassing
Subclassing is a technique that enables you to intercept Windows messages being sent
to a form or control. By intercepting these messages, you can then write your own
code to change or extend the behavior of the object. Subclassing can be complex, and
a thorough discussion of it is beyond the scope of this book. The following example
offers a brief illustration of the technique.
Important When Visual Basic is in break mode, you can't call vtable methods or AddressOf
functions. As a safety mechanism, Visual Basic simply returns 0 to the caller of an AddressOf
function without calling the function. In the case of subclassing, this means that 0 is returned to
Windows from the WindowProc. Windows requires nonzero return values from many of its
messages, so the constant 0 return may create a deadlock situation between Windows and the
Visual Basic, forcing you to end the process.
This application consists of a simple form with two command buttons. The code is
designed to intercept Windows messages being sent to the form and to print the
values of those messages in the Immediate window.
The first part of the code consists of declarations for the API functions, constant
values, and variables:
17
Next, two subroutines enable the code to hook into the stream of messages. The first
procedure (Hook) calls the SetWindowLong function with the GWL_WNDPROC
index to create a subclass of the window class that was used to create the window. It
then uses the AddressOf keyword with a callback function (WindowProc) to intercept
the messages and print their values in the Immediate window. The second procedure
(Unhook) turns off subclassing by replacing the callback with the original Windows
procedure.
Public Sub Hook()
lpPrevWndProc = SetWindowLong(gHW, GWL_WNDPROC, _
AddressOf WindowProc)
End Sub
Finally, the code for the form sets the initial hWnd value, and the code for the buttons
simply calls the two subroutines:
Private Sub Form_Load()
gHW = Me.hwnd
End Sub
18
19
Either of these arguments can be passed as a null value. Passing a zero-length string
("") does not work, however, as this passes a pointer to a zero-length string. The
value of this pointer will not be zero. You instead need to pass an argument with the
true value of zero. The easiest way to do this is by using the constant value
vbNullString for the appropriate argument:
hWndExcel = FindWindow(vbNullString, "Microsoft Excel")
Another way to handle this situation is to rewrite the declare to substitute a Long data
type for the argument that you want to pass as null, and then call that argument with
the value 0&. For example:
Declare Function FindWindowWithNull Lib "user32" -
Alias "FindWindowA" (ByVal lpClassName As Long, _
ByVal lpWindowName As String) As Long
Passing Properties
Properties must be passed by value. If an argument is declared with ByVal, you can
pass the property directly. For example, you can determine the dimensions of the
screen or printer in pixels with this procedure:
Declare Function GetDeviceCaps Lib "gdi32" Alias _
"GetDeviceCaps" (ByVal hdc As Long, _
ByVal nIndex As Long) As Long
You can also pass the hDC property of a form or the Printer object to this procedure
to obtain the number of colors supported by the screen or the currently selected
printer. For example:
Private Sub Form_Click ()
Const PLANES = 14, BITS = 12
Print "Screen colors ";
Print GetDeviceCaps(hDC, PLANES)* 2 ^ _
GetDeviceCaps(hDC, BITS)
Print "Printer colors ";
Print GetDeviceCaps(Printer.hDC, PLANES) * _
2 ^ GetDeviceCaps(Printer.hDC, BITS)
End Sub
To pass a property by reference, you must use an intermediate variable. For example,
suppose you want to use the GetWindowsDirectory procedure to set the Path property
of a file list box control. This example will not work:
ReturnLength = GetWindowsDirectory(File1.Path,_
Len(File1.Path))
20
Use this technique with numeric properties if you want to pass them to DLL
procedures that accept arguments by reference.
Passing Variants
Passing an argument of type Variant is similar to passing any other argument type, as
long as the DLL procedure uses the Automation VARIANT data structure to access
the argument data. To pass Variant data to a argument that is not a Variant type, pass
the Variant data ByVal.
21
22