在VB 中调用动态连接库_第1页
在VB 中调用动态连接库_第2页
在VB 中调用动态连接库_第3页
在VB 中调用动态连接库_第4页
在VB 中调用动态连接库_第5页
已阅读5页,还剩9页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

在 VB 中调用动态连接库作为一种简单易用的 Windows 开发环境, Visual Basic 从一推出就受到了广大编程人员的欢迎。它使 程序员不必再直接面对纷繁复杂的 Windows 消息,而可以将精力主要集中在程序功能的实现上,大大提高了编程效率。但凡事有利必有弊。VB 中高度的封装和模块化减轻了编程者的负担,同时也使开发人员失去了许多访问低层 API 函数和直接与 Windows 交互的机会。因此,相比而言,VB 应用程序的执行效率和功能比 C/C+或 Delphi 生成的程序要差。为了解决这个问题,在一个大型的 VB 开发应用中,直接调用 Windows API 函数几乎是不可避免的;同时,还有可能需 要程序员自己用 C/C+等开发一些动态连接库,用于在 VB中调用。本文主要讨论在 32 位开发环 境 Visual Basic 5.0 中直接调用Windows 95 API 函数或用户生成的 32 位动态连接库的方法 与规则。Windows 动态连接库是包含数据和函数的模块,可以被其它可执行文件(EXE、 DLL、OCX 等)调用。动态连接库包含两种函数:输出(exported)函数和内部(internal)函数。输出函数可以被其它模块调用,而内部函数则只能在动态 连接库内部使用。尽管动态连接库也能输出 数据,但实际上它的数据通常是只在内部使用的。使用动态连接库的优点是显而易见的。将应 用程序的一部分功能提取出来做成动态连接库,不但减小了主应用程序的大小,提高了程序 运行效率,还使它更加易于升级。多个应用程序共享一个动态连接库还能有效地节省系统资 源。正因为如此,在 Windows 系统中,动态连接库得到了大量的使用。一般来说,动态连接库都是以 DLL 为扩展名的文件,如 Kernel32.dll、commdlg.dll 等。但也有例外,如 16 位 Windows 的核心部件之一 GDI.exe 其实也是一个动态库。编写动态连 接库的工具很多,如VisualC+、BorlandC+、Delphi 等,具体方法可以参见相关文档。下面只以Visual C+5.0 为例,介绍一下开发应用于 VisualBasic5.0 的动态连接库时应注意的问题(本文中所有涉及 C/C+语言或编译环境的地方,都以 VC5 为例;所有涉及 VisualBasic 的地方都以 VB5 为例)。作为一种 32 位 Windows 应用程序的开发工具,VB5 生 成的 exe 文件自然也都是 32 位的,通常情况下也只能调用 32 位的动态连接库。但是,并不是所有的 32 位动态库都能被 VB 生成的 exe 文件正确地识别。一般来说,自己编写用于 VB 应用程序调用的动态连接库时,应注意以下几个方面的问题:1、生成动态库时要使用_stdcall 调用约定,而不能使用缺省的_cdecl调用约定;_stdcall 约定通常用于 32 位 API 函数的调用。2、在 VC5 中的定义文件(.def)中,必须列出输出函数的函数名,以强制 VC5 系统将输出函数的装饰名(decoratedname)改成普通函数 名;所谓装饰名是 VC 的编译器在编译过程中生成的输出函数名,它包含了用户定义的函数名、函数参数及函数所在的类等多方面的信息。由于在 VC5 中定义文件 不是必需的,因此工程不包含定义文件时 VC5 就按自己的约定将用户定义的输出函数名修改成装饰名后放到输出函数列表中,这样的输出函数在 VB 生成的应用程 序中是不能正确调用的(除非声明时使用 Alias 子句)。因此需要增加一个.def 文件,其中列出用户需要的函数名,以强制 VC5 不按装饰名进行输出。3、VC5 中的编译选项“结构成员对齐方式(structure member 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 variable 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 variable 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 类型的表达式LPVOID 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 0第二个描述将它定义为 SetWindowTextW, 尾部的“W“ 表明它是一个 Unicode 函数:WINUSERAPI BOOL WINAPI SetWindowTextW(HWND hWnd, LPCWSTR lpString);因为两个函数实际的名称都不是“SetWindowText“ ,要引用正确的函数就必 须增加一个 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.函数名是不标准的名称有时,个别的 DLL 过程的名称不是有效的标识符。例如,它可能包含了非法的字符(如连字符) ,或者名称是 VB 的关键字(如 GetObject) 。在这种情况下,可以使用Alias 关键字。例如,操作环境 DLLs 中的某些过程名以下划线开始。尽管在 VB 标识符中允许使用标识符,但是下划线不能作为标识符的第一个字符。为了使用这种过程,必须先声明一个名称合法的过程, 然后用 Alias 子句引用过程的真实名称:Declare Function lopen Lib “kernel32“ Alias “_lopen“ _(ByVal lpPathName As String, ByVal iReadWrite _As Long) As Long在上例中,lopen 是 VB 中使用的过程名称。而_lopen 则是动态连接库中可以识别的名 称。C.使用序号标识 DLL 过程除了使用名称之外,还可以使用序号来标识 DLL 过程。某些动态连接库中不包含过程的名称,在声明它们包含的过程时必须使用序号。同使用名称标识的 DLL 过程相比,如果使用序号,在最终的应用程序中消耗的内存将比较少,而且速度会快些。但是,一个具体的 API 的序号 在不同的操作系统中可能是不同的。例如 GetWindowsDirectory 在Win95 下的序号为 432,而在 WindowsNT4.0 下为 338。总而言之,如果希望应用程序能够在不同的操作系统下运行,那么最好不要使用序号来标识 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在这里,可以使用任意的合法名称作为过程的名称,VB 将用序号在 DLL 中寻找过程。为了得到要声明的过程的序号,可以使用 Dumpbin.exe 等实用工具(Dumpbin.exe是 Microsoft VisualC+提供的一个实用工具,它的使用说明可以参见 VC 的文档) 。利用Dumpbin,可以提取出.dll 文件中的各种信息,例如 DLL 中的函数列表,它们的序号以及与代码有关的其它信息。(三) 、使用值或引用传递在缺省的情况下,VB 以引用方式传递所有参数(ByRef) 。这意味着并没有传递实际的参数值,VB 只传递了数据的 32 位地址。另外有许多 DLL 过程要求参数以值方式传递(ByVal) 。这意味着它们需要实际的数据,而不是数据的内存地址。如果过程需要一个传值参数,而传递给它的参数是一个指针,那么由于得到了错误的数据,该过程将不能正确地工作。要使参数以使用值方式传递,在 Declare 语句中需要在参数声明的前面加上 ByVal关键字。例如 InvertRect 过程要求第一个参数用传值方式传递,而第二个用引用方式传递:Declare Function InvertRect Lib “user32“ Alias _“InvertRectA“ (ByVal hdc As Long, lpRect As RECT) As Long动态连接库的参数传递是一个复杂的问题,也是 VB 中调用动态连接库时最容易出现错误的地方。参数类型或传递方式的声明错误都可能导致应用程序出现 GPF(通用保护错误) ,甚至使操作系统崩溃,因此我们将在后面专门详细地讨论这个问题。(四) 、灵活的参数类型某些 DLL 过程的同一个参数能够接受多种数据类型。如果需要传递多种类型的数据,可 以将参数声明为 AsAny,从而取消类型限制。例如,下面的声明中的第三个参数(lpptAsAny) 既可以传递一个 POINT 结构的数组,也可以传递一个 RECT 结构:Declare Function MapWindowPoints Lib “user32“ Alias _“MapWindowPoints“ (ByVal hwndFrom As Long, _ByVal hwndTo As Long, lppt As Any, _ByVal cPoints As Long) As LongAsAny 子句提供了一定的灵活性,但是,由于它不进行任何的类型检查,风险也随之增 加。因此在使用 AsAny 子句时,必须仔细检查所有参数的类型。正确的函数声明是在 VB 中调用动态连接库的前提,但要想在 VB 中用对、用好动态库中的函数,仅仅有声明还是远远不够的。前面已经说过,由于 VB 不能验证应用程序传递到动态连接库中的参数值是否正确,因此就要求程序员应对参数类型有非常详细的了解,否则很容易引起应用程序发生通用保护错或导致潜在的 Bug,降低软件的可靠性。下面将参数类型分为简单数据类型、字符串、和用户自定义类型三种分别进行讨论。(1)、简单数据类型:简单数据类型是指 Numeric 数据类型(包括Integer、 Long、 Single、Double 、Currency 类型) 、Byte 数据类型和 Boolean 数据类型。它们的共同的特点是结构简单,操作系统在处理时不必进行特殊的转换。简单数据类型参数的传递比较简单。我们知道,在 VB 中传递参数的方式有两种:传值(Byval)和传址(ByRef ) ,缺省的方式是传址。所谓传值,就是对一个变量的具体值进行传递;而传址则是传递变量的地址。例如,在 VB 程序中需要将一个整型变量 m=10的值传进动态库,如果用传值方式,那么传进动态库的值就是 10,而在传址方式下,传入的则是变量 m 的地址,相当于 C/C+ 中&m 的值。需要注意的是,以传值方式传进动态连接库的变量,其值在动态库中是不能被改变的;如果需要在动态连接库中修改传入参数的值,则必须使用传址方式。一般来说,在 VB 和动态连接库之间传递单个的简单数据类型,只要注意了以上几个方面就可以了。当需要将一个简单数据类型的整个数组传进动态库时,必须将相应参数声明为传址方式,然后把数组的第一个元素作为参数传入,这样在动态连接库中就得到了数组的首地址,从而可以对整个数组进行访问。例如,声明了一个名为 ReadArray 的 DLL 过程,要求传入一个整型数组 aArray:Declare Function ReadArray Lib “mydll.dll“ _(aArray As Integer) As Integer在调用时可以采用如下方式:Dim ret,I(5) as Integer ret = ReadArray(I(0) 将整个数组传入动态连接库请保留地址(2)、字符串参数的传递:与简单数据类型相比,字符串类型(String、String*n)的参数传递要复杂得多,这主要是 Windows 95 API 和 VB 使用的字符串类型不同的缘故。VB 使用被称为 BSTR 的 String 数据类型,它是由自动化(以前被称为 OLE Automation)定义的数据类型。一个 BSTR 由头部和字符串组成,头部包含了字符串的长度信息,字符串中可以包含嵌入的 null 值。大部分的 BSTR 是 Unicode 的,即每个字符需要两个字节。BSTR 通常以两字节的两个 null 字符结束。下图表示 了一个 BSTR 类型的字符串。(前缀)aTest0头部 BSTR 指向数据的第一个字节另一方面,大部分的 DLL 过程(包括 Windows 95 API 中的所有过程)使用LPSTR 类型字符串,这是指向标准的以 null 结束的 C 语言字符串的指针,它也被称为ASCIIZ 字符串。LPSTR 没有前缀。下图显示了一个指向 ASCIIZ 字符串的 LPSTR。aTest0LPSTR 指向一个以 null 结尾的字符串数据的第一个字节如果 DLL 过程需要一个 LPSTR(指向以 null 结束的字符串的指针)作为参数,可以在 VB 中将一个字符串以传值的方式传递给它。因为指向 BSTR 的指针实际指向以null 值结束的字符串的第一个数据字节,所以对于 DLL 过程来说,它就是一个 LPSTR。这样传入动态连接库的字符串,DLL 过程也可以对它进行修改,尽管它是以传值方式传入的。只有当 DLL 过程需要一个指向 LPSTR 的指针时,才以传址的方式传入字符串,这时DLL 过程得到的是一个指向字符串指针的指针(相当于 C/C+中的 char*) ,而不是通常所用的字符串的首地址(相当于 C/C+中的 char*) 。当需要把一个字符串数组整个传入动态连接库时,情况就变得复杂多了,用传递简单数据类型数组的方式来传递字符串数组是行不通的。当我们以传值的方式将一个字符串数组的第一个元素传进动态连接库时,DLL 过程得到的实际上是该元素压入堆栈段后的地址,而不是数据段中整个数组的首地址。也就是说,这时 DLL 过程只能得到数组的第一个元素,而无法访问整个数组。而以传址方式传入第一个元素时,DLL 过程只能得到指向该元素在堆栈段中地址的指针,同样无法访问整个数组。这不能不说是 VB 的一个不足。因此,在程序设计中,如果确实需要将整个字符串数组传入动态库,就必须采取其它方法。我们知道,在 VB 中,有一种 Byte 数据类型。每个 Byte 型变量占一个字节,不含符号位,因此所能表示的范围为 0 到 255。这种数据类型是专门用于存放二进制数据的。为了将整个字符串数组传进动态库,可以用字节数组来保存字符串。由于 Byte 是一种简单数据类型,因此字节数组的传递是非常简单的。首先,需要把一个字符串正确地转变成一个字节数组。这要涉及一 些字符集的知识。Windows 95 和 VB 使用不同的字符集,Windows 95 API 使用的是 ANSI 或 DBCS 字符集,而 VB 使用的则是 Unicode 字符集。所谓 ANSI 字符集,是指每个字符都用一个字节表示, 因此最多只能有 28=256 个不同的字符,这对于英语来说已经足够了,但不能完全支持其它语 言。DBCS 字符集支持很多不同的东亚语言,如汉语、日语和朝鲜语,它使用数字 0-255 表示 ASCII 字符,其它大于 255或小于 0 的数字表明该字符属于非拉丁字符集;在 DBCS 中,ASCII 字符的长度是一个字节,而汉语、日语和其它东亚字符的长度是 2 个字节。而 Unicode 字符集则完全用两个字节表示一个字符,因此最多可以表示 216=65536 个不同字符。也就是说,ANSI 字符集中所有的字符都只占一个字节,DBCS 字符集中 ASCII 字符占一个字节,汉字占两个字节,Unicode 字符集中每个字符都占两个字节。由于 VB 与 WindowsAPI 使用的字符集不同,因此在进行字符串到字节数组的转换时,当用 Asc 函数取得一个字符的字节码后,需要判断它是否是一个 ASCII 字符;如果是 ASCII 字符,则在转换后的字节数组中就只占一个字节,否则要占两个字节。下面给出了转换函数:GetChar Byte 得到一个字符的高字节或低字节,它的第一个参数是一个字符的 ASCII 码,第二个参数是标志取高字节还是低字节;StrToByte 按DBCS 或 ANSI 格式将一个字符串转换成一个字节数组,第一个参数是待转换的字符串,第二个参数是转换后的一个定长字节数组,若该数组长度不足以存放整个字符串,则截去超长的部分;ChangeStrAryToByte 利用前两个函数将字符串数组转换成字节数组,第一个参数是定长的字符串数组,其中每个元素都是一个字符串(各个元素包含的字符数可以不同) ,第二个参数是一个变长的字节数组, 保存转换后的结果。Function GetCharByte(ByVal OneChar As Integer, ByVal IsHighByte As Boolean) As Byte 该函数获得一个字符的高字节或低字节If IsHighByte ThenIf OneChar = 0 ThenGetCharByte = CByte(OneChar 256)右移 8 位,得到高字节ElseGetCharByte = CByte(OneCharAnd &H7FFF) 256) Or &H80End IfExit FunctionElseGetCharByte = CByte(OneChar And &HFF)屏蔽掉高字节,得到低字节Exit FunctionEnd IfEnd FunctionSub StrToByte(StrToChange As String, ByteArray() As Byte)该函数将一个字符串转换成字节数组Dim LowBound, UpBound As IntegerDim i, count, length As IntegerDim 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 255) Or (OneChar 0) Then该字符是非 ASCII 字符ByteArray(i) = GetCharByte(OneChar, True) 得到高字节i = i + 1If i = UpBound Then ByteArray(i)= GetCharByte(OneChar, False)得到低字节Else该字符是 ASCII 字符ByteArray(i) = OneCharEnd IfElseExit ForEnd IfNextEnd SubSub ChangeStrAryToByte(StrAry()As String, ByteAry() As Byte)将字符串数组转换成字节数组Dim LowBound, UpBound As IntegerDim i, count, StartPos, MaxLen As IntegerDim TmpByte() As ByteLowBound = LBound(StrAry)UpBound = UBound(StrAry)count = 0ReDim ByteAry(0)For i = LowBound To UpBoundMaxLen = LenB(StrAry(i)ReDim TmpByte(MaxLen + 1)ReDim Preserve ByteAry(count + MaxLen + 1)Call StrToByte(StrAry(i), TmpByte) 转换一个字符串StartPos = countDoByteAry(count) = TmpByte(count - StartPos)count = count + 1If ByteAry(count - 1) = 0 Then Exit DoLoop 将每一个字符串对应的字节数组按顺序填入结果数组中ReDim Preserve ByteAry(count - 1)Next iEnd Sub下面看一个转换的例子:DimResultAry()asByteDimSomeStr(2)asStringSomeStr(0)=“测试 1“SomeStr(1)=“测试 222“SomeStr(2)=“测试 33“CallChangeStrAryToByte(SomeStr,ResultAry)转换字符串数组当转换完成以后,查看字节数组 ResultAry,其中包含了 21 个元素,依次是:178,226,202,212,49,0,178,226,202,212,50,50,50,0,178,226,202,212,51,51,0。其中,178, 226是“测“的字节码,202 , 112是“试“ 的字节码,49,50,51 分别为字符 1、2、3 的 ASCII 码。可见,经过转换后,字符串数组中的各个元素按顺序放在了字节数组中,相互间以终止符 0 分隔。这样,字符串数组就全部转换成了字节数组,然后只要将字节数组的第一个元素以传址的方式传入动态连接库,DLL 过程就可以正确地访问数组中的所有字符串了。但是,使用这种方法,当 DLL 过程处理结束返回 VB 时,VB 得到的仍然是字节数组。如果需要在 VB 中再次得到该字节数组表示的字符串,还要把整个字节数组重新以 0 为分割符分成多个子数组(每个子数组都对应原来字符串数组中的一个元素) ,然后使用 VB 函数StrConv 将每个子数组转换成字符串(转换时第二个参数选 vbUnicode) ,就可以显示或进行其它操作了。例如,其中一个子数组的名字是 SubAry,则函数 StrConv(SubAry,vbUnicode)就返回了它所对应的字符串。总之,VB 应用程序和动态库间字符串参数的传递是一个比较复杂的过程,使用时要非常谨慎。同时应尽可能避免传递字符串数组类型的参数,因为这很容易引起下标越界、堆栈溢出等严重错误。(3)、用户自定义类型(User-defined Type)参数的传递用户自定义类型在 VB 中是一种重要的数据类型,它为编程者提供了很大的灵活性,使开发人员可以根据需要构造自己的数据结构。它相当于 C/C+中的结构类型(structure)。在 VB 中,允许程序员以传址的方式将自定义数据类型参数传入动态库,DLL 过程也可以将修改后的参数返回 VB 程序。但是,在 VB 中仍然不支持以传值的方式传递用户自定义类型参数。传递用户自定义类型参数时,必须确保 VB 中的数据类型的成员与动态库中的结构成员是一一对应的,所占空间也必须严格一致。这里所说的一一对应,不仅是指 VB 中的所有结构成员在动态库的结构中都必须有对应的元素,而且它们在数据结构中定义的顺序也必须严格一致,这是 VB 中使用的“数据结构成员对齐方式“决定 的。在 VB 中,数据结构使用双字对齐方式(4-byte alignment),因此,在用户自己生成用于 VB 调用的动态连接库时,也必须把编译选项“structure member alignm

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论