LCD hierarchal menu class, comments/critiques?

[UPDATE]
Anyone wanting a menu class for a LCD should look at This Post.
This class supports infinitely nested menus, scrolling, and callbacks for each menu item.

I've made a hierarchal menu class for a 20x4 LiquidCrystal, and figured it might be useful to someone else. I'd also like to get some feedback as to whether I'm doing anything stupid or anything that should be fixed. It's not super flexible but it gets the job done.

#include <LiquidCrystal.h>
LiquidCrystal lcd(2,-1,3,4,5,6,7);

typedef boolean (*MYCALL)(void);

class menu
{
private:
int numchildren;   //Current number of children
menu * children[8];//Pointers to children
int inchild;       //This variable is not checked for < numchildren, so be careful when setting it
MYCALL callback;   //Function to call when this menu is clicked.
                   //Return true to go into the menu, false if the menu is just an item to perform a function

public:
char *name;

menu(char *n,MYCALL c)
{
name=n;
numchildren=0;
inchild=-1;
callback=c;
}


void addchild(menu &child)
{
if (numchildren<7)
 {
 children[numchildren++]=&child;
 }
else
 {
 serror("Too many children");
 }
}


void display()
{
Serial.print(name);
Serial.println(" Display");
if (inchild==-1)
 {
 lcd.clear();
 for (int i=0;i<numchildren;i++)
  {
  lcd.setCursor(((i%2)*10)+1,i/2);
  lcd.print(children[i]->name);
  //lcd.print("Test");
  }
 if (name!="Root")
  {
  lcd.setCursor(11,3);
  lcd.print("Back");
  }
 }
else
 {
 children[inchild]->display();
 }
}


void gochild(int thechild)
{
Serial.print(name);
Serial.println(" Gochild");
if (inchild==-1)
 {
 if (thechild<numchildren)
  {
  if (children[thechild]->callback())
   {
   inchild=thechild;
   display();
   }
  }
 else if (thechild==7&&name!="Root")
  {
  goupx();
  }
 }
else
 {
 children[inchild]->gochild(thechild);
 }
}


boolean goup()
{
Serial.print(name);
Serial.print(" Goup ");
if (inchild==-1)
 {
 Serial.println("1");
 return 1; //This one isn't in a menu and is ready to be left
 }
else if (children[inchild]->goup())//The child was ready to be left
 {
 Serial.println("0");
 inchild=-1;
 return 0;//Keep the next one from going up
 }
else //Shouldn't happen
 {
 serror("Error Going Up");
 }
}


};



void serror(char *error)
{
lcd.clear();
lcd.print("ERROR");
lcd.setCursor(0,1);
lcd.print(error);
}

boolean none()
{
Serial.println("none");
return 0;
}

boolean some()
{
Serial.println("some");
return 1;
}

menu Root("Root",none);

void goupx()
{
Root.goup();
Root.display();
}

menu Item1("Something",some);
menu Item11("Stuff",none);
menu Item12("More",some);
menu Item121("Deeper",none);
menu Item2("Other",none);
menu Item3("Etc",some);
menu Item31("So On",none);

void menuinit()
{
Root.addchild(Item1);
Root.addchild(Item2);
Root.addchild(Item3);
Item1.addchild(Item11);
Item1.addchild(Item12);
Item12.addchild(Item121);
Item3.addchild(Item31);
}

//UP,DOWN,LEFT,RIGHT,ENTER
int but[5]={8,9,10,11,12};
//Previous States of buttons
boolean pbut[5]={0,0,0,0,0};

int curloc;

boolean dread(int pin)
{
return digitalRead(pin);
}

void buttoncheck()
{
for (int i=0;i<5;i++)
 {
 if (dread(but[i]))
  {
  if (pbut[i]==0)
   {
   button(i);
   pbut[i]=1;
   }
  }
 else
  {
  pbut[i]=0;
  }
 }
}

void button(int which)
{
switch (which)
 {
 case 0://UP
  curloc-=2;
 break;
 case 1://DOWN
  curloc+=2;
 break;
 case 2://LEFT
  curloc--;
 break;
 case 3://RIGHT
  curloc++;
 break;
 case 4://ENTER
 Root.gochild(curloc);
 break;
 }
if (curloc<0){curloc+=8;}
curloc%=8;
}


void setup()
{
Serial.begin(9600);
Serial.println(sizeof(menu));
lcd.clear();
menuinit();
Root.display();
lcd.command(0x0F);
}
void loop()
{
lcd.setCursor((curloc%2)*10,curloc/2);
buttoncheck();
}

The dread() function is because I plan to put the buttons on a shift register eventually, so then I can just edit that function. The some and none functions are just placeholders, the point being that you could give an item a function that did something useful before it returned.

Eventually I plan to read config info in from an SD card and generate the menu items with malloc.

Clearly there's still debug stuff in there, I just wanted to get some feedback. :slight_smile:

I suggest...

  • Add "menu" as a parameter to the callback typedef boolean (*MYCALL)(menu&);. It may not seem useful now but at some point you'll be coding along and say to yourself, "Dang! That was a good idea!" :wink: Well, I hope so.
  • Instead of having an array of children, use a linked-list of children with the next pointers stored in the child instances. The code won't be very different, you can have any number of menu items, and you should save a bit of memory.

Good luck,
Brian

Cool!
This will help many, I'm sure of it.

Maybe turn this into a library for Arduino?
Arduino.cc Library Tutorial
Playground Library Tutorial
Arduino code usually use Uppercase letters on classes, in order to signal to the user that this is in fact, a class/library.

[edit]Not my intent to hijack this thread.[/edit]
Have you seen this: Arduino Playground - Menu Library - Menu Example :slight_smile:
It's more of a general menu thing, I've found it works best as an abstract infrastructure. So, one need to code what happens on changes and on use.

Maybe we could coordinate a playground page together, and maybe we could try to get our syntaxes more equal?

Good job CWAL :slight_smile:

Yeah, I have seen that. I wasn't too fond of the submenu type idea, I sort of wanted everything to just be one type.

Hijacking is totally fine, as long as it's somewhat related. :stuck_out_tongue: Always good to look around at other code.

Brian, thanks for the input. I've changed it to a linked list and it has made it much more flexible, in addition to more than cutting in half an instance of the object!

I can't seem to get the typedef boolean MYCALL(menu&) to work

error: typedef 'MYCALL' is initialized (use __typeof__ instead) In constructor 'menu::menu(char*, int)':
 At global scope:

I'm still having a little trouble though..

I changed my code such that in your main code, you should have a menu pointer, and everything will just return menu pointers that you can put into that. Makes it very flexible I think. I'm having trouble then referencing the menu that is currently pointed to, so I think I may be doing something wrong.

#include <LiquidCrystal.h>

LiquidCrystal lcd(2,-1,3,4,5,6,7);

typedef boolean (*MYCALL)(void);

class menu
{
private:
menu * parent;     //Pointer to parent menu

menu * child;      //Pointer to first child

menu * sibling;    //Pointer to next sibling

MYCALL callback;   //Function associated with menu item
                   
void setparent(menu &p)
{
parent=&p;
}

void addsibling(menu &s,menu &p)
{
if (sibling)
 {
 sibling->addsibling(s,p);
}
else
 {
 sibling=&s;
 sibling->setparent(p);
 }
}

menu * getsibling(int which)
{
if (which==0)
 {
 return this;
 }
else if (sibling)
 {
 return sibling->getsibling(which-1);
 }
else //Asking for a nonexistent sibling
 {
 serror("Sibling does not exist");
 return NULL;
 }
}

public:
char *name;

menu(char *n,MYCALL c)
{
name=n;
callback=c;
}

void addchild(menu &c)
{
if (child)
 {
 child->addsibling(c,*this);
 }
else
 {
 child=&c;
 child->setparent(*this);
 }
}

menu * getchild(int which)
{
if (child)
 {
 return child->getsibling(which);
 }
else //This menu item has no children
 {
 serror("Has no children");
 return NULL;
 }
}

menu * goup()
{
return parent;
}

};


void serror(char *error)
{
Serial.print(error);
/*
lcd.clear();
lcd.print("ERROR");
lcd.setCursor(0,1);
lcd.print(error);
*/
}

boolean none()
{
Serial.println("none");
return 0;
}

boolean some()
{
Serial.println("some");
return 1;
}


menu * Menu;

menu Root("Root",some);

menu Item1("Something",some);
menu Item11("Stuff",none);
menu Item12("More",some);
menu Item121("Deeper",none);
menu Item2("Other",none);
menu Item3("Etc",some);
menu Item31("So On",none);

void menuinit()
{
Root.addchild(Item1);
Root.addchild(Item2);
Root.addchild(Item3);
Item1.addchild(Item11);
Item1.addchild(Item12);
Item12.addchild(Item121);
Item3.addchild(Item31);

Menu=&Root;
}

//UP,DOWN,LEFT,RIGHT,ENTER
int but[5]={8,9,10,11,12};
//Previous States of buttons
boolean pbut[5]={0,0,0,0,0};

int curloc;

boolean dread(int pin)
{
return digitalRead(pin);
}

void buttoncheck()
{
for (int i=0;i<5;i++)
 {
 if (dread(but[i]))
  {
  if (pbut[i]==0)
   {
   button(i);
   pbut[i]=1;
   }
  }
 else
  {
  pbut[i]=0;
  }
 }
}


void button(int which)
{
switch (which)
 {
 case 0://UP
  curloc-=2;
 break;
 case 1://DOWN
  curloc+=2;
 break;
 case 2://LEFT
  curloc--;
 break;
 case 3://RIGHT
  curloc++;
 break;
 case 4://ENTER
 Menu=Menu->getchild(curloc);

 break;
 }
if (curloc<0){curloc+=8;}
curloc%=8;
}





void setup()
{
Serial.begin(9600);
Serial.println(sizeof(menu));
lcd.clear();
menuinit();
lcd.command(0x0F);
}
void loop()
{
lcd.setCursor((curloc%2)*10,curloc/2);
buttoncheck();
}

I would like a function (external to the class) display()
This function would I think be something like this:

void display()
{
void display()
{
menu * tmp;
int i=0;
lcd.clear();
while (tmp=Menu->getchild(i))
 {
 lcd.setCursor(((i%2)*10)+1,i/2);
 lcd.print(tmp->name);
 i++;
 }
}

This works fine for the first Root menu, but once you enter a menu the lcd displays garbage, so I think I'm having pointer trouble...

Actually, I was just being stupid in my button press function and not checking that the menu item exists.
I got the function type including menu&, and already have a good use for it, the ability for a callback function to back out to the menu above the one it was called from.
I wasn't able to do it with the typedef, I had to hardcode it into the class, but no big loss.

I am trying to create a function for when someone doesn't specify a callback, it will automatically pick a callback that just returns true.
I can't seem to get the type right and I'm not sure if this is even possible..

private:
boolean nothingspecial(menu &n)
{
return 1;
}

public:
char *name;
boolean (*canenter)(menu&); //Called when trying to enter item
                            //TRUE lets you in, FALSE keeps you out

menu(char *n)
{
name=n;
canenter=nothingspecial;
}

menu(char *n,boolean (*c)(menu&))
{
name=n;
canenter=c;
}

I've looked around and can't seem to figure out how to address this. It asked me to try &menu::nothingspecial which didn't work.. Every variation I've tried gives a type error related to the fact that the function is part of the class.

Currently I just set canenter=NULL and check it before calling, but I'd like to not have to do that.

I've made a library that will take my menu class and abstract it to an LCD display. It should support any display size and any number of items per row.

LCDMenu Library

It allows you to scroll through the menu if there are more items than your screen handles.

There is an example sketch in the zip.

Das sieht sehr gut aus, vor allem da auch mehrere Ebenen unterstützt werden.

Hi CWAL,

Hope you're still monitoring this thread.

I'm having trouble implementing the library in my project. Since I'm using a separate keypad I had to change some code that takes care of the button actions.
All seemed fine until I tried to scroll to any third item in de top-level or any submenu.
In all cases it keeps showing the menu items 1 and 2 on the lcd and the cursor disappears from the screen.
When the (invisible) 3rd item of a menu is selected it does bring you down to that menu, and even the invisible back button works.

The thing is that I'm not sure of current behavior is the result of my patching, or if it is also behaving like this in the original hardware setup. (I'm new to Arduino and C++ so analyzing your code is above my head right now).

FYI.
I'm using the standard LiquidCrystal library for output
and a separate keypad (with the i2ckeypad lib) for input.

Hope you can help.

Thanks,

Mario_H

Hi,
i update this lib:

http://nilsfeld.de/r/arduino/lib/LCDMenu2.rar

new:

  • cursor
  • cursor pos save, when going back
  • scroll bar
  • return selectet names (root.curfuncname)
  • return names of levels (root.funcname[0] - root.funcname[4])
  • better view

example:

  • in software: File->Examples->LCDMenu2->LCDMenu2

Have Fun !?!

Thanks for posting the Lib, Jomelo.

Before I give it a try: did you also have the scrolling problem as described in my post above?

I thing i fixed this problem, but i dont know :-|.
I removed the Software-Back-Button.
Now only the Hardware-BackBuotton works.

Ok.

Thanks.

I cant seem to get this to work. when I run it on my setup I only get a blinking cursor... I need to get a system online that uses this menu structure on a 16x2 4 or 8 bit LCD. I dont know what information I need to provide you with, but I have more info on this other post
http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1266688367

Your help is greatly appreciated

hmm,

i have`t done something sice august last year with arduino, i am have nothing ideas at the moment.