XP下MFC程序本地化研究
2024-03-17 17:33:41

MFC 的本地化方案

在 MFC 程序中,本地化是通过资源文件来完成的。菜单栏、对话框、字符串,图片等等资源都支持多个语言的副本。

在框架内部,是通过 CreateDialogDialogBoxLoadMenuLoadStringFindResource 来查找资源的。比如查找字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
inline const ATLSTRINGRESOURCEIMAGE* AtlGetStringResourceImage(
_In_ HINSTANCE hInstance,
_In_ UINT id) throw()
{
HRSRC hResource;
/*
The and operation (& static_cast<WORD>(~0)) protects the expression from being greater
than WORD - this would cause a runtime error when the application is compiled with /RTCc flag.
*/
hResource = ::FindResourceW(hInstance, MAKEINTRESOURCEW( (((id>>4)+1) & static_cast<WORD>(~0)) ), (LPWSTR) RT_STRING);
if( hResource == NULL )
{
return( NULL );
}

return _AtlGetStringResourceImage( hInstance, hResource, id );
}

这些函数有个特点:跟根据当前线程语言环境来定位资源
当一个线程创建时,它使用用户语言环境,该值由 GetUserDefaultLCID 返回。
也就是说,在默认情况下资源加载是根据用户的区域设置来的决定。

改变线程语言环境

使用 SetThreadLocale 可以改变当前线程的区域设置,从而改变 MFC 加载资源的语言。

1
2
3
4
5
// 将语言环境改为 英语(美国)
::SetThreadLocale(MAKELCID(MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US), SORT_DEFAULT));

CString title;
title.LoadString(AFX_IDS_APP_TITLE); // 会尝试加载 英语(美国) 字符资源

但是这个函数有明显的缺点,会影响所有涉及到字符串操作的地方,比如在中文系统上,线程语言改为英文,为用户提供一个通用对话框:

1
2
3
::SetThreadLocale(MAKELCID(MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US), SORT_DEFAULT));
CFileDialog dlg...
...

如果此时用户选了一个带有中文字符的路径,对话框将会得到一些乱码路径,问题还不仅仅只是这里而已。
所以不要使用 SetThreadLocale 来修改语言环境,除非你清楚自己在做什么。
正确做法应该是自行通过 FindResourceEx 来查找资源,这个函数不受线程语言影响,可以指定语言,但是也无法再使用 MFC 自带的一些加载资源的方法了。

线程UI语言

从 Vista 开始,提出了一个线程UI语言的概念,这解决了上面提到的SetThreadLocale影响全局的问题。通过 SetThreadUILanguageSetThreadPreferredUILanguages 来改变当前线程的UI语言,而它仅仅只影响资源加载时的语言版本,不会影响线程的语言设置

1
2
3
LCID loc = ::GetThreadLocale();  // 2052
::SetThreadUILanguage(MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US));
loc = ::GetThreadLocale(); // 还是 2052

但在 XP 系统下,这个函数会同时设置ThreadLocale

1
2
3
4
5
LCID loc = ::GetThreadLocale();  // 2052
::SetThreadUILanguage(MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US));
loc = ::GetThreadLocale(); // 1033
::SetThreadUILanguage(MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED));
loc = ::GetThreadLocale(); // 还是 1033

设置为英语后就改不回来了,很奇怪,抱着好奇的心态用IDA看了一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
LANGID __stdcall SetThreadUILanguage(LANGID LangId)
{
int v1; // eax
UINT ConsoleOutputCP; // eax
int v3; // esi
LCID v4; // esi
LCID v6; // esi
LCID CurrentLocale; // [esp+8h] [ebp-3Ch]
UINT v8; // [esp+Ch] [ebp-38h]
int v9; // [esp+10h] [ebp-34h] BYREF
int v10; // [esp+14h] [ebp-30h] BYREF
int v11; // [esp+18h] [ebp-2Ch]
LCID Locale; // [esp+1Ch] [ebp-28h]
WCHAR LCData[16]; // [esp+20h] [ebp-24h] BYREF

LOWORD(v1) = GetUserDefaultUILanguage();
v11 = v1;
CurrentLocale = NtCurrentTeb()->CurrentLocale;
Locale = 1033;
ConsoleOutputCP = GetConsoleOutputCP();
v8 = ConsoleOutputCP;
if ( dword_7C88593C || (v6 = gSystemLocale) == 0 )
{
v3 = dword_7C885938;
}
else
{
GetLocaleInfoW(gSystemLocale, 0x1004u, LCData, 16);
NlsConvertStringToIntegerW(-1, &dword_7C88593C);
GetLocaleInfoW(v6, 0xBu, LCData, 16);
NlsConvertStringToIntegerW(-1, &dword_7C885940);
ConsoleOutputCP = v8;
v3 = v6 & 0x3FF;
dword_7C885938 = v3;
}
if ( (_WORD)v11 )
{
v4 = (unsigned __int16)v11;
GetLocaleInfoW((unsigned __int16)v11, 0x1004u, LCData, 16);
NlsConvertStringToIntegerW(-1, &v9);
GetLocaleInfoW(v4, 0xBu, LCData, 16);
NlsConvertStringToIntegerW(-1, &v10);
v3 = dword_7C885938;
ConsoleOutputCP = v8;
}
if ( ConsoleOutputCP
&& v3 != 1
&& v3 != 13
&& v3 != 42
&& v3 != 30
&& (ConsoleOutputCP == dword_7C88593C || ConsoleOutputCP == dword_7C885940)
&& (ConsoleOutputCP == v9 || ConsoleOutputCP == v10) )
{
Locale = (unsigned __int16)v11;
}
if ( Locale != CurrentLocale && !SetThreadLocale(Locale) )
return CurrentLocale;
return Locale;
}

中间一大段不用看,看结尾两行的逻辑:只有在当前线程 LCID 不等于英语(美国)时才会调用SetThreadLocale,这解释了为什么设置为英语后就改不回去了。
因为SetThreadUILanguage在 Vista 后有不同的行为,所以也不能完全否定它的作用。

XP下的多语言解决方案

因为 XP 系统没有将线程语言和UI语言分离,所以不可以使用SetThreadLocaleSetThreadUILanguage去解决UI多语言问题。
有其他两个方案选择:

  1. 为每种语言单独制作一个资源型DLL,在APP初始化前用 AfxSetResourceHandle 设置一下,这样之后任何资源加载都会从这个DLL中获取。
  2. 非常规手段,HookFindResourceLoadMenu等等与资源加载的相关函数,在资源加载前设置语言环境,资源加载后还原语言环境。

第一种方法属于正规手段,但是很繁琐,维护起来很麻烦。
第二种方法可以将所有资源集中在EXE中,维护更方便。不过除了FindResourceEx以外函数无法指定语言ID,所以每次加载资源时需要切换ThreadLocale

参考

用户界面语言管理
SetThreadLocale function
SetThreadUILanguage function
创建纯资源 DLL
MFC 组件的本地化
MFC基于对话框使用dll进行多语言切换