ДеревоЗначений во встроенном языке 1С 8.3: от первой колонки до быстрой выборки на тысячи узлов
Содержание
Дерево значений в 1С создаётся конструктором Новый ДеревоЗначений. Колонки добавляют через Дерево.Колонки.Добавить(), строки первого уровня — через Дерево.Строки.Добавить(), а вложенные — через Строка.Строки.Добавить(). Вся работа с иерархией держится на двух свойствах строки: Строки (подчинённые) и Родитель (вышестоящая).
Типичная боль — дерево собрано, а итоги групп пустые, Найти не видит вложенную строку, а фильтр «съедает» половину узлов. Все примеры здесь проверены на платформе 8.3 и вставляются в общий модуль или серверную процедуру формы как есть.
Как создать дерево значений и задать колонки
Дерево живёт на сервере, в толстом клиенте и во внешнем соединении. В тонком и веб-клиенте объекта ДеревоЗначений нет — на форме с ним работают через реквизит типа «Дерево значений» (об этом ниже).
Колонкам лучше сразу задавать тип. Без типа колонка примет что угодно, но метод Итог() и сортировка по числу поведут себя непредсказуемо, если туда попадёт строка.
Дерево = Новый ДеревоЗначений;
Дерево.Колонки.Добавить("Наименование", Новый ОписаниеТипов("Строка"));
Дерево.Колонки.Добавить("Количество", Новый ОписаниеТипов("Число"));
Дерево.Колонки.Добавить("Сумма", Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15, 2)));
Склад = Дерево.Строки.Добавить();
Склад.Наименование = "Основной склад";
Товар = Склад.Строки.Добавить();
Товар.Наименование = "Бумага А4";
Товар.Количество = 10;
Товар.Сумма = 3500;
Товар = Склад.Строки.Добавить();
Товар.Наименование = "Скрепки";
Товар.Количество = 40;
Товар.Сумма = 1200;
У метода Колонки.Добавить() четыре параметра: имя, описание типов, заголовок и ширина. Заголовок пригодится, если дерево потом выводится в табличный документ или отладчик.
Полезные методы коллекции строк на старте:
- ➕
Добавить()— новая строка в конец уровня; - 📍
Вставить(Индекс)— строка на нужную позицию, отсчёт с нуля; - 🔢
Количество()— сколько строк на этом уровне (без вложенных); - 🗑️
Удалить(Строка)иОчистить()— удаление одной строки или всего уровня вместе с подчинёнными.
Дерево.Строки.Количество() считает только верхний уровень. Если в дереве 3 склада и 500 товаров, вернётся 3. Полное число узлов считают рекурсией.
Обход дерева значений и переходы между уровнями
Глубина дерева заранее неизвестна, поэтому стандартный обход — рекурсивная процедура, которая принимает коллекцию строк и вызывает себя для Строка.Строки.
Процедура ОбойтиДерево(КоллекцияСтрок)
Для Каждого СтрокаДерева Из КоллекцияСтрок Цикл
Уровень = Формат(СтрокаДерева.Уровень(), "ЧН=0; ЧГ=0");
Сообщить(Уровень + ": " + СтрокаДерева.Наименование);
ОбойтиДерево(СтрокаДерева.Строки);
КонецЦикла;
КонецПроцедуры
// Вызов
ОбойтиДерево(Дерево.Строки);
Метод Уровень() возвращает 0 для строк верхнего уровня, 1 для их детей и так далее. Формат с ЧН=0 нужен, чтобы ноль не превратился в пустую строку.
Путь от строки к корню собирают через Родитель. У строк верхнего уровня родитель равен Неопределено — это и есть условие остановки.
Функция ПутьКСтроке(СтрокаДерева)
Путь = СтрокаДерева.Наименование;
ТекущийРодитель = СтрокаДерева.Родитель;
Пока ТекущийРодитель <> Неопределено Цикл
Путь = ТекущийРодитель.Наименование + " / " + Путь;
ТекущийРодитель = ТекущийРодитель.Родитель;
КонецЦикла;
Возврат Путь;
КонецФункции
Поиск строки
Два метода, оба есть и у Дерево.Строки, и у Строка.Строки:
| Метод | Что возвращает | Когда брать |
|---|---|---|
Найти(Значение, "Колонки", ВключатьПодчиненные) | первую строку или Неопределено | нужен один узел по одному значению |
НайтиСтроки(Структура, ВключатьПодчиненные) | массив строк (может быть пустым) | отбор по нескольким колонкам или все совпадения |
Найденная = Дерево.Строки.Найти("Скрепки", "Наименование", Истина);
Если Найденная <> Неопределено Тогда
Сообщить(ПутьКСтроке(Найденная));
КонецЕсли;
Отбор = Новый Структура("Количество", 10);
Массив = Дерево.Строки.НайтиСтроки(Отбор, Истина);
Без третьего параметра Истина поиск идёт только по текущему уровню. Это самая частая причина вопроса «почему Найти вернул Неопределено, строка же есть».
Перенос узла к другому родителю
Сменить родителя у существующей строки нельзя: свойство Родитель только для чтения. Узел копируют вместе с поддеревом и удаляют старый.
Процедура СкопироватьУзел(Источник, КоллекцияПриемник)
Новая = КоллекцияПриемник.Добавить();
ЗаполнитьЗначенияСвойств(Новая, Источник);
Для Каждого Подчиненная Из Источник.Строки Цикл
СкопироватьУзел(Подчиненная, Новая.Строки);
КонецЦикла;
КонецПроцедуры
// Перенос: копия в новое место, затем удаление старой строки
СкопироватьУзел(Товар, НовыйСклад.Строки);
Товар.Родитель.Строки.Удалить(Товар);
Если строка на верхнем уровне, её родитель Неопределено — удаляйте через Дерево.Строки.Удалить(Строка). Внутри одного уровня строку двигают методом Сдвинуть(Строка, Смещение): −1 поднимает на одну позицию, 1 опускает.
Группировка и итоги в дереве значений
Есть три способа получить дерево с группами — выбирайте по источнику данных.
Способ 1. Запрос с ИТОГИ — самый быстрый
Если данные лежат в базе, дерево отдаёт сама платформа. Секция ИТОГИ ... ПО строит группировки, а Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам) возвращает готовое ДеревоЗначений с суммами в строках групп.
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Продажи.Контрагент КАК Контрагент,
| Продажи.Номенклатура КАК Номенклатура,
| Продажи.СуммаОборот КАК Сумма
|ИЗ
| РегистрНакопления.Продажи.Обороты(&Начало, &Конец, , ) КАК Продажи
|ИТОГИ
| СУММА(Сумма)
|ПО
| Контрагент";
Запрос.УстановитьПараметр("Начало", НачалоМесяца(ТекущаяДата()));
Запрос.УстановитьПараметр("Конец", КонецМесяца(ТекущаяДата()));
Дерево = Запрос.Выполнить().Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам);
Имя регистра и его измерения подставьте из своей конфигурации, здесь важен приём. Для иерархического справочника добавьте ИЕРАРХИЯ после поля группировки: ПО Номенклатура ИЕРАРХИЯ — и в дереве появятся папки номенклатуры со своими итогами.
Способ 2. Сборка из таблицы через Соответствие
Когда исходник — таблица значений в памяти, группы создают на лету и запоминают в Соответствие, чтобы не искать их каждый раз.
Группы = Новый Соответствие;
Для Каждого СтрокаТЗ Из Таблица Цикл
Группа = Группы.Получить(СтрокаТЗ.Склад);
Если Группа = Неопределено Тогда
Группа = Дерево.Строки.Добавить();
Группа.Наименование = Строка(СтрокаТЗ.Склад);
Группы.Вставить(СтрокаТЗ.Склад, Группа);
КонецЕсли;
Деталь = Группа.Строки.Добавить();
Деталь.Наименование = Строка(СтрокаТЗ.Номенклатура);
ЗаполнитьЗначенияСвойств(Деталь, СтрокаТЗ, "Количество, Сумма");
КонецЦикла;
Способ 3. Итоги снизу вверх рекурсией
После ручной сборки в строках групп пусто. Итоги считают от листьев к корню: сначала обрабатываем детей, потом суммируем их в родителя.
Процедура ПосчитатьИтоги(КоллекцияСтрок)
Для Каждого СтрокаДерева Из КоллекцияСтрок Цикл
Если СтрокаДерева.Строки.Количество() > 0 Тогда
ПосчитатьИтоги(СтрокаДерева.Строки);
СтрокаДерева.Количество = СтрокаДерева.Строки.Итог("Количество");
СтрокаДерева.Сумма = СтрокаДерева.Строки.Итог("Сумма");
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Здесь Итог() вызывается без второго параметра — по прямым детям. Если передать ВключатьПодчиненные = Истина в дереве, где у групп уже есть суммы, итог задвоится: метод сложит и группы, и их содержимое.
Для нашего примера: бумага 3 500 + скрепки 1 200 = 4 700 в строке «Основной склад», количество 10 + 40 = 50.
Сортировка и фильтрация строк дерева
Сортировку выполняет метод коллекции строк. Второй параметр включает подчинённые уровни, иначе отсортируется только текущий.
// Склады по убыванию суммы, внутри — товары тоже по убыванию
Дерево.Строки.Сортировать("Сумма Убыв", Истина);
// Несколько колонок через запятую
Дерево.Строки.Сортировать("Наименование, Количество Убыв", Истина);
Строки внутри группы никогда не «выпрыгнут» к другому родителю — сортировка идёт отдельно на каждом уровне.
Фильтр с сохранением групп
Свойства «Отбор» у дерева нет. Фильтр делают на копии: удаляют лишние листья, а затем пустые группы. Удаление при прямом обходе Для Каждого пропускает строки, поэтому идём с конца, зафиксировав количество до цикла.
Процедура ОставитьКрупные(КоллекцияСтрок, МинСумма)
Всего = КоллекцияСтрок.Количество();
Для Сч = 1 По Всего Цикл
СтрокаДерева = КоллекцияСтрок[Всего - Сч];
Если СтрокаДерева.Строки.Количество() > 0 Тогда
ОставитьКрупные(СтрокаДерева.Строки, МинСумма);
Если СтрокаДерева.Строки.Количество() = 0 Тогда
КоллекцияСтрок.Удалить(СтрокаДерева);
КонецЕсли;
ИначеЕсли СтрокаДерева.Сумма < МинСумма Тогда
КоллекцияСтрок.Удалить(СтрокаДерева);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Отфильтрованное = Дерево.Скопировать();
ОставитьКрупные(Отфильтрованное.Строки, 2000);
Почему индекс Всего - Сч безопасен: удаляем строку с индексом, скажем, 4, и следующей берём строку 3 — она не сдвинулась. При прямом проходе после удаления строки 3 на её место встала бы строка 4, а цикл перешёл бы к 5.
После фильтра пересчитайте итоги процедурой ПосчитатьИтоги: суммы групп в копии остались старыми.
Дерево значений и таблица значений: перевод в обе стороны
Из дерева в таблицу
Готового метода нет, нужна рекурсия. Колонки таблицы копируем из колонок дерева и добавляем «Уровень», чтобы не потерять глубину.
Функция ДеревоВТаблицу(Дерево, ТолькоЛистья = Ложь)
Таблица = Новый ТаблицаЗначений;
Для Каждого Колонка Из Дерево.Колонки Цикл
Таблица.Колонки.Добавить(Колонка.Имя, Колонка.ТипЗначения);
КонецЦикла;
Таблица.Колонки.Добавить("Уровень", Новый ОписаниеТипов("Число"));
ВыгрузитьУровень(Дерево.Строки, Таблица, ТолькоЛистья);
Возврат Таблица;
КонецФункции
Процедура ВыгрузитьУровень(КоллекцияСтрок, Таблица, ТолькоЛистья)
Для Каждого СтрокаДерева Из КоллекцияСтрок Цикл
ЭтоЛист = (СтрокаДерева.Строки.Количество() = 0);
Если ЭтоЛист Или Не ТолькоЛистья Тогда
НоваяСтрока = Таблица.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаДерева);
НоваяСтрока.Уровень = СтрокаДерева.Уровень();
КонецЕсли;
ВыгрузитьУровень(СтрокаДерева.Строки, Таблица, ТолькоЛистья);
КонецЦикла;
КонецПроцедуры
С ТолькоЛистья = Истина получится плоская таблица без строк групп — её удобно свернуть, загрузить в табличную часть или передать в запрос. Если в дереве уже есть колонка «Уровень», назовите служебную колонку иначе, иначе Колонки.Добавить вызовет ошибку.
Метод Дерево.Строки.ВыгрузитьКолонку("Сумма") вернёт массив значений колонки, но только с текущего уровня, без вложенных строк. Для всех уровней нужна та же рекурсия.
Из таблицы в дерево по колонке «Родитель»
Типичный случай — таблица с полями Ссылка и Родитель, выгруженная из иерархического справочника. Ищем детей узла через НайтиСтроки, а чтобы поиск не перебирал всю таблицу на каждом шаге, вешаем индекс.
Процедура ЗаполнитьУзел(КоллекцияСтрок, Таблица, Родитель)
Отбор = Новый Структура("Родитель", Родитель);
Для Каждого СтрокаТЗ Из Таблица.НайтиСтроки(Отбор) Цикл
Узел = КоллекцияСтрок.Добавить();
ЗаполнитьЗначенияСвойств(Узел, СтрокаТЗ);
ЗаполнитьУзел(Узел.Строки, Таблица, СтрокаТЗ.Ссылка);
КонецЦикла;
КонецПроцедуры
Таблица.Индексы.Добавить("Родитель");
ЗаполнитьУзел(Дерево.Строки, Таблица, Справочники.Номенклатура.ПустаяСсылка());
Если данные можно получить запросом, проще взять способ 1 с ИЕРАРХИЯ — платформа построит то же дерево без рекурсии в коде.
Дерево значений на управляемой форме
На форме создают реквизит с типом «Дерево значений», добавляют ему колонки в конфигураторе и перетаскивают реквизит в элементы. Данные готовят на сервере и передают в реквизит:
&НаСервере
Процедура ЗаполнитьДерево()
Дерево = РеквизитФормыВЗначение("ДеревоТоваров");
Дерево.Строки.Очистить();
// ... заполнение любым способом выше ...
ЗначениеВРеквизитФормы(Дерево, "ДеревоТоваров");
КонецПроцедуры
На клиенте объект уже другой — ДанныеФормыДерево, и методы у него свои:
- 📂
ДеревоТоваров.ПолучитьЭлементы()— строки верхнего уровня, у строки тожеПолучитьЭлементы(); - ⬆️
Элемент.ПолучитьРодителя()вместо свойстваРодитель; - 🎯
Элементы.ДеревоТоваров.ТекущиеДанные— выделенная строка; - 🔑
Элемент.ПолучитьИдентификатор()иДеревоТоваров.НайтиПоИдентификатору(Ид)— чтобы передать строку на сервер и найти её там.
Развернуть все группы после заполнения:
&НаКлиенте
Процедура РазвернутьВсе()
Для Каждого Элемент Из ДеревоТоваров.ПолучитьЭлементы() Цикл
Элементы.ДеревоТоваров.Развернуть(Элемент.ПолучитьИдентификатор(), Истина);
КонецЦикла;
КонецПроцедуры
Сортировать, Итог и НайтиСтроки с подчинёнными на клиенте у ДанныеФормыДерево недоступны. Тяжёлые операции делайте на сервере над ДеревоЗначений и возвращайте результат в реквизит.
Как ускорить работу с деревом значений
Дерево хранится в памяти целиком и индексов не поддерживает. На сотне строк это незаметно, на десятках тысяч — ощутимо.
| Медленно | Быстрее | Почему |
|---|---|---|
Найти(..., Истина) в цикле по каждой строке источника | Соответствие «ключ → строка дерева», заполненное один раз | поиск с подчинёнными каждый раз обходит всё дерево |
| сборка групп циклом по выборке из базы | запрос с ИТОГИ и выгрузка ПоГруппировкам | группировки и суммы считает СУБД |
Итог(..., Истина) в каждой группе | один рекурсивный проход снизу вверх | каждый узел суммируется ровно один раз |
частые ЗначениеВРеквизитФормы | одна передача после полного заполнения | каждая передача копирует всё дерево и гонит его на клиент |
| поиск детей в плоской таблице без индекса | Таблица.Индексы.Добавить("Родитель") | НайтиСтроки перестаёт перебирать таблицу целиком |
Ещё три ошибки, которые дают неверный результат, а не просто медленный:
- ❌ удаление строк в цикле
Для Каждого— часть строк пропускается; - ❌ колонка без типа, куда попала строка «0» вместо числа, —
Итог()и сортировка по сумме врут; - ❌ сравнение
Строка.Родитель = Неопределеноу строки формы на клиенте — там нуженПолучитьРодителя().
Частые вопросы
Чем дерево значений отличается от таблицы значений?
Свернуть(). У дерева строки вложены друг в друга, индексов нет, зато иерархия хранится без служебных колонок.Как сохранить дерево значений между сеансами?
ХранилищеЗначения и запишите в реквизит объекта или регистр сведений. Для передачи строкой подходит ЗначениеВСтрокуВнутр().Как посчитать все строки дерева, включая вложенные?
Строки.Количество() и вызывайте функцию для каждой строки. Готового метода для полного подсчёта нет.Почему после запроса с ИТОГИ в дереве есть строки без номенклатуры?
Строки.Количество() > 0.