Что такое дерево значений в 1С и как с ним работать в коде
Содержание
Дерево значений в 1С — это коллекция в памяти с колонками, как у таблицы, но каждая строка может содержать свои подчинённые строки. Создаётся конструктором Новый ДеревоЗначений, колонки описываются через Колонки.Добавить(), а строки добавляются не в само дерево, а в коллекцию Строки — корневую или любой строки-родителя.
Чаще всего дерево получают готовым: выгружают результат запроса с ИТОГИ ПО в режиме ОбходРезультатаЗапроса.ПоГруппировкам. Дальше его обходят рекурсией, ищут строки, сортируют, считают итоги и показывают на управляемой форме или в отчёте через СКД.
Чем дерево значений отличается от таблицы значений
У обеих коллекций одинаковые колонки и похожие методы. Разница — в строках. В таблице все строки лежат в одном плоском списке. В дереве у каждой строки есть свойство Строки (дочерние строки) и Родитель (строка уровнем выше или Неопределено для корня).
| Возможность | ТаблицаЗначений | ДеревоЗначений |
|---|---|---|
| Иерархия строк | нет | есть, любая глубина |
| Доступ к строке по индексу | Таблица[5] | только внутри своего уровня: Строка.Строки[0] |
Индексы для поиска (Индексы) | есть | нет |
Свёртка Свернуть() | есть | нет |
Параметр запроса, источник для ПОМЕСТИТЬ | можно | нельзя |
| Внешний набор данных СКД | можно | нельзя |
| Доступность на тонком клиенте | нет | нет |
Когда дерево действительно нужно:
- 🌳 данные иерархичны по смыслу: группы номенклатуры, подразделения, статьи затрат;
- 📊 нужны промежуточные итоги по уровням (организация → склад → товар);
- 🧾 надо показать пользователю раскрывающийся список на форме;
- 🔁 разбираете результат запроса с
ИТОГИ, где группировки уже вложены.
Если иерархии нет и нужен быстрый поиск по ключу, берите таблицу значений с индексом. Дерево в этом сценарии проигрывает.
Дерево и таблица — объекты сервера и толстого клиента. На тонкий клиент их напрямую не передать: для формы используется реквизит формы, об этом ниже.
Как создать дерево значений и описать колонки
Объект создаётся одной строкой. Колонки лучше сразу типизировать через ОписаниеТипов — так сортировка, итоги и вывод на форму ведут себя предсказуемо.
Дерево = Новый ДеревоЗначений;
ТипЧисло = Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 2));
ТипСтрока = Новый ОписаниеТипов("Строка", , Новый КвалификаторыСтроки(150));
Дерево.Колонки.Добавить("Номенклатура", Новый ОписаниеТипов("СправочникСсылка.Номенклатура"), "Товар");
Дерево.Колонки.Добавить("Наименование", ТипСтрока, "Наименование", 40);
Дерево.Колонки.Добавить("Количество", ТипЧисло, "Кол-во");
Дерево.Колонки.Добавить("Сумма", ТипЧисло, "Сумма");
Параметры Колонки.Добавить() по порядку:
- Имя — идентификатор без пробелов, по нему обращаются
Строка.Сумма. - Тип —
ОписаниеТипов; если не указать, колонка примет значение любого типа. - Заголовок — подпись при выводе.
- Ширина — в символах, влияет только на отображение.
Колонку можно добавить и после заполнения строк — у существующих строк в ней будет значение по умолчанию для типа (0, пустая строка) или Неопределено, если тип не задан. Проверить наличие колонки: Дерево.Колонки.Найти("Сумма") <> Неопределено.
Нетипизированная колонка — частая причина странной сортировки. Если в неё попали и числа, и строки, порядок будет по типам, а не по значениям.
Как заполнить дерево значений строками и уровнями иерархии
Главное правило: метод Добавить() вызывается у коллекции строк. Корневые строки — у Дерево.Строки, дочерние — у СтрокаРодитель.Строки.
Склад = Дерево.Строки.Добавить();
Склад.Наименование = "Основной склад";
Товар = Склад.Строки.Добавить();
Товар.Наименование = "Бумага А4";
Товар.Количество = 20;
Товар.Сумма = 9000;
Товар = Склад.Строки.Добавить();
Товар.Наименование = "Картридж";
Товар.Количество = 3;
Товар.Сумма = 7500;
Склад.Количество = Склад.Строки.Итог("Количество"); // 23
Склад.Сумма = Склад.Строки.Итог("Сумма"); // 16500
Полезные свойства и методы строки:
- 🔼
Строка.Родитель— строка уровнем выше; у корневой строкиНеопределено; - 🔢
Строка.Уровень()— глубина: 0 у корневой, 1 у её детей и так далее; - 📂
Строка.Строки.Количество()— сколько прямых потомков; - ➕
Строки.Вставить(Индекс)— вставка на конкретную позицию уровня; - ↕️
Строки.Сдвинуть(Строка, Смещение)— перемещение внутри уровня; - ❌
Строки.Удалить(Строка)— удаляет строку вместе со всеми потомками.
Рекурсивный обход всех уровней
Цикл Для Каждого по Дерево.Строки проходит только верхний уровень. Чтобы обойти всё дерево, нужна рекурсивная процедура:
Процедура ОбойтиСтроки(КоллекцияСтрок, Результат)
Для Каждого Строка Из КоллекцияСтрок Цикл
Если Строка.Строки.Количество() = 0 Тогда
Результат.Добавить(Строка.Наименование); // лист дерева
Иначе
ОбойтиСтроки(Строка.Строки, Результат);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Листья = Новый Массив;
ОбойтиСтроки(Дерево.Строки, Листья);
Тот же каркас подходит для пересчёта итогов снизу вверх: сначала рекурсивно обработать детей, потом записать в родителя Итог() по его строкам.
Удаление строк при обходе
Удалять строки внутри Для Каждого по той же коллекции нельзя — элементы сдвигаются, часть пропускается. Идите по индексу с конца:
Для Индекс = 1 По Склад.Строки.Количество() Цикл
Строка = Склад.Строки[Склад.Строки.Количество() - Индекс];
Если Строка.Количество = 0 Тогда
Склад.Строки.Удалить(Строка);
КонецЕсли;
КонецЦикла;
Дерево значений из запроса: ИТОГИ ПО и режимы выгрузки
Самый короткий путь получить заполненное дерево — запрос с итогами. Платформа сама построит уровни по группировкам и посчитает суммы.
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Остатки.Склад КАК Склад,
| Остатки.Номенклатура КАК Номенклатура,
| Остатки.КоличествоОстаток КАК Количество
|ИЗ
| РегистрНакопления.ТоварыНаСкладах.Остатки КАК Остатки
|ИТОГИ
| СУММА(Количество)
|ПО
| Склад";
Дерево = Запрос.Выполнить().Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам);
Имя регистра в примере условное — подставьте свой. Структура результата:
- верхний уровень — строки-итоги по каждому складу, в колонке
Количествосумма; - второй уровень — детальные записи склада.
Режимы метода Выгрузить():
| Параметр | Что получится |
|---|---|
ОбходРезультатаЗапроса.Прямой (по умолчанию) | плоская таблица значений, итоговые строки идут вперемешку с детальными |
ОбходРезультатаЗапроса.ПоГруппировкам | дерево значений по группировкам из ПО |
ОбходРезультатаЗапроса.ПоГруппировкамСИерархией | дерево с учётом иерархии справочника, если группировка указана с ключевым словом ИЕРАРХИЯ |
Для иерархии групп номенклатуры пишут ПО Номенклатура ИЕРАРХИЯ и выгружают в режиме ПоГруппировкамСИерархией: в дереве появятся строки групп, внутри — подгруппы и элементы.
Без раздела ИТОГИ выгрузка ПоГруппировкам тоже вернёт дерево, но одноуровневое — смысла в этом нет, берите таблицу.
Обратное направление: дерево как параметр запроса
Передать дерево в Запрос.УстановитьПараметр() для ВЫБРАТЬ ... ИЗ &Таблица нельзя — источником внешних данных в запросе может быть только таблица значений. Порядок такой: обойти дерево рекурсией, собрать нужные строки в ТаблицаЗначений с типизированными колонками, и уже её поместить во временную таблицу.
Поиск, отбор и сортировка строк в дереве значений
У коллекции строк есть методы, которые работают как по одному уровню, так и по всем вложенным — за это отвечает последний параметр ВключатьПодчиненные.
| Задача | Метод | Что вернёт |
|---|---|---|
| Найти первую строку по значению | Дерево.Строки.Найти(Значение, "Номенклатура", Истина) | СтрокаДереваЗначений или Неопределено |
| Найти все строки по условиям | Дерево.Строки.НайтиСтроки(Отбор, Истина) | массив строк |
| Отсортировать | Дерево.Строки.Сортировать("Сумма Убыв", Истина) | ничего, меняет порядок на месте |
| Сумма по колонке | Строки.Итог("Сумма", Истина) | число |
Отбор задаётся структурой, ключи — имена колонок, сравнение только на равенство:
Отбор = Новый Структура("Склад, Номенклатура", ТекСклад, ТекТовар);
Найденные = Дерево.Строки.НайтиСтроки(Отбор, Истина);
Для Каждого Строка Из Найденные Цикл
Строка.Количество = 0;
КонецЦикла;
Найденные строки — ссылки на строки дерева, а не копии. Изменение Строка.Количество сразу меняет дерево.
Сортировка по нескольким колонкам пишется через запятую: "Склад, Сумма Убыв". С параметром Истина каждый уровень сортируется внутри себя; строки не переезжают к чужим родителям.
Если в строках-итогах и в детальных строках одна колонка Сумма, то Итог("Сумма", Истина) сложит и итоги, и детали — получится удвоенная сумма. Для дерева из запроса с ИТОГИ берите сумму только по верхнему уровню: Дерево.Строки.Итог("Сумма").
Условий «больше/меньше» и «содержит» у НайтиСтроки() нет. Для таких отборов используйте рекурсивный обход с проверкой в Если.
Вывод дерева значений на форму и в отчёт
На управляемую форму
На форме нужен реквизит типа ДеревоЗначений с колонками — на клиенте он превращается в ДанныеФормыДерево. Элемент формы, привязанный к нему, — таблица формы с режимом отображения «Дерево».
Заполнение на сервере:
&НаСервере
Процедура ЗаполнитьДеревоНаСервере()
Дерево = Запрос.Выполнить().Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам);
ЗначениеВРеквизитФормы(Дерево, "ДеревоОстатков");
КонецПроцедуры
Обратно — РеквизитФормыВЗначение("ДеревоОстатков"). Имена колонок реквизита должны совпадать с колонками дерева: несовпадающие колонки просто не заполнятся.
На клиенте с ДанныеФормыДерево работают по-другому:
- 📂
ДеревоОстатков.ПолучитьЭлементы()— строки верхнего уровня, у каждой строки тоже естьПолучитьЭлементы(); - 🔼
Строка.ПолучитьРодителя()— родитель; - 🆔
Строка.ПолучитьИдентификатор()— идентификатор для элемента формы; - ↘️
Элементы.ДеревоОстатков.Развернуть(Идентификатор, Истина)— раскрыть узел вместе с вложенными.
Раскрытие всех узлов делают после заполнения, в клиентской процедуре, перебирая ПолучитьЭлементы() верхнего уровня.
В отчёт через систему компоновки
Внешним набором данных для СКД дерево быть не может — только таблица значений. Зато результат компоновки можно получить именно деревом, если группировки отчёта вложены:
КомпоновщикМакета = Новый КомпоновщикМакетаКомпоновкиДанных;
Макет = КомпоновщикМакета.Выполнить(Схема, Настройки, , ,
Тип("ГенераторМакетаКомпоновкиДанныхДляКоллекцииЗначений"));
Процессор = Новый ПроцессорКомпоновкиДанных;
Процессор.Инициализировать(Макет);
Результат = Новый ДеревоЗначений;
ПроцессорВывода = Новый ПроцессорВыводаРезультатаКомпоновкиДанныхВКоллекциюЗначений;
ПроцессорВывода.УстановитьОбъект(Результат);
ПроцессорВывода.Вывести(Процессор);
Так удобно брать готовые данные отчёта с итогами по группировкам и дальше обрабатывать их кодом — например, выгружать во внешнюю систему.
В табличный документ своим кодом
Если нужен печатный вид без СКД, дерево выводят рекурсией. Для каждой строки берут область макета, а уровни оформляют группировками строк табличного документа — пользователь сможет сворачивать их кнопками «+» и «−» слева.
Процедура ВывестиУровень(ТабДок, Макет, КоллекцияСтрок)
Для Каждого Строка Из КоллекцияСтрок Цикл
Область = Макет.ПолучитьОбласть("Строка");
Область.Параметры.Заполнить(Строка);
ТабДок.Вывести(Область);
Если Строка.Строки.Количество() > 0 Тогда
ТабДок.НачатьГруппуСтрок(, Истина);
ВывестиУровень(ТабДок, Макет, Строка.Строки);
ТабДок.ЗакончитьГруппуСтрок();
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Отступ по уровню удобно сделать отдельными областями макета (например, «Группа» и «Строка») и выбирать их по Строка.Уровень(). Параметры.Заполнить(Строка) переносит значения колонок в параметры области с теми же именами, так что параметры макета называйте как колонки дерева.
Производительность и типичные ошибки с деревом значений
Дерево хранит всё в оперативной памяти сервера. На десятках тысяч строк это уже заметно, на сотнях тысяч — риск долгих вызовов.
Что ускоряет работу:
- 🚀 считайте итоги в запросе через
ИТОГИ, а не рекурсией в коде; - 🎯 выбирайте в запросе только нужные колонки — каждая колонка умножает объём памяти;
- 🔍 для частого поиска по ключу соберите рядом
Соответствие«ключ → строка дерева»: поиск по нему мгновенный, аНайти(..., Истина)каждый раз проходит дерево целиком; - 🧱 не храните дерево между серверными вызовами в реквизите формы, если оно большое — каждое
ЗначениеВРеквизитФормыгоняет данные на клиент; - 🔄 сортируйте один раз после заполнения, а не после каждой добавленной строки.
Ниже — симптомы, с которыми чаще всего приходят разработчики.
Для Каждого по Дерево.Строки не заходит в дочерние строки. Нужна рекурсивная процедура, которая вызывает себя для Строка.Строки.ИТОГИ суммы уже лежат в строках-группировках. Итог(..., Истина) складывает их вместе с деталями. Берите итог по верхнему уровню без подчинённых.ЗначениеВРеквизитФормы. Проверьте, что заполнение выполняется в процедуре с директивой &НаСервере.ВЫБРАТЬ ИЗ &Таблица может быть только таблица значений. Преобразуйте дерево в плоскую таблицу с типизированными колонками.ОписаниеТипов, и в ней лежат значения разных типов. Задайте тип при Колонки.Добавить().Частые вопросы
Можно ли преобразовать дерево значений в таблицу значений одной командой?
Как узнать, что строка дерева — конечная, без вложенных?
Строка.Строки.Количество() = 0. Такая строка — лист дерева.Можно ли сохранить дерево значений в реквизит объекта или файл?
Новый ХранилищеЗначения(Дерево) в реквизит типа ХранилищеЗначения или через ЗначениеВСтрокуВнутр(). Это служебный формат, для обмена с другими системами лучше JSON или XML.Почему на тонком клиенте нельзя объявить Новый ДеревоЗначений?
ДанныеФормыДерево реквизита формы.