리소스를 분리하자!
해결 방법을 찾아 이리저리 인터넷을 헤매던 나고민 씨는 리소스 파일이 실행 파일에 포함되지 않고 별도의 DLL로도 존재할 수 있다는 사실을 알게 되었습니다. 나고민 씨는 우선 기존 프로그램을 약간 수정하여 언어를 선택할 수 있는 메뉴를 추가하고, 메뉴를 클릭했을 때 보여질 메시지를 스트링 테이블에 추가하였습니다.
[code]
LoadLibrary : 프로그램에서 사용할 리소스 파일을 불러옵니다.
LoadResource : 리소스 파일에서 해당 리소스를 가져오는 함수입니다.
LoadAccelerators/DestroyAcceleratorTable, LoadBitmap/ DeleteObject, LoadCursor/DestroyCursor, LoadIcon/ DestroyIcon, LoadMenu/DestroyMenu : 가속키, 비트맵, 커서, 아이콘, 메뉴를 가져오는 리소스 함수들과 각각의 리소스를 메모리에서 제거하는 함수들입니다. 리소스 사용량은 한정되어 있으니 필요 없는 리소스들은 메모리에서 즉시 삭제해 주는 것이 좋습니다.
LoadString : 문자열을 불러오는 리소스 함수입니다.
[/code]
이와 같이 필요한 리소스를 변경한 이후 이를 별개의 리소스 DLL로 만들기 위해서는 새로운 프로젝트를 생성해야 합니다. 우선 AppWizard를 실행시켜 MFC AppWizard(EXE) 타입의 프로젝트를 선택합니다. 다음 화면에서 MFC Extension DLL을 선택해 DoItAllDLL_ENG라는 이름의 프로젝트를 생성합니다. DoItAllDLL_ENG 프로젝트 폴더 아래의 res 폴더에 DoItAllDLL 프로젝트의 리소스 관련 파일을 복사하고(DoItAll.ico, DoItAllDoc.ico, Toolbar.bmp) DoItAllDLL_Eng.rc와 resource.h 파일도 수정해 줍니다. 영어 버전의 리소스 파일이 무사히 DLL로 컴파일되었다면 이제 이 프로젝트의 내용을 복사해 DoItAllDLL_KOR 프로젝트를 만듭니다. 이후 리소스의 내용을 번역하여 메뉴와 스트링 테이블의 내용을 한글로 변경합니다.
리소스를 DLL에서 읽어 들이자
이제 원래 프로그램이 리소스를 DLL에서 읽어올 수 있도록 변경해 보겠습니다.
CDoItAllApp 클래스 수정
우선 멤버 함수로 리소스 파일을 로딩하기 위한 준비를 위해 다음과 같이 InitRes()를 추가합니다. InitRes 함수에서는 시스템의 기본 로케일을 얻어(GetSystemDefaultLangID) 초기에 프로그램에서 사용할 리소스 DLL을 선택하게 됩니다. 이후 AfxGetResourceHandle()을 이용해 리소스의 핸들을 얻어온 이후 LoadLibrary(sDir+sDll)와 AfxSetResource Handle(hInstance)를 이용해 실제 사용할 리소스를 프로그램으로 로드하게 됩니다.
[code]
BOOL CDoItAllApp::InitRes()
{
CString sDll = GetProfileString(“Settings”, “Resource”, _T(“”));
// 리소스 파일의 지정이 잘못된 경우
if( sDll.CompareNoCase(“DoItAllDLL_ENG.dll”) != 0 &&
sDll.CompareNoCase(“DoItAllDLL_KOR.dll”) != 0 )
{
switch(GetSystemDefaultLangID())
{
// korean
case 0x0412:
sDll = “DoItAllDLL_ENG.dll”;
break;
// English(US)
case 0x0409:
default:
sDll = “DoItAllDLL_ENG.dll”;
break;
}
}
HINSTANCE hInstance = AfxGetResourceHandle();
if(hInstance != NULL)
FreeLibrary(hInstance);
// load Resource Dll
CString sDir = m_pszHelpFilePath;
sDir = sDir.Left(sDir.ReverseFind(‘\\’)+1);
hInstance = LoadLibrary(sDir+sDll);
if(NULL == hInstance)
{
AfxMessageBox(sDll + “ Load Error”);
return FALSE;
}
AfxSetResourceHandle(hInstance);
m_sResDll = sDll;
// 도움말 파일 설정
CString sHelp = m_pszHelpFilePath;
sHelp = sHelp.Left(sHelp.ReverseFind(‘.’)) + “.CHM”;
//First free the string allocated by MFC at CWinApp startup.
free((void*)m_pszHelpFilePath);
m_pszHelpFilePath=_tcsdup(sHelp);
return TRUE;
}
[/code]
이제 InitInstance 함수에 다음과 같은 행을 추가하여 기본 리소스 파일을 로드합니다.
[code]
BOOL CDoItAllApp::InitInstance()
:
:
SetRegistryKey(_T("Local AppWizard-Generated Applications"));
// 리소스 파일을 로드합니다.
if(!InitRes())
{
PostQuitMessage(-1);
return FALSE;
}
LoadStdProfileSettings();
// Load standard INI file options (including MRU)
}
[/code]
또한 이곳에 UI의 언어를 선택하는 메뉴를 처리하는 이벤트 핸들러도 넣어두도록 합시다. WriteProfileString을 이용해 UI의 언어를 저장해 두고, MainFrame의 ReplaceMenu 함수를 이용해 메뉴를 교체합니다.
[code]
void CDoItAllApp::OnOptionLanguageEng()
{
// TODO: Add your command handler code here
if(m_sResDll.CompareNoCase(“DoItAllDLL_ENG.dll”) == 0)
return;
if(AfxMessageBox(IDS_OPTION_LANG_ENG, MB_YESNO) != IDYES)
return;
WriteProfileString(“Settings”, “Resource”, _T(“DoItAllDLL_ENG.dll”));
InitRes();
((CMainFrame *) m_pMainWnd)->ReplaceMenu(IDR_MAINFRAME);
}
void CDoItAllApp::OnOptionLanguageKor()
{
// TODO: Add your command handler code here
if(m_sResDll.CompareNoCase(“DoItAllDLL_KOR.dll”) == 0)
return;
if(AfxMessageBox(IDS_OPTION_LANG_KOR, MB_YESNO) != IDYES)
return;
WriteProfileString(“Settings”, “Resource”, _T(“DoItAllDLL_KOR.dll”));
InitRes();
((CMainFrame *) m_pMainWnd)->ReplaceMenu(IDR_MAINFRAME);
}
[/code]
CMainFrame 클래스 수정
CMainFrame 클래스에는 리소스 파일에서 새로운 메뉴를 로드하여 기존의 메뉴와 교체하는 코드를 추가합니다.
[code]
BOOL CMainFrame::ReplaceMenu(UINT nIDResource)
{
// Load the new menu
CMenu NewMenu;
if(!NewMenu.LoadMenu(nIDResource))
return FALSE;
// Remove and destroy the old menu
SetMenu(NULL);
::DestroyMenu(m_hMenuDefault);
// Add the new menu
SetMenu(&NewMenu);
// Assign default menu
m_hMenuDefault = NewMenu.m_hMenu;
return TRUE;
}
[/code]
이제 완성된 프로그램을 실행해 보면, 메뉴를 통해 프로그램의 UI를 한국어와 영어로 바꿀 수 있게 되었습니다.
드디어 나고민 씨와 개발팀원들은 다국어 프로그램에 대한 고민에서 벗어나고, 마케팅 팀은 제품 판매에 날개를 달게 되었다고 기뻐했습니다.
설계 단계에서 기획하자
이번 연재에서는 비주얼 C++로 간단하게 다국어 프로그램으로 개발되지 않은 소스를 수정해 다국어를 지원할 수 있는 프로그램으로 변경해 보았습니다. 본 예제의 경우는 간단한 프로그램이어서 크게 문제는 없었지만, 설계 단계에서 다국어 프로그램으로 기획되지 않은 경우 별도의 리소스 파일로 DLL을 관리한다고 해도 리소스 하나를 추가할 때마다 각각의 리소스를 모두 변경해야 하는 번거로움이 있어 관리가 쉽지는 않습니다. 다음 시간에는 닷넷을 이용한 다국어 프로그램에 도전해 보기로 하겠습니다. 참고로 이 기사는 데브피아의 비주얼 C++ 게시판에서 홍진현 님의 다국어 관련 리소스 예제를 바탕으로 작성되었습니다. @
[code]
로케일 관련 함수들
GetSystemDefaultLangID : 시스템의 기본 언어 식별자 반환 함수
GetUserDefaultLangID : 현 사용자의 기본 언어 식별자 반환 함수
GetSystemDefaultLCID : 시스템의 기본 로케일 식별자 반환 함수
GetUserDefaultLCID : 현 사용자의 기본 로케일 식별자 반환 함수
ConvertDefaultLocale : 기본 로케일을 변경하는 함수
SetThreadLocale : 이 함수를 호출한 쓰레드의 현재 로케일을 변경하는 함수
CWinApp::WriteProfileString
WriteProfileString은 .ini 파일이나 레지스트리에 애플리케이션의 정보를 저장하는 함수입니다. NT 계열의 OS에서는 레지스트리에 저장되며, 윈도우 95/98 등에서는 Win.INI에 저장됩니다(MSDN 참조).
BOOL WriteProfileString(
LPCTSTR lpszSection,
LPCTSTR lpszEntry,
LPCTSTR lpszValue
);
[/code]
해결 방법을 찾아 이리저리 인터넷을 헤매던 나고민 씨는 리소스 파일이 실행 파일에 포함되지 않고 별도의 DLL로도 존재할 수 있다는 사실을 알게 되었습니다. 나고민 씨는 우선 기존 프로그램을 약간 수정하여 언어를 선택할 수 있는 메뉴를 추가하고, 메뉴를 클릭했을 때 보여질 메시지를 스트링 테이블에 추가하였습니다.
[code]
LoadLibrary : 프로그램에서 사용할 리소스 파일을 불러옵니다.
LoadResource : 리소스 파일에서 해당 리소스를 가져오는 함수입니다.
LoadAccelerators/DestroyAcceleratorTable, LoadBitmap/ DeleteObject, LoadCursor/DestroyCursor, LoadIcon/ DestroyIcon, LoadMenu/DestroyMenu : 가속키, 비트맵, 커서, 아이콘, 메뉴를 가져오는 리소스 함수들과 각각의 리소스를 메모리에서 제거하는 함수들입니다. 리소스 사용량은 한정되어 있으니 필요 없는 리소스들은 메모리에서 즉시 삭제해 주는 것이 좋습니다.
LoadString : 문자열을 불러오는 리소스 함수입니다.
[/code]
이와 같이 필요한 리소스를 변경한 이후 이를 별개의 리소스 DLL로 만들기 위해서는 새로운 프로젝트를 생성해야 합니다. 우선 AppWizard를 실행시켜 MFC AppWizard(EXE) 타입의 프로젝트를 선택합니다. 다음 화면에서 MFC Extension DLL을 선택해 DoItAllDLL_ENG라는 이름의 프로젝트를 생성합니다. DoItAllDLL_ENG 프로젝트 폴더 아래의 res 폴더에 DoItAllDLL 프로젝트의 리소스 관련 파일을 복사하고(DoItAll.ico, DoItAllDoc.ico, Toolbar.bmp) DoItAllDLL_Eng.rc와 resource.h 파일도 수정해 줍니다. 영어 버전의 리소스 파일이 무사히 DLL로 컴파일되었다면 이제 이 프로젝트의 내용을 복사해 DoItAllDLL_KOR 프로젝트를 만듭니다. 이후 리소스의 내용을 번역하여 메뉴와 스트링 테이블의 내용을 한글로 변경합니다.
리소스를 DLL에서 읽어 들이자
이제 원래 프로그램이 리소스를 DLL에서 읽어올 수 있도록 변경해 보겠습니다.
CDoItAllApp 클래스 수정
우선 멤버 함수로 리소스 파일을 로딩하기 위한 준비를 위해 다음과 같이 InitRes()를 추가합니다. InitRes 함수에서는 시스템의 기본 로케일을 얻어(GetSystemDefaultLangID) 초기에 프로그램에서 사용할 리소스 DLL을 선택하게 됩니다. 이후 AfxGetResourceHandle()을 이용해 리소스의 핸들을 얻어온 이후 LoadLibrary(sDir+sDll)와 AfxSetResource Handle(hInstance)를 이용해 실제 사용할 리소스를 프로그램으로 로드하게 됩니다.
[code]
BOOL CDoItAllApp::InitRes()
{
CString sDll = GetProfileString(“Settings”, “Resource”, _T(“”));
// 리소스 파일의 지정이 잘못된 경우
if( sDll.CompareNoCase(“DoItAllDLL_ENG.dll”) != 0 &&
sDll.CompareNoCase(“DoItAllDLL_KOR.dll”) != 0 )
{
switch(GetSystemDefaultLangID())
{
// korean
case 0x0412:
sDll = “DoItAllDLL_ENG.dll”;
break;
// English(US)
case 0x0409:
default:
sDll = “DoItAllDLL_ENG.dll”;
break;
}
}
HINSTANCE hInstance = AfxGetResourceHandle();
if(hInstance != NULL)
FreeLibrary(hInstance);
// load Resource Dll
CString sDir = m_pszHelpFilePath;
sDir = sDir.Left(sDir.ReverseFind(‘\\’)+1);
hInstance = LoadLibrary(sDir+sDll);
if(NULL == hInstance)
{
AfxMessageBox(sDll + “ Load Error”);
return FALSE;
}
AfxSetResourceHandle(hInstance);
m_sResDll = sDll;
// 도움말 파일 설정
CString sHelp = m_pszHelpFilePath;
sHelp = sHelp.Left(sHelp.ReverseFind(‘.’)) + “.CHM”;
//First free the string allocated by MFC at CWinApp startup.
free((void*)m_pszHelpFilePath);
m_pszHelpFilePath=_tcsdup(sHelp);
return TRUE;
}
[/code]
이제 InitInstance 함수에 다음과 같은 행을 추가하여 기본 리소스 파일을 로드합니다.
[code]
BOOL CDoItAllApp::InitInstance()
:
:
SetRegistryKey(_T("Local AppWizard-Generated Applications"));
// 리소스 파일을 로드합니다.
if(!InitRes())
{
PostQuitMessage(-1);
return FALSE;
}
LoadStdProfileSettings();
// Load standard INI file options (including MRU)
}
[/code]
또한 이곳에 UI의 언어를 선택하는 메뉴를 처리하는 이벤트 핸들러도 넣어두도록 합시다. WriteProfileString을 이용해 UI의 언어를 저장해 두고, MainFrame의 ReplaceMenu 함수를 이용해 메뉴를 교체합니다.
[code]
void CDoItAllApp::OnOptionLanguageEng()
{
// TODO: Add your command handler code here
if(m_sResDll.CompareNoCase(“DoItAllDLL_ENG.dll”) == 0)
return;
if(AfxMessageBox(IDS_OPTION_LANG_ENG, MB_YESNO) != IDYES)
return;
WriteProfileString(“Settings”, “Resource”, _T(“DoItAllDLL_ENG.dll”));
InitRes();
((CMainFrame *) m_pMainWnd)->ReplaceMenu(IDR_MAINFRAME);
}
void CDoItAllApp::OnOptionLanguageKor()
{
// TODO: Add your command handler code here
if(m_sResDll.CompareNoCase(“DoItAllDLL_KOR.dll”) == 0)
return;
if(AfxMessageBox(IDS_OPTION_LANG_KOR, MB_YESNO) != IDYES)
return;
WriteProfileString(“Settings”, “Resource”, _T(“DoItAllDLL_KOR.dll”));
InitRes();
((CMainFrame *) m_pMainWnd)->ReplaceMenu(IDR_MAINFRAME);
}
[/code]
CMainFrame 클래스 수정
CMainFrame 클래스에는 리소스 파일에서 새로운 메뉴를 로드하여 기존의 메뉴와 교체하는 코드를 추가합니다.
[code]
BOOL CMainFrame::ReplaceMenu(UINT nIDResource)
{
// Load the new menu
CMenu NewMenu;
if(!NewMenu.LoadMenu(nIDResource))
return FALSE;
// Remove and destroy the old menu
SetMenu(NULL);
::DestroyMenu(m_hMenuDefault);
// Add the new menu
SetMenu(&NewMenu);
// Assign default menu
m_hMenuDefault = NewMenu.m_hMenu;
return TRUE;
}
[/code]
이제 완성된 프로그램을 실행해 보면, 메뉴를 통해 프로그램의 UI를 한국어와 영어로 바꿀 수 있게 되었습니다.
드디어 나고민 씨와 개발팀원들은 다국어 프로그램에 대한 고민에서 벗어나고, 마케팅 팀은 제품 판매에 날개를 달게 되었다고 기뻐했습니다.
설계 단계에서 기획하자
이번 연재에서는 비주얼 C++로 간단하게 다국어 프로그램으로 개발되지 않은 소스를 수정해 다국어를 지원할 수 있는 프로그램으로 변경해 보았습니다. 본 예제의 경우는 간단한 프로그램이어서 크게 문제는 없었지만, 설계 단계에서 다국어 프로그램으로 기획되지 않은 경우 별도의 리소스 파일로 DLL을 관리한다고 해도 리소스 하나를 추가할 때마다 각각의 리소스를 모두 변경해야 하는 번거로움이 있어 관리가 쉽지는 않습니다. 다음 시간에는 닷넷을 이용한 다국어 프로그램에 도전해 보기로 하겠습니다. 참고로 이 기사는 데브피아의 비주얼 C++ 게시판에서 홍진현 님의 다국어 관련 리소스 예제를 바탕으로 작성되었습니다. @
[code]
로케일 관련 함수들
GetSystemDefaultLangID : 시스템의 기본 언어 식별자 반환 함수
GetUserDefaultLangID : 현 사용자의 기본 언어 식별자 반환 함수
GetSystemDefaultLCID : 시스템의 기본 로케일 식별자 반환 함수
GetUserDefaultLCID : 현 사용자의 기본 로케일 식별자 반환 함수
ConvertDefaultLocale : 기본 로케일을 변경하는 함수
SetThreadLocale : 이 함수를 호출한 쓰레드의 현재 로케일을 변경하는 함수
CWinApp::WriteProfileString
WriteProfileString은 .ini 파일이나 레지스트리에 애플리케이션의 정보를 저장하는 함수입니다. NT 계열의 OS에서는 레지스트리에 저장되며, 윈도우 95/98 등에서는 Win.INI에 저장됩니다(MSDN 참조).
BOOL WriteProfileString(
LPCTSTR lpszSection,
LPCTSTR lpszEntry,
LPCTSTR lpszValue
);
[/code]
'Windows > MFC' 카테고리의 다른 글
다국어 버전 준비작업.. (0) | 2013.10.02 |
---|---|
Multi language menu 다국어 버전 메뉴 (0) | 2013.10.02 |
다국어 버전에서 언어를 변경하려고 할 때 SetThreadLocale (0) | 2013.10.02 |
dll에 Bitmap Resource 가져오기 (0) | 2013.10.02 |
SDI, MDI 에서 메뉴 없애기 hMenu (0) | 2013.10.02 |