Дерево значений в 1С: развернуть в форме и обойти в коде

· 9 мин

Содержание

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

Ниже оба сценария разобраны на коротких примерах: как собрать дерево, как раскрыть его в форме, как превратить в таблицу и где чаще всего ломается код. Примеры рассчитаны на платформу 8.3 с интерфейсом «Такси», управляемые формы.

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

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

Главное отличие от таблицы: нет единого индекса строк. Строка с номером 3 в корне и строка с номером 3 внутри ветки — разные объекты. Поэтому «просто пройти по всем» циклом Для Каждого нельзя: он обходит только один уровень.

ПризнакТаблица значенийДерево значений
Структураплоский списоквложенные ветки
Доступ к строкамТаблица[Индекс]Дерево.Строки[Индекс], затем Строка.Строки[...]
Родитель строкинетСтрока.Родитель
Обход Для Каждоговсе строкитолько текущий уровень

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

Колонки добавляют методом Колонки.Добавить(), строки — методом Строки.Добавить() у дерева или у родительской строки. Небольшой пример: подразделения и сотрудники.

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

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

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

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

Колонки можно не создавать вручную, если дерево получено из запроса или скопировано: метод СкопироватьКолонки() создаёт пустое дерево с той же структурой.

Откуда берут данные для дерева

Три типичных источника:

  • 📋 Результат запроса с итогами. Раздел ИТОГИ ПО в тексте запроса и выгрузка с обходом по группировкам даёт готовое дерево.
  • 📁 Иерархический справочник. Запрос к справочнику с ИТОГИ ПО Ссылка ТОЛЬКО ИЕРАРХИЯ или с условием по Родитель.
  • 🔧 Ручная сборка. Данные из файла, макета или внешнего источника, где родителя находят по ключу.

Пример запроса, который строит иерархию по справочнику номенклатуры с суммой остатков:

Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
|	Номенклатура.Ссылка КАК Номенклатура,
|	Номенклатура.Наименование КАК Наименование
|ИЗ
|	Справочник.Номенклатура КАК Номенклатура
|
|УПОРЯДОЧИТЬ ПО
|	Номенклатура.Наименование
|
|ИТОГИ ПО
|	Номенклатура ТОЛЬКО ИЕРАРХИЯ";

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

Метод Выгрузить() с параметром ОбходРезультатаЗапроса.ПоГруппировкамСИерархией возвращает именно ДеревоЗначений. Если передать ОбходРезультатаЗапроса.Прямой, получится плоская таблица.

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

Как развернуть дерево значений в форме

Сначала дерево нужно показать на форме. Для этого создают реквизит формы типа ДеревоЗначений, добавляют на форму элемент «Таблица» (вид — дерево) и привязывают к реквизиту. Данные передают из серверного кода:

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

После заполнения все ветки по умолчанию свёрнуты. Раскрывает их метод Развернуть() у элемента формы. Он принимает идентификатор строки и флаг «включая подчинённые».

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

Второй параметр Истина раскрывает строку со всеми вложенными ветками. Если передать Ложь, раскроется только один уровень.

Обратная операция — Свернуть() с тем же набором параметров.

Когда вызывать раскрытие

Вызов сразу в процедуре заполнения на сервере не сработает: метод Развернуть() относится к элементу формы и работает на клиенте. Типичные места вызова:

  1. Обработчик ПриОткрытии формы — после того как данные уже заполнены.
  2. Обработчик кнопки «Развернуть все» в командной панели формы.
  3. Оповещение или обработчик ожидания после асинхронного заполнения.

Если дерево заполняют в ПриСозданииНаСервере, то раскрывать его нужно в ПриОткрытии, а не там же.

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

Теперь вторая трактовка — дерево нужно выгрузить в таблицу значений, чтобы вывести в отчёт, сохранить или передать дальше. Есть два способа.

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

Самый короткий вариант. Функция сама вызывает себя для каждого уровня и добавляет строки в общую таблицу.

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

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

ОбойтиСтроки(Дерево.Строки, Таблица, 0);

Колонка Уровень помогает потом отрисовать отступы или восстановить иерархию. Порядок строк в таблице — «сверху вниз», родитель стоит перед своими детьми.

Обход без рекурсии, через стек

Если дерево очень глубокое, рекурсия может упереться в ограничение глубины вызовов. Тогда используют массив-стек: в него кладут строки, которые ещё не обработаны.

Стек = Новый Массив;
Для Сч = 1 По Дерево.Строки.Количество() Цикл
	Стек.Добавить(Дерево.Строки[Дерево.Строки.Количество() - Сч]);
КонецЦикла;

Пока Стек.Количество() > 0 Цикл
	Последний = Стек.Количество() - 1;
	Строка = Стек[Последний];
	Стек.Удалить(Последний);

	НоваяСтрока = Таблица.Добавить();
	НоваяСтрока.Наименование = Строка.Наименование;
	НоваяСтрока.Сумма = Строка.Сумма;

	Для Сч = 1 По Строка.Строки.Количество() Цикл
		Стек.Добавить(Строка.Строки[Строка.Строки.Количество() - Сч]);
	КонецЦикла;
КонецЦикла;

Детей кладут в обратном порядке, чтобы сверху стека оказался первый. Так порядок вывода совпадает с рекурсивным вариантом.

СпособПлюсыМинусы
Рекурсиякороткий код, легко читатьглубина вызовов ограничена
Стекработает на любой глубинечуть сложнее код

Свойства узлов и поиск по дереву

В готовом дереве часто нужно найти строку или посчитать итоги по ветке. Для этого есть несколько методов:

  • 🔎 Дерево.Строки.Найти(Значение, "Колонка", Истина) — поиск по значению колонки; третий параметр Истина включает рекурсивный поиск по всем уровням.
  • 🧮 Итог по ветке считайте сами рекурсивной функцией: так вы точно знаете, какие уровни вошли в сумму.
  • ⬆️ Строка.Родитель — подняться на уровень выше; на верхнем уровне вернёт Неопределено, и перед обращением к свойствам родителя это нужно проверять.
  • 🗑 Строка.Родитель.Строки.Удалить(Строка) — удалить узел вместе с потомками; у верхнего уровня удаляют через Дерево.Строки.Удалить(Строка).

Для подсчёта итога по узлу удобна рекурсивная функция:

Функция СуммаПоВетке(Строка)
	Итог = Строка.Сумма;
	Для Каждого Потомок Из Строка.Строки Цикл
		Итог = Итог + СуммаПоВетке(Потомок);
	КонецЦикла;
	Возврат Итог;
КонецФункции

Если в колонке родительских строк уже хранится итог, его не нужно прибавлять повторно, иначе суммы удвоятся.

Раскрытие дерева: практические сценарии

Одной кнопки «Развернуть все» обычно мало. На практике встречаются три задачи, и у каждой свой приём.

Раскрыть только верхний уровень

Нужно показать подразделения, но не сотрудников. Вызывайте Развернуть() для строк верхнего уровня со вторым параметром Ложь. Каждая ветка раскроется на один шаг, вложенные останутся свёрнутыми. Пользователь сам решит, куда копать глубже.

Раскрыть путь до найденной строки

Пользователь ввёл фамилию, и нужно показать её в дереве. Найдите строку на сервере методом Найти() с рекурсивным поиском, получите её идентификатор и поднимитесь по цепочке Родитель, раскрывая каждого предка. Затем установите ТекущаяСтрока у элемента формы на найденный идентификатор: форма сама прокрутится к нужному месту.

Порядок действий:

  1. Найдите строку в данных формы и запомните идентификатор.
  2. Пройдите вверх по родителям и вызовите Развернуть() для каждого, начиная с верхнего.
  3. Назначьте найденную строку текущей.

Запомнить состояние раскрытия

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

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

Как выбрать способ

Короткая подсказка, чтобы не писать лишнего:

ЗадачаЧто использовать
Показать пользователю все веткиРазвернуть() у элемента формы, Истина для подчинённых
Показать только первый уровеньРазвернуть() со вторым параметром Ложь
Получить плоский список для отчётарекурсия или стек, результат в таблицу значений
Выгрузить иерархию справочниказапрос с ИТОГИ ПО ... ТОЛЬКО ИЕРАРХИЯ
Собрать дерево из плоских данныхсоответствие «ключ → строка дерева»

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

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

  • ❌ Обход одним циклом. Для Каждого Строка Из Дерево.Строки видит только верхний уровень. Вложенные ветки остаются необработанными, и в результате «теряются» данные.
  • ❌ Раскрытие на сервере. Метод Развернуть() у элемента формы вызывается только на клиенте. Элементы формы на сервере недоступны, поэтому раскрытие выносят в клиентскую процедуру.
  • ❌ Удаление во время обхода. Если удалять строки внутри Для Каждого по той же коллекции, порядок сбивается. Сначала соберите нужные строки в массив, потом удаляйте.
  • ❌ Обращение к колонке, которой нет. Ошибка вида «Поле объекта не обнаружено» означает, что колонка не создана в дереве. Проверьте Дерево.Колонки.Найти("Имя").
  • ❌ Склейка числа и строки. "Итого: " + Сумма вызовет ошибку несовпадения типов. Используйте СтрШаблон("Итого: %1", Сумма) или Формат(Сумма, "ЧГ=0").

При массовом удалении строк из дерева, которое привязано к реквизиту формы, работайте с копией данных на сервере. Изменение коллекции данных формы прямо во время цикла может дать неожиданный результат.

Производительность при больших деревьях

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

  1. Не создавайте дерево, если хватает таблицы. Если иерархия нужна только для вывода, возьмите запрос с ИТОГИ и выводите в табличный документ без промежуточной выгрузки.
  2. Не ищите строку циклом на каждом шаге. Построение дерева из плоского набора ускоряет соответствие: ключ — строка дерева. Родителя берут из соответствия за один шаг вместо повторного поиска.
  3. Раскрывайте выборочно. В форме с большим деревом раскрытие всех веток сразу создаёт нагрузку на клиент. Разворачивайте верхние уровни, остальное — по требованию пользователя.
  4. Работайте на сервере. Обход и расчёт итогов выполняйте на сервере, на клиент отдавайте готовый результат.

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

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

Здесь выборка должна идти так, чтобы родитель попадал в неё раньше потомка. В запросе это обеспечивает сортировка с иерархией, иначе часть потомков окажется в корне.

Перед запуском обхода дерева
Выполнено: 0 / 5

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

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