分享

如何使用 Unicode 版和 Ansi 版 API – 中文

 天狼_顺水推舟 2012-05-17

如何使用 Unicode 版和 Ansi 版 API

作者: 西西 (1 篇文章) 日期: 五月 12, 2010 在 4:01 下午

1、ANSI字符集和Unicode字符集

ANSI的ASCII字符集及其派生字符集(也称多字节字符集)比较旧,Unicode字符集比较新,固定以双字节表示一个字。具体见参考链接1,也可以看看这篇博文。随着32位世界和VB4的到来,我们迈进了一半是UNICODE,一半是ANSI的Windows世界。而在此之前,是ANSI一统天下。

2、WINDOWS API所用的字符集

操作字符串的API在声明时,会指定字符集。每个含有字符串的API同时有两个版本:即ANSI,Unicode。尾部带A的API是ANSI版本,带W的API是Unicode版本。例如:SetWindowTextA,是ANSI函数;而SetWindowTextW,是Unicode函数。

WINUSERAPI BOOL WINAPI SetWindowTextA(HWND hWnd, LPCSTR lpString)
WINUSERAPI BOOL WINAPI SetWindowTextW(HWND hWnd, LPCWSTR lpString)

相应的,凡涉及到字符集的通知事件都有A和W两种消息定义。比如,TVN_SELCHANGEDA和TVN_SELCHANGEDW实际上是两个不同的通知,其含义是相同的,不同之处在于通知中附带的结构,A结构中使用多字节字符串,W结构使用Unicode字符串。

3、VB所用的字符集

在VB中,所有字符串按UNICODE保存。如果给A版的API函数传字符串参数,这就要求在调用API函数之前,将字符串从UNICODE转换成ANSI,函数执行结束后,将返回的字符串从ANSI转换成UNICODE。如果给W版的函数传字符串参数,则不需要做这种转换。如下图所示:

由于以前大部分API调用使用的是ANSI字符串(现在已经不是这样了),为了减少用户调用API函数的麻烦,所以VB会自动做这种转换。也就是,只要用“Private/Public Declare ...”语法声明的函数,VB编译器认为就是API函数,那么只要是用“As String”声明的参数,VB编译器一概无条件地把字串由BSTR转换为ANSI传递。类似地,任何包含有字符串的结构直接传给API函数的话,也会经过这种双重转换。

注意:VB.Net是和VB6完全不同的东东,.Net是Unicode内核的,但是有ANSI和Unicode两种界面。

4、VB6中使用API函数

上节提到,VB6 的String传给API时会自动被转化为ANSI string,从API返回后又被自动转换为unicode String,所以用 VB6/VBA/VBS 最好用ANSI版的API。

虽然这种自动转换对A版函数是个好事,对W版函数却是个麻烦。你想啊,本来人家就想要个Unicode字符串,人程序员给传的也是Unicode字符串,可是VB6妈妈楞是中间插一手,把人程序员传过来的Unicode字符串换成ANSI字符串传了过来,全闹拧了。所以这种自动转换实际上使得我们不能将一个字符串类型的参数以UNICODE方式从VB传递给DLL。

如果有些API只有Unicode版本,又需要字符串参数,那怎么办呢?办法是:把string类型的参数转化为指针(以long声明)传进去。比如,下面两种声明:

Private Declare Function CharUpperWide Lib "user32" Alias "CharUpperW" (ByVal lpsz As Long) As Long
Private Declare Function CharUpperWide Lib "user32" Alias "CharUpperW" (ByVal s As String) As Long
用下面的代码调用

Dim s as String s = "hello"
Call CharUpperWide(StrPtr(s))
Debug.Print s

用第一种声明,结果是HELLO;而第二种却得不到预期结果。

因此,我们常可以看到,同样位置的参数,ANSI的API声明为string,而UNICODE的声明为long,像下面这样:

Private Declare Function SetWindowText Lib "user32.dll" Alias "SetWindowTextA" (ByVal hwnd As Long, ByVal lpString As String) As Long
Private Declare Function SetWindowText Lib "user32.dll" Alias "SetWindowTextW" (ByVal hwnd As Long, ByVal lpString As Long) As Long

5、VB6中模拟控件发送通知消息

两种字符集的通知都可以在任意字符集的程序中响应,但前提是控件必须要发出该类型的通知。通常情况下,控件发出通知的类型是与程序使用的字符集相同的。比如,VB6里的TreeView控件发的通知消息应该是A型的,与VB一致;但是如果Treeview是CreateWindowExW创建的,就得用W-type 消息。反之,用A-type。

另外,ahao提到,标准控件大部分都有一个消息,比如,TreeView 是 TVM_SETUNICODEFORMAT 、HeaderCtrl 是 HDM_SETUNICODEFORMAT、ListView 是LVM_SETUNICODEFORMAT。作用就是在运行期改变控件的字符集,不用重新创建控件,如果设置为TRUE,就是发送W版本的Notify消息;如果设置为FALSE;就是发送A版本的Notify消息。

6、VarPtr、StrPtr和ObjPtr函数的用法

上面提到,所有指针都一律定义为Long,但是自己要记得,调用该API函数的时候,要通过VarPtr等函数传入指针(注意,对应的实体一定不能被释放掉)。VarPtr/StrPtr/ObjPtr的执行速度非常非常快,因此调用UNICODE函数所造成的系统负担实际上小于调用相对应的ANSI函数,因为前者不需VB妈妈对字符串进行自动的UA/AU转换。

VarPtr:返回变量地址
StrPtr:返回真正的UNICODE字符串缓冲区的地址
ObjPtr:返回任何对象变量引用的地址
6.1 VarPtr

为了获取变量的地址,只须将变量名传递给VarPtr函数就行了。例如:

Dim l As Long
Debug.Print VarPtr(l)

类似地,为了获取字符串的指针,而非保存字符串的变量的指针,只须在变量名前加上ByVal即可。如:

Debug.Print VarPtr(s),VarPtr(ByVal s) '例1
在VB3之前,用这种方法来获取字符串缓冲的指针是非常普遍的。但是由于VB6的UNICODE和ANSI字符串的自动变化机制,和当一个字符串传递给VarPtr函数时,函数执行后所返回的地址是保存临时ANSI字符串的临时ANSI字符串或变量的地址。换句话说,这个地址并不是你声明的变量的真正地址。因此,对于字符串变量以及包括字符串的结构来讲,例1后半段这种用法完全失效了。

不过,现在这种方法貌似又可以用了。以前的 VarPtr 必须通过 Declare 语句声明,所以有 Unicode-Ansi 自动转换。而到了 VB6,ObjPtr/StrPtr/VarPtr 作为隐藏的内部函数直接提供,就不需要 Unicode-Ansi 自动转换。看这个帖子的0楼和3楼(谢谢Tiger_Zhao)。

该函数能与要求包含有UNICODE字符串的结构的API调用一起使用。如果将一个MyUDTVariable变量(一个自定义类型的变量)传递给一个由ByRef UDTParam As MyUDT定义的参数,就会发生ANSI/UNICODE之间的转换。但是,如果将VarPtr(MyUDTVariable)传递给由ByVal UDTParam As Long定义的参数,则不会发生这样的转换。但是有时你要小心,也许动态库期待的一块连续的内存,这时你传结构进去就未必对了,看这个例子。

6.2 StrPtr出现之前的办法:字节数组

如果有些API只有Unicode版本,你必须自己完成字符串的转换工作才能使用。在VB4中,这必须借助于Byte数组。

1 将 VB6 的字符串转化成 unicode 内码的字节数组

2 把字节数组传给 unnicode 界面的API

3 把API处理结果又自行转化为 String。

例如:

Declare Sub MyUnicodeCall Lib "MyUnicodeDll.dll" (pStr as Byte)

Sub MakeCall (MyStr as String)
Dim bTmp() as Byte
bTmp=MyStr & vbNullChar '-- 这里为何要加个vbNullChar?
MyUnicodeCall bTmp(0)
MyStr=bTmp
MyStr=left(MyStr, Len(MyStr)-1)
End Sub

6.3. StrPtr:返回真正的UNICODE字符串缓冲区的地址
如果使用StrPtr,上面的代码精简为:

Declare Sub MyUnicodeCall Lib "MyUnicodeDll.dll" (ByVal pStr as Long)

Sub MakeCall (MyStr as String)
MyUnicodeCall StrPtr(MyStr)
End Sub

也许细心的朋友会问,为何用Byte数组的时候要在最后加上vbNullChar,而用StrPtr的时候则不加。因为C字符串是以Null结尾的,如果要给一个DLL函数传字符串参数,就要考虑是否要加vbNullChar以防止DLL有可能会越界(越过传给它的字符串缓冲区的界)操作内存。不过的话,直接传String参数作为 char* 调用、或者用StrPtr()作为 wchar* 调用,都不需要额外追加vbNullChar。因为VB里的字符串都是BSTR结构,BSTR 结构包括:长度指示,字符串内容,vbNullChar(不计入长度,如果是Unicode编码时,占2个字节)。但是如果将字符串赋值给 Byte 数组,进行间接调用,就需要追加了,因为赋值给 Byte 数组的内容不包括 vbNullChar。注:这段解释来自这个帖子12楼的发言,感谢Tiger_Zhao。

StrPtr还能用于优化ANSI API函数的调用。在调用时使用StrConv和StrPtr就能避免将一个字符串变量多次传递给函数以及为每个调用而执行转换操作所造成的系统负担。例如

'原来的:
Declare Sub MyAnsiCall Lib "MyAnsiDll.dll" (ByVal pStr As String)
MyAnsiCall MyStr

'现在变为:
Declare Sub MyAnsiCall Lib "MyAnsiDll.dll" (ByVal pStr As Long)

MyStr=StrConv(MyStr,vbFromUnicode)
MyAnsiCall StrPtr(MyStr)
MyStr=StrConv(MyStr,vbUnicode) '注释:并不总是要求
StrPtr还是唯一能直观地告诉你空字符串和null字符串的不同的方法。对于null字符串(vbNullString),StrPtr的返回值为0,而对于空字符串,函数的返回值为非零。看下面的例子(输出结果在注释内):

view plaincopy to clipboardprint?
Sub Test1_Null()
Dim str1 As String, str2 As String

'str1 未初始化前,也就是真正的 vbNullString,也就是 Null 字符串
str2 = vbNullChar
Debug.Print str1 = str2, Len(str2), Len(str1) 'False, 1, 0
Debug.Print str1 = vbNullString, StrPtr(str1), Len(str1) 'True, 0, 0

'str1初始化为空字符串之后
str1 = ""
Debug.Print str1 = vbNullString, StrPtr(str1), Len(str1) 'True, 165209884, 0
Debug.Print str1 = str2 'False
End Sub
Sub Test1_Null()
Dim str1 As String, str2 As String

'str1 未初始化前,也就是真正的 vbNullString,也就是 Null 字符串
str2 = vbNullChar
Debug.Print str1 = str2, Len(str2), Len(str1) 'False, 1, 0
Debug.Print str1 = vbNullString, StrPtr(str1), Len(str1) 'True, 0, 0

'str1初始化为空字符串之后
str1 = ""
Debug.Print str1 = vbNullString, StrPtr(str1), Len(str1) 'True, 165209884, 0
Debug.Print str1 = str2 'False
End Sub

6.4. ObjPtr

该函数返回由对象变量引用的接口指针。由于大多数对象都支持多重接口,因此搞清楚地址对应的是对象的哪一个接口就非常重要了。通常这个函数用于处理放在集合中的对象。通过创建基于对象地址的关键字,你就可以在不需要遍历整个集合中所有元素的情况下,轻松地将对象从集合中删除。在许多情况下,对象地址是唯一可靠的能作为关键字的东西,示例如下:

ObjCol.Add MyObj1,CStr(ObjPtr(MyObj1))
.....
ObjCol.Remove CStr(ObjPtr(MyObj1))

6.5. VB6中字符串转换常用函数

对于VB的字符串,几个专门“武器”大概有:

StrConv() 'unicode与ansi的互换
VarPtr() '-- 获得字符串变量的地址
StrPtr() '-- 获得字符串缓冲区的地址
Asc(), AscB(), AscW()
Chr(), ChrB(), ChrW()
Len(), LenB()
vbNullString, vbNullChar

以Asc、AscB、AscW为例,其区别如下。

Asc(string) 返回与字符串的第一个字母对应的 ANSI 字符代码。返回值:英文 >0,中文 0,中文 >255

可以下面的例子:

view plaincopy to clipboardprint?
Sub Test2_StrFunc()
Dim str1 As String, str2 As String

str1 = "想你So"
str2 = "So想你"

Debug.Print Asc(str1), AscB(str1), AscW(str1) '-12309 243 24819
Debug.Print Asc(str2), AscB(str2), AscW(str2) ' 83 83 83

Debug.Print Chr(-12309) & "*", ChrB(243) & "*", ChrW(24819) & "*" '想* ? 想*
Debug.Print Chr(83) & "*", ChrB(83) & "*", ChrW(83) & "*" 'S* ? S*

str2 = ChrB(83) & ChrB(0) '"S"由二进制83和二进制0表示
Debug.Print str2 'S
End Sub
Sub Test2_StrFunc()
Dim str1 As String, str2 As String

str1 = "想你So"
str2 = "So想你"

Debug.Print Asc(str1), AscB(str1), AscW(str1) '-12309 243 24819
Debug.Print Asc(str2), AscB(str2), AscW(str2) ' 83 83 83

Debug.Print Chr(-12309) & "*", ChrB(243) & "*", ChrW(24819) & "*" '想* ? 想*
Debug.Print Chr(83) & "*", ChrB(83) & "*", ChrW(83) & "*" 'S* ? S*

str2 = ChrB(83) & ChrB(0) '"S"由二进制83和二进制0表示
Debug.Print str2 'S
End Sub

6.6. 指针啊指针

在6.1节里我们提到,如果结构里有字符串,那么最好用varptr传结构的指针过去,以避免无谓的WA转换。不过的话,由于VB里和C里对内存的使用不一样,结构定义得不小心的话,容易导致内存溢出。这又是另一个话题了,在另一篇文章里讨论吧。

7、小结

(0)在VB6中,用A版API写代码简单(因为编码转换VB自动给做了),用U版API效率要高些(因为有时可以省略不必要地编码转换)。

(1)在VB6中,API调用中,只要是声明为string类型的参数都会自动受到unicode到ansi的转换。

(2)用U版API,对于string型参数,要转化为long,并用StrPrt传递字符串缓冲区指针进去。

(3)用U版API,传结构型参数时,用varptr传进去可避免不必要的编码转换,见6.1节;或者也可以用字节数组的办法,见6.2节

(4)用A版API,用strconv和strptr结合,可提高效率,见6.4节

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多