Вводные замечания

Задача локализации создаваемых приложений встает перед разработчиком достаточно часто. Способы ее решения многократно обсуждались, и на сегодняшний день существует уже не одна «обкатанная» типовая реализация. В качестве самого простого примера можно привести горячо любимую мной программу ATnotes, хранящую все заголовки пунктов меню, подписи к элементам управления и выводимые сообщения в текстовом файле, содержимое которого считывается по мере необходимости. Другим популярным способом является создание так называемых resource-only DLLs для каждого поддерживаемого языка. При старте приложения загружается та библиотека, язык которой соответствует языку вызывающего (главного) потока. Можно также поместить в исполняемый файл приложения копии ресурсов на нескольких языках, предоставив операционной системе выбирать нужные в зависимости от текущих языковых установок. Ну а самым «забойным» решением является разработка отдельных локализованных версий выпускаемого программного продукта, дистрибутив которого занимает несколько CD.

Мне, однако, хотелось бы рассказать о наименее требовательной к ресурсам и наиболее прозрачной реализации динамической (без перезапуска приложения) смены языка пользовательского интерфейса. Подобная возможность, на мой взгляд, обеспечит конечному пользователю как удобство работы с приложением, так и некоторую степень свободы — некоторые (к их числу отношусь и я) предпочитают иметь дело с англоязычными версиями программных продуктов, используя при этом региональные настройки, соответствующие месту проживания.

Сразу оговорюсь, что хотя речь пойдет о реализации механизма переключения языка интерфейса в приложениях, разрабатываемых с использованием библиотеки MFC, я не вижу принципиальных трудностей при переносе его под «чистый» Win32 API. Итак, приступим…

Структура приложения

Наше многоязычное приложение имеет следующую структуру:

  • требующие локализации ресурсы для каждого из поддерживаемых приложением языков располагаются в отдельных MFC extension DLL (замечу, что эти библиотеки не являются resource-only, поскольку содержат «стандартную» функцию DllMain, отвечающую за инициализацию и завершение их работы);
  • соответствующие ресурсы, элементы управления и команды меню имеют одинаковые идентификаторы (то есть, одинаковое символическое имя и одинаковое значение - например, идентификатор главного меню приложения в любой из библиотек имеет символическое имя IDR_MAIN_MENU и численно равен 201);
  • в каждый момент времени в адресное пространство приложения загружена одна и только одна языковая библиотека.

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

Заключение

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

обновлено
16.07.2005
 
Проверка PR и ТИЦ