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都要这么搞,实在太麻烦。
所以虽然这种方法在网上很流行,不少支持NUI的软件都这么玩,但我还是不想这么干。
方案二:原创,根本性解决问题
- 参照ExecuteDlgInit的代码写一段combobox初始化代码,先把DLGINIT中的初始字符串从ANSI转换成Unicode后,再调用SendDlgItemMessageW插入comobobox。
- 写一个通用的对话框初始化函数,先周游对话框下的所有控件,删掉已经初始化过的combobox中的内容,再用上面的代码对combobox重新初始化。
- 在每一个对话框、PropertyPage的OnInitDialog()函数中,在调用完基类的OnInitDialog()函数后,调用上面这个初始化函数对combobox进行初始化。
与方案一相比,方案二显然简单得多,且能够使用资源编辑器设置combobox的初始化data,所以我用的就是这个方案。
二、消息框(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++都能看到。
三、PropertySheet按钮文字不按照设定语言显示的原因与解决方案
原因很简单,没有相应的语言包,即mui文件。所以最简单的办法还是安装语言包,如果实在不想或不能安装,再考虑下面的解决方法。
做产品式的解决方法:
- 从CPropertySheet派生出一个类来,重载OnInitDialog(),在其中对标准按钮(IDOK、IDCANCEL、ID_APPLY_NOW、IDHELP)的文字,按照选定语言用SetWindowText进行设置。
- 缺省情况下CPropertySheet、CPropertyPage不管资源编辑器中选择了什么字体、字号,一律按系统设定的字体、字号显示,令人不爽,正好在派生类中一并解决了。我的DjVuToy、TiffToy等软件就是这么玩的。
如果采用这种方案,CPrintDialogEx也要进行派生,然后重载DefWindowProc()函数,在其中处理WM_INITDIALOG函数,对按钮文字进行设置。
做项目式的解决方法:
用SetWindowsHookEx装一个消息钩子(WH_CALLWNDPROC),对WM_INITDIALOG消息进行监视,发现是PropertySheet,就查找按钮并重置按钮的文字。判断PropertySheet的依据:
- 自身的ClassName是"#32770"。
- 含SysTabControl32控件。
- 含4个按钮:
const static int IDs[] = {IDOK, IDCANCEL, IDD_APPLYNOW, IDHELP};
用这种方法,顺便也解决了CPrintDialogEx的按钮问题,因为CPrintDialogEx的主窗口本来就是一个PropertySheet。
四、SHBrowseForFolder按钮和提示文字不按照设定语言显示的原因与解决方案
原因和上面一样,没有相应的语言包。所以只有实在不想或不能安装语言包,再考虑下面的解决方法。
做产品式的解决方法:
- 把BROWSEINFO结构体的lpfn指针指向一个自定义的消息处理函数。
- 在该消息处理函数中,收到BFFM_INITIALIZED消息后,自己设置标题、按钮、提示。其中对于IDD_FOLDERLABLE要注意检查是否有足够的空间显示全部文字,否则可能会自动折行。
- 缺省SHBrowseForFolder显示的对话框尺寸太小,在处理BFFM_INITIALIZED消息时顺便可以扩展一下对话框。
SHBrowseForFolder的完整源代码在Windows 2000、XP、2003的源代码中都可以找到,对话框中的ID自然也在里面。我写的Pdg2Pic等软件就是这么玩的,所以选择文件夹的对话框看起来比别家的要大气一点。
做项目式的解决方法:
- 用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环境下,运行后各对话框文字、按钮仍然能显示简体中文。
链接:https://pan.baidu.com/s/11irniZke-hUgvDpim1knSA
提取码:uvk0
MFC软件国际化的几个问题及其解决方案的更多相关文章
- Atitit 软件国际化原理与概论
Atitit 软件国际化原理与概论 语言和文化习俗因地域不同而差别很大.对某一特定的地域的 语言环境称为"locale".它不仅包括语言和货币单位,而且还包括 数字标示格式, 日期 ...
- IOS软件国际化(本地化Localizable)
IOS软件国际化(本地化Localizable) iPhone是支持语言最多的手机,它支持各国语言及中国少数名族如蒙古等语言,这也是好多少数名族都用苹果的原因.在这一点上我们自主品牌还是要多学习学习. ...
- Chrome浏览器扩展开发系列之十八:扩展的软件国际化chrome.i18n API
i18n是internationalization 的简写,这里将讨论软件国际化的问题.熟悉软件国际化的朋友应该知道,软件国际化要求,页面中所有用户可见的字符串都必须置于资源属性文件中.资源属性文件中 ...
- JavaWeb开发——软件国际化(动态元素国际化)
软件国际化的第二个部分,就是动态元素国际化. 数值,货币,时间,日期等数据由于可能在程序运行时动态产生,所以无法像文字一样简单地将它们从应用程序中分离出来,而是需要特殊处理.Java 中提供了解决这些 ...
- JavaWeb开发——软件国际化(文本元素国际化)
前几天围绕着JDBC编程进行了系统的学习.现在我们对Java程序数据库操作已经是轻车熟路了.也学会了使用各种框架来帮助我们简化编程. 今天是学习计划的第七天,虽然学习热情没有前几天高涨了.但是,写博客 ...
- PC端的软件端口和adb 5037端口冲突解决方案
引用https://www.aliyun.com/jiaocheng/32552.html 阿里云 > 教程中心 > android教程 > PC端的软件端口和adb 50 ...
- Iso language code table之(软件国际化)
ISO 639是用来区分所有已知的语言规范的术语.每种语言都分配两个字母(639-1)或三个英文字母(639-2和639-3),小写字母的缩写,修订后的版本命名的.该系统是非常有用的语言学家和人类学家 ...
- MFC框架下Opengl窗口闪屏问题解决方案
转自https://blog.csdn.net/niusiqiang/article/details/43116153 虽然启用了双缓冲,但是仍然会出闪屏的情况,这是由于OpenGL自己有刷新背景的函 ...
- TestLink测试软件安装条件检查不通过的解决方案
在第一次安装的时候出现这个错误信息 解决办法: 修改config.inc.php文件里的两个属性值为: $tlCfg->log_path = TL_ABS_PATH . 'logs' . DIR ...
随机推荐
- tp5 商品模型的添加展示
路由 //商品模型展示的路由 Route::get('type','/pyg/good/listType'); //将type_id传送至/pyg/good/addType的路由 Route::get ...
- insert一个表的数据到另外一个表
insert into a(real_name,is_main,mobile,password,property_id,create_time) select linkman as real_name ...
- Docker 部署xxl-job 报错:xxl-rpc remoting error(connect timed out), for url : xxxxxx
使用Docker 部署的xxl-job,当调度中心和执行器部署在不同的容器内,此时xxl-job调用执行器的服务就会报: address:http://172.0.0.1:8841/ code:500 ...
- C盘爆满,你的专属清道夫来啦
一.C盘目录介绍 ■ 本人电脑信息:联想拯救者Win10 家庭版 1.C盘根目录默认目录情况: Intel:装Intel驱动,解压文件的临时文件,之前百度了解到它是一个有关Intel芯片信息的文件,可 ...
- 2022年官网下安装Studio 3T最全版与官网查阅方法(无需注册下载版)
目录 一.环境 1.构建工具(参考工具部署方式) 2.保持启动 二.下载安装 1.百度搜索,或者访问官网:https://robomongo.org/,选择下载进入下载页. 2.进入下载页,选择如下下 ...
- HIve的基本使用
WHERE从表中筛选行: SELECT从表中查询指定的列: group by在列上做聚合. -- 假设数据文件的内容,字段之间以ASCII 001(ctrl-A)分隔,行之间以换行分隔. CREATE ...
- ES77
PUT rr_bd202_chaos_20211220{ "aliases" : { "rr_bd202_chaos_pgold":{} }, "ma ...
- oracle数据库导入导出语句
一.导出: 导出语句: expdp sanyayun/sanyayun@syerpdb directory=DMP dumpfile=fooderp.dmp content=all SCHEMAS=s ...
- 如何批量修改图片名称(win下)
深度学习目标检测任务中常常需要大量的图片,这些图片一般来自网络爬虫或是自行批量下载,但下载下的图片常常在保存时被命名为长段英文数字混写,因此规律化命名下载的图片数据名称就显得尤为重要了,下面我演示在本 ...
- Rocket Mq 常用API 及简单运维
RocketMQ 常用API 消息 消息消费模式 消息消费模式由消费者来决定,可以由消费者设置MessageModel来决定消息模式. 消息模式默认为集群消费模式 consumer.setMessag ...