Однажды в очередную версию ABBYY FineReader Sprint (программа для распознавания текста, которая поставляется вместе с МФУ и сканерами) потребовалось добавить арабский язык интерфейса. И все заверте…
Обычно локализация на «новый» язык – дело (для разработки) нехитрое: завести константу-другую, поднастроить билдовую систему, и, в общем-то, все. Остальное ложится на плечи техписов и переводчиков. Но в арабском пишут справа налево, а это влечет за собой множество изменений и в интерфейсе. Об этом опыте я и расскажу в статье.
Разворачиваем все (на самом деле нет)
Задолго до того, как с перевода пришли первые переведенные на арабский язык строки, я занялся вопросом RTL (right-to-left, увы, так и не придумал хорошего русского перевода этого термина) интерфейса. Дело в том, что расположение дочерних окон следует за направлением письменности: «крестик» закрытия окна и скроллеры прижаты влево, а главное меню – вправо, даже прогресс-бары бегут справа налево. В общем, все перевернуто. Самый простой способ добиться такого поведения –вставить в код инициализации (до создания первого окна) такой вызов:
SetProcessDefaultLayout(LAYOUT_RTL);
После этого все окна верхнего уровня, создаваемые в текущем процессе, будут создаваться со стилем WS_EX_LAYOUTRTL. Этот стиль также получат в наследство и все дочерние окна (не совсем все, но как первое приближение сойдет). Кстати, не самый простой способ добиться зеркалированного поведения – это проставлять WS_EX_LAYOUTRTL всем окнам верхнего уровня при создании. В любом случае, получится примерно как на картинке, приведенной в начале статьи. То есть вот так:
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), но не с окном, в котором мы рисовали. Никогда не любил параметры по умолчанию. После исправления этого недоразумения стало гораздо лучше:
Кстати, несмотря на то, что флаги позиционирования текста я не менял (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 в качестве одного из вариантов решения этой проблемы предлагает зеркалить графику в редакторе изображений, чтобы после повторного зеркалирования она выводилась правильно). Что ж, скажу мнение автора нашей графики – значительно удобней работать без этого двойного зеркалирования. Потому что существует и третья проблема с графикой.
Нужно ли иметь отдельную графику для RTL?
В общем случае ответ: «Да, нужно». Вернемся все к тому же скриншоту. Можно заметить, что, несмотря на перевернутую W, иконка обладает одним важным достоинством: стрелка смотрит в правильную (для RTL) сторону. Да, для арабов (и израильтян) направление «вперед» будет влево, а не вправо. Вот, к примеру, Internet Explorer в Windows с английским (слева) и арабским (справа) UI для сравнения:
Кроме стрелок зеркалироваться должны и несимметричные знаки препинания, если они есть на картинке. В большинстве случаев это касается только вопросительного знака.
И, кстати, отдельная графика (или зеркалирование при рисовании) потребуется и для фона заголовка Tasks на первом скриншоте.
А что с диалогами и другими всплывающими окнами?
Как я упоминал, вызова SetProcessDefaultLayout почти достаточно для того, чтобы окна развернулись. Но помимо окон верхнего уровня (то есть тех WS_POPUP окон, у которых в качестве предка указан 0) есть и другие всплывающие окна. К примеру, диалоги. Они WS_EX_LAYOUTRTL автоматом не получают. Но тут все просто – этот стиль нужно просто указать в ресурсах, или, если всплывающее (WS_POPUP) окно создается не из ресурсов, – добавить этот флаг при создании. Например, по условию, что таковой стиль присутствует у родительского окна. Я иногда задумываюсь, почему Microsoft и тут остановился на полпути?
Дело в том, что не всегда мы управляем оконными флагами создания. И если в случае со страницами свойств (property sheets) и мастерами настройки (wizards) все просто – там направление лейаута определяется по первой добавленной странице, то с окнами сообщений и стандартными диалогами все сложнее.
Для начала про окна сообщений, которые показываются методом MessageBox. Для их разворота достаточно использовать комбинацию флагов MB_RIGHT | MB_RTLREADING. Вот так вот просто. Ну, разумеется, просто, если использовать системный вызов не напрямую, а через обертку. Потому что иначе придется добавлять эти флаги в каждый вызов MessageBox (и, если арабский и иврит – не единственные языки локализации, делать это по определенному условию).
Со стандартными диалогами все одновременно и сложнее, и проще. Проще, потому что их лейаут зависит только от локализации операционной системы. Сложнее – именно по той же причине. Если по какой-то причине важно, чтобы все приложение было RTL, то в этом месте без костылей не обойтись. Кстати, то же самое относится и к «новым» файловым диалогам (IFileDialog). В нашем случае мы оставили это как есть, посчитав проблему несущественной.
Что делать, если дочернему окну (WS_CHILD) не нужен флаг WS_EX_LAYOUTRTL
Как я уже говорил, дочерние окна наследуют этот флаг от родителя. Но иногда это не нужно (например, в ABBYY FineReader Sprint это окно редактора изображений – нет никакого смысла зеркалировать вывод картинок, и, тем более, рамки выделения для инструментов обрезания, разделения на страницы и т.п.). Есть два пути – снимать этот флаг сразу после создания или использовать при создании родительского окна флаг WS_EX_NOINHERITLAYOUT. Первый путь представляется более разумным в большинстве случаев.
Кстати, как я уже упоминал, не все окна наследуют стиль WS_EX_LAYOUTRTL от родительских. Многие стандартные элементы управления (common controls) заменяют его на другие (чтобы вести себя RTL образом). К примеру, для edit, static это будет сочетание флагов WS_EX_RIGHT | WS_EX_RTLREADING.
Именно с этим связана самая необычная ошибка, с которой я встретился. В комбобоксе выбора языка есть фича подсказки по мере ввода:
Как видно, всплывающее окно подсказки появляется в позиции каретки. Позицию каретки я получаю методом GetCaretPos, который возвращает ее в клиентских координатах. Дальше получаю (используя MapWindowPoints) оконные координаты каретки, помещаю в них окно подсказки, и, в случае RTL, вижу, что подсказка появляется в противоположном краю комбобокса:
Причина этой ошибки в том, что клиентские координаты, в которых я получаю позицию каретки, это координаты не комбобокса, а дочернего к нему edit контрола. Который, как я уже говорил выше, не наследует (в отличие от комбобокса) флаг WS_EX_LAYOUTRTL. Исправление, соответственно, очевидно: просто поменять окно в вызове MapWindowPoints на правильное.
И еще про рисование. Теперь GDI+
MSDN не рекомендует использование GDI+ в RTL окружении. А именно, GDI+ методы не учитывают свойство LAYOUT_RTL того контекста устройства, на котором рисуют. Тем не менее, попробовать можно –просто придется самому преобразовывать координаты при рисовании. Удобнее всего показался следующий способ: используя контекст устройства (HDC) с установленным свойством LAYOUT_RTL, для перевода координат вызывать метод LPToDP.
Впрочем, код рисования на GDI+ оказался после этих правок несколько запутанным, так что я нашел способ обойтись в этом продукте без GDI+ совсем.
Михаил Васильченко,
департамент продуктов для распознавания текстов
Автор: muxaeji