编写 Win32 多语言用户界面应用程序
Windows 2000 针对全球市场制定了新的增强支持标准,提供了许多国际化功能,例如完全支持 Unicode、预设支持数百种语言以及用于从右向左语言的镜像技术。在 Microsoft Systems Journal (MSJ) 中已经发表了多篇文章来说明如何针对 Windows 2000 来编写 Unicode、复杂脚本以及镜像的应用程序。
Windows 2000 多语言用户界面(也称为 MUI)是 Windows 2000 中一个最有用的国际化功能,它允许用户将系统的用户界面 (UI) 语言设置并更改为 Windows 2000 已发布的任何本地化语言。
本文将介绍如何在您的应用程序中提供可切换的 UI 功能。在本文中,您将:
- 了解为什么应该考虑投资编写多语言应用程序。
- 查看三种不同的多语言用户界面实现方式示例。
- 寻找建议的最佳方法及指导原则和代码示例。
本页内容
为什么要费力编写多语言应用程序?
有多种原因导致需要编写多语言应用程序。最明显的原因当然是通过打入国际市场(毕竟现在已经进入了互联网时代)来增加收入和提高软件销售量,此外还有两个有力的观点:
- 发布单一的二进制文件。通过向所有平台(和所有不同的语言版本)发布一个核心功能二进制文件来尽可能减少开发的繁琐程度和成本,从而避免对每种语言的源代码都分别提供编译条件和维护。Microsoft Windows 2000、Microsoft Office 2000 和 Microsoft Internet Explorer 5.0 均为单一的二进制文件 – 例如,美国英语、日语和阿拉伯语版本的 Windows 2000 均随附了相同的核心 gdi32.dll。未来 Service Pack 中对此模块的潜在更新可应用到所有语言,而无需任何其他技术方面的工作。
- 避免成为自身的竞争者。延迟发布不同语言版本的软件可能会严重影响客户的部署,并进而影响您的收入。在发布了应用程序的英语版 1.0 后,让客户等上几个月再发布德语版 1.0 似乎也没什么大不了的。但许多客户在部署之前,会一直等待应用程序首个更新的发布。如向原始版本的发布增量添加更新的发布增量,对客户部署的延误将远远超出您的预计。确保应用程序为多语言应用程序有助于避免出现这种情况。
为什么采用 Unicode?
必须要知道,无论使用哪种方法来发布本地化产品,应用程序都必须完全支持 Unicode。通过实现 Unicode 支持,您将不必再操心应用程序运行平台的语言和代码页支持问题。Unicode 是一种 16 位字符的编码,可表示全球各地常用的大多数语言,这明显不同于旧的 8 位字符编码(例如 ANSI,它将语言支持限制为大约 220 个不同字符)。支持 Unicode 的应用程序可处理和显示其中任意一种语言的字符。要了解有关实现 Unicode 和发布将要在 Windows 9x 和 Windows NT 上运行的单一二进制文件的详细信息,请参阅有关使用 Microsoft Layer for Unicode 开发应用程序的文章。.
Windows 2000 通过系统区域设置(从“区域选项”控制面板配置,如图 1 所示)选项来支持传统的 ANSI 应用程序。这意味着基于 ANSI 的应用程序的行为完全取决于系统设置(图 2)。更好的一种方法是全面采用 Unicode 字符编码。
图 1:系统区域设置是通过区域选项控制面板中的设置默认值…按钮来配置的。 |
图 2:在系统区域设置与应用程序语言不匹配的 Windows 2000 系统上运行的 ANSI 本地化消息框。 |
实现多语言用户界面
通常有三种方法可用来实现多语言用户界面二进制:
- 依赖于语言的二进制文件和内置资源
- 一个核心二进制文件和一个国际化资源 DLL
- 一个核心二进制文件和一个与每种目标语言对应的资源 DLL
图 3 展示了使用上述每种方法针对英语、德语和日语的国际化应用程序进行的文件分发。
依赖于语言的二进制文件
一个核心二进制文件和一个国际化资源 DLL
基于语言的附属 DLL
图 3:文件分发比较
以下部分将介绍每种实现的优点、缺点以及技术限制。下面是一个重要的假设:无论用户界面的语言是什么,都会发布一个核心二进制文件并且应用程序的核心功能均相同。这应该是多语言用户界面所有实现中的终极目标。
方法 1:依赖于语言的二进制文件
此传统方法包括编译一个二进制文件,既有源代码又有资源。每种目标语言都需要一个单独的二进制文件。
方法 1(依赖于语言的二进制文件)的优缺点总结 | |
优点 | 缺点 |
|
|
依赖于语言的二进制文件的缺点使得此方法现已被淘汰。以下介绍的两种方法更适合于需要切换 UI 的应用程序。
方法 2:适用于所有语言的一个资源 DLL
此方法的主要思想是将资源从源代码中分离出来,创建一个包含所有目标语言的全部本地化资源的仅资源 DLL。相同资源 ID 的多个副本在不同语言标记下的 RC 文件中定义。在以下示例中,针对法语和英语定义了字符串 ID IDS_ENUMSTRTEST。
// French (France) resources
#ifdef _WIN32LANGUAGE LANG_FRENCH, SUBLANG_FRENCH
#pragma code_page(1252)
#endif //_WIN32
// String Table
STRINGTABLE DISCARDABLE
BEGINIDS_ENUMSTRTEST "Cette phrase est en français...(France)"
END
#endif // French (France) resources
// English (U.S.) resources
#ifdef _WIN32
LANGUAGE LANG_ENGLISH,SUBLANG_ENGLISH_US
#pragma code_page(1252)
#endif _win32
// String TableSTRINGTABLE DISCARDABLE
BEGINIDS_ENUMSTRTEST "This is an English string...(USA)"
END
#endif // English (U.S.) resources
为了在运行时访问资源,因此通过 LoadLibrary API 加载了资源 DLL。然后,可使用 EnumResourceLanguages 来查找给定控件/资源的可用语言列表,使用 FindResourceEx 来确定具有指定类型、名称和语言的资源的位置。要显示给定的语言,接下来只需选择 DLL 中正确的资源即可。可通过重新加载新选择的资源并刷新客户端区域来实现语言的切换。
必须注意,Windows 资源加载程序始终使用默认的当前用户区域设置(线程区域设置会继承当前登录用户的用户区域设置 – 请参阅图 1)。GetThreadLocale 允许查询线程区域设置。在此方法中,加载 API 的预定义资源(LoadIcon、LoadString、LoadCursor…)始终返回与此区域设置相关的资源。例如,在以上资源示例中,如果系统区域设置被设为英语 (0x0409),将无法使用 LoadString 来加载法语资源。但实际并非完全如此:线程区域设置在创建线程时被继承后,将独立于用户区域设置,这意味着在这种情况下,可使用 SetThreadLocale 将线程区域设置改为法语 (0x040C),然后可调用 LoadString 来获取法语字符串。
// if our thread locale is English to start with…
g_hInst = LoadLibrary(_TEXT(“intl_res.dll”));
LoadString(g_hInst, IDS_ENUMSTRTEST,g_szTemp, MAX_STR);
// g_szTemp would then point to the English resources
// changing our thread locale to French.
// Always make sure that French is in fact one of the
// valid languages returned by EnumResourceLanguages()
// Save a copy of the current thread-locale to set back later
Lcid = GetThreadLocale();SetThreadLocale(MAKELCID(0x040c, SORT_DEFAULT));
LoadString(g_hInst, IDS_ENUMSTRTEST, g_szTemp, MAX_STR);
// g_szTemp would then point to the French resources
在此要注意的是,如果线程区域设置与当前选择的用户区域设置相同,系统的资源加载程序将默认使用语言 ID 0(中立)。如果所需的资源被定义为中立语言,则会返回此值。否则将会枚举所有语言资源(以语言 ID 顺序),并返回首个匹配的资源 ID(不论其语言是什么)。
可举例来说明此问题,比如在美国英语和法国法语资源中,用户区域设置被设为日语,则:原始线程区域设置将是日语 (0x0411)。如果找不到字符串 ID 且枚举时英语 (0x0409) 在法语 (0x40C) 之前,则首次调用 LoadString 时会在 0x0411 版本后返回英语资源。接下来,如果用户区域设置首先是法语:原始线程区域设置将是法语 (0x040C)。由于用户区域设置和线程区域设置匹配,因此系统资源加载程序将使用语言 ID 0 并开始枚举资源,然后再次返回英语版本!
对此的最佳解决方法是首先放弃更改线程区域设置。可通过调用 FindResourceEx 从多语言资源文件中手动加载资源。另一种解决方法是在所有者定义的语言标记中定义资源。在以上的 RC 示例中,英语部分定义为:
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
这将防止线程区域设置被切换回美国英语。但是,将资源定义为:
LANGUAGE LANG_ENGLISH, SUBLANG_NEUTRAL
将保证 SetThreadLocale 调用的成功,因为此语言 ID 没有匹配的用户区域设置,仍可继续使用预定义的资源加载程序 API。
方法 2(适用于所有语言的一个资源 DLL)的优缺点总结 | |
优点 | 缺点 |
|
|
方法 3:每种目标语言一个资源 DLL
这是先前技术的扩展。现在不是在单个 DLL 中包含所有语言,而是针对每种语言创建一个单独的 DLL。Microsoft Office 2000、Microsoft Internet Explorer 5.x 和 Microsoft Windows 2000 使用的都是这种方法。每种产品的实现不同,此方法的名称也各异,但附属 DLL 是这种方法的常用名称。
此方法会再一次用到要通过 LoadLibrary API 加载的资源 DLL。但这一次,必须选择适当的语言 DLL。通常应假设 Windows UI 语言是用户的首选语言,应用程序可通过加载此语言资源来启动。接下来,当用户选择另一种语言时,可释放当前语言并加载新语言。当然,此方法会使用新加载的资源重新创建窗口并初始化对话框。
系统 UI 语言的检测在不同版本的 Windows 中其处理方式也互不相同:
- 在 Windows 2000 中,可利用 GetUserDefaultUILanguage API 来查找当前用户的 UI 语言(请注意,在 Windows 2000 多语言版本中,可以有选择了不同 UI 语言的不同用户)。
- 在 Windows 95、Windows 98 和 Windows 98 SE 中,UI 语言存储在注册表的以下位置:HKCU\Control Panel\Desktop\ResourceLocale。此注册表项将以十六进制形式返回 UI 的语言 ID (LangID),例如,英语是 00000409。
- 在 Windows NT 3.5x 和 Windows NT 4.0 中,由于缺少相关的 API 和一致的注册表项,因此检查操作系统语言的最安全方式是检查 NTDLL.DLL 的版本戳记。此文件的语言与用户界面的语言相同。同样,唯一例外情况是阿拉伯语、希伯来语和泰语,这时版本戳记可帮助您检测启用的操作系统。具体步骤如下:
- 加载 ntdll.dll 文件
- 枚举版本戳记资源中的语言
- 如果有多种语言可用,它将是非美国英语语言
- 如果仅找到美国英语,则可能仍需要处理启用的语言,并且应该对活动代码页进行检查。
由于语言检测 API 和注册表项会返回 UI 语言的 LangID,因此对附属 DLL 进行相应命名会非常有用:不要将英语文件命名为 res_eng.dll 或 res_en.dll,而应使用 res409.dll。
以下代码示例显示了如何在 Windows 2000 中检测 UI 语言并加载正确的资源 DLL:
wLangId = GetUserDefaultUILanguage();
_stprintf(g_tcsTemp, _TEXT("res%x.dll"), wLangId);
if((hRes = LoadLibrary(g_tcsTemp)) == NULL)
{
// we didn‘t find the desired language satellite DLL, lets go with English (default).
hRes = LoadLibrary(_TEXT("res409.dll"));
}
如果未能加载相应的语言 DLL(例如,在应用程序中仅支持操作系统语言的一个子集),唯一的解决方案是假设系统中存在默认/首选语言 DLL。
另一种可能的方法是将英语(或默认语言)资源包括到可执行主文件中并发布其他语言的附属 DLL(方法 1 和 3 的混合)。由于此方法并无实际优点况且英语毕竟只是另一本地化语言,因此不再详细讨论此技术。
方法 3(每种目标语言一个资源 DLL)的优缺点总结 | |
优点 | 缺点 |
|
|
摘要
编写单一二进制代码是实现真正的全球通用产品的第一步,并且有助于降低开发和支持成本。实现多语言用户界面以允许用户在所有支持的语言之间切换,是达成令全球客户满意的下一个逻辑步骤。通过编写识别 Unicode 的代码和创建语言资源的附属 DLL,即可达成这些目标,轻松程度可能会超出您的想像。
术语表
线程区域设置 - 给定线程所处的区域设置。是在创建时从当前用户区域设置继承来的,可以在运行时改为有效的任何区域设置(按线程)。通过调用 NLS API,可使用此区域设置来格式化数字、日期、时间...
系统区域设置 - 并非真正的区域设置。可决定将要支持哪个脚本非 Unicode 应用程序。因此,在应用程序角度来看,它是系统模拟出来的区域设置。系统区域设置的作用范围是整个系统,因此适用于所有用户。更改系统区域设置需要重新启动。
UI 语言 - 操作系统显示其菜单、帮助文件和对话框时所使用的语言。
用户区域设置 - 格式化日期、货币、数字等项目时用户的首选项。用户区域设置是针对每个用户设置的,无需重新启动或注销/登录。