Динамическое переключение языка интерфейса в MFC-приложениях |
Вводные замечанияЗадача локализации создаваемых приложений встает перед разработчиком достаточно часто. Способы ее решения многократно обсуждались, и на сегодняшний день существует уже не одна «обкатанная» типовая реализация. В качестве самого простого примера можно привести горячо любимую мной программу ATnotes, хранящую все заголовки пунктов меню, подписи к элементам управления и выводимые сообщения в текстовом файле, содержимое которого считывается по мере необходимости. Другим популярным способом является создание так называемых resource-only DLLs для каждого поддерживаемого языка. При старте приложения загружается та библиотека, язык которой соответствует языку вызывающего (главного) потока. Можно также поместить в исполняемый файл приложения копии ресурсов на нескольких языках, предоставив операционной системе выбирать нужные в зависимости от текущих языковых установок. Ну а самым «забойным» решением является разработка отдельных локализованных версий выпускаемого программного продукта, дистрибутив которого занимает несколько CD. Мне, однако, хотелось бы рассказать о наименее требовательной к ресурсам и наиболее прозрачной реализации динамической (без перезапуска приложения) смены языка пользовательского интерфейса. Подобная возможность, на мой взгляд, обеспечит конечному пользователю как удобство работы с приложением, так и некоторую степень свободы — некоторые (к их числу отношусь и я) предпочитают иметь дело с англоязычными версиями программных продуктов, используя при этом региональные настройки, соответствующие месту проживания. Сразу оговорюсь, что хотя речь пойдет о реализации механизма переключения языка интерфейса в приложениях, разрабатываемых с использованием библиотеки MFC, я не вижу принципиальных трудностей при переносе его под «чистый» Win32 API. Итак, приступим… Структура приложенияНаше многоязычное приложение имеет следующую структуру:
Основанием выбора именно такой структуры приложения является тот факт, что ядро MFC при явной (например, CMenu::LoadMenu) или неявной (например, CDialog::DoModal) загрузке любого ресурса ищет его не только в том модуле, дескриптор которого возвращает функция AfxGetResourceHandle, но и во всех extension DLLs, загруженных в адресное пространство процесса. Соответственно, после загрузки нужной «языковой» библиотеки нам даже нет необходимости вызывать функцию AfxSetResourceHandle, и те ресурсы, которые не требуют локализации (иконки, бинарные данные, etc), могут быть в единственном экземпляре помещены в исполняемый файл приложения. Единственное, на что здесь стоит обратить особое внимание — множества значений идентификаторов «языковых» и общих ресурсов не должны пересекаться, так как это может привести к ошибкам в работе приложения. Код в студию!Давайте бросим взгляд на ключевые фрагменты исходного кода, полный текст которого прилагается в качестве демонстрационного примера. Для реализации описанной выше структуры в класс-приложение добавляются два поля:
Поле m_hResDLL предназначено для хранения дескриптора текущей загруженной «языковой» библиотеки, а поле m_mapLocales — для связывания команд меню Language (Язык) с именами «языковых» DLL, входящих в состав приложения. Мне представляется наиболее предпочтительным называть «языковые» библиотеки в соответствии с именами, передаваемыми в функцию CRT setlocale. Реализация метода CAfxPolyglotApp::InitInstance загружает «языковую» библиотеку по умолчанию и заполняет словарь поддерживаемых приложением языков:
Реализация метода CAfxPolyglotApp::ExitInstance выгружает из адресного пространства приложения текущую «языковую» библиотеку:
Собственно переключение языков выполняется в методе CAfxPolyglotApp::OnLanguage, который посредством макроса ON_COMMAND_RANGE включен в карту сообщений класса-приложения как обработчик любой из команд меню Language:
Вот, собственно, и все. Сравните результаты выполнения команд Help/About и Справка/О программе — при том, что в их обработчике создается одно и то же диалоговое окно (класса CAboutDialog): Планы на будущееСущественным недостатком рассмотренной выше реализации является, на мой взгляд, жестко определенный список поддерживаемых языков, который не может быть расширен ни сторонним разработчиком, ни нами самими — без исправления исходного кода приложения. Одним из возможных решений этой проблемы может быть хранение в системном реестре или конфигурационном файле приложения информации о доступных командах меню Language и соответствующих им «языковых» 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 при завершении приложения не требуется, поскольку ядро MFC делает это самостоятельно, если значение поля m_appLangDLL отлично от нуля. Теперь, если мы добавим аналог приведенного выше кода в метод OnLanguage, то при выборе пользователем любого поддерживаемого языка интерфейса, наше приложение будет соответствовать ему до последней точки. ЗаключениеБезусловно, предложенный способ не является единственно возможным и не претендует на звание истины в последней инстанции. Буду рад услышать любые замечания, пожелания и дополнения, сделанные вами на основании собственного бесценного опыта. | |||||