MFC软件国际化的几个问题及其解决方案
作者:马健
邮箱:stronghorse_mj@hotmail.com
主页:https://www.cnblogs.com/stronghorse/
以前我以为PDG相关软件只会在国内流行,所以发行简体中文版足矣,没想到现在流传到繁体中文环境下去了,还被人报告在繁体中文Windows下,Unicode版软件界面出现乱码。所以上网查了一下国际化多语言用户界面(Multilingual User Interface,MUI)技术,发现还有一些问题需要解决,所以把解决过程记录下来,形成这篇笔记。
=============================================================
目前网上能查到的基于MFC的多语言用户界面(MUI)实现,基本上都是对同一个资源ID复制不同的语言备份,然后在应用初始化时调用SetThreadLocale(XP)、SetThreadUILanguage(Vista+)设置语言,让FindResource函数自动根据所设置的语言读取对应的资源。这样做能达到以下效果:
- 如果同一资源ID有不同语言的备份,则FindResource会自动按照所设置的语言选择一个,从而达到根据用户选项切换界面语言文字的目的。
- 对于afxdlgs.h中定义的公共对话框,包括文件选择、字体选择、打印设置、查找替换等,也会自动按照所设置的语言显示按钮和文字。
- 项目的字符集必须设置为Unicode,否则在非同族语言下不论怎么搞都是乱码。
- PropertySheet、MessageBox的按钮不管是用SetThreadLocale还是SetThreadUILanguage设置,都会显示Windows当前语言的文字,如英文Windows下显示的按钮文字就是OK而不是“确定”,即使已经用SetThreadUILanguage设置了简体中文。
- 受PropertySheet影响,打印机选择对话框(CPrintDialogEx)左下角的两个按钮也会按当前语言显示。
- SHBrowseForFolder中的标题、按钮、提示不管用SetThreadLocale还是SetThreadUILanguage设置,都会按照Windows当前语言显示。
- 如果在资源编辑器中设置了下拉框(ComboBox)的中文data,即简体中文的初始化文字,则在其他语言下会出现乱码,包括对话框(Dialog)、PropertyPage中的下拉框都是这样。
一、ComboBox的中文data在其他语言下出现乱码的原因及解决方案
ComboBox初始化出现乱码的原因分析:
- 在CDialog::OnInitDialog()下断点,跟踪进去,可以看到一开始就调用CWnd::ExecuteDlgInit(LPCTSTR lpszResourceName)函数。
- 在CWnd::ExecuteDlgInit(LPCTSTR lpszResourceName)中,根据对话框ID来FindResource、LoadResource,LockResource,然后调用CWnd::ExecuteDlgInit(LPVOID lpResource)。
- 在CWnd::ExecuteDlgInit(LPVOID lpResource)中,关键是下面的代码:
#ifndef _AFX_NO_OCC_SUPPORT
else if (nMsg == LB_ADDSTRING || nMsg == CB_ADDSTRING)
#endif // !_AFX_NO_OCC_SUPPORT
{
// List/Combobox returns -1 for error
if (::SendDlgItemMessageA(m_hWnd, nIDC, nMsg, 0, (LPARAM) lpnRes) == -1)
bSuccess = FALSE;
}
因此:
- 尽管VC已经用Unicode编码保存资源文件(.rc文件),但资源文件的DLGINIT数据段,仍然按照传统采用ANSI编码保存combobox和listbox的初始data。
- 在CWnd::ExecuteDlgInit(LPVOID lpResource)函数中,读取到DLGINIT数据段中的ANSI编码字符串后,直接用ANSI版的SendDlgItemMessageA发消息对combobox和listbox进行初始化,即逐一插入初始化字符串。
- 反编译user32.dll可以看出,SendDlgItemMessageA内部是GetDlgItem、SendMessageA。
- 由于combobo已经设置成Unicode,SendMessageA自动按照当前代码页(ACP)转码成Unicode,而不是按SetThreadUILanguage所设置的语言转码,导致出现乱码。
方案一:流行,但回避矛盾
既然MFC的初始化代码会导致乱码,那么combobox的初始值就干脆不在资源编辑器里设置,而是独立成一条字符串放到string table里,用的时候从资源里读取出来,自己拆解后插入combobox。
特点:
- 不能利用资源编辑器所见即所得的便利,combobox的大小不好控制。
- 每个combobox都要这么搞,实在太麻烦。
方案二:原创,根本性解决问题
- 参照ExecuteDlgInit的代码写一段combobox初始化代码,先把DLGINIT中的初始字符串从ANSI转换成Unicode后,再调用SendDlgItemMessageW插入comobobox。
- 写一个通用的对话框初始化函数,先周游对话框下的所有控件,删掉已经初始化过的combobox中的内容,再用上面的代码对combobox重新初始化。
- 在每一个对话框、PropertyPage的OnInitDialog()函数中,在调用完基类的OnInitDialog()函数后,调用上面这个初始化函数对combobox进行初始化。
二、消息框(MessageBox)的按钮文字没有按照设定语言显示文字的原因及解决方案
原因分析:
查了一下Windows XP的源代码,对消息框是这样实现的:
int MessageBoxW( HWND hwndOwner, LPCWSTR lpszText, LPCWSTR lpszCaption, UINT wStyle) { EMIGETRETURNADDRESS(); return MessageBoxExW(hwndOwner, lpszText, lpszCaption, wStyle, 0); }int MessageBoxExW( HWND hwndOwner, LPCWSTR lpszText, LPCWSTR lpszCaption, UINT wStyle, WORD wLanguageId) { return MessageBoxTimeoutW(hwndOwner, lpszText, lpszCaption, wStyle, wLanguageId, INFINITE); }
为保险起见,反编译了win10下的user32.dll做对照,发现win0果然有所长进,没有采用这种俄罗斯套娃式的低效代码,而是在MessageBoxW函数中直接:
return MessageBoxTimeoutW(hwndOwner, lpszText, lpszCaption, wStyle, 0, INFINITE);
同样win10下的MessageBoxExW,也是直接:
return MessageBoxTimeoutW(hwndOwner, lpszText, lpszCaption, wStyle, wLanguageId, INFINITE);
即不论XP还是Win10,调用MessageBox,均相当于用MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL)参数调用MessageBoxEx。所以网上有些传言说不应该用MessageBox,而应该用MessageBoxEx,其实是不对的,因为源代码和反编译代码都说明二者等价。
本来按照MSDN对MessageBoxEx函数的说法,用MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL)参数调用MessageBoxEx,应该按照当前线程所设置的语言显示按钮文字,这些文字存放在对应语言文件夹下的user32.dll.mui文件的资源中。
但问题在于简体中文Windows下有en-US\user32.dll.mui,但原版英文Windows下却没有zh-CN\user32.dll.mui。所以设置为英语后,在简体中文Windows下消息框按钮显示为OK,但设置为简体中文后,在英文Windows下消息框按钮仍然是OK而不是“确定”,除非在英文版Windows下已经安装过中文语言包。
解决办法可以有多种:
- 要求用户安装微软发行的Windows简体中文语言包,这是最简单、最正宗的方法。
- 如果不能,用户要求也不高,要不就这么算了吧,因为按照Windows缺省语言显示的按钮文字,用户肯定看得懂,所以虽然影响观瞻,但不影响使用。
- 如果要求比较高,可以参考wine或Windows XP源代码中的MessageBox实现代码,自己写一个,对11个按钮想按照什么语言、文字SetWindowText都可以。wine的源代码简单一些,没有声音、没有copy功能,消息框的对话框模板也在rc文件中定义。Windows源代码的实现水平要更高一些,消息框的对话框模板都不屑于在资源中定义,而是按需在内存中动态生成,我初见的时候也懵了一下,感觉如果真能看懂,编程水平都要涨一截。
- 如果想简单点,就用SetWindowsHookEx装一个消息钩子(WH_CALLWNDPROC),对WM_INITDIALOG消息进行监视,发现初始化的是消息框,就查找按钮并重置按钮的文字。
- window style含DS_ABSALIGN、DS_NOIDLEMSG。一般其他对话框很少含这两个style。
- 如果调用的是AfxMessageBox,而不是直接调用::MessageBox,则除了MB_ABORTRETRYIGNORE、MB_RETRYCANCEL风格之外的消息框都会带一个icon,这个icon的ID是20,style含SS_ICON,ClassName是Static。以上这些通过Spy++都能看到。
原因很简单,没有相应的语言包,即mui文件。所以最简单的办法还是安装语言包,如果实在不想或不能安装,再考虑下面的解决方法。
做产品式的解决方法:
- 从CPropertySheet派生出一个类来,重载OnInitDialog(),在其中对标准按钮(IDOK、IDCANCEL、ID_APPLY_NOW、IDHELP)的文字,按照选定语言用SetWindowText进行设置。
- 缺省情况下CPropertySheet、CPropertyPage不管资源编辑器中选择了什么字体、字号,一律按系统设定的字体、字号显示,令人不爽,正好在派生类中一并解决了。我的DjVuToy、TiffToy等软件就是这么玩的。
做项目式的解决方法:
用SetWindowsHookEx装一个消息钩子(WH_CALLWNDPROC),对WM_INITDIALOG消息进行监视,发现是PropertySheet,就查找按钮并重置按钮的文字。判断PropertySheet的依据:
- 自身的ClassName是"#32770"。
- 含SysTabControl32控件。
- 含4个按钮:
const static int IDs[] = {IDOK, IDCANCEL, IDD_APPLYNOW, IDHELP};
四、SHBrowseForFolder按钮和提示文字不按照设定语言显示的原因与解决方案
原因和上面一样,没有相应的语言包。所以只有实在不想或不能安装语言包,再考虑下面的解决方法。
做产品式的解决方法:
- 把BROWSEINFO结构体的lpfn指针指向一个自定义的消息处理函数。
- 在该消息处理函数中,收到BFFM_INITIALIZED消息后,自己设置标题、按钮、提示。其中对于IDD_FOLDERLABLE要注意检查是否有足够的空间显示全部文字,否则可能会自动折行。
- 缺省SHBrowseForFolder显示的对话框尺寸太小,在处理BFFM_INITIALIZED消息时顺便可以扩展一下对话框。
做项目式的解决方法:
- 用SetWindowsHookEx装一个消息钩子(WH_CALLWNDPROC),对WM_PARENTNOTIFY消息进行监视,发现是SHBrowseForFolder,就查找按钮并重置按钮的文字。
- 判断SHBrowseForFolder的依据:含有ClassName是"SHBrowseForFolder ShellNameSpace Control"的控件。
上面二、三、四部分如果都用消息钩子实现,则其钩子相关函数如下:
HHOOK g_hMsgHook4MUI = NULL; static LRESULT CALLBACK CallMsgWndProc( int nCode, WPARAM wParam, LPARAM lParam ) { // 先调用原始的消息处理函数,处理WM_INITDIALOG等消息 LRESULT ret = CallNextHookEx(g_hMsgHook4MUI, nCode, wParam, lParam); CWPSTRUCT* pStruc = (CWPSTRUCT*)lParam; if (wParam == 0) { if (pStruc->message == WM_INITDIALOG) { if (IsMsgBox(pStruc->hwnd)) FixMsgBoxButtons(pStruc->hwnd); else if (IsPropertySheet(pStruc->hwnd)) FixPropertySheet(pStruc->hwnd); } else if (pStruc->message == WM_PARENTNOTIFY && pStruc->wParam == BFFM_INITIALIZED) { if (IsSHBrowseForFolder(pStruc->hwnd)) FixSHBrowseForFolder(pStruc->hwnd); } } return ret; }void InstallMsgHook4MUI() { g_hMsgHook4MUI = SetWindowsHookEx(WH_CALLWNDPROC, CallMsgWndProc, NULL, ::GetCurrentThreadId()); }void UnInstallMsgHook4MUI() { if ( g_hMsgHook4MUI != NULL ) { if ( UnhookWindowsHookEx( g_hMsgHook4MUI ) != 0 ) g_hMsgHook4MUI = NULL; } }
然后在App的InitInstance(),或主对话框的OnInitDialog()里,调用InstallMsgHook4MUI()安装钩子;在App的ExitInstance(),或主对话框的OnDestroy()里调用UnInstallMsgHook4MUI()取消钩子。
当然在App的InitInstance()函数里,别忘了调用
SetThreadUILanguage(MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED));
对语言进行设置。
按照上面说明实现的一个测试例子见下面链接,在未安装简体中文的Windows环境下,运行后各对话框文字、按钮仍然能显示简体中文。
【MFC软件国际化的几个问题及其解决方案】链接:https://pan.baidu.com/s/11irniZke-hUgvDpim1knSA
提取码:uvk0
推荐阅读
- 软件测试|web自动化测试入门到精通---selenium详解
- 架构|软件工程之美-笔记
- 社工|如何防止社工钓鱼——软件伪造
- 戏说|Linux基础(软件包管理)
- Mac|Adobe Premiere Pro 2021 for Mac v15.4.1 强大的视频编辑软件
- 实用软件|Mathematica,亮瞎你的双眼
- 实用软件|Mathematica绘制函数
- 软件测试实战项目,问题答疑
- 小白入行|0基础能学“软件测试”吗(好学吗?怎么学?)
- 软件测试|学会这篇至少涨薪10K(appium+python+jenkins自动化测试框架持续集成)