Арабская локализация: окна и рисование

2841924fd9934680b62a011dc934d299.pngОднажды в очередную версию ABBYY FineReader Sprint (программа для распознавания текста, которая поставляется вместе с МФУ и сканерами) потребовалось добавить арабский язык интерфейса. И все заверте…Обычно локализация на «новый» язык — дело (для разработки) нехитрое: завести константу-другую, поднастроить билдовую систему, и, в общем-то, все. Остальное ложится на плечи техписов и переводчиков. Но в арабском пишут справа налево, а это влечет за собой множество изменений и в интерфейсе. Об этом опыте я и расскажу в статье.

Разворачиваем все (на самом деле нет)Задолго до того, как с перевода пришли первые переведенные на арабский язык строки, я занялся вопросом RTL (right-to-left, увы, так и не придумал хорошего русского перевода этого термина) интерфейса. Дело в том, что расположение дочерних окон следует за направлением письменности: «крестик» закрытия окна и скроллеры прижаты влево, а главное меню — вправо, даже прогресс-бары бегут справа налево. В общем, все перевернуто. Самый простой способ добиться такого поведения –вставить в код инициализации (до создания первого окна) такой вызов: SetProcessDefaultLayout (LAYOUT_RTL);

После этого все окна верхнего уровня, создаваемые в текущем процессе, будут создаваться со стилем WS_EX_LAYOUTRTL. Этот стиль также получат в наследство и все дочерние окна (не совсем все, но как первое приближение сойдет). Кстати, не самый простой способ добиться зеркалированного поведения — это проставлять WS_EX_LAYOUTRTL всем окнам верхнего уровня при создании. В любом случае, получится примерно как на картинке, приведенной в начале статьи. То есть вот так:

f2af6b6081004bfa8b00052b6d79c81f.png

Cразу же видны многие проблемы, с которыми и придётся побороться: зеркалированные иконки на тулбарах (1) и кнопках (2), развернутый текст (3), не стыкующийся фон под заголовком Tasks (4), улетевшая не туда пунктуация (5) и т.п.

Собственно, стиль WS_EX_LAYOUTRTL разворачивает клиентские координаты (и те, что используются в WM_NCPAINT). Теперь они будут иметь началом верхнюю правую точку, а абсцисса будет увеличиваться справа-налево. Также, все контексты устройств (HDC), связанные с этим окном, будут LAYOUT_RTL (краткое описание здесь).

В RTL мире Windows разворачиваются только клиентские координаты. Оконные (десктопные) остаются привычной LTR направленности. И первыми пострадавшими в этой ситуации оказываются методы ClientToScreen и ScreenToClient (раздел Remarks в этой статье). Поскольку мы используем эти методы не напрямую, оказалось достаточным простым научить наши обертки справляться в RTL ситуации. Я иногда развлекаюсь поисками причин, почему то же самое не могли сделать разработчики Windows. Кстати, рекомендация MSDN состоит в использовании метода MapWindowPoints. Во вновь написанном коде я так и поступил, так как это иногда удобнее.Куда более неприятным (т.к. не обнаруживается простым поиском по проекту) может оказаться смешивание в вычислениях оконных и клиентских координат. Пример вот здесь, в разделе Mapping Coordinates. К счастью, такого у нас в проекте не оказалось (я просмотрел все подозрительные места, ну и тестирование проблем не выявило). Что я могу сказать, просто не делайте так.

Для борьбы с мерцанием мы используем стандартный метод буферизации: все рисование делается в BITMAP в памяти, а в конце он просто копируется в контекст устройства с помощью BitBlt. Корни проблемы оказались в используемой нами обертке над парой методов CreateCompatibleDC и SelectObject (/*…*/, bitmap), а именно вот в таком конструкторе с параметром по умолчанию: explicit CBitmapDC (HBITMAP bitmap, HDC compatibleWith = 0);

В коде повсеместно использовался этот конструктор, причем именно что с параметром по умолчанию. В результате полученный контекст устройства был совместим c рабочим столом (который, как и декстопные координаты, остается LTR), но не с окном, в котором мы рисовали. Никогда не любил параметры по умолчанию. После исправления этого недоразумения стало гораздо лучше:

d25089241d114e56b75f80b1b9ac9fdb.png

Кстати, несмотря на то, что флаги позиционирования текста я не менял (c DT_LEFT), текст прижимается вправо. Это просто работает, что приятно.

Как видно из предыдущего скриншота, проблема с зеркалированием картинки осталась не решенной. По правде говоря, правильнее было бы назвать этот раздел «Image List — как не сойти с ума». Дело в том, что вся проблемная графика в изделии представлена именно в виде HIMAGELIST. И проблема тут далеко не одна.Для того чтобы иконка из image list при не разворачивалась, MSDN рекомендует использовать при создании image list флаги ILC_MIRROR и ILC_PERITEMMIRROR. Начиная с Windows XP (в наших системных требованиях указана минимально поддерживаемая версия Window XP SP3) я не нашел никаких различий между тем, как работает флаг ILC_MIRROR, и тем, как работает объединение этих флагов. В любом случае, добавление этого флага наших проблем почти не решило.

Почти вся графика в продукте существует в двух вариантах: 8-битная в системной палитре и 32-битная с альфа-каналом. Сделано это для нормальной работы в малоцветных режимах для тех, кому это нужно, при сохранении «красоты» у всех остальных. И вышеуказанный способ борьбы с зеркалированием сработал только с 8-битными иконками. А все потому, что для рисования полупрозрачных иконок мы используем не системный метод ImageList_Draw, а собственную реализацию — мы рисуем напрямую в DIB представление контекста устройства. Кстати, DIB ничего про RTL не знает, и пикселы в нем индексируются как обычно — слева направо. Поэтому, казалось бы, проблем быть не должно. Их и не было бы, если бы в рамках подготовки к будущему RTL команда, отвечающая за общую библиотеку, не добавила в код рисования пресловутого зеркалирования. Насколько я понимаю, причина была в том, что на тот момент еще не было понимания, как стоит работать с графикой в RTL (MSDN в качестве одного из вариантов решения этой проблемы предлагает зеркалить графику в редакторе изображений, чтобы после повторного зеркалирования она выводилась правильно). Что ж, скажу мнение автора нашей графики — значительно удобней работать без этого двойного зеркалирования. Потому что существует и третья проблема с графикой.

В общем случае ответ: «Да, нужно». Вернемся все к тому же скриншоту. Можно заметить, что, несмотря на перевернутую W, иконка обладает одним важным достоинством: стрелка смотрит в правильную (для RTL) сторону. Да, для арабов (и израильтян) направление «вперед» будет влево, а не вправо. Вот, к примеру, Internet Explorer в Windows с английским (слева) и арабским (справа) UI для сравнения: baf960ea599c460fa302a93e736a9333.png

Кроме стрелок зеркалироваться должны и несимметричные знаки препинания, если они есть на картинке. В большинстве случаев это касается только вопросительного знака.

924cb552cdd34c87b82aa7a64ecbd3d1.png

И, кстати, отдельная графика (или зеркалирование при рисовании) потребуется и для фона заголовка Tasks на первом скриншоте.

Как я упоминал, вызова SetProcessDefaultLayout почти достаточно для того, чтобы окна развернулись. Но помимо окон верхнего уровня (то есть тех WS_POPUP окон, у которых в качестве предка указан 0) есть и другие всплывающие окна. К примеру, диалоги. Они WS_EX_LAYOUTRTL автоматом не получают. Но тут все просто — этот стиль нужно просто указать в ресурсах, или, если всплывающее (WS_POPUP) окно создается не из ресурсов, — добавить этот флаг при создании. Например, по условию, что таковой стиль присутствует у родительского окна. Я иногда задумываюсь, почему Microsoft и тут остановился на полпути? Дело в том, что не всегда мы управляем оконными флагами создания. И если в случае со страницами свойств (property sheets) и мастерами настройки (wizards) все просто — там направление лейаута определяется по первой добавленной странице, то с окнами сообщений и стандартными диалогами все сложнее.

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

Со стандартными диалогами все одновременно и сложнее, и проще. Проще, потому что их лейаут зависит только от локализации операционной системы. Сложнее — именно по той же причине. Если по какой-то причине важно, чтобы все приложение было RTL, то в этом месте без костылей не обойтись. Кстати, то же самое относится и к «новым» файловым диалогам (IFileDialog). В нашем случае мы оставили это как есть, посчитав проблему несущественной.

Как я уже говорил, дочерние окна наследуют этот флаг от родителя. Но иногда это не нужно (например, в ABBYY FineReader Sprint это окно редактора изображений — нет никакого смысла зеркалировать вывод картинок, и, тем более, рамки выделения для инструментов обрезания, разделения на страницы и т.п.). Есть два пути — снимать этот флаг сразу после создания или использовать при создании родительского окна флаг WS_EX_NOINHERITLAYOUT. Первый путь представляется более разумным в большинстве случаев.Кстати, как я уже упоминал, не все окна наследуют стиль WS_EX_LAYOUTRTL от родительских. Многие стандартные элементы управления (common controls) заменяют его на другие (чтобы вести себя RTL образом). К примеру, для edit, static это будет сочетание флагов WS_EX_RIGHT | WS_EX_RTLREADING.

Именно с этим связана самая необычная ошибка, с которой я встретился. В комбобоксе выбора языка есть фича подсказки по мере ввода:

28efcb87575f4cbfbe66ad9d3fab9e46.png

Как видно, всплывающее окно подсказки появляется в позиции каретки. Позицию каретки я получаю методом GetCaretPos, который возвращает ее в клиентских координатах. Дальше получаю (используя MapWindowPoints) оконные координаты каретки, помещаю в них окно подсказки, и, в случае RTL, вижу, что подсказка появляется в противоположном краю комбобокса:

83e86b55e83e41af918054bf34cf467f.png

Причина этой ошибки в том, что клиентские координаты, в которых я получаю позицию каретки, это координаты не комбобокса, а дочернего к нему edit контрола. Который, как я уже говорил выше, не наследует (в отличие от комбобокса) флаг WS_EX_LAYOUTRTL. Исправление, соответственно, очевидно: просто поменять окно в вызове MapWindowPoints на правильное.

MSDN не рекомендует использование GDI+ в RTL окружении. А именно, GDI+ методы не учитывают свойство LAYOUT_RTL того контекста устройства, на котором рисуют. Тем не менее, попробовать можно –просто придется самому преобразовывать координаты при рисовании. Удобнее всего показался следующий способ: используя контекст устройства (HDC) с установленным свойством LAYOUT_RTL, для перевода координат вызывать метод LPToDP.Впрочем, код рисования на GDI+ оказался после этих правок несколько запутанным, так что я нашел способ обойтись в этом продукте без GDI+ совсем.Михаил Васильченко, департамент продуктов для распознавания текстов

© Habrahabr.ru