This post is going to be a tutorial – how to use LVGL (Light and Versatile Graphics Library) with an ESP-32 microprocessor running Arduino framework. So, I have decided to write about it, because when I tried to learn more about it myself, there wasn’t a lot of information, tutorials and/or examples using the library (except for the original documentation).
I will try to cover from first steps running the display and touch screen outside the library to a simple, yet covering all the basics, LVGL project. In the end, the tutorial should help to get familiar with the library’s porting to a particular project, its working logic, GUI’s styling capabilities and some widgets.
Table of contents
- About LVGL
- Part list
- PlatformIO
- Hardware setup
- Running LCD outside LVGL
- First simple LVGL project
- Component styles
- Themes
- Widgets
- Links
About LVGL
In short, LVGL is a graphics library with extensive list of features. It should work with probably all displays out there, because it is not an LCD driver – it depends on additional libraries to drive the display. Also, it is a free, open-source library which, in my opinion, can be used for any complexity projects. You can find some demos on its official page which shows off what the library is capable of.
Part list
Hardware:
- ESP32 development board (Affiliate Aliexpress)
- LCD with SPI interface and resistive touch screen (used in this tutorial, could be used other kind of display) (Affiliate Aliexpress)
- Wires and/or adapter
- USB cable
Software/firmware:
- PlatformIO (you can use Arduino IDE, but in this tutorial PlatformIO IDE will be used)
- Adafruit’s, eSPI or other display driving library
- Adafruit’s Touch library (can be used other libs -depends on the touch screen’s type)
PlatformIO
For this tutorial I will be using PlatformIO IDE to write a firmware for an ESP-32 microcontroller. I have chosen this IDE, because, in my opinion, it is easier to work with than Arduino IDE. The code should also be easily adaptable to an ESP8266 microcontroller. Also, Arduino libraries are going to be used as a main framework.
To start with PlatformIO, first of all, you will need to download it. Actually, it is an add-on for Visual Studio Code – extensible text editor.
When you have it installed, you can create your first project. To do it, when the Visual Studio Code opens, you will need to press on “New Project” in PlatformIO’s home screen. If you don’t see it (for e.g. you see a blank screen), press on a little home icon in the bottom left corner to open PlatformIO’s home screen.
After pressing “New Project” button you will get a setup screen, where you will have to write the project’s name, select the board which you are using and select a framework which you will be using. If you use ESP board you will get to choose either Arduino or Espressif IOT Development Framework. This tutorial will use Arduino, but if you are creating some other project and want to use different framework your are free to choose it here.
Finally, you can choose whether to use default project or a custom one. Then, press “Finish” button and PlatformIO will create a new project with an empty screen:
PlatformIO file structure
So, you have your new project created, the question might arise, where all your code goes?
The file structure of a newly created project folder is as follows:
The most important folders are “lib”, “src” and “include”. Folder “lib” should contain all libraries which are not the part of Arduino/Espressif IOT frameworks. Usually, if you download some libraries from the internet or create your own libs, you will put them in the “lib” folder.
Folder “src” as the name suggests will contain al your source files. Newly created project will already contain “main.cpp
” file. Moreover, in this file you will find familiar “setup
” and “loop
” functions, if you are using Arduino.
Folder “include” should contain all your project wide header files. In other words, there might be .h
files for your source files in the “src” folder.
Lastly, an important file is platformio.ini
. It is a PlatformIO project configuration file. There you can set which board or framework is used. Also, there you can define what serial port needs to be used, if the default is not detected correctly, or to which IP upload the firmware, if OTA is used.
Hardware setup
In this tutorial an SPI 3.2 inch resistive touch display will be used. The display has 18 pin interface which is connected to the ESP-32 development board as follows:
LCD Connector Pin No. | LCD Pin Name | ESP32 Pin No. |
1 | GND | GND |
2 | RESET | 5 |
3 | SCL | 18 |
4 | RS/A0 | 16 |
5 | CS | 17 |
6 | SDA | 23 |
7 | SDO | not used |
8 | GND | GND |
9 | VCC | +3.3V |
10 | A | +3.3V |
11 | K1 | GND |
12 | K2 | GND |
13 | K3 | GND |
14 | K4 | GND |
15 | XL(X-) | 33 |
16 | YU(Y-) | 32 |
17 | XR(X+) | 25 |
18 | YD(Y+) | 26 |
So, the whole electronics connection looks like this:
You can see that the display uses a CNC-cut 18 pin break out board which was made in the previous tutorial. Of course, it is easier just to use an LCD with already attached header board for a simpler wiring.
Running LCD outside LVGL
First of all, you need a display which is working outside LVGL – with only its driver libraries. Depending on the display type (SPI, I2C, Parallel) there are several choices. If you are using Adafruit’s (or similar LCD with the same driver IC) display, you can use Adafruit libraries. You can also use Bodmer’s eSPI library which based on Adafruit’s libraries. eSPi library can be used if you are already familiar with it. You could also write you own several driver functions for a display – that way you will save some program memory, because LVGL does require only some functions from the mentioned libraries.
Using Adafruit’s libraries
If you are using Adafruit’s libs, download adafruit_gfx
, adafruit_ili9341
(or another library – it depends on your driver IC) and BusIO
libraries. Put them into PlatformIO project folder. Alternatively, you can just download needed code from my GitHub.
So, firstly we need to define some pins:
#define _cs 17 // goes to TFT CS
#define _dc 16 // goes to TFT DC
#define _mosi 23 // goes to TFT MOSI
#define _sclk 18 // goes to TFT SCK/CLK
#define _rst 5 // goes to TFT RESET
These pins are going to be used to create a screen’s object:
Adafruit_ILI9341 tft = Adafruit_ILI9341((int8_t)_cs, (int8_t)_dc, _mosi, _sclk, (int8_t)_rst);
With the code shown above we will create a tft
object which will use software SPI. To us hardware SPI code will look like this:
Adafruit_ILI9341 tft = Adafruit_ILI9341((int8_t)_cs, (int8_t)_dc, (int8_t)_rst);
I would recommend to start with software SPI to see if the screen works, then change it to hardware SPI, as I myself had difficulties to start LCD working with HW SPI. Also, you should add some #include
‘s so the tft
object would be recognized by the IDE:
#include <Arduino.h>
#include "Adafruit_GFX.h"
#include "Adafruit_ILI9341.h"
Now, let’s fill in the setup()
function as shown below:
void setup() {
Serial.begin(9600);
Serial.println("HELLO");
tft.begin();
tft.fillScreen(0xffff);
delay(500);
tft.fillScreen(0x0000);
delay(500);
tft.fillScreen(ILI9341_RED);
delay(500);
tft.fillScreen(ILI9341_GREEN);
tft.setRotation(0);
tft.setCursor(0, 0);
tft.setTextColor(ILI9341_BLUE);
tft.setTextSize(1);
tft.println("Hello World!");
}
Function Serial.begin(9600)
starts serial interface which will be used later, while Serial.println("HELLO")
prints out “HELLO” string during startup on the serial monitor.
Function tft.begin();
starts SPI driver for the LCD. All tft.fillScreen(value)
functions fill whole screen with one color.
Function tft.setRotation(0)
sets whether the text and drawing will be shown rotated or not. Value 0 means that the rotation is set to default which is vertical view in my case.
Function tft.setCursor(0, 0)
sets cursor position to the top left corner. After that, goes function tft.setTextColor(ILI9341_BLUE)
which sets text color to blue. Note, that ILI9341_BLUE
is a color definition which is defined in Adafruit_ILI9341 library. Furthermore, function tft.setTextSize(1)
sets smallest text size (actually a text size which is defined as the first). Finally, tft.println("Hello World!")
prints out “Hello World!” string to the display.
If we leave loop()
function empty and upload the code to the ESP, we should see that the screen starts to change its colors and finally writes “Hello World” string:
Its tiny, but it is possible to see “Hello World” string in the above picture. Also, I have an LCD with some defects (two “dead” lines on the top of the screen).
A file with full code (also, touch included) you can find here.
Touch input
Next, let’s read input data from the touch screen. For this purpose we will use Adafruit’s TouchScreen library.
You should download Adafruit’s TouchScreen library and put it into PlatformIO project’s “lib” folder.
In main.cpp
file add an include:
#include "TouchScreen.h"
Next, we should define the pins that are used as touch pins:
#define YP 26
#define XM 25
#define YM 32
#define XP 33
Here, YP goes to LCD’s Y+ pin, XM to X-, YM to Y- and XP to X+ pins.
Then, let’s create a touch screen object:
TouchScreen ts = TouchScreen(XP, YP, XM, YM, 340);
Touch screen library uses ADC’s and digital pins to apply a voltage across the touch plane and read voltage when the screen is touched. The library expects to get 10 bit ADC value, but with default settings ESP-32 reads 12 bit values. So, we need to change it with a line in the setup()
function:
analogReadResolution(10);
Finally, to read and output to Serial touch values, add the code to the loop()
function:
void loop() {
// a point object holds x y and z coordinates
TSPoint p = ts.getPoint();
if (p.z > ts.pressureThreshhold) {
Serial.print("X = "); Serial.print(p.x);
Serial.print("\tY = "); Serial.print(p.y);
Serial.print("\tPressure = "); Serial.println(p.z);
}
delay(100);
}
Here we create TSPoint object p which holds three coordinates x, y, z. Coordinates x and y shows where the touch happens, z – holds touch “pressure” value. So, by evaluating what z value is, we can know if the screen is pressed or not.
So, checking if p.z > ts.pressureThreshhold
gives us an answer if the screen is pressed. pressureThreshhold
is a defined threshold value by the library. Then, if expression is true, it prints out to the Serial the touch point values. After uploading the code to the ESP you should see output values on the Serial monitor when you press the screen:
One thing to note here – you need to get lower x and y values on the top-left corner of the screen and higher values on the bottom-right corner. If one or both values behaves differently – you can just invert physical connection of the wires. So, for example, if you get higher X value on the left side than on the right, swap X+ and X- cable connections and you should get them inverted. This behavior is needed to get the LVGL library to correctly understand where the touch really happens.
A file with full code (also, with GFX included) you can find here.
Using eSPI
If you are using eSPI – download the files from here and put them into PlatformIO project’s “lib” folder. Alternatively, you can find the full code at my git.
Firstly, after copying the library files to PlatformIO “lib” folder, you will need to edit “User_Setup.h” file to suit your needs. In my case, all definitions were left intact, except these lines:
#define TFT_MISO 19
#define TFT_MOSI 23
#define TFT_SCLK 18
#define TFT_CS 17
#define TFT_DC 16
#define TFT_RST 5
Because eSPI library is based on Adafruits graphics library, I have only modified several lines from the previous part. So, I have changed included libraries:
#include <Arduino.h>
#include "SPI.h"
#include "TouchScreen.h"
//include if you are using a resistive touchscreen
#include "TFT_eSPI.h"
Also, now another tft object is going to be used:
TFT_eSPI tft = TFT_eSPI();
Other lines were left intact and in result I have got the same “hello World!” screen as previously with Adafruit’s libraries.
Using other libraries
As I have already mentioned, you can use other display and touch libraries/drivers. Also, you can write your own driver libraries.
These libraries should work similarly to the ones mentioned earlier. Therefore, for LVGL to work correctly, LCD library needs to be able to write to a defined part of the screen. While the touch library has to be able to read the touch position in x-y plane and tell if the screen is pressed or not.
To clarify what functions should the custom libraries have, please read below.
First simple starting project with LVGL and ESP-32
When you have your LCD up and running, next step is to adapt LVGL to your display setup. Firstly, download LVGL and put it to PlatformIO project’s “lib” folder. Alternatively, you can download whole project from my GitHub.
This tutorial will use 7.7.1 version of the LVGL library. So, keep in mind that not everything in this tutorial might be applicable if your are using older or a newer version.
Editing config file
In the beginning we will need to configure a bit LVGL library. Go to LVGL folder in your project. You should be able to find “lv_conf_template.h” file. Rename it to “lv_conf.h”. Then, open it for editing. Inside you will find a line which tells you “copy this file as `lv_conf.h` NEXT TO the `lvgl` folder”. Although you might want to do that, but in my case lvgl library worked fine when that file was left inside ‘lvgl’ folder.
Firstly, you should change the first definition to look like this:
#if 1
Then, find a line:
#define LV_TICK_CUSTOM 0
When using Arduino, you will need to change its value to 1:
#define LV_TICK_CUSTOM 1
Although there are more settings in this file, those two lines needs to be changed to be able to start a simple LVGL example on an Arduino system. Furthermore, if you will leave LV_TICK_CUSTOM with a 0 value, the screen won’t be updated periodically or after screen touches.
Editing main.cpp file
Now open again your main.cpp file where you have already written code for writing data to display and reading touch inputs (from the previous steps).
Include LVGL library:
#include "../lvgl/src/lvgl.h"
You will probably notice a long path in the include above. Usually it should work like that:
#include "lvgl.h"
But sometimes PlatformIO decides not to find needed header files and you will have to write a relative path from the current file.
Next, let’s create some variables:
TSPoint oldPoint;
static lv_disp_buf_t disp_buf;
static lv_color_t buf[LV_HOR_RES_MAX * 10];
lv_disp_drv_t disp_drv;
lv_indev_drv_t indev_drv;
lv_obj_t *btn1;
lv_obj_t *btn2;
lv_obj_t *screenMain;
lv_obj_t *label;
Variable “oldPoint” will be used later to hold old touch point value. Variables “disp_buf” and “buf” are buffers used by the LVGL. Furthermore, “disp_drv” and “indev_drv” are used to define LCD and Input functions as it will be seen later. Finally, all “lv_obj_t” variables are pointers to the used screen objects, like buttons, labels or the screen itself.
The two functions
Afterwards, we need to create two very important functions. Firstly, let’s start from a function:
void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p)
{
uint32_t w = (area->x2 - area->x1 + 1);
uint32_t h = (area->y2 - area->y1 + 1);
uint32_t wh = w*h;
tft.startWrite();
tft.setAddrWindow(area->x1, area->y1, w, h);
while (wh--) tft.pushColor(color_p++->full);
tft.endWrite();
lv_disp_flush_ready(disp);
}
This function writes color information from the “color_p” pointer to the needed “area”. So let’s go trough some lines.
Variables “w”, “h” and “wh” simplifies the usage of Adafruits functions, because it needs to be passed by variables of such values. Next function “tft.startWrite()” initiates the data write process to the LCD. Then, “tft.setAddrWindow(area->x1, area->y1, w, h)” write data to the LCD part which starts at “x1”, “y1” and is in “w” and “h” dimensions. After that, a loop writes all data with the function “tft.pushColor(color_p++->full)”. Finally, “tft.endWrite()” ends the data write.
In the end, “lv_disp_flush_ready(disp)” informs the LVGL that we have written needed data.
This was an example for Adafruit’s LCD driver library. If you are using eSPI – you can change this line:
while (wh--) tft.pushColor(color_p++->full);
with this one:
tft.pushColors(&color_p->full, w * h, true);
Now, let’s go to input function:
bool my_input_read(lv_indev_drv_t * drv, lv_indev_data_t*data)
{
TSPoint p = ts.getPoint();
if (p.z > ts.pressureThreshhold){
data->state = LV_INDEV_STATE_PR;
data->point.x = p.x*240/1024;
data->point.y = p.y*320/1024;
oldPoint.x = p.x;
oldPoint.y = p.y;
}
else {
data->state = LV_INDEV_STATE_REL;
data->point.x = oldPoint.x*240/1024;
data->point.y = oldPoint.y*320/1024;
}
return false; /*No buffering now so no more data read*/
}
Fist of all, we read a new point data with “ts.getPoint()” function. Then, we it check if there was an actual screen press. If it was, it sets the state to “pressed” and writes in the point data. If it is not pressed, then it keeps the old data from “oldPoint” variable. Note, that we need to do some math, because when it read point coordinates they are actually 10 bit ADC values which are in the range 0…1023. Touch data needs to correspond to the screens resolution. So, if you have 240×320 screen resolution, you need to modify the touch data so its maximum value becomes 239 and 319 respectively.
Also, it should be noted, that in reality with resistive screen you might never get 0 and 1023 values. In my case, I’ve got minimum value of around 100 and maximum – ~900. So, there should be some additional calibration done for touches to be detected more precise. In my case it worked without additional calibration, but if you need to get precise touch data on small screen objects – it might become an issue.
Other LCD and Touch driver libraries
If you are using other LCD or Touch libraries, or you are writing them own, by looking at the two functions above, you should be able to tell what it is needed to adapt LVGL to your own project. Your driver libraries should be able to write screen data to a selected LCD area and read current touch point coordinates and the status of the press point (whether the screen is pressed or not).
Setup function
When we have two main functions written, we need to will needed lines of code to the “setup()
” function:
void setup() {
Serial.begin(9600);
tft.begin();
tft.setRotation(0);
analogReadResolution(10);
oldPoint = ts.getPoint();
lv_init();
lv_disp_buf_init(&disp_buf, buf, NULL, LV_HOR_RES_MAX * 10);
/*Initialize the display*/
lv_disp_drv_init(&disp_drv);
disp_drv.hor_res = 240;
disp_drv.ver_res = 320;
disp_drv.flush_cb = my_disp_flush;
disp_drv.buffer = &disp_buf;
lv_disp_drv_register(&disp_drv);
/*Initialize the input device driver*/
lv_indev_drv_init(&indev_drv);
indev_drv.type = LV_INDEV_TYPE_POINTER;
indev_drv.read_cb = my_input_read;
lv_indev_drv_register(&indev_drv);
/*Create screen objects*/
screenMain = lv_obj_create(NULL, NULL);
label = lv_label_create(screenMain, NULL);
lv_label_set_long_mode(label, LV_LABEL_LONG_BREAK);
lv_label_set_text(label, "Press a button");
lv_label_set_align(label, LV_LABEL_ALIGN_CENTER);
lv_obj_set_size(label, 240, 40);
lv_obj_set_pos(label, 0, 15);
btn1 = lv_btn_create(screenMain, NULL);
lv_obj_set_event_cb(btn1, event_handler_btn);
lv_obj_set_width(btn1, 70);
lv_obj_set_height(btn1, 32);
lv_obj_set_pos(btn1, 32, 100);
lv_obj_t * label1 = lv_label_create(btn1, NULL);
lv_label_set_text(label1, "Hello");
btn2 = lv_btn_create(screenMain, NULL);
lv_obj_set_event_cb(btn2, event_handler_btn);
lv_obj_set_width(btn2, 70);
lv_obj_set_height(btn2, 32);
lv_obj_set_pos(btn2, 142, 100);
lv_obj_t * label2 = lv_label_create(btn2, NULL);
lv_label_set_text(label2, "Goodbye");
lv_scr_load(screenMain);
}
First several lines are the same as previously – Serial, Adafruit’s LCD initialization, ADC resolution setup. Then setup()
initializes lvgl, display and touch input. Here you can find references to “my_disp_flush
” and “my_input_read
” functions that we have previously written.
When everything is initialized, we need to create an actual screen object which will be shown on the screen.
The line
screenMain = lv_obj_create(NULL, NULL);
creates a screen object which will hold all other objects. In other words, it will be the parent object to everything else. If you will hide the “screenMain"
object, all child objects will be also hidden. This is a convenient behavior when you need to switch between the screens.
Next, goes label creation:
label = lv_label_create(screenMain, NULL);
lv_label_set_long_mode(label, LV_LABEL_LONG_BREAK);
lv_label_set_text(label, "Press a button");
lv_label_set_align(label, LV_LABEL_ALIGN_CENTER);
lv_obj_set_size(label, 240, 40);
lv_obj_set_pos(label, 0, 15);
It is created as “screenMain
” object’s child. Moreover, there are set some label’s parameters like break the line if the text is tool long, label’s text string and its alignment. Finally, label’s size and position is set.
Next, let’s create a button:
btn1 = lv_btn_create(screenMain, NULL);
lv_obj_set_event_cb(btn1, event_handler_btn);
lv_obj_set_width(btn1, 70);
lv_obj_set_height(btn1, 32);
lv_obj_set_pos(btn1, 32, 100);
Again, it is child of the “screenMain
“. There you can see some code lines which set button width, height and position. Also, there is a line which sets the event handling function. In this case it is called “event_handler_btn
“. Up until now we haven’t written that function, but we will do it later.
Then, goes a label for the first button:
lv_obj_t * label1 = lv_label_create(btn1, NULL);
lv_label_set_text(label1, "Hello");
Now, the label is a child of “btn1
” and not the whole screen. So, that means, if you move the button, the label also moves. Because of this reason, you don’t need to set label’s position.
Also, you can see that this label is declared and initialized inside the setup()
function. So, it will not be available outside the setup()
function, but it is not a problem, because we need it only one time.
After that, we again create second button with a label.
Finally, function
lv_scr_load(screenMain);
loads the main screen.
So, if you upload current code to the ESP you should be able to see a screen like this:
The loop() and event handler
But let’s not stop here because now the touch is still not working.
Modify the loop()
function to look like this:
void loop() {
lv_task_handler();
delay(1);
}
Now we will have periodic update LVGL routines running.
Next, remember function “event_handler_btn
” which we haven’t written till now? Now it is time to write it:
static void event_handler_btn(lv_obj_t * obj, lv_event_t event){
if(event == LV_EVENT_CLICKED) {
if (obj == btn1)
lv_label_set_text(label, "Hello");
else if (obj == btn2){
lv_label_set_text(label, "Goodbye");
}
}
}
This function checks whether the button was pressed. If it was, it checks which button was pressed and accordingly writes different string to the label which is on the main screen.
Of course, it is possible to write it a bit differently. For example, you could have two different handling functions which are assigned to different buttons. In that case, you won’t need to check which button was pressed. But I have written in such way, so there is an example how you can find out which object calls the handling function (the line “obj == btn1
” does that).
Finally, upload the code to the ESP (or before that check in my GitHub if I haven’t left out any code) and you should see a working interface:
It looks the same, but now, if you press on the button, the label changes its text according to which button was pressed.
Summary
So, this is a working LVGL example in its simplest form which shows how to incorporate display/touch drivers into LVGL, how to configure LVGL, draw some button and labels on the screen and handle an event when a button is pressed.
This is a base how whole logic works – there are some objects (which can be pressed) on the screen, when you press them you rise an event (run a function) which does something that the code is written to do. This is the basic principle for simple to complex interfaces.
If you want to have example for a complex interface you can find it from my DIY generator’s project (although, the code is not very cleanly written).
Component styles
Next important topic is object styling. LVGL library uses CSS styling idea. So, it means that you need to create a style object, set its options like text, border, line, shadow sizes, colors, etc. After having the style object created you should apply it to one or more objects (labels, buttons, switches, etc.).
Let’s continue previous example and crate a styling element as follows:
static lv_style_t style1;
lv_style_init(&style1);
lv_style_set_text_color(&style1, LV_STATE_DEFAULT, LV_COLOR_GREEN);
Here we create a style object named “style1
“. Then we initialize it and afterwards add (set) one styling option to it – the green text color. Of course, we also need to apply the style to the needed object. Note, that this color is applied to a LV_STATE_DEFAULT
. This state is the default state when the object is created (for example, a button which is not pressed or interacted in any way). The style might be applied to other states, like LV_STATE_DISABLED
, LV_STATE_EDITED
, LV_STATE_FOCUSED
, LV_STATE_HOVERED
or LV_STATE_PRESSED
.
Now, let’s style the label element:
lv_obj_add_style(label, LV_LABEL_PART_MAIN, &style1);
Here LV_LABEL_PART_MAIN
means that the styling is applied to whole label (the “main” part of the label). Although the label itself does not have other parts which can be styled, other object can have those. For example, if you have a table, you could apply only to its cell (LV_TABLE_PART_CELL1
) or if you have a gauge you could apply the style on to its needle (LV_GAUGE_PART_NEEDLE
) and so on.
After styling the label, the screen interface should look like this:
Let’s add some more styling to it:
static lv_style_t style1;
lv_style_init(&style1);
lv_style_set_text_color(&style1, LV_STATE_DEFAULT, LV_COLOR_GREEN);
lv_style_set_border_color(&style1, LV_STATE_DEFAULT, LV_COLOR_RED);
lv_style_set_border_width(&style1, LV_STATE_DEFAULT, 2);
lv_style_set_bg_color(&style1, LV_STATE_DEFAULT, LV_COLOR_YELLOW);
lv_style_set_bg_opa(&style1, LV_STATE_DEFAULT, 255);
lv_style_set_radius(&style1, LV_STATE_DEFAULT, 1);
lv_obj_add_style(label, LV_LABEL_PART_MAIN, &style1);
We add a border color (red) and set its width to 2px, set background color to yellow and maximum opacity. Finally, we change the border radius to 1. The result should look like it is shown in the image below:
As I have already mentioned, the same style can be applied to several object, so let’s style the button on the left by adding one line:
lv_obj_add_style(btn1, LV_LABEL_PART_MAIN, &style1);
Now you will see that the button changes it appearance:
Styling second button
Finally, we could create another style and apply it to the second button:
static lv_style_t style2;
lv_style_init(&style2);
lv_style_set_text_color(&style2, LV_STATE_DEFAULT, LV_COLOR_GRAY);
lv_style_set_border_color(&style2, LV_STATE_DEFAULT, lv_color_hex(0x222222));
lv_style_set_border_width(&style2, LV_STATE_DEFAULT, 1);
lv_style_set_bg_color(&style2, LV_STATE_DEFAULT, lv_color_hex(0x222222));
lv_style_set_bg_opa(&style2, LV_STATE_DEFAULT, 255);
lv_style_set_radius(&style2, LV_STATE_DEFAULT, 4);
lv_style_set_shadow_spread(&style2, LV_STATE_DEFAULT, 1);
lv_style_set_shadow_color(&style2, LV_STATE_DEFAULT, LV_COLOR_GRAY);
lv_style_set_shadow_opa(&style2, LV_STATE_DEFAULT, 255);
lv_style_set_shadow_width(&style2, LV_STATE_DEFAULT, 1);
lv_obj_add_style(btn2, LV_LABEL_PART_MAIN, &style2);
You will find here that a lv_color_hex(0x222222)
function was used. This function just sets the color from its hex code. Also here are more styling functions – some additional shadow options. The result should be:
What if you want to change the font of the main label?
You could add some lines to your code, for example:
static lv_style_t bigStyle;
lv_style_init(&bigStyle);
lv_style_set_text_font(&bigStyle ,LV_STATE_DEFAULT, &lv_font_montserrat_36);
lv_obj_add_style(label, LV_LABEL_PART_MAIN, &bigStyle);
Here we create another style and apply it to the “label
“. But this peace of code will throw an error, because “lv_font_montserrat_36
” is not defined. LVGL has its definition, but by default it is not enabled. To enable it, you will have to go to LVGL lib’s “lv_conf.h
” file and find a line:
#define LV_FONT_MONTSERRAT_36 0 //Change to 1
Change 0 to 1 and save it. Then the code will work and you should be able to see the result:
Themes
Next topic to cover is themes. Initially LVGL has 4 themes. There is an empty, template, material and mono (black/white) themes. The theme is a collection of styles which are applied to some or most objects (buttons, backgrounds, etc.). You can change the theme in use by editing “lv_conf.h
” file. There you should be able to find a line:
#define LV_THEME_DEFAULT_INIT lv_theme_material_init
By default “material
” theme is used. Also, it has additional flags, like “dark” or “light” to change a colors scheme. If you want to change to another theme you should change the above definition. Possible values are: lv_theme_material_init
, lv_theme_empty_init
, lv_theme_template_init
, lv_theme_mono_init
. Moreover, below that line, there are several other important code lines:
#define LV_THEME_DEFAULT_COLOR_PRIMARY lv_color_hex(0x01a2b1)
#define LV_THEME_DEFAULT_COLOR_SECONDARY lv_color_hex(0x44d1b6)
#define LV_THEME_DEFAULT_FLAG LV_THEME_MATERIAL_FLAG_DARK
#define LV_THEME_DEFAULT_FONT_SMALL &lv_font_montserrat_14
#define LV_THEME_DEFAULT_FONT_NORMAL &lv_font_montserrat_14
#define LV_THEME_DEFAULT_FONT_SUBTITLE &lv_font_montserrat_14
#define LV_THEME_DEFAULT_FONT_TITLE &lv_font_montserrat_14
They should be easy to understand. These lines are used to set main theme colors, set flags, set fonts to be used.
For your custom theme it is suggested to use “template” theme. To change its style, you can open its file (“lv_theme_template.c
“) and modify or add additional styling code. Also, if you have (as written previously) created custom styles for some objects, it should not be a problem to understand, how theme is written.
Other screen objects
This tutorial contains only some useable objects from LVGL’s library. Besides already mentioned label and button there are lots of other available objects, or widgets, as it is called in LVGL’s documentation. So, there are checkboxes, switches, sliders, keyboards and keypads, charts, lists, text areas, tables and so on.
All of LVGL’s widgets will not be in this tutorial’s scope, but I just wanted to mention that there are such objects as widgets which are the main building blocks of your GUI.
More information you can find in the official documentation.
Also, you can find a real world example in my last project’s GitHub page (DIY Signal Generator). How that interface looks in reality you can find in the project’s final post.
Links
Tutorial project files at GitHub