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

Создание локализованных приложений

Автор: Гулай Борис aka BoresExpress
Источник: Журнал "Программист"

Несколько замечаний по локализации приложений
Диалоговые окна
Меню
Информация о версии
Таблицы сообщений
Таблицы строк
Значки и курсоры

Исходные тексты – 12 K
Демонстрационное приложение – 15 K

Многие разработчики мечтают о всемирной популярности своих приложений, но почти никто не создает локализованных версий своих приложений, ошибочно полагая, что программа должна сначала завоевать популярность. Локализованные версии – это первый шаг к завоеванию мирового господства!

В этой статье я расскажу о создании локализованных версий приложений и, подробнее, о локализации следующих ресурсов:

ПРИМЕЧАНИЕ

Вообще-то, если вам нужно иметь в одном модуле строки на нескольких языках, то лучше использовать таблицы сообщений. Но об этом ниже.

Кроме того, в качестве бонуса, я покажу, как работать с локализованными значками и курсорами! Но сначала…

Несколько замечаний по локализации приложений

Во многих примерах из этой статьи будет использоваться функция LoadResourceLang. Вот ее исходный текст:

HGLOBAL LoadResourceLang(LPCTSTR resType, DWORD resID)
{
  HINSTANCE hAppInstance=GetModuleHandle(NULL);

  HRSRC hrRes=FindResourceEx(hAppInstance, resType, MAKEINTRESOURCE(resID), GetUserDefaultLangID());
  if (!hrRes)
  {
    hrRes=FindResourceEx(hAppInstance, resType, MAKEINTRESOURCE(resID), MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US));
  }

  return LoadResource(hAppInstance, hrRes);
}

Как видите, ничего сложного. Она просто загружает ресурс указанного типа с указанным идентификатором. При этом загружается ресурс на основном языке, который пользователь выбрал в настройках Windows (Панель управления > Языки и стандарты). Если такой ресурс загрузить не удается, функция пытается загрузить ресурс на английском языке.

Диалоговые окна

Для создания локализованного диалогового окна нужно указать в его свойствах требуемый язык и набрать все строки в этом диалоговом окне на выбранном языке. Для загрузки диалогового окна с нужным языком необходимо написать следующий код:

DialogBoxIndirect(hAppInstance, (LPDLGTEMPLATE)LoadResourceLang(RT_DIALOG, IDD_DIALOG_ID), hParent, &DialogMsgProc);

Сначала мы находим ресурс диалога на нужном нам языке, затем загружаем его и передаем функции DialogBoxIndirect(Param) как указатель на структуру DLGTEMPLATE. Хитрость состоит в том, что ресурсы диалоговых окон лежат внутри файла в виде корректно сформированных структур DLGTEMPLATE, поэтому не нужно прикладывать никаких дополнительных усилий для создания диалога из указателя на ресурс.

Если вы используете MFC, то в классе, производном от CDialog необходимо перегрузить метод DoModal и написать в нём следующий код (всё остальное оставьте без изменений):

HRSRC hrRes=FindResourceEx(hAppInstance, resType, m_lpszTemplateName,
								GetUserDefaultLangID());
	if (!hrRes)
	{
hrRes=FindResourceEx(hAppInstance, resType,
m_lpszTemplateName,
MAKELANGID(LANG_ENGLISH,
SUBLANG_ENGLISH_US));
	}

if (!CreateIndirect(
		(LPDLGTEMPLATE)LoadResource(hAppInstance, hrRes),
m_pParentWnd))
	return IDCANCEL;

return CDialog::DoModal();

Меню

Работа с меню на разных языках во многом аналогична работе с диалоговыми окнами. Вот пример кода, загружающего локализованное меню:

HMENU hMenu=LoadMenuIndirect(LoadResourceLang(RT_MENU, IDR_MAIN));

Как вы, наверное, уже догадались, ресурс меню представляет собой корректную структуру MENUTEMPLATE. Поэтому работа с локализованными меню также не должна вызвать затруднений.

Информация о версии

Поскольку приложению редко приходится работать со своей «информацией о версии» (обычно это делают специальные утилиты), я не буду описывать её получение. Скажу лишь, что Win32 API предоставляет набор функций, которые позволяют сделать это без особых ухищрений.

Для добавления в приложение локализованной «информации о версии» вам следует добавить не новый ресурс Version Info, а в уже существующий добавить новый Version Info Block и выбрать для него требуемый язык.

Таблицы сообщений

Таблицы сообщений нельзя назвать широко используемым ресурсом. Так произошло потому, что Resource Editor из комплекта Visual Studio не поддерживает их создание и редактирование. Но, с другой стороны, этот тип ресурса широко применяется в самой Windows и, в отличие от таблиц строк, напрямую поддерживает локализацию. Рассмотрим этот ресурс подробнее.

Таблицы сообщений во многом похожи на таблицы строк, но, в отличие от последних, позволяют сопоставить каждому идентификатору несколько текстовых строк, отличающихся языком. Кроме того, в строке могут встречаться специальные комбинации символов %n (где n - число от 1 до 99), которые при загрузке строки (при помощи функции FormatMessage) могут автоматически заменяться предоставляемыми вами строками или числами.

На что именно будет заменена та или иная комбинация символов, вы можете указать, написав такую последовательность символов: %n!x!, где x – один из форматных символов функции printf. Например, если вы хотите, чтобы на место первой специальной комбинации символов было подставлено беззнаковое число, напишите следующее: %1!u!. По умолчанию считается, что на место всех спецпоследовательностей будут подставляться строки.

Исходный файл таблицы строк обрабатывается компилятором mc (Message Compiler), который в результате своей работы создает:

Формат исходного файла для компилятора mc я описывать не буду, так как он очень прост. Подробнее вы можете ознакомиться с ним здесь.

Расскажу о том, что делать с заголовочными файлами, полученными после компиляции таблицы строк. h-файл нужно включить в тот же файл, в который включен resource.h (в сам resource.h его включать не стоит). Это предоставит вам доступ к числовым идентификаторам сообщений.

rc-файл нужно включить в rc-файл вашего проекта с помощью директивы #include. Сделать это лучше ближе к началу файла (например, сразу после строки #undef APSTUDIO_READONLY_SYMBOLS, если она у вас есть). После этого в Resource Editor вы увидите новый Custom Resource. Только не пытайтесь открывать даже папку этого ресурса! Иначе Resource Editor вставит все содержимое bin-файлов прямо в rc-файл вашего проекта, после чего он перестанет компилироваться.

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

LPTSTR buff=NULL;
FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_HMODULE, hAppInstance, IDM_MSG_ID, GetUserDefaultLangID(), (LPTSTR)&buff, 0xFF, NULL);
// ...
// Используем buff
// ...
LocalFree(buff);

Если же в вашем сообщении есть спецпоследовательности и вы хотите произвести подстановку, то напишите следующий код:

BYTE argArr[sizeof(DWORD)+sizeof(LPCTSTR)];
BYTE *pCurr=pArr;

(*(DWORD*)pCurr)=10;
pCurr+=sizeof(DWORD);

(*(LPCTSTR*)pCurr)=TEXT("aaa");

LPTSTR buff=NULL;
FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_ARGUMENT_ARRAY, hAppInstance,IDM_MSG_ID, GetUserDefaultLangID(), (LPTSTR)&buff, 0xFF, (va_list*)&pArr);
// ...
// Используем buff
// ...
LocalFree(buff);

Такой хитрый способ передачи значений используется потому, что паскалевская конвенция вызова (__stdcall), которая используется в Win32 API, не позволяет передавать переменное число аргументов.

Таблицы строк

Вот мы и подошли к самому неприятному моменту. Дело в том, что Win32 API принципиально не поддерживает работу с таблицами строк на нескольких языках, поэтому нам придется собственноручно написать аналог функции LoadString:

WORD LoadStringLang(UINT strID, LPTSTR destStr /*=NULL*/, WORD strLen /*=0*/)
{

  LPCWSTR str=(LPCWSTR)LoadResourceLang(RT_STRING, 1+(strID >> 4));
  if (!str)
    return 0;

  for (WORD strPos=0; strPos < (strID & 0x000F); strPos++)
    str+=*str+1;

  if (!strLen)
    return *str;
  if (!destStr)
    return 0;

#ifdef _UNICODE
  lstrcpyn(destStr, str+1, min(strLen, *str+1));
#<kw>else</kw>
  WideCharToMultiByte(CP_ACP, 0, str+1, *str+1, destStr, strLen, NULL, NULL);
#endif

	destStr[min(strLen-1, *str)]=TEXT('\0');
	return min(strLen, *str+1);
}

Поясню принцип работы этой функции: строки хранятся в ресурсах (в кодировке Unicode) группами по 16 штук, и минимальная загружаемая единица – это группа строк. Каждая группа имеет свой идентификатор, который равен идентификатору первой строки в группе плюс 1. После того, как мы загрузили нужную группу строк, мы ищем (последовательным перебором) нужную строку в группе. Строки внутри группы хранятся следующим образом:

Длина строкиТекст строкиДлина строкиТекст строки
2 байта(длина строки)*2 байт2 байта(длина строки)*2 байт

Тем, кто знаком с COM, это напомнит строки BSTR. При переборе мы просто прибавляем к текущему значению указателя длину строки плюс 1. Обратите внимание, что завершающий нулевой символ у строк отсутствует! Такое поведение компилятора ресурсов можно изменить, указав при компиляции ключ /n.

Любая группа строк всегда заполнена полностью. На местах отсутствующих строк находятся слова, хранящие длину строки, в которые записаны нули. Например:

13Просто строка5Текст000

Вот, собственно, и все. Теперь, при необходимости, нужно перевести строку из Unicode в ANSI и скопировать в предоставленный пользователем буфер.

Обратите внимание, что наша реализация имеет полезную особенность, отсутствующую у LoadString. Если передать ей при вызове в качестве длины буфера 0, то она вернет размер загружаемой строки, не считая завершающего нуля.

Значки и курсоры

А теперь обещанный бонус – локализованные значки и курсоры! Это непростая задача, но ее решение стоит того. Представьте, как было бы удобно использовать эту возможность, например, в индикаторах клавиатуры! Итак, приступим:

HICON LoadIconLang(DWORD iconID, bool bIcon /*=true*/)
{
  int iid=LookupIconIdFromDirectory((BYTE*)LoadResourceLang(bIcon ? RT_GROUP_ICON : RT_GROUP_CURSOR, iconID), bIcon);
  if (!iid)
    return NULL;

  HRSRC hrIcon=FindResource(hAppInstance, MAKEINTRESOURCE(iid), bIcon ? RT_ICON : RT_CURSOR);

  return CreateIconFromResource((BYTE*)LoadResource(hAppInstance, hrIcon), SizeofResource(hAppInstance, hrIcon), bIcon, 0x00030000);
}

Вы наверняка знаете, что один ресурс значка (или ico-файл) может содержать значки разного размера и с разной глубиной цвета. Один такой «сложный» значок (называемый Icon Directory) и определяется идентификатором, который вы видите в редакторе ресурсов. Каждый «простой» значок имеет свой уникальный идентификатор, который, в общем случае, не равен идентификатору каталога. С помощью функции LookupIconIdFromDirectory можно получить идентификатор значка из группы, указатель на которую передан ей в качестве одного из параметров. Функция возвращает идентификатор значка, который, по ее мнению, лучше всего подходит к текущим параметрам экрана.

Затем необходимо загрузить значок с полученным идентификатором и передать указатель на него функции CreateIconFromResource. Функция по данным, определяемым этим указателем, и другой переданной информации создаст значок и вернет его дескриптор.

Для того чтобы загрузить не значок, а курсор, нужно передать нашей функции LoadIconLang в качестве последнего параметра false.

Вот, собственно, и все, что я хотел рассказать про создание локализованных приложений. Приложение - пример к статье умеет полностью изменять язык интерфейса на тот, который вы выберете в настройках Windows. Приложение поддерживает следующие языки:

Помните, что видеть на экране слова на родном языке значительно приятней, чем на чужом!


Статья опубликована с разрешения автора
    Сообщений 38    Оценка 150        Оценить