第二章 Unicode
之所以将这一章放在本书的开头,是因为考虑到U n i c o d e 是开发任何应用程序时要采用的基本步骤。本书的每一章中几乎都要讲到关于U n i c o d e 的问题,而且书中给出的所有示例应用程序都是“用U n i c o d e 实现的”。如果你为Microsoft Windows 2000 或Microsoft Windows CE 开发应用程序,你应该使用U n i c o d e 进行开发。如果你为Microsoft Windows 98 开发应用程序,你必须对某些问题作出决定。本章也要讲述Windows 98 的有关问题。
Windows 操作系统始终不逾地提供各种支持,以帮助软件开发人员进行应用程序的本地化工作。应用软件可以从各种不同的函数中获得特定国家的信息,并可观察控制面板的设置,以确定用户的首选项。Wi n d o w s 甚至支持不同的字体,以适应应用的需要。
2.1 字符集
软件的本地化要解决的真正问题,实际上就是如何来处理不同的字符集。多年来,许多人一直将文本串作为一系列单字节字符来进行编码,并在结尾处放上一个零。对于我们来说,这已经成了习惯。当调用strlen 函数时,它在以0 结尾的单字节字符数组中返回字符的数目。
有些文字和书写规则(比如日文中的汉字就是个典型的例子)的字符集中的符号太多了,因此单字节(它提供的符号最多不能超过2 5 6 个)是根本不敷使用的。为此出现了双字节字符集(D B C S ),以支持这些文字和书写规则。
2.1.1 单字节与双字节字符集
在双字节字符集中,字符串中的每个字符可以包含一个字节或包含两个字节。例如,日文中的汉字,如果第一个字符在0 x 8 1 与0 x 9 F 之间,或者在0 x E 0 与0 x F C 之间,那么就必须观察下一个字节,才能确定字符串中的这个完整的字符。使用双字节字符集,对于程序员来说简直是个很大的难题,因为有些字符只有一个字节宽,而有些字符则是两个字节宽。
如果只是调用strlen 函数,那么你无法真正了解字符串中究竟有多少字符,它只能告诉你到达结尾的0 之前有多少个字节。ANSI的C 运行期库中没有配备相应的函数,使你能够对双字节字符集进行操作。但是,Microsoft Visual C++的运行期库却包含许多函数,如_mbslen ,它可以用来操作多字节(既包括单字节也包括双字节)字符串。
为了帮助你对D B C S 字符串进行操作,Wi n d o w s 提供了下面的一组帮助函数(见表2 - 1 )。前两个函数CharNext 和Char Prev 允许前向或逆向遍历DBCS 字符串,方法是每次一个字符。第三个函数IsDBCSLeadByte, 在字节返回到一个两字字节符的第一个字节时将返回T R U E 。
表2-1 对D B C S 字符串进行操作的帮助函数
函数 | 描述 |
PTSTR CharNext(PCTSTR pszCurrentChar); | 返回字符串中的下一个字符的地址 |
PTSTR CharPrev (PCTSTR pszStart,PCTSTR p s z C u r r e n t C h a r); | 返回字符串中的上一个字符的地址 |
BOOL IsDBCSLeadByteTRUE(BYTE bTestChar); | 如果该字节是DBCS 字符的第一个字节,则返回 |
这65536个字符可以分成不同的区域。表2-2 显示了这样的区域的一部分以及分配给这些区域的字符。
表2-2 区域字符
1 6 位代码 | 字符 | 16 位代码 | 字符 |
0 0 0 0 - 0 0 7 F | A S C I I | 0 3 0 0 - 0 3 6 F | 通用区分标志 |
0 0 8 0 - 0 0 F F | 拉丁文1 字符 | 0 4 0 0 - 0 4 F F | 西里尔字母 |
0 1 0 0 - 0 1 7 F | 欧洲拉丁文 | 0 5 3 0 - 0 5 8 F | 亚美尼亚文 |
0 1 8 0 - 0 1 F F | 扩充拉丁文 | 0 5 9 0 - 0 5 F F | 西伯莱文 |
0 2 5 0 - 0 2 A F | 标准拼音 | 0 6 0 0 - 0 6 F F | 阿拉伯文 |
0 2 B 0 - 0 2 F F | 修改型字母 | 0 9 0 0 - 0 9 7 F | 梵文 |
目前尚未分配的代码点大约还有29 000 个,不过它们是保留供将来使用的。另外,大约有6 0 0 0 个代码点是保留供个人使用的。
2.2 为什么使用Unicode
当开发应用程序时,当然应该考虑利用Unicode 的优点。即使现在你不打算对应用程序进行本地化,开发时将U n i c o d e 放在心上,肯定可以简化将来的代码转换工作。此外,Unicode 还具备下列功能:
• 可以很容易地在不同语言之间进行数据交换。
• 使你能够分配支持所有语言的单个二进制. e x e 文件或D L L 文件。
• 提高应用程序的运行效率(本章后面还要详细介绍)。
2.3 Windows 2000与Unicode
Windows 2000 是使用Unicode 从头进行开发的,用于创建窗口、显示文本、进行字符串操作等的所有核心函数都需要Unicode 字符串。如果调用任何一个Windows 函数并给它传递一个ANSI 字符串,那么系统首先要将字符串转换成Unicode ,然后将Unicode 字符串传递给操作系统。如果希望函数返回ANSI 字符串,系统就会首先将Unicode 字符串转换成ANSI 字符串,然后将结果返回给你的应用程序。所有这些转换操作都是在你看不见的情况下发生的。当然,进行这些字符串的转换需要占用系统的时间和内存。
例如,如果调用CreateWindowEx 函数,并传递类名字和窗口标题文本的非U n i c o d e 字符串,那么CreateWindowEx 必须分配内存块(在你的进程的默认堆中),将非Unicode 字符串转换成Unicode 字符串,并将结果存储在分配到的内存块中,然后调用Unicode 版本的CreateWindowEx函数。
对于用字符串填入缓存的函数来说,系统必须首先将U n i c o d e 字符串转换成非U n i c o d e 字符串,然后你的应用程序才能处理该字符串。由于系统必须执行所有这些转换操作,因此你的应用程序需要更多的内存,并且运行的速度比较慢。通过从头开始用U n i c o d e 来开发应用程序,就能够使你的应用程序更加有效地运行。
2.7 对COM的简单说明
当Microsoft 公司将COM 从1 6 位Windows 转换成Wi n 3 2 时,公司作出了一个决定,即需要字符串的所有COM 接口方法都只能接受Unicode 字符串。这是个了不起的决定,因为COM 通常用于使不同的组件能够互相进行通信,而Unicode 则是传递字符串的最佳手段。
如果你为Windows 2000 或Windows CE 开发应用程序,并且也使用C O M ,那么你将会如虎添翼。在你的整个源代码中使用U n i c o d e ,将使与操作系统进行通信和与C O M 对象进行通信的操作变成一件轻而易举的事情。
如果你为Windows 98 开发应用程序,并且也使用C O M ,那么将会遇到一些问题。C O M 要求使用Unicode 字符串,而操作系统的大多数函数要求使用A N S I 字符串。那是多么难办的事情啊!我曾经从事过若干个项目的开发,在这些项目中,我编写了许多代码,仅仅是为了来回进行字符串的转换。
2.8 如何编写Unicode源代码
Microsoft 公司为U n i c o d e 设计了Windows API ,这样,可以尽量减少对你的代码的影响。实际上,你可以编写单个源代码文件,以便使用或者不使用U n i c o d e 来对它进行编译。只需要定义两个宏(UNICODE 和_UNICODE ),就可以修改然后重新编译该源文件。(UNICODE 宏则用于Wi n d o w s 头文件, 而_UNICODE 宏用于C 运行期头文件。)
2.8.1 C 运行期库对Unicode的支持
为了利用U n i c o d e 字符串,定义了一些数据类型。标准的C 头文件S t r i n g . h 已经作了修改,以便定义一个名字为wchar_ t 的数据类型,它是一个Unicode 字符的数据类型:
typedef unsigned short wchar_t;
例如,如果想要创建一个缓存,用于存放最多为99 个字符的U n i c o d e 字符串和一个结尾为零的字符,可以使用下面这个语句:
wchar_t szBuffer[100];
该语句创建了一个由1 0 0 个1 6 位值组成的数组。当然,标准的C 运行期字符串函数,如s t r c p y 、s t r c h r 和s t r c a t 等,只能对A N S I 字符串进行操作,不能正确地处理U n i c o d e 字符串。因此,ANSI C 也拥有一组补充函数。清单2 - 1 显示了一些标准的ANSI C 字符串函数,后面是它们的等价U n i c o d e 函数。
char * strcat(char *,const char *);
wchar_t * wcscat(wchar_t *,const wchar_t *);
清单2-1 标准的ANSI C 字符串函数和它们的等价U n i c o d e 函数
char * strchr(const char *,int);
wchar_t * wcschr(const wchar_t *,wchar_t);
int strcmp(const char *,const char *);
int wcscmp(const wchar_t *,const wchar_t *);
char * strcpy(char *,const char *);
wchar_t * wcscpy(wchar_t *,const wchar_t *);
size_t strlen(const char *);
size_t wcslen(const wchar_t *);
请注意,所有的Unicode 函数均以wcs 开头,wcs 是宽字符串的英文缩写。若要调用U n i c o d e函数,只需用前缀wcs 来取代ANSI 字符串函数的前缀str 即可。
注意大多数软件开发人员可能已经不记得这样一个非常重要的问题了,那就是M i c r o s o f t 公司提供的C 运行期库与A N S I 的标准C 运行期库是一致的。ANSI C 规定,C运行期库支持U n i c o d e 字符和字符串。这意味着始终都可以调用C 运行期函数,以便对U n i c o d e 字符和字符串进行操作,即使是在Windows 98 上运行,也可以调用这些函数。换句话说,w c s c a t 、w c s l e n 和w c s t o k 等函数都能够在Windows 98 上很好地运行,这些都是必须关心的操作系统函数。
对于包含了对s t r 函数或w c s 函数进行显式调用的代码来说,无法非常容易地同时为A N S I 和U n i c o d e 对这些代码进行编译。本章前面说过,可以创建同时为A N S I 和U n i c o d e 进行编译的单个源代码文件。若要建立双重功能,必须包含TChar. h 文件,而不是包含String . h 文件。
TChar.h 文件的唯一作用是帮助创建A N S I / U n i c o d e 通用源代码文件。它包含你应该用在源代码中的一组宏,而不应该直接调用str 函数或者wcs 函数。如果在编译源代码文件时定义了UNICODE ,这些宏就会引用w c s 这组函数。如果没有定义_UNICODE ,那么这些宏将引用str 这组宏。
例如,在T C h a r. h 中有一个宏称为_ t c s c p y 。如果在包含该头文件时没有定义_ U N I C O D E ,那么_ t c s c p y 就会扩展为A N S I 的s t r c p y 函数。但是如果定义了_UNICODE, _tcscpy 将扩展为U n i c o d e的w c s c p y 函数。拥有字符串参数的所有C 运行期函数都在T C h a r. h 文件中定义了一个通用宏。如果使用通用宏,而不是A N S I / U n i c o d e 的特定函数名,就能够顺利地创建可以为A N S I 或U n i c o d e进行编译的源代码。
但是,除了使用这些宏之外,还有一些操作是必须进行的。TChar.h 文件包含了另外一些宏.若要定义一个ANSI/Unicode 通用的字符串数组,请使用下面的TCHAR 数据类型。如果定义了_UNICODE ,TCHAR 将声明为下面的形式:
typedef wchar_t TCHAR;
如果没有定义_UNICODE ,则TCHAR 将声明为下面的形式:
typedef char TCHAR;
使用该数据类型,可以像下面这样分配一个字符串:
TCHAR szString[100];
也可以创建对字符串的指针:
TCHAR *szError="Error";
不过上面这行代码存在一个问题。按照默认设置,Microsoft 公司的C++编译器能够编译所有的字符串,就像它们是A N S I 字符串,而不是U n i c o d e 字符串。因此,如果没有定义_UNICODE ,该编译器将能正确地编译这一行代码。但是,如果定义了_UNICODE ,就会产生一个错误。若要生成一个UNICODE 字符串而不是A N S I 字符串,必须将该代码行改写为下面的样子:
TCHAR *szError=L"Error";
字符串(literal string )前面的大写字母L ,用于告诉编译器该字符串应该作为U n i c o d e 字符串来编译。当编译器将字符串置于程序的数据部分中时,它在每个字符之间分散插入零字节。这种变更带来的问题是,现在只有当定义了_UNICODE 时,程序才能成功地进行编译。我们需要另一个宏,以便有选择地在字符串的前面加上大写字母L 。这项工作由_TEXT 宏来完成,_TEXT 宏也在TChar.h 文件中做了定义。如果定义了_ U N I C O D E ,那么_ T E X T 定义为下面的形式:
#define _TEXT(x) L ## x
如果没有定义_ U N I C O D E ,_ T E X T 将定义为
#define _TEXT(x) x
使用该宏,可以改写上面这行代码,这样,无论是否定义了_ U N I C O D E 宏,它都能够正确地进行编译。如下所示:
TCHAR *szError=_TEXT("Error");
_ T E X T 宏也可以用于字符串。例如,若要检查一个字符串的第一个字符是否是大写字母J ,只需编写下面的代码即可:
if(szError[0]==_TEXT('J')){
//First character is a 'J'
...
}
else{
//First character is not a 'J'
...
}
2.8.2 Windows定义的Unicode数据类型
Wi n d o w s 头文件定义了表2 - 3 列出的数据类型。
这些数据类型是指U n i c o d e 字符和字符串。Windows 头文件也定义了ANSI/Unicode 通用数据类型PTSTR 和PCTSTR 。这些数据类型既可以指ANSI 字符串,也可以指Unicode 字符串,这取决于当编译程序模块时是否定义了U N I C O D E 宏。
请注意,这里的UNICODE 宏没有前置的下划线。_UNICODE 宏用于C 运行期头文件,而UNICODE 宏则用于Wi n d o w s 头文件。当编译源代码模块时,通常必须同时定义这两个宏。
2.8.3 Windows中的Unicode函数和ANSI函数
前面已经讲过,有两个函数称为CreateWindowEx ,一个CreateWindowEx接受U n i c o d e 字符串,另一个CreateWindowEx 接受A N S I 字符串。这两个宏。
CreateWindowExW 是接受U n i c o d e 字符串的函数版本。函数名结尾处的大写字母W 是英文wide(宽)的缩写。每个Unicode 字符的长度是1 6 位,因此,它们常常称为宽字符。CreateWindowExA的结尾处的大写字母A 表示该函数可以接受ANSI 。
但是,在我们代码中,通常只包含了对CreateWindowEx 的调用,而不是直接调用CreateWindowExW 或CreateWindowExA 。在Wi n U s e r. h 文件中,CreateWindowEx 实际上是定义为下面这种形式的一个宏:
#ifdef UNICODE
#define CreateWindowEx CreateWindowExW
#else
#define CreateWindowEx CreateWindowExA
#endif //!UNICODE
当编译源代码模块时,U N I C O D E 是否已经作了定义,将决定你调用的是哪个CreateWindowEx 版本。当转用一个1 6 位的Wi n d o w s 应用程序时,你在编译期间可能没有定义U N I C O D E 。对CreateWindowEx 函数的任何调用都会将该宏扩展为对CreateWindowExA 的调用,即对CreateWindowEx的A N S I 版本的调用。由于1 6 位Wi n d o w s 只提供了CreateWindowEx 的A N S I 版本,因此可以比较容易地转用它的应用程序。
在Windows 2000 下,Microsoft 的CreateWindowExA 源代码只不过是一个形实替换程序层或翻译层,用于分配内存,以便将A N S I 字符串转换成Unicode 字符串。该代码然后调用CreateWindowExW ,并传递转换后的字符串。当CreateWindowEx返回时,CreateWindowExA 便释放它的内存缓存,并将窗口句柄返回给你。
如果要创建其他软件开发人员将要使用的动态链接库(DLL),请考虑使用下面的方法。在DLL 中提供两个输出函数。一个是ANSI 版本,另一个是U n i c o d e 版本。在ANSI 版本中,只需要分配内存,执行必要的字符串转换,并调用该函数的Unicode 版本(本章后面部分介绍这个进程)。
在Windows 98 下,M i c r o s o f t 的CreateWindowExA 源代码是执行操作的函数。Windows 98提供了接受U n i c o d e 参数的所有Wi n d o w s 函数的进入点,但是这些函数并不将U n i c o d e 字符串转换成A N S I 字符串,它们只返回运行失败的消息。调用G e t L a s t E r r o r 将返回E R R O R _C A L L _ N O T _ I M P L E M E N T E D 。这些函数中只有A N S I 版本的函数才能正确地运行。如果编译的代码调用了任何宽字符函数,应用程序将无法在Windows 98 下运行。
Windows API 中的某些函数,比如WinExec 和OpenFile 等,只是为了实现与1 6 位Wi n d o w s 程序的向后兼容而存在,因此,应该避免使用。应该使用对CreateProcess 和CreateFile 函数的调用来取代对WinExec 和OpenFile 函数的调用。从系统内部来讲,老的函数完全可以调用新的函数。老的函数存在的一个大问题是,它们不接受U n i c o d e 字符串。当调用这些函数时,必须传递ANSI 字符串。另一方面,所有新的和未过时的函数在Windows 2000 中都同时拥有A N S I 和U n i c o d e 两个版本。
2.8.4 Windows字符串函数
Windows 还提供了一组范围很广的字符串操作函数。这些函数与C 运行期字符串函数(如strcpy 和wcscpy )很相似。但是该操作系统函数是操作系统的一个组成部分,操作系统的许多组件都使用这些函数,而不使用C 运行期库。建议最好使用操作系统函数,而不要使用C 运行期字符串函数。这将有助于稍稍提高你的应用程序的运行性能,因为操作系统字符串函数常常被大型应用程序比如操作系统的外壳进程E x p l o r e r. e x e 所使用。由于这些函数使用得很多,因此,在你的应用程序运行时,它们可能已经被装入R A M 。
若要使用这些函数,系统必须运行Windows 2000 或Windows 98 。如果安装了InternetExplorer 4.0 或更新的版本,也可以在较早的Windows 版本中获得这些函数。在经典的操作系统函数样式中,操作系统字符串函数名既包含大写字母,也包含小写字母,它的形式类似这个样子:StrCat 、StrChr 、StrCmp 和StrCpy 等。若要使用这些函数,必须加上ShlWApi . h 头文件。另外,如前所述,这些字符串函数既有ANSI 版本,也有Unicode 版本,例如StrCatA 和StrCatW 。由于这些函数属于操作系统函数,因此,当创建应用程序时,如果定义了U N I C O D E (不带前置下划线),那么它们的符号将扩展为宽字符版本。
2.9 成为符合ANSI和Unicode的应用程序
即使你不打算立即使用U n i c o d e ,最好也应该着手将你的应用程序转换成符合U n i c o d e 的应用程序。下面是应该遵循的一些基本原则:
• 将文本串视为字符数组,而不是chars 数组或字节数组。
• 将通用数据类型(如TCHAR 和PTSTR )用于文本字符和字符串。
• 将显式数据类型(如BYTE 和PBYTE )用于字节、字节指针和数据缓存。
• 将TEXT 宏用于原义字符和字符串。
• 执行全局性替换(例如用P T S T R 替换P S T R )。
• 修改字符串运算问题。例如函数通常希望你在字符中传递一个缓存的大小,而不是字节。
这意味着你不应该传递sizeof(szBuffer ) ,而应该传递 sizeof(szBuffer) /sizeof(TCHAR)。另外,如果需要为字符串分配一个内存块,并且拥有该字符串中的字符数目,那么请记住要按字节来分配内存。这就是说,应该调用malloc(nCharacters *sizeof(TCHAR)),而不是调用malloc( nCharacters )。在上面所说的所有原则中,这是最难记住的一条原则,如果操作错误,编译器将不发出任何警告。
当我为本书的第一版编写示例程序时,我编写的原始程序只能编译为A N S I 程序。后来,当我开始撰写本章的内容时,我想我应该鼓励使用U n i c o d e ,并且打算创建一些示例程序,以便展示你可以非常容易地编写既可以用U n i c o d e 也可以用A N S I 来编译的程序。这时我发现最好的办法是将本书的所有示例程序进行转换,使它们都能够用U n i c o d e 和A N S I 进行编译。我用了大约4 个小时将所有程序进行了转换。考虑到我以前从来没有这方面的转换经验,这个速度是相当不错了。
2.9.1 Windows字符串函数
Windows 也提供了一组用于对Unicode 字符串进行操作的函数,表2 - 4 对它们进行了描述。
表2-4 对U n i c o d e 字符串进行操作的函数
函数 | 描述 |
lstrcat | 将一个字符串置于另一个字符串的结尾处 |
lstrcmp | 对两个字符串进行区分大小写的比较 |
lstrcmpi | 对两个字符串进行不区分大小写的比较 |
lstrcpy | 将一个字符串拷贝到内存中的另一个位置 |
lstrlen | 返回字符串的长度(按字符数来计量) |
有两个字符串函数,即lstrcmp 和lstrcmpi ,它们的行为特性与等价的C 运行期函数是不同的。C 运行期函数strcmp 、strcmpi 、wcscmp 和wcscmpi 只是对字符串中的代码点的值进行比较,这就是说,这些函数将忽略实际字符的含义,只是将第一个字符串中的每个字符的数值与第二个字符串中的字符的数值进行比较。而Windows 函数lstrcmp 和lstrcmpi 是作为对Windows 函数CompareString 的调用来实现的。
int CompareString(
LCID lcid,
DWORD fdwStyle,
PCWSTR pString1,
int cch1,
PCTSTR pString2,
int cch2);
该函数对两个U n i c o d e 字符串进行比较。CompareString 的第一个参数用于设定语言I D(L C I D ),这是个3 2 位值,用于标识一种特定的语言。CompareString 使用这个L C I D 来比较这两个字符串,方法是对照一种特定的语言来查看它们的字符的含义。这种操作方法比C 运行期函数简单地进行数值比较更有意义。
当lstrcmp 函数系列中的任何一个函数调用CompareString 时,该函数便将调用Windows 的GetThreadLocale 函数的结果作为第一个参数来传递:
LCID GetThreadLocale();
每次创建一个线程时,它就被赋予一种语言。函数将返回该线程的当前语言设置。CompareString 的第二个参数用于标识一些标志,这些标志用来修改该函数比较两个字符串时所用的方法。表2 - 5 显示了可以使用的标志。
当lstrcmp 调用CompareString 时,它传递0 作为fdwStyle 的参数。但是,当lstrcmpi 调用CompareString 时,它就传递NORM_IGNORECASE 。CompareString 的其余4个参数用于设定两个字符串和它们各自的长度。如果为cch1 参数传递-1 ,那么该函数将认为pString1 字符串是以0结尾,并计算该字符串的长度。对于pString2 字符串来说,参数cch2 的作用也是一样。
表2-5 CompareString 的标志及含义
标志 | 含义 |
N O R M _ I G N O R E C A S E | 忽略字母的大小写 |
N O R M _ I G N O R E K A N AT Y P E | 不区分平假名与片假名字符 |
N O R M _ I G N O R E N O N S PA C E | 忽略无间隔字符 |
N O R M _ I G N O R E S Y M B O L S | 忽略符号 |
N O R M _ I G N O R E W I D T H | 不区分单字节字符与作为双字节字符的同一个字符 |
S O RT _ S T R I N G S O RT | 将标点符号作为普通符号来处理 |
其他C 运行期函数没有为U n i c o d e 字符串的操作提供很好的支持。例如,tolower 和toupper 函数无法正确地转换带有重音符号的字符。为了弥补C 运行期库中的这些不足,必须调用下面这些Wi n d o w s 函数,以便转换U n i c o d e 字符串的大小写字母。这些函数也可以正确地用于A N S I字符串。
头两个函数:
PTSTR CharLower(PTSTR pszString);
PTSTR CharUpper(PTSTR pszString);
既可以转换单个字符,也可以转换以0 结尾的整个字符串。若要转换整个字符串,只需要传递字符串的地址即可。若要转换单个字符,必须像下面这样传递各个字符:
将单个字符转换成一个P T S T R ,便可调用该函数,将一个值传递给它,在这个值中,较低的1 6 位包含了该字符,较高的1 6 位包含0 。当该函数看到较高位是0 时,该函数就知道你想要转换单个字符,而不是整个字符串。返回的值是个3 2 位值,较低的1 6 位中是已经转换的字符。
TCHAR cLowerCase=CharLower((PTSTR szString[0]);
将单个字符转换成一个P T S T R ,便可调用该函数,将一个值传递给它,在这个值中,较低的1 6 位包含了该字符,较高的1 6 位包含0 。当该函数看到较高位是0 时,该函数就知道你想要转换单个字符,而不是整个字符串。返回的值是个3 2 位值,较低的1 6 位中是已经转换的字符。
下面两个函数与前面两个函数很相似,差别在于它们用于转换缓存中包含的字符(该缓存不必以0 结尾):
DWORD CharLowerBuff(
/* pointer to buffer containing characters to process */
PTSTR pszString,
/* number of bytes or characters to process */
DWORD cchLength
);
DWORD CharUpperBuff(
/* pointer to buffer containing characters to process */
LPTSTR lpsz,
/* number of characters to process */
DWORD cchLength
);
其他的C 运行期函数,如i s a l p h a 、i s l o w e r 和i s u p p e r ,返回一个值,指明某个字符是字母字符、小写字母还是大写字母。Windows API 提供了一些函数,也能返回这些信息,但是Wi n d o w s 函数也要考虑用户在控制面板中指定的语言:
BOOL IsCharAlpha(TCHAR ch); BOOL IsCharAlphaNumeric(TCHAR ch); BOOL IsCharLower(TCHAR ch); BOOL IsCharUpper(TCHAR ch);
printf 函数家族是要介绍的最后一组C 运行期函数。如果在定义了_UNICODE 的情况下编译你的源代码模块,那么printf 函数家族便希望所有字符和字符串参数代表U n i c o d e 字符和字符串。但是,如果在没有定义_ U N I C O D E 的情况下编译你的源代码模块,p r i n t f 函数家族便希望传递给它的所有字符和字符串都是A N S I 字符和字符串。
Microsoft 公司已经给C 运行期的p r i n t f 函数家族增加了一些特殊的域类型。其中有些域类型尚未被ANSI C 采用。新类型使你能够很容易地对A N S I 和U n i c o d e 字符和字符串进行混合和匹配。操作系统的wsprintf 函数也得到了增强。下面是一些例子(请注意大写S 和小写s 的使用):
char szA[100]; //An ANSI string buffer WCHAR szW[100]; //A Unicode string buffer
//Normal sprintf:all strings are ANSI
sprintf(szA, "%s","ANSI Str");
//Converts Unicode string to ANSI
sprintf(szA,"%S",L"Unicode Str");
//Normal swprintf:all strings are Unicode
wsprintf(szW,L"%s",L"Unicode Str");
//Converts ANSI string to Unicode
swprintf(szW,L"%S", "ANSI Str");
2.9.2 资源
当资源编译器对你的所有资源进行编译时,输出文件是资源的二进制文件。资源(字符串表、对话框模板和菜单等)中的字符串值总是写作U n i c o d e 字符串。在Windows 98 和Wi n d o w s 2 0 0 0 下,如果应用程序没有定义U N I C O D E 宏,那么系统就会进行内部转换。
例如,如果在编译源代码模块时没有定义UNICODE ,调用LoadString 实际上就是调用LoadStringA 函数。这时LoadStringA 就从你的资源中读取字符串,并将该字符串转换成A N S I 字符串。A N S I 形式的字符串将从该函数返回给你的应用程序。
2.9.3 确定文本是ANSI文本还是Unicode文本
对于许多用来打开文本文件和处理这些文件的应用程序(如编译器)来说,打开一个文件后,应用程序就能方便地确定该文本文件是包含A N S I 字符还是U n i c o d e 字符。IsTextUnicode 函数能够帮助进行这种区分:
DWORD IsTextUnicode(CONST PVOID pvBuffer, int cb,PINT pResult);
文本文件存在的问题是,它们的内容没有严格和明确的规则,因此很难确定该文件是包含ANSI 字符还是Unicode 字符。IsTextUnicode 使用一系列统计方法和定性方法,以便猜测缓存的内容。由于这不是一种确切的科学方法,因此IsTextUnicode 有可能返回不正确的结果。
第一个参数p v B u ff e r 用于标识要测试的缓存的地址。该数据是个无效指针,因为你不知道你拥有的是A N S I 字符数组还是U n i c o d e
字符数组。
第二个参数c b 用于设定p v B u ff e r 指向的字节数。同样,由于你不知道缓存中放的是什么,因此c b 是个字节数,而不是字符数。请注意,不必设定缓存的整个长度。当然,I s Te x t U n i c o d e能够测试的字节越多,得到的结果越准确。
第三个参数p R e s u l t 是个整数的地址,必须在调用I s Te x t U n i c o d e 之前对它进行初始化。对该整数进行初始化后,就可以指明你要I s Te x t U n i c o d e 执行哪些测试。也可以为该参数传递N U L L ,在这种情况下,I s Te x t U n i c o d e 将执行它能够进行的所有测试(详细说明请参见Platform SDK 文档)。
如果I s Te x t U n i c o d e 认为缓存包含U n i c o d e 文本,便返回T R U E ,否则返回FA L S E 。确实是这样,尽管M i c r o s o f t将该函数的原型规定为返回D W O R D ,但是它实际上返回一个布尔值。如果在p R e s u l t 参数指向的整数中必须进行特定的测试,该函数就会在返回之前设定整数中的信息位,以反映每个测试的结果。
Wi n d o w s 9 8 在Windows 98 下,I s Te x t U n i c o d e 函数没有有用的实现代码,它只是返回FA L S E 。调用G e t L a s t E r r o r 函数将返回E R R O R _ C A L L _ N O T _ I M P L E M E N T D 。
第1 7 章中的Flie Rev 示例应用程序演示了I s TextUnicode 函数的使用。
2.9.4 在Unicode与ANSI之间转换字符串
Windows 函数MultiByteToWideChar 用于将多字节字符串转换成宽字符串。下面显示了MultiByteToWideChar 函数。
int MultiByteToWideChar(
UINT CodePage, //code page
DWORD dwFlags, //character-type options
LPCSTR lpMultiByteStr, //address of string to map
int cchMultiByte, //number of bytes in string
LPWSTR lpWideCharStr, //address of wide-character buffer
int cchWideChar //size of buffer
);
u C o d e P a g e 参数用于标识一个与多字节字符串相关的代码页号。d w F l a g s 参数用于设定另一个控件,它可以用重音符号之类的区分标记来影响字符。这些标志通常并不使用,在d w F l a g s参数中传递0 。p M u l t i B y t e S t r 参数用于设定要转换的字符串,c c h M u l t i B y t e 参数用于指明该字符串的长度(按字节计算)。如果为c c h M u l t i B y t e 参数传递- 1 ,那么该函数用于确定源字符串的长度。
转换后产生的U n i c o d e 版本字符串将被写入内存中的缓存,其地址由p Wi d e C h a r S t r 参数指定。必须在c c h Wi d e C h a r 参数中设定该缓存的最大值(以字符为计量单位)。如果调用M u l t i B y t e To Wi d e C h a r ,给c c h Wi d e C h a r 参数传递0 ,那么该参数将不执行字符串的转换,而是返回为使转换取得成功所需要的缓存的值。一般来说,可以通过下列步骤将多字节字符串转换成U n i c o d e 等价字符串:
1) 调用M u l t i B y t e To Wi d e C h a r 函数,为p Wi d e C h a r S t r 参数传递N U L L ,为c c h Wi d e C h a r 参数传递0 。
2) 分配足够的内存块,用于存放转换后的U n i c o d e 字符串。该内存块的大小由前面对M u l t B y t e To Wi d e C h a r 的调用返回。
3) 再次调用M u l t i B y t e To Wi d e C h a r ,这次将缓存的地址作为p Wi d e C h a r S t r 参数来传递,并传递第一次调用M u l t i B y t e To Wi d e C h a r 时返回的缓存大小,作为c c h Wi d e c h a r 参数。
4. 使用转换后的字符串。
5) 释放U n i c o d e 字符串占用的内存块。
函数Wi d e C h a r To M u l t i B y t e 将宽字符串转换成等价的多字节字符串,如下所示: