第02章 U n i c o d e

         随着M i c r o s o f t公司的Wi n d o w s操作系统在全世界日益广泛的流行,对于软件开发人员来说,将目标瞄准国际上的各个不同市场,已经成为一个越来越重要的问题。美国的软件版本比国际版本提前 6个月推向市场,这曾经是个司空见惯的现象。但是,由于各国对 Wi n d o w s操作系统提供了越来越多的支持,因此就更加容易为国际市场生产各种应用软件,从而缩短了软件的美国版本与国际版本推出的时间间隔。
         Wi n d o w s操作系统始终不逾地提供各种支持,以帮助软件开发人员进行应用程序的本地化工作。应用软件可以从各种不同的函数中获得特定国家的信息,并可观察控制面板的设置,以确定用户的首选项。 Wi n d o w s甚至支持不同的字体,以适应应用的需要。
         之所以将这一章放在本书的开头,是因为考虑到 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的有关问题。

2.1 字符集
        软件的本地化要解决的真正问题,实际上就是如何来处理不同的字符集。多年来,许多人一直将文本串作为一系列单字节字符来进行编码,并在结尾处放上一个零。对于我们来说,这已经成了习惯。当调用s t r l e n函数时,它在以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之间,那么就必须观察下一个字节,才能确定字符串中的这个完整的字符。使用双字节字符集,对于程序员来说简直是个很大的难题,因为有些字符只有一个字节宽,而有些字符则是两个字节宽。
        如果只是调用 s t r l e n函数,那么你无法真正了解字符串中究竟有多少字符,它只能告诉你到达结尾的0之前有多少个字节。 A N S I的C运行期库中没有配备相应的函数,使你能够对双字节字符集进行操作。但是, Microsoft Visual C++的运行期库却包含许多函数,如 _ m b s l e n ,它可以用来操作多字节(既包括单字节也包括双字节)字符串。
         为了帮助你对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字符的第一个字节,则返回

          尽管这些函数使得我们对 D B C S的操作更容易,但还需要,一个更好的方法让我们来看看U n i c o d e。

2.1.2 Unicode:宽字节字符集
         U n i c o d e是A p p l e和X e r o x公司于1 9 8 8年建立的一个技术标准。 1 9 9 1年,成立了一个集团机构负责U n i c o d e的开发和推广应用。该集团由 A p p l e、 C o m p a q、 H P、 I B M、 M i c r o s o f t、 O r a c l e、Silicon Graphics, Inc.、 S y b a s e、 U n i s y s和X e r o x等公司组成(若要了解该集团的全部成员,请通过网址w w w. U n i c o d e . o rg查找)。该集团负责维护U n i c o d e标准。 U n i c o d e的完整描述可以参阅A d d i s o n We s l e y出版的《Unicode Standard》一书(该书可以通过网址w w w. U n i c o d e . o rg订购)。
         U n i c o d e提供了一种简单而又一致的表示字符串的方法。 U n i c o d e字符串中的所有字符都是1 6位的(两个字节)。它没有专门的字节来指明下一个字节是属于同一个字符的组成部分,还是一个新字符。这意味着你只需要对指针进行递增或递减,就可以遍历字符串中的各个字符,不再需要调用C h a r N e x t、 C h a r P r e v和I s D B C S L e a d B y t e之类的函数。
         由于U n i c o d e用一个1 6位的值来表示每个字符,因此总共可以得到 65 000个字符,这样,它就能够对世界各国的书面文字中的所有字符进行编码,远远超过了单字节字符集的 2 5 6个字符的数目。
         目前,已经为阿拉伯文、中文拼音、西里尔字母(俄文)、希腊文、西伯莱文、日文、韩文和拉丁文(英文)字母定义了 U n i c o d e代码点 。(代码点是字符集中符号的位置。)这些字符集中还包含了大量的标点符号、数学符号、技术符号、箭头、装饰标志、区分标志和其他许多字符。如果将所有这些字母和符号加在一起,总计约达 3 5 0 0 0个不同的代码点,这样,总计 65 000多个代码点中,大约还有一
半可供将来扩充时使用。
          这65 536个字符可以分成不同的区域。表 2-2 显示了这样的区域的一部分以及分配给这些
区域的字符。

                                                                                                 表2-2 区域字符

1 6位代码
 
字 符16 位 代 码字 符
0 0 0 0 - 0 0 7 FA S C I I0 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 为什么使用U n i c o d e
        当开发应用程序时,当然应该考虑利用 U n i c o d e的优点。即使现在你不打算对应用程序进行本地化,开发时将U n i c o d e放在心上,肯定可以简化将来的代码转换工作。此外, U n i c o d e还具备下列功能:
       • 可以很容易地在不同语言之间进行数据交换。
       • 使你能够分配支持所有语言的单个二进制 . e x e文件或D L L文件。
       • 提高应用程序的运行效率(本章后面还要详细介绍)。

2.3 Windows 2000与U n i c o d e
         Windows 2000是使用U n i c o d e从头进行开发的,用于创建窗口、显示文本、进行字符串操作等的所有核心函数都需要 U n i c o d e字符串。如果调用任何一个 Wi n d o w s函数并给它传递一个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字符串,然后将结果返回给你的应用程序。所有这些转换操作都是在你看不见的情况下发生的。当然,进行这些字符串的转换需要占用系统的时间和内存。
         例如,如果调用C r e a t e Wi n d o w E x函数,并传递类名字和窗口标题文本的非 U n i c o d e字符串,那么C r e a t e Wi n d o w E x必须分配内存块(在你的进程的默认堆中),将非U n i c o d e字符串转换成U n i c o d e字符串,并将结果存储在分配到的内存块中,然后调用 U n i c o d e版本的C r e a t e Wi n d o w E x函数。
         对于用字符串填入缓存的函数来说,系统必须首先将 U n i c o d e字符串转换成非U n i c o d e字符串,然后你的应用程序才能处理该字符串。由于系统必须执行所有这些转换操作,因此你的应用程序需要更多的内存,并且运行的速度比较慢。通过从头开始用 U n i c o d e来开发应用程序,就能够使你的应用程序更加有效地运行。


2.4 Windows 98与U n i c o d e
        Windows 98不是一种全新的操作系统。它继承了 1 6位Wi n d o w s操作系统的特性,它不是用来处理U n i c o d e的。如果要增加对U n i c o d e的支持,其工作量非常大,因此在该产品的特性列表中没有包括这个支持项目。由于这个原因, Windows 98像它的前任产品一样,几乎都是使用A N S I字符串来进行所有的内部操作的。
        仍然可以编写用于处理U n i c o d e字符和字符串的Wi n d o w s应用程序,不过,使用Wi n d o w s函数要难得多。例如,如果想要调用 C r e a t e Wi n d o w E x函数并将A N S I字符串传递给它,这个调用的速度非常快,不需要从你进程的默认堆栈中分配缓存,也不需要进行字符串转换。但是,如果想要调用C r e a t e Wi n d o w E x函数并将U n i c o d e字符串传递给它,就必须明确分配缓存,并调用函数,以便执行从 U n i c o d e到A N S I字符串的转换操作。然后可以调用 C r e a t e Wi n d o w E x,传递A N S I字符串。当C r e a t e Wi n d o w E x函数返回时,就能释放临时缓存。这比使用 Windows 2000上的U n i c o d e要麻烦得多。本章的后面要介绍如何在 Windows 98下进行这些转换。

虽然大多数U n i c o d e函数在Windows 98中不起任何作用,但是仍有少数 U n i c o d e函数确实非常有用。这些函数是:
■E n u m R e s o u r c e L a n g u a g e s W■ G e t Te x t E x t e n t P o i n t 3 2 W
■ E n u m R e s o u r c e N a m e s W■ G e t Te x t E x t e n t P o i n t W
■ E n u m R e s o u r c e Ty p e s W■ L s t r l e n W
■ E x t Te x t O u t W■M e s s a g e B o x E xW
■ F i n d R e s o u r c e W■ M e s s a g e B o x W
■ F i n d R e s o u r c e E x W■ Te x t O u t W
■ G e t C h a r Wi d t h W■ Wi d e C h a r To M u l t i B y t e
■ G e t C o m m a n d L i n e W■ M u l t iBy t e To Wi d e C h a r

        可惜的是,这些函数中有许多函数在Windows 98中会出现各种各样的错误。有些函数无法使用某些字体,有些函数会破坏内存堆栈,有些函数会使打印机驱动程序崩溃,等等。如果要使用这些函数,必须对它们进行大量的测试。即使这样,可能仍然无法解决问题。因此必须向用户说明这些情况。

2.5 Windows CE与U n i c o d e
        Windows CE操作系统是为小型设备开发的,这些设备的内存很小,并且不带磁盘存储器。你可能认为,由于 M i c r o s o f t公司的主要目标是建立一种尽可能小的操作系统,因此它会使用A N S I作为自己的字符集。但是 M i c r o s o f t公司并非鼠目寸光,他们懂得, Windows CE的设备要在世界各地销售,他们希望降低软件开发成本,这样就能更加容易地开发应用程序。为此,Windows CE 本身就是使用U n i c o d e的一种操作系统。
        但是,为了使Windows CE尽量做得小一些, M i c r o s o f t公司决定完全不支持ANSI Wi n d o w s函数。因此,如果要为Windows CE开发应用程序,必须懂得 U n i c o d e,并且在整个应用程序中使用U n i c o d e。


2.6 需要注意的问题
         下面让我们进一步明确一下“M i c r o s o f t公司对U n i c o d e支持的情况”:
        • Windows 2000既支持U n i c o d e,也支持A N S I,因此可以为任意一种开发应用程序。
        • Windows 98只支持A N S I,只能为A N S I开发应用程序。
        • Windows CE只支持U n i c o d e,只能为U n i c o d e开发应用程序。
        虽然M i c r o s o f t公司试图让软件开发人员能够非常容易地开发在这 3种平台上运行的软件,但是U n i c o d e与A N S I之间的差异使得事情变得困难起来,并且这种差异通常是我遇到的最大的问题之一。请不要误解, M i c r o s o f t公司坚定地支持U n i c o d e,并且我也坚决鼓励你使用它。不过你应该懂得,你可能遇到一些问题,需要一定的时间来解决这些问题。建议你尽可能使用U n i c o d e。如果运行Windows 98 ,那么只有在必要时才需转换到A N S I。
        不过,还有另一个小问题你应该了解,那就是 C O M。


2.7 对C O M的简单说明
         当M i c r o s o f t公司将C O M从1 6位Wi n d o w s转换成Wi n 3 2时,公司作出了一个决定,即需要字符串的所有C O M接口方法都只能接受U n i c o d e字符串。这是个了不起的决定,因为 C O M通常用于使不同的组件能够互相进行通信,而 U n i c o d e则是传递字符串的最佳手段。
        如果你为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要求使用U n i c o d e字符串,而操作系统的大多数函数要求使用 A N S I字符串。那是多么难办的事情啊!我曾经从事过若干个项目的开发,在这些项目中,我编写了许多代码,仅仅是为了来回进行字符串的转换。
 

2.8 如何编写U n i c o d e源代码
         M i c r o s o f t公司为U n i c o d e设计了Windows API,这样,可以尽量减少对你的代码的影响。实际上,你可以编写单个源代码文件,以便使用或者不使用 U n i c o d e来对它进行编译。只需要定义两个宏(U N I C O D E和_ U N I C O D E),就可以修改然后重新编译该源文件。


2.8.1 C运行期库对U n i c o d e的支持
         为了利用 U n i c o d e字符串,定义了一些数据类型。标准的 C头文件S t r i n g . h已经作了修改,以便定义一个名字为w c h a r _ t的数据类型,它是一个U n i c o d e字符的数据类型:

typedef unsigned short wchar_t;

        例如,如果想要创建一个缓存,用于存放最多为 9 9个字符的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 *str1, const char *str2 );
wchar_t *wcscat( wchar_t  *str1, const wchar_t *str2 );

                                          清单2-1 标准的ANSI C字符串函数和它们的等价U n i c o d e函数

char *strchr( const char *str, int ch );
wchar_t *wcschr( const wchar_t *str, wchar_t ch );

int strcmp( const char *str1, const char *str2 );
int wcscmp( const wchar_t *str1, const wchar_t *str2 );

char *strcpy( char *to, const char *from );
wchar_t *wcscpy( wchar_t *to, const wchar_t *from );

size_t strlen( char *str );
size_t wcslen( wchar_t *str );


       请注意,所有的U n i c o d e函数均以w c s开头, w c s是宽字符串的英文缩写。若要调用 U n i c o d e函数,只需用前缀w c s来取代 A N S I字符串函数的前缀 s t r即可。
      注意 大多数软件开发人员可能已经不记得这样一个非常重要的问题了,那就是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进行编译的单个源代码文件。若要建立双重功能,必须包含 T C h a r. h文件,而不是包含S t r i n g . h文件
       T C h a r. h文件的唯一作用是帮助创建 A N S I / U n i c o d e通用源代码文件。它包含你应该用在源代码中的一组宏,而不应该直接调用 s t r函数或者 w c s函数。如果在编译源代码文件时定义了_ U N I C O D E,这些宏就会引用w c s这组函数。如果没有定义 _ U N I C O D E,那么这些宏将引用 s t r这组宏。
       例如,在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进行编译的源代码。
       但是,除了使用这些宏之外,还有一些操作是必须进行的。 T C h a r. h文件包含了另外一些宏。
       若要定义一个A N S I / U n i c o d e通用的字符串数组,请使用下面的 T C H A R数据类型。如果定
义了_ U N I C O D E, T C H A R将声明为下面的形式:

typedef wchar_t TCHAR;

         如果没有定义_ U N I C O D E,则T C H A R将声明为下面的形式:

typedef char TCHAR;

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

TCHAR szString[100];

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

TCHAR  *szError = "Error";

       不过上面这行代码存在一个问题。按照默认设置, M i c r o s o f t公司的C + +编译器能够编译所有的字符串,就像它们是 A N S I 字符串,而不是 U n i c o d e 字符串。因此,如果没有定义_ U N I C O D E,该编译器将能正确地编译这一行代码。但是,如果定义了 _ U N I C O D E,就会产生一个错误。若要生成一个 U n i c o d e字符串而不是A N S I字符串,必须将该代码行改写为下面的样子:

TCHAR  *szError = L"Error";

     字符串(literal string)前面的大写字母L,用于告诉编译器该字符串应该作为 U n i c o d e字符串来编译。当编译器将字符串置于程序的数据部分中时,它在每个字符之间分散插入零字节。这种变更带来的问题是,现在只有当定义了 _ U N I C O D E时,程序才能成功地进行编译。我们需要另一个宏,以便有选择地在字符串的前面加上大写字母 L。这项工作由 _ T E X T宏来完成,_ T E X T宏也在T C h a r. 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 Wi n d o w s定义的U n i c o d e数据类型
         Wi n d o w s头文件定义了表2 - 3列出的数据类型。
                                                                          表2-3 Uincode 数据类型

数 据 类 型说 明
W C H A RU n i c o d e字符
P W S T R指向U n i c o d e字符串的指针
P C W S T R指向一个恒定的U n i c o d e字符串的指针

         这些数据类型是指 U n i c o d e字符和字符串。 Wi n d o w s头文件也定义了 A N S I / U n i c o d e通用数据类型P T S T R和P C T S T R。这些数据类型既可以指 A N S I字符串,也可以指 U n i c o d e字符串,这取决于当编译程序模块时是否定义了 U N I C O D E宏。
         请注意,这里的 U N I C O D E宏没有前置的下划线。 _ U N I C O D E宏用于C运行期头文件,而U N I C O D E宏则用于Wi n d o w s头文件。当编译源代码模块时,通常必须同时定义这两个宏。

2.8.3 Wi n d o w s中的U n i c o d e函数和A N S I函数
         前面已经讲过,有两个函数称为 C r e a t e Wi n d o w E x,一个C r e a t e Wi n d o w E x接受U n i c o d e字符串,另一个C r e a t e Wi n d o w E x接受A N S I字符串。情况确实如此,不过,这两个函数的原型实际上是下面的样子:
 

HWND WINAPI CreateWindowExW(
DWORD DdwExStyle,        //窗口的扩展风格
PCWSTR lpClassName,    //指向注册类名的指针
PCWSTR lpWindowName,   //指向窗口名称的指针
DWORD dwStyle,          //窗口风格
int x,                  //窗口的水平位置
int y,                  //窗口的垂直位置
int nWidth,             //窗口的宽度
int nHeight,            //窗口的高度
HWND hWndParent,        //父窗口的句柄
HMENU hMenu,            //菜单的句柄或是子窗口的标识符
HINSTANCE hInstance,    //应用程序实例的句柄
LPVOID lpParam          //指向窗口的创建数据
);


HWND WINAPI CreateWindowExW(
DWORD DdwExStyle,        //窗口的扩展风格
PCTSTR lpClassName,    //指向注册类名的指针
PCTSTR lpWindowName,   //指向窗口名称的指针
DWORD dwStyle,          //窗口风格
int x,                  //窗口的水平位置
int y,                  //窗口的垂直位置
int nWidth,             //窗口的宽度
int nHeight,            //窗口的高度
HWND hWndParent,        //父窗口的句柄
HMENU hMenu,            //菜单的句柄或是子窗口的标识符
HINSTANCE hInstance,    //应用程序实例的句柄
LPVOID lpParam          //指向窗口的创建数据
);

         C r e a t e Wi n d o w E x W是接受U n i c o d e字符串的函数版本。函数名结尾处的大写字母W是英文w i d e(宽)的缩写。每个U n i c o d e字符的长度是1 6位,因此,它们常常称为宽字符。 C r e a t e Wi n d o w E x A的结尾处的大写字母A表示该函数可以接受A N S I字符串。
         但是,在我们的代码中,通常只包含了对 C r e a t e Wi n d o w E x的调用,而不是直接调用C r e a t e Wi n d o w E x W或者C r e a t e Wi n d o w E x A。在Wi n U s e r. h文件中, C r e a t e Wi n d o w E x实际上是定义为下面这种形式的一个宏:
 

当编译源代码模块时, U N I C O D E 是否已经作了定义,将决定你调用的是哪个
C r e a t e Wi n d o w E x版本。当转用一个 1 6位的Wi n d o w s应用程序时,你在编译期间可能没有定义
U N I C O D E。对C r e a t e Wi n d o w E x函数的任何调用都会将该宏扩展为对 C r e a t e Wi n d o w E x A的调用,
即对C r e a t e Wi n d o w E x的A N S I版本的调用。由于 1 6位Wi n d o w s只提供了 C r e a t e Wi n d o w s E x的
A N S I版本,因此可以比较容易地转用它的应用程序。
在Windows 2000下, M i c r o s o f t的C r e a t e Wi n d o w E x A源代码只不过是一个形实替换程序层或
翻译层,用于分配内存,以便将 A N S I字符串转换成 U n i c o d e字符串。该代码然后调用 C r e a t e
Wi n d o w E x W,并传递转换后的字符串。当 C r e a t e Wi n d o w E x W返回时, C r e a t e Wi n d o w E x A便释
放它的内存缓存,并将窗口句柄返回给你。
如果要创建其他软件开发人员将要使用的动态链接库(D L L),请考虑使用下面的方法。
在D L L中提供两个输出函数。一个是A N S I版本,另一个是U n i c o d e版本。在A N S I版本中,只需
要分配内存,执行必要的字符串转换,并调用该函数的 U n i c o d e版本(本章后面部分介绍这个
进程)。
在Windows 98下, M i c r o s o f t的C r e a t e Wi n d o w E x A源代码是执行操作的函数。 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中的某些函数,比如Wi n E x e c和O p e n F i l e等,只是为了实现与1 6位Wi n d o w s程
序的向后兼容而存在,因此,应该避免使用。应该使用对 C r e a t e P r o c e s s和C r e a t e F i l e函数的调用
来取代对Wi n E x e c和O p e n F i l e函数的调用。从系统内部来讲,老的函数完全可以调用新的函数。
老的函数存在的一个大问题是,它们不接受 U n i c o d e字符串。当调用这些函数时,必须传递
A N S I字符串。另一方面,所有新的和未过时的函数在 Windows 2000 中都同时拥有 A N S I和U n i c o d e两个版本。
2.8.4 Wi n d o w s字符串函数
Wi n d o w s还提供了一组范围很广的字符串操作函数。这些函数与 C运行期字符串函数(如
s t r c p y和w c s c p y)很相似。但是该操作系统函数是操作系统的一个组成部分,操作系统的许多
组件都使用这些函数,而不使用 C运行期库。建议最好使用操作系统函数,而不要使用 C运行
期字符串函数。这将有助于稍稍提高你的应用程序的运行性能,因为操作系统字符串函数常常
被大型应用程序比如操作系统的外壳进程 E x p l o r e r. e x e所使用。由于这些函数使用得很多,因
此,在你的应用程序运行时,它们可能已经被装入 R A M。
若要使用这些函数,系统必须运行 Windows 2000 或Windows 98 。如果安装了 I n t e r n e t
Explorer 4.0或更新的版本,也可以在较早的 Wi n d o w s版本中获得这些函数。
在经典的操作系统函数样式中,操作系统字符串函数名既包含大写字母,也包含小写字母,
它的形式类似这个样子: S t r C a t、 S t r C h r、 S t r C m p和S t r C p y等。若要使用这些函数,必须加上
S h l WA p i . h头文件。另外,如前所述,这些字符串函数既有 A N S I版本,也有U n i c o d e版本,例
如S t r C a t A和S t r C a t W。由于这些函数属于操作系统函数,因此,当创建应用程序时,如果定义
了U N I C O D E(不带前置下划线),那么它们的符号将扩展为宽字符版本。
2.9 成为符合A N S I和U n i c o d e的应用程序
即使你不打算立即使用U n i c o d e,最好也应该着手将你的应用程序转换成符合 U n i c o d e的应
用程序。下面是应该遵循的一些基本原则:
• 将文本串视为字符数组,而不是c h a r s数组或字节数组。
• 将通用数据类型(如T C H A R和P T S T R)用于文本字符和字符串。
• 将显式数据类型(如B Y T E和P B Y T E)用于字节、字节指针和数据缓存。
• 将T E X T宏用于原义字符和字符串。
• 执行全局性替换(例如用P T S T R替换P S T R)。
• 修改字符串运算问题。例如函数通常希望你在字符中传递一个缓存的大小,而不是字节。
这意味着你不应该传递 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 ( T C H A R )。另外,
如果需要为字符串分配一个内存块,并且拥有该字符串中的字符数目,那么请记住要按字节来
分配内存。这就是说,应该调用 malloc(nCharacters *sizeof(TCHAR)), 而不是调用 m a l l o c
( n C h a r a c t e r s )。在上面所说的所有原则中,这是最难记住的一条原则,如果操作错误,编译器
将不发出任何警告。
当我为本书的第一版编写示例程序时,我编写的原始程序只能编译为 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 Wi n d o w s字符串函数
Wi n d o w s也提供了一组用于对 U n i c o d e字符串进行操作的函数,表2 - 4对它们进行了描述。
表2-4 对U n i c o d e字符串进行操作的函数
函 数 描 述
l s t r c a t 将一个字符串置于另一个字符串的结尾处
l s t r c m p 对两个字符串进行区分大小写的比较
l s t r c m p i 对两个字符串进行不区分大小写的比较
l s t r c p y 将一个字符串拷贝到内存中的另一个位置
l s t r l e n 返回字符串的长度(按字符数来计量)

这些函数是作为宏来实现的,这些宏既可以调用函数的 U n i c o d e版本,也可以调用函数的
A N S I版本,这要根据编译源代码模块时是否已经定义了 U N I C O D E而定。例如,如果没有定义
U N I C O D E, l s t r c a t函数将扩展为l s t r c a t A。如果定义了U N I C O D E, l s t r c a t将扩展为l s t r c a t W。
有两个字符串函数,即l s t r c m p和l s t r c m p i,它们的行为特性与等价的C运行期函数是不同的。
C运行期函数s t r c m p、 s t r c m p i、 w c s c m p和w c s c m p i只是对字符串中的代码点的值进行比较,这
就是说,这些函数将忽略实际字符的含义,只是将第一个字符串中的每个字符的数值与第二个
字符串中的字符的数值进行比较。而 Wi n d o w s函数l s t r c m p和l s t r c m p i是作为对 Wi n d o w s函数
C o m p a r e S t r i n g的调用来实现的。
该函数对两个 U n i c o d e字符串进行比较。 C o m p a r e S t r i n g的第一个参数用于设定语言 I D
(L C I D),这是个3 2位值,用于标识一种特定的语言。 C o m p a r e S t r i n g使用这个L C I D来比较这两
个字符串,方法是对照一种特定的语言来查看它们的字符的含义。这种操作方法比 C运行期函
数简单地进行数值比较更有意义。
当l s t r c m p函数系列中的任何一个函数调用 C o m p a r e S t r i n g时,该函数便将调用 Wi n d o w s的
G e t T h r e a d S t r i n g函数的结果作为第一个参数来传递:
每次创建一个线程时,它就被赋予一种语言。函数将返回该线程的当前语言设置。
C o m p a r e S t r i n g的第二个参数用于标识一些标志,这些标志用来修改该函数比较两个字符
串时所用的方法。表2 - 5显示了可以使用的标志。
表2-5 Compare String 的标志及含义
标 志 含 义
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 将标点符号作为普通符号来处理
当l s t r c m p调用C o m p a r e S t r i n g时,它传递 0作为f d w S t y l e的参数。但是,当 l s t r c m p i调用
C o m p a r e S t r i n g时,它就传递N O R M _ I G N O R E C A S E。 C o m p a r e S t r i n g的其余4个参数用于设定两个字符串和它们各自的长度。如果为 c c h 1参数传递- 1,那么该函数将认为p S t r i n g 1字符串是以0
结尾,并计算该字符串的长度。对于 p S t r i n g 2字符串来说,参数c c h 2的作用也是一样。
其他C运行期函数没有为 U n i c o d e字符串的操作提供很好的支持。例如, t o l o w e r和t o u p p e r
函数无法正确地转换带有重音符号的字符。为了弥补 C运行期库中的这些不足,必须调用下面
这些Wi n d o w s函数,以便转换 U n i c o d e字符串的大小写字母。这些函数也可以正确地用于 A N S I
字符串。
头两个函数:

既可以转换单个字符,也可以转换以 0结尾的整个字符串。若要转换整个字符串,只需要
传递字符串的地址即可。若要转换单个字符,必须像下面这样传递各个字符:
将单个字符转换成一个P T S T R,便可调用该函数,将一个值传递给它,在这个值中,较低
的1 6位包含了该字符,较高的1 6位包含0。当该函数看到较高位是0时,该函数就知道你想要转
换单个字符,而不是整个字符串。返回的值是个 3 2位值,较低的1 6位中是已经转换的字符。
下面两个函数与前面两个函数很相似,差别在于它们用于转换缓存中包含的字符(该缓存
不必以0结尾):
其他的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函数也要考虑用户在控制面板中指定的语言:
p r i n t f函数家族是要介绍的最后一组 C运行期函数。如果在定义了 _ U N I C O D E的情况下编译
你的源代码模块,那么p r i n t f函数家族便希望所有字符和字符串参数代表 U n i c o d e字符和字符串。
但是,如果在没有定义 _ U N I C O D E的情况下编译你的源代码模块, p r i n t f函数家族便希望传递
给它的所有字符和字符串都是A N S I字符和字符串。
M i c r o s o f t公司已经给C运行期的p r i n t f函数家族增加了一些特殊的域类型。其中有些域类型
尚未被ANSI C采用。新类型使你能够很容易地对A N S I和U n i c o d e字符和字符串进行混合和匹配。
操作系统的w s p r i n t f函数也得到了增强。下面是一些例子(请注意大写 S和小写s的使用):
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宏,那么系统就会进行内部转换。
例如,如果在编译源代码模块时没有定义 U N I C O D E,调用 L o a d S t r i n g实际上就是调用
L o a d S t r i n g A函数。这时L o a d S t r i n g A就从你的资源中读取字符串,并将该字符串转换成 A N S I字
符串。 A N S I形式的字符串将从该函数返回给你的应用程序。
2.9.3 确定文本是A N S I文本还是U n i c o d e文本
到现在为止, U n i c o d e文本文件仍然非常少。实际上, M i c r o s o f t公司自己的大多数产品并
没有配备任何 U n i c o d e文本文件。但是预计将来这种情况是会改变的(尽管这需要一个很长的
过程)。当然, Windows 2000的N o t e p a d (记事本)应用程序允许你既能打开 U n i c o d e文件,也能
打开A N S I文件,并且可以创建这些文件。图 2 - 1显示了N o t e p a d的Save As(文件另存为)对话
框。请注意可以用不同的方法来保存文本文件。
图2-1 Windows 2000 Notepad的File Save As对话框
对于许多用来打开文本文件和处理这些文件的应用程序(如编译器)来说,打开一个文件
后,应用程序就能方便地确定该文本文件是包含 A N S I字符还是U n i c o d e字符。 I s Te x t U n i c o d e函
数能够帮助进行这种区分:
文本文件存在的问题是,它们的内容没有严格和明确的规则,因此很难确定该文件是包含A N S I字符还是U n i c o d e字符。 I s Te x t U n i c o d e使用一系列统计方法和定性方法,以便猜测缓存的
内容。由于这不是一种确切的科学方法,因此 I s Te x t U n i c o d e有可能返回不正确的结果。
第一个参数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 在U n i c o d e与A N S I之间转换字符串
Wi n d o w s函数M u l t i B y t e To Wi d e C h a r用于将多字节字符串转换成宽字符串。下面显示了
M u l t i B y t e To Wi d e C h a r函数。
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将宽字符串转换成等价的多字节字符串,如下所示:
该函数与M u l t i B i t e To Wi d e C h a r函数相似。同样, 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 Wi d e C h a r S t r参数用于设定要转换的字符串的内存地址, c c h Wi d e C h a r参数用于指明该字符串
的长度(用字符数来计量)。如果你为c c h Wi d e C h a r参数传递- 1,那么该函数用于确定源字符串的
长度。
转换产生的多字节版本的字符串被写入由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
参数中设定该缓存的最大值(用字节来计量)。如果传递 0作为Wi d e C h a r To M u l t i B y t e函数的
c c h M u l t i B y t e参数,那么该函数将返回目标缓存需要的大小值。通常可以使用将多字节字符串
转换成宽字节字符串时介绍的一系列类似的事件,将宽字节字符串转换成多字节字符串。
你会发现, Wi d e C h a r To M u l t i B y t e函数接受的参数比 M u l t i B y t e To Wi d e C h a r函数要多2个,
即p D e f a u l t C h a r和p f U s e d D e f a u l t C h a r。只有当Wi d e C h a r To M u l t i B y t e函数遇到一个宽字节字符,
而该字符在u C o d e P a g e参数标识的代码页中并没有它的表示法时, Wi d e C h a r To M u l t i B y t e函数才
使用这两个参数。如果宽字节字符不能被转换,该函数便使用 p D e f a u l t C h a r参数指向的字符。
如果该参数是 N U L L(这是大多数情况下的参数值),那么该函数使用系统的默认字符。该默
认字符通常是个问号。这对于文件名来说是危险的,因为问号是个通配符。
p f U s e d D e f a u l t C h a r参数指向一个布尔变量,如果宽字符串中至少有一个字符不能转换成等
价多字节字符,那么函数就将该变量置为 T R U E。如果所有字符均被成功地转换,那么该函数
就将该变量置为FA L S E。当函数返回以便检查宽字节字符串是否被成功地转换后,可以测试该
变量。同样,通常为该测试传递 N U L L。
关于如何使用这些函数的详细说明,请参见 Platform SDK文档。
如果使用这两个函数,就可以很容易创建这些函数的 U n i c o d e版本和A N S I版本。例如,你
可能有一个动态链接库,它包含一个函数,能够转换字符串中的所有字符。可以像下面这样编
写该函数的U n i c o d e版本:

你可以编写该函数的 A N S I版本以便该函数根本不执行转换字符串的实际操作。你也可以
编写该函数的A N S I版本,以便该函数它将 A N S I字符串转换成U n i c o d e字符串,将U n i c o d e字符
串传递给S t r i n g R e v e r s e W函数,然后将转换后的字符串重新转换成 A N S I字符串。该函数类似下
面的样子:
最后,在用动态链接库分配的头文件中,可以像下面这样建立这两个函数的原型:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值