LVGL est une bibliothèque ouverte et gratuite pour créer des interfaces graphiques utilisateur sur des dispositifs embarqués. Elle comporte de nombreuses fonctions et intégrations, ce qui la rend adaptée à la création d'interfaces simples comme complexes. Elle offre un large choix de composants. Grâce à des intégrations prêtes à l’emploi avec des bibliothèques externes, il est possible d’utiliser dans un projet des images aux formats PNG, JPEG ou WEBP ; des animations au format Lottie ; des polices au format TrueType, OpenType ou WOFF ; il est même possible de charger des modèles 3D au format glTF. La bibliothèque supporte les microcontrôleurs de fabricants tels que STMicroelectronics (série STM32), Espressif ou NXP.
LVGL peut exploiter les capacités des microprocesseurs modernes pour les applications HMI. Elle dispose d'un moteur de rendu avancé qui supporte le dessin d'arcs, d'éléments semi-transparents ainsi que des dégradés. De plus, elle intègre des pilotes pour des circuits graphiques dédiés présents sur certains microprocesseurs.

La bibliothèque peut être réduite pour être utilisée sur des systèmes moins puissants. Dans le fichier de configuration, il est possible de désactiver tous les éléments inutilisés, le support de différents formats de couleur, les bibliothèques externes et autres fonctionnalités afin de diminuer la taille du fichier binaire. On peut aussi utiliser un moteur de rendu moins avancé pour réduire la charge du processeur. La bibliothèque ne nécessite pas beaucoup de mémoire, bien que cela dépende du nombre d’éléments créés.
Premiers pas
La première étape consiste à installer la bibliothèque et à l'inclure dans le projet. La procédure varie selon le type de projet. Par exemple, dans les projets utilisant ESP-IDF, il suffit d’ajouter la dépendance lvgl/lvgl. Il est aussi possible d’ajouter directement le code de la bibliothèque au projet.
L’étape suivante est la configuration, qui s’effectue par édition du fichier lv_conf.h ou via l'outil Kconfig. Dans la configuration, on peut définir la quantité de mémoire disponible et son mode d’allocation, activer ou désactiver différentes fonctionnalités, y compris les bibliothèques externes ou les éléments d’interface disponibles.
Avant utilisation, il faut initialiser la bibliothèque en appelant la fonction lv_init(). Ensuite, il faut créer une instance d’affichage lv_display_t avec la fonction lv_display_create(). Le premier écran créé après l’initialisation devient l’écran par défaut. Certaines fonctions acceptent la valeur NULL à la place d’une instance lv_display_t et utilisent alors l’écran par défaut. Avec l’écran, un écran UI est créé, qui constitue la racine de l’arbre des objets pour cet affichage. On peut obtenir l’instance de l’écran actif via la fonction lv_display_get_screen_active().
L’interface se construit avec des éléments appelés objets (objects) dans LVGL. Les éléments sont arrangés en une hiérarchie en forme d’arbre. Les fonctions de création d’objets prennent comme paramètre l’élément parent. Ce parent peut être n’importe quel élément, notamment un élément vide, ce qui permet de grouper des éléments. Ce mécanisme est similaire à l’utilisation de la balise div en HTML. Une description détaillée ainsi que la liste des éléments se trouvent dans la documentation de la bibliothèque.
On peut positionner les éléments sur l’écran de manière absolue, en définissant directement leurs coordonnées avec lv_obj_set_pos()/lv_obj_set_x()/lv_obj_set_y(), ou dynamiquement par rapport au parent avec lv_obj_align() ou par rapport à un autre objet avec lv_obj_align_to(). Il est important de noter que la position est recalculée automatiquement uniquement si l’élément est lié à son parent (par exemple si ce dernier change de position).
L’apparence des éléments peut être modifiée en utilisant des styles. Les propriétés de style peuvent être définies directement pour un objet donné, ou on peut créer une instance lv_style_t, configurer les propriétés appropriées, puis les appliquer à plusieurs composants via lv_obj_add_style(). Ce mécanisme ressemble aux classes en CSS, notamment parce que les styles dans LVGL fonctionnent également de façon en cascade. La liste complète des propriétés de style se trouve dans la documentation de la bibliothèque.
Pour ajouter la prise en charge des périphériques d’entrée (panneau tactile, souris, clavier), il faut créer une instance lv_indev_t et lui attacher (avec lv_indev_set_read_cb()) une fonction callback qui lira les données du périphérique. Pour les panneaux tactiles prenant en charge plusieurs points de contact simultanément (multitouch), une instance lv_indev_t distincte doit être créée pour chaque point. Les périphériques d’entrée sont gérés en mode polling, ce qui signifie qu’ils sont automatiquement interrogés périodiquement.
Compteur haut/bas
Voici comment implémenter une application simple. L’application conserve l’état d’un compteur et l’affiche sur une étiquette. L'utilisateur peut diminuer le compteur avec un bouton « - » ou l’augmenter avec un bouton « + ».
Pour faciliter, on a utilisé la possibilité d’employer la bibliothèque SDL comme plateforme d’exécution. Ainsi, les tests peuvent être effectués directement sur un ordinateur, sans besoin d’un microprocesseur avec écran. La bibliothèque SDL2 doit être installée, puis LV_USE_SDL doit être réglé à 1 dans le fichier de configuration pour activer son intégration.
Au début du programme, il faut appeler la fonction d'initialisation, créer l’écran, ainsi que le périphérique d’entrée. Dans le cas de SDL2, ces deux fonctionnalités sont déjà implémentées dans LVGL et il suffit d’appeler les fonctions prêtes à l’emploi. À la fin de la fonction main se trouve la gestion de la boucle d’événements (event-loop). Celle-ci est responsable de rafraîchir l’interface utilisateur en réponse à divers événements, par exemple la détection et le traitement d’un clic sur le panneau tactile. 
Le premier élément ajouté sera une étiquette pour afficher le résultat. Elle sera ajoutée directement comme enfant de l’écran actif par défaut. L’étiquette sera centrée avec la fonction lv_obj_align. Cette fonction accepte aussi un décalage en pixels par rapport au point de référence, ici fixé à zéro sur les deux axes. La fonction lv_label_set_text_static a été utilisée au lieu de lv_label_set_text, car un littéral de chaîne de caractères est passé en argument, disponible pendant toute la durée d'exécution du programme, n’exigeant donc pas la copie du texte dans la mémoire interne de l’étiquette.

Ensuite, deux boutons marqués « + » et « - » sont ajoutés. Lors de l’ajout des boutons, on a utilisé la possibilité d’ajouter un décalage dans lv_obj_align pour les éloigner un peu des bords. Le bouton en lui-même ne contient pas le texte ou les images affichées dessus. Ce sont des éléments séparés ayant le bouton comme parent. Ici, il a suffi d’ajouter une étiquette, puisque LVGL fournit un ensemble de symboles sous forme de police. La liste complète des symboles disponibles se trouve dans la documentation de la bibliothèque.

Pour rendre l’application interactive, il faut ajouter une fonction callback aux boutons, appelée lors d’un appui. La valeur du compteur est stockée dans une variable globale g_counter. Pour accéder à l’étiquette on utilise les fonctions d’accès à l’arbre d’éléments lv_obj_get_parent et lv_obj_get_child. L'argument 0 signifie l’accès au premier enfant ajouté, tandis que lv_obj_get_child accepte aussi des valeurs négatives, qui inversent l’ordre d’accès, -1 correspondant au dernier enfant ajouté.

Pour connecter les fonctions callback aux événements, on utilise la fonction lv_obj_add_event_cb. Le troisième paramètre est un filtre d’événements, permettant ainsi d’ajouter une seule fonction pour gérer divers événements. Le quatrième paramètre est un pointeur void* classique en C, utilisé pour transmettre des paramètres utilisateur aux callbacks.

L’apparence des éléments peut être modifiée en réglant les propriétés directement sur un élément donné ou via des styles. Les styles sont utiles quand plusieurs éléments doivent avoir le même aspect, évitant ainsi la répétition. Ils fonctionnent en cascade : les valeurs de propriétés d’un style peuvent être écrasées par un deuxième style ajouté ensuite. Dans cet exemple, deux boutons ont le même aspect. Il est donc judicieux d’utiliser des styles pour eux.

Les éléments ont divers états selon ce qui leur arrive, par exemple : lorsque l’utilisateur survole un élément avec le curseur, quand il est pressé, ou quand rien ne se passe. Différents styles peuvent être assignés selon l’état, opérant de façon en cascade. Lorsqu’on ajoute un style à un objet, on spécifie le sélecteur indiquant l’état dans lequel le style est actif. Ce mécanisme est similaire aux pseudo-classes en CSS. Pour des modifications ou éléments uniques, on peut aussi les définir directement sur l’élément.

La logique de l’application peut être implémentée en utilisant le modèle de conception observateur. LVGL fournit l’objet observable lv_subject_t auquel on peut connecter des observateurs. Certains éléments ont des fonctions spéciales intégrées facilitant les opérations sur les objets observés. Par exemple, le texte d'une étiquette peut être lié à un objet observable, ce qui met à jour automatiquement le texte à chaque changement de la valeur observée. Un autre exemple est un bouton qui modifie la valeur actuelle lorsqu’il est pressé. Il est également possible de connecter un observateur personnalisé sous forme de fonction callback.
L'implémentation serait la suivante : la variable globale stockant le compteur est remplacée par une instance lv_subject_t. Le texte de l’étiquette est lié à l’objet observable via lv_label_bind_text. Les boutons, lorsqu’ils sont cliqués, ajoutent automatiquement +1 ou -1 à la valeur actuelle. Cela est réalisé via lv_obj_add_subject_increment_event, qui ajoute à un objet l’incrémentation de la valeur de l’objet observable par une constante donnée, en réaction à un événement spécifié en argument.
Ci-dessous le code à ajouter pour utiliser lv_observer_t. Les callbacks s_on_down_clicked et s_on_up_clicked ne seront pas nécessaires ici, tout comme l’initialisation initiale du texte de l’étiquette.

Exemple d’application : éditeur de texte
Comme exemple d’application plus élaborée, utilisant des polices vectorielles, l’intégration avec le système de fichiers et des éléments d’UI étendus, un éditeur de texte est présenté. L’interface s’inspire d’une part des applications traditionnelles de bureau par la barre d’outils, et d’autre part est adaptée aux écrans tactiles grâce au clavier virtuel. On utilise des éléments prêts à l’emploi de la bibliothèque sans changements majeurs, ainsi que des éléments customisés basés sur ceux-ci.
Pour disposer les éléments, on emploie un layout de type flex. Le clavier, le champ texte et la barre d’outils sont arrangés en colonne. La barre d’outils elle-même est un conteneur flex en ligne. Les espacements automatiques entre éléments sont supportés et utilisés pour placer le bouton de fermeture. Le layout flex dans LVGL est similaire à celui du CSS et constitue une solution très pratique pour positionner facilement les composants à l’écran.

Un exemple d’utilisation avancée des composants est la fenêtre de sélection de fichier pour sauvegarder (ou charger). Elle utilise lv_msgbox qui sert normalement à afficher des messages courts à l’utilisateur. La partie principale de l’élément est dans le cas le plus simple une étiquette avec le texte du message. Mais au lieu d’une étiquette, on peut ajouter plusieurs éléments, traitant la fenêtre comme une boîte générique. La liste de fichiers est construite sur une table avec une fonction de dessin personnalisée, ce qui nécessite beaucoup moins de mémoire qu’une liste d’étiquettes.
Grâce à la bibliothèque stb_truetype, LVGL supporte les polices vectorielles au format TrueType. Pour activer cette fonction, il faut activer le moteur de polices Tiny TTF dans la configuration. La police d'un élément (comme un champ texte) peut être modifiée pendant l’exécution. L'élément modifié est automatiquement marqué pour être redessiné. Le moteur Tiny TTF supporte le changement de taille de police à tout moment après chargement. Il faut rappeler de marquer manuellement le changement de style pour que les éléments utilisant cette police soient rafraîchis. Avec le support de l’encodage UTF-8, on peut pleinement exploiter les polices multilingues. Une large sélection de polices TrueType open source est disponible sur Google Fonts.
L’application utilise le module lv_fs pour la gestion des fichiers, qui fournit une interface commune pour plusieurs systèmes de fichiers. Plusieurs implémentations sont disponibles, parmi lesquelles : basée sur l’interface standard de la bibliothèque C, l’API POSIX ou Win32. Plusieurs implémentations peuvent être utilisées simultanément. Les chemins commencent par une lettre majuscule identifiant le pilote de système de fichiers. Par exemple, on peut assigner P pour POSIX, et W pour Win32. Ainsi, le chemin P:/home/user/file.txt sera géré par POSIX, tandis que W:C:\Users\user\file.txt par Win32.
Résumé
LVGL a de larges applications : des interfaces simples pour du matériel très limité aux interfaces très élaborées ressemblant à des applications mobiles. La conception est facilitée par un large choix de composants prêts à l’emploi, facilement modifiables selon les besoins. Le travail est aussi facilité par le système de styles ainsi que les layouts flex et grid, basés sur ceux du CSS. Le support du système de fichiers, allocation et logs indépendants de la plateforme permet d’écrire des applications pleinement portables ou faciles à porter sur une autre plateforme. La possibilité de faire tourner sur ordinateur Linux, Windows ou Mac accélère la conception UI. C’est donc un excellent choix pour créer des interfaces graphiques professionnelles sur systèmes embarqués.
