資源DLL與語言選擇菜單的實現

 
資源DLL與語言選擇菜單的實現
 
 
         簡介
         在當今這個發展越來越快的世界中,軟件的本地化及翻譯工作越來越重要,極大地關係到軟件的銷量及普及率;就拿常見的Win32/MFC程序來說,一個比較方便的辦法就是附加單獨的資源DLL文件。
         本文介紹了一種易於應用的方法,可在C++/MFC程序中支持多種語言,並演示了怎樣用少量的代碼添加對資源DLL的支持,這包含了兩個方面:根據用戶偏好在程序開始時自動選擇最合適的語言;提供一個語言選擇子菜單(以供用戶自行選擇)。如圖所示:
 
 
 
         關於資源DLL
         有關在程序中支持多語言最靈活的方法,也許就是使用所謂的“資源DLL”了,其主要思想是爲每種語言創建一個單獨的DLL文件,而這個DLL中包含了已翻譯爲某種特定語言的程序資源;因此,如果你的程序最初版本是中文,且又翻譯成了法文、德文、日文,這樣你就有了三個資源DLL。中文資源在.exe文件中,而其他三種語言各自對應一個DLL文件。
         如果程序中又需要支持某種新的語言,只須簡單地添加一個DLL到安裝文件中就行了。在程序運行時,會根據用戶偏好,相應地加載資源DLL。
         資源DLL可由一個專門的Visual Studio工程來創建,也可由某些專用的工具來創建,本文以Visual Studio 2003來創建,Visual Studio 2005也差不多。順便提一下,把所有的語言資源都打包進EXE文件,在理論上是可行的,但在實際中卻行不通;因爲,大多數加載資源的高層API——如LoadString()、DialogBox()等等——不會讓你指定想要的語言,而SetThreadLocale()也不會如預期那樣工作(此API在Win9X中不存在)。
 
 
         一步一步支持資源DLL
         以下是在主程序中,添加支持資源DLL(語言選擇菜單)的步驟:
 
1、 把LanguageSupport.h及LanguageSupport.cpp添加到你的工程。
在MyApp.h及MyApp.cpp(假定CMyApp是工程類)中,加入以下黑體行:
 
#include "LanguageSupport.h"
 
class CMyApp : public CWinApp
{
public:
   CLanguageSupport m_LanguageSupport;
   ...
};
BOOL CMyApp::InitInstance()
{
   //把以下這行註釋掉,防止MFC進行自己的資源DLL處理。
   // CWinApp::InitInstance();
   ...
   SetRegistryKey(_T("MyCompany"));
 
   //根據用戶偏好,加載相應的資源DLL。
   m_LanguageSupport.LoadBestLanguage();
   ...
}
 
         在主菜單中,添加一個名爲“語言(Language)”的菜單項。接下來,在CMainFrame類中,爲“語言(Language)”菜單項添加一個菜單更新處理程序,假定名爲OnUpdateToolsLanguage(),如下所示:
 
void CMainFrame::OnUpdateToolsLanguage(CCmdUI *pCmdUI)
{
 
   //創建語言子菜單(只在菜單第一次打開時)。
   theApp.m_LanguageSupport.CreateMenu(pCmdUI);
}
 
 
2、 爲語言選擇菜單項添加菜單處理程序。
在MainFrm.h中,把處理程序afx_msg void OnLanguage(UINT nID)添加在CMainFrame的protected部分中某處;在MainFrm.cpp中,添加定義及消息映射入口:
 
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
...
ON_COMMAND_RANGE(ID_LANGUAGE_FIRST,ID_LANGUAGE_LAST, OnLanguage)
//這些ID聲明在LanguageSupport.h中
END_MESSAGE_MAP()
 
void CMainFrame::OnLanguage(UINT nID)
{ //用戶選擇了菜單中的某種語言
 theApp.m_LanguageSupport.OnSwitchLanguage(nID);
}
 
         注意,這個處理程序不能用嚮導來添加,因爲它是一個COMMAND_RANGE菜單處理程序——用於處理“語言(Language)”菜單中所有語言項。
 
 
3、 也是最後一步,在字符資源表中,添加一個名爲IDS_RESTART的字符串,值爲“請重新運行%1”,%1可爲程序名。
 
 
怎樣創建資源DLL
         首先,CLanguageSupport類假定所有的DLL都名爲MyAppXXX.dll,這裏MyApp.exe是可執行文件名,XXX是所支持語言的三個字母縮寫(CHN代表中文、FRA代表法文、DEU代表德文、JPN代表日文)。同時,exe文件與dll文件都應有一個Version版本信息資源,其語言匹配文件名中的三個縮寫字母。接着,我們創建一個Win32 DLL工程:
 
1、 打開Visual Studio 2003,選擇文件-新建-工程,輸入工程名如MyAppDEU創建一個德文版本,單擊確定;在程序設置頁,選擇DLL及空項目。
2、 把它轉換爲一個資源DLL:打開項目屬性,在“配置”下拉框中選擇“所有配置”;打開鏈接器-高級,將“純資源DLL”設爲“是”。另外,如果你使用Visual C++ 6.0,則需要手工在鏈接設置的編輯框中添加 /NOENTRY作爲命令行選項;而Visual C++ 2005,與Visual C++ 2003類似,在鏈接器-高級裏,選擇“無入口點”爲“是”。
3、 創建一份EXE資源文件的副本,並把它添加到DLL工程中:建議將MyApp.rc文件改名成所需的語言版本,如MyAppDEU.rc。
4、 修改路徑:在資源視圖中,鼠標右鍵單擊MyAppDEU.rc,打開“資源包含”,修改所有包含資源文件的路徑。每種語言都有會一個子目錄l.xxx,例如,修改#include "afxres.rc"爲#include "l.deu/afxres.rc"。
5、 設置語言屬性:在資源視圖中,打開“版本信息version info”(如果沒有就創建一個),設置語言屬性爲正確的語言,如German(Germany),確保語言匹配DLL名中的三個字母。
 
         現在可以編譯DLL了,之後便可得到一個資源DLL,當然,它還沒有經過翻譯,但這已是翻譯者的任務了。按照上述步驟就可創建出一系統語言的DLL,你唯一要做的事情,就是把它們複製到應用程序的目錄了。另外,從程序中加載資源DLL也非常簡單,LoadLibrary()與AfxSetResourceHandle(hDll)就可以勝任了。
 
 
以下是源代碼:
 
LanguageSupport.h
 
#pragma once
 
#include <afxcoll.h>
 
#define ID_LANGUAGE_FIRST 0x6F00
#define ID_LANGUAGE_LAST 0x6FFF
 
class CLanguageSupport
{
public:
 CLanguageSupport();
 ~CLanguageSupport();
 
 void CreateMenu(CCmdUI *pCmdUI, UINT nFirstItemId= ID_LANGUAGE_FIRST);
 void OnSwitchLanguage(UINT nId, bool bLoadNewLanguageImmediately= false);
 
 void LoadBestLanguage();
 
protected:
 CWordArray m_aLanguages;
 LANGID m_nExeLanguage;
 LANGID m_nCurrentLanguage;
 
 HINSTANCE m_hDll;
 
 static const TCHAR szLanguageId[];
 static LANGID GetLangIdFromFile(LPCTSTR pszFilename);
 static CString GetLanguageName(LANGID wLangId);
 
 static LANGID GetUserUILanguage();
 static LANGID GetSystemUILanguage();
 
 void GetAvailableLanguages();
 
 bool LoadLanguage();
 bool LoadLanguageDll();
 void LookupExeLanguage();
 void UnloadResourceDll();
 
 static void SetResourceHandle(HINSTANCE hDll);
};
 
 
LanguageSupport.cpp
 
#include "stdafx.h"
#include "resource.h"
#include "LanguageSupport.h"
#include <shlwapi.h>
 
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
 
#pragma comment(lib, "shlwapi.lib")
#pragma comment(lib, "version.lib")
 
const TCHAR CLanguageSupport::szLanguageId[]=_T("LanguageId");
 
#ifndef countof
#define countof(x) (sizeof(x)/sizeof((x)[0]))
#endif
 
void CLanguageSupport::LoadBestLanguage()
{
 ASSERT(AfxGetApp()->m_pszRegistryKey!=NULL && AfxGetApp()->m_pszRegistryKey[0]!=0);
 
 m_nCurrentLanguage=(LANGID)AfxGetApp()->GetProfileInt(_T(""),szLanguageId,0);
 if (LoadLanguage())
    return;
 
 m_nCurrentLanguage= GetUserUILanguage();
 if (LoadLanguage())
    return;
 
 m_nCurrentLanguage= GetSystemUILanguage();
 if (LoadLanguage())
    return;
 
 m_nCurrentLanguage= GetUserDefaultLangID();
 if (LoadLanguage())
    return;
 
 m_nCurrentLanguage= GetSystemDefaultLangID();
 if (LoadLanguage())
    return;
 
 m_nCurrentLanguage= m_nExeLanguage;
 VERIFY(LoadLanguage());
}
 
bool CLanguageSupport::LoadLanguage()
{
 
 if (m_nCurrentLanguage==0)
    return false;
 
 if (LoadLanguageDll())
    return true;
 
 WORD wSubLanguage= SUBLANGID(m_nCurrentLanguage);
 if (wSubLanguage!=SUBLANG_NEUTRAL)
 {
    m_nCurrentLanguage= MAKELANGID( PRIMARYLANGID(m_nCurrentLanguage), SUBLANG_NEUTRAL );
    if (LoadLanguageDll())
     return true;
 }
 
 if (wSubLanguage!=SUBLANG_DEFAULT)
 {
    m_nCurrentLanguage= MAKELANGID( PRIMARYLANGID(m_nCurrentLanguage), SUBLANG_DEFAULT );
    if (LoadLanguageDll())
      return true;
 }
 
 return false;
}
 
typedef LANGID (WINAPI*PFNGETUSERDEFAULTUILANGUAGE)();
typedef LANGID (WINAPI*PFNGETSYSTEMDEFAULTUILANGUAGE)();
 
#if _MFC_VER<0x0700 // for VC6 users. Depending on your version of the platform SDK, you may need these lines or not.
typedef LPARAM LONG_PTR;
#endif
 
static BOOL CALLBACK _EnumResLangProc(HMODULE /*hModule*/, LPCTSTR /*pszType*/,
         LPCTSTR /*pszName*/, WORD langid, LONG_PTR lParam)
{ // Helper used to identify the language in NTDLL.DLL (used for NT4)
 
         if(lParam == NULL) // lParam = ptr to var that receives the LangId
                   return FALSE;
                  
         LANGID* plangid = reinterpret_cast< LANGID* >( lParam );
         *plangid = langid;
 
         return TRUE;
}
 
LANGID CLanguageSupport::GetUserUILanguage()
{
 HINSTANCE hKernel32= ::GetModuleHandle(_T("kernel32.dll"));
         ASSERT(hKernel32 != NULL);
        
 PFNGETUSERDEFAULTUILANGUAGE pfnGetUserDefaultUILanguage =
    (PFNGETUSERDEFAULTUILANGUAGE)::GetProcAddress(hKernel32, "GetUserDefaultUILanguage"); // NB: GetProcAddress() takes an ANSI string
 
         if(pfnGetUserDefaultUILanguage != NULL)
         {
                  return pfnGetUserDefaultUILanguage();
 }
         else
         {
    return 0;
 }
}
 
LANGID CLanguageSupport::GetSystemUILanguage()
{
 HINSTANCE hKernel32= ::GetModuleHandle(_T("kernel32.dll"));
         ASSERT(hKernel32 != NULL);
        
 PFNGETSYSTEMDEFAULTUILANGUAGE pfnGetSystemDefaultUILanguage =
    (PFNGETSYSTEMDEFAULTUILANGUAGE)::GetProcAddress(hKernel32, "GetSystemDefaultUILanguage"); // NB: GetProcAddress() takes an ANSI string
 
         if(pfnGetSystemDefaultUILanguage != NULL)
         {
                   return pfnGetSystemDefaultUILanguage();
 }
         else
         {
                   if (::GetVersion()&0x80000000)
                   {
             DWORD dwLangID= 0; // Assume error
 
                            HKEY hKey = NULL;
                            LONG nResult = ::RegOpenKeyEx(HKEY_CURRENT_USER,
                                     _T( "Control Panel//Desktop//ResourceLocale" ), 0, KEY_READ, &hKey);
 
                            if (nResult == ERROR_SUCCESS)
                            {
                                     DWORD dwType;
                                     TCHAR szValue[16];
                                     ULONG nBytes = sizeof( szValue );
                                     nResult = ::RegQueryValueEx(hKey, NULL, NULL, &dwType, LPBYTE( szValue ), &nBytes );
                                     if (nResult==ERROR_SUCCESS && dwType==REG_SZ)
                                               _stscanf( szValue, _T( "%x" ), &dwLangID );
 
                                     ::RegCloseKey(hKey);
                            }
 
      return (LANGID)dwLangID;
                   }
                   else
                   {
                    LANGID LangId = 0;
                            HMODULE hNTDLL = ::GetModuleHandle( _T( "ntdll.dll" ) );
                            if (hNTDLL != NULL)
                            {
                                     ::EnumResourceLanguages( hNTDLL, RT_VERSION, MAKEINTRESOURCE( 1 ),
                                               _EnumResLangProc, reinterpret_cast< LONG_PTR >( &LangId ) );
                            }
 
      return LangId;
                   }
         }
}
 
bool CLanguageSupport::LoadLanguageDll()
{
 
 if (m_nCurrentLanguage==m_nExeLanguage)
 {
    TRACE0("Resource DLL is... the EXE./n");
    UnloadResourceDll();
    return true;
 }
 
 TCHAR szAbbrevName[4];
 if (GetLocaleInfo(MAKELCID(m_nCurrentLanguage, SORT_DEFAULT), LOCALE_SABBREVLANGNAME, szAbbrevName, 4)==0)
 {
    TRACE1("Attempt to load DLL for unsupported language. Language = %u/n", m_nCurrentLanguage);
    return false;
 }
 
 TCHAR szFilename[MAX_PATH];
 DWORD cch= GetModuleFileName( NULL, szFilename, MAX_PATH);
 ASSERT(cch!=0);
 
 LPTSTR pszExtension= PathFindExtension(szFilename);
 lstrcpy(pszExtension, szAbbrevName);
 lstrcat(pszExtension, _T(".dll"));
 
 HINSTANCE hDll = LoadLibrary(szFilename);
 if (hDll != NULL)
 {
    TRACE1("Resource DLL %s loaded successfully/n",szFilename);
    UnloadResourceDll();
 
    m_hDll= hDll;
    SetResourceHandle(m_hDll); 
 
    return true;
 }
 else
    return false;
}
 
CLanguageSupport::CLanguageSupport()
{
 m_hDll= NULL;
 m_nCurrentLanguage= 0;
 LookupExeLanguage();
}
 
CLanguageSupport::~CLanguageSupport()
{
 UnloadResourceDll();
}
 
void CLanguageSupport::UnloadResourceDll()
{
 if (m_hDll!=NULL)
 {
    SetResourceHandle(AfxGetApp()->m_hInstance);    FreeLibrary(m_hDll);
    m_hDll= NULL;
 }
}
 
void CLanguageSupport::SetResourceHandle(HINSTANCE hDll)
{
 AfxSetResourceHandle(hDll);
 
#if _MFC_VER>=0x0700
 _AtlBaseModule.SetResourceInstance(hDll);
#endif
}
 
LANGID CLanguageSupport::GetLangIdFromFile(LPCTSTR pszFilename)
{
 DWORD dwHandle;
 DWORD dwLength=GetFileVersionInfoSize((LPTSTR)pszFilename,&dwHandle);
 if (dwLength==0)
 {
   TRACE(_T("Failed to read file's version info. Error= %1!u!/n"), GetLastError());
    return 0;
 }
 
 LANGID nLangId=0;
 CByteArray abData;
 abData.SetSize(dwLength);
 
 if (GetFileVersionInfo((LPTSTR)pszFilename,dwHandle,dwLength,(LPVOID)abData.GetData()))
 {
    LANGID *pLanguageId; // NB: LANGID = WORD
    if (VerQueryValue(abData.GetData(),_T("//VarFileInfo//Translation"),(void**)&pLanguageId,(PUINT)&dwLength))
      nLangId=*pLanguageId;
 }
 
 return nLangId;
}
 
void CLanguageSupport::OnSwitchLanguage(UINT nId, bool bLoadNewLanguageImmediately)
{
 int nLanguageIndex= nId-ID_LANGUAGE_FIRST;
 
 if (nLanguageIndex<0 || nLanguageIndex>=m_aLanguages.GetSize())
    return;
 
 LANGID LangId= m_aLanguages[nLanguageIndex];
 
 AfxGetApp()->WriteProfileInt(_T(""),szLanguageId,(int)LangId);
 
 if (bLoadNewLanguageImmediately)
 {
    LoadBestLanguage();
 }
 else
 {
    LANGID nCurrentLanguage= m_nCurrentLanguage;
 
    m_nCurrentLanguage= LangId;
    VERIFY(LoadLanguage());
 
    CString csFormat(MAKEINTRESOURCE(IDS_RESTART));    // Don't forget to add a string in the String Table :
 
    m_nCurrentLanguage= nCurrentLanguage;
    VERIFY(LoadLanguage());
 
    CString csMessage;
    csMessage.FormatMessage(csFormat, LPCTSTR(AfxGetAppName())); // IDS_RESTART : Please restart %1.
    AfxMessageBox(csMessage,MB_ICONINFORMATION); // Please restart MyApp.
 }
}
 
void CLanguageSupport::GetAvailableLanguages()
{
 
 m_aLanguages.SetSize(0);
 
 if (m_nExeLanguage!=0)
    m_aLanguages.Add(m_nExeLanguage);
 
 TCHAR szFileMask[MAX_PATH+10];
 DWORD cch= GetModuleFileName( NULL, szFileMask, MAX_PATH);
 ASSERT(cch!=0);
 LPTSTR pszExtension= PathFindExtension(szFileMask);
 lstrcpy(pszExtension, _T("???.dll"));
 
 CFileFind finder;
 BOOL bWorking = finder.FindFile(szFileMask);
 while (bWorking)
 {
    bWorking = finder.FindNextFile();
 
    LANGID nLanguageID=GetLangIdFromFile(finder.GetFilePath());
    if (nLanguageID!=0)
      m_aLanguages.Add(nLanguageID);
 }
}
 
void CLanguageSupport::CreateMenu(CCmdUI *pCmdUI, UINT nFirstItemId)
{
 GetAvailableLanguages();
 
 UINT nCurrentItem= 0;
 CMenu SubMenu;
 SubMenu.CreatePopupMenu();
 for (int i=0; i<m_aLanguages.GetSize(); i++)
 {
    SubMenu.AppendMenu(MF_STRING, ID_LANGUAGE_FIRST+i, GetLanguageName(m_aLanguages[i]) );
    if (m_nCurrentLanguage==m_aLanguages[i])
      nCurrentItem= ID_LANGUAGE_FIRST+i;
 }
 
 if (nCurrentItem!=0)
    SubMenu.CheckMenuRadioItem(ID_LANGUAGE_FIRST, ID_LANGUAGE_FIRST+(int)m_aLanguages.GetSize()-1, nCurrentItem, MF_BYCOMMAND);
 
 MENUITEMINFO mii= { sizeof(mii) };
 mii.fMask= MIIM_SUBMENU;
 mii.hSubMenu= SubMenu.m_hMenu;
 
 ::SetMenuItemInfo(pCmdUI->m_pMenu->m_hMenu, pCmdUI->m_nID, FALSE, &mii);
 pCmdUI->Enable();
 
 SubMenu.Detach();
}
 
CString CLanguageSupport::GetLanguageName(LANGID wLangId)
{
 TCHAR szLanguage[200], szCP[10];
 
 GetLocaleInfo( MAKELCID(wLangId, SORT_DEFAULT), LOCALE_IDEFAULTANSICODEPAGE, szCP, 10);
 int nAnsiCodePage= _ttoi(szCP);
 
 int cch= GetLocaleInfo( MAKELCID(wLangId, SORT_DEFAULT), LOCALE_SNATIVELANGNAME, szLanguage, countof(szLanguage));
 if (cch!=0)
 {
#ifndef UNICODE
    wchar_t szLanguageW[200];
    cch= MultiByteToWideChar(nAnsiCodePage, MB_ERR_INVALID_CHARS, szLanguage, -1, szLanguageW, countof(szLanguageW));
 
    BOOL bUsed= FALSE;
    cch= WideCharToMultiByte(CP_ACP, 0, szLanguageW, -1, szLanguage, countof(szLanguage), "X", &bUsed);
    if (bUsed || nAnsiCodePage==0)
    {
      cch= 0;
    }
#endif
 }
 
 if (cch==0)
 {
    cch= GetLocaleInfo( MAKELCID(wLangId, SORT_DEFAULT), LOCALE_SLANGUAGE, szLanguage, countof(szLanguage));
    if (cch==0)
      _stprintf(szLanguage, _T("%u - ???"), wLangId); // Ouch ! We can't even display the name in the current language !
 }
 
 return szLanguage;
}
 
void CLanguageSupport::LookupExeLanguage()
{
 TCHAR szFilename[MAX_PATH]={0};
 DWORD cch= GetModuleFileName( NULL, szFilename, MAX_PATH);
 ASSERT(cch!=0);
 
 m_nExeLanguage= GetLangIdFromFile(szFilename);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章