A dynamic-link library (DLL) is a library of procedures that applications can link to and use at run time rather than link to statically at compile time. This means that DLLs can be updated without updating the application, and many applications can share a single DLL. Microsoft Windows is itself composed of several DLLs that contain the procedures all applications use to perform their activities, such as displaying windows and graphics, managing memory, and so on. These procedures are sometimes referred to as the Windows application programming interface (API).
Your Microsoft Access applications can call the procedures in Windows DLLs to perform special actions that you can’t perform directly in Microsoft Access. You can also call procedures in other DLLs you have in your system.
Because DLL procedures reside in files that are external to your Microsoft Access application, you have to give Microsoft Access some information so that it can find and run the DLL procedures you want to use. You provide this information with the Declare statement. Once you have declared a DLL procedure, you can use it in your code like any other procedure. However, you have to be especially careful about the arguments that you pass to DLL procedures, or the procedure may not be able to interpret the data and may not function as you expect.
There are two basic steps in using a DLL procedure:
You declare a DLL procedure only once. After declaring the procedure, you can call it any number of times from anywhere in your application.
If a database created in Microsoft Access version 2.x or earlier called procedures in the Windows API, you need to make some changes in your code when you convert the database to Microsoft Access 97. These earlier versions of Microsoft Access were 16-bit applications and ran on 16-bit versions of Windows. Microsoft Access 97 is a 32-bit application and runs only on 32-bit versions of Windows.
If your existing Access Basic code made calls to a 16-bit Windows API, you need to modify these calls when you convert your database to Microsoft Access 97. For example, you need to change the names of the DLLs and the data types of some parameters. Also, note that 32-bit API procedure names are case-sensitive, so you need to make sure that the calls you make exactly match the procedure names in the Windows DLLs.
There is a tool called the Windows API Viewer included with both Visual Basic and Microsoft Office 97, Developer Edition. This tool includes Visual Basic syntax for all 32-bit declarations, data types, and constants. You can copy and paste these declarations, data types, and constants into your Microsoft Access applications.
See Also For tips on converting code that calls a DLL, search the Help index for “converting code.”
If your Visual Basic code makes calls into a custom 16-bit DLL, either the developer of the DLL must provide a 32-bit compatibility layer for the DLL, or the DLL source code must be recompiled into a 32-bit version of the DLL.
To declare a DLL procedure, place a Declare statement in the Declarations section of a form, report, or standard module. If you declare a DLL procedure in a standard module, it can be called by code anywhere in your application. If you declare a DLL procedure in a form or report module, you must use the Private keyword. The DLL procedure can only be called by other code in the form or report module it resides in.
See Also For the complete syntax of the Declare statement, search the Help index for “Declare statement.”
If the procedure doesn’t return a value, declare it as a Sub procedure. For example:
Declare Sub InvertRect Lib "user32"(ByVal hDC As Long, lpRect As RECT)
If the procedure does return a value, declare it as a Function procedure. For example:
Declare Function GetSystemMetrics Lib "user32"(ByVal nIndex As Long) As Long
Note The procedure name you use in a Declare statement is case-sensitive. The library name is also case-sensitive.
Notice the Lib and ByVal keywords in the Declare statement. The Declare statement can also contain an optional Alias keyword. The use of these keywords is explained later in this section.
The declarations for DLL procedures can get fairly complex. If you plan to declare and call DLL procedures in the Windows API, it’s a good idea to have complete documentation of the API for reference. You can find detailed documentation of the Windows API in each of these Microsoft products:
The rest of this section explains the syntax of the Declare statement in detail.
The Lib libname clause in the Declare statement tells Microsoft Access where to find the dynamic-link library. For the Windows DLLs, this is either "user32"
, "gdi32"
, "kernel32"
, or one of the other system DLLs such as "WINMM"
. For other DLLs, libname is a file name that can include a path. For example:
Declare Function LzCopy Lib "C:\Win\LzExpand.dll" (ByVal S As Long, _
ByVal D As Long) As Long
Note If no path is included in the library name, Visual Basic searches the default system path, then the \Windows folder, and finally the \Windows\System subfolder.
By default, Microsoft Access passes all arguments by reference, which means that Visual Basic passes a 32-bit address where the value is stored instead of passing the actual value of the argument. If you want to clarify how data is being passed, you can use the ByRef keyword.
Many DLL procedures expect an argument to be passed by value, which means that the procedure expects the actual value of the argument instead of its memory location. If you pass an argument by reference to a procedure that expects an argument passed by value, the procedure can’t interpret the data and doesn’t operate properly.
To pass an argument by value, place the ByVal keyword in front of the argument declaration in the Declare statement. This ensures that each time you call the procedure, the argument is passed by value.
For example, the following InvertRect procedure accepts its first argument by value and its second argument by reference:
Declare Sub InvertRect Lib "user32"(ByVal hDC As Long, lpRect As RECT)
Note When you’re looking at DLL procedure documentation that uses C-language syntax, remember that C passes by value all arguments except arrays.
See Also For more information on passing arguments by value or by reference, see “Argument Data Types” in Chapter 4, “Working with Variables, Data Types, and Constants.”
Some DLL procedures can accept more than one type of data for the same argument. To pass more than one type of data, declare the argument with the As Any keyword to remove type restrictions. For example, you can declare a procedure as follows:
Declare Function FindWindow Lib "user32" Alias "FindWindowA" _
(ByVal lpClassName As Any, ByVal lpWindowName As Any) As Long
Then you can call this procedure and pass either a string or a Null value to each function argument:
Function WindowExist ()
' Assign values to constants that will be passed to FindWindow.
Const lpClassName = "SciCalc"
Const lpWindowName = "Calculator"
' This demonstrates three different ways to call FindWindow:
' 1. The Class name only.
' 2. The Caption only.
' 3. Both the Class name and the Caption.
' A Null is represented by the symbol "0&".
MsgBox "Window Handle = " & FindWindow(lpClassName, 0&)
MsgBox "Window Handle = " & FindWindow(0&, lpWindowName)
MsgBox "Window Handle = " & FindWindow(lpClassName, lpWindowName)
WindowExist = FindWindow(lpClassName, 0&)
' Any of the above FindWindow calls could be used to return the
' window handle.
End Function
Note The preceding code uses window handles to identify the window returned by the FindWindow function. For more information on handles, see “Handles” later in this chapter.
When you remove type restrictions, Microsoft Access assumes the argument is passed by reference. Use the ByVal keyword in the actual call to the procedure to pass arguments by value. When passing strings, you must use the ByVal keyword to convert a Visual Basic string into a null-terminated string.
See Also For more information on passing strings, see “Calling DLL Procedures with Specific Data Types” later in this chapter.
Occasionally, a DLL procedure has a name that isn’t a valid identifier in Microsoft Access. It may have an invalid character (such as a hyphen), or be the same as a Visual Basic keyword (such as GetObject). When either is the case, use the Alias keyword.
For example, some procedures in Windows DLLs begin with an underscore character. While you can use an underscore in a Visual Basic identifier, you can’t begin an identifier with an underscore. To use one of these procedures, you must declare the procedure with the Alias keyword.
Declare Function LOpen Lib "kernel32" Alias "_lopen" (ByVal _
lpPathName As String, ByVal ReadWrite As Long) As Long
In this example, LOpen
becomes the name of the procedure as it’s referred to in your Microsoft Access procedures. The name _lopen
is the name recognized in the DLL.
In addition to a name, all DLL procedures can be identified by an ordinal number that specifies the position of the procedure in the DLL. Some DLLs don’t include the names of their procedures and require you to use ordinal numbers when declaring the procedures they contain. Using an ordinal number consumes less memory in your finished application and is slightly faster than identifying a procedure in a DLL by name. You may also want to use ordinal numbers when declaring other DLL procedures. To obtain the ordinal number of the DLL procedure you want to declare, consult the documentation for the DLL.
To declare a DLL procedure by ordinal number, use the Alias keyword with a string containing the number sign character (#) and the ordinal number of the procedure. For example, if you want to call a function called GetAppSettings in a library called Utilities, and that function has the ordinal number 47, you can declare the DLL procedure as follows:
Declare Function GetAppSettings Lib "Utilities" Alias "#47" () As Long
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.
Once a procedure is declared, you can call it just as you would a Microsoft Access statement or function. For example:
Private Sub Form_Load()
Const SM_MOUSEPRESENT = 19
If GetSystemMetrics(SM_MOUSEPRESENT) Then
MsgBox "Mouse installed"
End If
End Sub
Important Microsoft Access can’t verify that you are passing correct values to a DLL procedure. If you pass incorrect values, the procedure may fail, which may cause your Microsoft Access application to shut down. This doesn’t cause permanent harm to your application, but you’ll have to reload and restart the application. Take care when using DLL procedures, and save your work often.
Microsoft Access incorporates a rich assortment of data types, including many—such as variable-length strings, Currency, and properties—that aren’t supported by the procedures in most dynamic-link libraries. Therefore, you must take care when using Visual Basic variables with DLL procedures.
The procedures in most DLLs (and all the procedures in the Windows API) expect standard C strings (sometimes called ASCIIZ strings) which end in a null character (binary zero). If a DLL procedure expects a null-terminated string as an argument, declare the argument as a string with the ByVal keyword. When used with a string argument, the ByVal keyword tells Microsoft Access to pass the string as a null-terminated string.
Strings are always passed to DLL procedures by reference. (The ByVal keyword for string arguments specifies that Microsoft Access should convert the string to a null-terminated string, not that the string should be passed by value.) A DLL procedure can therefore modify a Microsoft Access string variable it receives as an argument. However, be careful when calling a DLL procedure that modifies a string. A DLL can’t increase the length of a Microsoft Access string; if the string isn’t long enough, the DLL simply writes beyond the end of the string. This corrupts other areas of memory. You can avoid this problem by making the string argument long enough that the DLL procedure never writes past the end of it.
For example, the GetWindowsDirectory procedure returns the path for the Windows directory in its first argument.
Declare Function GetWindowsDirectory Lib "kernel32" Alias _
"GetWindowsDirectoryA" (ByVal lpBuffer As String, _
ByVal nSize As Long) As Long
One way to call this procedure is to make the returned argument a specified length by filling it with characters—in this case, null (ANSI zero) characters.
strPath = String(255, 0)
strWorked = GetWindowsDirectory(strPath, Len(strPath))
Another way to call this procedure is to define the string as fixed-length.
Dim strPath As String * 255
strWorked = GetWindowsDirectory(strPath, Len(strPath))
Note The Windows DLL procedures could possibly return strings longer than 255 characters. Always consult the documentation for information on the procedure at hand.
You can use Microsoft Access strings when the DLL procedure calls for a memory buffer. Use one of the processes outlined previously in this section to ensure that the string is long enough to accept whatever data the procedure supplies.
You pass individual elements of an array in the same way you pass any variable that has the same type as the base type of the array. For example, the sndPlaySound function plays a digitized sound (.wav) file (if you have the sound hardware and drivers that can use these files).
Declare Function sndPlaySound Lib "WINMM" (ByVal lpszSoundName _
As String, ByVal uFlags As Integer) As Integer
You can use the sndPlaySound function to play a series of .wav files stored in an array.
Dim intX As Integer, intWorked As Integer
For intX = 0 To UBound(WaveFiles)
intWorked = sndPlaySound(WaveFile(intX), 0)
Next intX
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. A DLL procedure, if passed the first element of an array, has access to all the array’s elements. For example, if you made a call to a function in a C library that contained the following header:
int CalcMean(int intArray, int intElements)
/*Returns the arithmetic mean of an array of integers.*/
Then you would declare the function in your Visual Basic module:
Declare Function CalcMean Lib "Calc.dll" (intArray As Integer, _
intElements As Integer) As Integer
And you would pass the array to the library function by specifying the first element of the array as one of the function arguments, as shown in the last line of the following procedure:
Function AvgOfRecords ()
Dim intN() As Integer
Dim dbs As Database, rst As Recordset
Dim intTotal As Integer, intX As Integer
Set dbs = CurrentDb
Set rst = dbs.OpenRecordset("Order Details")
rst.MoveLast
intTotal = rst.RecordCount
' ReDim the array to the # of recs in the table.
ReDim intN(intTotal)
rst.MoveFirst
' Fill the array with values from table.
For intX = 0 To (intTotal - 1)
intN(intX) = rst!Quantity
rst.MoveNext
Next intX
' Pass the first element of the array intN to CalcMean().
MsgBox "The arithmetic mean is " & CalcMean(intN(0),intTotal)
rst.Close
End Function
Caution You can’t pass an entire string array. Each element of a string array must be passed individually, as a null-terminated string using the ByVal keyword. If a DLL procedure tries to access memory beyond the end of the first element in a string array, it may corrupt memory or cause an error. For more information on passing strings, see “Strings” earlier in this chapter.
Some DLL procedures take user-defined types as arguments. User-defined types can contain one or more elements of any data type, array, or a previously defined user-defined type.
Note User-defined types are referred to as structures in C and as records in Pascal. DLL documentation often uses the C terminology.
As with an array, you can pass the individual elements of a user-defined type the same way you pass ordinary numeric or string variables.
You can pass an entire user-defined type as a single argument if you pass it by reference. You can’t pass user-defined types by value. Microsoft Access passes the address of the first element, and the rest of the elements of a user-defined type are stored in memory following the first element. For example, the Windows GetSystemTime procedure uses the following user-defined type, which you can place in the Declarations section of any module:
Type SYSTEMTIME
Year As Integer
Month As Integer
DayOfWeek As Integer
Day As Integer
Hour As Integer
Minute As Integer
Second As Integer
Milliseconds As Integer
End Type
The GetSystemTime procedure accepts the user-defined type SYSTEMTIME and retrieves detailed time information from the system clock. To use the procedure, place the following declaration in the Declarations section of any module:
Declare Sub GetSystemTime Lib "kernel32" (lpSystemTime As SYSTEMTIME)
Now you can use the following Sub procedure to call the DLL procedure that retrieves the current time.
Sub GetTheTime()
Dim usrTime As SYSTEMTIME
' Call the procedure to load the SYSTEMTIME structure.
GetSystemTime usrTime
' Print the individual time elements to the Immediate pane of the Debug window.
Debug.Print "Year = "; usrTime.Year
Debug.Print "Month = "; usrTime.Month
Debug.Print "DayOfWeek = "; usrTime.DayOfWeek
Debug.Print "DayOfMonth = "; usrTime.Day
' Note that hour is given in Greenwich Mean Time (GMT).
Debug.Print "Hour = "; usrTime.Hour
Debug.Print "Minute = "; usrTime.Minute
Debug.Print "Second = "; usrTime.Second
Debug.Print "Milliseconds = "; usrTime.Milliseconds
End Sub
Most procedures that accept user-defined types don’t expect the user-defined types to contain string data. However, you can pass a user-defined type that includes fixed-length string elements to a DLL procedure. You can’t pass a user-defined type that includes variable-length string elements.
Note When passing a user-defined type that contains binary data to a DLL procedure, be sure to store the binary data in a variable of an array of the Byte data type, instead of in a string variable. Because strings are assumed to contain characters, binary data may not be read properly in external procedures if it’s passed as a string variable.
See Also For more information on user-defined data types, see “Creating Your Own Data Types” in Chapter 4, “Working with Variables, Data Types, and Constants.”
Some DLL procedures occasionally expect to receive a null pointer as an argument. A null pointer is a pointer to nothing; null pointers have a value of zero. If you need to pass a null pointer to a DLL procedure, declare the argument with the As Any keyword and pass the expression ByVal 0&
.
See Also For more information on the As Any keyword, see “Passing Flexible Argument Types” earlier in this chapter.
For example, the FindWindow procedure accepts two string arguments, so you may expect to declare it as follows:
Declare Function FindWindow Lib "user32" (ByVal lpClassName As String, _
ByVal lpWindowName As String) As Long
However, the FindWindow procedure also accepts a null pointer for either or both of its arguments. If you declare it as in the previous example, you can’t pass null pointers to it. Passing a zero-length string ("") doesn’t work; this passes a pointer to a null string rather than passing a null pointer.
To pass a null pointer, declare the procedure as follows:
Declare Function FindWindow Lib "user32" Alias "FindWindowA" _
(lpClassName As Any, lpWindowName As Any) As Long
Although you can still call the procedure and pass it two strings, you can also pass a null pointer as one of the arguments.
HWndExcel = FindWindow(ByVal 0&, ByVal "Microsoft Excel")
Notice the use of the ByVal keyword with both arguments. You must use the ByVal keyword when passing the null pointer to ensure that the routine receives a zero value (a null pointer) instead of a pointer to a zero value.
Also, notice the ampersand character (&) after the zero. This informs Microsoft Access that you are passing a Long integer (32-bit), ensuring that a null pointer of the right size is passed. Pointers in Microsoft Access are always far (32-bit) pointers. You can use this technique to pass a null pointer to any procedure that accepts an argument by reference.
The Windows DLL procedures make extensive use of handles—handles to Windows (hWnd), handles to device contexts (hDC), and so on. A handle is a unique long value defined by Windows and used to refer to objects such as forms and reports. When a procedure takes a handle as an argument, always declare it as a ByVal Long. DLL functions that return a handle can be declared as Long functions. Handles are identifier (ID) numbers, not pointers or numeric values. You should never perform mathematical operations on handles.
The hWnd property of forms and reports supplies a valid handle that you can pass to DLL procedures. Like any other property passed to a DLL procedure, it can be passed only by value.
To pass a property directly, you must pass it by value with the ByVal keyword. To pass a property by reference, you must use an intermediate variable. All strings are passed by reference; therefore, to pass a string property to a DLL procedure, you must first assign the property to a string variable and then pass the variable to the procedure.
You can’t pass an object variable to a DLL procedure. Object variables are actually complex data structures, and DLL procedures can’t make use of them. Likewise, you can’t pass any of the system objects (Application, Screen, or Debug) or any DAO objects (Database, Recordset, and so on) to a DLL procedure.
The procedures in DLLs are most commonly documented using C-language syntax. To call DLL procedures from Microsoft Access, you must translate them into valid Declare statements and call them correctly. The following table lists common C-language declarations and their Visual Basic equivalents.
C-language declaration | Visual Basic equivalent | Call with |
Boolean | ByVal B As Boolean | Any Integer or Variant variable |
Pointer to a string (LPSTR) | ByVal S As String | Any String or Variant variable |
Pointer to an integer (LPINT) | I As Integer | Any Integer or Variant variable |
Pointer to a long integer (LPDWORD) | L As Long | Any Long or Variant variable |
Pointer to a structure (for example, LPRECT) | S As RECT | Any variable of that user-defined type |
Integer (INT, UINT, WORD, BOOL) | ByVal I As Integer | Any Integer or Variant variable |
Handle (32-bit) | ByVal H As Long | Any Long or Variant variable |
Long (DWORD, LONG) | ByVal L As Long | Any Long or Variant variable |
Pointer to an array of integers | I As Integer | The first element of the array, such as I(0) |
Pointer to a void (void *) | V As Any | Any variable (use ByVal when passing a string) |
Void (function return value) | Sub procedure | Not applicable |
NULL | As Any | ByVal 0& |
Char | ByVal Ch As Byte | Any Byte or Variant variable |
Pointer to a char | Ch As Byte | Any Byte or Variant variable |