In this tutorial we will learn how to create menus (and scrolling menus) and navigate between the pages of an application using the TFT_eSPI library. This tutorial is primarily intended for ESP32 TTGO T-Watch watches from LilyGo. With a few small adaptations of the code, you can use it for other Arduino projects with a classic TFT screen.
The TFT_eSPI library is very easy to use for creating menus and interface elements.
It is entirely possible to use the TFT_eSPI and LVGL library together in an application project for T-Watch.
Activate the TFT_eSPI library
By default, the TFT_eSPI library is integrated and activated by the LilyGoWatch library. This is also the case regardless of the model of T-Watch you use. So there is nothing more to do.
At the programming level, it is easier to declare a pointer to the object at the start of the project. This way you won’t need to re-declare the class each time to access the functions of the TFT_eSPI library
TTGOClass *ttgo; // Pointer to the API of the watch devices
TFT_eSPI *tft; // Pointer to the functions of the TFT_eSPI library
In the setup, we create the ttgo and tft objects that we place in the pointers created at the start of the project
void setup(){
...
ttgo = TTGOClass::getWatch();
ttgo->begin();
tft = ttgo->tft;
...
}
Now you can access the screen management functions anywhere in the project.
How to port code from an Arduino project with the TFT_eSPI library?
It is quite possible to port code developed for an Arduino and adapt it to the T-Watch.
It will simply be necessary to replace all the calls to the functions which are made with the operator point (.) By the operator arrow (->).
Prepare the XBM icons (images)
First of all, it is necessary to convert the images to XBM format of each icon, for each resolution. Indeed, the TFT_eSPI library does not offer any function to resize the images.
You will find on the project’s GitHub repository several icons in PNG and XBM formats that you can use in your projects. They are available in several resolutions.
Icon | File name | 80×80 | 32×32 | 30×30 |
Menu | main | |||
WiFi | wifi | |||
Bluetooth | bluetooth | |||
Accelerometer | accel | |||
GPS | gps | |||
Settings | settings | |||
Clock | clock | |||
Music | music | |||
Tools | tools | |||
Exit | exit |
It is very easy to make your own monochrome icons by converting an image to XBM format. Read this tutorial which explains in detail how to do it.
Organization of the pages and menus of the T-Watch application
Obviously, we are not going to develop the entire application in this tutorial. We will focus on the menus and navigation between the pages (screens) of the application here.
- The top of the main page is occupied by a status bar. The left icon provides access to the scrolling menu to access a page of the application. The menu on the right is identical. Each option is preceded by a 30×30 pixel monochrome icon.
- In the center of the page, there is an icon that allows access to the full page menu
- Each page of the app simply displays the title in the status bar
Create a full page menu
The first menu that we are going to create is a menu that is displayed in full screen. It consists of 4 buttons. Here, to exit the menu, an icon is reserved for this use. We could also use a button or the accelerometer (shake to go back for example).
To create a full-page menu, all you have to do is draw the 4 buttons in their position using the drawXBitmap() method whose parameters are as follows:
- Origin x, y of the image
- Image point array in XBM format (const unsigned char)
- The width and height of the image
- Icon color
- Background color. In general, it is identical to the bottom of the screen. It is enough to invert the colors in the event of erroneous rendering.
We stay in a while loop until the exitMenu variable changes to False .
By testing the position of the finger on the screen returned by the getTouch() function , we can determine which button has been touched and return the corresponding index.
int toolPageMenu(){
tft->fillScreen(TFT_BLACK); // Fill background in black
tft->drawXBitmap(25, 25, wifi, icon_width, icon_height, TFT_WHITE, TFT_BLACK);
tft->drawXBitmap(130, 25, bluetooth, icon_width, icon_height, TFT_WHITE, TFT_BLACK);
tft->drawXBitmap(25, 130, gps, icon_width, icon_height, TFT_WHITE, TFT_BLACK);
tft->drawXBitmap(130, 130, menuexit, icon_width, icon_height, TFT_WHITE, TFT_BLACK);
int16_t x, y, tx, ty;
int mSelect = 0;
// Keep the TFT_eSPI page open until the user touches an option
boolean exitMenu = false;
while (!exitMenu) {
// Wait for the user to touch the screen
if (ttgo->getTouch(x, y)) {
while (ttgo->getTouch(tx, ty)) {}
Serial.printf("toolPageMenu clicked %u %u\n", x, y);
int index = 0;
if ( x < w / 2 && y < h / 2 ) { mSelect = 1; exitMenu = true; } else if ( x > w / 2 && y < h / 2 ) {
mSelect = 2;
exitMenu = true;
} else if ( x < w / 2 && y > h / 2 ) {
mSelect = 6;
exitMenu = true;
} else {
mSelect = 0;
exitMenu = true;
}
}
}
ttgo->tft->fillScreen(TFT_BLACK);
menuOpened = false;
// Returns the index of the desired page
return mSelect;
}
Here is what we get with this function
Scrolling menu with icon on the left (or not)
Unlike the LVGL library, the TFT_eSPI library only “allows” (it’s already a lot!) To draw simple geometric shapes. To create a vertical scrolling menu, the trick is to redraw the menu when you touch the top or bottom of the screen.
The displayScrollingMenu() function allows you to monitor user action on the screen. Here, the menu consists of three cells. The cell labels are retrieved using a pointer. As soon as the user touches a line of the menu, it is rebuilt by modifying the position of the mSelect cursor using the createMenuList() or createMenuListIcon() methods .
int displayScrollingMenu(){
int16_t x, y, tx, ty;
int mSelect = menuSelected; // The currently highlighted app
boolean exitMenu = false; // used to stay in the menu until user selects app
refreshMenu(menuSelected); // display vertical scrolling menu
while (!exitMenu) {
if (ttgo->getTouch(x, y)) { // If you have touched something...
while (ttgo->getTouch(tx, ty)) {} // wait until you stop touching
//Serial.printf("Menu y %u \n", y);
//Serial.printf("Height %u \n", tft->height());
// The user moves to the bottom of the menu
if (y >= 160 ) {
mSelect += 1;
if (mSelect == appMax) mSelect = 0;
refreshMenu(mSelect);
}
// User moves to the top of the menu
if (y <= 80 ) {
mSelect -= 1;
if (mSelect < 0) mSelect = appMax - 1; refreshMenu(mSelect); } // The user has selected the central option, we exit the menu and open the corresponding page if (y > 80 && y <160 ) { exitMenu = true; } } delay(5); } //Return with mSelect containing the desired mode ttgo->tft->fillScreen(TFT_BLACK);
menuOpened = false;
return mSelect;
}
The createMenuListIcon() method creates a vertical scrolling menu. Each option is preceded by the application icon. The icon is nothing more than an XBM image drawn with the drawXBitmap() method as before.
void createMenuListIcon(int mSel) {
menuOpened = true;
int curSel = 0;
// Create blue background with a black line in the middle of the screen
ttgo->tft->fillScreen(TFT_BLUE);
ttgo->tft->fillRect(0, 80, 240, 80, TFT_BLACK);
// First line of the scrolling menu
if (mSel == 0) curSel = appMax - 1;
else curSel = mSel - 1;
ttgo->tft->setTextSize(2);
ttgo->tft->setTextColor(TFT_WHITE);
ttgo->tft->drawXBitmap(10, 20, getMenuIcon(curSel), 32, 32, TFT_WHITE, TFT_BLUE);
ttgo->tft->setCursor(50, 30);
ttgo->tft->println(appName[curSel]);
// Central line (selected option)
ttgo->tft->setTextSize(3);
ttgo->tft->setTextColor(TFT_RED);
ttgo->tft->drawXBitmap(10, 100, getMenuIcon(mSel), 32, 32, TFT_WHITE, TFT_BLACK);
ttgo->tft->setCursor(50, 110); // 40
ttgo->tft->println(appName[mSel]);
// Last line
if (mSel == appMax - 1) curSel = 0;
else curSel = mSel + 1;
ttgo->tft->setTextSize(2);
ttgo->tft->setTextColor(TFT_WHITE);
ttgo->tft->drawXBitmap(10, 180, getMenuIcon(curSel), 32, 32, TFT_WHITE, TFT_BLUE);
ttgo->tft->setCursor(50, 190);
ttgo->tft->print(appName[curSel]);
}
How to navigate between pages, pass and return parameters?
Here too, the TFT_eSPI library does not offer any advanced solution for managing the pages of the ESP32 application. In fact when we go from one screen to another on the watch, we only rebuild the screen from scratch each time.
Each page (screen) is a simple C++ function. When you want to pass parameters to a screen, you just need to declare variables or use global variables (beware of bugs!).
For example here, we pass the index of the displayed page which will allow its title to be retrieved. To create the new screen, we start by erasing it with the desired background color (here in black).
Read this article which details the basic elements of the TFT_eSPI library
void createPage(int pageindex){
tft->fillScreen(TFT_BLACK); // Fill the screen background in black
tft->setTextDatum(TL_DATUM);
tft->setTextColor(TFT_WHITE);
tft->setTextSize(2);
tft->drawString(appName[pageindex], menuSize + 2 * menuPadding, menuPadding, 1);
tft->drawFastHLine(0, menuSize + menuPadding, w, TFT_DARKGREY);
createLeftMenuButton();
createRightMenuButton();
}
Since a screen is a simple function, we can simply return parameters to the return command. This is what we do for example to select a menu option.
int displayScrollingMenu(){
...
return mSelect;
}
Complete project code
The full code for the project is available on GitHub only.
The source code has been tested on the T-Watch Touch 2019
As well as on the T-Watch 2020
Demonstration
To conclude this article, here is a small demonstration video. Even if the creation of the menus is a bit DIY with TFT_eSPI, the rendering is efficient and quick to do.
Updates
2020/11/25 Publication of the article
- T-Watch. Simplified code for shutdown and wake-up with BMA423 or AXP202 of the ESP32
- T-Watch. Sleep and wake-up ESP32 with BMA423 accelerometer or AXP202 button
- T-Watch. Menu, pages, navigation between screens with TFT_eSPI
- T-Watch. Draw Mandelbrot or Julia fractals with an ESP32 and LVGL + TFT_eSPI
- T-Watch. Display XBM (TFT_eSPI) and C++ (LVGL) images. ESP32, Arduino compatible