Ограничение числа строк для вывода многоуровневого меню

Доброго времени суток, уважаемые форумчане!
Я новичок на форуме. На старом форуме поиском я нашел тему " Создание многоуровневого графического меню" ЕвгенийП выложил код древовидного меню. Задать вопрос я не смог, так как не был зарегистрирован, регистрация возможна для новых пользователей только на новом форуме. Но здесь я не нашел данную тему. Если правила не запрещают, то я могу выложить ссылку на тему старого форума.
С работой кода многоуровневого меню почти разобрался. Многое понял, как изменить и модифицировать. Но возник вопрос, который я не могу задать на старом форуме. Задам здесь.
Сам код выводит все пункты меню. Предположим, что их 20, а на экран помещается только 8. Подскажите, пожалуйста, как выводить на экран заданное число строк меню (к примеру 4 или 8), и листать строки меню вверх, при переходе на следующий пункт меню, “не помещающийся на экране”?

Вот код меню, автором которого является ЕвгенийП.

   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;
   }
}```

Как я понял, за вывод строк меню отвечает метод showChildren, который печатает все строки меню вызывая метод show(), но как заставить showChildren печатать не все, а нужное число строк на экран с возможностью их скролинга?

Если по уму, то надо отделить мух от котлет. Основной код должен выводить все 20 (или сколько там) на “виртуальный экран” на котором всё помещается и не париться. Т.е. основной код вообще ничего не знает об этой проблеме и никак не меняется.

А модуль (класс или что там) “виртуального экрана” делает всю работу со скроллингом.

Получается мы не добавили новую сложность в код, а сделали отдельный код со своей сложностью.

Начать можно со следующего. Заведите пустой класс “виртуальный экран” с методами вывести, стереть, и т.п. - т.е. с теми методами, которые у Вас уже есть. В основном коде вызывайте эти методы, всё должно работать. А потому уж добавляйте в этом модуле ограничение физического экрана.

2 лайка

Спасибо за Ваш ответ!
Правда я слабо представляю, как это сделать. У меня пока есть только Ваш код меню.Его я и изучаю и думаю как реализовать скролинг.
Может быть у Вас какой нибудь пример, чтобы разобравшись по аналогии сделать тоже самое и в Вашем коде многоуровнего меню?

Ну так первый шаг описан в последнем абзаце сообщения №2.
Второй шаг - добавить два поля: максимальное количество строк виртуального экрана и номер первой отображаемой строки.
Третий шаг - придумать алгоритм, по которому будет изменяться номер первой отображаемой строки.

1 лайк

Спасибо! Буду думать. Пробовать. Если что, сново буду сюда стучаться. :wink:
А образец бы поглядеть было бы вообще здорово.

Так образца ни у кого нет.
Петрович предлагает наиболее красивое решение (хотя не уверен, что оптимизированное по расходу памяти), у меня - свой код - довольно кустарный, но хочется верить, более оптимизированный по памяти. Если бы у Петровича был тот вариант, который нужен Вам, думаю, он бы его уже давно выложил. А мой код с его кодом скрестить не удастся.
Так что Вам остается либо делать самому, либо заказывать в разделе “Ищу исполнителя”.

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

А есть ли возможность на него взглянуть? Если это корректно с моей стороны…

К сожалению, это так. Я имел ввиду пример создания класса “виртуального экрана” и его методов - просто пустышку без рабочего кода. Как пример. Слаб я еще в программировании. Обучаюсь на примерах.

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

но там специфика - оно написано для самого распространенного типа дисплея и изменение количества строк не предусмотрено. Но, в принципе, меню тоже иерархическое.

1 лайк

Посмотрите GitHub - abcminiuser/micromenu-v2: Tiny text-orientated menu library in C for embedded use.
Идеология похожа. Есть пример. У меня работает на дисплее 16х2 с глубиной вложения 10х5
Ну и что б совсем не думать есть вот такое GitHub - VaSe7u/LiquidMenu: Menu creation Arduino library for LCDs, wraps LiquidCrystal.

1 лайк

Спеасибо большое! Завтра буду изучать.

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

1 лайк

Уже проникаюсь постепенно. Но где об этом почитать? Как все это работает и как создается? Я пока только на начальном этапе в изучении программирования микроконтроллеров. Так, знаю азы программирования. Поэтому буду очень рад любому совету!

Надобности выводить 8строк меню не возникало.
А при выводе двух , на дисплей 1602, первым вывожу строку текущего пункта, а следом все тоже самое, но уже по указателю на который ссылается пункт next структуры

Зрение у меня слабовато, вот и приходится использовать TFT дисплеи - и видно лучше, и информативность и удобство управления повышается.

Ну, так с ходу не скажу, но пример поищу вечером. Что-то было для семисегментников, надо глянуть какого качесвта.

1 лайк

Было бы здорово! Если понять принцип, то все пойдет легче. А на наглядном примере и понимание быстрей приходит. Очень буду вечера ждать :slight_smile: .

Ну первое что приходит в голову, это создать временный экземпляр структуры, выводить данные первой строки , менять значение экземпляра на ->next и т.д. пока не выведется нужное количество строк

1 лайк

Я так и сделал. Вывел на экран 8 строк меню. Бегу по ним. А дальше указатель уходит за пределы экрана. Вот как в этом коде при достижении 9 строки поднять предыдущие строки вверх? Проскролить. Как я понял, в этом коде вывод строк идет, пока они есть.

   	if (! showGrandChilren) return;
   	for (SMenuItem * ptr = children; ptr; ptr = ptr->next) {
   		ptr->show();
   		ptr->showChildren(showGrandChilren - 1);
   	}
   })```
Вот тут я и засел.

Так у тебя в next последнего экземпляра должен быть указан первый, если нужно зациклить перемещение.

Я уже писал в 4 сообщении: нужно добавить переменную - номер строки меню, которая выводится самой первой строкой экрана. Ну или то же самое: с какой строки меню начинать вывод на экран. Т.е. строки вверх не поднимаются, а просто игнорируются.