作者:馬健
郵箱: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"的控制元件。
順便一提,MFC現在建議選擇資料夾對話方塊應該用CFolderPickerDialog,但這個的介面與檔案選擇對話方塊CFileDialog基本一樣,太容易混淆,所以在我的軟體中仍然用修改過的SHBrowseForFolder選擇資料夾。
五、部分關鍵原始碼及測試例項
上面二、三、四部分如果都用訊息鉤子實現,則其鉤子相關函式如下:
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