Дерево значений в 1С 8.3: от создания до вывода на форму и в отчёт
Содержание
Показать дерево значений в 1С можно тремя способами: на управляемой форме через реквизит типа ДеревоЗначений и ЗначениеВРеквизитФормы(), в табличном документе через группировки строк НачатьГруппуСтрок() и в отладчике — через «Вычислить выражение» (Shift+F9) или табло. Для пользователя чаще всего нужна форма, для печати — табличный документ, для разработчика — отладчик.
Ниже — рабочий код для каждого варианта на платформе 8.3 (тонкий клиент, интерфейс «Такси»), плюс рекурсивный обход, сортировка, отбор и разбор ошибок, на которых спотыкаются чаще всего.
Как устроено дерево значений в 1С
ДеревоЗначений — универсальная коллекция с иерархией. У неё две главные части:
- 📐 Колонки — общий для всех уровней набор полей. Колонка добавляется один раз и есть у каждой строки на любой глубине.
- 🌳 Строки — коллекция строк верхнего уровня. У каждой строки есть своё свойство
Строкис подчинёнными строками.
Полезные свойства и методы строки дерева:
| Что | Назначение |
|---|---|
Строка.Строки | Подчинённые строки (коллекция того же типа) |
Строка.Родитель | Строка-родитель; у верхнего уровня — Неопределено |
Строка.Уровень() | Глубина: 0 — верхний уровень, 1 — следующий и т. д. |
Строки.Добавить() | Новая строка в конец уровня |
Строки.Вставить(Индекс) | Новая строка на нужную позицию |
Строки.Удалить(Строка) | Удаляет строку вместе со всеми потомками |
Строки.Количество() | Число строк только этого уровня, без вложенных |
Дерево значений доступно на сервере, в толстом клиенте и во внешнем соединении. В тонком и веб-клиенте его нет. Поэтому дерево всегда собирают на сервере, а на клиент передают уже в виде данных формы.
Создание и заполнение дерева значений кодом
Сначала описываем колонки, потом добавляем строки. Типизируйте колонки через ОписаниеТипов: без этого сортировка и итоги работают непредсказуемо, а на форме колонка не получит нормального поля ввода.
&НаСервереБезКонтекста
Функция ПостроитьДерево()
Дерево = Новый ДеревоЗначений;
Дерево.Колонки.Добавить("Наименование", Новый ОписаниеТипов("Строка"), "Наименование");
Дерево.Колонки.Добавить("Сумма", Новый ОписаниеТипов("Число"), "Сумма");
// Верхний уровень — подразделение
Отдел = Дерево.Строки.Добавить();
Отдел.Наименование = "Отдел продаж";
// Второй уровень — сотрудники
Сотрудник = Отдел.Строки.Добавить();
Сотрудник.Наименование = "Иванов И. И.";
Сотрудник.Сумма = 50000;
Сотрудник = Отдел.Строки.Добавить();
Сотрудник.Наименование = "Петрова А. С.";
Сотрудник.Сумма = 42000;
Отдел.Сумма = Отдел.Строки.Итог("Сумма"); // 92000
Возврат Дерево;
КонецФункции
Дерево из запроса с итогами
Ручное заполнение нужно редко. Обычно дерево получают из запроса с секцией ИТОГИ ПО, выгружая результат с обходом по группировкам:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Продажи.Номенклатура.Родитель КАК Группа,
| Продажи.Номенклатура КАК Номенклатура,
| СУММА(Продажи.Сумма) КАК Сумма
|ИЗ
| РегистрНакопления.Продажи.Обороты КАК Продажи
|СГРУППИРОВАТЬ ПО
| Продажи.Номенклатура.Родитель,
| Продажи.Номенклатура
|ИТОГИ
| СУММА(Сумма)
|ПО
| Группа";
Дерево = Запрос.Выполнить().Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам);
Колонки дерева создадутся по полям запроса автоматически, а итоги по группе запишутся в строку верхнего уровня. Если группировать по иерархическому справочнику и нужны все уровни групп, используйте ИТОГИ ... ПО Номенклатура ИЕРАРХИЯ и обход ОбходРезультатаЗапроса.ПоГруппировкамСИерархией.
Имя регистра Продажи в примере условное. Подставьте реальный регистр своей конфигурации — его видно в дереве метаданных конфигуратора, ветка «Регистры накопления».
Вывод дерева значений на управляемую форму
Это самый частый сценарий: пользователь должен увидеть иерархию, раскрывать и сворачивать узлы.
Шаг 1. Реквизит формы
- Откройте форму в конфигураторе, вкладка «Реквизиты».
- Добавьте реквизит, например
ДеревоДанных, тип —ДеревоЗначений. - Внутри реквизита добавьте подчинённые реквизиты-колонки:
Наименование(Строка) иСумма(Число). Имена должны совпадать с именами колонок дерева, которое вы будете загружать. - Перетащите реквизит на вкладку «Элементы». Платформа создаст таблицу формы, у которой в свойствах отображение уже будет «Дерево».
Шаг 2. Загрузка дерева в реквизит
&НаСервере
Процедура ЗаполнитьДеревоНаСервере()
Дерево = ПостроитьДерево();
ЗначениеВРеквизитФормы(Дерево, "ДеревоДанных");
КонецПроцедуры
&НаКлиенте
Процедура Заполнить(Команда)
ЗаполнитьДеревоНаСервере();
РазвернутьВсе();
КонецПроцедуры
Обратное преобразование — когда нужно поработать с данными формы как с обычным деревом на сервере:
Дерево = РеквизитФормыВЗначение("ДеревоДанных");
// ... изменения ...
ЗначениеВРеквизитФормы(Дерево, "ДеревоДанных");
Шаг 3. Раскрыть узлы при открытии
По умолчанию узлы свёрнуты, и пользователь видит только верхний уровень. Раскрыть всё можно с клиента методом таблицы формы Развернуть():
&НаКлиенте
Процедура РазвернутьВсе()
Для Каждого Элемент Из ДеревоДанных.ПолучитьЭлементы() Цикл
Элементы.ДеревоДанных.Развернуть(Элемент.ПолучитьИдентификатор(), Истина);
КонецЦикла;
КонецПроцедуры
Второй параметр Истина раскрывает и все вложенные уровни. Свернуть узел — метод Свернуть() с идентификатором строки.
На клиенте реквизит имеет тип ДанныеФормыДерево, а не ДеревоЗначений. Вместо .Строки у него ПолучитьЭлементы(), вместо .Родитель — ПолучитьРодителя(). Методов Найти, Сортировать, Итог там нет — всё это делают на сервере.
Колонки создаются программно
Если состав колонок заранее неизвестен (например, по периодам отчёта), подчинённые реквизиты добавляют кодом через ИзменитьРеквизиты(), а затем создают для них поля через Элементы.Добавить(). Это отдельная техника, но порядок один: сначала реквизиты, потом элементы, потом ЗначениеВРеквизитФормы().
Большое дерево: подгрузка по раскрытию
Если в дереве десятки тысяч строк, грузить всё сразу долго: каждая передача формы гоняет все данные между сервером и клиентом. Практичнее подгружать ветку в момент раскрытия.
- При открытии формы выведите только верхний уровень. В каждую группу добавьте одну пустую строку-заглушку, чтобы у узла появился значок раскрытия.
- В таблице формы назначьте обработчик события
ПередРазворачиванием. Он получает идентификатор раскрываемой строки. - В обработчике проверьте, лежит ли в узле заглушка. Если да — вызовите серверную процедуру, которая удалит заглушку и добавит реальных потомков этой ветки.
Так пользователь ждёт загрузки только той ветки, которую открыл, а форма остаётся лёгкой.
Как показать дерево в табличном документе
Для печатной формы или отчёта без СКД дерево выводят в ТабличныйДокумент рекурсивно, а иерархию оформляют группировками строк. Пользователь получит плюсики слева, как в стандартных отчётах.
&НаСервере
Процедура ВывестиУровень(ТабДок, Строки)
Для Каждого Строка Из Строки Цикл
Отступ = СтрПовторить(" ", Строка.Уровень());
ТабДок.Вывести(ПолучитьОбласть(ТабДок, Отступ + Строка.Наименование, Строка.Сумма));
Если Строка.Строки.Количество() > 0 Тогда
ТабДок.НачатьГруппуСтрок(, Истина);
ВывестиУровень(ТабДок, Строка.Строки);
ТабДок.ЗакончитьГруппуСтрок();
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Функцию ПолучитьОбласть() обычно пишут под свой макет: берут область строки из макета, заполняют параметры и возвращают её. Второй параметр НачатьГруппуСтрок — Истина, если группа должна быть раскрыта сразу.
Если отчёт строится на СКД, дерево руками рисовать не нужно: компоновщик сам выведет группировки с иерархией. А получить результат СКД именно деревом значений можно процессором вывода ПроцессорВыводаРезультатаКомпоновкиДанныхВКоллекциюЗначений с методом УстановитьОбъект(Дерево).
Рекурсивный обход узлов дерева
Метод Количество() считает только строки текущего уровня. Чтобы пройти все узлы, нужна рекурсия: процедура вызывает сама себя для Строка.Строки.
Процедура ОбойтиДерево(Строки, Результат)
Для Каждого Строка Из Строки Цикл
Результат.Добавить(Строка.Наименование);
ОбойтиДерево(Строка.Строки, Результат);
КонецЦикла;
КонецПроцедуры
// Вызов
Список = Новый Массив;
ОбойтиДерево(Дерево.Строки, Список);
Пересчёт итогов снизу вверх
Частая задача — посчитать суммы групп по листьям. Рекурсия здесь возвращает сумму поддерева:
Функция ПересчитатьИтоги(Строки)
Всего = 0;
Для Каждого Строка Из Строки Цикл
Если Строка.Строки.Количество() > 0 Тогда
Строка.Сумма = ПересчитатьИтоги(Строка.Строки);
КонецЕсли;
Всего = Всего + Строка.Сумма;
КонецЦикла;
Возврат Всего;
КонецФункции
Для группы с двумя листьями по 50 000 и 42 000 функция запишет 92 000 и вернёт это же число уровнем выше.
Удаление строк при обходе
Удалять строки в цикле Для Каждого нельзя: коллекция сдвигается, и часть строк будет пропущена. Идите с конца по индексам:
Процедура УдалитьПустые(Строки)
Для Сч = 1 По Строки.Количество() Цикл
Строка = Строки[Строки.Количество() - Сч];
УдалитьПустые(Строка.Строки);
Если Строка.Сумма = 0 И Строка.Строки.Количество() = 0 Тогда
Строки.Удалить(Строка);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Граница цикла вычисляется один раз при входе. Индекс Количество() - Сч берётся заново на каждом шаге, поэтому после удаления последней строки следующей становится предыдущая. Сначала чистим детей, потом проверяем родителя: так удаляются и группы, которые опустели после чистки.
Сортировка и отбор иерархических данных
Сортировка
Дерево.Строки.Сортировать("Сумма Убыв, Наименование", Истина);
Первый параметр — список колонок через запятую с направлением Возр или Убыв. Второй — Истина, чтобы сортировать и все вложенные уровни. Без него упорядочится только верхний уровень.
Сортировать можно и отдельную ветку: СтрокаОтдела.Строки.Сортировать("Наименование").
Поиск
| Метод | Что делает |
|---|---|
Строки.Найти(Значение, "Колонка", Истина) | Первая строка с этим значением, Истина — искать во вложенных |
Строки.НайтиСтроки(Структура, Истина) | Массив всех строк по условию «колонка = значение» |
Отбор = Новый Структура("Наименование", "Иванов И. И.");
Найденные = Дерево.Строки.НайтиСтроки(Отбор, Истина);
Отбор по условию
Готового отбора «сумма больше N» у дерева нет: НайтиСтроки проверяет только равенство. Варианты:
- ✂️ Удалить лишнее из копии дерева процедурой обхода с конца, как выше. Копию делают методом
Скопировать(), чтобы не портить исходник. - 🧱 Собрать новое дерево рекурсией: переносить строку, если она подходит под условие или у неё есть подходящие потомки.
- 🗄️ Отобрать в запросе — самый быстрый вариант для больших объёмов: условие в
ГДЕилиИМЕЮЩИЕ, а дерево уже получается отфильтрованным.
Сборка отфильтрованного дерева:
Функция СкопироватьПоУсловию(Источник, Приемник, Порог)
ЕстьПодходящие = Ложь;
Для Каждого Строка Из Источник Цикл
Новая = Приемник.Добавить();
ЗаполнитьЗначенияСвойств(Новая, Строка);
ДетиПодходят = СкопироватьПоУсловию(Строка.Строки, Новая.Строки, Порог);
Если Строка.Сумма > Порог Или ДетиПодходят Тогда
ЕстьПодходящие = Истина;
Иначе
Приемник.Удалить(Новая);
КонецЕсли;
КонецЦикла;
Возврат ЕстьПодходящие;
КонецФункции
Приёмник — пустое дерево с теми же колонками. Его удобно получить так: Результат = Дерево.Скопировать(); Результат.Строки.Очистить();.
Как посмотреть дерево значений в отладчике
Когда дерево собирается неправильно, проще всего увидеть его содержимое прямо во время выполнения.
- Поставьте точку останова (
F9) на строке после заполнения дерева. - Запустите отладку (
F5) и дождитесь остановки. - Выделите переменную и нажмите
Shift+F9— откроется окно вычисления выражения. - Раскрывайте свойство
Строки, затем строки по индексу и ихСтроки— так видна вся вложенность. - Чтобы следить за деревом на каждом шаге, добавьте переменную в табло отладки.
Быстрые проверки прямо в окне выражения:
Дерево.Строки.Количество()— сколько строк на верхнем уровне;Дерево.Колонки.Количество()— создались ли колонки из запроса;Дерево.Строки[0].Строки[0].Уровень()— правильная ли глубина.