Админ

четверг, 10 октября 2024 г.

Free Pascal / Lazarus: как сделать кросс-платформенный индикатор раскладки клавиатуры (Windows и GNU/Linux)

|

Не знаю, как вы, а я нередко забываю посмотреть на индикатор языка клавиатуры в приложениях при наборе текста. Хуже всего, когда это происходит при вводе пароля, ведь там символы обычно скрыты звёздочками, сообщение об ошибке возникает только после неудачной попытки входа, и приходится вводить пароль повторно. И вот однажды я задумался: обычно системный индикатор раскладки клавиатуры находится где-то в самом углу экрана — а нельзя ли продублировать его прямо в окне приложения максимально близко к тому месту, где текст вводится? Беглый поиск не дал приемлемого готового решения, и я сделал своё.

Сразу отвечу на вопрос «почему Free Pascal / Lazarus?»: на волне импортозамещения у нас на работе объявили о постепенном переходе на отечественную ОС семейства GNU/Linux. А мне давно хотелось попробовать написать что-нибудь на Free Pascal под Lazarus, чтобы оценить, как эта IDE подходит для кросс-платформенной разработки, да и вообще понять, насколько сложно делать программы, одинаково работающие под разными ОС. Поэтому очередной новый проект весной 2023 года я начал писать на этой системе.

Так окно ввода пароля выглядит под Windows 

Разумеется, всё многообразие мира Linux охватить невозможно, поэтому я написал код только для системы X Window (X11) с модулем поддержки клавиатуры xkb, что охватывает достаточно большой процент десктопных ОС этого семейства и, в частности, все рекомендованные отечественные ОС.

А так под отечественной ALT Linux

Сначала дам ссылку на репозиторий GitHub, а потом расскажу, как это работает: https://github.com/MichaelDemidov/LazarusKeyboardLayout. По ссылке имеются не только сам основной модуль, но и демонстрационный проект, из которого я и взял два скриншота выше.

Ядром алгоритма является кроссплатформенный класс TKeyboardLayoutIndicator, совместимый с Windows и GNU/Linux — он также должен работать на всех ОС с поддержкой X11 и xkb, таких как FreeBSD. Он отслеживает переключение раскладки клавиатуры, считывает текущую раскладку клавиатуры из ОС в удобной для восприятия форме («en», «ru» и т. д.) и записывает ее с помощью обработчика событий OnUpdateIndicator:

TIndicatorEvent = procedure(LayoutText: string) of object;

TKeyboardLayoutIndicator = class
public
    // Constructor and destructor
    constructor Create;
    destructor Destroy; override;

    // Start keyboard layout watching
    procedure StartWatching;

    // Stop keyboard layout watching
    procedure StopWatching;

    // Update indicator event
    property OnUpdateIndicator: TIndicatorEvent; 
end;

Сразу предупреждаю: TKeyboardLayoutIndicator не является визуальным компонентом для IDE, это класс, предназначенный только для получения аббревиатуры текущей раскладки, и он не умеет переключать раскладку сам. Я не ставил себе заведомо провальную задачу продублировать на 100% функции системного индикатора-переключателя из графической оболочки ОС. Для отображения информации можно использовать любой подходящий визуальный компонент, в простейшем варианте (использованном мною для демо-проекта) это TLabel.

Итак, под Windows используются хорошо известные системные хуки для перехвата смены раскладки. Из этого сразу же следует фундаментальное ограничение: создание нескольких экземпляров класса TKeyboardLayoutIndicator бессмысленно, потому что системный хук использует глобальную переменную, содержащую экземпляр этого класса. Если вам нужно под Windows иметь в приложении больше одного визуального индикатора одновременно, придётся либо реализовать что-нибудь вроде списка объектов, обрабатываемого одним обработчиком событий OnUpdateIndicator, либо переписать класс TKeyboardLayoutIndicator, добавив туда процедуру для регистрации множества обработчиков событий вместо одного OnUpdateIndicator. Мне это было не нужно, поэтому я это не реализовал.

Под GNU/Linux + X11 пришлось придумать более изощрённую схему с созданием отдельного потока для перехвата сообщений системы X. Звучит устрашающе, но сам код на самом деле проще, чем кажется.

Но под Linux есть другое замечание, касающееся способа получения аббревиатуры. Дело в том, что системная функция Windows GetLocaleInfo() умеет считывать двухбуквенное (или трехбуквенное в некоторых экзотических случаях) сокращение названия языка из ОС в соответствии со стандартом ISO-639. В xkb под GNU/Linux аналогичной функции я не нашёл, поэтому там класс получает полное название языка и берёт из него первые две буквы (например, «english» превращается в «en»). Таким образом, на некоторых раскладках класс возвращает разные аббревиатуры в Windows и Linux. Если кто-то знает, как это сделать более элегантно, пожалуйста, напишите тут в комментариях или в GitHub. Вариант «просто хранить все аббревиатуры прямо в коде» я счёл избыточным.

Также под Linux класс, вероятнее всего (я не проверял), не будет работать в традиционном консольном приложении, потому что механизм событий X Window требует наличия окна, обрабатывающего эти события. В рамках эксперимента я пробовал создавать фоновое невидимое окно для их перехвата, и это работало… впрочем, в конце концов мне оказалось сложно представить себе консольное приложение, которое где-то как-то выводит индикатор раскладки клавиатуры, и я отказался от этой идеи.

Также не реализована совместимость с macOS, потому что у меня просто нет соответствующего компьютера.

Финальное замечание касается совместимости с Delphi. К сожалению, современных версий этой IDE у меня нет, скачивать гигантское и недолго живущее демо IDE только ради одного этого проекта неудобно, а на устаревшей версии тестировать было бы странно.

Поэтому я не проверял, но полагаю, что сам модуль keyboardlayout.pas должен быть совместим с Delphi — там не используются никакие специфические для Free Pascal классы, структуры или синтаксические особенности языка. Если у вас есть Delphi и вы хотите проверить работу индикатора под Windows сами, то можете попробовать убрать всё лишнее: строку {$mode ObjFPC}{$H+} в начале файла, все строки {$IF defined(WINDOWS)}, всё между соответствующими им строками {$ELSE} и {$ENDIF}, включая их сами, и все блоки {$IF not defined(WINDOWS)}...{$ENDIF}. Также придётся исправить секцию uses (в зависимости от версии Delphi).

Демонстрационный проект проще переписать с нуля, потому что он несовместим с Delphi из-за разной структуры файлов форм и использования специфических для Lazarus свойств компонентов (выравнивание и т. д.).

Комментариев нет:

Отправить комментарий

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

К началу