作者:马健
邮箱:stronghorse_mj@hotmail.com主页:https://www.cnblogs.com/stronghorse/

以前我以为PDG相关软件只会在国内流行,所以发行简体中文版足矣,没想到现在流传到繁体中文环境下去了,还被人报告在繁体中文Windows下,Unicode版软件界面出现乱码。所以上网查了一下国际化多语言用户界面(Multilingual User Interface,MUI)技术,发现还有一些问题需要解决,所以把解决过程记录下来,形成这篇笔记。

=============================================================

目前网上能查到的基于MFC的多语言用户界面(MUI)实现,基本上都是对同一个资源ID复制不同的语言备份,然后在应用初始化时调用SetThreadLocale(XP)、SetThreadUILanguage(Vista+)设置语言,让FindResource函数自动根据所设置的语言读取对应的资源。这样做能达到以下效果:

  1. 如果同一资源ID有不同语言的备份,则FindResource会自动按照所设置的语言选择一个,从而达到根据用户选项切换界面语言文字的目的。
  2. 对于afxdlgs.h中定义的公共对话框,包括文件选择、字体选择、打印设置、查找替换等,也会自动按照所设置的语言显示按钮和文字。

但也存在下列问题:

  1. 项目的字符集必须设置为Unicode,否则在非同族语言下不论怎么搞都是乱码。
  2. PropertySheet、MessageBox的按钮不管是用SetThreadLocale还是SetThreadUILanguage设置,都会显示Windows当前语言的文字,如英文Windows下显示的按钮文字就是OK而不是“确定”,即使已经用SetThreadUILanguage设置了简体中文。
  3. 受PropertySheet影响,打印机选择对话框(CPrintDialogEx)左下角的两个按钮也会按当前语言显示。
  4. SHBrowseForFolder中的标题、按钮、提示不管用SetThreadLocale还是SetThreadUILanguage设置,都会按照Windows当前语言显示。
  5. 如果在资源编辑器中设置了下拉框(ComboBox)的中文data,即简体中文的初始化文字,则在其他语言下会出现乱码,包括对话框(Dialog)、PropertyPage中的下拉框都是这样。

以上问题至少我目前没有在网上找到答案,所以下面的分析及解决方案除非特殊说明,均为原创。

一、ComboBox的中文data在其他语言下出现乱码的原因及解决方案

ComboBox初始化出现乱码的原因分析:

  1. 在CDialog::OnInitDialog()下断点,跟踪进去,可以看到一开始就调用CWnd::ExecuteDlgInit(LPCTSTR lpszResourceName)函数。
  2. 在CWnd::ExecuteDlgInit(LPCTSTR lpszResourceName)中,根据对话框ID来FindResource、LoadResource,LockResource,然后调用CWnd::ExecuteDlgInit(LPVOID lpResource)。
  3. 在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;
}

因此:

  1. 尽管VC已经用Unicode编码保存资源文件(.rc文件),但资源文件的DLGINIT数据段,仍然按照传统采用ANSI编码保存combobox和listbox的初始data。
  2. 在CWnd::ExecuteDlgInit(LPVOID lpResource)函数中,读取到DLGINIT数据段中的ANSI编码字符串后,直接用ANSI版的SendDlgItemMessageA发消息对combobox和listbox进行初始化,即逐一插入初始化字符串。
  3. 反编译user32.dll可以看出,SendDlgItemMessageA内部是GetDlgItem、SendMessageA。
  4. 由于combobo已经设置成Unicode,SendMessageA自动按照当前代码页(ACP)转码成Unicode,而不是按SetThreadUILanguage所设置的语言转码,导致出现乱码。

解决方案有两种:

方案一:流行,但回避矛盾

既然MFC的初始化代码会导致乱码,那么combobox的初始值就干脆不在资源编辑器里设置,而是独立成一条字符串放到string table里,用的时候从资源里读取出来,自己拆解后插入combobox。

特点:

  1. 不能利用资源编辑器所见即所得的便利,combobox的大小不好控制。
  2. 每个combobox都要这么搞,实在太麻烦。

所以虽然这种方法在网上很流行,不少支持NUI的软件都这么玩,但我还是不想这么干。

方案二:原创,根本性解决问题

  1. 参照ExecuteDlgInit的代码写一段combobox初始化代码,先把DLGINIT中的初始字符串从ANSI转换成Unicode后,再调用SendDlgItemMessageW插入comobobox。
  2. 写一个通用的对话框初始化函数,先周游对话框下的所有控件,删掉已经初始化过的combobox中的内容,再用上面的代码对combobox重新初始化。
  3. 在每一个对话框、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下已经安装过中文语言包。

解决办法可以有多种:

  1. 要求用户安装微软发行的Windows简体中文语言包,这是最简单、最正宗的方法。
  2. 如果不能,用户要求也不高,要不就这么算了吧,因为按照Windows缺省语言显示的按钮文字,用户肯定看得懂,所以虽然影响观瞻,但不影响使用。
  3. 如果要求比较高,可以参考wine或Windows XP源代码中的MessageBox实现代码,自己写一个,对11个按钮想按照什么语言、文字SetWindowText都可以。wine的源代码简单一些,没有声音、没有copy功能,消息框的对话框模板也在rc文件中定义。Windows源代码的实现水平要更高一些,消息框的对话框模板都不屑于在资源中定义,而是按需在内存中动态生成,我初见的时候也懵了一下,感觉如果真能看懂,编程水平都要涨一截。
  4. 如果想简单点,就用SetWindowsHookEx装一个消息钩子(WH_CALLWNDPROC),对WM_INITDIALOG消息进行监视,发现初始化的是消息框,就查找按钮并重置按钮的文字。

在消息钩子中判断消息框的依据:

  1. window style含DS_ABSALIGN、DS_NOIDLEMSG。一般其他对话框很少含这两个style。
  2. 如果调用的是AfxMessageBox,而不是直接调用::MessageBox,则除了MB_ABORTRETRYIGNORE、MB_RETRYCANCEL风格之外的消息框都会带一个icon,这个icon的ID是20,style含SS_ICON,ClassName是Static。以上这些通过Spy++都能看到。

三、PropertySheet按钮文字不按照设定语言显示的原因与解决方案

原因很简单,没有相应的语言包,即mui文件。所以最简单的办法还是安装语言包,如果实在不想或不能安装,再考虑下面的解决方法。

做产品式的解决方法:

  1. 从CPropertySheet派生出一个类来,重载OnInitDialog(),在其中对标准按钮(IDOK、IDCANCEL、ID_APPLY_NOW、IDHELP)的文字,按照选定语言用SetWindowText进行设置。
  2. 缺省情况下CPropertySheet、CPropertyPage不管资源编辑器中选择了什么字体、字号,一律按系统设定的字体、字号显示,令人不爽,正好在派生类中一并解决了。我的DjVuToy、TiffToy等软件就是这么玩的。

如果采用这种方案,CPrintDialogEx也要进行派生,然后重载DefWindowProc()函数,在其中处理WM_INITDIALOG函数,对按钮文字进行设置。

做项目式的解决方法:

用SetWindowsHookEx装一个消息钩子(WH_CALLWNDPROC),对WM_INITDIALOG消息进行监视,发现是PropertySheet,就查找按钮并重置按钮的文字。判断PropertySheet的依据:

  1. 自身的ClassName是"#32770"。
  2. 含SysTabControl32控件。
  3. 含4个按钮:
    const static int IDs[] = {IDOK, IDCANCEL, IDD_APPLYNOW, IDHELP};

用这种方法,顺便也解决了CPrintDialogEx的按钮问题,因为CPrintDialogEx的主窗口本来就是一个PropertySheet。

四、SHBrowseForFolder按钮和提示文字不按照设定语言显示的原因与解决方案

原因和上面一样,没有相应的语言包。所以只有实在不想或不能安装语言包,再考虑下面的解决方法。

做产品式的解决方法:

  1. 把BROWSEINFO结构体的lpfn指针指向一个自定义的消息处理函数。
  2. 在该消息处理函数中,收到BFFM_INITIALIZED消息后,自己设置标题、按钮、提示。其中对于IDD_FOLDERLABLE要注意检查是否有足够的空间显示全部文字,否则可能会自动折行。
  3. 缺省SHBrowseForFolder显示的对话框尺寸太小,在处理BFFM_INITIALIZED消息时顺便可以扩展一下对话框。

SHBrowseForFolder的完整源代码在Windows 2000、XP、2003的源代码中都可以找到,对话框中的ID自然也在里面。我写的Pdg2Pic等软件就是这么玩的,所以选择文件夹的对话框看起来比别家的要大气一点。

做项目式的解决方法:

  1. 用SetWindowsHookEx装一个消息钩子(WH_CALLWNDPROC),对WM_PARENTNOTIFY消息进行监视,发现是SHBrowseForFolder,就查找按钮并重置按钮的文字。
  2. 判断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软件国际化的几个问题及其解决方案的更多相关文章

  1. Atitit 软件国际化原理与概论

    Atitit 软件国际化原理与概论 语言和文化习俗因地域不同而差别很大.对某一特定的地域的 语言环境称为"locale".它不仅包括语言和货币单位,而且还包括 数字标示格式, 日期 ...

  2. IOS软件国际化(本地化Localizable)

    IOS软件国际化(本地化Localizable) iPhone是支持语言最多的手机,它支持各国语言及中国少数名族如蒙古等语言,这也是好多少数名族都用苹果的原因.在这一点上我们自主品牌还是要多学习学习. ...

  3. Chrome浏览器扩展开发系列之十八:扩展的软件国际化chrome.i18n API

    i18n是internationalization 的简写,这里将讨论软件国际化的问题.熟悉软件国际化的朋友应该知道,软件国际化要求,页面中所有用户可见的字符串都必须置于资源属性文件中.资源属性文件中 ...

  4. JavaWeb开发——软件国际化(动态元素国际化)

    软件国际化的第二个部分,就是动态元素国际化. 数值,货币,时间,日期等数据由于可能在程序运行时动态产生,所以无法像文字一样简单地将它们从应用程序中分离出来,而是需要特殊处理.Java 中提供了解决这些 ...

  5. JavaWeb开发——软件国际化(文本元素国际化)

    前几天围绕着JDBC编程进行了系统的学习.现在我们对Java程序数据库操作已经是轻车熟路了.也学会了使用各种框架来帮助我们简化编程. 今天是学习计划的第七天,虽然学习热情没有前几天高涨了.但是,写博客 ...

  6. PC端的软件端口和adb 5037端口冲突解决方案

    引用https://www.aliyun.com/jiaocheng/32552.html 阿里云 >  教程中心   >  android教程 >  PC端的软件端口和adb 50 ...

  7. Iso language code table之(软件国际化)

    ISO 639是用来区分所有已知的语言规范的术语.每种语言都分配两个字母(639-1)或三个英文字母(639-2和639-3),小写字母的缩写,修订后的版本命名的.该系统是非常有用的语言学家和人类学家 ...

  8. MFC框架下Opengl窗口闪屏问题解决方案

    转自https://blog.csdn.net/niusiqiang/article/details/43116153 虽然启用了双缓冲,但是仍然会出闪屏的情况,这是由于OpenGL自己有刷新背景的函 ...

  9. TestLink测试软件安装条件检查不通过的解决方案

    在第一次安装的时候出现这个错误信息 解决办法: 修改config.inc.php文件里的两个属性值为: $tlCfg->log_path = TL_ABS_PATH . 'logs' . DIR ...

随机推荐

  1. LGP4463题解

    这玩意儿怎么看上去就很经典啊( 哦互不相同啊,那没事了( 考虑一个 \(\rm DP\).由于限制了互不相同,那么我们考虑从值域开始想. 设 \(dp_{n,k}\) 为在 \([1,n]\) 中选了 ...

  2. 《前端运维》一、Linux基础--06Shell流程控制

    这章我们来学习下流程控制,简单来说就是逻辑判断和循环的写法.并不复杂,我们来简单地看下. 1.if语句 shell的if语句有两种写法,一种是shell脚本式的,一种是命令式的. if conditi ...

  3. 面板Panel

    面板 主要步骤: 1.new一个frame窗口 格式 Frame frame = new Frame() 2.设置窗口的大小.位置.可见性 3.设置frame窗口的布局格式(分为流式布局,东西南北中, ...

  4. Linux 显示文件大小的命令

    ll显示的是字节,可以使用-h参数来提高文件大小的可读性,另外ll不是命令,是ls -l的别名ls -al 是以字节单位显示文件或者文件夹大小: 字节b,千字节kb, 1G=1024M=1024*10 ...

  5. 嵌套OOPS导致系统卡死 每个CPU都上报softlockup的问题

    问题现象:在ARM服务器上,构造oops异常,本应该产生panic,进入dump流程,并且系统重启,但是系统并未重启,而是出现了卡死,在串口会隔一段时间就循环打印调用栈信息.如下所示 linux-fA ...

  6. MSSQL得知密码后getshell

    本文用了 sql server 2000和sql server 2008 MSSQL连接 连接MSSQL 2000 新建连接: 填写目的IP.目的端口.用户名.密码: 一直下一步,完成后,数据库导航窗 ...

  7. CF 920A Water The Garden

    本题可以看做是一个数学题 因为 在第 1 和第 3 个洒水器之间的 花园灌溉的时间只要 (1 + 3 ) >> 1 - 1 + 1;//这么长的时间 那么我么就可以以此类推到 从而我么可以 ...

  8. [邮件服务]Foxmail安装及配置指引(Windows)

    操作指引: Step1:访问Foxmail邮箱官网:https://www.foxmail.com/,选择"Windows"版,点击"立即下载". Step2: ...

  9. Redis 集群,集群的原理是什么?

    1).Redis Sentinal 着眼于高可用,在 master 宕机时会自动将 slave 提升为master,继续提供服务. 2).Redis Cluster 着眼于扩展性,在单个 redis ...

  10. Java中自动装箱与拆箱

    一.什么是封装类? Java中存在基础数据类型,但是在某些情况下,我们要对基础数据类型进行对象的操作,例如,集合中只能存在对象,而不能存在基础数据类型,于是便出现了包装器类.包装器类型就是对基本数据类 ...