Дерево значений 1С — универсальная коллекция, в которой у каждой строки могут быть свои подчинённые строки. Колонки общие для всех уровней, глубина вложенности не ограничена. Ниже — как создать дерево программно, добавить колонки, корневые и подчинённые строки, заполнить его из плоского списка и из запроса, обойти циклом и рекурсией, а также методы поиска, сортировки и итогов с примерами кода для платформы 8.3.
С деревом сталкиваются в нескольких типовых задачах:
ИТОГИ выгружают сразу в иерархию;Объект ДеревоЗначений доступен на сервере, в толстом клиенте и во внешнем соединении. В тонком клиенте и веб-клиенте такого типа нет: там с деревом работают через реквизит формы, об этом — в конце статьи.
Если вы работали с таблицей значений, большая часть приёмов покажется знакомой: те же колонки с типами, те же Добавить(), Найти(), Итог(). Разница в том, что строки хранятся не одним списком, а вложенными коллекциями.
| Таблица значений | Дерево значений | |
|---|---|---|
| Строки | один плоский список | у каждой строки своя коллекция Строки |
| Связь с родителем | нет | свойство Родитель, метод Уровень() |
| Поиск | по всей таблице, можно ускорить индексами | по одному уровню или по всем с флагом «включать подчинённые», индексов нет |
Свёртка Свернуть() | есть | нет |
| Источник данных в запросе | можно передать параметром | нельзя, сначала развернуть в таблицу |
| На форме | обычный список строк | список с раскрытием групп |
Если в данных нет вложенности, берите таблицу значений: у неё есть индексы и свёртка. Дерево нужно, когда иерархия — часть самих данных.
Дерево создаётся конструктором Новый ДеревоЗначений, затем в коллекцию Колонки добавляют колонки. У метода Добавить() четыре необязательных параметра: имя, тип (ОписаниеТипов), заголовок и ширина в символах.
Дерево = Новый ДеревоЗначений;
Дерево.Колонки.Добавить("Код",
Новый ОписаниеТипов("Строка", , Новый КвалификаторыСтроки(20)), "Код");
Дерево.Колонки.Добавить("Наименование",
Новый ОписаниеТипов("Строка", , Новый КвалификаторыСтроки(150)), "Статья");
Дерево.Колонки.Добавить("Сумма",
Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 2)), "Сумма");
Дерево.Колонки.Добавить("Объект"); // без типа: примет значение любого типа
// Проверка, что колонка есть, перед обращением к ней
ЕстьКолонкаСумма = Дерево.Колонки.Найти("Сумма") <> Неопределено;
Если группам и элементам нужны разные поля, заводят общий набор колонок, а лишние оставляют пустыми.
Тип указывать не обязательно, но лучше указать. Типизированная колонка приводит значение при присваивании: в числовую колонку не попадёт строка, а в новой строке там будет 0, а не Неопределено. От этого зависят Итог() и сортировка. Имя колонки подчиняется тем же правилам, что имя переменной: буквы, цифры и подчёркивание, без пробелов, первой не может быть цифра.
Строки верхнего уровня добавляют в Дерево.Строки, подчинённые — в коллекцию Строки нужной строки. Метод Добавить() возвращает новую строку, в неё сразу записывают значения колонок.
Доходы = Дерево.Строки.Добавить();
Доходы.Код = "1";
Доходы.Наименование = "Доходы";
Строка = Доходы.Строки.Добавить();
Строка.Код = "1.1";
Строка.Наименование = "Выручка от продаж";
Строка.Сумма = 150000;
Строка = Доходы.Строки.Добавить();
Строка.Код = "1.2";
Строка.Наименование = "Прочие доходы";
Строка.Сумма = 12000;
// Сумма группы = сумма её строк (только ближайший уровень)
Доходы.Сумма = Доходы.Строки.Итог("Сумма");
// Новая строка первой в верхнем уровне
Расходы = Дерево.Строки.Вставить(0);
Расходы.Наименование = "Расходы";
К значению колонки обращаются через точку (Строка.Сумма), по имени в квадратных скобках (Строка["Сумма"]) или по индексу колонки (Строка[2]). Скобки нужны, когда имя колонки хранится в переменной. Кроме колонок, у строки есть служебные свойства и методы:
Строка.Строки — коллекция подчинённых строк;Строка.Родитель — вышестоящая строка, у строк верхнего уровня — Неопределено;Строка.Уровень() — номер уровня, у верхнего — 0;Строка.Владелец() — само дерево.Частая практическая задача: данные приходят плоским списком, где у записи есть свой код и код родителя, — из внешней системы, файла или таблицы значений. Каждую запись нужно положить под её родителя, при этом родитель может встретиться в списке позже потомка. Для этого удобно держать соответствие «код → строка дерева» и создавать цепочку родителей рекурсивно. На входе — массив структур с ключами Код, КодРодителя, Наименование, Сумма; функция НовоеДеревоСтатей() создаёт пустое дерево с колонками, как в первом примере (полный модуль — в конце статьи).
Функция ДеревоИзСписка(Список) Экспорт
Дерево = НовоеДеревоСтатей();
ДанныеПоКоду = Новый Соответствие;
Для Каждого Запись Из Список Цикл
ДанныеПоКоду.Вставить(Запись.Код, Запись);
КонецЦикла;
СтрокиПоКоду = Новый Соответствие;
Для Каждого Запись Из Список Цикл
СтрокаДляКода(Дерево, Запись.Код, ДанныеПоКоду, СтрокиПоКоду);
КонецЦикла;
Возврат Дерево;
КонецФункции
Функция СтрокаДляКода(Дерево, Код, ДанныеПоКоду, СтрокиПоКоду)
Строка = СтрокиПоКоду.Получить(Код);
Если Строка = Null Тогда
ВызватьИсключение "Цикл в иерархии: элемент с кодом " + Код + " оказался предком самого себя";
ИначеЕсли Строка <> Неопределено Тогда
Возврат Строка;
КонецЕсли;
СтрокиПоКоду.Вставить(Код, Null); // метка: строка сейчас создаётся
Запись = ДанныеПоКоду.Получить(Код);
СтрокаРодителя = Неопределено;
Если ЗначениеЗаполнено(Запись.КодРодителя)
И ДанныеПоКоду.Получить(Запись.КодРодителя) <> Неопределено Тогда
СтрокаРодителя = СтрокаДляКода(Дерево, Запись.КодРодителя, ДанныеПоКоду, СтрокиПоКоду);
КонецЕсли;
Если СтрокаРодителя = Неопределено Тогда
Строка = Дерево.Строки.Добавить();
Иначе
Строка = СтрокаРодителя.Строки.Добавить();
КонецЕсли;
ЗаполнитьЗначенияСвойств(Строка, Запись);
СтрокиПоКоду.Вставить(Код, Строка);
Возврат Строка;
КонецФункции
Что здесь важно:
ДанныеПоКоду даёт запись родителя по коду без повторного перебора списка;СтрокиПоКоду хранит уже созданные строки, поэтому каждая запись попадает в дерево ровно один раз, в каком бы порядке ни шёл список;Null ставится до рекурсивного вызова. Если в данных есть цикл, функция выбросит понятное исключение вместо бесконечной рекурсии;ЗаполнитьЗначенияСвойств() копирует только поля с совпадающими именами, поэтому лишний ключ КодРодителя ошибки не вызовет.Если данные берутся из базы, дерево можно не строить вручную. Запрос с разделом ИТОГИ ПО выгружается в дерево одной строкой: каждая группировка становится уровнем, а в строках групп уже лежат итоги. Имена регистра и полей в примере условные.
// Сервер: в тонком клиенте и веб-клиенте объект Запрос недоступен.
Функция ПродажиПоКонтрагентам(НачалоПериода, КонецПериода) Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Продажи.Контрагент КАК Контрагент,
| Продажи.Номенклатура КАК Номенклатура,
| Продажи.СуммаОборот КАК Сумма
|ИЗ
| РегистрНакопления.Продажи.Обороты(&НачалоПериода, &КонецПериода) КАК Продажи
|ИТОГИ
| СУММА(Сумма)
|ПО
| Контрагент";
Запрос.УстановитьПараметр("НачалоПериода", НачалоПериода);
Запрос.УстановитьПараметр("КонецПериода", КонецПериода);
Возврат Запрос.Выполнить().Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам);
КонецФункции
В полученном дереве верхний уровень — контрагенты с общей суммой, под каждым — детальные записи по номенклатуре. Колонки создаются автоматически по полям запроса. В строках групп поля, по которым группировки нет (здесь Номенклатура), содержат NULL — учитывайте это при выводе.
Для иерархического справочника добавьте в итоги слово ИЕРАРХИЯ (ПО Номенклатура ИЕРАРХИЯ) и выгружайте с параметром ОбходРезультатаЗапроса.ПоГруппировкамСИерархией: группы справочника станут отдельными уровнями дерева.
Для Каждого Строка Из Дерево.Строки перебирает только верхний уровень. Чтобы пройти все строки, процедуру вызывают рекурсивно для коллекции Строки каждой строки. Пример — пересчёт сумм групп снизу вверх: функция возвращает сумму коллекции, а строка-группа получает сумму своих подчинённых.
Функция ПосчитатьИтоги(КоллекцияСтрок) Экспорт
Всего = 0;
Для Каждого Строка Из КоллекцияСтрок Цикл
Если Строка.Строки.Количество() > 0 Тогда
Строка.Сумма = ПосчитатьИтоги(Строка.Строки);
КонецЕсли;
Всего = Всего + Строка.Сумма;
КонецЦикла;
Возврат Всего;
КонецФункции
// Вызов: ВсегоПоДереву = ПосчитатьИтоги(Дерево.Строки);
Групповые строки перезаписываются, листовые остаются как были. Второй типовой обход — развернуть дерево в таблицу значений с колонкой уровня:
Функция ДеревоВТаблицу(Дерево) Экспорт
Таблица = Новый ТаблицаЗначений;
Для Каждого Колонка Из Дерево.Колонки Цикл
Таблица.Колонки.Добавить(Колонка.Имя, Колонка.ТипЗначения, Колонка.Заголовок);
КонецЦикла;
Таблица.Колонки.Добавить("Уровень", Новый ОписаниеТипов("Число"));
ДобавитьСтрокиВТаблицу(Дерево.Строки, Таблица);
Возврат Таблица;
КонецФункции
Процедура ДобавитьСтрокиВТаблицу(КоллекцияСтрок, Таблица)
Для Каждого Строка Из КоллекцияСтрок Цикл
НоваяСтрока = Таблица.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, Строка);
НоваяСтрока.Уровень = Строка.Уровень();
ДобавитьСтрокиВТаблицу(Строка.Строки, Таблица);
КонецЦикла;
КонецПроцедуры
Строки попадают в таблицу в том же порядке, в каком их видно в раскрытом дереве: группа, затем её содержимое. Без рекурсии обход делают через массив-стек необработанных коллекций — пригодится, если глубина дерева заранее неизвестна и может быть большой:
Стек = Новый Массив;
Стек.Добавить(Дерево.Строки);
ВсегоСтрок = 0;
Пока Стек.Количество() > 0 Цикл
Коллекция = Стек[Стек.ВГраница()];
Стек.Удалить(Стек.ВГраница());
Для Каждого Строка Из Коллекция Цикл
ВсегоСтрок = ВсегоСтрок + 1;
Если Строка.Строки.Количество() > 0 Тогда
Стек.Добавить(Строка.Строки);
КонецЕсли;
КонецЦикла;
КонецЦикла;
Порядок обхода здесь не совпадает с экранным: для подсчёта и поиска это неважно, для вывода используйте рекурсию.
Основные методы вызываются не у самого дерева, а у коллекции строк — Дерево.Строки или Строка.Строки. По умолчанию они работают только на своём уровне.
| Метод | Что делает |
|---|---|
Добавить(), Вставить(Индекс) | новая строка в конце или на заданной позиции |
Найти(Значение, Колонки, ВключатьПодчиненные) | первая строка с таким значением или Неопределено |
НайтиСтроки(Отбор, ВключатьПодчиненные) | массив строк, подходящих под структуру отбора |
Сортировать(Колонки, ВключатьПодчиненные, ОбъектСравнения) | сортировка уровня или всех уровней |
Итог(Колонка, ВключатьПодчиненные) | сумма числовых значений колонки |
Количество(), Индекс(Строка) | число строк на уровне, позиция строки |
Удалить(Строка), Очистить() | удаление строки вместе с подчинёнными, очистка уровня |
Сдвинуть(Строка, Смещение) | перемещение строки внутри уровня |
ВыгрузитьКолонку(Колонка) | массив значений колонки по строкам уровня |
// Поиск по всем уровням: третий параметр Истина
Строка = Дерево.Строки.Найти("1.2", "Код", Истина);
Если Строка <> Неопределено Тогда
УровеньНайденной = Строка.Уровень();
КонецЕсли;
// Все строки с нулевой суммой на любом уровне
НулевыеСтроки = Дерево.Строки.НайтиСтроки(Новый Структура("Сумма", 0), Истина);
// Каждый уровень: по убыванию суммы, затем по наименованию
Дерево.Строки.Сортировать("Сумма Убыв, Наименование", Истина);
// Итог верхнего уровня и итог по всем строкам всех уровней
ИтогВерхнегоУровня = Дерево.Строки.Итог("Сумма");
ИтогВсехСтрок = Дерево.Строки.Итог("Сумма", Истина);
С Итог(..., Истина) осторожнее: в сумму попадают и групповые строки, поэтому при уже посчитанных группах итог задвоится. Отбор для НайтиСтроки() задаётся структурой: ключ — имя колонки, значение — искомое значение.
Удалять строки внутри Для Каждого нельзя: коллекция сдвигается, и часть строк пропускается. Обходите уровень с конца по индексу:
Процедура УдалитьНулевыеСтроки(КоллекцияСтрок) Экспорт
Индекс = КоллекцияСтрок.Количество() - 1;
Пока Индекс >= 0 Цикл
Строка = КоллекцияСтрок[Индекс];
УдалитьНулевыеСтроки(Строка.Строки);
Если Строка.Строки.Количество() = 0 И Строка.Сумма = 0 Тогда
КоллекцияСтрок.Удалить(Строка);
КонецЕсли;
Индекс = Индекс - 1;
КонецЦикла;
КонецПроцедуры
Сначала чистятся вложенные уровни, потом проверяется сама строка, поэтому группа, у которой не осталось подчинённых, тоже удаляется.
На форме создают реквизит типа ДеревоЗначений, задают ему колонки и перетаскивают в элементы — получается таблица формы с отображением «Дерево». В модуле формы и на клиенте, и в серверных процедурах реквизит — не дерево значений, а ДанныеФормыДерево: вместо коллекции Строки у него метод ПолучитьЭлементы(), вместо свойства Родитель — метод ПолучитьРодителя(). Поиска по значениям, итогов и сортировки там нет; есть только НайтиПоИдентификатору(). Поэтому на сервере реквизит преобразуют в настоящее дерево через РеквизитФормыВЗначение(), обрабатывают и возвращают через ЗначениеВРеквизитФормы().
// Модуль формы. У реквизита ДеревоСтатей (тип ДеревоЗначений)
// в редакторе формы созданы колонки Наименование и Сумма.
&НаСервере
Процедура ЗаполнитьДеревоНаСервере()
Дерево = РеквизитФормыВЗначение("ДеревоСтатей");
Дерево.Строки.Очистить();
Доходы = Дерево.Строки.Добавить();
Доходы.Наименование = "Доходы";
Строка = Доходы.Строки.Добавить();
Строка.Наименование = "Выручка от продаж";
Строка.Сумма = 150000;
Доходы.Сумма = Доходы.Строки.Итог("Сумма");
ЗначениеВРеквизитФормы(Дерево, "ДеревоСтатей");
КонецПроцедуры
&НаКлиенте
Процедура Заполнить(Команда)
ЗаполнитьДеревоНаСервере();
Для Каждого Элемент Из ДеревоСтатей.ПолучитьЭлементы() Цикл
Элементы.ДеревоСтатей.Развернуть(Элемент.ПолучитьИдентификатор(), Истина);
КонецЦикла;
КонецПроцедуры
РеквизитФормыВЗначение() возвращает настоящий ДеревоЗначений с колонками реквизита, ЗначениеВРеквизитФормы() кладёт результат обратно. Если дерево строит функция общего модуля, имена её колонок должны совпадать с колонками реквизита. Разворачивают группы на клиенте: метод Развернуть() есть у элемента формы, а не у данных, второй параметр Истина раскрывает все вложенные уровни.
Новый ДеревоЗначений написан в процедуре с директивой &НаКлиенте. Перенесите код в серверную процедуру.(Строки) в модуле формы значит, что к ДанныеФормыДерево обратились как к дереву значений — нужен ПолучитьЭлементы() или РеквизитФормыВЗначение().Найти() возвращает Неопределено, хотя строка есть. Без третьего параметра поиск идёт только по текущему уровню. Так же ведут себя НайтиСтроки(), Сортировать() и Итог().Для Каждого по Дерево.Строки не заходит во вложенные уровни — нужна рекурсия. Если строки при этом удаляются, часть пропускается: обходите с конца.Итог(..., Истина) по дереву, где группы уже содержат суммы подчинённых.Итог() «не видит» значения. Колонка создана без типа, и в неё записали числа строками. Итог() учитывает только числовые значения.РодительСсылка.Пока по родителям построение просто не заканчивается. Проверяйте циклы, как в функции СтрокаДляКода().Строка.Уровень() возвращает номер уровня, у верхнего — 0. Строка.Родитель возвращает вышестоящую строку, у верхнего уровня — Неопределено. Путь до корня получают циклом: пока Родитель заполнен, поднимаетесь вверх.
Готового метода нет. Дерево в таблицу разворачивают рекурсией (функция ДеревоВТаблицу() выше), таблицу в дерево собирают через соответствие «код → строка», как в ДеревоИзСписка(). Если нужна группировка по значениям колонок, таблицу передают параметром, сначала помещают во временную таблицу, а затем вторым запросом пакета выбирают данные с ИТОГИ:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Т.Контрагент КАК Контрагент,
| Т.Сумма КАК Сумма
|ПОМЕСТИТЬ ВТДанные
|ИЗ
| &Таблица КАК Т
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТДанные.Контрагент КАК Контрагент,
| ВТДанные.Сумма КАК Сумма
|ИЗ
| ВТДанные КАК ВТДанные
|ИТОГИ
| СУММА(Сумма)
|ПО
| Контрагент";
Запрос.УстановитьПараметр("Таблица", Таблица);
Дерево = Запрос.Выполнить().Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам);
Колонки таблицы значений в таком запросе должны быть типизированы.
Нет. Источником в конструкции ИЗ &Параметр может быть только таблица значений, и выбрать её можно только во временную таблицу через ПОМЕСТИТЬ. Разверните дерево в таблицу, передайте её параметром, поместите во временную таблицу и дальше работайте с ней в следующих запросах пакета.
Дерево.Скопировать() возвращает независимую копию со всеми колонками и строками. Если нужна одна ветка, создайте новое дерево с теми же колонками и перенесите строки рекурсией через ЗаполнитьЗначенияСвойств().
Если дерево нужно для отчёта или обработки с группировками, отметками и выгрузкой, а разбираться с рекурсией и формой некогда, такую доработку можно поручить исполнителю. Подробнее — на странице доработки отчётов и обработок 1С. Разместите задачу — оценка бесплатная, ответ в течение рабочего дня.
Станьте частью сообщества!
Войдите или зарегистрируйтесь, и вы сможете участвовать в обсуждениях.