Создание многоуровневого графического меню

Туцик
Туцик аватар
Offline
Зарегистрирован: 31.03.2020

Помогите допилить многоуровневое меню. Само меню работает, но с под меню не могу разобраться, как его реализовать. Код прилагаю

#include <SPI.h>                           //  библиотека для работы SPI (дисплей)
#include <TFT_eSPI.h>                      //  библиотека дисплея
#include "GyverEncoder.h"                  //  библиотека энкодера


#import "menu1.h"                          //  иконка 1 пункта меню
#import "menu2.h"                          //  иконка 2 пункта меню
#import "menu3.h"                          //  иконка 3 пункта меню
#import "menu4.h"                          //  иконка 4 пункта меню
#import "menu5.h"                          //  иконка 5 пункта меню
#import "menu6.h"                          //  иконка 6 пункта меню

#import "menu11.h"                          //  иконка подменю 1 1
#import "menu12.h"                          //  иконка подменю 1 2


// ---------------------------------------- МОДУЛЬ ДИСПЛЕЯ -----------------------------------------------------------

#define lightOFF 5                                    // пин подсветки дисплея
uint16_t selectcolor = 0xFEC0;                        // цвет выделения пункта меню

TFT_eSPI tft = TFT_eSPI();                            // создаем объект tft
TFT_eSprite timespriteON = TFT_eSprite(&tft);         // создаем спрайт для обновления настройки времени включения освещения
TFT_eSprite timespriteOFF = TFT_eSprite(&tft);        // создаем спрайт для обновления настройки времени включения освещения
TFT_eSprite luxspriteON = TFT_eSprite(&tft);          // создаем спрайт для обновления настройки включения освещения по датчику освещенности
TFT_eSprite luxspriteOFF = TFT_eSprite(&tft);         // создаем спрайт для обновления настройки выключения освещения по датчику освещенности
TFT_eSprite waterspriteONm = TFT_eSprite(&tft);
TFT_eSprite waterspriteONHUMa = TFT_eSprite(&tft);
TFT_eSprite waterspriteONa = TFT_eSprite(&tft);


// ---------------------------------------- МОДУЛЬ ЭНКОДЕРА -----------------------------------------------------------

#define CLK 14                                        // пины энкодера 
#define DT  27                                        // пины энкодера 
#define SW  26                                        // пины энкодера

Encoder enc1(CLK, DT, SW);                            // настраиваем пины энкодера

byte menu =1;                                         // обозначаем переменную пунктов меню

byte menuu =1;

//***************************************НАЧАЛО ПРОГРАММЫ***************************************************************

void setup () {
  enc1.setType(TYPE2);                       // устанавливаем тип энкодера
  enc1.setTickMode(AUTO);                    // устанавливаем режим энкодера AUTO
  
  tft.init();                                // запускаем дисплей
 
  
  tft.setRotation(1);                        // поворачиваем изображение на дисплее
  tft.fillScreen(TFT_BLACK);                 // закрашиваем дисплей в черный цвет
  tft.setSwapBytes(true);                    
   updateMenu();                             // выполняем меню
  
}
//-------------------------------------ЦИКЛИЧЕСКАЯ ЧАСТЬ ПРОГРАММЫ-----------------------------------------------------------
void loop() {

    
  if (enc1.isRight()) {                      // если энкодер вправо 
    menu++;                                  // то выбираем следующий пункт меню ++
    updateMenu();                            // отрисовываем меню заново
    while (enc1.isRight());
  }
  if (enc1.isLeft()) {                       // если энкодер влево
    menu--;                                  // то выбираем предыдущий пункт меню --
    updateMenu();                            // отрисовываем меню заново
    while (enc1.isLeft());
  }
  if (enc1.isPress()) {                      // при нажатии на энкодер включаем дисплей
  
     executeAction();                       // переходим в блок выполнения
         
    updateMenu();
      while (enc1.isPress());
  }
   
 }
 //-------------------------------------------ФУНКЦИЯ ВЫДЕЛЕНИЯ ЭЛЕМЕНТА МЕНЮ--------------------------------------------------

 void select (uint16_t x0, uint16_t y0, uint16_t w, uint16_t h, uint16_t selectcolor) {                                         
    tft.drawRect(x0, y0, w, h, selectcolor);                // делаем жирный квадрат из 5 квадратов вложенных друг в друга
    tft.drawRect(x0+1, y0+1, w-2, h-2, selectcolor);        // делаем жирный квадрат из 5 квадратов вложенных друг в друга
    tft.drawRect(x0+2, y0+2, w-4, h-4, selectcolor);        // делаем жирный квадрат из 5 квадратов вложенных друг в друга
    tft.drawRect(x0+3, y0+3, w-6, h-6, selectcolor);        // делаем жирный квадрат из 5 квадратов вложенных друг в друга
    tft.drawRect(x0+4, y0+4, w-8, h-8, selectcolor);        // делаем жирный квадрат из 5 квадратов вложенных друг в друга
  }

//------------------------------------------------ ФУНКЦИЯ ОТРИСОВКИ МЕНЮ --------------------------------------------------
void updateMenu(){            

switch (menu) {
   case 0:
     menu = 1;
   break;    
case 1:
  tft.pushImage(1, 8, 50, 52, menu1);       // 1й пункт меню
  tft.pushImage(56 ,8, 50, 52, menu2);      // 2й пункт меню
  tft.pushImage(110 ,8, 50, 52, menu3);     // 3й пункт меню
  tft.pushImage(1 ,68, 50, 52, menu4);      // 4й пункт меню
  tft.pushImage(56 ,68, 50, 52, menu5);     // 5й пункт меню
  tft.pushImage(110 ,68, 50, 52, menu6);    // 6й пункт меню 
 select(1, 8, 50,52,  selectcolor);         // выделяем 1 пунет меню
  break;   
case 2:
  tft.pushImage(1, 8, 50, 52, menu1);       // 1й пункт меню
  tft.pushImage(56 ,8, 50, 52, menu2);      // 2й пункт меню
  tft.pushImage(110 ,8, 50, 52, menu3);     // 3й пункт меню
  tft.pushImage(1 ,68, 50, 52, menu4);      // 4й пункт меню
  tft.pushImage(56 ,68, 50, 52, menu5);     // 5й пункт меню
  tft.pushImage(110 ,68, 50, 52, menu6);    // 6й пункт меню  
 select(56, 8, 50,52,  selectcolor);        // выделяем 2 пунет меню
  break;   
case 3: 
  tft.pushImage(1, 8, 50, 52, menu1);       // 1й пункт меню
  tft.pushImage(56 ,8, 50, 52, menu2);      // 2й пункт меню
  tft.pushImage(110 ,8, 50, 52, menu3);     // 3й пункт меню
  tft.pushImage(1 ,68, 50, 52, menu4);      // 4й пункт меню
  tft.pushImage(56 ,68, 50, 52, menu5);     // 5й пункт меню
  tft.pushImage(110 ,68, 50, 52, menu6);    // 6й пункт меню 
 select(110, 8, 50,52,  selectcolor);       // выделяем 3 пунет меню   
    break;
case 4:
  tft.pushImage(1, 8, 50, 52, menu1);       // 1й пункт меню
  tft.pushImage(56 ,8, 50, 52, menu2);      // 2й пункт меню
  tft.pushImage(110 ,8, 50, 52, menu3);     // 3й пункт меню
  tft.pushImage(1 ,68, 50, 52, menu4);      // 4й пункт меню
  tft.pushImage(56 ,68, 50, 52, menu5);     // 5й пункт меню
  tft.pushImage(110 ,68, 50, 52, menu6);    // 6й пункт меню 
 select(1, 68, 50,52,  selectcolor);         // выделяем 4 пунет меню 
    break;  
case 5:
  tft.pushImage(1, 8, 50, 52, menu1);       // 1й пункт меню
  tft.pushImage(56 ,8, 50, 52, menu2);      // 2й пункт меню
  tft.pushImage(110 ,8, 50, 52, menu3);     // 3й пункт меню
  tft.pushImage(1 ,68, 50, 52, menu4);      // 4й пункт меню
  tft.pushImage(56 ,68, 50, 52, menu5);     // 5й пункт меню
  tft.pushImage(110 ,68, 50, 52, menu6);    // 6й пункт меню 
 select(56, 68, 50,52,  selectcolor);       // выделяем 5 пунет меню
   break;
case 6:
  tft.pushImage(1, 8, 50, 52, menu1);       // 1й пункт меню
  tft.pushImage(56 ,8, 50, 52, menu2);      // 2й пункт меню
  tft.pushImage(110 ,8, 50, 52, menu3);     // 3й пункт меню
  tft.pushImage(1 ,68, 50, 52, menu4);      // 4й пункт меню
  tft.pushImage(56 ,68, 50, 52, menu5);     // 5й пункт меню
  tft.pushImage(110 ,68, 50, 52, menu6);    // 6й пункт меню 
 select(110, 68, 50,52,  selectcolor);       // выделяем 6 пунет меню
   break;   
case 7:
  menu = 6;
   break;
  }
}


//----------------------------------ПОДМЕНЮ 1--------------------------------------------------------

void submenu1(){            
  tft.fillScreen(TFT_BLACK); 
switch (menuu) {
   case 0:
     menuu = 1;
   break;    
case 1:
  tft.pushImage(1, 8, 50, 52, menu11);       // 1й пункт подменю
  tft.pushImage(56 ,8, 50, 52, menu12);      // 2й пункт подменю
  tft.pushImage(110 ,8, 50, 52, menu3);     // 3й пункт подменю
 select(1, 8, 50,52,  selectcolor);         // выделяем 1 пунет подменю
  break;   
case 2:
  tft.pushImage(1, 8, 50, 52, menu11);       // 1й пункт подменю
  tft.pushImage(56 ,8, 50, 52, menu12);      // 2й пункт подменю
  tft.pushImage(110 ,8, 50, 52, menu3);     // 3й пункт подменю
 select(56, 8, 50,52,  selectcolor);        // выделяем 2 пунет подменю
  break;   
case 3: 
  tft.pushImage(1, 8, 50, 52, menu11);       // 1й пункт подменю
  tft.pushImage(56 ,8, 50, 52, menu12);      // 2й пункт подменю
  tft.pushImage(110 ,8, 50, 52, menu3);     // 3й пункт подменю
 select(110, 8, 50,52,  selectcolor);       // выделяем 3 пунет подменю   
 break;
 case 4:
   menuu = 3;
   break;

  
 if (enc1.isRight()) {                      // если энкодер вправо 
    menuu++;                                  // то выбираем следующий пункт подменю ++
    submenu1();                            // отрисовываем меню заново
    while (enc1.isRight());
  }
  if (enc1.isLeft()) {                       // если энкодер влево
    menuu--;                                  // то выбираем предыдущий пункт подменю --
    submenu1();                            // отрисовываем меню заново
    while (enc1.isLeft());
  }
if (enc1.isPress()) {                      // при нажатии на энкодер включаем дисплей
    //   executeAction();                       // переходим в блок выполнения
         tft.fillScreen(TFT_BLACK);
        tft.drawString("Execute sub 1 1",5,5);
         delay(5000);    
 submenu1();
     while (enc1.isPress());
}  
  }
 }
//*****************************БЛОК ВЫПОЛНЕНИЯ*********************************************************
void executeAction() {                         // фунция выполнения пунктов меню
  switch (menu) { 
    case 1: 
      submenu1();                               // переход в подменю 1 1
      break;
    case 2:
      action2();                               // выполнение 2го пункта меню
      break;
    case 3:
      action3();                               // выполнение 3го пункта меню
      break;
    case 4:
      action4();                               // выполнение 4го пункта меню
      break;
    case 5:
      action5();                               // выполнение 5го пункта меню
      break;
    case 6:
      action6();                               // выполнение 6го пункта меню
      break;    
  }
}

// ********************************** БЛОКИ ИСПОЛНЕНИЯ ПУНКТОВ МЕНЮ ******************************************************
// ------------------------------------------ 1й Пункт меню --------------------------------------------------------------
void action1() {
 tft.fillScreen(TFT_BLACK);
  tft.setCursor (10, 10);
  tft.print(">Executing #1");
  digitalWrite(13, HIGH);
  delay(2000);
  digitalWrite(13, LOW);
  tft.fillScreen(TFT_BLACK);
}
// ------------------------------------------ 2й Пункт меню --------------------------------------------------------------
void action2() {
 tft.fillScreen(TFT_BLACK);
  tft.setCursor (10, 10);
  tft.print(">Executing #2");
  digitalWrite(13, HIGH);
  delay(2000);
  digitalWrite(13, LOW);
  tft.fillScreen(TFT_BLACK);
}
//------------------------------------------------------------------------------------------------------------------------------------
void action3() {      
  tft.fillScreen(TFT_BLACK);
  tft.setCursor (10, 10);
  tft.print(">Executing #3");
  digitalWrite(13, HIGH);
  delay(2000);
  digitalWrite(13, LOW);
  tft.fillScreen(TFT_BLACK);
}
void action4() {
  tft.fillScreen(TFT_BLACK);
  tft.setCursor (10, 10);
  tft.print(">Executing #4");
  delay(2000);
  tft.fillScreen(TFT_BLACK);
}
void action5() {
  tft.fillScreen(TFT_BLACK);
  tft.setCursor (10, 10);
  tft.print(">Executing #5");
  delay(2000);
  tft.fillScreen(TFT_BLACK);
}
void action6() {
  tft.fillScreen(TFT_BLACK);
  tft.setCursor (10, 10);
  tft.print(">Executing #6");
  delay(2000);
  tft.fillScreen(TFT_BLACK);
}

 

Туцик
Туцик аватар
Offline
Зарегистрирован: 31.03.2020

https://ibb.co/m8DrRs3

выглядит так. Проблема в том, что при переходе в под меню 1.1 программа вываливается в основное меню.

 

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Туцик пишет:

с под меню не могу разобраться

А оно так не делается. С таким походом Вы его на 99,99% не сделаете, а на оставшиеся 0,01% программа получится  получится такой огромной, сложной и запутанной, что вероятность отладки до приемлемой надёжности практически неотличима от 0. По крайней мере, я бы не справился и даже браться бы не стал.

Многоуровневое меню делается не в лоб, а через дерево элементов. Тогда код остаётся в разумных размерах, разумной понятности, и всё получается. дерево элементов можно делать по-разному, но по-любому это должно быть дерево.

Oscar
Offline
Зарегистрирован: 10.04.2020

Хотел создавать отдельную тему тоже с многоуровневым меню, но меня опередили. В сети я нашел очень простой, на мой взгляд, вариант одноуровневого меню, самое главное что оно уже функциональное. Но опыта мало и не знаю как правильно сделать навигацию вверх/вниз в подменю.  Возможно здесь то что тебе нужно.

#include <LiquidCrystal_I2C.h>            // подключаем библиотеку дисплея
#include <Wire.h>                         // подключаєм библиотеку
LiquidCrystal_I2C lcd (0x3F,16,2);        // указиваем тип дисплея (конвектора)

int m=0; // переменная для экранов меню
int p1=0; // переменная  1
int p2=0; // переменная  2
int p3=0; // переменная  3

#define nextPin 46 // кнопка меню на 6 входе ENTER
#define prevPin 42 // кнопка возврата ESC
#define upPin 48 // увеличение значения отображаемого параметра +
#define downPin 44 // уменьшение значения -
#define enterPin 52
#define escPin 50


boolean buttonnextWasUp = true;
boolean buttonprevWasUp = true;
boolean buttonupWasUp = true;
boolean buttondownWasUp = true;
boolean buttonenterWasUp = true;
boolean buttonescWasUp = true;

boolean ledEnabled = false;

void setup()
{

// Установка пинов как входов

pinMode(nextPin, INPUT_PULLUP);
pinMode(prevPin, INPUT_PULLUP);
pinMode(upPin, INPUT_PULLUP);
pinMode(downPin, INPUT_PULLUP);
pinMode(enterPin, INPUT_PULLUP);
pinMode(escPin, INPUT_PULLUP);

Serial.begin(9600);
lcd.init();            // инициализация LCD
lcd.backlight();       // включаем подсветку

// Вывод приветствия
lcd.print( "MY PROGECT" );
// задержка
delay (1000);
}


void loop ()
{
  boolean buttonnextIsUp = digitalRead(nextPin);
  boolean buttonprevIsUp = digitalRead(prevPin);
  boolean buttonupIsUp = digitalRead(upPin);
  boolean buttondownIsUp = digitalRead(downPin);
  boolean buttonenterIsUp = digitalRead(enterPin);
  boolean buttonescIsUp = digitalRead(escPin);

//обработка нажатия кнопки enter
if (buttonenterWasUp && !buttonenterIsUp)
{
delay(10);
buttonenterIsUp = digitalRead(enterPin);
if (!buttonenterIsUp)
{
if (m==3)
{
m=30;
}
delay(100);
lcd.clear();
}
}

// обработка нажатия кнопки esc
if (buttonescWasUp && !buttonescIsUp)
{
delay(10);
buttonescIsUp = digitalRead(escPin);
if (!buttonescIsUp)
{
if (m==30)  
{
m=3;
}
else if(m==31)  
{
m=3;
}
else if(m==32)  
{
m=3;
}
delay(100);
lcd.clear();
}
}

//Обработка нажатия кнопки вниз 
if (buttonnextWasUp && !buttonnextIsUp)
{
delay(10);
buttonnextIsUp = digitalRead(nextPin);
if (!buttonnextIsUp)
{
m++;
if (m>4)
{
m=0;
}
delay(100);
lcd.clear();
}
}

//Обработка нажатия кнопки вверх
if (buttonprevWasUp && !buttonprevIsUp)
{
delay(10);
buttonprevIsUp = digitalRead(prevPin);
if (!buttonprevIsUp)
{
m--;
if (m<0)
{
m=4;
}
delay(100);
lcd.clear();
}
}


// Обработка нажатия для р1 +
if (buttonupWasUp && !buttonupIsUp)
{
delay(10);
buttonupIsUp = digitalRead(upPin);
if (!buttonupIsUp && m==1)
{
p1++;
 if (p1>10)
{
p1=0;
}
delay (100);
lcd.setCursor(5, 1);
lcd.print("  ");
}
}


// Обработка нажатия для р1 -
if (buttondownWasUp && !buttondownIsUp)
{
delay(10);
buttondownIsUp = digitalRead(downPin);
if (!buttondownIsUp && m==1)
{
p1--;
if (p1<0)
{
p1=10;
}
delay (100);
lcd.setCursor(5, 1);
lcd.print("  ");
}
}


// Обработка нажатия для р2 +
if (buttonupWasUp && !buttonupIsUp)
{
delay(10);
buttonupIsUp = digitalRead(upPin);
if (!buttonupIsUp && m==2)
{
p2++;
if (p2>10)
{
p2=0;
}
delay (100);
lcd.setCursor(5, 1);
lcd.print("  ");
}
}

// Обработка нажатия для р2 -
if (buttondownWasUp && !buttondownIsUp)
{
delay(10);
buttondownIsUp = digitalRead(downPin);
if (!buttondownIsUp && m==2)
{
p2--;
if (p2<0)
{
p2=10;
}
delay (100);
lcd.setCursor(5, 1);
lcd.print("  ");
}
}



//вывод меню



if (m==0)
{ 
lcd.setCursor(0, 0);
lcd.print( "Time" );
lcd.setCursor(14, 0);
lcd.print( "<-" );
lcd.setCursor(0, 1);
lcd.print( "Measurement" );
}

else if (m==1)
{
lcd.setCursor(0, 0);
lcd.print( "Measurement" );
lcd.setCursor(14, 0);
lcd.print( "<-" );
lcd.setCursor(0, 1);
lcd.print( "Setpoint trip" );
}

else if (m==2)
{ 
lcd.setCursor(0, 0);
lcd.print( "Setpoint trip" );
lcd.setCursor(14, 0);
lcd.print( "<-" );
lcd.setCursor(0, 1);
lcd.print( "Configuration");
}

else if (m==3)
{
lcd.setCursor(0, 0);
lcd.print( "Configuration" );
lcd.setCursor(14, 0);
lcd.print( "<-" );
lcd.setCursor(0, 1);
lcd.print( "Registration");
}

else if (m==4)
{
lcd.setCursor(0, 0);
lcd.print( "Registration");
lcd.setCursor(14, 0);
lcd.print( "<-" );
}


if (m==30)
{
lcd.setCursor(0, 0);
lcd.print( "MOTOR1" );
lcd.setCursor(14, 0);
lcd.print( "<-" );
lcd.setCursor(0, 1);
lcd.print( "MTZ2" );
}

else if (m==31)
{
lcd.setCursor(0, 0);
lcd.print( "MOTOR2" );
lcd.setCursor(14, 0);
lcd.print( "<-" );
lcd.setCursor(0, 1);
lcd.print( "MTZ3" );
}

else if (m==32)
{
lcd.setCursor(0, 0);
lcd.print( "MOTOR3" );
lcd.setCursor(14, 0);
lcd.print( "<-" );
}

buttonupWasUp = buttonupIsUp;
buttondownWasUp = buttondownIsUp;
buttonnextWasUp = buttonnextIsUp;
buttonprevWasUp = buttonprevIsUp;
buttonenterWasUp = buttonenterIsUp;
buttonescWasUp = buttonescIsUp;
}

 

Туцик
Туцик аватар
Offline
Зарегистрирован: 31.03.2020

Можете ткнуть пальцем, хоть куда копать. примеры или еще что...

b707
Offline
Зарегистрирован: 26.05.2017

Oscar. поправте меня, но по-моему понятия "одноуровневое меню" не предполагает никаких "подменю"... или я не прав?

Что касается кода, судя по огромному числу операторов delay() - вряд ли его писал продвинутый автор...

Oscar
Offline
Зарегистрирован: 10.04.2020

b707 пишет:

Oscar. поправте меня, но по-моему понятия "одноуровневое меню" не предполагает никаких "подменю"... или я не прав?

 

мм.. впринципе Вы правы, но в конце есть последние три m=30 ... 32. Это я пытался прикрутить второй уровень что бы сделать его многоуровневым, но не получается сделать прокрутку вверх/вниз. Я пытался сделать по аналогии как и для первого уровня, но не работает.

Oscar
Offline
Зарегистрирован: 10.04.2020

b707 пишет:

Что касается кода, судя по огромному числу операторов delay() - вряд ли его писал продвинутый автор...

Я так понимаю что автор это использовал для борьбы с дребезгом контактов, но в любом случае контроллер это не грузит.

b707
Offline
Зарегистрирован: 26.05.2017

Oscar пишет:

Я так понимаю что автор это использовал для борьбы с дребезгом контактов, но в любом случае контроллер это не грузит.

delay(100) уже ПОСЛЕ обработки КАЖДОГО нажатия? - а что оно делает кроме загрузки контроллера, по вашему?

Oscar
Offline
Зарегистрирован: 10.04.2020

Второй delay да, лишний, я его не увидел. 

Туцик
Туцик аватар
Offline
Зарегистрирован: 31.03.2020

Так у меня тоже меню работает, а вот под меню в данном варианте Нет. В цикле программы идет привязка к основному меню. Здесь действительно нужен другой подход.

Kakmyc
Offline
Зарегистрирован: 15.01.2018

Выглядеть должно примерно так:

void loop(){
switch(menu_item){
        case MAIN_SCREEN:
        draw_main();
       //Сюда интерфейс
}
        break;
        case SETUP_SCREEN:
        draw_setup();
        break;
        
        case TIME_MENU:
        set_time();
        break;
        
        case TEMP_MENU:
        set_temp();
        break;
        
        case HEAT_MENU:
        set_heat();
        break;
        
        case VENT_MENU:
        set_vent();
        break;
        
        case HUM_MENU:
        set_hum();
        break;
        
        case MOIST_MENU:
        set_moist();
        break;
        
        case CLOCK_MENU:
        set_clock();
        break;
        
    }//end switch menu_item
}

Внутри каждого подменю такой же интерфейс и switch с вариантами .

Т.е. когда menu_item =MAIN_SCREEN, мы видим главный экран и кнопками(энкодером и тп) меняем значение курсора и переменную. По кнопке ОК изменяем значение menu_item и соответственно видим другую отрисовку и уже в другом, вложенном switch меняем значение другой переменной(положение курсора)

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Туцик пишет:

Можете ткнуть пальцем, хоть куда копать. примеры или еще что...

Странный Вы человек. Я же Вам написал, что это делается на деревьях. Куда Вам ещё тыкать? Ну, ладно, если так хотите - пожалуйста.

Туцик
Туцик аватар
Offline
Зарегистрирован: 31.03.2020

Kakmyc

Как в вашем случае выглядит цикл программы? с опросом кнопок.  Просто если кнопка вниз то осн меню --. Если вверх, то осн меню ++. дальше выбрали подменю. вверх под меню++, вниз под меню --. Как вы в void loop будете опрашивать кнопки? Сразу для меню и подменю и под подменю? Вы привели аналог моего кода.

WavGat
Offline
Зарегистрирован: 14.04.2020

Тихо. Тут религиозные фанатики "правильного кода" сейчас нас с тобой заклюют, но я попробую тебе помочь.

Добавь переменную, напр. SubMenu

	void loop() {
	 
	     
	  if (enc1.isRight()) {                      // если энкодер вправо
	    if (SubMenu == 0) {// то выбираем следующий пункт меню ++
            menu++;
	    updateMenu();                            // отрисовываем меню заново
         } else {
           SubMenu++;
	    updateSubMenu();                            // отрисовываем меню заново
        }
	    while (enc1.isRight());
	  }

....

	 }

Дальше, думаю ты догадаешься что и как поправить.

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

WavGat пишет:

Дальше, думаю ты догадаешься что и как поправить.

Ну, да. А у подменю могут быть свои подменю. А одно меню (одного уровня) может содержать как терминальные элементы, так и подменю. И так на каждый случай добавляем переменные. Если меню содержит хотя бы два десятка элементов, то отладить такое будет невозможно.

Зачем обязательно придумывать костыли, если можно сделать системно и правильно? Это спорт такой?

b707
Offline
Зарегистрирован: 26.05.2017

ЕвгенийП пишет:

Зачем обязательно придумывать костыли, если можно сделать системно и правильно? Это спорт такой?

ждем выступление вавгата "а ТС именно так и заказывал" :)

Kakmyc
Offline
Зарегистрирован: 15.01.2018

Туцик пишет:

Kakmyc

Как в вашем случае выглядит цикл программы? с опросом кнопок.  Просто если кнопка вниз то осн меню --. Если вверх, то осн меню ++. дальше выбрали подменю. вверх под меню++, вниз под меню --. Как вы в void loop будете опрашивать кнопки? Сразу для меню и подменю и под подменю? Вы привели аналог моего кода.

Я не знаю, как у вас опрашиваются кнопки.
Но в моем варианте, как обычно.
Только вот результаты используются в одном меню , в том которое на данный момент является активным.

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

ЕвгенийП пишет:
А у подменю могут быть свои подменю. А одно меню (одного уровня) может содержать как терминальные элементы, так и подменю.
И ещё забыл сказать - некоторые подменю (или отдельные элементы) могут появляться и исчезать в зависимости от выбора в других элементах меню. Не деревьях всё это делается единообразно, без особых усилий, а с переменными - попробуйте!

Kakmyc
Offline
Зарегистрирован: 15.01.2018
int _key;
int menu_page;
enum MAIN_MENU {main_menu,menu1,menu2,menu3};
enum SUBMENU1{main_submenu1,submenu11,submenu12,submenu13,submenu1_exit};
enum SUBMENU2{main_submenu2,submenu21,submenu22,submenu23,submenu2_exit};

int key(){//опрос кнопки
    int val=0;//абсурдное значение , на самом деле тут результат опроса кнопки
   return val ;
     }

void f_menu1(){
    static byte menu_subpage=1;
    switch(menu_subpage){
        
        case main_submenu1:
        static byte val=0;
        //опустим подробности опроса, пусть входящие значения будут трех типов +/-/ок
        if('+')val++;
        if('-')val--;
        val=constrain(val,1,4);
        if("ok")menu_subpage=val;
        break;
        
        case submenu11:
        //submenu11 func
        break;
        case submenu12:
        //submenu12 func
        break;
        case submenu13:
        //submenu13 func
        break;
        case submenu1_exit:
        menu_page=main_menu;
        break;
        }
}

void f_menu2(){
    
}

void f_menu3(){
    
}

void setup(){
    
}

void loop(){
   int _key=key();
    switch(menu_page){
        case main_menu:
        static byte val=0;
        //опустим подробности опроса, пусть входящие значения будут трех типов +/-/ок
        if('+')val++;
        if('-')val--;
        val=constrain(val,1,3);
        if("ok")menu_page=val;
        break;
        
        case menu1:
        f_menu1();
        break;
        
        case menu2:
        f_menu2();
        break;
        
        case menu3:
        f_menu3();
        break;
        
    }

}

 

b707
Offline
Зарегистрирован: 26.05.2017

ну раз все побежали показывать свой г... код :) то я тоже продемонстрирую

Я в одном из проектов описывал все строчки меню единообразно

typedef struct PROGMEM {
  const char  Text[MENU_NAME_SIZE];
  const uint8_t Next;
  const uint8_t Type;
} menuItem;

и потом собирал из таких строчек все меню со всеми вложениями:

const menuItem menustack[] PROGMEM  = {
{"Settings menu:", 0, SUB_MENU},
{"Select shunt",   1, MENU_LINE},
{"Tune shunt",   COM_TUNE,MENU_LINE},
{"Save settings", 2 ,MENU_LINE},
{"Clear settings", 3 ,MENU_LINE},
{"Exit", COM_EXIT,MENU_LINE},

{"Shunt list:",1, SUB_MENU},
{"1 Ohm",COM_SEL_SHUNT, MENU_LINE},
{"0.1 Ohm",COM_SEL_SHUNT, MENU_LINE},
{"0.05 Ohm",COM_SEL_SHUNT, MENU_LINE},
{"0.01 Ohm",COM_SEL_SHUNT, MENU_LINE},
{"0.005 Ohm",COM_SEL_SHUNT, MENU_LINE},
{"<< back", 0 ,MENU_LINE},

{"Save current shunt & tune?", 2, SUB_MENU},
{"Save", COM_SAVE, MENU_LINE},
{"Cancel", 0 ,MENU_LINE},

{"Clear current shunt & tune?", 3, SUB_MENU},
{"Clear", COM_CLEAR, MENU_LINE},
{"Cancel", 0 ,MENU_LINE},
{"",0,MENU_END}
};

для наглядности вставил пустые строки между подменю.

Цифра после текста означает id элемента, на который указывает строчка. Если такому id соответвует строка с типом SUB_MENU -значит переходим на уровень ниже (или выше). Если для такого id подменю нет - значит это просто действие.

Например, строчка

{"Select shunt",   1, MENU_LINE},

указывает на подменю

{"Shunt list:",1, SUB_MENU},

а строка

{"Cancel", 0 ,MENU_LINE},

указывает на меню с ID =0, то есть выход на верхнй уровень

 

Топорно конечно, зато любое кол-чество дополнительный уровней и строчек меню можно добавить независимо от других.

 

nik182
Offline
Зарегистрирован: 04.05.2015

В своё время  прочитал http://easyelectronics.ru/organizaciya-drevovidnogo-menyu.html проникся, адаптировал под себя и с тех пор использую во всех местах, где нужно многоуровневое меню. Перегружено конечно, но за то очень легко сделать любой глубины и ширины, при этом вставить в любую точку дополнительный пункт даже не надо ничего переделывать. Потом появилось micromenu V2 https://github.com/abcminiuser/micromenu-v2  обещали что легче, но руки посмотреть не дошли.   

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

b707 пишет:

typedef struct PROGMEM {
  const char  Text[MENU_NAME_SIZE];
  const uint8_t Next;
  const uint8_t Type;
} menuItem;

любое кол-чество дополнительный уровней и строчек меню можно добавить независимо от других.

Вот и я ж про тоже. Деревья - они такие.

Я бы еще в дополнение к next, добавил бы ещё prev - тогда удаление на лету  - как два пальца получается.

Туцик
Туцик аватар
Offline
Зарегистрирован: 31.03.2020

ЕвгенийП не совсем пойму, этот пример для построения текстового меню. У меня графическое.

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Туцик пишет:

этот пример 

Какой? Я не давал никаких примеров.

Туцик пишет:

этот пример для построения текстового меню. У меня графическое.

С точки зрения организации данных разницы нет никакой от слова совсем. У любого меню есть терминальные элементы и подменю. У любого элемента (кроме корневого) есть "родитель". У большинства элементов есть "братья и сёстры". Организация данных всегда одинаковая. А уж как Вы там его показываете, командой "showText" или "showImage" - это вопрос №321.

Если хотите грамотно делать меню, изучите работу с деревьями. Так в лоб, с какими-то бессистемными переменными-флагами Вы этого не сделаете.

Туцик
Туцик аватар
Offline
Зарегистрирован: 31.03.2020

Сорри за тупость, но я не понял. дерево своего меню нарисовал:

 

дальше как я понял, мы описываем меню.

typedef struct PROGMEM{
  void       *Next;
  void       *Previous;
  void       *Execute;
} menuItem;

А вот дальше я нифига не понял.

 

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Туцик пишет:

дальше как я понял, мы описываем меню.

typedef struct PROGMEM{
  void       *Next;
  void       *Previous;
  void       *Execute;
} menuItem;

А вот дальше я нифига не понял.

Этого маловато. Я сейчас Вам набросаю общих идей в виде псевдокода, но именно "псевдо". Уж простите, запускать и отлаживать сейчас некогда, а кроме того, я тут за ужином немного поборолся с вирусом (дезинфицирующего срецтва принял), но идейно как это делать покажу (на примере Вашего дерева). Щас ... , возвращайтесь через часик.

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Ну, вот, смотрите.

Я сделал некое универсальное меню, в которое напихал всё, что спьяну вспомнил. Для любого реального проекта, здесь много лишнего и ненужного, но я хотел показать как делаются разные вещи.

Здесь можно создавать меню, показывать его с любым уровнем вложенности, на лету добавлять и удалять элементы.

На все эти возможности есть комментарии и примеры. Читайте комментарии, запускайте примеры, если чего – спрашивайте. Для примера взято Ваше меню, но я добавил ещё один уровень.

Сразу предупреждаю, не проверял, т.к. в данный момент не могу, но на UNO по идее должно работать, разве что опечатка какая – тогда поправим. 

Запускайте. смотрите примеры, как сделано, разбирайтесь, если что - спрашивайте, но не обещаю насчёт сегодня - что-то я сегодня перебрал с дезинфекцией :-(


struct SMenuItem {
	int id; 						// уникальный идентификатор данного элемента
	SMenuItem * prev; 		// следующий элемент данного урвоня (nullptr - если этот - последний)
	SMenuItem * next; 		// предыдущий элемент данного урвоня (nullptr - если этот - первый)
	SMenuItem * parent;		// адрес родительского элемента (nullptr - если нет родителей)
	SMenuItem * children;	// адрес списка элементов "подменю" (nullptrll - если это лист)
	const char * itemText;	// Название данного элемента (тут может быть не текст, а адрес картинки. Функция show знает, что с этим делать)

	//
	// Показать данный элемент
	void show(void) {
		for (SMenuItem * ptr = parent; ptr; ptr = ptr->parent) {
			if (ptr->parent) Serial.print("   ");
		}
		Serial.println(itemText);
	}

	//
	// Показать всех детей данного элемента
	//	Параметр: глубина показа "внуков". 
	//	Если 1 - только дети, если 2 - то и внуик и т.д.
	void showChildren(const int showGrandChilren = 1) {
		if (! showGrandChilren) return;
		for (SMenuItem * ptr = children; ptr; ptr = ptr->next) {
			ptr->show();
			ptr->showChildren(showGrandChilren - 1);
		}
	}

	//
	//	выполнить действие, когда этот элемент выбран
	void action(void) {
		Serial.print("*** The menu \"");
		Serial.print(itemText);
		Serial.print("\" is executed");
	}

	//
	//	Удаление данного элемент из меню
	void removeMe(void) {
		//
		// Если есть следующий элемент, то 
		//	делаем мой предыдущий, его предыдущим
		if (next) next->prev = prev;
		//
		// Если есть предыдущий
		// то делаем наш следущий, его следущим
		// Иначе говорим родителю, что наше next - его первый ребёнок.
		if (prev) prev->next = next; 
		else parent->children = next;
	}

	//
	// Вставить себя как первого ребёнка объекта _parent
	void insertAsFirstChild(SMenuItem & _parent) {
		// 
		// Вставляем первого ребёнка после себя, а себя делаем первым ребёнком
		next = _parent.children;
		prev = nullptr;
		_parent.children = this;
	}

	//
	// Вставить себя после объекта _prev
	void insertAfter(SMenuItem & _prev) {
		// 
		// Ставим себя следующим объекту _prev, а его следующего - своим следующим
		next = _prev.next;
		_prev.next = this;
	}

	//
	//	Конструктор - создать элемент
	SMenuItem(const char * const _itemText, SMenuItem * _prev = nullptr, SMenuItem * _parent = nullptr) {
		itemText = _itemText;
		prev = _prev;
		parent = _parent;
		children = nullptr;
		next = nullptr;
		//
		// Если у нас есть родитель
		if (parent) {
			// если у родителя пока нет детей, записываемся началом списка детей.
			if (parent->children == nullptr) parent->children = this;
		}
		//
		// Если у нас есть предыдущий элемент
		if (prev) {
			// записываемся ему в "следующие"
			prev->next = this;
		}
	}
};


// Корневой элемент всего меню. Он обычно невидимый. 
//	он родитель элементов верхнего уровня!
SMenuItem m0("");	// у этого нет ни предыдущего, ни родителя

SMenuItem m1("Menu1", nullptr, & m0);	// у этого нет ни предыдущего
SMenuItem m2("Menu2", & m1, &m0);
SMenuItem m3("Menu3", & m2, &m0);
SMenuItem m4("Menu4", & m3, &m0);
SMenuItem m5("Menu5", & m4, &m0);
SMenuItem m6("Menu6", & m5, &m0);
SMenuItem m11("Menu11", nullptr, & m1);// у этого нет предыдущего, но есть родитель
SMenuItem m12("Menu12", & m11, & m1);	// у этого есть и предыдущий, и родитель
SMenuItem m41("Menu41", nullptr, & m4);// у этого нет предыдущего, но есть родитель
SMenuItem m42("Menu42", & m41, & m4);	// у этого есть и предыдущий, и родитель
SMenuItem m411("Menu411", nullptr, & m41);
SMenuItem m412("Menu412", & m411, & m41);
SMenuItem m413("Menu413", & m412, & m41);


void prompt(const char * const title) {
	Serial.println (title);
	while(!Serial.available()); // Ждём ввода
	while(Serial.available()) Serial.read(); // Вычитываем всё, что есть
}

void setup() {
	Serial.begin(115200);
	Serial.println("Веселье начинается!");
	
	prompt("Печатаем меню целиком. Дави ENTER");
	m0.showChildren(1000);

	prompt("Печатаем три уровня (это тоже, что целиком). Дави ENTER");
	m0.showChildren(3);

	prompt("Печатаем два уровня. Дави ENTER");
	m0.showChildren(2);

	prompt("Печатаем один уровень. Дави ENTER");
	m0.showChildren(1);

	prompt("Удаляем Menu412. Дави ENTER");
	m412.removeMe();
	m0.showChildren(1000);
	
	prompt("Удаляем Menu3. Дави ENTER");
	m3.removeMe();
	m0.showChildren(1000);
	
	prompt("Удаляем Menu1 вместе с детьми. Дави ENTER");
	m1.removeMe();
	m0.showChildren(1000);
	
	prompt("Вставляем Menu3 - в самое начало. Дави ENTER");
	m3.insertAsFirstChild(m0);
	m0.showChildren(1000);
	
	prompt("Вставляем Menu1 после Menu3. Дави ENTER");
	m1.insertAfter(m3);
	m0.showChildren(1000);
}

void loop() {
}
Туцик
Туцик аватар
Offline
Зарегистрирован: 31.03.2020

ЕвгенийП ого! лихо. спасибо, буду разбираться. надеюсь разберусь. Скетч без ошибок, все работает. Я так же по Вашим рекомендациям смотрел в нете построение меню древом. После графического представления меню, идет построение матрицы, которую Вы реализовали в коде. Дети, внуки, правнуки только не понятны. Меню11 и меню12 это дети родителя меню1 или дети родителей меню1-меню6 ?

SMenuItem m0("");  --- согласен, это нулевое меню бога, которое не отображается и служит типа контрольной точки отсчета, у которого нет ни предыдущего ни последующего соседа

SMenuItem m1("Menu1", nullptr, & m0); ----- до него ничего нет, справа по идее от него должен быть m2 ??

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Туцик пишет:
Дети, внуки, правнуки только не понятны. Меню11 и меню12 это дети родителя меню1 или дети родителей меню1-меню6 ?

Родитель всегда только один - третий параметр конструктора

SMenuItem m4("Menu4", & m3, &m0); // Родитель m4 -> это m0
SMenuItem m412("Menu412", & m411, & m41); // Родитель m412 -> это m41

Вообще, схема ТОЧНО ТАКАЯ как на Вашей картинке с единственной разницей - я добавил три элемента, которых у Вас не было - m411, m412 и m413 - все трое - дети 41.

Поэтому, если Вам непонятно кто чей родитель, смотрите свою картинку или же то, что печатается - там же ясно кто чьи дети - они со сдвигом печатаются - дети под родителем со сдвигом вправо..

Туцик пишет:

SMenuItem m1("Menu1", nullptr, & m0); ----- до него ничего нет, справа по идее от него должен быть m2 ??

Перед ним - никого, т.к. у него второй параметр nullptr. А за ним тот, у кого второй параметр m1. Вообще, второй параметр - это имя того, кто перед этим элементом. Вы смотрите на печать и на свою картинку.

 

Туцик
Туцик аватар
Offline
Зарегистрирован: 31.03.2020

Понятно с детьми изложили. Я думал строится все слоями: вначале все пункты меню, 2й слой пункты ПОДменю, 3 слой пункты ПОДпод меню и тд и точка перехода между слоями.

 int id;    непонятно где этот идентификатор используется.

 

И главное, это вызов.

Начало программы. Рисую все пункты меню. Курсор ставлю в позицию меню1. Вызывать так: SMenuItem m1; ??

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Туцик пишет:

 int id;    непонятно где этот идентификатор используется.

Нигде. Это на вырост. Почти наверняка пригодится в будущем.

Туцик пишет:

И главное, это вызов.

Начало программы. Рисую все пункты меню. Курсор ставлю в позицию меню1. Вызывать так: SMenuItem m1; ??

На самом деле, это тоже делается единообразно (всегда одинаково). Рисуете меню. Заводите специальный указатель currentItem и настраиваете его на текущее активное меню:

currentItem = & m1

Через него делаете все переходы и вызовы.

.... блин, перечитал ... сам не понимаю давайте-ка я сейчас тот пример расширю.

Только я не буду добавлять никаких экранов, а сделаю так. Текущий элемент меню будет всегда показываться "звёздочкой". Вместо ENTER вводим цифирь. Означает она следующее:

1 - продвинуться по меню вверх, если есть куда (на том же уровне)
2 - продвинуться вниз, если есть куда (на том же уровне)
3 - перейти на уровень вверх (к родителю)
4 - если это не листик, то перейти на уровень вниз, а если листик, то выполнить этот пункт

Другие числа (и знаки) игнорируются.

Походите по меню туда сюда, убедитесь, что всё работает.  Потом попробуйте что-то менять и смотрите, правильно ли Вы поняли программу (адекватно ли она реагирует на Ваши изменения).

И, главное, имейте в виду - любое меню, хоть текстовое, хоть графическое. хоть голосовое - любое делается примерно также. Разница только в функция показа (у меня они называются show).


struct SMenuItem {
	int id; 						// уникальный идентификатор данного элемента
	SMenuItem * prev; 		// следующий элемент данного урвоня (nullptr - если этот - последний)
	SMenuItem * next; 		// предыдущий элемент данного урвоня (nullptr - если этот - первый)
	SMenuItem * parent;		// адрес родительского элемента (nullptr - если нет родителей)
	SMenuItem * children;	// адрес списка элементов "подменю" (nullptrll - если это лист)
	const char * itemText;	// Название данного элемента (тут может быть не текст, а адрес картинки. Функция show знает, что с этим делать)

	//
	// Показать данный элемент
	void show(void) {
		// ДОПОЛНЕНИЕ - если мы активны, печатаем звёздочку, иначе - пробел
		Serial.print(iAmActive ? '*' : ' ');

		for (SMenuItem * ptr = parent; ptr; ptr = ptr->parent) {
			if (ptr->parent) Serial.print("   ");
		}
		Serial.println(itemText);
	}

	//
	// Показать всех детей данного элемента
	//	Параметр: глубина показа "внуков". 
	//	Если 1 - только дети, если 2 - то и внуик и т.д.
	void showChildren(const int showGrandChilren = 1) {
		if (! showGrandChilren) return;
		for (SMenuItem * ptr = children; ptr; ptr = ptr->next) {
			ptr->show();
			ptr->showChildren(showGrandChilren - 1);
		}
	}

	//
	//	выполнить действие, когда этот элемент выбран
	void action(void) {
		Serial.println("***********************************************");
		Serial.print("***** The menu \"");
		Serial.print(itemText);
		Serial.println("\" is executed");
		Serial.println("***********************************************");
	}

	//
	//	Удаление данного элемент из меню
	void removeMe(void) {
		//
		// Если есть следующий элемент, то 
		//	делаем мой предыдущий, его предыдущим
		if (next) next->prev = prev;
		//
		// Если есть предыдущий
		// то делаем наш следущий, его следущим
		// Иначе говорим родителю, что наше next - его первый ребёнок.
		if (prev) prev->next = next; 
		else parent->children = next;
	}

	//
	// Вставить себя как первого ребёнка объекта _parent
	void insertAsFirstChild(SMenuItem & _parent) {
		// 
		// Вставляем первого ребёнка после себя, а себя делаем первым ребёнком
		next = _parent.children;
		prev = nullptr;
		_parent.children = this;
	}

	//
	// Вставить себя после объекта _prev
	void insertAfter(SMenuItem & _prev) {
		// 
		// Ставим себя следующим объекту _prev, а его следующего - своим следующим
		next = _prev.next;
		_prev.next = this;
	}

	//
	//	Конструктор - создать элемент
	SMenuItem(const char * const _itemText, SMenuItem * _prev = nullptr, SMenuItem * _parent = nullptr) {
		itemText = _itemText;
		prev = _prev;
		parent = _parent;
		children = nullptr;
		next = nullptr;
		//
		// Если у нас есть родитель
		if (parent) {
			// если у родителя пока нет детей, записываемся началом списка детей.
			if (parent->children == nullptr) parent->children = this;
		}
		//
		// Если у нас есть предыдущий элемент
		if (prev) {
			// записываемся ему в "следующие"
			prev->next = this;
		}
		// 
		// ДОПОЛНЕНИЕ - мы неактивны
		iAmActive = false;
	}

//
//  ДОПОЛНЕНИЯ
//
	bool iAmActive;

	// Активировать (параметр - true) или деактивировать
	// Возвращает адрес себя
	SMenuItem * activate(const bool actDeact) {
		iAmActive = actDeact;
		return this;
	}

	// Перейти к следующему (если есть)
	// Возвращает адрес нового (или старого, если не перешли) активного элемента
	SMenuItem * goNext(void) {
		// Если есть следующий, то активируем его
		if (next) {
			activate(false); // деактивируем себя
			return next->activate(true);
		}
		return this;
	}

	// Перейти к предыдущему (если есть)
	// Возвращает адрес нового (или старого, если не перешли) активного элемента
	SMenuItem * goPrev(void) {
		// Если есть предыдущий, то активируем его
		if (prev) {
			activate(false); // деактивируем себя
			return prev->activate(true);
		}
		return this;
	}

	// Перейти к родителю (если есть)
	// на самом деле переходим только в том случае, если у родителя есть 
	// ещё и свой родитель, т.к. нам не нужно переходиь к общему прародителю
	// (в нашем случае к m0)
	// Возвращает адрес нового (или старого, если не перешли) активного элемента
	SMenuItem * goParent(void) {
		// Если есть родитель и у того тоже есть родитель, то активируем родителя
		if (parent && parent->parent) {
			activate(false); // деактивируем себя
			return parent->activate(true);
		}
		return this;
	}

	// Если нет детей, то выполнить данный пункт
	// А если есть дети, то перейти к первому ребёнку 
	// Возвращает адрес нового (или старого, если не перешли) активного элемента
	SMenuItem * goChildOrRun(void) {
		// Если нет детей, то выполняем данный элемент
		if (! children) action();
		else {
			activate(false); // деактивируем себя
			return children->activate(true);
		}
		return this;
	}
};


// Корневой элемент всего меню. Он обычно невидимый. 
//	он родитель элементов верхнего уровня!
SMenuItem m0("");	// у этого нет ни предыдущего, ни родителя

SMenuItem m1("Menu1", nullptr, & m0);	// у этого нет ни предыдущего
SMenuItem m2("Menu2", & m1, &m0);
SMenuItem m3("Menu3", & m2, &m0);
SMenuItem m4("Menu4", & m3, &m0);
SMenuItem m5("Menu5", & m4, &m0);
SMenuItem m6("Menu6", & m5, &m0);
SMenuItem m11("Menu11", nullptr, & m1);// у этого нет предыдущего, но есть родитель
SMenuItem m12("Menu12", & m11, & m1);	// у этого есть и предыдущий, и родитель
SMenuItem m41("Menu41", nullptr, & m4);// у этого нет предыдущего, но есть родитель
SMenuItem m42("Menu42", & m41, & m4);	// у этого есть и предыдущий, и родитель
SMenuItem m411("Menu411", nullptr, & m41);
SMenuItem m412("Menu412", & m411, & m41);
SMenuItem m413("Menu413", & m412, & m41);

//
// Ввод числа от 1 до 4
//
int prompt(void) {
	int res = 0;
	while (res < 1 || res > 4) {
		Serial.println ("Введи число от 1 до 4: ");
		Serial.println ("  1 - к предыдущему;");
		Serial.println ("  2 - к следующему;");
		Serial.println ("  3 - к родителю;");
		Serial.println ("  4 - к ребёнку или (если нет детей, то выполнить)");
		res = Serial.parseInt(); // Вводим число
		while(Serial.available()) Serial.read(); // Вычитываем всё, что осталось
	}
	return res;
}

void setup() {
	Serial.begin(115200);
	Serial.setTimeout(0xFFFFFFFF);
	Serial.println("Веселье начинается!");
}

void loop() {
	static SMenuItem * activeElement = m1.activate(true);

	Serial.println("----------------------------------");
	m0.showChildren(1000); // Печатаем меню

// Обрабатываем комнду
//	1 - продвинуться по меню вверх, если есть куда (на том же уровне)
//	2 - продвинуться вниз, если есть куда (на том же уровне)
//	3 - перейти на уровень вверх (к родителю)
//	4 - если это не листик, то перейти на уровень вниз, а если листик, то выполнить этот пункт	
	switch (prompt()) {
		case 1:  activeElement = activeElement->goPrev(); break;
		case 2:  activeElement = activeElement->goNext(); break;
		case 3:  activeElement = activeElement->goParent(); break;
		case 4:  activeElement = activeElement->goChildOrRun(); break;
	}
}

Пробуйте. Следите за звёздочкой - она означает активный (выбранный) в данный момент элемент. Как видите, можно ходить по меню и исполнять терминальные элементы.

Кстати, можно на лету добавлять и удалять элементы (как мы делали в прошлом примере), всё продолжает адекватно работать (можете попробовать), однако ... ВНИМАНИЕ!!! Тут есть грабли. Если Вы попытаетесь удалить элемент, который в настоящий момент активен - всё поломается. Чтобы такого не было, надо проверять при удалении не активен ли удаляемый элемент и либо отказываться удалять, либо переводить активность на другой элемент. Но это Вы уж сами. Как переводить активность в этом примере показано (кнопки 1 и 2).

Туцик
Туцик аватар
Offline
Зарегистрирован: 31.03.2020

ЕвгенийП

Огромное спасибо. Это почти готовая операционка. Неплохо было бы модеру в шапку приклеить. Если не секрет, сколько осваивали язык?

sadman41
Offline
Зарегистрирован: 19.10.2016

Туцик пишет:

Если не секрет, сколько осваивали язык?

Пишет, что за день в новом языке разобраться может. Думаю, что он - энопланетянин.

Туцик
Туцик аватар
Offline
Зарегистрирован: 31.03.2020

Многие языки по синтаксису и структуре похожи. Мне для того, чтобы перейти с Бейсика на Паскаль тоже немного времени понадобилось. Тк очень похожи.

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Туцик пишет:

Если не секрет, сколько осваивали язык?

Я по работе язычник, т.е. я занимаюсь наукой в области языков программирования (кстати, Ворота - тоже). Правда, если смотреть на более узкую специализацию, то языками несколько другого типа, но в контексте любительского форума это совсем несущественно.

Kakmyc
Offline
Зарегистрирован: 15.01.2018

Ппц я тупой похоже. Целый день въезжал в то как это дерево структурное работает. Ладно хоть дошло в итоге. Ещё два дня ушло на то, что бы понять, как прикрутить туда функции. Грёбаный синтаксис, какое то написание не совсем очевидное if(curItem->Handler){(*curItem->Handler)();}

Зато теперь понял, что если прикрутить туда какой-нить идентификатор действия, типа :

typedef struct menuItem{
//Blablabla
byte identify_num;
//blablabla
}menuItem;



if(currentItem->ok){
switch (currentItem->identify_num){
case 1:
//do1
break;
case 2:
(*currentItem->Handler)();
break;
case 3:
//do3
break;
}

можно делать вообще что угодно одним энкодером или тремя кнопками.

sadman41
Offline
Зарегистрирован: 19.10.2016

Ещё немного и лямбда-функции начнут зохватывать мозг.

Kakmyc
Offline
Зарегистрирован: 15.01.2018

sadman41 пишет:

Ещё немного и лямбда-функции начнут зохватывать мозг.

 

Мозг такими темпами не доживёт.

Долго искал причину, почему скачет по меню программа. Оказалось энкодер какашный, программно не фильтруется толком.

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

sadman41 пишет:

лямбда-функции начнут зохватывать мозг.

Ну, уж нет! Эта корова коронавируса и он её доит! И никаким функциям он её не отдаст, будь они хоть лямбда, хоть дзета.

Туцик
Туцик аватар
Offline
Зарегистрирован: 31.03.2020

Скоро всем в мозги внедрят интерпретаторы машинного кода. И в мире закончится срачЪ какой язык лучше. И с компьютером мы все будем на "ты".

Kakmyc
Offline
Зарегистрирован: 15.01.2018

Туцик пишет:

Скоро всем в мозги внедрят интерпретаторы машинного кода. И в мире закончится срачЪ какой язык лучше. И с компьютером мы все будем на "ты".

Вряд ли.
Подавляющее большинство людей диссонируют с понятием логики. Там любой интерпретатор загнётся

Kakmyc
Offline
Зарегистрирован: 15.01.2018

Очередной затык приключился.

//структура такого типа

typedef struct menuItem
{   char *name;
    menuItem *prev;
    menuItem *next;
    menuItem *ok;
    byte func_num;
    void (*Handler)();
    int value;
} menuItem;

menuItem m1;

//И переменная 
int myInt=2;

//Не могу понять как правильно написать.

m1={"Setup",&m4,&m2,&m11,1,NULL, ??????      };
//Что бы при изменении значений так:
if(нужно убавить){m1->value--;}
Что бы менялось значение myInt

 

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Ничего не понял.

Kakmyc
Offline
Зарегистрирован: 15.01.2018

ЕвгенийП пишет:

Ничего не понял.

 

Есть переменная, как структуре на нее передать ссылку ?

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Ну, не знаю, есть 100500 способов. Можно в структуре завести поле - ссылку или указатель на эту переменную. Но, проще завести эту переменную как статическое поле структуры - тогда она будет одна на все экземпляры структуры, но при этом оставаться её внутренним полем.

Kakmyc
Offline
Зарегистрирован: 15.01.2018
m111={"setup_volume",NULL,NULL,&m1,2,NULL,*myInt};

На такое компилятор ругается

ЕвгенийП пишет:

Ну, не знаю, есть 100500 способов. Можно в структуре завести поле - ссылку или указатель на эту переменную. Но, проще завести эту переменную как статическое поле структуры - тогда она будет одна на все экземпляры структуры, но при этом оставаться её внутренним полем.

 

В общем то я про это и спрашиваю.

Как будет выглядеть правильный синтаксис, для передачи указателя структуре

sadman41
Offline
Зарегистрирован: 19.10.2016

В ардуине посмари исходник attachInterrupt(), там ссылка на функцию-хендлер сейвится в массив.

Kakmyc
Offline
Зарегистрирован: 15.01.2018

sadman41 пишет:
В ардуине посмари исходник attachInterrupt(), там ссылка на функцию-хендлер сейвится в массив.

 

Handler по указателю у меня работает.

У меня на int матом ругается

ЕвгенийП
ЕвгенийП аватар
Offline
Зарегистрирован: 25.05.2015

Приводите код полностью. Откуда мне знать на что он ругается?

Kakmyc пишет:

Как будет выглядеть правильный синтаксис, для передачи указателя структуре

Ну, для начала нужно передавать именно указатель, а не его разыменование, т.е., разумеется не *myInt, а хотя бы & myInt, но, повторяю, приводите код полностью.

Только на 100500% более грамотно не передавать указатель, а завести статическое поле. Расход памяти будет меньше на 2 * <количество экземпляров>. Да и вообще, если класс работает с внешними указателями - это признак неграмотного проектирования программы.

Kakmyc
Offline
Зарегистрирован: 15.01.2018

Хорошо, попробуем с другой стороны:

int x=5;

typedef struct myStruct{
    int a;
    byte b;
    long c;
};

myStruct abc={10,20,x};

void setup(){
    Serial.begin(9600);
}

void loop(){
    delay(500);
    abc.c=abc.c+=1;
    Serial.println(x);//так всегда выводит '5'
    Serial.println(abc.c);//так инкрементируется
    
//как сделать, что бы инкрементировался "х"
}