Сообщений 34 Оценка 75 Оценить |
Вводные замечания Структура приложения Код в студию! Планы на будущее А как же сама MFC? Заключение |
Задача локализации создаваемых приложений встает перед разработчиком достаточно часто. Способы ее решения многократно обсуждались, и на сегодняшний день существует уже не одна «обкатанная» типовая реализация. В качестве самого простого примера можно привести горячо любимую мной программу ATnotes, хранящую все заголовки пунктов меню, подписи к элементам управления и выводимые сообщения в текстовом файле, содержимое которого считывается по мере необходимости. Другим популярным способом является создание так называемых resource-only DLLs для каждого поддерживаемого языка. При старте приложения загружается та библиотека, язык которой соответствует языку вызывающего (главного) потока. Можно также поместить в исполняемый файл приложения копии ресурсов на нескольких языках, предоставив операционной системе выбирать нужные в зависимости от текущих языковых установок. Ну а самым «забойным» решением является разработка отдельных локализованных версий выпускаемого программного продукта, дистрибутив которого занимает несколько CD.
Мне, однако, хотелось бы рассказать о наименее требовательной к ресурсам и наиболее прозрачной реализации динамической (без перезапуска приложения) смены языка пользовательского интерфейса. Подобная возможность, на мой взгляд, обеспечит конечному пользователю как удобство работы с приложением, так и некоторую степень свободы – некоторые (к их числу отношусь и я) предпочитают иметь дело с англоязычными версиями программных продуктов, используя при этом региональные настройки, соответствующие месту проживания.
Сразу оговорюсь, что хотя речь пойдет о реализации механизма переключения языка интерфейса в приложениях, разрабатываемых с использованием библиотеки MFC, я не вижу принципиальных трудностей при переносе его под «чистый» Win32 API. Итак, приступим…
Наше многоязычное приложение имеет следующую структуру:
Основанием выбора именно такой структуры приложения является тот факт, что ядро MFC при явной (например, CMenu::LoadMenu) или неявной (например, CDialog::DoModal) загрузке любого ресурса ищет его не только в том модуле, дескриптор которого возвращает функция AfxGetResourceHandle, но и во всех extension DLLs, загруженных в адресное пространство процесса. Соответственно, после загрузки нужной «языковой» библиотеки нам даже нет необходимости вызывать функцию AfxSetResourceHandle, и те ресурсы, которые не требуют локализации (иконки, бинарные данные, etc), могут быть в единственном экземпляре помещены в исполняемый файл приложения. Единственное, на что здесь стоит обратить особое внимание – множества значений идентификаторов «языковых» и общих ресурсов не должны пересекаться, так как это может привести к ошибкам в работе приложения.
Давайте бросим взгляд на ключевые фрагменты исходного кода, полный текст которого прилагается в качестве демонстрационного примера. Для реализации описанной выше структуры в класс-приложение добавляются два поля:
// файл AfxPolyglotApp.h class CAfxPolyglotApp: public CWinApp { ... // атрибутыpublic: HINSTANCE m_hResDLL; CMap<UINT, UINT, CString, LPCTSTR> m_mapLocales; ... }; |
Поле m_hResDLL предназначено для хранения дескриптора текущей загруженной «языковой» библиотеки, а поле m_mapLocales – для связывания команд меню «Language» («Язык») с именами «языковых» DLL, входящих в состав приложения. Мне представляется наиболее предпочтительным называть «языковые» библиотеки в соответствии с именами, передаваемыми в функцию CRT setlocale.
Реализация метода CAfxPolyglotApp::InitInstance загружает «языковую» библиотеку по умолчанию и заполняет словарь поддерживаемых приложением языков:
// файл AfxPolyglotApp.cpp BOOL CAfxPolyglotApp::InitInstance(void) { m_hResDLL = ::LoadLibrary(_T("English_USA.1252.dll")); _tsetlocale(LC_ALL, _T("English_USA.1252")); m_mapLocales.SetAt(IDM_LANGUAGE_ENGLISH, _T("English_USA.1252")); m_mapLocales.SetAt(IDM_LANGUAGE_RUSSIAN, _T("Russian_Russia.1251")); ... // обычная инициализация } |
Реализация метода CAfxPolyglotApp::ExitInstance выгружает из адресного пространства приложения текущую «языковую» библиотеку:
int CAfxPolyglotApp::ExitInstance(void) { ::FreeLibrary(m_hResDLL); return (CWinApp::ExitInstance()); } |
Собственно переключение языков выполняется в методе CAfxPolyglotApp::OnLanguage, который посредством макроса ON_COMMAND_RANGE включен в карту сообщений класса-приложения как обработчик любой из команд меню «Language»:
void CAfxPolyglotApp::OnLanguage(UINT uID) { CString strLocale; if (m_mapLocales.Lookup(uID, strLocale)) { ASSERT(!strLocale.IsEmpty()); // загружаем новую и выгружаем старую языковую библиотеку HINSTANCE hPrevResDLL = m_hResDLL; m_hResDLL = ::LoadLibrary(strLocale + _T(".dll")); _tsetlocale(LC_ALL, strLocale); ::FreeLibrary(hPrevResDLL); // заменяем главное меню приложения CMenu* pPrevMainMenu = m_pMainWnd->GetMenu(); CMenu menuMain; menuMain.LoadMenu(IDR_MAIN_MENU); m_pMainWnd->SetMenu(&menuMain); m_pMainWnd->DrawMenuBar(); pPrevMainMenu->DestroyMenu(); menuMain.Detach(); } } |
Вот, собственно, и все. Сравните результаты выполнения команд «Help/About» и «Справка/О программе» – при том, что в их обработчике создается одно и то же диалоговое окно (класса CAboutDialog).
Существенным недостатком рассмотренной выше реализации является, на мой взгляд, жестко определенный список поддерживаемых языков, который не может быть расширен ни сторонним разработчиком, ни нами самими – без исправления исходного кода приложения. Одним из возможных решений этой проблемы может быть хранение в системном реестре или конфигурационном файле приложения информации о доступных командах меню «Language» и соответствующих им «языковых» DLL. Это может выглядеть следующим образом:
; файл AfxPolyglot.ini [Languages] 40002=English 40003=Russian ... [LangDLLs] English=English_USA.1252.dll Russian=Russian_Russia.1251.dll ... |
В методе InitInstance приложение может прочесть секцию Languages (с помощью функции Win32 API GetPrivateProfileSection) и динамически сформировать одноименное меню, используя ключи как идентификаторы команд меню, а значения – как их тексты. Одновременно можно заполнить и m_mapLanguages именами соответствующих «языковых» библиотек, используя значения из секции Languages в качестве ключей секции LangDLLs. Единственное, с чем придется жестко определиться на этапе разработки приложения – это диапазон допустимых идентификаторов меню «Language», поскольку он требуется макросу ON_COMMAND_RANGE.
Действительно, mfc42.dll содержит немало ресурсов, которые ядро MFC использует для отображения сообщений, элементов управления, etc – и все они по-прежнему останутся англоязычными при любой из загруженных нашим приложением «языковых» библиотек. К сожалению, официальный способ локализации MFC, изложенный в MSDN (MFC Library Reference. TN057: Localization of MFC Components), не поддерживает динамической смены языка, поэтому вам придется обратиться к документу «TN058: MFC Module State Implementation» и исходным кодам библиотеки.
В заголовочном файле <afxstat_.h> (всегда включаемом в исходный код через <afx.h>, включенный, в свою очередь, в <afxwin.h>) объявлен класс AFX_MODULE_STATE, содержащий разного рода служебную информацию, которая обеспечивает корректное функционирование как самой библиотеки MFC, так и использующего ее приложения. Получить доступ к экземпляру такого класса, связанному с текущим выполняющимся модулем, можно с помощью функции AfxGetModuleState. В данном случае интересно поле m_appLangDLL, имеющее тип HINSTANCE и предназначенное для хранения дескриптора модуля, в котором MFC ищет свои собственные ресурсы (если это поле имеет значение NULL, то ресурсы загружаются непосредственно из mfc42.dll). Именно в это поле записывается дескриптор библиотеки mfc42loc.dll (подробности – в TN057), если MFC удается найти и загрузить ее при старте приложения.
Таким образом, для динамического переключения ресурсов самой MFC на другой язык мы должны сделать примерно следующее:
// загружается DLL, содержащая ресурсы MFC на нужном языке // (в данном случае – русском) HINSTANCE hRusDLL = ::LoadLibrary(_T("MFC42RUS.DLL")); // если это получилось...if (hRusDLL != NULL) { AFX_MODULE_STATE* pState = AfxGetModuleState(); ASSERT(pState != NULL); // ...и какие-то локализованные ресурсы MFC уже используются...if (pState->m_appLangDLL != NULL) { // ...то вначале выгружается текущая DLL... ::FreeLibrary(pState->m_appLangDLL); } // ...а затем запоминается новая pState->m_appLangDLL = hRusDLL; } |
Заметим, что явно выгружать текущую DLL с ресурсами MFC при завершении приложения не требуется, поскольку ядро MFC делает это самостоятельно, если значение поля m_appLangDLL отлично от нуля.
Теперь, если мы добавим аналог приведенного выше кода в метод OnLanguage, то при выборе пользователем любого поддерживаемого языка интерфейса, наше приложение будет соответствовать ему до последней точки.
Безусловно, предложенный способ не является единственно возможным и не претендует на звание истины в последней инстанции. Буду рад услышать любые замечания, пожелания и дополнения, сделанные вами на основании собственного бесценного опыта.
Сообщений 34 Оценка 75 Оценить |