1、完整版VB调用动态链接库DLLVB调用动态链接库(DLL) 作为一种简单易用的Windows开发环境,Visual Basic从一推出就受到了广大编程人员的欢迎。它使 程序员不必再直接面对纷繁复杂的Windows消息,而可以将精力主要集中在程序功能的实现上,大大提高了编程效率。但凡事有利必有弊。 VB中高度的封装和模块化减轻了编程者的负担,同时也使开发人员失去了许多访问低层API函数和直接与Windows交互的机会.因此,相比而言,VB应用程序的执行效率和功能比C/C+或Delphi生成的程序要差。为了解决这个问题,在一个大型的VB开发应用中,直接调用Windows API函数几乎是不可避免的
2、;同时,还有可能需 要程序员自己用C/C+等开发一些动态连接库,用于在VB中调用。本文主要讨论在32位开发环 境Visual Basic 5。0中直接调用Windows 95 API函数或用户生成的32位动态连接库的方法与规则。Windows动态连接库是包含数据和函数的模块,可以被其它可执行文件(EXE、DLL、OCX 等)调用。动态连接库包含两种函数:输出(exported)函数和内部(internal)函数.输出函数可以被其它模块调用,而内部函数则只能在动态连接库内部使用。尽管动态连接库也能输出 数据,但实际上它的数据通常是只在内部使用的。使用动态连接库的优点是显而易见的.将应 用程序的一
3、部分功能提取出来做成动态连接库,不但减小了主应用程序的大小,提高了程序 运行效率,还使它更加易于升级.多个应用程序共享一个动态连接库还能有效地节省系统资 源。正因为如此,在Windows系统中,动态连接库得到了大量的使用。一般来说,动态连接库都是以DLL为扩展名的文件,如Kernel32.dll、commdlg.dll等.但也有例外,如16位Windows的核心部件之一GDI。exe其实也是一个动态库。编写动态连接库的工具很多,如VisualC+、BorlandC+、Delphi等,具体方法可以参见相关文档。下面只以Visual C+5。0为例,介绍一下开发应用于VisualBasic5.0的
4、动态连接库时应注意的问题(本文中所有涉及C/C+语言或编译环境的地方,都以VC5为例;所有涉及VisualBasic的地方都以VB5 为例)。作为一种32位Windows应用程序的开发工具,VB5生成的exe文件自然也都是32位的,通常情况下也只能调用32位的动态连接库.但是,并不是所有的32位动态库都能被VB生成的exe 文件正确地识别。一般来说,自己编写用于VB应用程序调用的动态连接库时,应注意以下几个方面的问题:1、生成动态库时要使用_stdcall调用约定,而不能使用缺省的_cdecl调用约定;_stdcall 约定通常用于32位API函数的调用。2、在VC5中的定义文件(。def)中
5、,必须列出输出函数的函数名,以强制VC5系统将输出函数的装饰名(decoratedname)改成普通函数名;所谓装饰名是VC的编译器在编译过程中生成的输出函数名,它包含了用户定义的函数名、函数参数及函数所在的类等多方面的信息。由于在VC5中定义文件不是必需的,因此工程不包含定义文件时VC5就按自己的约定将用户定义的输出函数名修改成装饰名后放到输出函数列表中,这样的输出函数在VB生成的应用程序中是不能正确调用的(除非声明时使用Alias子句)。因此需要增加一个.def文件,其中列出用户需要的函数名,以强制VC5不按装饰名进行输出。3、VC5中的编译选项”结构成员对齐方式(structure me
6、mber alignment)” 应设成4字节,其原因将在后文详细介绍。4、由于在C中整型变量是4个字节,而VB中的整型变量依然只有2个字节,因此在C中声 明的整型(int)变量在VB中调用时要声明为长整型(long),而C中的短整型(short)在VB中则 要声明成整型(integer);下表针对最常用的C语言数据类型列出了与之等价的Visual Basic 类型(用于32位版本的Windows). C语言数据类型在VisualBasic中声明为调用时使用的表达式 ATOM ByVal variable As Integer 结果为Integer 类型的表达式 BOOL ByVal vari
7、able As Long 结果为 Long 类型的表达式 BYTE ByVal variable As Byte 结果为 Byte 类型的表达式 CHAR ByVal variable As Byte 结果为 Byte 类型的表达式 COLORREF ByVal variable As Long 结果为 Long 类型的表达式 DWORD ByVal variable As Long 结果为 Long 类型的表达式 HWND, HDC, HMENU ByVal variable As Long 结果为 Long 类型的表达式等Windows 句柄 INT, UINT ByVal variabl
8、e As Long 结果为 Long 类型的表达式 LONG ByVal variable As Long 结果为 Long 类型的表达式 LPARAM ByVal variable As Long 结果为 Long 类型的表达式 LPDWORD variable As Long 结果为 Long 类型的表达式 LPINT, LPUINT variable As Long 结果为 Long 类型的表达式 LPRECT variable As type 自定义类型的任意变量 LPSTR, LPCSTR ByVal variable As String 结果为 String 类型的表达式 LPVO
9、ID variable As Any 任何变量(在传递字符串的时候使用ByVal) LPWORD variable As Integer 结果为Integer 类型的表达式 LRESULT ByVal variable As Long 结果为 Long 类型的表达式 NULL As Any 或 ByVal Nothing 或 ByVal variable As Long ByVal 0amp; 或 VBNullString SHORT ByVal variable As Integer 结果为Integer 类型的表达式 VOID Sub procedure 不可用 WORD ByVal va
10、riable As Integer 结果为Integer 类型的表达式 WPARAM ByVal variable As Long 结果为 Long 类型的表达式 5、VB中进行32位动态库的声明时,函数名是大小写敏感的.在获得了需要的动态连接 库之后,就可以在VB中进行调用了。但是,由于VB不能验证应用程序传递到动态连接库中的参 数值是否正确,因此VB程序中大量的API调用可能会降低整个应用程序的稳定性,也会增加以 后维护的难度。所以,决定在VB程序中直接调用API函数时要慎重,但适当的使用API调用确实 能够有效地提高VB程序的性能。这之间的平衡需要编程人员根据实际情况来掌握。下面就具体介
11、绍一下在VB中调用API函数时需要做的工作.要声明一个DLL过程,首先需要在代码窗口的通用(General)”部分增加一个Declare语句。如果该过程返回一个值,应将其声明为Function: Declare Function publicname Lib libname Alias alias” (ByVal variable As type ,ByVal variable As type.) As Type 如果过程没有返回值,可将其声明为Sub: Declare Sub publicname Lib libname Alias ”alias” (ByVal variable As ty
12、pe ,ByVal variable As type。) 缺省情况下,在标准模块中声明的DLL过程,可以在应用程序的任何地方调用它。在其它类型的模块中定义的DLL过程则是模块私有的,必须在它们前面声明Private关键字,以示区分。下面分别介绍声明语句的各个组成部分。 (一)、指定动态库:Declare语句中的Lib子句用来告诉Visual Basic如何找到包含过程的。dll文件。 如果引用的过程属于Windows核心库(User32、Kernel32或GDI32),则可以不包含文件扩展名,如: Declare Function GetTickCount Lib kernel32 Alias
13、 ”GetTickCount” () As Long 对于其它动态连接库,可以在Lib子句指定文件的路径: Declare Function lzCopy Lib c:windowslzexpand.dll” _ (ByVal S As Integer, ByVal D As Integer) As Long 如果未指定libname的路径,Visual Basic将按照下列顺序查找该文件:.exe文件所在的目录当前目录Windows系统目录Windows目录Path环境变量中的目录下表中列出了常用的操作系统环境库文件。动态链接库描述Advapi32。dll高级API服务,支持大量的API(其
14、中包括许多安全与注册方面的调用) Comdlg32.dll通用对话框API库Gdi32.dll图形设备接口API库Kernel32.dllWindows32位核心的API支持Lz32.dll32位压缩例程Mpr.dll多接口路由器库Netapi32.dll32位网络API库Shell32。dll32位ShellAPI库User32。dll用户接口例程库Version。dll版本库Winmm.dllWindows多媒体库Winspool。drv后台打印接口,包含后台打印API调用。对于Windows的系统API函数,可以利用VB提供的工具API Viewer查找某一函数及其相 关数据结构和常数的
15、声明,并复制到自己的程序中。(二)、使用别名:Declare语句中的Alias子句是一个可选的部分,用户可以通过它所标识的别名对动态 库中的函数进行引用。例如,在下面的语句中,声明了一个在VB中名为MyFunction的函数,而它在动态库Mydll.dll中最初的名字是MyFunctionX。 Private Declare Function MyFunction Lib ”Mydll。dll _ Alias MyFunctionX ( ) As Long 需要注意的是,Alias子句中的函数名是大小写敏感的,也就是说,必须与函数在生成时的声明(如在C源文件中的声明)一致。这是因为32位动态库
16、与16位动态库不同,其中的函数名是区分大小写的。同样道理,如果没有使用Alias子句,那么在Function(或Sub)后的函数名也是区分大小写的。通常在以下几种情况时需要使用Alias子句:A。处理使用字符串的系统Windows API过程如果调用的系统Windows API过程要使用字符串,那么声明语句中必须增加一个Alias 子句,以指定正确的字符集。包含字符串的系统Windows API函数实际有两种格式:ANSI和Unicode( 关于ANSI和Unicode两种字符集的区别将在后面详细阐述)。因此,在Windows头文件中,每 个包含字符串的函数都同时有ANSI版本和Unicode
17、版本。例如,下面是SetWindowText函数 的两种C语言描述。可以看到,第一个描述将函数定义为SetWindowTextA,尾部的A” 表明它是一个ANSI函数: WINUSERAPI BOOL WINAPI SetWindowTextA(HWND hWnd, LPCSTR lpString); 第二个描述将它定义为 SetWindowTextW, 尾部的W 表明它是一个 Unicode 函数: WINUSERAPI BOOL WINAPI SetWindowTextW(HWND hWnd, LPCWSTR lpString); 因为两个函数实际的名称都不是”SetWindowText,
18、要引用正确的函数就必 须增加一个Alias子句:Private Declare Function SetWindowText Lib ”user32 _Alias ”SetWindowTextA (ByVal hwnd As Long, ByVal _lpString As String) As Long 应当注意,对于VB中使用的系统WindowsAPI函数,应该指定函数的ANSI版本,因为只 有WindowsNT才支持Unicode版本,而Windows95不支持这个版本.仅当应用程序只运行 在WindowsNT平台上的时候才可以使用Unicode版本。B。函数名是不标准的名称有时,个别的
19、DLL过程的名称不是有效的标识符.例如,它可能包含了非法的字符(如连 字符),或者名称是VB的关键字(如GetObject).在这种情况下,可以使用Alias关键字。例 如,操作环境DLLs中的某些过程名以下划线开始。尽管在VB标识符中允许使用标识符,但是 下划线不能作为标识符的第一个字符。为了使用这种过程,必须先声明一个名称合法的过程, 然后用Alias子句引用过程的真实名称:Declare Function lopen Lib kernel32 Alias ”_lopen _(ByVal lpPathName As String, ByVal iReadWrite _As Long) As
20、 Long 在上例中,lopen是VB中使用的过程名称。而_lopen则是动态连接库中可以识别的名 称。C。使用序号标识DLL过程除了使用名称之外,还可以使用序号来标识DLL过程。某些动态连接库中不包含过程的名称,在声明它们包含的过程时必须使用序号.同使用名称标识的DLL过程相比,如果使用序号,在最终的应用程序中消耗的内存将比较少,而且速度会快些。但是,一个具体的API的序号 在不同的操作系统中可能是不同的。例如GetWindowsDirectory在Win95下的序号为432,而在WindowsNT4。0下为338。总而言之,如果希望应用程序能够在不同的操作系统下运行,那么最好不要使用序号来
21、标识API过程.如果过程不属于API,或者应用程序使用的范围很有 限,那么使用序号还是有好处的。要使用序号来声明DLL过程,Alias子句中的字符串需要包含过程的序号,并在序号的 前面加一个数字标记字符()。例如,Windowskernel中的GetWindowsDirectory函数的序 号为432;可以用下面的语句来声明该DLL过程:Declare Function GetWindowsDirectory Lib ”kernel32” _Alias ”#432” (ByVal lpBuffer As String, _ByVal nSize As Long) As Long 在这里,可以使
22、用任意的合法名称作为过程的名称,VB将用序号在DLL中寻找过程.为了得到要声明的过程的序号,可以使用Dumpbin.exe等实用工具(Dumpbin。exe是Microsoft VisualC+提供的一个实用工具,它的使用说明可以参见VC的文档).利用Dumpbin,可以提取出.dll文件中的各种信息,例如DLL中的函数列表,它们的序号以及与代码有关的其它信息。 (三)、使用值或引用传递在缺省的情况下,VB以引用方式传递所有参数(ByRef)。这意味着并没有传递实际的参 数值,VB只传递了数据的32位地址。另外有许多DLL过程要求参数以值方式传递(ByVal).这意味着它们需要实际的数据,而不
23、是数据的内存地址。 如果过程需要一个传值参数,而传递给它的参数是一个指针,那么由于得到了错误的数据,该过程将不能正确地工作。要使参数以使用值方式传递,在Declare语句中需要在参数声明的前面加上ByVal关键字。例如InvertRect过程要求第一个参数用传值方式传递,而第二个用引用方式传递:Declare Function InvertRect Lib user32 Alias _InvertRectA (ByVal hdc As Long, lpRect As RECT) As Long 动态连接库的参数传递是一个复杂的问题,也是VB中调用动态连接库时最容易出现错误的地方。参数类型或传递
24、方式的声明错误都可能导致应用程序出现GPF(通用保护错误),甚至使操作系统崩溃,因此我们将在后面专门详细地讨论这个问题.(四)、灵活的参数类型某些DLL过程的同一个参数能够接受多种数据类型。如果需要传递多种类型的数据,可 以将参数声明为AsAny,从而取消类型限制。例如,下面的声明中的第三个参数(lpptAsAny) 既可以传递一个POINT结构的数组,也可以传递一个RECT结构:Declare Function MapWindowPoints Lib user32” Alias _MapWindowPoints” (ByVal hwndFrom As Long, _ByVal hwndTo
25、As Long, lppt As Any, _ByVal cPoints As Long) As LongAsAny子句提供了一定的灵活性,但是,由于它不进行任何的类型检查,风险也随之增 加。因此在使用AsAny子句时,必须仔细检查所有参数的类型。正确的函数声明是在VB中调用动态连接库的前提,但要想在VB中用对、用好动态库中的 函数,仅仅有声明还是远远不够的。前面已经说过,由于VB不能验证应用程序传递到动态连接 库中的参数值是否正确,因此就要求程序员应对参数类型有非常详细的了解,否则很容易引 起应用程序发生通用保护错或导致潜在的Bug,降低软件的可靠性。下面将参数类型分为简单数据类型、字符串、
26、和用户自定义类型三种分别进行讨论。(1)、简单数据类型:简单数据类型是指Numeric数据类型(包括Integer、Long、Single、Double、Currency类型)、Byte数据类型和Boolean数据类型。它们的共同的特点是结构简单,操作系统在处理时不必进行特殊的转换。简单数据类型参数的传递比较简单。我们知道,在VB中传递参数的方式有两种:传值(Byval) 和传址(ByRef),缺省的方式是传址。所谓传值,就是对一个变量的具体值进行传递;而传址则 是传递变量的地址.例如,在VB程序中需要将一个整型变量m=10的值传进动态库,如果用传值 方式,那么传进动态库的值就是10,而在传址
27、方式下,传入的则是变量m的地址,相当于C/C+ 中&;m的值。需要注意的是,以传值方式传进动态连接库的变量,其值在动态库中是不能 被改变的;如果需要在动态连接库中修改传入参数的值,则必须使用传址方式。一般来说,在VB 和动态连接库之间传递单个的简单数据类型,只要注意了以上几个方面就可以了。当需要将 一个简单数据类型的整个数组传进动态库时,必须将相应参数声明为传址方式,然后把数组 的第一个元素作为参数传入,这样在动态连接库中就得到了数组的首地址,从而可以对整个 数组进行访问。例如,声明了一个名为ReadArray的DLL过程,要求传入一个整型数组aArray:Declare Functio
28、n ReadArray Lib mydll。dll _(aArray As Integer) As Integer 在调用时可以采用如下方式:Dim ret,I(5) as Integer ret = ReadArray(I(0) 注释: 将整个数组传入动态连接库(2)、字符串参数的传递:与简单数据类型相比,字符串类型(String、Stringn)的参数传递要复杂得多,这主要是Windows 98 API和VB使用的字符串类型不同的缘故。VB使用被称为BSTR的String数据类型,它是由自动化(以前被称为OLE Automation)定义的数据类型。一个BSTR由头部和字符串组成,头部包含
29、了字符串的长度信息,字符串中可以包含嵌入的null值.大部分的BSTR是 Unicode的,即每个字符需要两个字节。BSTR通常以两字节的两个null字符结束。下图表示 了一个BSTR类型的字符串。 Function GetCharByte(ByVal OneChar As Integer, ByVal IsHighByte As Boolean) As Byte 注释: 该函数获得一个字符的高字节或低字节 If IsHighByte ThenIf OneChar gt;= 0 ThenGetCharByte = CByte(OneChar 256) 注释:右移8位,得到高字节 ElseGet
30、CharByte = CByte((OneCharAnd &H7FFF) 256) Or amp;H80End IfExit FunctionElseGetCharByte = CByte(OneChar And amp;HFF) 注释:屏蔽掉高字节,得到低字节Exit FunctionEnd IfEnd FunctionSub StrToByte(StrToChange As String, ByteArray() As Byte)注释:该函数将一个字符串转换成字节数组Dim LowBound, UpBound As IntegerDim i, count, length As In
31、tegerDim OneChar As Integercount = 0length = Len(StrToChange)LowBound = LBound(ByteArray)UpBound = UBound(ByteArray)For i = LowBound To UpBoundByteArray(i) = 0 注释:初始化字节数组NextFor i = LowBound To UpBoundcount = count + 1If count lt;= length ThenOneChar = Asc(Mid(StrToChange, count, 1))If (OneChar gt; 255) Or (OneChar <; 0) Then注释:该字符是非ASCII字符ByteArray(
copyright@ 2008-2022 冰豆网网站版权所有
经营许可证编号:鄂ICP备2022015515号-1