MFC 的本地化方案 在 MFC 程序中,本地化是通过资源文件来完成的。菜单栏、对话框、字符串,图片等等资源都支持多个语言的副本。 在框架内部,是通过 CreateDialog 、DialogBox 、LoadMenu 、LoadString 、FindResource 来查找资源的。比如查找字符串:
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; 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
影响全局的问题。通过 SetThreadUILanguage 或 SetThreadPreferredUILanguages 来改变当前线程的UI语言,而它仅仅只影响资源加载时的语言版本,不会影响线程的语言设置
1 2 3 LCID loc = ::GetThreadLocale (); ::SetThreadUILanguage (MAKELANGID (LANG_ENGLISH, SUBLANG_ENGLISH_US)); loc = ::GetThreadLocale ();
但在 XP 系统下,这个函数会同时设置ThreadLocale
1 2 3 4 5 LCID loc = ::GetThreadLocale (); ::SetThreadUILanguage (MAKELANGID (LANG_ENGLISH, SUBLANG_ENGLISH_US)); loc = ::GetThreadLocale (); ::SetThreadUILanguage (MAKELANGID (LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED)); loc = ::GetThreadLocale ();
设置为英语后就改不回来了,很奇怪,抱着好奇的心态用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; UINT ConsoleOutputCP; int v3; LCID v4; LCID v6; LCID CurrentLocale; UINT v8; int v9; int v10; int v11; LCID Locale; WCHAR LCData[16 ]; 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, 0x1004 u, LCData, 16 ); NlsConvertStringToIntegerW (-1 , &dword_7C88593C); GetLocaleInfoW (v6, 0xB u, 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, 0x1004 u, LCData, 16 ); NlsConvertStringToIntegerW (-1 , &v9); GetLocaleInfoW (v4, 0xB u, 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语言分离,所以不可以使用SetThreadLocale
或SetThreadUILanguage
去解决UI多语言问题。 有其他两个方案选择:
为每种语言单独制作一个资源型DLL,在APP初始化前用 AfxSetResourceHandle 设置一下,这样之后任何资源加载都会从这个DLL中获取。
非常规手段,HookFindResource
、LoadMenu
等等与资源加载的相关函数,在资源加载前设置语言环境,资源加载后还原语言环境。
第一种方法属于正规手段,但是很繁琐,维护起来很麻烦。 第二种方法可以将所有资源集中在EXE中,维护更方便。不过除了FindResourceEx
以外函数无法指定语言ID,所以每次加载资源时需要切换ThreadLocale
。
参考 用户界面语言管理 SetThreadLocale function SetThreadUILanguage function 创建纯资源 DLL MFC 组件的本地化 MFC基于对话框使用dll进行多语言切换