分享

Unicode与Windows编程(3) | 梦之城堡

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

UnicodeWindows编程(3)

2.8 如何编写UNICODE源代码

Microsoft公司为UNICODE设计了Windows API,这样,可以尽量减少对你的代码的影响。实际上,你可以编写单个源代码文件,以便使用或者不使用UNICODE来对它进行编译。只需要定义两个宏(UNICODE_ UNICODE),就可以修改然后重新编译该源文件。

2.8.1 C运行期库对UNICODE的支持

为了利用UNICODE字符串,定义了一些数据类型。标准的C头文件String,h已经作了修改,以便定义一个名字为wchart的数据类型,它是一个UNICODE字符的数据类型:

例如,如果想要创建一个缓存,用于存放最多为99个字符的UNICODE字符串和一个结尾为

零的字符,可以使用下面这个语句:

该语句创建了一个由10016位值组成的数组。当然,标准的C运行期字符串函数,如

strcpystrchrstrcat等,只能对ANSI字符串进行操作,不能正确地处理UNICODE字符串。因此,ANSI C也拥有一组补充函数。清单2 – 1显示了一些标准的ANSI C字符串函数,后面是它们的等价UNICODE函数。

clip_image001

清单2-1 标准的ANSI C字符串函数和它们的等价UNICODE函数

clip_image002

请注意,所有的UNICODE函数均以wcs开头,wcs是宽字符串的英文缩写。若要调用UNICODE函数,只需用前缀wcs来取代ANSI字符串函数的前缀str即可。

 

注意       大多数软件开发人员可能已经不记得这样一个非常重要的问题了,那就是

Microsoft公司提供的C运行期库与ANSI的标准C运行期库是一致的。ANSI C规定,C运行期库支持UNICODE字符和字符串。这意味着始终都可以调用C运行期函数,以便对UNICODE字符和字符串进行操作,即使是在Windows 98上运行,也可以调用这些函数。换句话说, wcscatwcslenwcstok等函数都能够在Windows 98上很好地运行,这些都是必须关心的操作系统函数。

对于包含了对str函数或wcs函数进行显式调用的代码来说,无法非常容易地同时为ANSIUNICODE对这些代码进行编译。本章前面说过,可以创建同时为ANSIUNICODE进行编译的单个源代码文件。若要建立双重功能,必须包含TChar.h文件,而不是包含String,h文件。

TChar.h文件的唯一作用是帮助创建ANSI / UNICODE通用源代码文件。它包含你应该用在源代码中的一组宏,而不应该直接调用str函数或者wcs函数。如果在编译源代码文件时定义了_ UNICODE,这些宏就会引用wcs这组函数。如果没有定义_ UNICODE,那么这些宏将引用str这组宏。

例如,在TChar.h中有一个宏称为_tcscpy。如果在包含该头文件时没有定义_ UNICODE ,那么_tcscpy就会扩展为ANSIstrcpy函数。但是如果定义了_UNICODE, _tcscpy将扩展为UNICODEwcscpy函数。拥有字符串参数的所有C运行期函数都在TChar.h文件中定义了一个通用宏。如果使用通用宏,而不是ANSI / UNICODE的特定函数名,就能够顺利地创建可以为ANSIUNICODE进行编译的源代码。

但是,除了使用这些宏之外,还有一些操作是必须进行的。TChar.h文件包含了另外一些宏。若要定义一个ANSI / UNICODE通用的字符串数组,请使用下面的TCHAR数据类型。如果定义了_ UNICODETCHAR将声明为下面的形式:

clip_image003

使用该数据类型,可以像下面这样分配一个字符串:

clip_image004

也可以创建对字符串的指针:

clip_image005

不过上面这行代码存在一个问题。按照默认设置, Microsoft公司的C + +编译器能够编译所有的字符串,就像它们是ANSI字符串,而不是UNICODE字符串。因此,如果没有定义_ UNICODE,该编译器将能正确地编译这一行代码。但是,如果定义了_ UNICODE,就会产生一个错误。若要生成一个UNICODE字符串而不是ANSI字符串,必须将该代码行改写为下面的样子:

clip_image006

字符串(literal string)前面的大写字母L,用于告诉编译器该字符串应该作为UNICODE字符串来编译。当编译器将字符串置于程序的数据部分中时,它在每个字符之间分散插入零字节。这种变更带来的问题是,现在只有当定义了_ UNICODE时,程序才能成功地进行编译。我们需要另一个宏,以便有选择地在字符串的前面加上大写字母L。这项工作由_TEXT宏来完成,_TEXT宏也在TChar.h文件中做了定义。如果定义了_ UNICODE,那么_TEXT定义为下面的形式:

clip_image007

如果没有定义_ UNICODE_TEXT将定义为

clip_image008

使用该宏,可以改写上面这行代码,这样,无论是否定义了_ UNICODE宏,它都能够正确地进行编译。如下所示:

clip_image009

_TEXT宏也可以用于字符串。例如,若要检查一个字符串的第一个字符是否是大写字母J,只需编写下面的代码即可:

clip_image010

2.8.2 Windows定义的UNICODE数据类型

Windows头文件定义了表2 – 3列出的数据类型。

2-3 Uincode 数据类型

clip_image012

这些数据类型是指UNICODE字符和字符串。Windows头文件也定义了ANSI / UNICODE通用数据类型P T STRP C T STR。这些数据类型既可以指ANSI字符串,也可以指UNICODE字符串,这取决于当编译程序模块时是否定义了UNICODE宏。请注意,这里的UNICODE宏没有前置的下划线。_ UNICODE宏用于C运行期头文件,而UNICODE宏则用于Windows头文件。当编译源代码模块时,通常必须同时定义这两个宏。

2.8.3 Windows中的UNICODE函数和ANSI函数

前面已经讲过,有两个函数称为CreateWindowEx,一个CreateWindowEx接受UNICODE字符串,另一个CreateWindowEx接受ANSI字符串。情况确实如此,不过,这两个函数的原型实际上是下面的样子:

clip_image013

clip_image014

CreateWindowEx W是接受UNICODE字符串的函数版本。函数名结尾处的大写字母W是英文wide()的缩写。每个UNICODE字符的长度是1 6位,因此,它们常常称为宽字符。CreateWindowEx A的结尾处的大写字母A表示该函数可以接受ANSI字符串。

但是,在我们的代码中,通常只包含了对CreateWindowEx的调用,而不是直接调用CreateWindowEx W或者CreateWindowEx A。在Wi n U s e r. h文件中,CreateWindowEx实际上是定义为下面这种形式的一个宏:

clip_image015

当编译源代码模块时, UNICODE 是否已经作了定义,将决定你调用的是哪CreateWindowEx版本。当转用一个1 6位的Windows应用程序时,你在编译期间可能没有定义UNICODE。对CreateWindowEx函数的任何调用都会将该宏扩展为对CreateWindowEx A的调用,即对CreateWindowExANSI版本的调用。由于1 6Windows只提供了CreateWindowExANSI版本,因此可以比较容易地转用它的应用程序。

Windows 2000下,MicrosoftCreateWindowEx A源代码只不过是一个形实替换程序层或

翻译层,用于分配内存,以便将ANSI字符串转换成UNICODE字符串。该代码然后调用CreateWindowEx,并传递转换后的字符串。当CreateWindowEx W返回时,CreateWindowEx A便释放它的内存缓存,并将窗口句柄返回给你。

如果要创建其他软件开发人员将要使用的动态链接库(DLL),请考虑使用下面的方法。在D L L中提供两个输出函数。一个是ANSI版本,另一个是UNICODE版本。在ANSI版本中,只需要分配内存,执行必要的字符串转换,并调用该函数的UNICODE版本(本章后面部分介绍这个进程)。

Windows 98下,MicrosoftCreateWindowEx A源代码是执行操作的函数。Windows 98

提供了接受UNICODE参数的所有Windows函数的进入点,但是这些函数并不将UNICODE字符串转换成ANSI字符串,它们只返回运行失败的消息。调用GetLastError将返ERROR_CALLNOTIMPLEMENTED。这些函数中只有ANSI版本的函数才能正确地运行。如果编译的代码调用了任何宽字符函数,应用程序将无法在Windows 98下运行。

Windows API中的某些函数,比如WinExecOpenFile等,只是为了实现与1 6Windows程序的向后兼容而存在,因此,应该避免使用。应该使用对CreateProcessCreateFile函数的调用来取代对WinExecOpenFile函数的调用。从系统内部来讲,老的函数完全可以调用新的函数。老的函数存在的一个大问题是,它们不接受UNICODE字符串。当调用这些函数时,必须传递ANSI字符串。另一方面,所有新的和未过时的函数在Windows 2000中都同时拥有ANSIUNICODE两个版本。

2.8.4 Windows字符串函数

Windows还提供了一组范围很广的字符串操作函数。这些函数与C运行期字符串函数(如strcpywcscpy)很相似。但是该操作系统函数是操作系统的一个组成部分,操作系统的许多组件都使用这些函数,而不使用C运行期库。建议最好使用操作系统函数,而不要使用C运行期字符串函数。这将有助于稍稍提高你的应用程序的运行性能,因为操作系统字符串函数常常被大型应用程序比如操作系统的外壳进程Explorer.exe所使用。由于这些函数使用得很多,因此,在你的应用程序运行时,它们可能已经被装入RAM

若要使用这些函数,系统必须运行Windows 2000Windows 98。如果安装了InternetExplorer 4.0或更新的版本,也可以在较早的Windows版本中获得这些函数。在经典的操作系统函数样式中,操作系统字符串函数名既包含大写字母,也包含小写字母,它的形式类似这个样子: StrcatStrchrStrCmpStrcpy等。若要使用这些函数,必须加上S h l WA p i . h头文件。另外,如前所述,这些字符串函数既有ANSI版本,也有UNICODE版本,例如Strcat AStrcat W。由于这些函数属于操作系统函数,因此,当创建应用程序时,如果定义了UNICODE(不带前置下划线),那么它们的符号将扩展为宽字符版本。

2.9 成为符合ANSIUNICODE的应用程序

即使你不打算立即使用UNICODE,最好也应该着手将你的应用程序转换成符合UNICODE的应

用程序。下面是应该遵循的一些基本原则:

将文本串视为字符数组,而不是c h a r s数组或字节数组。

将通用数据类型(如TCHARP T STR)用于文本字符和字符串。

将显式数据类型(如B Y T EP B Y T E)用于字节、字节指针和数据缓存。

T E X T宏用于原义字符和字符串。

执行全局性替换(例如用P T STR替换P STR)。

修改字符串运算问题。例如函数通常希望你在字符中传递一个缓存的大小,而不是字节。

这意味着你不应该传递s i z e o f ( s z B u ff e r ) ,而应该传递( s i z e o f ( s z B u ff e r ) / s i z e o f ( TCHAR )。另外,

如果需要为字符串分配一个内存块,并且拥有该字符串中的字符数目,那么请记住要按字节来分配内存。这就是说,应该调用malloc(nCharacters *sizeof(TCHAR)), 而不是调用malloc(nChar acters )。在上面所说的所有原则中,这是最难记住的一条原则,如果操作错误,编译器

将不发出任何警告。

当我为本书的第一版编写示例程序时,我编写的原始程序只能编译为ANSI程序。后来,当我开始撰写本章的内容时,我想我应该鼓励使用UNICODE,并且打算创建一些示例程序,以便展示你可以非常容易地编写既可以用UNICODE也可以用ANSI来编译的程序。这时我发现最好的办法是将本书的所有示例程序进行转换,使它们都能够用UNICODEANSI进行编译。我用了大约4个小时将所有程序进行了转换。考虑到我以前从来没有这方面的转换经验,这个速度是相当不错了。

2.9.1 Windows字符串函数

Win d o w s也提供了一组用于对UNICODE字符串进行操作的函数,表2 – 4对它们进行了描述。

clip_image017

这些函数是作为宏来实现的,这些宏既可以调用函数的UNICODE版本,也可以调用函数的ANSI版本,这要根据编译源代码模块时是否已经定义了UNICODE而定。例如,如果没有定义UNICODEl strcat函数将扩展为l strcat A。如果定义了UNICODEl strcat将扩展为l strcat W。有两个字符串函数,即l StrCmpl StrCmp i,它们的行为特性与等价的C运行期函数是不同的。C运行期函数StrCmpStrCmpiwcscmpwcscmpi只是对字符串中的代码点的值进行比较这

就是说,这些函数将忽略实际字符的含义,只是将第一个字符串中的每个字符的数值与第二个

字符串中的字符的数值进行比较。而Windows函数l StrCmpl StrCmpi是作为对Windows函数CompareString的调用来实现的。

clip_image018

该函数对两个UNICODE字符串进行比较。CompareString的第一个参数用于设定语言IDLCID),这是个3 2位值,用于标识一种特定的语言。CompareString使用这个LCID来比较这两个字符串,方法是对照一种特定的语言来查看它们的字符的含义。这种操作方法比C运行期函数简单地进行数值比较更有意义。

l StrCmp函数系列中的任何一个函数调用CompareString时,该函数便将调用Windows

GetTheadString函数的结果作为第一个参数来传递:

clip_image019

每次创建一个线程时,它就被赋予一种语言。函数将返回该线程的当前语言设置。CompareString的第二个参数用于标识一些标志,这些标志用来修改该函数比较两个字符串时所用的方法。表2 – 5显示了可以使用的标志。

clip_image021

l StrCmp调用CompareString时,它传递0作为fdwStyle的参数。但是,当lStrCmp i调用CompareString时,它就传递NORMIGNORECASECompareString的其余4个参数用于设定两个字符串和它们各自的长度。如果为cch1参数传递- 1,那么该函数将认为pString1字符串是以0结尾,并计算该字符串的长度。对于pString2字符串来说,参数cch2的作用也是一样。其他C运行期函数没有为UNICODE字符串的操作提供很好的支持。例如,tolowertoupper函数无法正确地转换带有重音符号的字符。为了弥补C运行期库中的这些不足,必须调用下面这些Windows函数,以便转换UNICODE字符串的大小写字母。这些函数也可以正确地用于ANSI字符串。头两个函数:

clip_image022

既可以转换单个字符,也可以转换以0结尾的整个字符串。若要转换整个字符串,只需要传递字符串的地址即可。若要转换单个字符,必须像下面这样传递各个字符:

clip_image024

6位包含了该字符,较高的1 6位包含0。当该函数看到较高位是0时,该函数就知道你想要转换单个字符,而不是整个字符串。返回的值是个3 2位值,较低的1 6位中是已经转换的字符。下面两个函数与前面两个函数很相似,差别在于它们用于转换缓存中包含的字符(该缓存不必以0结尾):

clip_image025

其他的C运行期函数,如isalphaislowerisupper,返回一个值,指明某个字符是字母字符、小写字母还是大写字母。Windows API 提供了一些函数,也能返回这些信息,但是Windows函数也要考虑用户在控制面板中指定的语言:

clip_image026

printf函数家族是要介绍的最后一组C运行期函数。如果在定义了_ UNICODE的情况下编译你的源代码模块,那么printf函数家族便希望所有字符和字符串参数代表UNICODE字符和字符串。但是,如果在没有定义_ UNICODE的情况下编译你的源代码模块, printf函数家族便希望传递

给它的所有字符和字符串都是ANSI字符和字符串。Microsoft公司已经给C运行期的printf函数家族增加了一些特殊的域类型。其中有些域类型尚未被ANSIC采用。新类型使你能够很容易地对ANSIUNICODE字符和字符串进行混合和匹配。

操作系统的w s printf函数也得到了增强。下面是一些例子(请注意大写S和小写s的使用):

clip_image027

clip_image028

2.9.2 资源

当资源编译器对你的所有资源进行编译时,输出文件是资源的二进制文件。资源(字符串表、对话框模板和菜单等)中的字符串值总是写作UNICODE字符串。在Windows 98Windows2000下,如果应用程序没有定义UNICODE宏,那么系统就会进行内部转换。

例如,如果在编译源代码模块时没有定义UNICODE,调用LoadString实际上就是调用LoadString A函数。这时LoadString A就从你的资源中读取字符串,并将该字符串转换成ANSI字符串。ANSI形式的字符串将从该函数返回给你的应用程序。

2.9.3 确定文本是ANSI文本还是UNICODE文本

到现在为止,UNICODE文本文件仍然非常少。实际上, Microsoft公司自己的大多数产品并没有配备任何UNICODE文本文件。但是预计将来这种情况是会改变的(尽管这需要一个很长的过程)。当然,Windows 2000Notepad (记事本)应用程序允许你既能打开UNICODE文件,也能打开ANSI文件,并且可以创建这些文件。图2 – 1显示了NotepadSave As(文件另存为)对话框。请注意可以用不同的方法来保存文本文件。

clip_image030

对于许多用来打开文本文件和处理这些文件的应用程序(如编译器)来说,打开一个文件后,应用程序就能方便地确定该文本文件是包含ANSI字符还是UNICODE字符。IsTest UNICODE函数能够帮助进行这种区分:

clip_image032

文本文件存在的问题是,它们的内容没有严格和明确的规则,因此很难确定该文件是包含ANSI字符还是UNICODE字符。IsTest UNICODE使用一系列统计方法和定性方法,以便猜测缓存的内容。由于这不是一种确切的科学方法,因此IsTest UNICODE有可能返回不正确的结果。第一个参数pvBuffer用于标识要测试的缓存的地址。该数据是个无效指针,因为你不知道你拥有的是ANSI字符数组还是UNICODE字符数组。

第二个参数c b用于设定pvBuffer指向的字节数。同样,由于你不知道缓存中放的是什么,因此c b是个字节数,而不是字符数。请注意,不必设定缓存的整个长度。当然, IsTest UNICODE能够测试的字节越多,得到的结果越准确。

第三个参数pResult是个整数的地址,必须在调用IsTest UNICODE之前对它进行初始化。对该整数进行初始化后,就可以指明你要IsTest UNICODE执行哪些测试。也可以为该参数传递NULL,在这种情况下,IsTest UNICODE将执行它能够进行的所有测试(详细说明请参见Platform SDK文档)。

如果IsTest UNICODE认为缓存包含UNICODE文本,便返回TRUE,否则返回FALSE。确实是这样,尽管Microsoft将该函数的原型规定为返回D W O R D,但是它实际上返回一个布尔值。如果在pResult参数指向的整数中必须进行特定的测试,该函数就会在返回之前设定整数中的信息位,以反映每个测试的结果。

Windows98 Windows 98下,IsTest UNICODE函数没有有用的实现代码,它只是返回FALSE。调用GetLastError函数将返回E R R O R C A L L N O T I M P L E M E N T D

 

2.9.4 UNICODEANSI之间转换字符串

Windows函数MultiByteToWideChar用于将多字节字符串转换成宽字符串。下面显示了MultiByteToWideChar函数。

clip_image033

uCodePage参数用于标识一个与多字节字符串相关的代码页号。dwFlags参数用于设定另一个控件,它可以用重音符号之类的区分标记来影响字符。这些标志通常并不使用,在dwFlags参数中传递0pMultiByteStr参数用于设定要转换的字符串, cchMultiByte参数用于指明该字符串的长度(按字节计算)。如果为cchMultiByte参数传递- 1,那么该函数用于确定源字符串的长

度。

转换后产生的UNICODE版本字符串将被写入内存中的缓存,其地址由pWideCharStr参数指定。必须在cchWideChar参数中设定该缓存的最大值(以字符为计量单位)。如果调用MultiByteToWideChar,给cchWideChar参数传递0,那么该参数将不执行字符串的转换,而是返回为使转换取得成功所需要的缓存的值。一般来说,可以通过下列步骤将多字节字符串转换

UNICODE等价字符串:

1) 调用MultiByteToWideChar函数,为pWideCharStr参数传递NULL,为cchWideChar参数

传递0

2) 分配足够的内存块,用于存放转换后的UNICODE字符串。该内存块的大小由前面对M u l t B y t e To Wi d e C h a r的调用返回。

3) 再次调用MultiByteToWideChar,这次将缓存的地址作为pWideCharStr参数来传递,并传递第一次调用MultiByteToWideChar时返回的缓存大小,作为cchWideChar参数。

4. 使用转换后的字符串。

5) 释放UNICODE字符串占用的内存块。函数WideCharToMultiByte将宽字符串转换成等价的多字节字符串,如下所示:

clip_image034

该函数与MultiByteToWideChar函数相似。同样,uCodePage参数用于标识与新转换的字符串相关的代码页。dwFlags则设定用于转换的其他控件。这些标志能够作用于带有区分符号的字符和系统不能转换的字符。通常不需要为字符串的转换而拥有这种程度的控制手段,你将为dwFlags参数传递0pWideCharStr参数用于设定要转换的字符串的内存地址,cchWideChar参数用于指明该字符串的长度(用字符数来计量)。如果你为cchWideChar参数传递- 1,那么该函数用于确定源字符串的长度。

转换产生的多字节版本的字符串被写入由pMultiByteStr参数指明的缓存。必须在cchMultiByte参数中设定该缓存的最大值(用字节来计量)。如果传递0作为WideCharToMultiByte函数的cchMultiByte参数,那么该函数将返回目标缓存需要的大小值。通常可以使用将多字节字符串转换成宽字节字符串时介绍的一系列类似的事件,将宽字节字符串转换成多字节字符串。你会发现,WideCharToMultiByte函数接受的参数比MultiByteToWideChar函数要多2个,即pDefaulTCHARpfUsedDefaulTCHAR。只有当WideCharToMultiByte函数遇到一个宽字节字符,而该字符在uCodePage参数标识的代码页中并没有它的表示法时,WideCharToMultiByte函数才使用这两个参数。如果宽字节字符不能被转换,该函数便使用pDefaulTCHAR参数指向的字符。如果该参数是NULL(这是大多数情况下的参数值),那么该函数使用系统的默认字符。该默认字符通常是个问号。这对于文件名来说是危险的,因为问号是个通配符。pfUsedDefaulTCHAR参数指向一个布尔变量,如果宽字符串中至少有一个字符不能转换成等价多字节字符,那么函数就将该变量置为TRUE。如果所有字符均被成功地转换,那么该函数就将该变量置为FALSE。当函数返回以便检查宽字节字符串是否被成功地转换后,可以测试该变量。同样,通常为该测试传递NULL

关于如何使用这些函数的详细说明,请参见Platform SDK文档。

如果使用这两个函数,就可以很容易创建这些函数的UNICODE版本和ANSI版本。例如,你

可能有一个动态链接库,它包含一个函数,能够转换字符串中的所有字符。可以像下面这样编写该函数的UNICODE版本:

clip_image036

clip_image038

你可以编写该函数的ANSI版本以便该函数根本不执行转换字符串的实际操作。你也可以编写该函数的ANSI版本,以便该函数它将ANSI字符串转换成UNICODE字符串,将UNICODE字符串传递给StringReverseW函数,然后将转换后的字符串重新转换成ANSI字符串。该函数类似下面的样子:

clip_image040

clip_image041

clip_image043

最后,在用动态链接库分配的头文件中,可以像下面这样建立这两个函数的原型:

clip_image044

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

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多