Что такое дерево значений в 1С и как с ним работать в коде

· 9 мин

Содержание

Дерево значений в 1С — это коллекция в памяти с колонками, как у таблицы, но каждая строка может содержать свои подчинённые строки. Создаётся конструктором Новый ДеревоЗначений, колонки описываются через Колонки.Добавить(), а строки добавляются не в само дерево, а в коллекцию Строки — корневую или любой строки-родителя.

Чаще всего дерево получают готовым: выгружают результат запроса с ИТОГИ ПО в режиме ОбходРезультатаЗапроса.ПоГруппировкам. Дальше его обходят рекурсией, ищут строки, сортируют, считают итоги и показывают на управляемой форме или в отчёте через СКД.

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

У обеих коллекций одинаковые колонки и похожие методы. Разница — в строках. В таблице все строки лежат в одном плоском списке. В дереве у каждой строки есть свойство Строки (дочерние строки) и Родитель (строка уровнем выше или Неопределено для корня).

ВозможностьТаблицаЗначенийДеревоЗначений
Иерархия строкнетесть, любая глубина
Доступ к строке по индексуТаблица[5]только внутри своего уровня: Строка.Строки[0]
Индексы для поиска (Индексы)естьнет
Свёртка Свернуть()естьнет
Параметр запроса, источник для ПОМЕСТИТЬможнонельзя
Внешний набор данных СКДможнонельзя
Доступность на тонком клиентенетнет

Когда дерево действительно нужно:

  • 🌳 данные иерархичны по смыслу: группы номенклатуры, подразделения, статьи затрат;
  • 📊 нужны промежуточные итоги по уровням (организация → склад → товар);
  • 🧾 надо показать пользователю раскрывающийся список на форме;
  • 🔁 разбираете результат запроса с ИТОГИ, где группировки уже вложены.

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

Дерево и таблица — объекты сервера и толстого клиента. На тонкий клиент их напрямую не передать: для формы используется реквизит формы, об этом ниже.

Как создать дерево значений и описать колонки

Объект создаётся одной строкой. Колонки лучше сразу типизировать через ОписаниеТипов — так сортировка, итоги и вывод на форму ведут себя предсказуемо.

Дерево = Новый ДеревоЗначений;

ТипЧисло = Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 2));
ТипСтрока = Новый ОписаниеТипов("Строка", , Новый КвалификаторыСтроки(150));

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

Параметры Колонки.Добавить() по порядку:

  1. Имя — идентификатор без пробелов, по нему обращаются Строка.Сумма.
  2. Тип — ОписаниеТипов; если не указать, колонка примет значение любого типа.
  3. Заголовок — подпись при выводе.
  4. Ширина — в символах, влияет только на отображение.

Колонку можно добавить и после заполнения строк — у существующих строк в ней будет значение по умолчанию для типа (0, пустая строка) или Неопределено, если тип не задан. Проверить наличие колонки: Дерево.Колонки.Найти("Сумма") <> Неопределено.

Нетипизированная колонка — частая причина странной сортировки. Если в неё попали и числа, и строки, порядок будет по типам, а не по значениям.

Как заполнить дерево значений строками и уровнями иерархии

Главное правило: метод Добавить() вызывается у коллекции строк. Корневые строки — у Дерево.Строки, дочерние — у СтрокаРодитель.Строки.

Склад = Дерево.Строки.Добавить();
Склад.Наименование = "Основной склад";

Товар = Склад.Строки.Добавить();
Товар.Наименование = "Бумага А4";
Товар.Количество = 20;
Товар.Сумма = 9000;

Товар = Склад.Строки.Добавить();
Товар.Наименование = "Картридж";
Товар.Количество = 3;
Товар.Сумма = 7500;

Склад.Количество = Склад.Строки.Итог("Количество"); // 23
Склад.Сумма = Склад.Строки.Итог("Сумма");           // 16500

Полезные свойства и методы строки:

  • 🔼 Строка.Родитель — строка уровнем выше; у корневой строки Неопределено;
  • 🔢 Строка.Уровень() — глубина: 0 у корневой, 1 у её детей и так далее;
  • 📂 Строка.Строки.Количество() — сколько прямых потомков;
  • ➕ Строки.Вставить(Индекс) — вставка на конкретную позицию уровня;
  • ↕️ Строки.Сдвинуть(Строка, Смещение) — перемещение внутри уровня;
  • ❌ Строки.Удалить(Строка) — удаляет строку вместе со всеми потомками.

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

Цикл Для Каждого по Дерево.Строки проходит только верхний уровень. Чтобы обойти всё дерево, нужна рекурсивная процедура:

Процедура ОбойтиСтроки(КоллекцияСтрок, Результат)
    Для Каждого Строка Из КоллекцияСтрок Цикл
        Если Строка.Строки.Количество() = 0 Тогда
            Результат.Добавить(Строка.Наименование); // лист дерева
        Иначе
            ОбойтиСтроки(Строка.Строки, Результат);
        КонецЕсли;
    КонецЦикла;
КонецПроцедуры

Листья = Новый Массив;
ОбойтиСтроки(Дерево.Строки, Листья);

Тот же каркас подходит для пересчёта итогов снизу вверх: сначала рекурсивно обработать детей, потом записать в родителя Итог() по его строкам.

Удаление строк при обходе

Удалять строки внутри Для Каждого по той же коллекции нельзя — элементы сдвигаются, часть пропускается. Идите по индексу с конца:

Для Индекс = 1 По Склад.Строки.Количество() Цикл
    Строка = Склад.Строки[Склад.Строки.Количество() - Индекс];
    Если Строка.Количество = 0 Тогда
        Склад.Строки.Удалить(Строка);
    КонецЕсли;
КонецЦикла;

Дерево значений из запроса: ИТОГИ ПО и режимы выгрузки

Самый короткий путь получить заполненное дерево — запрос с итогами. Платформа сама построит уровни по группировкам и посчитает суммы.

Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
|    Остатки.Склад КАК Склад,
|    Остатки.Номенклатура КАК Номенклатура,
|    Остатки.КоличествоОстаток КАК Количество
|ИЗ
|    РегистрНакопления.ТоварыНаСкладах.Остатки КАК Остатки
|ИТОГИ
|    СУММА(Количество)
|ПО
|    Склад";

Дерево = Запрос.Выполнить().Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам);

Имя регистра в примере условное — подставьте свой. Структура результата:

  • верхний уровень — строки-итоги по каждому складу, в колонке Количество сумма;
  • второй уровень — детальные записи склада.

Режимы метода Выгрузить():

ПараметрЧто получится
ОбходРезультатаЗапроса.Прямой (по умолчанию)плоская таблица значений, итоговые строки идут вперемешку с детальными
ОбходРезультатаЗапроса.ПоГруппировкамдерево значений по группировкам из ПО
ОбходРезультатаЗапроса.ПоГруппировкамСИерархиейдерево с учётом иерархии справочника, если группировка указана с ключевым словом ИЕРАРХИЯ

Для иерархии групп номенклатуры пишут ПО Номенклатура ИЕРАРХИЯ и выгружают в режиме ПоГруппировкамСИерархией: в дереве появятся строки групп, внутри — подгруппы и элементы.

Без раздела ИТОГИ выгрузка ПоГруппировкам тоже вернёт дерево, но одноуровневое — смысла в этом нет, берите таблицу.

Обратное направление: дерево как параметр запроса

Передать дерево в Запрос.УстановитьПараметр() для ВЫБРАТЬ ... ИЗ &Таблица нельзя — источником внешних данных в запросе может быть только таблица значений. Порядок такой: обойти дерево рекурсией, собрать нужные строки в ТаблицаЗначений с типизированными колонками, и уже её поместить во временную таблицу.

Поиск, отбор и сортировка строк в дереве значений

У коллекции строк есть методы, которые работают как по одному уровню, так и по всем вложенным — за это отвечает последний параметр ВключатьПодчиненные.

ЗадачаМетодЧто вернёт
Найти первую строку по значениюДерево.Строки.Найти(Значение, "Номенклатура", Истина)СтрокаДереваЗначений или Неопределено
Найти все строки по условиямДерево.Строки.НайтиСтроки(Отбор, Истина)массив строк
ОтсортироватьДерево.Строки.Сортировать("Сумма Убыв", Истина)ничего, меняет порядок на месте
Сумма по колонкеСтроки.Итог("Сумма", Истина)число

Отбор задаётся структурой, ключи — имена колонок, сравнение только на равенство:

Отбор = Новый Структура("Склад, Номенклатура", ТекСклад, ТекТовар);
Найденные = Дерево.Строки.НайтиСтроки(Отбор, Истина);
Для Каждого Строка Из Найденные Цикл
    Строка.Количество = 0;
КонецЦикла;

Найденные строки — ссылки на строки дерева, а не копии. Изменение Строка.Количество сразу меняет дерево.

Сортировка по нескольким колонкам пишется через запятую: "Склад, Сумма Убыв". С параметром Истина каждый уровень сортируется внутри себя; строки не переезжают к чужим родителям.

Если в строках-итогах и в детальных строках одна колонка Сумма, то Итог("Сумма", Истина) сложит и итоги, и детали — получится удвоенная сумма. Для дерева из запроса с ИТОГИ берите сумму только по верхнему уровню: Дерево.Строки.Итог("Сумма").

Условий «больше/меньше» и «содержит» у НайтиСтроки() нет. Для таких отборов используйте рекурсивный обход с проверкой в Если.

Вывод дерева значений на форму и в отчёт

На управляемую форму

На форме нужен реквизит типа ДеревоЗначений с колонками — на клиенте он превращается в ДанныеФормыДерево. Элемент формы, привязанный к нему, — таблица формы с режимом отображения «Дерево».

Заполнение на сервере:

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

Обратно — РеквизитФормыВЗначение("ДеревоОстатков"). Имена колонок реквизита должны совпадать с колонками дерева: несовпадающие колонки просто не заполнятся.

На клиенте с ДанныеФормыДерево работают по-другому:

  • 📂 ДеревоОстатков.ПолучитьЭлементы() — строки верхнего уровня, у каждой строки тоже есть ПолучитьЭлементы();
  • 🔼 Строка.ПолучитьРодителя() — родитель;
  • 🆔 Строка.ПолучитьИдентификатор() — идентификатор для элемента формы;
  • ↘️ Элементы.ДеревоОстатков.Развернуть(Идентификатор, Истина) — раскрыть узел вместе с вложенными.

Раскрытие всех узлов делают после заполнения, в клиентской процедуре, перебирая ПолучитьЭлементы() верхнего уровня.

В отчёт через систему компоновки

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

КомпоновщикМакета = Новый КомпоновщикМакетаКомпоновкиДанных;
Макет = КомпоновщикМакета.Выполнить(Схема, Настройки, , ,
    Тип("ГенераторМакетаКомпоновкиДанныхДляКоллекцииЗначений"));

Процессор = Новый ПроцессорКомпоновкиДанных;
Процессор.Инициализировать(Макет);

Результат = Новый ДеревоЗначений;
ПроцессорВывода = Новый ПроцессорВыводаРезультатаКомпоновкиДанныхВКоллекциюЗначений;
ПроцессорВывода.УстановитьОбъект(Результат);
ПроцессорВывода.Вывести(Процессор);

Так удобно брать готовые данные отчёта с итогами по группировкам и дальше обрабатывать их кодом — например, выгружать во внешнюю систему.

В табличный документ своим кодом

Если нужен печатный вид без СКД, дерево выводят рекурсией. Для каждой строки берут область макета, а уровни оформляют группировками строк табличного документа — пользователь сможет сворачивать их кнопками «+» и «−» слева.

Процедура ВывестиУровень(ТабДок, Макет, КоллекцияСтрок)
    Для Каждого Строка Из КоллекцияСтрок Цикл
        Область = Макет.ПолучитьОбласть("Строка");
        Область.Параметры.Заполнить(Строка);
        ТабДок.Вывести(Область);
        Если Строка.Строки.Количество() > 0 Тогда
            ТабДок.НачатьГруппуСтрок(, Истина);
            ВывестиУровень(ТабДок, Макет, Строка.Строки);
            ТабДок.ЗакончитьГруппуСтрок();
        КонецЕсли;
    КонецЦикла;
КонецПроцедуры

Отступ по уровню удобно сделать отдельными областями макета (например, «Группа» и «Строка») и выбирать их по Строка.Уровень(). Параметры.Заполнить(Строка) переносит значения колонок в параметры области с теми же именами, так что параметры макета называйте как колонки дерева.

Производительность и типичные ошибки с деревом значений

Дерево хранит всё в оперативной памяти сервера. На десятках тысяч строк это уже заметно, на сотнях тысяч — риск долгих вызовов.

Что ускоряет работу:

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

Ниже — симптомы, с которыми чаще всего приходят разработчики.

Диагностика: выберите симптом

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

Можно ли преобразовать дерево значений в таблицу значений одной командой?
Нет, готового метода нет. Создайте таблицу с теми же колонками и заполните её рекурсивным обходом, при необходимости добавив колонку с уровнем строки.
Как узнать, что строка дерева — конечная, без вложенных?
Проверьте Строка.Строки.Количество() = 0. Такая строка — лист дерева.
Можно ли сохранить дерево значений в реквизит объекта или файл?
Да, через Новый ХранилищеЗначения(Дерево) в реквизит типа ХранилищеЗначения или через ЗначениеВСтрокуВнутр(). Это служебный формат, для обмена с другими системами лучше JSON или XML.
Почему на тонком клиенте нельзя объявить Новый ДеревоЗначений?
Тип недоступен в тонком клиенте и веб-клиенте. Создавайте дерево в серверной процедуре, а на клиенте работайте с ДанныеФормыДерево реквизита формы.
Сколько уровней вложенности допускает дерево?
Жёсткого ограничения в коде нет, глубина определяется данными. На практике упираются в память и глубину рекурсии при обходе, а не в само дерево.