VC實現介面多語言化
幾種常見的多語言化方法
VC實現多語言化有多種方法,下面簡要介紹了幾種方法並進行了比較。
方法1:為每個資源項(對話方塊、字串、選單等)建立一個或者多個副本,並將其內容更改為目標語言,並在程式啟動時呼叫SetThreadLocale()函式設定語言環境,以後程式就會自動呼叫指定語言版本的資源了。如需從資源載入一個字串,程式碼如下:
CString s;
s.LoadString(IDS_MESSAGE);
//Do whatever with s…
但是LoadString()函式不是直接返回字串,使用它會增加程式碼行數,以下函式可解決此問題:
static __inline
CString LS(int ids)
{
CString s;
s.LoadString(ids);
return s;
}
AfxMessagebox(LS(IDS_MESSAGE));
1 2 3 4 5 6 7 8 9 |
方法2:使用一個純資源DLL來實現不同語言的動態載入。
新建一個DLL專案,將exe專案的res資料夾、rc檔案和resource.h標頭檔案一併拷貝到新專案目錄,然後編新專案生成資源DLL檔案。在exe專案啟動時呼叫AfxLoadLibrary和AfxSetResourceHandle指示需要載入資源的來源。
下一節將以此方法為例詳細說明如何實現多語言化。
方法3:使用GNU gettext機制
首先建立基於不同語言的po檔案,裡面分別存放著對同一ID的不同語言的描述,然後將其編譯為mo檔案供gettext函式使用。po檔案是可讀的,推薦用poedit軟體來編輯,而mo檔案是二進位制的。在使用gettext機制的程式裡,容易看到以下程式碼片段:
#define _(String) gettext (String)#define LOCALEDIR "/usr/share/locale/"#define PACKAGE "foonly" int main(int argc, char* argv[]) {setlocale (LC_ALL, "");bindtextdomain (PACKAGE, LOCALEDIR);textdomain (PACKAGE);printf(_("Hello, GetText!\n"));return 0;}
2345678910111213#define _(String) gettext (String)#define LOCALEDIR "/usr/share/locale/"#define PACKAGE "foonly" int main(int argc, char* argv[]) {setlocale (LC_ALL, "");bindtextdomain (PACKAGE, LOCALEDIR);textdomain (PACKAGE);printf(_("Hello, GetText!\n"));return 0;}但是gettext機制還沒有移植到Windows下,所以具體過程並不像上面說的這麼容易。然而可貴的是思想,我們可以用ini檔案來實現一個簡單的gettext機制,當然需要付出多寫幾百行程式碼的代價 :(
方法4:使用Xml檔案。如果你瞭解Android開發,那麼你可能早就想到了這種方法。在Android應用程式中,豈止是國際化問題,就連介面佈局都是基於Xml的,Google已經為開發者做好了基礎工作(InFlate函式),開發者只需要按照規範來編寫XML檔案就可以了。但是有點同方法3一樣,VC並沒有提供這種機制,必須要開發者自己實現才行,比如使用libxml2庫。用這種方法的好處是,普通翻譯人員就可以修改xml檔案來輔助開發。這幾種方法的比較:
方法 | 來源 | 優點 | 缺點 | 使用頻率 |
資源副本 | VC預設 | 無需編寫額外的程式碼和新建專案,實現快 | 當要翻譯成的語言版本較多時,會造成資源臃腫和執行時記憶體佔用量大;不利於多人合作翻譯 | 一般 |
資源DLL | VC預設 | 保持原專案資源的簡潔;利於人員分工進行翻譯;減小執行時內容佔用; | 當要新增新的資源時需要同步所有語言專案中的資源 | 較高 |
gettext | GNU Linux | 程式碼比較簡練;提高可移植性;簡化翻譯過程;減小執行時內容佔用; | 需要開發人員來實現此機制 | 較少 |
xml | Android | 使程式更加模組化;提高可移植性;簡化翻譯過程;減小執行時內容佔用; | 需要開發人員來實現此機制 | 較少 |
資源DLL實現多語言化
以下內容基於VC 6.0。要實現介面多語言化,必須要先配置專案使其支援Unicode編碼,文章對此有詳細介紹。
首先建立一個基於MFC的工程,在選擇語言時選擇 中文[簡體,中國]。
專案框架選擇對話方塊、單文件和多文件都可以,這裡就選個基於對話方塊的吧,然後立即修該專案屬性使其支援Unicode。
MFC已經為我們加入了一些資源,包括2個對話方塊、1個String Table等。為了節省空間修改了對話方塊的大小。
現在專案中的資源情況如下:
然後再新建一個DLL專案,為了方便管理可以將其新增到當前工作空間中。
立即修改此DLL專案使其支援Unicode,然後設定專案依賴性(Project->Dependencies…) ,資源DLL要在exe專案之前編譯:
為了將生成的資源DLL自動拷貝到exe專案執行目錄下,還需要修改DLL專案的Post-build Step:
然後將主專案目錄下的res資料夾、resource.h、TestMultiLang.rc檔案拷貝到ResENG專案目錄下,res資料夾和resource.h直接替換,將ResENG.rc刪除後再將TestMultiLang.rc重新命名為ResENG.rc。
切換到資源檢視就會發現這兩個專案的資源內容是一樣的了,將ResENG專案的資源更改為英文如下:
為了讓程式在啟動的時候載入英文語言資源,需要在CTestMultiLangApp::InitInstance()函式中Dlg建立之前新增如下語句:
HINSTANCE hLanguageDll = AfxLoadLibrary(_T("ResENG")); if (hLanguageDll) AfxSetResourceHandle(hLanguageDll);
1 2 3 |
最後重新編譯TestMultiLang專案,執行就會發現對話方塊已經是英文介面了 :)
動態實現語言切換
以上程式僅為示例,為了能使程式自動選擇合適的語言,還需要做許多工作。比如要在程式中新增程式語言切換選單或語言選擇下拉列表框,並將使用者喜好儲存在ini配置檔案中,然後在程式啟動時自動讀取此ini檔案載入相應的資源DLL;如果使用者未設定語言,則預設根據作業系統語言載入合適資源,如果不存在針對此語言的資源DLL,就使用最國際化的語言——英語。將實現如上功能的程式碼包裝成一個函式如下:
void CTestMultiLangApp::LoadLanguage(){CString strPath, strIni, strLang, strDLL;HINSTANCE hLanguageDll, hLanguageNow;CIni ini; //儲存本身的資源控制代碼static HINSTANCE hOriginalHandle = ::AfxGetResourceHandle(); //讀取ini配置檔案GetModulePath(strPath);strIni = strPath + "data\\config\\language.ini";ini.SetPathName(strIni);strLang = ini.GetString(_T("Default"), _T("Prefer"), _T("")); //根據使用者喜好來設定if (strLang == _T("English"))//英文hLanguageDll = ::AfxLoadLibrary(_T("ResENG"));else if (strLang == _T("Chinese"))//中文hLanguageDll = hOriginalHandle;else{//使用者未指定,則根據系統選擇合適語言,預設為英文WORD wLangPID = PRIMARYLANGID(GetSystemDefaultLangID()); if (wLangPID == LANG_CHINESE)hLanguageDll = hOriginalHandle;else if (wLangPID == LANG_ENGLISH)hLanguageDll = ::AfxLoadLibrary(_T("ResENG"));elsehLanguageDll = ::AfxLoadLibrary(_T("ResENG"));} //儲存已載入的資源DLL控制代碼hLanguageNow = ::AfxGetResourceHandle(); //載入新的資源DLLif(hLanguageDll)::AfxSetResourceHandle(hLanguageDll); //釋放之前載入的資源DLLif (hLanguageNow != hOriginalHandle)FreeLibrary(hLanguageNow);}
因為在切換介面語言的過程中需要頻繁地銷燬和建立對話方塊,所以我們也把建立對話方塊的程式碼包裝成一個函式:
BOOL CTestMultiLangApp::OpenWindow(){CTestMultiLangDlg dlg;m_pMainWnd = &dlg;dlg.DoModal();}
1 2 3 4 5 6 |
現在在InitInstance()函式裡簡單得呼叫一下這兩個函式就可以了:
BOOL CTestMultiLangApp::InitInstance()
{
AfxEnableControlContainer();
#ifdef _AFXDLL
Enable3dControls();// Call this when using MFC in a shared DLL
#else
Enable3dControlsStatic();// Call this when linking to MFC statically
#endif
LoadLanguage();
if (!OpenWindow())
return FALSE;
return FALSE;
}
然後要實現使用者介面的語言選擇訊息響應函式。該函式要將使用者的選擇儲存到ini檔案裡,銷燬當前視窗,然後再重新載入資源並建立新的視窗:
void CMainFrame::OnSelLanguage(UINT nID)
{
CString strPath, strIni, strLang;
CIni ini;
GetModulePath(strPath);
strIni = strPath + "data\\config\\language.ini";
ini.SetPathName(strIni);
if (nID == ID_EDT_LANG_ENG)
strLang = _T("English");
else if (nID == ID_EDT_LANG_CHN)
strLang = _T("Chinese");
//寫入配置檔案
ini.WriteString(_T("Default"), _T("Prefer"), strLang);
//銷燬當前視窗
CTestMultiLangApp*pApp = (CTestMultiLangApp*)AfxGetApp();
pApp->m_pMainWnd = NULL;
this->DestroyWindow();
//建立新的視窗
pApp->LoadLanguage();
pApp->OpenWindow();
}
上面這段程式碼中pApp->m_pMainWnd = NULL這一句是關鍵,它切斷了銷燬當前程式訊息向上的路由,因此程序不會被銷燬,才有了機會重新建立新的視窗。如果專案是基於單文件/多文件的程式的話,還要加一句pApp->m_pDocManager = NULL才可以。
重新編譯專案,現在語言就可以動態切換了。
繼續新增其他語言
如果專案不僅要支援中英文這兩種語言,比如還需要支援德語,這裡介紹兩種方法來實現。
第一種方法顯而易見————新建一個資源DLL專案。理論上這是可行的,但是有個棘手的問題:如果需要支援的語言很多,那麼對程式資源的任何更改都需要同步更新到其他所有資源專案!
第二種方法是借用工具,由一個資源DLL製作出其他各語言版本的資源DLL。這個工具軟體會把資源DLL中的字串、對話方塊、選單項等資源提取出來並儲存到一個po檔案裡,之後使用poedit就可以進行翻譯了。翻譯完成後再根據此po檔案和原資源DLL生成新的資源DLL。
工具軟體截圖如下: