VB6被很多程序员认为是一个过气的开发工具,但它实在是微软最经典的开发工具,没有之一!编译出的程序短小精悍,一般就几十K,而且从XP以后的操作系统,均携带其运行时库,只要程序只使用VB的标准控件,连安装都不需要,可以直接运行。所以,不太复杂的GUI程序我都使用VB6开发,速度那怎叫一个快字了得!
最近,我做了一个Mini流图软件,但需要支持多国语言。查了一下网上的资料,方案感觉都很山寨。基本上都是头疼医头,脚疼医脚,没有一个基于系统的全面调理方案。于是,仔细研究了VB6的资源文件体系,发现原来微软早就提供了完美的解决方案,只是中国的程序员大多不习惯这种贵族式的优雅。VC6也使用了这个机制,但GUI做得比VB差几条街,一点儿也不直观,智商低于120的程序员基本无缘此功能,但以后Visual Studio的VC一直继承下来这个机制(虽然表达方式依旧那么烂),不像VB从.net开始抛弃了这一机制,使用了简单粗暴的方法来解决。也许,简单粗暴是这个社会的潮流,但我依然追求优雅。所以以下内容仅适用于对完美有变态追求的同学们。
VB6的资源文件中一般包括字符串、图标、光标、位图和用户定义的字节流,共5种类型的资源,每种资源的实例都有2个属性,一个是资源ID,另一个就是语言ID。就是说,一个资源需要两个ID共同标识。例如:一个资源ID为101的字符串,其语言ID为1028的字符串内容是台湾中文,其语言ID为1033的字符串内容是美式英文,其语言ID为1049的字符串内容是俄文,其语言ID为2052的字符串内容是中国中文,其语言ID为2057的字符串内容是英式英文等等。如图所示是VB6的字符串资源编辑器,很直观吧!开发VC的项目经理太自大!
当程序请求资源时,只需要输入一个资源ID参数,运行时库会根据系统语言ID自动返回对应的资源。对于急功近利的同学们,这些基本就够用了。因为项目经理说做8国语言,大部分程序员不会考虑第9国人民的感受。后面将分享VB6资源文件体系的读取策略以便能优雅的解决第9国人民的问题。
在这之前,得先普及一下Windows内核的多国语言知识。微软从NT开始使用Unicode内核,其好处就是大大降低全球部署的成本,这是微软称霸全球的重要技术支持手段之一。对使用者而言,最直观的好处就是在输出的一个字符串里面可以同时包含多个国家的文字。虽然这种需求很少见,但切换语言这种事在Unicode技术的支持下,那就都不是事!
虽然Windows在发行时,都指明了语言版本,但实际上,任何一个语言的Windows都支持任意微软支持的语言。所以在语言上,Windows有两种级别的语言,一种是安装级(Installed),即系统(System)语言;另一种是支持级(Supported),即用户(User)语言。
一般情况下,系统语言和用户语言是一样的。那二般是个什么情况?举个例子,在一个国际学校,老师是美国人,他当然安装英文版Windows,但他的学生分别是法国人、德国人、中国人、韩国人,就是没有美国人!为了考虑学生的爱国情怀,老师给他们每个人分配一个登录账号,登录后,可以修改控制面板中的地区和语言,以便符合每个学生的习惯。这样,在每个学生的登录会话环境中,系统语言都是英语,用户语言是用户在控制面板中设置的语言。
OK,现在来看看VB6的运行时库对资源请求是如何动作的。当程序请求资源时,只需要输入一个资源ID参数,运行时库首先使用用户语言ID与请求的资源ID所拥有的所有语言ID进行匹配,如果成功,就把请求的资源ID和匹配成功的语言ID对应的资源返回;如果不成功,再使用系统语言ID与请求的资源ID所拥有的所有语言ID进行匹配,如果成功,就把请求的资源ID和匹配成功的语言ID对应的资源返回;如果还不成功,则把请求的资源ID所拥有的所有语言ID中ID值最小的那个对应的资源返回。
看那个例子,为了方便讲解,我加了一个资源ID是99的字符串,里面的内容是语言ID值。如果登录会话环境的用户语言和系统语言都没有匹配上,则返回中文(台湾)的字符串资源,因为1028是所有提供的语言ID中最小的!
说到这里,大家可能很快想到,ID值最小的一定是0,那0是怎么定义的呢?那我们得先看看微软是如何定义
语言ID的:
#define MAKELANGID(PrimaryLanguage, SubLanguage) ((((WORD) (SubLanguage)) << 10) | (WORD) (PrimaryLanguage))
意思是语言ID由1个16位的字表示(最多支持65536种语言),其中低10位为主语言ID(最多支持1024种主语言),高6位为子语言ID(每个主语言最多有64种子语言)。
主语言和子语言策略是表达以下情况:如西班牙语,世界上母语是西班牙语的国家有20多个,比英语还多,这个要问那位15世纪才华横溢、精力充沛的伊莎贝拉女王,是她指使哥伦布同学全球殖民而导致我们现在的问题。这20多个国家的西班牙文叫子语言,但他们都属于西班牙语区,这个区称之为主语言。
主语言ID为0被微软称之为中性语言(Neutral),它居然也有子语言,分别是SUBLANG_NEUTRAL(0)、SUBLANG_DEFAULT(1)和SUBLANG_SYS_DEFAULT(2),分别对应3种中性语言Neutral、Neutral Default和Neutral System Default。
所以,前面讲的是个家庭版,现在看看微软的运行时库对资源请求动作的专业版。当程序请求资源时,只需要输入一个资源ID参数,运行时库首先使用用户语言ID与请求的资源ID所拥有的所有语言ID进行匹配,如果成功,就把请求的资源ID和匹配成功的语言ID对应的资源返回;如果不成功,则返回主语言ID为0且子语言ID也为0的资源;如果没找到,再使用系统语言ID与请求的资源ID所拥有的所有语言ID进行匹配,如果成功,就把请求的资源ID和匹配成功的语言ID对应的资源返回;如果还不成功,则把请求的资源ID所拥有的所有语言ID中ID值最小的那个对应的资源返回。
所以,使用VB6多国语言资源文件的最佳实践是:根据软件主要投放语言区域,把那个语言设置为Neutral Default或Neutral System Default(这两个效果一样,如果设置为Neutral,则系统语言将失去一次匹配的机会)。例如:软件准备投放欧洲市场,项目只准备了英、法、德、意4国语言,你可以把英语设置为Neutral Default。当用户语言和系统语言都不是法、德、意时,均使用英语;如果不这么配置,就会使用语言ID最小的那个语言(德语)。显然,在欧洲,认识英文的人应该比认识德文的人多,这样的用户体验更好。
当然,最好的方案还是提供所有语言!其实,也不是很难!关键是工欲善其事,必先利其器!
2001年,有个叫Phil Jollans(http://www.jollans.com)的德国小伙子做了一个叫做World Resource String Editor的VB6插件,可以直接从excel中导入字符串资源!有图有真相:
我就是用这个工具,只需要填写中国(中文)那一列的内容,把文件分发给各个翻译组,再汇总导入,工作之轻松令人发指!什么?西班牙语有20个国家?来吧,我把西班牙(西班牙)那一列复制20列,再把子语言ID一换,轻松支持所有西班牙地区的国家。羽扇纶巾,樯橹灰飞烟灭的感觉!满足需求都弱爆了,预测需求才能立于不败之地。
但预测需求也只是个中级水平,只有引领需求才是最高境界!那什么才是最高境界呢?说到这里,上面已经解决了80%的问题,但只花费了20%的功力,下面的内容解决剩下20%的问题,但需要80%的功力。
其实,微软的整个策略还是对需求了解得不够充分,略显简单粗暴。例如:比利时、瑞士等几个欧洲小国也说法语,它对法国的标准法语进行了一些扩展,就像方言一样,懂比利时法语的人肯定也懂法国的标准法语,这是个非常自然的需求。所以,资源提取的完整最佳策略应该是这样的:
当程序请求资源时,只需要输入一个资源ID参数,运行时库首先使用用户语言ID与请求的资源ID所拥有的所有语言ID进行匹配,如果成功,就把请求的资源ID和匹配成功的语言ID对应的资源返回;如果不成功,就把用户语言ID的主语言ID与请求的资源ID所拥有的所有语言ID的主语言ID进行匹配,如果成功,就把请求的资源ID和匹配成功的主语言ID里面子语言ID最小的那个语言ID对应的资源返回;如果不成功,则返回主语言ID为0且子语言ID也为0的资源;如果没找到,再使用系统语言ID与请求的资源ID所拥有的所有语言ID进行匹配,如果成功,就把请求的资源ID和匹配成功的语言ID对应的资源返回;如果不成功,就把系统语言ID的主语言ID与请求的资源ID所拥有的所有语言ID的主语言ID进行匹配,如果成功,就把请求的资源ID和匹配成功的主语言ID里面子语言ID最小的那个语言ID对应的资源返回;如果还不成功,则把请求的资源ID所拥有的所有语言ID中ID值最小的那个对应的资源返回。我已经竭尽毕生才华企图让阿甘都能看懂了,如果还看不懂,就洗洗睡吧,不求甚解才是真正的幸福。
好吧,终极对决如期而至。这么屌暴天的需求怎么优雅地实现呢?翻阅MSDN上所有关于多国语言资源方面的资料,发现一套修改资源的API:BeginUpdateResource, UpdateResource和EndUpdateResource,可以修改资源的语言ID,但还是太复杂,严重损坏VB6的简单美形象。换一个思路,既然改资源那么累,能不能改用户语言呢?又发现了这么一个函数SetLocaleInfo,可以设置用户语言。哈哈,这样的话,大体思路就出来了:程序启动后,首先调用一个调整函数:这个函数首先获取用户语言,与已有的资源语言比对,如果有完全匹配的,就可以退出了;如果没有完全匹配的,再使用用户语言的主语言与已有的资源语言的主语言比对,如果有匹配的,就用SetLocaleInfo把用户语言设置为匹配了主语言的那个语言ID。这样,资源里只需提供一种主语言的标准语言即可支持使用这个主语言的所有国家。系统语言的修改非常麻烦,要调用一系列API,最致命的是需要重启操作系统,这个基本上就放弃了。但也无大碍,因为用户语言才是最重要的依据,匹配到系统语言上多少有点掩耳盗铃的感觉。
但是,SetLocaleInfo可是真真切切地修改了用户语言,即使你退出前再把原来的用户语言改回去,也会影响到其它在你之后启动的应用,更何况你的程序有可能崩溃,连改回去的机会都没有。
再经过仔细搜索,又发现这么一个函数SetThreadLocale,参数更简单,最重要的是其修改的语言信息只在本线程内起作用,并不真正地修改用户语言。这下大家满足了吧!
有意见,请加群3867100讨论,口令:图乃大。或发邮件[email protected],标题注明图乃大。