浅谈文字编码和Unicode
来源:互联网 发布:电驴下载软件 编辑:程序博客网 时间:2024/04/30 08:09
source:http://www.fmddlmyy.cn/text16.html
浅谈文字编码和Unicode(上)
我曾经写过一篇《谈谈Unicode编码,简要解释UCS、UTF、BMP、BOM等名词》 (以 下简称《谈谈Unicode编码》),在网上流传较广,我也收到不少朋友的反馈。本文探讨《谈谈Unicode编码》中未介绍或介绍较少的代码页、 Surrogates等问题,补充一些Unicode资料,顺带介绍一下我最近编写的一个Unicode工具:UniToy。本文虽然是前文的补充,但在 写作上尽量做到独立成篇。
标题中的“浅谈”是对自己的要求,我希望文字能尽量浅显易懂。但本文还是假设读者知道字节、16进制,了解《谈谈Unicode编码》中介绍过的字节序和Unicode的基本概念。
0 UniToy
UniToy是我编写的一个小工具。通过UniToy,我们可以全方位、多角度地查看Unicode,了解Unicode和语言、代码页的关系,完成一些文字编码的相关工作。本文的一些内容是通过UniToy演示的。大家可以从我的网站(www.fmddlmyy.cn )下载UniToy的演示版本 。
1 文字的显示
1.1 发生了什么?
我们首先以Windows为例来看看文字显示过程中发生了什么。用记事本打开一个文本文件,可以看到文件包含的文字:
如果我们用UltraEdit或Hex Workshop查看这个文件的16进制数据,可以看到:
我们看到:文件“例子GBK.txt”有10个字节,依次是“D7 D6 B7 FB BA CD B1 E0 C2 EB”,这就是记事本从文件中读到的内容。记事本是用来打开文本文件的,所以它会调用Windows的文本显示函数将读到的数据作为文本显示。 Windows首先将文本数据转换到它内部使用的编码格式:Unicode,然后按照文本的Unicode去字体文件中查找字体图像,最后将图像显示到窗 口上。 总结一下前面的分析,文字的显示应该是这样的:
- 步骤1:文字首先以某种编码保存在文件中。
- 步骤2:Windows将文件中的文字编码映射到Unicode。
- 步骤3:Windows按照Unicode在字体文件中查找字体图像,画到窗口上。
所谓编码就是用数字表示字符,例如用D7D6表示“字”。当然,编码 还意味着约定 ,即大家都认可。从《谈谈 Unicode编码》中,我们知道Unicode也是一种文字编码,它的特殊性在于它是由国际组织设计,可以容纳全世界所有语言文字。而我们平常使用的文 字编码通常是针对一个区域的语言、文字设计,只支持特定的语言文字。例如:在上面的例子中,文件“例子GBK.txt”采用的就是GBK编码。
如果上述3个步骤中任何一步发生了错误,文字就不能被正确显示,例如:
错误1:如果弄错了编码,例如将Big5编码的文字当作GBK编码,就会出现乱码。
错误2:如果从特定编码到Unicode的映射发生错误,例如文本数据中出现该编码方案未定义的字符,Windows就会使用缺省字符,通常是?。
- 如果当前字体不支持要显示的字符,Windows就会显示字体文件中的缺省图像:空白或方格。
在Unicode被广泛使用前,有多少种语言、文字,就可能有多少种文字编码方案。一种文字也可能有多种编码方案。那么我们怎么确定文本数据采用了什么编码?
1.2 采用了哪种编码?
按照惯例,文本文件中的数据都是文本编码,那么它怎么表明自己的编码格式?在记事本的“打开”对话框上:
我们可以看到记事本支持4种编码格式:ANSI、Unicode、Unicode big endian、UTF-8。如果读者看过《谈谈Unicode编码》,对Unicode、Unicode big endian、UTF-8应该不会陌生,其实它们更准确的名称应该是UTF-16LE(Little Endian)、UTF-16BE(Big Endian)和UTF-8,它们是基于Unicode的不同编码方案。
在《谈谈Unicode编码》中介绍过,Windows通过在文本文件开头增加一些特殊字节(BOM)来区分上述3种编码,并将没有BOM的文本数据按照ANSI代码页处理。那么什么是代码页,什么是ANSI代码页?
2 代码页和字符集
2.1 Windows的代码页
2.1.1 代码页
代码页(Code Page)是个古老的专业术语,据说是IBM公司首先使用的。代码页和字符集的含义基本相同,代码页规定了适用于特定地区的字符集合,和这些字符的编码。可以将代码页理解为字符和字节数据的映射表。
Windows为自己支持的代码页都编了一个号码。例如代码页936就是简体中文 GBK,代码页950就是繁体中文 Big5。代码页的概念比较简单,就是一个字符编码方案。但要说清楚Windows的ANSI代码页,就要从Windows的区域(Locale)说起 了。
2.1.2 区域和ANSI代码页
微软为了适应世界上不同地区用户的文化背景和生活习惯,在Windows中设计了区域(Locale)设置的功能。Local是指特定于某个国家或 地区的一组设定,包括代码页,数字、货币、时间和日期的格式等。在Windows内部,其实有两个Locale设置:系统Locale和用户 Locale。系统Locale决定代码页,用户Locale决定数字、货币、时间和日期的格式。我们可以在控制面板的“区域和语言选项”中设置系统 Locale和用户Locale:
每个Locale都有一个对应的代码页。Locale和代码页的对应关系,大家可以参阅我的另一篇文章《谈谈Windows程序中的字符编码》 的附录1。系统Locale对应的代码页被作为Windows的默认代码页。在没有文本编码信息时,Windows按照默认代码页的编码方案解释文本数据。这个默认代码页通常被称作ANSI代码页(ACP)。
ANSI代码页还有一层意思,就是微软自己定义的代码页。在历史上,IBM的个人计算机和微软公司的操作系统曾经是PC的标准配置。微软公司将 IBM公司定义的代码页称作OEM代码页,在IBM公司的代码页基础上作了些增补后,作为自己的代码页,并冠以ANSI的字样。我们在“区域和语言选项” 高级页面的代码页转换表中看到的包含ANSI字样的代码页都是微软自己定义的代码页。例如:
- 874 (ANSI/OEM - 泰文)
- 932 (ANSI/OEM - 日文 Shift-JIS)
- 936 (ANSI/OEM - 简体中文 GBK)
- 949 (ANSI/OEM - 韩文)
- 950 (ANSI/OEM - 繁体中文 Big5)
- 1250 (ANSI - 中欧)
- 1251 (ANSI - 西里尔文)
- 1252 (ANSI - 拉丁文 I)
- 1253 (ANSI - 希腊文)
- 1254 (ANSI - 土耳其文)
- 1255 (ANSI - 希伯来文)
- 1256 (ANSI - 阿拉伯文)
- 1257 (ANSI - 波罗的海文)
- 1258 (ANSI/OEM - 越南)
在UniToy中,我们可以按照代码页编码顺序查看这些代码页的字符和编码:
我们不能直接设置ANSI代码页,只能通过选择系统Locale,间接改变当前的ANSI代码页。微软定义的Locale只使用自己定义的代码页。所以,我们虽然可以通过“区域和语言选项”中的代码页转换表安装很多代码页,但只能将微软的代码页作为系统默认代码页。
2.1.3 代码页转换表
在Windows 2000以后,Windows统一采用UTF-16作为内部字符编码。现在,安装一个代码页就是安装一张代码页转换表。通过代码页转换表,Windows 既可以将代码页的编码转换到UTF-16,也可以将UTF-16转换到代码页的编码。代码页转换表的具体实现可以是一个以nls为后缀的数据文件,也可以 是一个提供转换函数的动态链接库。有的代码页是不需要安装的。例如:Windows将UTF-7和UTF-8分别作为代码页65000和代码页 65001。UTF-7、UTF-8和UTF-16都是基于Unicode的编码方案。它们之间可以通过简单的算法直接转换,不需要安装代码页转换表。
在安装过一个代码页后,Windows就知道怎样将该代码页的文本转换到Unicode文本,也知道怎样将Unicode文本转换成该代码页的文 本。例如:UniToy有导入和导出功能。所谓导入功能就是将任一代码页的文本文件转换到Unicode文本;导出功能就是将Unicode文本转换到任 一指定的代码页。这里所说的代码页就是指系统已安装的代码页:
其实,如果全世界人民在计算机刚发明时就统一采用Unicode作为字符编码,那么代码页就没有存在的必要了。可惜在Unicode被发明前,世界 各国人民都发明并使用了各种字符编码方案。所以,Windows必须通过代码页支持已经被广泛使用的字符编码。从这种意义看,代码页主要是为了兼容现有的 数据、程序和习惯而存在的。
2.1.4 SBCS、DBCS和MBCS
SBCS、DBCS和MBCS分别是单字节字符集、双字节字符集和多字节字符集的缩写。SBCS、DBCS和MBCS的最大编码长度分别是1字节、 两字节和大于两字节(例如4或5字节)。例如:代码页1252 (ANSI-拉丁文 I)是单字节字符集;代码页936 (ANSI/OEM-简体中文 GBK)是双字节字符集;代码页54936 (GB18030 简体中文)是多字节字符集。
单字节字符集中的字符都用一个字节表示。显然,SBCS最多只能容纳256个字符。
双字节字符集的字符用一个或两个字节表示。那么我们从文本数据中读到一个字节时,怎么判断它是单字节字符,还是双字节字符的首字符?答案是通过字节 所处范围来判断。例如:在GBK编码中,单字节字符的范围是0x00-0x80,双字节字符首字节的范围是0x81到0xFE。我们顺序读取字节数据,如 果读到的字节在0x81到0xFE内,那么这个字节就是双字节字符的首字节。GBK定义双字节字符的尾字节范围是0x40到0x7E和0x80到 0xFE。
GB18030是多字节字符集,它的字符可以用一个、两个或四个字节表示。这时我们又如何判断一个字节是属于单字节字符,双字节字符,还是四字节字 符?GB18030与GBK是兼容的,它利用了GBK双字节字符尾字节的未使用码位。GB18030的四字节字符的第一字节的范围也是0x81到 0xFE,第二字节的范围是0x30-0x39。通过第二字节所处范围就可以区分双字节字符和四字节字符。GB18030定义四字节字符的第三字节范围是 0x81到0xFE,第四字节范围是0x30-0x39。
2.2 代码页实例
2.2.1 实例一:GB18030代码页
1.1节的“错误2”中演示了一个全被显示成'?'的文件。这个文件的数据是:
其实,这是一个包含了6个四字节字符的GB18030编码的文件。记事本按照GBK显示这些数据,而GB18030的四字节字符编码在GBK中是未 定义的。Windows根据首字节范围判断出12个双字节字符,然后因为找不到匹配的转换而将其映射到默认字符'?'。使用UniToy按照 GB18030代码页导入这个文件,就可以看到:
这个GB18030编码的文件是用UniToy创建的,编辑Unicode文本,然后导出到GB18030编码格式。
2.2.2 实例二:GBK和Big5的转换
综合使用UniToy的导入、导出功能就可以在任意两个代码页之间转换文本。其实,由于各代码页支持的字符范围不同,我们一般不会直接在代码页间转换文本。例如将以下GBK编码的文本:
直接转换到Big5编码,就会看到:
变成'?'的字符都是Big5编码不支持的简化字。在从Unicode转换到Big5编码时,由于Big5编码不支持这些字符,Windows就用默认字符'?'代替。在UniToy中,我们可以先将简体字转换到繁体字,然后再导出到Big5编码,就可以正常显示:
同理,将Big5编码的文本转换到GBK编码的步骤应该是:
- 将Big5编码的文本导入到Unicode文本;
- 将繁体的Unicode文本转换简体的Unicode文本;
- 将简体的Unicode文本导出到GBK文本。
2.3 互联网的字符集
2.3.1 字符集
互联网上的信息缤纷多彩,但文本依然是最重要的信息载体。html文件通过标记表明自己使用的字符集。例如:
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
或者:
<meta http-equiv="charset" content="iso-8859-1">
那么我们可以使用哪些字符集(charset)呢?在IETF(互联网工程任务组)的网页上维护着一份可以在互联网上使用的字符集的清单:CHARACTER SETS 。如果有新的字符集被登记,IETF会更新这份文档。
简单浏览一下,2006年12月7日的版本列出了253个字符集。其中也包括微软的CP1250 ~ CP1258,在这里它们不会被称作什么ANSI代码页,而是被简单地称作windows-1250、windows-1251等。其实在Unicode 被广泛使用前,除了中日韩等大字符集,世界上,特别是西方使用最广泛的字符集应该是ISO 8859系列字符集。
2.3.2 ISO 8859系列字符集
ISO 8859系列字符集是欧洲计算机制造商协会(ECMA)在上世纪80年代中期设计,并被国际标准化(ISO)组织采纳为国际标准。ISO 8859系列字符集目前有15个字符集,包括:
- ISO 8859-1 大部分的西欧语系,例如英文、法文、西班牙文和德文等(Latin-1)
- ISO 8859-2 大部分的中欧和东欧语系,例如捷克文、波兰文和匈牙利文等(Latin-2)
- ISO 8859-3 欧洲东南部和其它各种文字(Latin-3)
- ISO 8859-4 斯堪的那维亚和波罗的海语系(Latin-4)
- ISO 8859-5 拉丁文与斯拉夫文(俄文、保加利亚文等)
- ISO 8859-6 拉丁文与阿拉伯文
- ISO 8859-7 拉丁文与希腊文
- ISO 8859-8 拉丁文与希伯来文
- ISO 8859-9 为土耳其文修正的Latin-1(Latin-5)
- ISO 8859-10 拉普人、北欧与爱斯基摩人的文字(Latin-6)
- ISO 8859-11 拉丁文与泰文
- ISO 8859-13 波罗的海周边语系,例如拉脱维亚文等(Latin-7)
- ISO 8859-14 凯尔特文,例如盖尔文、威尔士文等(Latin-8)
- ISO 8859-15 改进的Latin-1,增加遗漏的法文、芬兰文字符和欧元符号(Latin-9)
- ISO 8859-16 罗马尼亚文(Latin-10)
其中缺少的编号12据说是为了预留给天城体梵文字母(Deva-nagari)的。印地文和尼泊尔文都使用了这种在七世纪形成的字母表。由于印度定 义了自己的编码ISCII(Indian Script Code for Information Interchange),所以这个编号就未被使用。ISO 8859系列字符集都是单字节字符集,即只使用0x00-0xFF对字符编码。
大家都知道ASCII吧,那么大家知道ANSI X3.4和ISO 646吗?在1968年发布的ANSI X3.4和1972年发布的ISO 646就是ASCII编码,只不过是不同组织发布的。绝大多数字符集都与ASCII编码保持兼容,ISO 8859系列字符集也不例外,它们的0x00-0x7f都与ASCII码保持一致,各字符集的不同之处在于如何利用0x80-0xff的码位。使用 UniToy可以查看ISO 8859系列所有字符集的编码,例如:
通过这些演示,大家是不是觉得代码页和字符集都是很简单、朴实的东西呢?好,在进入Unicode的话题前,让我们先看一个很深奥的概念。
谈谈Windows程序中的字符编码
写这篇文章的起因是这么一个问题:我们在使用和安装Windows程序时,有时会看到以“2052”、“1033”这些数字为名的文件夹,这些数字似乎和字符集有关,但它们究竟是什么意思呢?
研究这个问题的同时,又会遇到其它问题。我们会谈到Windows的内部架构、Win32 API的A/W函数、Locale、ANSI代码页、与字符编码有关的编译参数、MBCS和Unicode程序、资源和乱码等,一起经历这段琐碎细节为主,间或乐趣点缀的旅程。
0 Where is Win32 API
Windows程序有用户态和核心态的说法。在32位地址空间中,用户态代码只能访问0x80000000以下空间(其实只是 0x00010000-0x7FFEFFFF),核心态代码可以访问0x80000000以上空间。所有硬件管理都在核心态。用户态代码不能直接使用核心 态的任何代码。所谓用户态、核心态其实只是不同的CPU特权级别。在x86 CPU上,用户态处于ring 3,核心态处于ring 0。
从用户态进入核心态的最常用的方法是在寄存器eax填一个功能码,然后执行int 2e。这有点像DOS时代的DOS和BIOS系统调用。在NT架构中这种机制被称作system service。
在核心态提供system service的有两个家伙:ntoskrnl.exe和win32k.sys。ntoskrnl.exe是Windows的大脑,它的上层被称为 Executive,下层被称作Kernel。Win32k.sys提供与显示有关的system service。
在用户态一侧,有一个重要的角色叫作ntdll.dll,大多数system service都是它调用的。它封装这些system service,然后提供一个API接口。这个接口被称作native API。 native API的用户是各个子系统(subsystem),包括Win32子系统、OS/2子系统、POSIX子系统。各个子系统为Win32、OS2、POSIX程序提供了运行平台。
ntdll.dll由于提供了平台无关的API接口,所以被看作是NT系统的原生接口,由之得到了“native API”的匪号。其实它的主要工作是将调用传递到核心态。
Win32、OS/2、POSIX,听起来很庞大。其实真正做好的只有Win32子系统。OS2、POSIX都是Console UI,即只有字符界面。提供OS/2子系统,只因为在1988年,NT的主要设计目标就是与OS/2兼容,后来由于Windows 3.0卖得很好,所以设计目标被变更为与Windows兼容。提供POSIX子系统,是为了应付美国政府的一个编号为FIPS 151-2的标准。
Win32子系统的管理员是一个叫作csrss.exe的弟兄,它的全名是:Client/Server Run-Time Subsystem。它刚上任时,本来要分管所有的子系统,但后来POSIX和OS/2都被分别处理了,所以只管了一个Win32。即使这样也很了不起, 所有的Win32程序的进程、线程们都要向它登记。
不过Win32程序用得最多的还是Win32子系统的DLL们,最核心的DLL包括:kernel32.dll、User32.dll、Gdi32.dll、Advapi32.dll。这些DLL包装了ntdll.dll的native API。其中Gdi32.dll比较特殊,它与核心态的win32k.sys直接保持联系,以提高NT系统的图形处理能力。Win32子系统的DLL们提供的接口函数在MSDN文档中被详细介绍,它们就是Win32 API。
附录0 Windows的启动
计算机上电后,从BIOS的ROM开始运行。BIOS在做一些初始化后会将硬盘的第一个扇区的数据读入内存,然后将控制权交给它,这段数据被称作Master Boot Record(MBR)。
MBR包含一段启动代码和硬盘的主分区表。这段启动代码扫描主分区表,找到第一个可以启动的分区,然后将这个分区的第一个扇区读入内存并运行。这个扇区被称作引导扇区(boot sector)。
引导扇区的代码具备读文件系统根目录的能力,显然不同的文件系统需要不同的代码。引导扇区会从根目录中读出一个叫作ntldr的文件。顾名思义,这个文件是load NT的主要角色。它的业绩主要包括将CPU从实模式转入保护模式,启动分页机制,处理boot.ini等。
如果boot.ini中有一句:
C:/bootsect.rh="Red Hat Linux"
bootsect.rh的内容是Linux引导扇区,用户又选择了“Red Hat Linux”,ntldr就会将执行Linux的引导扇区,开始Linux的引导。如果用户选择继续使用Windows,ntldr会装载并运行我们前面提到的ntoskrnl.exe。
ntoskrnl.exe会启动会话管理器smss.exe。smss.exe启动csrss.exe和winlogon.exe。 smss.exe会永远等待csrss.exe和winlogon.exe返回。如果两者之一异常中止,就会导致系统崩溃。所以病毒们经常以打击 csrss.exe为乐。
winlogon.exe负责用户登录,在完成登录后,它会启动注册表HKLM/SOFTWARE/Microsoft/Windows NT/Current Version/Winlogon项下Userinit值指定的程序。该值的缺省数据是userinit.exe。userinit.exe会装载个人设 置,让硬盘响个不停,并考验我们的耐性,最后启动注册表同一项下Shell值指定的程序。该值的缺省数据是Explorer.exe。 Explorer.exe运行后,我们就会看到熟悉的开始菜单和桌面。
1 Win32 API的A/W函数
要了解Win32子系统的DLL们提供了哪些API,最直接的方法就是用Win32dsm直接查看DLL们的导出表。这时我们会发现Win32 API中带字符串的API一般都有两个版本,例如CreateFileA和CreateFileW。当然也有例外,例如GetProcAddress函 数。
A代表ANSI代码页,W是宽字符,即Unicode字符。Windows中的Unicode字符一般指UCS2的UTF16-LE编码。让我们通过几个实例观察A/W版本间的关系。
例1:用WIn32dsm查看gdi32.dll的汇编代码,可以看到TextOutA调用GdiGetCodePage获取当前代码页,再调用MultiByteToWideChar转换输入的字符串,然后调用一个内部函数。而TextOutW直接调用这个内部函数。
例2:用调试器跟踪一个使用了CreateFileA的程序,可以看到:CreateFileA在将输入字符串转换为Unicode后,会调用CreateFileW。假设输入文件名是“测试.txt”,对应的数据就是:“B2 E2 CA D4 2E 74 78 74 00”。
在调试器中可以看到传给CreateFileW的文件名数据是:“4B 6D D5 8B 2E 00 74 00 78 00 74 00 00 00”。 这是"测试.txt"对应的Unicdoe字符串。CreateFileW会接着调用ntdll.dll中的NtCreateFile。顺便看看 NtCreateFile的代码:
mov eax, 00000020
lea edx, dword ptr [esp+04]
int 2E
ret 002C
可见这个native API只是简单地调用了核心态提供的0x20号system service。
例3:gdi32.dll中的GetGlyphOutline函数可以获取指定字符的字模。GetGlyphOutlineA和GetGlyphOutlineW函数都会调用同一个内部函数(记作F)。函数F在返回前将通过int 2E调用0x10B1号system service。
GetGlyphOutlineW直接调用函数F。GetGlyphOutlineA在调用函数F前,要依次调用GdiGetCodePage、 IsDBCSLeadByteEx和MultiByteToWideChar,将当前代码页的字符编码转换成Unicode编码。
如果我们调用GetGlyphOutlineA时传入“baba”,这是“汉”字的GBK编码,用调试器可以看到传给函数F的字符编码是“6c49”,这是“汉”字的Unicode编码。
从以上例子可见,A版本总会在某处将输入的字符串转换为Unicode字符串,然后和W版本执行相同的代码。在由A/W版本API引出MBCS程序和Unicode程序前,让我们先解释一下Locale和ANSI代码页。
2 Locale和ANSI代码页
2.1 Locale和LCID
Locale是指特定于某个国家或地区的一组设定,包括字符集,数字、货币、时间和日期的格式等。在Windows中,每个Locale可以用一个 32位数字表示,记作LCID。在winnt.h中可以看到LCID的组成。它的高16位表示字符的排序方法,一般为0。在它的低16位中,低10位是 primary language的ID,高4位指定sublanguage。sublanguage被用来区分同一种语言的不同编码。下面是部分primary language和sublanguage的常数定义:
#define LANG_CHINESE 0x04
#define LANG_ENGLISH 0x09
#define LANG_FRENCH 0x0c
#define LANG_GERMAN 0x07
#define SUBLANG_CHINESE_TRADITIONAL 0x01 // Chinese (Taiwan Region)
#define SUBLANG_CHINESE_SIMPLIFIED 0x02 // Chinese (PR China)
#define SUBLANG_ENGLISH_US 0x01 // English (USA)
#define SUBLANG_ENGLISH_UK 0x02 // English (UK)
好,现在我们可以计算简体中文的LCID了,将sublanguage的常数左移10位,即乘上1024,再加上primary language的常数:2*1024+4=2052,16进制是0804。美国英语是:1*1024+9=1033,16进制是0409。。繁体中文是 1*1024+4=1028,16进制是0404。
2.2 代码页
每个Locale都联系着很多信息,可以通过GetLocalInfo函数读取。其中最重要的信息就是字符集了,即Locale对应的语言文字的编码。Windows将字符集称作代码页。
每个Locale可以对应一个ANSI代码页和一个OEM代码页。Win32 API使用ANSI代码页,底层设备使用OEM代码页,两者可以相互映射。
例如English (US)的ANSI和OEM代码页分别为“1252 (ANSI - Latin I)”和“437 (OEM - United States)”。 Chinese (PRC)的ANSI和OEM代码页都是“936 (ANSI/OEM - Simplified Chinese GBK)”。 Chinese (TW)的ANSI和OEM代码页都是“950 (ANSI/OEM - Traditional Chinese Big5)”。
附录1中有一张很长的表。列出了我正在使用的Windows所支持的135个Locale的部分信息,包括 LCID、国家/地区名称、语言名称、语言缩写和对应的ANSI代码页。
2.3 系统Locale、用户Locale,再谈ANSI代码页
在Windows中,通过控制面板可以为系统和用户分别设置Locale。系统Locale决定代码页,用户Locale决定数字、货币、时间和日期的格式。这不是一个好的设计,后面会谈到它带来的问题。
使用GetSystemDefaultLCID函数和GetUserDefaultLCID函数分别得到系统和用户的LCID。有很多材料将这两个 函数和另外两个函数混淆:GetSystemDefaultUILanguage和GetUserDefaultUILanguage。
GetSystemDefaultUILanguage和GetUserDefaultUILanguage得到的是您当前使用的Windows版本所带的UI资源的语言。
用户程序缺省使用的代码页是当前系统Locale的ANSI代码页,可以称作ANSI编码,也就是A版本的Win32 API默认的字符编码。对于一个未指定编码方式的文本文件,Windows会按照ANSI编码解释。
2.4 AppLocale
如果一个文本文件采用BIG5编码,系统当前的ANSI代码页是GBK。打开这个文件,就会显示乱码。例如“中文”在BIG5中的编码是A4A4、A4E5,这两个编码在GBK中对应的字符是“いゅ”。这是日文的两个平假名。
在Windows XP平台有一个AppLocale程序,可以以指定的语言运行非Unicode程序。用Win32dsm打开看一看,其实它只是在运行程序前设置了两个环境变量。我们可以用个批处理文件模仿一下:
@ECHO OFF
SET __COMPAT_LAYER=#ApplicationLocale
SET ApplocaleID=0404
start notepad.exe
在简体中文平台,用这个批处理文件启动的记事本可以正确显示BIG5编码的文本文件。用它打开GBK编码的文本文件会怎么样?“中文”会被显示为“笢恅”。设置这两个环境变量会作用于当前进程和其子进程。Windows 2000平台不支持这个方法。
3 MBCS程序和Unicode程序
3.1 与字符编码有关的编译参数
让我们回到Win32 API。我们在程序中使用的Win32 API没有A/W后缀,Windows的头文件会根据编译参数UNICODE将没有后缀的函数名替换为A版本或W版本,例如:
#ifdef UNICODE
#define CreateFile CreateFileW
#else
#define CreateFile CreateFileA
#endif
C RunTime库(CRT)使用_UNICODE和_MBCS来区分三套字符串处理函数,分别用于SBCS、MBCS和Unicdoe字符串。SBCS和 MBCS分别指单字节字符串和多字节字符串。例如_tcsclen的3个版本分别为strlen、_mbslen和wcslen ,猜猜以下函数返回几?
strlen("VOIP网关");
_mbslen((unsigned char *)"VOIP网关");
wcslen(L"VOIP网关");
答案是8、6、6。L"ANSI字符串"通知编译器将ANSI字符串转换为Unicode字符串,这是VC++编译器提供的一个小甜点。不过我们应 该用宏:_T("ANSI字符串")。_T宏只在我们定义了_UNICODE时才转换。这样同一套代码既可以编译MBCS版本,也可以编译Unicode 版本。
MFC用_UNICODE参数区分Unicode版本特有的代码,决定使用什么版本的导入库或静态库。
3.2 Unicode程序、MBCS程序和多语言支持
Unicode程序直接使用Unicode版本的CRT和Win32 API。Unicode程序的运行与当前的ANSI代码页没有关系。MBCS程序的运行依赖于ANSI代码页。如果设计者和使用者使用不同的代码页,就可 能出现乱码。微软开发的程序大都是Unicode程序,不管我们怎样变换系统Locale,它们总能正常运行。
使用VCL类库的Delphi程序都是MBCS程序。VCL框架在程序启动会调用GetThreadLocale获取当前用户的LCID,然后在当 前目录查找对应的资源文件,命名规则是:程序名+'.'+语言缩写,语言缩写可以参见附录1。在找不到时才会使用EXE文件中的资源。不过如果系统 LCID是English(United States),用户LCID是Chinese(PRC),由VCL产生的程序就会出现乱码。读者可以自己分析原因。
为VCL程序做多语言版本。只要用Delphi自带的Resource DLL Wizard再做一个特定语言的资源DLL,原来的程序都不用改。不过很多程序员用其它组件做多语言版本,例如TsiLang 。
MBCS程序虽然也可以做成多语言版本,但它无法在同时显示不同代码页特有的字符,这时就必须使用Unicode程序了。
VS.NET文档中有个多语言资源的例子:SatDLL。它只用Win32 API的例子,却用了VC7项目。我在学习时将它改成了VC6项目,并纠正了它的两个问题:
1、用GetUserDefaultUILanguage读到的是Windows资源版本,不是当前用户设置的代码页。
2、启动时没有使用资源DLL里的菜单。
在我的个人主页(http://www.fmddlmyy.cn)上可以下载修改过的SatDLL 。 这个程序说明了支持多语言资源的基本思路:将不同语言资源放到不同的DLL中,在程序启动时根据当前Locale装载对应的资源DLL。必要时动态切换资 源。为了标记不同语言的资源,可以将它们放到不同的目录中,以LCID作为目录名,例如“2052”、“1033”。当然我们也可以用其它方法联系 LCID和资源DLL。
MFC程序可以在App类的InitInstance函数中用AfxSetResourceHandle函数设置资源DLL。在Delphi中动态切换资源可以参考Delphi Demo目录RichEdit项目的ReInit.pas。在读取当前设定时,建议用GetSystemDefaultLCID函数,因为系统Locale决定ANSI代码页。
3.4 资源和乱码
通过检查可执行文件,我们可以确定VC和Delphi的资源编译器都以Unicode保存字符资源。在VC环境编辑资源时,我们会指定资源的代码页。编译器根据资源的代码页,将其转换到Unicode。
Unicode程序直接使用以Unicode编码保存的资源。MBCS程序需要将Unicode资源先转换回当前ANSI代码页,然后再使用。如果资源中的Unicode字符串不能映射到当前代码页中的字符,就会出现??。
例如Windows的标准对话框也会出现乱码。假设我们使用简体中文Windows,当前Locale是Chinese (TW),我们的程序是MBCS的,使用标准的打开文件对话框。因为在BIG5中没有“开”这个字,所以“打开”会被显示成“打?”。将程序编译成 Unicode版本,就可以避免这个问题。
如果字符不是保存在资源中,而是硬编码在程序中。然后开发者和用户使用不同的代码页,就会导致乱码。假设开发者的Locale是Chinese (PRC),用户的Locale是English (US),程序中硬编码了字符串“文件”。 Chinese (PRC)的ANSI代码页是GBK,“文件”的编码“CE C4 BC FE”。English (US)的ANSI代码页是Latin I,用户按照Latin I编码去解释“CE C4 BC FE”,就会看到“???t”。
回答我前面提过的一个问题:Delphi程序根据用户LCID转换资源中的字符串。如果用户LCID是Chinese (PRC),系统LCID是English (US)。那么资源中的Unicode字符串会被转换为GBK编码,然后按照Latin I显示,这时我们看到的就是类似“???t”的东东,不是??。
既然资源是以Unicode保存的,MBCS程序如果不将其转换到ANSI代码页,而用W版本的函数直接显示,就不会产生乱码。例如MFC程序菜单里的中文,在English (US)的Locale也可以正常显示。不过这取决于各部分代码的具体实现,menu bar控件里的中文在English (US)的Locale会全部显示成??。
进一步的参考资料
本文的第0节和附录0主要参考了《Inside Windows 2000 Third Edition》,国内出过该书的影印版。DDK文档中有大量Windows内核的信息。用Win32dsm和各种调试器查看Windows系统文件可以获得更直接的信息。
关于Window程序的字符编码,最好的参考资料是winnt.h等SDK的包含文件、VCL、MFC、CRT的源文件。我们不需要阅读它们,只要找到自己感兴趣的信息就可以了,用Source Insight可能方便一些。
本文所谈的不是什么万古不迁的道理,只是别的程序员的一些设定,我们因为需要使用他们的程序,所以有必要了解一些细节。研究问题的方法和兴趣永远比问题本身重要,如一句拉丁俗语所说:res, non verba,实质胜于文字。
尾声
“明月虽有圆缺,但毕竟永恒不灭,人生却如过眼烟云,一去不回,真不知计较为何?”
“蛙声虽是短促,但却是万籁中一个活泼的禅机,也可以说万古如斯,永恒不迁,无奈感受到的,能有几人?”
这是一本武侠书中的对话。在时间的长河中,人生和蛙声一样易逝。说到蛙声,我的20个月的小宝宝在喝汤后,略加酝酿,就会紧闭着嘴巴,发出很像蛙鸣的声音。我们会逗他说:“小青蛙又来了”。小家伙益发得意,不管我的抗议,将连汤带油的小下巴亲热地贴在我的身上。
附录1 一些关于LCID的信息
使用EnumSystemLocales函数可以枚举系统支持的LCID。用GetLocaleInfo可以得到ANSI代码页的ID,再通过GetCPInfoEx可以获得代码页的全称。以下是我在中文Windows XP上读到的内容。
LCID
国家或地区
语言
语言缩写
ANSI代码页
1025
沙特阿拉伯
阿拉伯语(沙特阿拉伯)
ARA
1256 (ANSI - 阿拉伯文)
1026
保加利亚
保加利亚语
BGR
1251 (ANSI - 西里尔文)
1027
西班牙
加泰隆语
CAT
1252 (ANSI - 拉丁文 I)
1028
台湾
中文(台湾)
CHT
950 (ANSI/OEM - 繁体中文 Big5)
1029
捷克共和国
捷克语
CSY
1250 (ANSI - 中欧)
1030
丹麦
丹麦语
DAN
1252 (ANSI - 拉丁文 I)
1031
德国
德语(德国)
DEU
1252 (ANSI - 拉丁文 I)
1032
希腊
希腊语
ELL
1253 (ANSI - 希腊文)
1033
美国
英语(美国)
ENU
1252 (ANSI - 拉丁文 I)
1034
西班牙
西班牙语(传统)
ESP
1252 (ANSI - 拉丁文 I)
1035
芬兰
芬兰语
FIN
1252 (ANSI - 拉丁文 I)
1036
法国
法语(法国)
FRA
1252 (ANSI - 拉丁文 I)
1037
以色列
希伯来语
HEB
1255 (ANSI - 希伯来文)
1038
匈牙利
匈牙利语
HUN
1250 (ANSI - 中欧)
1039
冰岛
冰岛语
ISL
1252 (ANSI - 拉丁文 I)
1040
意大利
意大利语(意大利)
ITA
1252 (ANSI - 拉丁文 I)
1041
日本
日语
JPN
932 (ANSI/OEM - 日文 Shift-JIS)
1042
朝鲜
朝鲜语
KOR
949 (ANSI/OEM - 韩文)
1043
荷兰
荷兰语(荷兰)
NLD
1252 (ANSI - 拉丁文 I)
1044
挪威
挪威语(伯克梅尔)
NOR
1252 (ANSI - 拉丁文 I)
1045
波兰
波兰语
PLK
1250 (ANSI - 中欧)
1046
巴西
葡萄牙语(巴西)
PTB
1252 (ANSI - 拉丁文 I)
1048
罗马尼亚
罗马尼亚语
ROM
1250 (ANSI - 中欧)
1049
俄罗斯
俄语
RUS
1251 (ANSI - 西里尔文)
1050
克罗地亚
克罗地亚语
HRV
1250 (ANSI - 中欧)
1051
斯洛伐克语
斯洛伐克语
SKY
1250 (ANSI - 中欧)
1052
阿尔巴尼亚
阿尔巴尼亚语
SQI
1250 (ANSI - 中欧)
1053
瑞典
瑞典语
SVE
1252 (ANSI - 拉丁文 I)
1054
泰国
泰语
THA
874 (ANSI/OEM - 泰文)
1055
土耳其
土耳其语
TRK
1254 (ANSI - 土耳其文)
1056
巴基斯坦伊斯兰共和国
乌都语
URD
1256 (ANSI - 阿拉伯文)
1057
印度尼西亚
印度尼西亚语
IND
1252 (ANSI - 拉丁文 I)
1058
乌克兰
乌克兰语
UKR
1251 (ANSI - 西里尔文)
1059
比利时
比利时语
BEL
1251 (ANSI - 西里尔文)
1060
斯洛文尼亚
斯洛文尼亚语
SLV
1250 (ANSI - 中欧)
1061
爱沙尼亚
爱沙尼亚语
ETI
1257 (ANSI - 波罗的海文)
1062
拉脱维亚
拉脱维亚语
LVI
1257 (ANSI - 波罗的海文)
1063
立陶宛
立陶宛语
LTH
1257 (ANSI - 波罗的海文)
1065
伊朗
法斯语
FAR
1256 (ANSI - 阿拉伯文)
1066
越南
越南语
VIT
1258 (ANSI/OEM - 越南)
1068
阿塞拜疆
阿塞拜疆语(拉丁文)
AZE
1254 (ANSI - 土耳其文)
1069
西班牙
巴士克语
EUQ
1252 (ANSI - 拉丁文 I)
1071
前南斯拉夫马其顿共和国
马其顿语(FYROM)
MKI
1251 (ANSI - 西里尔文)
1078
南非
南非语
AFK
1252 (ANSI - 拉丁文 I)
1080
法罗群岛
法罗语
FOS
1252 (ANSI - 拉丁文 I)
1086
马来西亚
马来语(马来西亚)
MSL
1252 (ANSI - 拉丁文 I)
1087
吉尔吉斯坦
哈萨克语
KKZ
1251 (ANSI - 西里尔文)
1088
吉尔吉斯斯坦
吉尔吉斯语 (西里尔文)
KYR
1251 (ANSI - 西里尔文)
1089
肯尼亚
斯瓦希里语
SWK
1252 (ANSI - 拉丁文 I)
1091
乌兹别克斯坦
乌兹别克语(拉丁文)
UZB
1254 (ANSI - 土耳其文)
1092
鞑靼斯坦
鞑靼语
TTT
1251 (ANSI - 西里尔文)
1104
蒙古
蒙古语(西里尔文)
MON
1251 (ANSI - 西里尔文)
1110
西班牙
加里西亚语
GLC
1252 (ANSI - 拉丁文 I)
2049
伊拉克
阿拉伯语(伊拉克)
ARI
1256 (ANSI - 阿拉伯文)
2052
中华人民共和国
中文(中国)
CHS
936 (ANSI/OEM - 简体中文 GBK)
2055
瑞士
德语(瑞士)
DES
1252 (ANSI - 拉丁文 I)
2057
英国
英语(英国)
ENG
1252 (ANSI - 拉丁文 I)
2058
墨西哥
西班牙语(墨西哥)
ESM
1252 (ANSI - 拉丁文 I)
2060
比利时
法语(比利时)
FRB
1252 (ANSI - 拉丁文 I)
2064
瑞士
意大利语(瑞士)
ITS
1252 (ANSI - 拉丁文 I)
2067
比利时
荷兰语(比利时)
NLB
1252 (ANSI - 拉丁文 I)
2068
挪威
挪威语(尼诺斯克)
NON
1252 (ANSI - 拉丁文 I)
2070
葡萄牙
葡萄牙语(葡萄牙)
PTG
1252 (ANSI - 拉丁文 I)
2074
塞尔维亚
塞尔维亚语(拉丁文)
SRL
1250 (ANSI - 中欧)
2077
芬兰
瑞典语(芬兰)
SVF
1252 (ANSI - 拉丁文 I)
2092
阿塞拜疆
阿塞拜疆语(西里尔文)
AZE
1251 (ANSI - 西里尔文)
2110
文莱达鲁萨兰
马来语(文莱达鲁萨兰)
MSB
1252 (ANSI - 拉丁文 I)
2115
乌兹别克斯坦
乌兹别克语(西里尔文)
UZB
1251 (ANSI - 西里尔文)
3073
埃及
阿拉伯语(埃及)
ARE
1256 (ANSI - 阿拉伯文)
3076
香港特别行政区
中文(香港特别行政区)
ZHH
950 (ANSI/OEM - 繁体中文 Big5)
3079
奥地利
德语(奥地利)
DEA
1252 (ANSI - 拉丁文 I)
3081
澳大利亚
英语(澳大利亚)
ENA
1252 (ANSI - 拉丁文 I)
3082
西班牙
西班牙语(国际)
ESN
1252 (ANSI - 拉丁文 I)
3084
加拿大
法语(加拿大)
FRC
1252 (ANSI - 拉丁文 I)
3098
塞尔维亚
塞尔维亚语(西里尔文)
SRB
1251 (ANSI - 西里尔文)
4097
利比亚
阿拉伯语(利比亚)
ARL
1256 (ANSI - 阿拉伯文)
4100
新加坡
中文(新加坡)
ZHI
936 (ANSI/OEM - 简体中文 GBK)
4103
卢森堡
德语(卢森堡)
DEL
1252 (ANSI - 拉丁文 I)
4105
加拿大
英语(加拿大)
ENC
1252 (ANSI - 拉丁文 I)
4106
危地马拉
西班牙语(危地马拉)
ESG
1252 (ANSI - 拉丁文 I)
4108
瑞士
法语(瑞士)
FRS
1252 (ANSI - 拉丁文 I)
5121
阿尔及利亚
阿拉伯语(阿尔及利亚)
ARG
1256 (ANSI - 阿拉伯文)
5124
澳门特别行政区
中文(澳门特别行政区)
ZHM
950 (ANSI/OEM - 繁体中文 Big5)
5127
列支敦士登
德语(列支敦士登)
DEC
1252 (ANSI - 拉丁文 I)
5129
新西兰
英语(新西兰)
ENZ
1252 (ANSI - 拉丁文 I)
5130
哥斯达黎加
西班牙语(哥斯达黎加)
ESC
1252 (ANSI - 拉丁文 I)
5132
卢森堡
法语(卢森堡)
FRL
1252 (ANSI - 拉丁文 I)
6145
摩洛哥
阿拉伯语(摩洛哥)
ARM
1256 (ANSI - 阿拉伯文)
6153
爱尔兰
英语(爱尔兰)
ENI
1252 (ANSI - 拉丁文 I)
6154
巴拿马
西班牙语(巴拿马)
ESA
1252 (ANSI - 拉丁文 I)
6156
摩纳哥公国
法语(摩纳哥)
FRM
1252 (ANSI - 拉丁文 I)
7169
突尼斯
阿拉伯语(突尼斯)
ART
1256 (ANSI - 阿拉伯文)
7177
南非
英语(南非)
ENS
1252 (ANSI - 拉丁文 I)
7178
多米尼加共和国
西班牙语(多米尼加共和国)
ESD
1252 (ANSI - 拉丁文 I)
8193
阿曼
阿拉伯语(阿曼)
ARO
1256 (ANSI - 阿拉伯文)
8201
牙买加
英语(牙买加)
ENJ
1252 (ANSI - 拉丁文 I)
8202
委内瑞拉
西班牙语(委内瑞拉)
ESV
1252 (ANSI - 拉丁文 I)
9217
也门
阿拉伯语(也门)
ARY
1256 (ANSI - 阿拉伯文)
9225
加勒比海
英语(加勒比海)
ENB
1252 (ANSI - 拉丁文 I)
9226
哥伦比亚
西班牙语(哥伦比亚)
ESO
1252 (ANSI - 拉丁文 I)
10241
叙利亚
阿拉伯语(叙利亚)
ARS
1256 (ANSI - 阿拉伯文)
10249
伯利兹
英语(伯利兹)
ENL
1252 (ANSI - 拉丁文 I)
10250
秘鲁
西班牙语(秘鲁)
ESR
1252 (ANSI - 拉丁文 I)
11265
约旦
阿拉伯语(约旦)
ARJ
1256 (ANSI - 阿拉伯文)
11273
特立尼达和多巴哥
英语(特立尼达)
ENT
1252 (ANSI - 拉丁文 I)
11274
阿根廷
西班牙语(阿根廷)
ESS
1252 (ANSI - 拉丁文 I)
12289
黎巴嫩
阿拉伯语(黎巴嫩)
ARB
1256 (ANSI - 阿拉伯文)
12297
津巴布韦
英语(津巴布韦)
ENW
1252 (ANSI - 拉丁文 I)
12298
厄瓜多尔
西班牙语(厄瓜多尔)
ESF
1252 (ANSI - 拉丁文 I)
13313
科威特
阿拉伯语(科威特)
ARK
1256 (ANSI - 阿拉伯文)
13321
菲律宾共和国
英语(菲律宾)
ENP
1252 (ANSI - 拉丁文 I)
13322
智利
西班牙语(智利)
ESL
1252 (ANSI - 拉丁文 I)
14337
阿联酋
阿拉伯语(阿联酋)
ARU
1256 (ANSI - 阿拉伯文)
14346
乌拉圭
西班牙语(乌拉圭)
ESY
1252 (ANSI - 拉丁文 I)
15361
巴林
阿拉伯语(巴林)
ARH
1256 (ANSI - 阿拉伯文)
15370
巴拉圭
西班牙语(巴拉圭)
ESZ
1252 (ANSI - 拉丁文 I)
16385
卡塔尔
阿拉伯语(卡塔尔)
ARQ
1256 (ANSI - 阿拉伯文)
16394
玻利维亚
西班牙语(玻利维亚)
ESB
1252 (ANSI - 拉丁文 I)
17418
萨尔瓦多
西班牙语(萨尔瓦多)
ESE
1252 (ANSI - 拉丁文 I)
18442
洪都拉斯
西班牙语(洪都拉斯)
ESH
1252 (ANSI - 拉丁文 I)
19466
尼加拉瓜
西班牙语(尼加拉瓜)
ESI
1252 (ANSI - 拉丁文 I)
20490
波多黎各(美)
西班牙语(波多黎各(美))
ESU
1252 (ANSI - 拉丁文 I)
LCID取决于语言,在表中列出国家名只是为了增加趣味性。例如可以看到以色列还在使用古老的希伯来语。“希伯来语”的法文是hébreu,这个单词还有一个意思,就是“不能理解的东西”。
转自: http://blog.csdn.net/shyboy_nwpu/article/details/4431656
- 浅谈文字编码和Unicode
- 浅谈文字编码和Unicode
- 浅谈文字编码和Unicode
- 浅谈文字编码和Unicode
- 浅谈文字编码和Unicode
- 浅谈文字编码和Unicode
- 浅谈文字编码和Unicode
- 浅谈文字编码和Unicode(下)
- 浅谈文字编码和Unicode(上)
- 浅谈文字编码和Unicode(上)
- 浅谈文字编码和Unicode(下)
- 浅谈文字编码和Unicode(上)
- 浅谈文字编码和Unicode(下)
- 浅谈文字编码和Unicode(上)
- 浅谈文字编码和Unicode(下)
- 浅谈文字编码和Unicode(上)
- 浅谈文字编码和Unicode(转)
- 浅谈文字编码和Unicode(上)
- Android事件总线框架Otto使用
- 河源代办医院诊断书
- 清远代办医院诊断书
- OC学习笔记七 Category
- 东莞代办医院诊断书
- 浅谈文字编码和Unicode
- 潮州代办医院诊断书
- poj 1149 pigs 增广路 ford
- 天声人語 20150813
- 南宁代办医院诊断书
- 柳州代办医院诊断书
- leetcode 114: Flatten Binary Tree to Linked List
- 桂林代办医院诊断书
- 【usaco/codevs2033/codevs1047/NOIP1999TG】 邮票问题浅谈