Add a menu to your Nextion Project

Image by Freepik

Introduction

At the end of the last blog article where we saw together how one could (almost) automatically add an animated status bar to each page of a Nextion HMI project, I promised you that it would still evolve by adding a menu to it. When I started developing it, I discovered so many amazing things, and I found ways to optimize several aspects, that it took much more time than foreseen, but finally, here we are!

If you didn’t already, you really should read the previous article and have a look onto its demo project code, to fully benefit from today’s extensions.

Modifying and optimizing the status bar

Remember, we had a global variable tb_h, which served as a kind of raster for the status bar rendering. Alas, it required a second variable, tb_dx, to handle the horizontal positioning of the elements and it even didn’t allow fully automatic scaling to adapt to the screen width. Thus, I decided to eliminate this second variable (saves 4 bytes of global memory!) and to make the status bar layout more flexible.

I decided to divide the status bar width up as follows:
– 25% or w/4 for the first status item (section a)
– 25% or w/4 for the second status item (section b)
– 50% or w/2 for the date/time field (section c)

The respective  status item sections (a and b) would further be divided up into two parts: first, a square of the size tb_h x tb_h containing the corresponding icon or pictogram (x-centered), followed by the content text with a width of (w/4 – tb_h), left aligned.

The bigger date/time section (c) would draw the date/time string first, right aligned with a width of (w/2-thb), followed by a tb_h x tb_h square containing the menu “hamburger”.

Thus, the code to draw the sections a and b plus the menu hamburger in each page’s postInitialize event has been simplified to:

tb_h=b[0].w/16 // initialize the status bar raster
// select the divider depending on the display size to give
// an integer result between 20 and 30. Adapt the h and w
// of hotspot m0 and the font sizes accordingly everywhere.
fill 0,0,b[0].w,tb_h,tb_bco // draw the tb background with page width
xstr 0,0,tb_h,tb_h,0,tb_pco,tb_bco,1,1,1,"⬌"
covx baud,tb_disp.txt,0,0
xstr tb_h,0,b[0].w/4-tb_h,tb_h,0,tb_pco,tb_bco,0,1,1,tb_disp.txt
xstr b[0].w/4,0,tb_h,tb_h,0,tb_pco,tb_bco,1,1,1,"☀"
covx dim,tb_disp.txt,0,0
tb_disp.txt+="%"
xstr b[0].w/4+tb_h,0,b[0].w/4-tb_h,tb_h,0,tb_pco,tb_bco,0,1,1,tb_disp.txt
// draw the menu hamburger in the rightmost corner
xstr b[0].w-tb_h,0,tb_h,tb_h,0,tb_pco,tb_bco,1,1,1,"☰"

And the code to update date and time in tb_timer‘s event code looks now like that:

// Assemble the date-time string
covx rtc0,tb_disp.txt,4,0 // year
covx rtc1,tb_tmp.txt,2,0 // month
tb_disp.txt+="-"
tb_disp.txt+=tb_tmp.txt
covx rtc2,tb_tmp.txt,2,0 // day
tb_disp.txt+="-"
tb_disp.txt+=tb_tmp.txt
covx rtc3,tb_tmp.txt,2,0 // hour
tb_disp.txt+=" "
tb_disp.txt+=tb_tmp.txt
covx rtc4,tb_tmp.txt,2,0 // minute
tb_disp.txt+=":"
tb_disp.txt+=tb_tmp.txt
// Update the display
xstr b[0].w/2,0,b[0].w/2-tb_h,tb_h,0,tb_pco,tb_bco,2,1,1,tb_disp.txt
// Take off the startup second interval
if(tb_timer.tim!=60000)
{
  tb_timer.tim=60000
}

Finally, a little less code, a little less memory consumption, improved performance and better scaling! But when looking at the result, I was not satisfied by the optic and esthetic result. In ever case, I had to to touch at the status bar font to add the menu “hamburger”, and that was the moment where I made the amazing discovery: There is no need to restart from scratch! No, you can…

Rework a font

Sometimes, you generate a font for your HMI project, and only afterwards, you discover that you did select the wrong code page, you forgot to add custom characters, that it is a little too small or too big, or that, finally, you want to select a different font. No need to restart from scratch, simply rework it! How to do that ?

Step 1: Double click on the font which you want to edit in the editor’s font pane

This will open the font preview window

Did you ever notice the “Change Font” button in the bottom left corner? After many years, I finally discovered it! It allows to open the well known font creator window, but this time, everything you did last time is already pre-filled and you can easily change the size, the font type, the range, etc… I used it to change the font into Microsoft Sans Serif with a height of 18px and I added the menu hamburger and the closing cross to the specified character pane.

A click onto “Generate font” and confirming everything allowed me to replace the font in the project in a very convenient way!

Invoking the menu

All we have to do is to place a little hotspot over the menu hamburger’s position on our start page which will serve later as a template to generate the other pages of the project through duplication. The menu is thought to consume the least amount of resources as possible, as we will see later. The hotspot will just have two lines of code, the first lets the menu page know by setting its global variable menu.call_page which page was calling it so that we can return to it if needed, and then open the menu page:

menu.call_page.val=dp
page menu

That’s all. The other magic happens in…

The menu page

The menu page is, similar to the system keyboard pages, designed to look like a transparent overlay above the calling page. This is achieved through setting the page’s .sta attribute to “No background”. This will leave the graphics buffer with the calling page although it is unloaded when the menu page is loaded. The menu page has no visible components, only a few invisible variable components, so that everything is self-contained and it can easily be exported and be added to another project. Thus, in the editor, it looks pretty empty, and that is intended! The only global variable dependency is again tb_h, the menu page will use it to determine the y coordinate of the first menu item below the status bar, and to set the height of all menu items accordingly.

The demo project has 6 pages to show the full functionality:

Page 0 as the start page serves at the same time as master template page, page1, page2, page3, and page4 have later been generated through copying page0 after all the status bar and menu calling functionality was implemented. The page specific elements have been added afterwards to make each page individual. The idea behind the demo project is thus to have 5 pages which have fantasy functions like Start page (page0), North page (page1), East page (page2), South page (page3), and West page (page4). You are naturally free to rename and modify this to tase and according your project’s needs.

To let the menu page know what it shall display, it has a text variable called cfg_menu which holds the menu configuration in a csv like style as multiline text: Each line represents 1 menu item and holds the id of the page to be called, followed by a semicolon, and by the display text which can thus be totally different from the page’s name, if you wish it.

All other variables on the menu page are internal, you may just adapt the first line of the page preInitialize event to set the menu width in the variable menu_w. This is then used to calculate the x coordinate menu_x of the menu items from the right border. The y coordinate menu_y, as written above, is calculated with the help of the global tb_h variable to start one pixel below the status bar:

// initialize draw variables
menu_w.val=187
menu_x.val=b[0].w-menu_w.val-1
menu_y.val=tb_h+1

The rendering of the menu happens then in the page PostInitialize event code. It loops through the menu configuration variable cfg_menu and extracts one line after the other with the spstr function into the variable item_buf. Once again, spstr and covx are used to split the item into the page id which goes into item_index and the text which goes, you guess it, into item_text. The strlen function helps to detect if there is an item to display, and if this is the case, the display string is assembled as follows: If the current menu item corresponds to the calling page, the text is preceded by a ” < “. All other items are preceded by ” > ” to show that a page change will happen. Then, the menu item is drawn with the xstr function at the menu_x and menu_y coordinates, menu_w wide and tb_h high. After that, menu_y is incremented by tb_h and by 1 to compute the top coordinate of the next item:

// draw the menu close in the rightmost corner
xstr b[0].w-tb_h,0,tb_h,tb_h,0,tb_pco,tb_bco,1,1,1,"✕"
// list a maximum of 8 menu items
for(tmp.val=0;tmp.val<8;tmp.val++) 
{ 
  // get the menu item 
  spstr cfg_menu.txt,item_buf.txt,"\r",tmp.val 
  // extract the index 
  spstr item_buf.txt,item_text.txt,";",0 
  covx item_text.txt,item_index.val,0,0 
  // extract the text 
  spstr item_buf.txt,item_text.txt,";",1 
  // check the length of the text entry 
  strlen item_text.txt,tmp2.val 
  // show the entry only if existing 
  if(tmp2.val>0)
  {
    if(item_index.val==call_page.val)
    {
      item_buf.txt=" < "+item_text.txt 
    }else 
    { 
      item_buf.txt=" > "+item_text.txt
    }
    xstr menu_x.val,menu_y.val,menu_w.val,tb_h,1,tb_pco,tb_bco,0,1,1,item_buf.txt
    menu_y.val+=tb_h
    menu_y.val++
  }
}

Basically, and especially if you filter out all the explaining comment lines, it’s very short and compact code! The result looks like this, if the menu is invoked from page0, the start page:

And that’s how it looks when invoked from page3, the south page:

Same menu page, same code, just the ” < ” and ” > ” before the menu text have been magically adapted!

We are now almost done! Now, we need still to detect which menu item is touched and to open the corresponding page. But how can this be done without any visual components and the corresponding event code containers? Yes, we write our own universal…

The menu touch event handler

Just as a reminder, if there are no visible components, it’s the page itself which gets all the touchPress and touchRelease events. Thus, we’ll put our code into the page’s touchPress event. We know that our menu’s left is at menu_x. The top is just below tb_h, and the bottom is in the variable menu_y after drawing all items. Thus, we have first to check if our click was within that rectangle. If it is elsewhere, be it on the closing cross above, or somewhere in the empty space, we go back to the calling page. The detection is made through the system variables tch0 which holds the x coordinate of the touch, and tch1 which holds the y coordinate.

Thus, if tch0menu_x AND tch1tb_h AND tch1menu_y, a menu item has been touched. Now, to detect which item it was, we use the fact that the menu top is at tb_h, so we subtract this from tch1 and save the result in tmp. Then, we know that each menu item is tb_h + 1, thus we divide tmp by this to get the index of the touched menu item. Now, we use shorter variant of the spstr and covx engine as before when we draw the menu items, but this time just to get the page id to which we’ll have to jump. Then, with a simple page command, we are done.

Let’s translate this description directly into the menu page touchPress event code :

// check if the click was within the visible menu area
if(tch0>menu_x.val&&tch1>tb_h&&tch1<menu_y.val)
{
  // compute the index of the clicked menu item
  tmp.val=tch1-tb_h
  tmp2.val=tb_h+1
  item_index.val=tmp.val/tmp2.val
  // get the corresponding menu item
  spstr cfg_menu.txt,item_buf.txt,"\r",item_index.val
  // extract the page index
  spstr item_buf.txt,item_text.txt,";",0
  covx item_text.txt,tmp.val,0,0
  // go to the page
  page tmp.val
}else
{
  // return to calling page
  page menu.call_page.val
}

Again, simple, compact, short, and efficient!

Now, I let you download the demo project’s hmi file to study it, to play with it, to modify and to re-use it in your own projects: add_menu_t.HMI

I can already hear your voices: “And what if I want an individual menu for each page ?” and I have already the answer! You’ll see that in my next article. Stay tuned!

Thank you for reading and happy Nextioning!

Comments, critics, suggestions? Just send me an email to thierry (at) itead (dot) cc! 🙂