Форум Flasher.ru
Ближайшие курсы в Школе RealTime
Список интенсивных курсов: [см.]  
  
Специальные предложения: [см.]  
  
 
Регистрация Блоги Правила Справка Пользователи Календарь Поиск рулит! Сообщения за день Все разделы прочитаны
 

Вернуться   Форум Flasher.ru > Блоги > dark256

Рейтинг: 5.00. Голосов: 3.

Многоуровневое меню

Запись от dark256 размещена 27.11.2015 в 00:10

Данный пост не есть декларация достижений,
а просьба наставить на истинный путь велосипедостроителя

Итак. Очередной раз возникла у меня необходимость сгенерить N-уровневое меню.
До настоящего момента такие конструкты в моей жизни случались, но все
как-то на колене и по-быстрому. А тут захотелось глобального универсального
мегагенератора. Структура меню выглядит так:

Название: Screenshot_2.jpg
Просмотров: 1840

Размер: 43.0 Кб

Как можно легко заметить невооруженным взглядом, тут 3 уровня вложенности.
На текущий момент мне этого достаточно, но хотелось именно N уровней.
Произвольное, стало быть, количество.

Задать генерацию хотелось максимально простым для взгляда способом.
Без ручной индексации элементов и буллетов. WYSIWYG, тк скть.
Мы просто пишем ДОБАВИТЬ, такой-то текст, на такой-то уровень ( lvl )
и что он делает ( par ). При этом последовательность строк кода задает
последовательность расположения элементов в меню без дополнительных телодвижений.

п.1. Как избавиться от задания уровня - я не придумал. Кроме как вкладывать вызовы друг в друга, но
это сразу становилось нечитаемым. Оставил так. Вроде не сильно режет глаз.

п.2. par - гарантия избавления от будущих гемороев, так как все мы знаем, как
клиент просит "поиграться" фонтами и т.п. Тут уж вандалоустойчивость в чистом виде и
этот аппендикс
было решено оставить и не трогать.. Хотя.......

Итак, садимся и пишем от руки "код":

Код:
addMenuIt( my_menu, { lvl:0, dsc:"История операций, выписки", lvl:0 } )
	addMenuIt( my_menu, { lvl:1, dsc:"История операций", par:function(){} } )
	addMenuIt( my_menu, { lvl:1, dsc:"Выписка по карточному счету", par:function(){} } )
	addMenuIt( my_menu, { lvl:1, dsc:"История заявлений", par:function(){} } )
			
addMenuIt( my_menu, { lvl:0, dsc:"Карты и счета" } )
	addMenuIt( my_menu, { lvl:1, dsc:"Карты и счета" } )
			addMenuIt( my_menu, { lvl:2, dsc:"Услуги", par:function(){} } )
			addMenuIt( my_menu, { lvl:2, dsc:"Информация", par:function(){} } )
			addMenuIt( my_menu, { lvl:2, dsc:"Карты (Если выбран карточный счет)", par:function(){} } )
			addMenuIt( my_menu, { lvl:2, dsc:"Платежные реквизиты", par:function(){} } )
			
	addMenuIt( my_menu, { lvl:1, dsc:"Заявления", par:function(){} } )
			addMenuIt( my_menu, { lvl:2, dsc:"Заявка на кредит", par:function(){} } )
			addMenuIt( my_menu, { lvl:2, dsc:"Заявка на кредитную карту", par:function(){} } )
....
Вроде бы читаемо и редактируемо даже спустя неск. лет. Ок!

Далее, перед тем как это рисовать, выводить, обрабатывать и вообще юзать, это куда-то надо
записать. Да? Ну, вроде да. Думаем структуру данных. Иерархия данного объекта
привиделась мне такой:

Нажмите на изображение для увеличения
Название: Screenshot_4.jpg
Просмотров: 270
Размер:	60.1 Кб
ID:	528

Элемент меню, описуха, фишки, плюс массив элементов следующего уровня...

Ну, допустим. Сажусь кодить, имея в виду всяческие красивые рекурсивные вызовы и...
И тут не выходит ничего и получается такой вот уродец:

Код AS1/AS2:
// Ну понятно. Тут лежат эл-ты 1-го уровня. 
var my_menu:Array = new Array()
// Массив ИНДЕКСОВ... ох... вложенности уровней... бррр.....
// Типа Добавляем эл-т ур1.
//1,0,0
// Добавляем к нему суб уровень
//1,1,0
// Еще туда же
//1,2,0
// Добавляем суб к субу
//1,2,1
//Добавляем эл-т ур1.
//2,0,0.... как-то так....
 
var mindx:Array = new Array(0,0,0,0,0)
 
function addMenuIt( arr, DAT ){
 
	switch ( DAT.lvl ){
		case 0:
			// При обнаружении эл-та 1-го уровня обнуляем счетчики вложений всех последующих уровней
			mindx[ DAT.lvl+1 ] = 0 
			mindx[ DAT.lvl+2 ] = 0
			mindx[ DAT.lvl+3 ] = 0
			arr[ mindx[ 0 ] ] = new Object( DAT ) 
		break;
 
		case 1:
			var NODE = arr[ mindx[0]-1 ]
			// Аналогично, но кроме уровня родителя
			mindx[ DAT.lvl+1 ] = 0
			mindx[ DAT.lvl+2 ] = 0
			// Если блока данных субменю нет - создаем
			if ( NODE.SUB == undefined ) NODE.SUB = new Array()
			//И пихаем в него аналогичную ветку для последующих уровней меню
			NODE.SUB.push( DAT )
		break;
 
		case 2:
			var NODE = arr[ mindx[0]-1 ].SUB[ mindx[1]-1  ]
			// Аналогично! :)
			mindx[ DAT.lvl+1 ] = 0
			// Если блока данных субменю нет - создаем
			if ( NODE.SUB == undefined )	NODE.SUB = new Array()
			NODE.SUB.push( DAT )
		break;		
 
	}
	// Увеличиваем счетчик текущего уровня меню
	mindx[ DAT.lvl ] ++
}
Данный свитч реализует 3-уровневое меню.
По идее, кейс можно до посинения укопипащивать в бесконечность, путем дописывания
arr[ mindx[0]-1 ].SUB[ mindx[1]-1 ].SUB[ mindx[2]-1 ].SUB[ mindx[3]-1 ]....SUB[ mindx[N]-1 ]
Но, естественно, это безобразная гадость.

С рекурсивной отрисовкой полученного объекта, как ни странно, проблем не возникло.

Код AS1/AS2:
function buildMenu( DAT ){
	trace("■ buildMenu")
	var ct = 0
	var yPos = DAT.Y
	var dest = DAT.TARG.createEmptyMovieClip( "dest", DAT.TARG.getNextHighestDepth() )
 
	function addMItem( iDat, PT ){
		var obj = dest.attachMovie( "menu"+iDat.lvl,"menu"+ct, dest.getNextHighestDepth() )
			ct++
			obj._x = DAT.X + DAT.xShift*iDat.lvl
			obj._y = yPos
			obj.cap.text = PT+". "+iDat.dsc//+". "+ct
			yPos += 18
			if ( iDat.SUB != undefined )
			for ( var j=0; j<iDat.SUB.length; j++ ) addMItem( iDat.SUB[j], PT+"."+(j+1) )
	}
 
	for ( var i=0; i<DAT.STRUCT.length; i++ ){
		addMItem( DAT.STRUCT[i], (i+1) )
	}
}
Итак, ищу совета - как правильно реализовать рекурсивное ФОРМИРОВАНИЕ объекта данных?

menuBuilder2.swf   (128.3 Кб)

menuBuilder2.rar
Вложения
Тип файла: swf menuBuilder2.swf (128.3 Кб, 431 просмотров)
Всего комментариев 17

Комментарии

Старый 27.11.2015 12:32 Psycho Tiger вне форума
Psycho Tiger
 
Аватар для Psycho Tiger
Читал сквозь пальцы, за что извиняюсь, но увидел причину бед.
Плохая абстракция.

У тебя есть пункт меню. Он может содержать в себе другие пункты меню или быть конечным элементом.
Это вся структура данных, что тебе нужна.

Вложенный элемент на глубине 3 не должен отличаться от самого верхнего элемента.
Грубо говоря, класс выглядит как-то так (это псевдокод, потому что я не знаю версию языка используемого тобой и синтаксиса):
Код:
class MenuItem
   label:String;
   items:Array/* of MenuItem */
   depth:int;
end
Зачем нужно depth? Для получения стилей. С визуальной точки зрения вложенные элементы отличаются от тех, что наверху. Этот параметр не нужно задавать вручную, его должен создавать "билдер".

... я пытался написать псевдокод здесь, максимально похожий на AS1/AS2, но он был каким-то... непонятным. Поэтому потратил несколько минут и написал обход на ruby, потому что... а почему нет?


Изначальное дерево (элементы вложены в друг в друга, понятия "уровень" для них не существует.)
Код AS1/AS2:
menu_items = [
  {
    label: '1st Item'
  },
  {
    label: '2st Item',
    items: [
      {
        label: '2.1 item',
        items: [
          {
            label: '2.1.1 item'
          },
          {
            label: '2.1.2 item'
          }
        ]
      },
      {
        label: '2.2 item'
      }
    ]
  },
  {
    label: '3st item',
    items: [
      {
        label: '3.1. item'
      }
    ]
  }
]
Сам обход (посыпал комментариями).
Код AS1/AS2:
# объявление функции build
def build(root, depth = 0)
  # итерирую по коллекции (массиву) root. menu_item это элемент массива внутри итерационного блока
  root.each do |menu_item|
    # умножить строку на число = повторить эту строку N раз и добавляю label, вывожу на экран
    puts '* ' * depth + menu_item[:label]
    # проверяю, если ли у элемента внутренние элементы
    if menu_item.has_key?(:items)
      # повторяю процесс для них
      build(menu_item[:items], depth + 1)
    end
  end
end
Запуская
Код:
build menu_items
Получаю менюшку.
Код:
1st Item
2st Item
* 2.1 item
* * 2.1.1 item
* * 2.1.2 item
* 2.2 item
3st item
* 3.1. item
Старый 27.11.2015 13:28 dark256 вне форума
dark256
 
Аватар для dark256
Хм. С ОТРИСОВКОЙ (построением) самого меню, со стилями и прочим как раз проблем и не возникло
Проблема построения древа ДАННЫХ, структуры описания.

Вместо
Код:
menu_items = [
  {
    label: '1st Item'
  },
  {
    label: '2st Item',
    items: [
...
Я хочу использовать
Код:
addMenuIt( my_menu, { lvl:0, dsc:"История операций, выписки", lvl:0 } )
Что выглядит нагляднее и удобнее для редактирования меню.
Представь - я просто переношу addMenuIt из одной строчки в другую и правлю индекс уровня, и пункт меню переносится на любой уровень и в любую порядковую позицию.
Без модифкации рукописных индексов "2.1.2" и т.п.

В твоем случае - надо вручную модифицировать дерево описания, путаясь в скобках, запятых, идентификаторах и прочем. Что неудобно и чего я и хочу избегать в дальнейшем...
В моем случае всё описание меню идеально выносится в линейную ХМЛ без узлов и торжественно вручается юзеру для дальнейших сколь угодно злобных извращений.
В твоем случае - юзеру будет ооочень сложно разбираться в древовидной системе.
Старый 27.11.2015 14:57 callme вне форума
callme
 
Аватар для callme
А если вот так хранить меню?

Первый пункт
Второй пункт
*Вложенный пункт
**Вложенный во вложенный
**Второй вложенный во вложенный
*Второй вложенный
Третий пункт
Старый 27.11.2015 15:31 dark256 вне форума
dark256
 
Аватар для dark256
Перед тем как передавать модель контроллеру и вьюэру, её надо построить
Проблема именно с построением модели, то есть блока данных.
Вопрос именно по грамотному построению данных в блоке addMenuIt
Старый 27.11.2015 15:45 GBee вне форума
GBee
 
Аватар для GBee
Дарк, но Тигра прав, так удобнее и не надо переписывать код. Можно брать деревяху снаружи, можно деревяху захардкодить, но главное, чтобы она не была с кодом вперемешку. Если тебе не нравится древовидная структура самого документа, то элементы можно задать списком, указывая их родителя. Я бы к Тигровым еще добавил уникальный ид, чтобы обрабатывать переходы в меню.
Старый 27.11.2015 15:47 callme вне форума
callme
 
Аватар для callme
А что если при вызове addMenuIt каждый раз перестраивать меню с нуля? Тогда пробегаешься сначала по пунктам, у которых lvl=0, потом по пунктам lvl=1, и так далее, пока не останется пунктов.
Старый 27.11.2015 15:52 dark256 вне форума
dark256
 
Аватар для dark256
Код:
Дарк, но Тигра прав, так удобнее и не надо переписывать код.
Как раз в случае дерева Тигра - код (дерева) именно надо переписывать.
У меня просто строчки addMenuIt меняются местами.
Мне надо ПОСТРОИТЬ дерево. И я его таки выстроил, но коряво. В улучшении чего и требуется помощь

Код:
А что если при вызове addMenuIt каждый раз перестраивать меню с нуля
А это мысль.... некторый препроцессинг..... Надо подумать.....
Старый 27.11.2015 15:54 GBee вне форума
GBee
 
Аватар для GBee
Погоди addMenuIt это что не функция в коде что ли? Строчки меняются. Короче добавляй ИД и парентИД. И будет тебе вообще все списком.
Старый 27.11.2015 16:50 dark256 вне форума
dark256
 
Аватар для dark256
Ну смотри.... Общая идея и задача - максимально простой и интуитивно понятный генератор структуры данных.
Если следовать указаниям тебя и Тигры: я прописал все уровень-, парент- и суб- айди.
Работает. Показал клиенту........... Он репу почесал и предложил переставить пункты меню местами.
На коленке, быстро за 6 сек. Потом еще раз. Потом опять.
Переписывать переиндексировать переделывать блок данных? И переподписывать лэйблы с пересчитыванием буллетов?

Я же просто меняю местами addMenuIt и:
Было:
http://dark256.space/HALK/S1.JPG
Стало:
http://dark256.space/HALK/S2.JPG

Программирования в данном случае как такого не производится
Обновил(-а) dark256 27.11.2015 в 17:10
Старый 27.11.2015 18:57 GBee вне форума
GBee
 
Аватар для GBee
Да тебе даже пересобирать не надо будет, просто текстовый файлик рядом поправил и все - красота. А так сажаешь клиента на твою поддержку.
Старый 27.11.2015 20:09 dark256 вне форума
dark256
 
Аватар для dark256
Подсадить клиента на суппорт - дело вполне себе благое,
ибо клоузсорсами да цмс рукпоисными строим мы светлое будущее себе.
Аминь.

В целом - всё меню легко выносится в ХМЛ и клиент благословенен будет.
Просто в данном случае я весь констракт в одном коде уместил, дабы отлаживать и изощряться проще было.
Старый 27.11.2015 23:03 Psycho Tiger вне форума
Psycho Tiger
 
Аватар для Psycho Tiger
Гм, у меня – древовидная структура в JSON'e, ты хочешь потом перейти на XML.
Не совсем понимаю в чем разница )

Проблема была исключительно в лучшем варианте построения? Тогда, действительно, XML будет самое оное – там есть много синтаксических и семантических проверок на уровне языка, что хорошо. Этих проверок, наверное, в твоей самописной реализации нет.
Старый 28.11.2015 00:56 dark256 вне форума
dark256
 
Аватар для dark256
Ох.... не могу я донести свою мысль до умов... не могу......
Я спросил - как рекурсивно или как еще сгенерить структуру данных описания....
Выждем.....
Старый 28.11.2015 10:49 Psycho Tiger вне форума
Psycho Tiger
 
Аватар для Psycho Tiger
Как из твоего примера с addMentIt сделать структуру данных? Это императивный подход, поэтому здесь без parent'a не обойтись.
Фигачу прямо здесь наугад.
Код AS1/AS2:
var root = {items: []}
var currentLevel = 1;
var currentItem = root;
function addMenuIt(my_menu, data) { 
   if (data.lvl == currentLevel){
      currentItem.items.push({desc: data.desc, parent: currentItem})
   }
   else if (data.lvl == currentLevel + 1){
      tmp = {desc: data.desc, items: [], parent: currentItem}
      currentItem.items.push(tmp);
      currentItem = tmp;
   }
   else if (data.lvl < currentLevel){
      while (data.lvl != currentLevel){
           currentItem = currentItem.parent;
           currentLevel -=1;
      }
      currentItem.items.push({desc: data.desc, parent: currentItem})
   }
   else {
       throw new Error('Can not jump more than 1 level up');
   }
}
Старый 28.11.2015 13:14 callme вне форума
callme
 
Аватар для callme
Написал вроде то, что тебе нужно

Код AS3:
package
{
    import flash.display.Sprite;
    import flash.events.Event;
 
    public class Main extends Sprite 
    {
        public function Main() 
        {
            var data:Array = [
                { lvl:0, dsc:"История операций, выписки" },
                    { lvl:1, dsc:"История операций", par:function():void { } },
                    { lvl:1, dsc:"Выписка по карточному счету", par:function():void { } },
                    { lvl:1, dsc:"История заявлений", par:function():void { } },
                { lvl:0, dsc:"Карты и счета" },
                    { lvl:1, dsc:"Карты и счета" },
                        { lvl:2, dsc:"Услуги", par:function():void { } },
                        { lvl:2, dsc:"Информация", par:function():void { } },
                        { lvl:2, dsc:"Карты (Если выбран карточный счет)", par:function():void { } },
                        { lvl:2, dsc:"Платежные реквизиты", par:function():void { } },
                    { lvl:1, dsc:"Заявления", par:function():void { } },
                        { lvl:2, dsc:"Заявка на кредит", par:function():void { } },
                        { lvl:2, dsc:"Заявка на кредитную карту", par:function():void { } }
 
            ];
 
            printMenu(buildTree(data));
        }
 
        private function buildTree(data:Array):Object 
        {
            var result:Object = { };
 
            var nesting:Array = [];
 
            nesting[0] = result;
 
            for (var i:uint = 0; i < data.length; i++) 
            {
                var item:Object = clone(data[i]);
 
                var parent:Object = nesting[item.lvl];
 
                if (!parent.hasOwnProperty('children'))
                    parent.children = [];
 
                parent.children.push(item);
 
                nesting[item.lvl + 1] = item;
            }
 
            return result;
        }
 
        private function clone(input:Object):Object
        {
            var output:Object = { };
 
            for (var name:String in input)
                output[name] = input[name];
 
            return output;
        }
 
        private function printMenu(tree:Object, depth:int = 0):void
        {
            var s:String = '';
 
            for (var j:int = 0; j < depth; j++) 
            {
                s += '*';
            }
 
            var array:Array = tree.children;
 
            if (array == null) return;
 
            var len:int = array.length;
 
            for (var i:int = 0; i < len; i++) 
            {
                trace(s + array[i].dsc);
                printMenu(array[i], depth + 1);
            }
 
        }
    }
}
На выходе из функции buildTree() получаем такой объект

Код AS3:
var tree:Object = 
{
    children:
    [
        {
            lvl:0,
            dsc: "История операций, выписки",
            children: [
                {
                    lvl:1,
                    dsc: "История операций",
                    par: function():void { } 
                },
                {
                    lvl:1,
                    dsc: "Выписка по карточному счету",
                    par: function():void { } 
                },
                {
                    lvl:1,
                    dsc: "История заявлений",
                    par: function():void { }
                }
            ]
        },
        {
            lvl:0,
            dsc: "Карты и счета",
            children: [
                {
                    lvl:1,
                    dsc: "Карты и счета",
                    children: [
                        {
                            lvl:2,
                            dsc: "Услуги",
                            par: function():void { } 
                        },
                        {
                            lvl:2,
                            dsc: "Информация",
                            par: function():void { } 
                        },
                        {
                            lvl:2,
                            dsc: "Карты (Если выбран карточный счет)",
                            par: function():void { } 
                        },
                        {
                            lvl:2,
                            dsc: "Платежные реквизиты",
                            par: function():void { } 
                        }
                    ]
                },
                {
                    lvl:1,
                    dsc: "Заявления",
                    children: [
                        {
                            lvl:2,
                            dsc: "Заявка на кредит",
                            par: function():void { } 
                        },
                        {
                            lvl:2,
                            dsc: "Заявка на кредитную карту",
                            par: function():void { } 
                        }
                    ]
                }
            ]
        }
    ]
}
На выходе из функции printMenu получаем
Цитата:
История операций, выписки
*История операций
*Выписка по карточному счету
*История заявлений
Карты и счета
*Карты и счета
**Услуги
**Информация
**Карты (Если выбран карточный счет)
**Платежные реквизиты
*Заявления
**Заявка на кредит
**Заявка на кредитную карту
Старый 09.12.2015 21:34 dimarik вне форума
dimarik
 
Аватар для dimarik
Обычный шаблон Composite. И Тигра умница по поводу depth. Впрочем, как обычно. Кору не читал, наверное опять грубит.
Старый 10.12.2015 15:03 in4core вне форума
in4core
 
Аватар для in4core
dimarik - чей то грублю то сразу? ))) Наоборот было весело)
 

 


Часовой пояс GMT +4, время: 23:47.


Copyright © 1999-2008 Flasher.ru. All rights reserved.
Работает на vBulletin®. Copyright ©2000 - 2019, Jelsoft Enterprises Ltd. Перевод: zCarot
Администрация сайта не несёт ответственности за любую предоставленную посетителями информацию. Подробнее см. Правила.