Как перебрать все строки дерева значений в 1С и не сломать иерархию
Содержание
Чтобы обойти дерево значений в 1С целиком, переберите Дерево.Строки циклом Для Каждого и для каждой строки рекурсивно вызовите ту же процедуру с её коллекцией Строка.Строки. Обычный цикл по Дерево.Строки видит только верхний уровень, а вложенные строки остаются нетронутыми.
Типичная ситуация: запрос с ИТОГИ ПО Контрагент, Договор выгрузили в дерево, отчёт печатает только контрагентов, а договоры и документы пропали. Причина почти всегда одна: правильно обойти дерево значений нужно по уровням, а код перебирает одну коллекцию. Ниже — рабочие приёмы с кодом: перебор, поиск, итоги, изменение структуры и работа с деревом на управляемой форме.
Как устроено дерево значений: строки, колонки, родители
У ДеревоЗначений два главных свойства:
- 📋
Колонки— одна коллекция колонок на всё дерево. Колонки общие для всех уровней. - 🌳
Строки— коллекция строк верхнего уровня (КоллекцияСтрокДереваЗначений).
Каждая строка (СтрокаДереваЗначений) хранит значения колонок и ещё три вещи, которые нужны при обходе:
| Что | Что возвращает |
|---|---|
Строка.Строки | Коллекцию подчинённых строк, такую же, как Дерево.Строки |
Строка.Родитель | Строку-родителя; у верхнего уровня — Неопределено |
Строка.Уровень() | Номер уровня, верхний уровень — 0 |
Ключевая мысль: коллекции строк вложены друг в друга. Поэтому любой полный обход — это либо рекурсия, либо собственный стек.
Дерево создаётся двумя способами. Вручную:
Дерево = Новый ДеревоЗначений;
Дерево.Колонки.Добавить("Наименование", Новый ОписаниеТипов("Строка"));
Дерево.Колонки.Добавить("Сумма", Новый ОписаниеТипов("Число"));
Группа = Дерево.Строки.Добавить();
Группа.Наименование = "Поставщики";
Строка = Группа.Строки.Добавить();
Строка.Наименование = "ООО Ромашка";
Строка.Сумма = 15000;
Или из запроса с итогами:
Результат = Запрос.Выполнить();
Дерево = Результат.Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам);
Задавайте колонкам тип через ОписаниеТипов. Без типа колонка хранит что угодно, и сложение Неопределено + 100 упадёт с ошибкой, а метод Итог() для такой колонки не сработает.
ДеревоЗначений — серверный объект. На клиенте тонкого клиента
его нет, там работают с данными формы (об этом — в отдельном разделе ниже).
Перебор всех строк дерева значений без учёта уровней
Иногда иерархия не важна: нужно пройти каждую строку и, например, пересчитать цену или проверить заполнение. Рекурсия здесь тоже подходит, но есть способ без неё — собственный стек на массиве.
Функция ВсеСтрокиДерева(Дерево)
Результат = Новый Массив;
Стек = Новый Массив;
Стек.Добавить(Дерево.Строки);
Пока Стек.Количество() > 0 Цикл
Коллекция = Стек[Стек.ВГраница()];
Стек.Удалить(Стек.ВГраница());
Для Каждого Строка Из Коллекция Цикл
Результат.Добавить(Строка);
Если Строка.Строки.Количество() > 0 Тогда
Стек.Добавить(Строка.Строки);
КонецЕсли;
КонецЦикла;
КонецЦикла;
Возврат Результат;
КонецФункции
Функция возвращает плоский массив всех строк. Дальше с ним работают обычным циклом:
Для Каждого Строка Из ВсеСтрокиДерева(Дерево) Цикл
Строка.Сумма = Окр(Строка.Сумма * 1.1, 2);
КонецЦикла;
Когда этот вариант лучше рекурсии:
- 🔁 дерево очень глубокое, и рекурсия рискует переполнить стек встроенного языка;
- 📦 строки нужно обработать несколько раз — массив собрали один раз и переиспользуете;
- 🧩 обработку удобно вынести в другой модуль и передать туда готовый массив.
Порядок строк в массиве не совпадает с порядком в дереве: сначала идёт верхний уровень, потом ветки в обратном порядке. Если важен порядок «родитель, затем его дети», используйте рекурсию из следующего раздела.
Рекурсивный обход дерева значений с сохранением порядка
Рекурсия даёт естественный порядок: родитель, все его потомки, следующий родитель. Так дерево выглядит на экране, так его и печатают.
Процедура ОбойтиСтроки(КоллекцияСтрок)
Для Каждого Строка Из КоллекцияСтрок Цикл
// Обработка текущей строки
Сообщить(СтрШаблон("Уровень %1: %2", Строка.Уровень(), Строка.Наименование));
// Спуск на уровень ниже
ОбойтиСтроки(Строка.Строки);
КонецЦикла;
КонецПроцедуры
// Вызов
ОбойтиСтроки(Дерево.Строки);
Процедура принимает именно коллекцию строк, а не дерево. Поэтому один и тот же код работает и для верхнего уровня (Дерево.Строки), и для любой ветки (Строка.Строки).
Обработка «после детей»
Если результат родителя зависит от потомков (итоги, проверка «все ли дети заполнены»), переставьте вызов: сначала рекурсия, потом обработка строки. Это обход в обратном порядке — снизу вверх.
Для Каждого Строка Из КоллекцияСтрок Цикл
ОбойтиСтроки(Строка.Строки);
// здесь все потомки строки уже обработаны
КонецЦикла;
Обход только до нужной глубины
Передайте в процедуру предельный уровень и не спускайтесь глубже:
Процедура ОбойтиДоУровня(КоллекцияСтрок, МаксУровень)
Для Каждого Строка Из КоллекцияСтрок Цикл
// обработка
Если Строка.Уровень() < МаксУровень Тогда
ОбойтиДоУровня(Строка.Строки, МаксУровень);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
С МаксУровень = 1 обработаются строки уровней 0 и 1, например, контрагенты и их договоры без документов.
Обход дерева из запроса без выгрузки
Если дерево нужно только для чтения, его можно не создавать. Выборка по группировкам обходит результат запроса так же иерархично и расходует меньше памяти:
ВыборкаКонтрагенты = Результат.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаКонтрагенты.Следующий() Цикл
ВыборкаДетали = ВыборкаКонтрагенты.Выбрать();
Пока ВыборкаДетали.Следующий() Цикл
// строки внутри контрагента
КонецЦикла;
КонецЦикла;
Поиск строки в дереве значений: Найти и НайтиСтроки
Писать свою рекурсию ради поиска не нужно. У коллекции строк есть два метода, и оба умеют искать по всем уровням.
| Метод | Что вернёт | Когда применять |
|---|---|---|
Найти(Значение, Колонки, ВключатьПодчиненные) | Первую найденную строку или Неопределено | Нужна одна строка по одному значению |
НайтиСтроки(Отбор, ВключатьПодчиненные) | Массив строк (может быть пустым) | Нужны все совпадения или отбор по нескольким колонкам |
Параметр ВключатьПодчиненные по умолчанию равен Ложь. Без Истина поиск идёт только по той коллекции, у которой вызван метод.
// Одна строка по значению в колонке «Номенклатура», на всех уровнях
НайденнаяСтрока = Дерево.Строки.Найти(Товар, "Номенклатура", Истина);
Если НайденнаяСтрока <> Неопределено Тогда
НайденнаяСтрока.Количество = НайденнаяСтрока.Количество + 1;
КонецЕсли;
// Все строки с отбором по двум колонкам
Отбор = Новый Структура("Склад, Организация", Склад, Организация);
МассивСтрок = Дерево.Строки.НайтиСтроки(Отбор, Истина);
Колонки в Найти перечисляют через запятую строкой. Если колонки не указать, поиск пройдёт по всем.
Путь от найденной строки до корня
Часто строку нашли, а нужно понять, в какой она группе. Поднимайтесь по Родитель, пока не получите Неопределено:
Путь = Новый Массив;
Текущая = НайденнаяСтрока;
Пока Текущая <> Неопределено Цикл
Путь.Вставить(0, Текущая.Наименование);
Текущая = Текущая.Родитель;
КонецЦикла;
Сообщить(СтрСоединить(Путь, " / "));
Получится строка вида Поставщики / ООО Ромашка / Договор № 12.
Много поисков подряд
У дерева, в отличие от таблицы значений, нет индексов. Если в цикле по 10 000 документов вы каждый раз вызываете Найти(..., Истина), каждый вызов заново проходит всё дерево. Один раз соберите Соответствие «ключ → строка», а дальше берите строку по ключу:
КартаСтрок = Новый Соответствие;
Для Каждого Строка Из ВсеСтрокиДерева(Дерево) Цикл
КартаСтрок.Вставить(Строка.Номенклатура, Строка);
КонецЦикла;
СтрокаТовара = КартаСтрок.Получить(Товар);
Итоги по узлам дерева: сумма снизу вверх
Ручное дерево не считает итоги само. Родительские строки остаются пустыми, пока их не заполнить. Задача решается рекурсией «после детей»: функция возвращает сумму ветки и тут же записывает её в родителя.
Функция ЗаполнитьИтоги(КоллекцияСтрок)
Всего = 0;
Для Каждого Строка Из КоллекцияСтрок Цикл
Если Строка.Строки.Количество() > 0 Тогда
Строка.Сумма = ЗаполнитьИтоги(Строка.Строки);
КонецЕсли;
Всего = Всего + Строка.Сумма;
КонецЦикла;
Возврат Всего;
КонецФункции
ИтогПоДереву = ЗаполнитьИтоги(Дерево.Строки);
Проверим на примере. Группа «Поставщики» содержит две строки: 15 000 и 7 500. Группа «Покупатели» — одну строку 4 200. После вызова у «Поставщиков» будет 22 500, у «Покупателей» 4 200, а ИтогПоДереву = 26 700.
Тот же каркас подходит для других агрегатов:
- 🔢 количество листьев: прибавляйте 1 только для строк без потомков;
- 📈 максимум по ветке: вместо суммы храните наибольшее значение;
- ✅ признак «ветка заполнена»: родитель истинен, только если истинны все дети.
У коллекции строк есть метод Итог("Сумма") — он складывает колонку по одной коллекции. Если включить в подсчёт подчинённые строки, а в родителях уже лежат итоги, сумма задвоится. Для дерева, где родители заполнены, берите Итог только по верхнему уровню или по листьям.
Если дерево получено из запроса с ИТОГИ СУММА(Сумма) ПО ..., итоги в группировках уже посчитаны платформой. Пересчитывать их не нужно, если вы не меняли суммы в деталях.
Добавление, перенос и удаление строк при обходе дерева
Добавление
Новую строку добавляют в коллекцию того уровня, где она нужна:
Дерево.Строки.Добавить()— на верхний уровень;Родитель.Строки.Добавить()— в конец ветки;Родитель.Строки.Вставить(0)— в начало ветки.
Добавлять строки в коллекцию, которую вы прямо сейчас перебираете, не стоит: цикл может пройти и по новым строкам. Соберите, куда и что вставить, в массив, а вставку сделайте после цикла.
Удаление: только с конца
Удаление в прямом цикле сдвигает индексы, и строка после удалённой пропускается. Правильный вариант — обратный цикл с границей, посчитанной один раз до начала:
Процедура УдалитьПустыеВетки(КоллекцияСтрок)
Всего = КоллекцияСтрок.Количество();
Для Сч = 1 По Всего Цикл
Строка = КоллекцияСтрок[Всего - Сч];
УдалитьПустыеВетки(Строка.Строки);
Если Строка.Строки.Количество() = 0 И Строка.Сумма = 0 Тогда
КоллекцияСтрок.Удалить(Строка);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Индекс идёт от Всего - 1 до 0. Сначала чистятся потомки, поэтому группа, у которой удалили всех детей, тоже удалится, если её сумма нулевая.
Удаление строки удаляет и всю её ветку. Отдельно чистить потомков перед этим не нужно.
Перенос ветки к другому родителю
Метода «сменить родителя» у строки дерева нет. Ветку копируют рекурсивно в новое место, а старую удаляют:
Процедура СкопироватьВетку(Источник, КоллекцияПриемник)
Новая = КоллекцияПриемник.Добавить();
ЗаполнитьЗначенияСвойств(Новая, Источник);
Для Каждого Подчиненная Из Источник.Строки Цикл
СкопироватьВетку(Подчиненная, Новая.Строки);
КонецЦикла;
КонецПроцедуры
СкопироватьВетку(СтрокаДляПереноса, НовыйРодитель.Строки);
СтарыйРодитель = СтрокаДляПереноса.Родитель;
Если СтарыйРодитель = Неопределено Тогда
Дерево.Строки.Удалить(СтрокаДляПереноса);
Иначе
СтарыйРодитель.Строки.Удалить(СтрокаДляПереноса);
КонецЕсли;
Не переносите ветку внутрь её же потомка. Копия попадёт в копируемую ветку, и рекурсия станет бесконечной. Перед переносом поднимитесь от НовыйРодитель по Родитель и убедитесь, что СтрокаДляПереноса на этом пути не встречается.
Внутри одной коллекции строку двигают методом Сдвинуть(Строка, Смещение): отрицательное смещение — вверх, положительное — вниз.
Обход дерева значений на управляемой форме
На форме дерево хранится как реквизит типа «Дерево значений», но в коде это уже ДанныеФормыДерево. Названия методов другие:
| На сервере (ДеревоЗначений) | На форме (ДанныеФормыДерево) |
|---|---|
Дерево.Строки | Дерево.ПолучитьЭлементы() |
Строка.Строки | Элемент.ПолучитьЭлементы() |
Строка.Родитель | Элемент.ПолучитьРодителя() |
Рекурсия на клиенте пишется так же, только через ПолучитьЭлементы(). Пример — команда «Развернуть всё»:
&НаКлиенте
Процедура РазвернутьВсе(Команда)
Для Каждого Элемент Из Дерево.ПолучитьЭлементы() Цикл
Элементы.Дерево.Развернуть(Элемент.ПолучитьИдентификатор(), Истина);
КонецЦикла;
КонецПроцедуры
Второй параметр Истина раскрывает строку со всеми подчинёнными, поэтому достаточно пройти верхний уровень.
Для тяжёлой обработки переведите реквизит в обычное дерево на сервере, а результат верните обратно:
&НаСервере
Процедура ПересчитатьНаСервере()
ДеревоЗн = РеквизитФормыВЗначение("Дерево");
ЗаполнитьИтоги(ДеревоЗн.Строки);
ЗначениеВРеквизитФормы(ДеревоЗн, "Дерево");
КонецПроцедуры
Так работают все методы ДеревоЗначений, включая Найти и НайтиСтроки с поиском по уровням.
Ошибки и скорость обхода дерева значений
Что чаще всего ломает обход и как это распознать:
Что ускоряет обход на больших деревьях:
- Вызывайте
Строка.Строки.Количество()перед рекурсией в цикле на сотнях тысяч строк — пустые ветки не создают лишних вызовов. - Для поиска берите
НайтииНайтиСтрокисВключатьПодчиненные = Истина. Они работают внутри платформы и быстрее ручной рекурсии на встроенном языке. - Повторный поиск — через
Соответствие, собранное один раз. - Если данные нужны только для чтения, обходите выборку запроса по группировкам и не выгружайте дерево.
- Не гоняйте
ДанныеФормыДеревомежду клиентом и сервером в цикле: один серверный вызов на всю обработку.