资源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);
}