Сообщений 34    Оценка 75        Оценить  
Система Orphus

Динамическое переключение языка интерфейса в MFC-приложениях

Автор: Илья Зарецкий
АО "Промышленно-строительный банк"

Источник: RSDN Magazine #2-2004
Опубликовано: 23.10.2004
Исправлено: 10.12.2016
Версия текста: 1.0
Вводные замечания
Структура приложения
Код в студию!
Планы на будущее
А как же сама 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.

А как же сама MFC?

Действительно, 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, то при выборе пользователем любого поддерживаемого языка интерфейса, наше приложение будет соответствовать ему до последней точки.

Заключение

Безусловно, предложенный способ не является единственно возможным и не претендует на звание истины в последней инстанции. Буду рад услышать любые замечания, пожелания и дополнения, сделанные вами на основании собственного бесценного опыта.


Эта статья опубликована в журнале RSDN Magazine #2-2004. Информацию о журнале можно найти здесь
    Сообщений 34    Оценка 75        Оценить