Win32 API Programming with Visual Basic
作者:Steven Roman
摘自《Win32 API Programming with Visual Basic》。请联系:nuts@oreilly.com或http://www.oreilly.com.
前言:
字符串很容易把人搞糊涂,但是只要关心一下细节其实还是可以做到心中有数的。主要的问题是术语“string”在Visual Basic中至少有两种不同的使用方式。
在Visual Basic什么叫做string?Visual Basic文档中是这样说的:
由一组有次序的相邻的字符组成的相对于数字来说是表达字符性质的一种数据类型。
哈!
好象微软打算说string类型的根本特征是有限个的字符长度。对Visual Basic来说,所有的字符是以2字节的Unicode形式表述的。(译者:在Visual Basic里处理字符是以Unicode格式的,不管所处的操作系统是可以以ANSI格式处理字符的Win98还是以Unicode格式的WinNT,但Visual Basic也支持ANSI格式的字符串传送,例如调用一个需要接收ANSI字符串的API函数时,Visual Basic会自动将其内部以Unicode格式表述的字符串转换为ANSI格式字符串传送。ANSI格式的字符是仅以一个字节来表述的字符。)从另一方面来说,Visual Basic使用Unicode格式在字符串中表述字符。例如,ASCII这样表述字符h:&H68,而Unicode是这样表述的:&H0068,同时在内存中则是这样保存:68 00。
本来,字符串“help”在内存中应该保存为这样:
00 68 00 65 00 6c 00 70
注意,由于字符串在内存中是颠倒保存的,所以,实际上“help“在内存是这样保存的:
68 00 65 00 6c 00 70 00
好了,我们现在知道在Visual Basic中字符串类string并非我们原先想象的那样。为了避免任何可能的误解,我们把string类型理解为Unicode格式的字符数组,而实际上它就是这样子的。这也有助于我们区别ANSI格式的字符数组。
请仔细阅读以下几句话,因为这是理解string类型的关键:
当我们写下这几句代码时:
Dim str as String
Str=”help”
本质上来说,我们其实并没有定义了一个Unicode格式的字符数组。我们其实定义了一个BSTR类型的变量。一个BSTR类型的变量就是一个指向以NULL结尾的,以四个字节为保留字开头的Unicode格式字符数组的指针。
BSTR 类型:
事实上,在Visual Basic3升级到Visual Basic4的时候, 用Dim语句定义一个string已经经历了一个根本性的变化。这种变化部份的起因是为了使string类型更好地兼容Win32操作系统。
为了对比一下(也为了显示我们现在是多么地幸运),图6-1展示了在Visual Basic3时叫做HLSTR(High-Level String)的Visual Basic string类型数据格式。
Figure 6-1. The high-level string format (HLSTR) used by VB3
这种复杂的HLSTR格式的数据其实是一个指向string descriptor的指针。String descriptor共占用4个字节。2字节描述字符串长度,另外2字节保存指向ANSI格式字符数组的指针。
在调用Win32 API的时候,这种数据类型对程序员来说简直就是个恶梦。从Visual Basic 4开始,string类型就不同了。新的数据类型叫做BSTR字符串。请参考图6-2:
Figure 6-2. A BSTR
这种数据类型事实上是以OLE 2.0规范定义的,也就是说,它是Microsoft’s ActiveX规范的一部分。
BSTR字符串有以下几个要点要强调一下:
1.一个BSTR字符串变量实际上是一个指针变量。它占用32bit即4个字节,就像其它的指针一样。而且,它指向一个Unicode格式的字符数组。但是,我们不能把字符串与BSTR字符串等同起来。我们必须用它自己确切的名字――“BSTR“。
2.一个BSTR字符串变量指向的字符串数组必须由4个字节的保留字开始(保存字符串数组的字节数而不是字符数),由2个空字符结束。
3.由于空字符在Unicode格式的字符串中的任何位置都有可能出现,所以,以空字符声明一个Unicode格式的字符串的结束并不合适。因此,在4个保留字中保存字符串的长度是至关重要的。
4.我们再强调一下,BSTR字符串指针实际指向的是Unicode格式的字符数组的首地址,而不是开头的4个字节。接下来我们就会看到,在这儿这样不厌其烦地强调BSTR字符串变量的特征是为了与马上就要解释的VC++中的string类型作个比较。
5.图示中的4个字节的length记录的是字符数组的字节数(注意,不是字符数),包括结尾的空字节。因为数组是Unicode格式的,所以实际字符是length的一半。
在这儿强调一下,一个Unicode格式的空字符其实是占用2个字节的空间,而不是1个字节。在Unicode格式的数组中测试空字符时要当心这一点。
我们一般在惯例上说BSTR字符串“help”是“一个BSTR类型的字符串”。一般公认为,一个BSTR字符串变量指向的一个字符数组中包括至少两个空字符。
就Visual Basic来说,BSTR字符串未尾的两个空字节可能没什么用处,但是对Win32来说,它们却是至关重要的。原因在于,Win32版本的Unicode String(它称为LPWSTR)定义为指向一个空字符结尾的Unicode格式的字符串的指针。
从这个原因上解释BSTR字符串为什么要以空字符结尾就合情合理了。下面让我们讨论一下C++类型的string变量。
还是刚才的那两句代码:
Dim str as String
str=”help”
str表示的是一个BSTR字符串变量的名字,而不是一个Unicode格式的字符数组。换句话说,str是一个保存地址xxxx(参见图6-2)的变量的名字。
以下是个小小的实验,它表明Visual Basic中string变量是指向字符数组的指针而不是字符数组。下面定义了一个结构,它的成员变量的类型是string。
Private Tyep utTest
astring As String
bstring As String
End Type
Dim uTest As utTest
Dim s as String
s=”testing”
uTest.astring=”testing”
uTest.astring=”testing”
Debug.Print Len(s)
Debug.Print Len(uTest)
这几句代码的执行结果是:
7
8
对string变量来说,Len函数返回的是字符串数组的字符个数。所以7个字符的字符串“testing”返回7。对结构变量uTest来说,Len函数返回的是该结构占用的内存空间。所以返回值8清楚地表明了每一个BSTR变量在内存中占用4个字节。因为BSTR是一个Win32的指针!
C类型的LPSTR和LPWSTR 字符串
Visual C++使用LPSTR和LPWSTR字符串。
LPSTR类型字符串的定义是:指向一个空字节结尾的ANSI格式的字符串数组的指针。但是,因为我们是以空字节的位置来判断LPSTR字符串的终止的,所以,在LPSTR中是不允许字符串中还有第二个空字节存在。同样,LPWSTR是一个指向空字节终止的Unicode格式的字符串的指针,它的中间也不允许有空字节存在。LPWSTR中的W指Wide,它是微软对Unicode的另一种说法。LPWSTR如图6-3所示。
Fig. 6-3.LPSTR and LPWSTR data types
可能我们也会碰到LPCSTR和LPCWSTR类型的字符串。其中的C表示Constant(常量)。这种字符串是不能被API函数修改的。除此以外,LPCSTR都与LPSTR相同。同理,LPCWSTR除不能修改外,其它的都与LPWSTR相同。
再说LPTSTR,LPTSTR一般都用在条件编译中,就象TCHAR一样。以下是一个例子代码:
#ifdef UNICODE
typedef LPWSTR LPTSTR; // 在Unicode下LPTSTR与LPWSTR是相同的
typedef LPCWSTR LPCTSTR; // 在Unicode下LPCTSTR与LPCWSTR是相同的
#else
typedef LPSTR LPTSTR; //在ANSI下LPTSTR与LPSTR是相同的
typedef LPCSTR LPCTSTR; //在ANSI下LPTCSTR与LPCSTR是相同的
#endif
这几种类型的图解如下:
Figure 6-4.The LP... STR mess.
大家只须记住,其中的C只不过是只读的意思就可以了。
有关String这个术语
为了避免任何可能的误解,以下我们仅使用术语BSTR、Unicode字符数组和ANSI字符数组,我们会尽量避开使用string这个术语。如果必须使用String,我们会把它改为Vusual Basic String(就是BSTR)或Visual C++ String(就是以上说过的LP??STR)。
然而,在Vusual Basic文档里,你会经常看到String这个字,至于它是以上所说的三种字符串中的哪一种,就要靠你自己的去判断了。
研究String的工具
如果我们想继续研究String,那么我们还需要一些工具。让我们来看看以下几个工具。
Visual Basic的StrConv函数:
StrConv函数是用来转变字符数组的格式的函数。它的语法为:
StrConv(string,conversion,LCID)
其中的String指的是一个BSTR类型的字符串,Conversion是一个常量(接下来马上就要介绍),而LCID是一个可选的标识符(在此我们忽略它)。
我们感兴趣的Conversion参数是以下两个:
VbUnicode(其实叫VbToUnicode更合适)
VbFromUnicode
这两个参数将BSTR字符数组转换为Unicode格式或ANS格式I。
但是现在我们有个麻烦。我们没有一个ANSI BSTR类型的字符串。定义中已经说过,BSTR字符数组指向的是Unicode格式的字符数组。
但我们还是可以想象一下,一个ANSI BSTR的字符串应该是怎样的。如同在图6-2中描述的一样,只须将Unicode格式的字符数组用ANSI格式的字符数组代替就是了。
现在我们说StrConv至少有两种合法的形式:
StrConv(an_ANSI_BSTR,vbFromUnicode)
StrConv(an_BSTR,vbUnicode)
具有讽刺意思的是,在第一种情况的时候,Visual Basic居然不认识自己的函数的返回值!请看下面的代码:
s = "help"
Debug.Print s
Debug.Print StrConv(s, vbFromUnicode)
其输出居然是:
help
??
原因是Visual Basic试图把ANSI BSTR字符串解释为BSTR字符串。请再看下面的代码:
s = "h" & vbNullChar & "e" & vbNullChar & "l" & vbNullChar & "p" &
vbNullChar
Debug.Print s
Debug.Print StrConv(s, vbFromUnicode)
输出结果是:
h e l p
help
在这儿我们为了让StrConv正常工作,而仿照Unicode格式填充一个字符串数组并传递给Visual Basic让它处理。而这种结果是:一个ANSI BSTR字符串于是有了一个合法的BSTR的解释。
这表明,StrConv函数并不真正理解或关心传过来的字符串究竟是BSTR还是ANSI BSTR。它只是设想无论如何你只要传递给它一个字符串指针就成了,它的任务只是盲目地去转换这个字符数组而已。我们以后会发现,很多其它的字符串函数都是这样。这就是说,它们可以接收一个BSTR或是一个ANSI BSTR字符指针,只要这个指针指向的是空字节结尾的字符数组就行了。
Len和LenB函数
Visual Basic有两个返回字符串长度的函数:Len和LenB。它们都可以接收一个BSTR或是一个ANSI BSTR,并且返回一个长整型数值。以下的代码说明了一切:
s = "help"
Debug.Print Len(s), LenB(s)
Debug.Print Len(StrConv(s, vbFromUnicode)), LenB(StrConv(s, vbFromUnicode))
输出结果是:
4 8
2 4
这表明,Len返回的是字符数,而LenB返回的是在BSTR中的字节数。
Chr,ChrB和ChrW函数
这几个函数的输入参数和输出结果都不相同。刚接触它们时,你可能会感到无所适从。对此,我的建议是:把它们的定义多读几遍。
Chr函数接收一个在0到255之间的长整型的整数,返回一个长度为1的BSTR类型字符串。此处,BSTR指向的字符是Unicode格式的。所以,虽然长度为1,但实际占用2字节。从最新的Visual Basic的文档来看,Chr和Chr$没有区别。
ChrB函数接收一个在0到255之间的长整型的整数,但它的返回值是一个长度为1字节的ANSI BSTR类型的字符串。该字符串是ANSI格式,所以占用1个字节。
ChrW函数接收一个在0到255之间的长整型的整数,返回值是一个长度为1的BSTR的字符串。该字符串是Unicode格式,所以占用2个字节。
Asc,AscB和AscW函数
这些函数就是Chr的反函数。其中,AscB接收一个ANSI BSTR类型的字符串,并返回一个Byte型的该字符串第一个字符的ASCII代码。为了证明返回值确是Byte类型的数据,请看下面的代码:
Debug.Print VarType(AscB("h")) = vbByte
输出结果是True。从字面上来看,可能你会认为AscB会接收一个BSTR类型的字符,但是,实际上它只认这个BSTR字符的第一个字节,其它的字节就被忽略掉了。
Asc函数接收一个BSTR类型的字符串(不是ANSI BSTR),并返回一个该字符串第一个字符的Unicode代码。
Null字符串和Null字符
Visual Basic允许Null值的BSTR类型字符串。请看下面代码:
Dim s As String
s = vbNullString
Debug.Print VarPtr(s) ‘有关这两个函数,接下来马上介绍。
Debug.Print StrPtr(s)
输出结果是:
1243948
0
这说明,一个Null值的BSTR字符串仅仅是一个指向内容为0的字符指针。在Win32和Visual C++中,这种字符串叫做空指针。让我们再来看看vbNullString和vbNullChar的区别。vbNullChar并不是指针,它是一个值为0的Unicode字符。
请大家不要把一个值为Null的BSTR和一个空BSTR搞混了。请看下面代码:
Dim s As String
Dim t As String
s = vbNullString
t = ""
空BSTR字符串t是一个指向非空内存地址的指针。在那个地址保存的是空BSTR字符串的终止符。而且,它前面的那4个保留字节中保存的该字符串的长度信息为0。
VarPtr和StrPtr函数
在微软的文档里并没有VarPtr和StrPtr函数的描述,但它们是非常有用的。特别是VarPtr函数。
请看,如果参数var为有效变量的话,那么,
VarPtr(var)
返回的是变量的长整型地址。如果str是一个BSTR字符串变量,那么:
StrPtr(str)
返回的是BSTR字符串指针指向的Unicode字符串的首地址。
让我们看下图:
Figure 6-5. A BSTR
如果代码如下:
Dim str As String
str = "help"
注意,变量str在内存中的地址是aaaa,str保存的内容是字符串的首地址xxxx.。
请看:
VarPtr = aaaa
StrPtr = xxxx
请再运行以下程序:
Dim lng As Long
Dim i As Integer
Dim s As String
Dim b(1 To 10) As Byte
Dim sp As Long, vp As Long
s = "help"
sp = StrPtr(s ) ’sp是字符串首地址即字符h在内存中的地址xxxx
Debug.Print "StrPtr:" & sp
vp = VarPtr(s) ‘vp是sp的保存的内容xxxx在内存中的地址aaaa
Debug.Print "VarPtr:" & vp
CopyMemory lng, ByVal vp, 4 ‘将长整型指针vp指向的内容xxxx拷贝到长整型变量lng,并作比较。
Debug.Print lng = sp
CopyMemory b(1), ByVal sp, 10 ‘将sp保存的地址的实际内容拷贝给byte型数组b(),并输出。
For i = 1 To 10
Debug.Print b(i);
Next
输出结果为:
StrPtr:1836612
VarPtr:1243988
True
104 0 101 0 108 0 112 0 0 0
在此我们清楚地看到,在内存中BSTR类型的字符串是以Unicode格式保存的。
请在这几行代码:
sp = StrPtr(s)
Debug.Print "StrPtr:" & sp
后加入以下几行代码:
Dim ct As Long
CopyMemory ct, ByVal sp - 4, 4
Debug.Print "Length field: " & ct
运行后可以得到这条输出结果:
Length field: 8
在此我们清楚地看到,这四个字节的保留字保存的是字符串的字节数,而不是字符数。
Visual Basic如何传递字符串到动态链接库
现在让我们来面对这个奇怪的问题:Visual Basic如何向外部的动态链接库传递BSTR类型的字符串。
我们已经知道,Visual Basic内部使用Unicode编码,也就是说,BSTR字符串使用Unicode格式。Windows NT了使用Unicode编码,但是,Windows 9x却不支持Unicode(除非某些特例)。让我们来看一下一个BSTR变量传递到一个外部的动态链接库(Win32 API或其它)所经过的过程。
为了兼容Windows 95,Visual Basic总是(甚至在Windows NT下)创建一个ANSI BSTR字符串,将BSTR字符串的Unicode格式的字符数组转换为ANSI格式,将转换完成的字符保存在这个ANSI BSTR字符串中。Visual Basic接下来就把这个ANSI BSTR字符串传递给一个外部的函数。它甚至在需要传送Unicode字符串的Windows NT中也这么做。
传送BSTR字符串之前的准备
在传递一个BSTR字符串到一个外部的动态链接库之前,Visual Basic在其它的内存地址中创建一个新的ANSI BSTR字符串。然后再把ANSI BSTR字符串传递给动态链接库。这种复制并转移的过程如图6-6所示。
Figure 6-6. Translating a BSTR to an ABSTR
当我们第一次介绍函数CopyMemory时,我们使用它来演示Unicode到ANSI的转化过程。现在让我们用另一种方式来重新演示这个过程。(译者注:接下来的程序调用了《Win32 API Programming with Visual Basic》所附的光盘中的动态链接库rpiAPI.dll的一个函数rpiBSTRtoByteArray,由于本人没有该光盘,所以大家无法演示,敬请原谅。在此,我们虽然不能运行该函数,但仅仅拿它来了解作者的程序思想,却未为不可。)rpiAPI.dll类库中有一个函数rpiBSTRtoByteArray,它的作用是返回一个传递到这个函数的字符串的地址值VarPtr和指向值StrPtr。该函数的定义如下:
Public Declare Function rpiBSTRtoByteArray Lib "???\rpiAPI.dll" ( _
ByRef pBSTR As String, _
ByRef bArray As Byte, _
pVarPtr As Long, _
pStrPtr As Long
) As Long
第一个参数pBSTR为一个BSTR类型的字符串,并按引用接收。因此,传送的是BSTR的地址,而不是字符串数组的首地址。(也就是说,我们传送了一个字符串的二级指针)
第二个参数是一个缓冲区,用来存放Visual Basic传递过来的ANSI BSTR字符串的首字符内存地址(注意是按引用传递,所以传递的应该是地址。)。
最后的两个参数也是缓冲区。函数在pVarPtr变量存放的是BSTR字符串指针的地址,pStrPtr存放的是BSTR字符串的首地址。
函数返回的是字符串的字符数。最后,函数会将传递给它的字符串的首字母改为字符“x”,这样,通过查看字符串的首字母我们便可以知道动态链接库接收的是哪一个字符串。
以下是演示代码:
Sub BSTRTest()
Dim i As Integer
Dim sString As String
Dim bBuf(1 To 10) As Byte
Dim pVarPtr As Long
Dim pStrPtr As Long
Dim bTarget As Byte
Dim lTarget As Long
sString = "help"
Debug.Print "VarPtr:" & VarPtr(sString) ' 输出BSTR的地址和指向的字符串的地址
Debug.Print "StrPtr:" & StrPtr(sString)
Debug.Print "Function called. Return value:" & _
rpiBSTRToByteArray(sString, bBuf(1), pVarPtr, pStrPtr)
Debug.Print "Address of temp ABSTR as DLL sees it: " & pVarPtr ‘输出实际传递到外部动态链接库的ANSI BSTR的地址和字符串地址
Debug.Print "Contents of temp ABSTR as DLL sees it: " & pStrPtr
Debug.Print "Temp character array: "; ‘输出临时建立的传送到外部动态链接库的ANSI BSTR字符串
For i = 1 To 10
Debug.Print bBuf(i);
Next
Debug.Print
VBGetTarget lTarget, pVarPtr, 4 ‘函数返回后检查ANSI BSTR字符串原先的地址并将其内容输出
Debug.Print "Contents of temp ABSTR after DLL returns: " & lTarget
Debug.Print "BSTR is now: " & sString‘函数返回后输出原BSTR字符串
End Sub
输出结果为:
VarPtr:1242736
StrPtr:2307556
Function called. Return value:4
Address of temp ABSTR as DLL sees it: 1242688
Contents of temp ABSTR as DLL sees it: 1850860
Temp character array: 104 101 108 112 0 0 0 0 0 0
Contents of temp ABSTR after DLL returns: 0
BSTR is now: Xelp
这几段代码首先输出的是Visual Basic中的BSTR字符串指针的地址和字符串首字符的地址。接下来调用动态链接库的函数。
在动态链接库返回后,我们试图输出ANSI BSTR字符串,但得到的结果为0。这说明这种字符串是临时创建的,函数返回后就已经被删除掉了。
最后,大家要知道,我这段代码是在Windows NT下运行的。这说明,这种字符串转换在支持Unicode的Windows NT下也发生了!真是莫名其妙!
返回的BSTR字符串
对BSTR字符串来说,传送到一个DLL函数并被返回是一个复杂的过程。
图6-7显示了这整个过程。在ABSTR字符串传递到DLL函数后,转换过程就反过来了。原来的BSTR变量str会指向一