Строка дерева значений 1С: Родитель, Уровень, рекурсия

Дата: 19.09.2026 09:22:56
Дерево значений
Рекурсия
Коллекции значений
Встроенный язык
Управляемые формы
Комментарий:
0
Платформа:
8.3
Конфигурация:
Любая
Формы:
Любые

Строка дерева значений 1С — это объект СтрокаДереваЗначений, один узел иерархической коллекции. В отличие от строки таблицы значений, у неё есть подчинённые строки и ссылка на родителя, поэтому почти любая задача с деревом сводится к трём действиям: подняться к родителю, спуститься в подчинённые и понять, на каком уровне находишься. Ниже — свойства строки, уровень вложенности и максимальная глубина дерева, рекурсивный обход, удаление и перенос строк, а также отличия дерева на управляемой форме. Код рассчитан на платформу 8.3 и работает везде, где доступен тип ДеревоЗначений: на сервере, во внешнем соединении и в толстом клиенте обычного приложения.

Где встречается

Дерево значений появляется там, где данные сгруппированы:

  • запрос с ИТОГИ ПО, выгруженный через РезультатЗапроса.Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам): каждая группировка становится отдельным уровнем;
  • вывод результата СКД в коллекцию через ПроцессорВыводаРезультатаКомпоновкиДанныхВКоллекциюЗначений, когда отчёт нужен не для печати, а для дальнейшей обработки;
  • реквизит формы типа «Дерево значений» — например, подбор с флажками по группам номенклатуры;
  • собственные структуры: разузлование спецификаций, дерево подразделений, счета с субсчетами.

Как создать дерево, добавить колонки и заполнить его из запроса, разобрано в статье про дерево значений. Здесь — всё, что касается отдельной строки.

Строка дерева значений 1С: свойства и методы

Свойство или методЧто возвращаетКомментарий
РодительСтрокаДереваЗначений или НеопределеноУ строк верхнего уровня — Неопределено. Только для чтения.
СтрокиКоллекцияСтрокДереваЗначенийПодчинённые строки. У коллекции есть Добавить, Вставить, Удалить, Индекс, Найти, НайтиСтроки, Сдвинуть, Сортировать, Итог.
Уровень()Число0 — верхний уровень, 1 — его подчинённые и так далее.
Владелец()ДеревоЗначенийДерево, которому принадлежит строка.
СтрокаДерева.Сумма, СтрокаДерева["Сумма"]Значение колонкиВариант со скобками нужен, когда имя колонки лежит в переменной.

Небольшой пример, на котором удобно проверить поведение свойств в отладчике:

Дерево = Новый ДеревоЗначений;
Дерево.Колонки.Добавить("Наименование", Новый ОписаниеТипов("Строка"));
Дерево.Колонки.Добавить("Сумма", Новый ОписаниеТипов("Число"));

Группа = Дерево.Строки.Добавить();
Группа.Наименование = "Канцтовары";

Позиция = Группа.Строки.Добавить();
Позиция.Наименование = "Бумага А4";
Позиция.Сумма = 1200;

// Группа.Родитель = Неопределено, Группа.Уровень() = 0
// Позиция.Родитель = Группа,       Позиция.Уровень() = 1
// Позиция.Владелец() = Дерево

Сравнение строк через = проверяет, один ли это объект, а не совпадение значений колонок. Поэтому условие Позиция.Родитель = Группа работает как ожидается, а две строки с одинаковым наименованием равными не считаются.

Функции из следующих разделов удобно держать в одном общем модуле, например РаботаСДеревомЗначений. Флажок «Сервер» нужен всегда; если дерево обрабатывается в толстом клиенте обычного приложения, включите ещё «Клиент (обычное приложение)», а для кода во внешнем соединении — «Внешнее соединение». Без нужного флажка модуль в этом контексте не виден, и вызов не скомпилируется.

Уровень вложенности и глубина дерева значений

Уровень вложенности дерева значений в 1С определяют для каждой строки отдельно: метод Уровень() возвращает 0 для строк верхнего уровня, 1 — для их подчинённых и так далее. Готового метода, который вернул бы глубину всего дерева, у платформы нет, её считают обходом. Рекурсивный вариант получается коротким: глубина коллекции — это максимум из «1 + глубина подчинённых» по всем её строкам.

// Количество уровней дерева: 0 — строк нет, 1 — только верхний уровень.
Функция ГлубинаДерева(Дерево) Экспорт
    Возврат ГлубинаКоллекции(Дерево.Строки);
КонецФункции

Функция ГлубинаКоллекции(КоллекцияСтрок)
    Максимум = 0;
    Для Каждого СтрокаДерева Из КоллекцияСтрок Цикл
        Максимум = Макс(Максимум, 1 + ГлубинаКоллекции(СтрокаДерева.Строки));
    КонецЦикла;
    Возврат Максимум;
КонецФункции

Функция возвращает количество уровней: 0 для пустого дерева, 1 — если есть только строки верхнего уровня. Если нужен номер самого глубокого уровня в терминах Уровень(), вычтите единицу. Второй способ — собрать все строки в массив (см. обход ниже) и найти максимум СтрокаДерева.Уровень(); он удобен, если массив всё равно нужен для других целей.

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

Родитель строки: путь от корня и соседние строки

Родитель строки дерева значений в 1С — строка, в коллекции Строки которой лежит текущая. Её возвращает свойство Родитель, а у строк верхнего уровня оно равно Неопределено. Поднимаясь по родителям в цикле, можно собрать полный путь к строке, найти группу верхнего уровня или проверить, входит ли строка в нужную ветку.

// Путь от корня: "Канцтовары / Бумага / Бумага А4".
Функция ПутьКСтроке(СтрокаДерева, ИмяКолонки, Разделитель = " / ") Экспорт
    Части = Новый Массив;
    Текущая = СтрокаДерева;
    Пока Текущая <> Неопределено Цикл
        Части.Вставить(0, Строка(Текущая[ИмяКолонки]));
        Текущая = Текущая.Родитель;
    КонецЦикла;
    Возврат СтрСоединить(Части, Разделитель);
КонецФункции

// Коллекция, в которой лежит строка: нужна для Удалить, Индекс, Сдвинуть.
Функция КоллекцияСтроки(СтрокаДерева) Экспорт
    Если СтрокаДерева.Родитель = Неопределено Тогда
        Возврат СтрокаДерева.Владелец().Строки;
    КонецЕсли;
    Возврат СтрокаДерева.Родитель.Строки;
КонецФункции

СтрСоединить появилась в 8.3.6; на более старых версиях путь склеивают конкатенацией в том же цикле. Функция КоллекцияСтроки понадобится дальше: методы Удалить, Индекс и Сдвинуть есть у коллекции, а не у строки, а для верхнего уровня этой коллекцией служит Дерево.Строки, до которого добираются через Владелец().

Рекурсивный обход дерева значений

Передать в процедуру «что сделать с каждой строкой» во встроенном языке напрямую нельзя, поэтому удобный приём — один раз собрать все строки в массив в порядке обхода, а дальше работать обычным циклом.

// Все строки дерева в порядке обхода: строка, затем её подчинённые.
Процедура СобратьСтроки(КоллекцияСтрок, ВсеСтроки) Экспорт
    Для Каждого СтрокаДерева Из КоллекцияСтрок Цикл
        ВсеСтроки.Добавить(СтрокаДерева);
        СобратьСтроки(СтрокаДерева.Строки, ВсеСтроки);
    КонецЦикла;
КонецПроцедуры

// Использование
ВсеСтроки = Новый Массив;
СобратьСтроки(Дерево.Строки, ВсеСтроки);
МаксУровень = -1;
Для Каждого СтрокаДерева Из ВсеСтроки Цикл
    МаксУровень = Макс(МаксУровень, СтрокаДерева.Уровень());
    // здесь же — вывод в табличный документ, проверка условий и т.п.
КонецЦикла;

Порядок получается «сверху вниз»: группа, затем её содержимое, затем следующая группа. В таком порядке дерево выводят в табличный документ с отступами или ищут строки по условию, которое не укладывается в НайтиСтроки.

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

// Сумма группы = сумма подчинённых строк. Обход снизу вверх.
Функция ПересчитатьСуммы(КоллекцияСтрок) Экспорт
    Итого = 0;
    Для Каждого СтрокаДерева Из КоллекцияСтрок Цикл
        Если СтрокаДерева.Строки.Количество() > 0 Тогда
            СтрокаДерева.Сумма = ПересчитатьСуммы(СтрокаДерева.Строки);
        КонецЕсли;
        // В дереве из запроса вместо числа может оказаться NULL.
        Итого = Итого + ?(ЗначениеЗаполнено(СтрокаДерева.Сумма), СтрокаДерева.Сумма, 0);
    КонецЦикла;
    Возврат Итого;
КонецФункции

Проверка через ЗначениеЗаполнено защищает от NULL. В дереве, выгруженном из запроса, он попадает в числовые колонки, например при левом соединении, и сложение с ним прерывается ошибкой. Если запрос свой, надёжнее сразу выбирать поле как ЕСТЬNULL(Сумма, 0).

Рекурсия читается проще всего, но это не единственный вариант. Если не хочется зависеть от глубины стека вызовов или нужно прервать обход по условию без флагов, используйте явный стек на массиве:

Функция СтрокиБезРекурсии(Дерево) Экспорт
    Результат = Новый Массив;
    Стек = Новый Массив;
    ДобавитьВСтек(Стек, Дерево.Строки);
    Пока Стек.Количество() > 0 Цикл
        СтрокаДерева = Стек[Стек.ВГраница()];
        Стек.Удалить(Стек.ВГраница());
        Результат.Добавить(СтрокаДерева);
        ДобавитьВСтек(Стек, СтрокаДерева.Строки);
    КонецЦикла;
    Возврат Результат;
КонецФункции

// Кладёт строки с конца, чтобы первой из стека достали первую строку.
Процедура ДобавитьВСтек(Стек, КоллекцияСтрок)
    Индекс = КоллекцияСтрок.Количество() - 1;
    Пока Индекс >= 0 Цикл
        Стек.Добавить(КоллекцияСтрок[Индекс]);
        Индекс = Индекс - 1;
    КонецЦикла;
КонецПроцедуры

Строки кладутся в стек в обратном порядке, чтобы извлекаться в исходном, поэтому результат совпадает с СобратьСтроки. Методы Добавить, Удалить и ВГраница разобраны в статье про массив.

Удаление строк дерева

Строку удаляют из той коллекции, в которой она лежит: СтрокаДерева.Родитель.Строки.Удалить(СтрокаДерева) или, для верхнего уровня, Дерево.Строки.Удалить(СтрокаДерева). Вместе со строкой уходят все её подчинённые. Если удаление идёт по условию, коллекцию проходят с конца: тогда удаление текущей строки не сдвигает ещё не проверенные.

// Удаляет строки без подчинённых с нулевой или пустой (NULL) суммой,
// а затем опустевшие группы.
Процедура УдалитьПустыеСтроки(КоллекцияСтрок) Экспорт
    Индекс = КоллекцияСтрок.Количество() - 1;
    Пока Индекс >= 0 Цикл
        СтрокаДерева = КоллекцияСтрок[Индекс];
        УдалитьПустыеСтроки(СтрокаДерева.Строки);
        Если СтрокаДерева.Строки.Количество() = 0
            И Не ЗначениеЗаполнено(СтрокаДерева.Сумма) Тогда
            КоллекцияСтрок.Удалить(СтрокаДерева);
        КонецЕсли;
        Индекс = Индекс - 1;
    КонецЦикла;
КонецПроцедуры

Сначала чистятся подчинённые, затем проверяется сама строка — так группа, в которой после чистки ничего не осталось, тоже удаляется. Условие Не ЗначениеЗаполнено срабатывает и на 0, и на NULL: обычное сравнение Сумма = 0 для NULL даёт Ложь, и такие строки остались бы в дереве. Чтобы очистить ветку целиком и оставить саму строку, достаточно СтрокаДерева.Строки.Очистить().

Перенос строки к другому родителю

Свойство Родитель доступно только для чтения, а метод Сдвинуть меняет позицию лишь внутри одной коллекции. Чтобы перенести строку в другую группу, её копируют вместе с веткой в новое место, а старую удаляют.

// Истина, если Проверяемая — сама строка Предок или любая её подчинённая.
Функция ЭтоВетка(Проверяемая, Предок) Экспорт
    Текущая = Проверяемая;
    Пока Текущая <> Неопределено Цикл
        Если Текущая = Предок Тогда
            Возврат Истина;
        КонецЕсли;
        Текущая = Текущая.Родитель;
    КонецЦикла;
    Возврат Ложь;
КонецФункции

// Копирует значения колонок и все подчинённые строки.
Процедура СкопироватьВетку(Источник, Приемник) Экспорт
    ЗаполнитьЗначенияСвойств(Приемник, Источник);
    Для Каждого Подчиненная Из Источник.Строки Цикл
        СкопироватьВетку(Подчиненная, Приемник.Строки.Добавить());
    КонецЦикла;
КонецПроцедуры

// Переносит строку с подчинёнными под НовыйРодитель.
// НовыйРодитель — строка дерева или само дерево (перенос на верхний уровень).
Функция ПеренестиСтроку(СтрокаДерева, НовыйРодитель) Экспорт
    Если ТипЗнч(НовыйРодитель) = Тип("СтрокаДереваЗначений")
        И ЭтоВетка(НовыйРодитель, СтрокаДерева) Тогда
        ВызватьИсключение "Нельзя перенести строку внутрь её собственной ветки";
    КонецЕсли;
    НоваяСтрока = НовыйРодитель.Строки.Добавить();
    СкопироватьВетку(СтрокаДерева, НоваяСтрока);
    КоллекцияСтроки(СтрокаДерева).Удалить(СтрокаДерева);
    Возврат НоваяСтрока;
КонецФункции

Ключевые места:

  • ЗаполнитьЗначенияСвойств копирует значения колонок с одинаковыми именами, подчинённые строки добавляет рекурсия.
  • Проверка ЭтоВетка не даёт перенести группу в её же подчинённую строку. Без неё копирование пойдёт по строкам, которые само и добавляет, и не закончится.
  • Вторым параметром можно передать само дерево: у ДеревоЗначений тоже есть свойство Строки, и строка уйдёт на верхний уровень. Подойдёт и строка другого дерева — скопируются колонки с совпадающими именами.
  • Функция возвращает новую строку. Старая удалена, и ссылки на неё, сохранённые в массивах или соответствиях, больше не указывают на строку дерева.

Строка дерева на управляемой форме

В тонком и веб-клиенте типа ДеревоЗначений нет. Реквизит формы с этим типом и на клиенте, и в серверных процедурах формы представлен объектом ДанныеФормыДерево, а его строки — ДанныеФормыЭлементДерева. Методы у них другие:

ЗадачаДеревоЗначений (сервер)Дерево на форме (клиент)
Строки верхнего уровняДерево.СтрокиДерево.ПолучитьЭлементы()
Подчинённые строкиСтрокаДерева.СтрокиЭлементДерева.ПолучитьЭлементы()
РодительСтрокаДерева.РодительЭлементДерева.ПолучитьРодителя()
УровеньСтрокаДерева.Уровень()считать циклом по родителям
Строка по идентификаторуДерево.НайтиПоИдентификатору(Ид)

Уровня у элемента формы нет, его считают подъёмом по родителям:

// Модуль формы. Реквизит формы Дерево (тип ДеревоЗначений),
// реквизит УровеньТекущейСтроки (Число), таблица формы Дерево.

&НаКлиенте
Функция УровеньЭлемента(ЭлементДерева)
    Уровень = 0;
    ЭлементРодитель = ЭлементДерева.ПолучитьРодителя();
    Пока ЭлементРодитель <> Неопределено Цикл
        Уровень = Уровень + 1;
        ЭлементРодитель = ЭлементРодитель.ПолучитьРодителя();
    КонецЦикла;
    Возврат Уровень;
КонецФункции

&НаКлиенте
Процедура ДеревоПриАктивизацииСтроки(Элемент)
    Если Элемент.ТекущаяСтрока = Неопределено Тогда
        Возврат;
    КонецЕсли;
    ЭлементДерева = Дерево.НайтиПоИдентификатору(Элемент.ТекущаяСтрока);
    УровеньТекущейСтроки = УровеньЭлемента(ЭлементДерева);
КонецПроцедуры

Для сложной обработки дерево переносят на сервер как значение, обрабатывают функциями общего модуля и возвращают обратно:

&НаСервере
Процедура ПересчитатьИтогиНаСервере()
    ДеревоОбъект = РеквизитФормыВЗначение("Дерево");
    РаботаСДеревомЗначений.ПересчитатьСуммы(ДеревоОбъект.Строки);
    ЗначениеВРеквизитФормы(ДеревоОбъект, "Дерево");
КонецПроцедуры

После ЗначениеВРеквизитФормы дерево на форме строится заново, поэтому текущая строка и раскрытые узлы могут сброситься. Если это мешает пользователю, вносите изменения прямо в ДанныеФормыДерево через ПолучитьЭлементы(): алгоритмы те же, меняются только имена методов.

Типичные ошибки

  • Удаление внутри Для Каждого. Платформа ошибку не выдаёт, но после удаления следующая строка встаёт на место текущей и пропускается. Проходите коллекцию по индексу с конца.
  • Родитель.Строки у строки верхнего уровня. Родитель равен Неопределено, и выполнение прервётся с ошибкой «Поле объекта не обнаружено (Строки)». Проверяйте родителя или берите Владелец().Строки.
  • Смена родителя присваиванием. СтрокаДерева.Родитель = Группа заканчивается ошибкой «Поле объекта недоступно для записи (Родитель)». Переносите строку копированием.
  • Методы ДеревоЗначений в клиентском коде. ЭлементДерева.Уровень() даёт «Метод объекта не обнаружен (Уровень)», ЭлементДерева.Строки — «Поле объекта не обнаружено (Строки)», а Новый ДеревоЗначений в процедуре &НаКлиенте — «Тип не определен (ДеревоЗначений)».
  • Удаление вложенной строки из Дерево.Строки. Эта коллекция содержит только строки верхнего уровня; вложенную строку удаляйте через коллекцию её родителя или функцию КоллекцияСтроки.
  • Подсчёт через Количество(). Дерево.Строки.Количество() возвращает число строк верхнего уровня, а не всех строк дерева.
  • NULL в числовой колонке. В дереве из запроса вместо числа может оказаться NULL. Тогда Итого + СтрокаДерева.Сумма прерывается ошибкой «Преобразование значения к типу Число не может быть выполнено», а условие Сумма = 0 для такой строки ложно. Используйте ЕСТЬNULL в запросе или ЗначениеЗаполнено в коде.

Частые вопросы

Как проверить, что строка находится на верхнем уровне?

Подойдёт любое из условий: СтрокаДерева.Родитель = Неопределено или СтрокаДерева.Уровень() = 0. Для элемента дерева на форме используйте ЭлементДерева.ПолучитьРодителя() = Неопределено.

Как найти строку на любом уровне вложенности?

У коллекции строк есть Найти(Значение, "ИмяКолонки", Истина): третий параметр включает поиск в подчинённых. Для отбора по нескольким колонкам — НайтиСтроки(Новый Структура("Колонка", Значение), Истина), он возвращает массив строк. Про структуру для отбора — в статье о структуре.

Как узнать номер строки среди соседних?

КоллекцияСтроки(СтрокаДерева).Индекс(СтрокаДерева): индекс считается с нуля внутри коллекции родителя. Для строки верхнего уровня это позиция в Дерево.Строки.

Как отсортировать дерево на всех уровнях?

Вызовите Дерево.Строки.Сортировать("Наименование", Истина): второй параметр включает сортировку подчинённых строк. Без него порядок изменится только на верхнем уровне.

Чем строка дерева отличается от строки таблицы значений?

Колонки и доступ к значениям у них устроены одинаково, но у строки дерева есть Строки, Родитель и Уровень(). Если иерархия не нужна, проще работать с таблицей значений: у неё больше методов для группировки и выгрузки.

Когда проще заказать

Если дерево — часть задачи побольше (иерархический отчёт с произвольной вложенностью, подбор с флажками по группам, разузлование спецификаций), её можно передать программисту. Опишите, что нужно, на странице разместить задачу — оценка на площадке бесплатная, ответ в течение рабочего дня. Подробнее о направлении — в разделе отчёты и обработки 1С.

По теме

Не хотите разбираться сами?

Адаптируем это решение под вашу базу. Оценка бесплатно.

Оставить задачу

Станьте частью сообщества!
Войдите или зарегистрируйтесь, и вы сможете участвовать в обсуждениях.


1С, 1С:Предприятие — товарные знаки ООО «1С». Сайт не является официальным представительством фирмы «1С» и не аффилирован с ней.