ДеревоЗначений во встроенном языке 1С 8.3: от первой колонки до быстрой выборки на тысячи узлов

· 9 мин

Содержание

Дерево значений в 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» вместо числа, — Итог() и сортировка по сумме врут;
  • ❌ сравнение Строка.Родитель = Неопределено у строки формы на клиенте — там нужен ПолучитьРодителя().
Проверьте себя
Что вернёт Дерево.Строки.Количество(), если в дереве 3 группы и 120 вложенных строк?
Как найти строку на любом уровне дерева?
Можно ли присвоить строке новое значение свойства Родитель?

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

Чем дерево значений отличается от таблицы значений?
У таблицы строки в одном уровне, есть индексы и метод Свернуть(). У дерева строки вложены друг в друга, индексов нет, зато иерархия хранится без служебных колонок.
Как сохранить дерево значений между сеансами?
Поместите его в ХранилищеЗначения и запишите в реквизит объекта или регистр сведений. Для передачи строкой подходит ЗначениеВСтрокуВнутр().
Как посчитать все строки дерева, включая вложенные?
Рекурсией: на каждом уровне прибавляйте Строки.Количество() и вызывайте функцию для каждой строки. Готового метода для полного подсчёта нет.
Почему после запроса с ИТОГИ в дереве есть строки без номенклатуры?
Это строки группировок: в них заполнено поле группы и итоговые суммы, а детальные поля пусты. Отличить их можно по Строки.Количество() > 0.
Можно ли передать ДеревоЗначений на тонкий клиент?
Нет, этот тип там недоступен. Передайте его через реквизит формы типа «Дерево значений» или переведите в массив структур.