LVGL — это открытая бесплатная библиотека для создания графических пользовательских интерфейсов на встроенных устройствах. Она обладает множеством функций и интеграций, что делает её отличным выбором для создания как простых, так и сложных интерфейсов. Предлагает широкий выбор компонентов. Благодаря готовым интеграциям с внешними библиотеками возможно использование в проекте изображений в форматах PNG, JPEG или WEBP; анимаций в формате Lottie; шрифтов в форматах TrueType, OpenType или WOFF; даже возможно загружать 3D-модели в формате glTF. Библиотека поддерживает микроконтроллеры производителей таких как: STMicroelectronics (серия STM32), Espressif или NXP.
LVGL умеет использовать возможности современных микропроцессоров для HMI-приложений. Она имеет продвинутый движок рендеринга, который поддерживает рисование дуг, полупрозрачных элементов и градиентов. Дополнительно встроены драйверы для специализированных графических чипов, присутствующих в некоторых микропроцессорах.

Библиотеку можно урезать для использования в менее мощных системах. В конфигурационном файле можно отключить все неиспользуемые элементы, поддержку различных цветовых форматов, внешние библиотеки и другие функции, чтобы уменьшить размер бинарного файла. Можно также применить менее сложный движок рендеринга для снижения нагрузки на процессор. Библиотека не требует много памяти, хотя это зависит от количества созданных элементов.
Первые шаги
В первую очередь необходимо установить библиотеку и подключить её к проекту. Процедура будет отличаться в зависимости от типа проекта. Например, в проектах с использованием ESP-IDF достаточно добавить зависимость (dependency) lvgl/lvgl. Также возможно напрямую добавить код библиотеки в проект.
Следующий шаг — настройка. Это делается через редактирование файла lv_conf.h или с помощью инструмента Kconfig. В настройках можно указать доступный объём памяти и способ её выделения, включить или отключить различные функции, включая внешние библиотеки или доступные элементы интерфейса.
Перед использованием необходимо инициализировать библиотеку вызовом функции lv_init(). Далее нужно создать экземпляр дисплея lv_display_t, используя функцию lv_display_create(). Первый созданный после инициализации дисплей становится дисплеем по умолчанию. Некоторые функции принимают вместо экземпляра lv_display_t значение NULL и в таком случае используют дисплей по умолчанию. Вместе с дисплеем создаётся экран — UI-объект, являющийся корнем дерева объектов на данном дисплее. Активный экран можно получить, вызвав функцию lv_display_get_screen_active().
Интерфейс строится из элементов, называемых в LVGL объектами (objects). Элементы расположены в иерархии в виде дерева. Функции создания объектов принимают в качестве параметра родительский элемент. Родителем может быть любой элемент, в частности пустой элемент, что позволяет группировать элементы. Механизм похож на использование тега div в HTML. Подробное описание и список элементов можно найти в документации библиотеки.
Элементы на экране можно позиционировать абсолютным образом, устанавливая координаты напрямую функциями lv_obj_set_pos()/lv_obj_set_x()/lv_obj_set_y(), либо динамически относительно родителя функцией lv_obj_align() или другого объекта lv_obj_align_to(). Стоит отметить, что позиция автоматически пересчитывается заново только при привязке к родителю (если тот изменит своё положение).
Внешний вид элементов можно менять с помощью стилей. Свойства стилей можно задать напрямую для конкретного объекта или создать экземпляр lv_style_t, настроить нужные свойства и применить к нескольким компонентам с помощью функции lv_obj_add_style(). Механизм похож на классы в CSS, в частности стили в LVGL также действуют каскадно. Полный список свойств стилей приведён в документации библиотеки.
Для добавления поддержки устройств ввода (сенсорная панель, мышь, клавиатура) необходимо создать экземпляр lv_indev_t и подключить к нему (lv_indev_set_read_cb()) callback-функцию, которая будет считывать данные с устройства. Для сенсорных панелей с поддержкой нескольких точек касания одновременно (multitouch) нужно для каждой точки создать отдельный экземпляр lv_indev_t. Устройства ввода работают в режиме активного опроса (polling), то есть данные считываются автоматически через определённые интервалы времени.
Счётчик вверх/вниз
Ниже показано, как реализовать простое приложение. Оно хранит состояние счётчика и отображает его на метке. Пользователь может уменьшить значение счётчика кнопкой «-» или увеличить кнопкой «+».
Для удобства использована возможность использовать библиотеку SDL как среду запуска. Это позволяет тестировать приложение непосредственно на компьютере без необходимости наличия микропроцессора с дисплеем. Нужно установить библиотеку SDL2, а затем в конфигурационном файле установить LV_USE_SDL в 1, чтобы включить интеграцию с ней.
В начале программы следует вызвать функцию инициализации, создать дисплей и устройство ввода. В случае SDL2 обе эти вещи уже реализованы в LVGL, достаточно вызвать готовые функции. В конце main размещён обработчик цикла событий (event-loop). Он отвечает за обновление UI в ответ на разные события, например распознавание и обработку касания на сенсорной панели. 
Первым добавляемым элементом будет метка для отображения результата. Она добавляется напрямую как ребёнок активного экрана по умолчанию. Метка устанавливается по центру экрана с помощью функции lv_obj_align. Эта функция принимает также смещение в пикселях от точки привязки, здесь оно установлено по обеим осям в ноль. Использована функция lv_label_set_text_static, а не lv_label_set_text, потому что аргументом передан текстовый литерал, доступный всё время выполнения программы, поэтому нет необходимости копировать текст во внутреннюю память метки.

Далее будут добавлены две кнопки с обозначениями «+» и «-». При добавлении кнопок использовалось смещение в lv_obj_align, чтобы немного отодвинуть их от краёв. Сама кнопка не хранит текст или изображения, отображаемые на ней. Это отдельные элементы, чьим родителем является кнопка. В данном случае достаточно добавить метку, потому что LVGL предоставляет набор символов в виде шрифта. Полный список символов доступен в документации библиотеки.

Чтобы приложение стало интерактивным, кнопкам нужно добавить callback-функцию, вызываемую при нажатии. Значение счётчика хранится в глобальной переменной g_counter. Для доступа к метке использовались функции доступа к дереву элементов lv_obj_get_parent и lv_obj_get_child. Аргумент 0 обозначает доступ к старшему (первому добавленному) ребёнку. Функция также принимает отрицательные значения, которые обращают порядок доступа, например -1 — доступ к последнему добавленному ребёнку.

Для связывания callback-функций с событием используется функция lv_obj_add_event_cb. Третий параметр этой функции — фильтр событий, что даёт возможность одной функции обрабатывать разные события. Четвёртый параметр — известный в C параметр void*, используемый для передачи пользовательских параметров в callback.

Внешний вид элементов можно менять, устанавливая свойства прямо для элемента или с помощью стилей. Стили полезны, когда несколько элементов должны иметь одинаковый вид, чтобы избежать повторений. Они работают каскадно — значения свойств одного стиля могут быть переопределены добавленным позже стилем. В примере добавлены две кнопки, которые должны иметь одинаковый вид. Хорошей идеей будет применить к ним стили.

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

Логику приложения можно реализовать, используя паттерн наблюдатель. LVGL предоставляет объект наблюдаемый lv_subject_t, к которому можно подключить наблюдателей. Некоторые элементы имеют встроенные специальные функции для работы с наблюдаемыми объектами. Например, текст метки может быть связан с таким объектом и автоматически обновляться при изменении наблюдаемого значения. Другой пример — кнопка, которая по нажатию может изменить текущее значение. Также можно подключить собственный наблюдатель в виде callback-функции.
Реализация выглядела бы так: глобальная переменная счётчика заменяется экземпляром lv_subject_t. Текст метки связывается с наблюдаемым объектом с помощью функции lv_label_bind_text. Кнопки по нажатию автоматически будут добавлять к текущему значению +1 или -1. Для этого служит функция lv_obj_add_subject_increment_event, которая добавляет к объекту инкремент наблюдаемого объекта с данным константным значением, обрабатывая указанное событие.
Ниже приведён код, необходимый для использования lv_observer_t. Callback-функции s_on_down_clicked и s_on_up_clicked в этом случае не потребуются, как и начальная инициализация текста метки.

Пример приложения: текстовый редактор
В качестве примера более сложного приложения, использующего: векторные шрифты, интеграцию с файловой системой и расширенные UI-элементы, представлен текстовый редактор. UI с одной стороны вдохновлён традиционным настольным приложением, благодаря панели инструментов, с другой стороны адаптирован для сенсорных экранов благодаря экранной клавиатуре. Для построения использованы готовые элементы библиотеки без больших изменений, а также собственные элементы, созданные на основе готовых.
Для расположения элементов применён flex-лейаут. Клавиатура, текстовое поле и панель инструментов расположены в колонку. Сама панель инструментов является flex-контейнером с горизонтальной ориентацией. Автоматически рассчитываемые отступы между элементами поддерживаются и использованы для размещения кнопки закрытия. Flex-лейаут в LVGL похож на тот, что известен по CSS. Это удобно и позволяет легко позиционировать компоненты на экране.

Примером продвинутого использования компонентов является окно выбора файла для сохранения (и открытия). Для создания использован элемент lv_msgbox, который в базовой версии служит для отображения коротких сообщений пользователю. Основная часть элемента в простом случае — метка с текстом сообщения. Однако вместо метки можно добавить больше элементов и рассматривать окно сообщений как универсальное окно. Список файлов построен на основе таблицы с собственной функцией отрисовки. Это требует значительно меньше памяти, чем список из меток.
Благодаря библиотеке stb_truetype LVGL поддерживает векторные шрифты в формате TrueType. Для использования этой функции в конфигурации нужно включить движок шрифтов Tiny TTF. Шрифт элемента (например, текстового поля) можно менять в процессе работы приложения. Изменённый элемент автоматически помечается для повторного перерисовывания. Движок Tiny TTF поддерживает изменение размера шрифта. Размер может быть изменён в любой момент после загрузки шрифта. Нужно помнить о необходимости вручную помечать изменение стиля, чтобы элементы, использующие этот стиль, автоматически обновлялись. Благодаря поддержке кодировки UTF-8 можно полностью использовать возможности мультиязычных шрифтов. Широкий выбор открытых шрифтов в формате TrueType можно найти на сайте Google Fonts.
Приложение использует модуль lv_fs для работы с файлами, который предоставляет единый интерфейс для нескольких файловых систем. Доступно несколько реализаций, в том числе: основанная на интерфейсе стандартной библиотеки C, API POSIX или Win32. Поддерживается одновременная работа нескольких реализаций. Пути начинаются с заглавной буквы, идентифицирующей драйвер файловой системы. Например, букве P может соответствовать реализация POSIX, а W — Win32. Тогда путь P:/home/user/file.txt будет обрабатываться POSIX, а W:C:\Users\user\file.txt — Win32.
Итоги
LVGL имеет широкое применение: от простых интерфейсов на оборудовании с очень ограниченными ресурсами до сложных интерфейсов, напоминающих мобильные приложения. Проектирование облегчает широкий выбор готовых компонентов, которые легко модифицировать по необходимости. Системы стилей и layоуты flex и grid, основанные на CSS, также упрощают работу. Поддержка файловой системы, аллокации и логов независимо от платформы позволяет создавать полностью портируемые приложения или легко переносить их между платформами. Возможность запускать на компьютерах с Linux, Windows или Mac ускоряет разработку UI. Таким образом, это отличный выбор для создания профессиональных графических интерфейсов на встроенных системах.
