Дерево значений в 1С 8.3: от создания до вывода на форму и в отчёт

· 9 мин

Содержание

Показать дерево значений в 1С можно тремя способами: на управляемой форме через реквизит типа ДеревоЗначений и ЗначениеВРеквизитФормы(), в табличном документе через группировки строк НачатьГруппуСтрок() и в отладчике — через «Вычислить выражение» (Shift+F9) или табло. Для пользователя чаще всего нужна форма, для печати — табличный документ, для разработчика — отладчик.

Ниже — рабочий код для каждого варианта на платформе 8.3 (тонкий клиент, интерфейс «Такси»), плюс рекурсивный обход, сортировка, отбор и разбор ошибок, на которых спотыкаются чаще всего.

Как устроено дерево значений в 1С

ДеревоЗначений — универсальная коллекция с иерархией. У неё две главные части:

  • 📐 Колонки — общий для всех уровней набор полей. Колонка добавляется один раз и есть у каждой строки на любой глубине.
  • 🌳 Строки — коллекция строк верхнего уровня. У каждой строки есть своё свойство Строки с подчинёнными строками.

Полезные свойства и методы строки дерева:

ЧтоНазначение
Строка.СтрокиПодчинённые строки (коллекция того же типа)
Строка.РодительСтрока-родитель; у верхнего уровня — Неопределено
Строка.Уровень()Глубина: 0 — верхний уровень, 1 — следующий и т. д.
Строки.Добавить()Новая строка в конец уровня
Строки.Вставить(Индекс)Новая строка на нужную позицию
Строки.Удалить(Строка)Удаляет строку вместе со всеми потомками
Строки.Количество()Число строк только этого уровня, без вложенных

Дерево значений доступно на сервере, в толстом клиенте и во внешнем соединении. В тонком и веб-клиенте его нет. Поэтому дерево всегда собирают на сервере, а на клиент передают уже в виде данных формы.

Создание и заполнение дерева значений кодом

Сначала описываем колонки, потом добавляем строки. Типизируйте колонки через ОписаниеТипов: без этого сортировка и итоги работают непредсказуемо, а на форме колонка не получит нормального поля ввода.

&НаСервереБезКонтекста
Функция ПостроитьДерево()

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

    // Верхний уровень — подразделение
    Отдел = Дерево.Строки.Добавить();
    Отдел.Наименование = "Отдел продаж";

    // Второй уровень — сотрудники
    Сотрудник = Отдел.Строки.Добавить();
    Сотрудник.Наименование = "Иванов И. И.";
    Сотрудник.Сумма = 50000;

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

    Отдел.Сумма = Отдел.Строки.Итог("Сумма");   // 92000

    Возврат Дерево;

КонецФункции

Дерево из запроса с итогами

Ручное заполнение нужно редко. Обычно дерево получают из запроса с секцией ИТОГИ ПО, выгружая результат с обходом по группировкам:

Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
|    Продажи.Номенклатура.Родитель КАК Группа,
|    Продажи.Номенклатура КАК Номенклатура,
|    СУММА(Продажи.Сумма) КАК Сумма
|ИЗ
|    РегистрНакопления.Продажи.Обороты КАК Продажи
|СГРУППИРОВАТЬ ПО
|    Продажи.Номенклатура.Родитель,
|    Продажи.Номенклатура
|ИТОГИ
|    СУММА(Сумма)
|ПО
|    Группа";

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

Колонки дерева создадутся по полям запроса автоматически, а итоги по группе запишутся в строку верхнего уровня. Если группировать по иерархическому справочнику и нужны все уровни групп, используйте ИТОГИ ... ПО Номенклатура ИЕРАРХИЯ и обход ОбходРезультатаЗапроса.ПоГруппировкамСИерархией.

Имя регистра Продажи в примере условное. Подставьте реальный регистр своей конфигурации — его видно в дереве метаданных конфигуратора, ветка «Регистры накопления».

Вывод дерева значений на управляемую форму

Это самый частый сценарий: пользователь должен увидеть иерархию, раскрывать и сворачивать узлы.

Шаг 1. Реквизит формы

  1. Откройте форму в конфигураторе, вкладка «Реквизиты».
  2. Добавьте реквизит, например ДеревоДанных, тип — ДеревоЗначений.
  3. Внутри реквизита добавьте подчинённые реквизиты-колонки: Наименование (Строка) и Сумма (Число). Имена должны совпадать с именами колонок дерева, которое вы будете загружать.
  4. Перетащите реквизит на вкладку «Элементы». Платформа создаст таблицу формы, у которой в свойствах отображение уже будет «Дерево».

Шаг 2. Загрузка дерева в реквизит

&НаСервере
Процедура ЗаполнитьДеревоНаСервере()

    Дерево = ПостроитьДерево();
    ЗначениеВРеквизитФормы(Дерево, "ДеревоДанных");

КонецПроцедуры

&НаКлиенте
Процедура Заполнить(Команда)

    ЗаполнитьДеревоНаСервере();
    РазвернутьВсе();

КонецПроцедуры

Обратное преобразование — когда нужно поработать с данными формы как с обычным деревом на сервере:

Дерево = РеквизитФормыВЗначение("ДеревоДанных");
// ... изменения ...
ЗначениеВРеквизитФормы(Дерево, "ДеревоДанных");

Шаг 3. Раскрыть узлы при открытии

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

&НаКлиенте
Процедура РазвернутьВсе()

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

КонецПроцедуры

Второй параметр Истина раскрывает и все вложенные уровни. Свернуть узел — метод Свернуть() с идентификатором строки.

На клиенте реквизит имеет тип ДанныеФормыДерево, а не ДеревоЗначений. Вместо .Строки у него ПолучитьЭлементы(), вместо .Родитель — ПолучитьРодителя(). Методов Найти, Сортировать, Итог там нет — всё это делают на сервере.

Колонки создаются программно

Если состав колонок заранее неизвестен (например, по периодам отчёта), подчинённые реквизиты добавляют кодом через ИзменитьРеквизиты(), а затем создают для них поля через Элементы.Добавить(). Это отдельная техника, но порядок один: сначала реквизиты, потом элементы, потом ЗначениеВРеквизитФормы().

Большое дерево: подгрузка по раскрытию

Если в дереве десятки тысяч строк, грузить всё сразу долго: каждая передача формы гоняет все данные между сервером и клиентом. Практичнее подгружать ветку в момент раскрытия.

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

Так пользователь ждёт загрузки только той ветки, которую открыл, а форма остаётся лёгкой.

Как показать дерево в табличном документе

Для печатной формы или отчёта без СКД дерево выводят в ТабличныйДокумент рекурсивно, а иерархию оформляют группировками строк. Пользователь получит плюсики слева, как в стандартных отчётах.

&НаСервере
Процедура ВывестиУровень(ТабДок, Строки)

    Для Каждого Строка Из Строки Цикл

        Отступ = СтрПовторить("    ", Строка.Уровень());
        ТабДок.Вывести(ПолучитьОбласть(ТабДок, Отступ + Строка.Наименование, Строка.Сумма));

        Если Строка.Строки.Количество() > 0 Тогда
            ТабДок.НачатьГруппуСтрок(, Истина);
            ВывестиУровень(ТабДок, Строка.Строки);
            ТабДок.ЗакончитьГруппуСтрок();
        КонецЕсли;

    КонецЦикла;

КонецПроцедуры

Функцию ПолучитьОбласть() обычно пишут под свой макет: берут область строки из макета, заполняют параметры и возвращают её. Второй параметр НачатьГруппуСтрок — Истина, если группа должна быть раскрыта сразу.

Если отчёт строится на СКД, дерево руками рисовать не нужно: компоновщик сам выведет группировки с иерархией. А получить результат СКД именно деревом значений можно процессором вывода ПроцессорВыводаРезультатаКомпоновкиДанныхВКоллекциюЗначений с методом УстановитьОбъект(Дерево).

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

Метод Количество() считает только строки текущего уровня. Чтобы пройти все узлы, нужна рекурсия: процедура вызывает сама себя для Строка.Строки.

Процедура ОбойтиДерево(Строки, Результат)

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

КонецПроцедуры

// Вызов
Список = Новый Массив;
ОбойтиДерево(Дерево.Строки, Список);

Пересчёт итогов снизу вверх

Частая задача — посчитать суммы групп по листьям. Рекурсия здесь возвращает сумму поддерева:

Функция ПересчитатьИтоги(Строки)

    Всего = 0;
    Для Каждого Строка Из Строки Цикл
        Если Строка.Строки.Количество() > 0 Тогда
            Строка.Сумма = ПересчитатьИтоги(Строка.Строки);
        КонецЕсли;
        Всего = Всего + Строка.Сумма;
    КонецЦикла;
    Возврат Всего;

КонецФункции

Для группы с двумя листьями по 50 000 и 42 000 функция запишет 92 000 и вернёт это же число уровнем выше.

Удаление строк при обходе

Удалять строки в цикле Для Каждого нельзя: коллекция сдвигается, и часть строк будет пропущена. Идите с конца по индексам:

Процедура УдалитьПустые(Строки)

    Для Сч = 1 По Строки.Количество() Цикл
        Строка = Строки[Строки.Количество() - Сч];
        УдалитьПустые(Строка.Строки);
        Если Строка.Сумма = 0 И Строка.Строки.Количество() = 0 Тогда
            Строки.Удалить(Строка);
        КонецЕсли;
    КонецЦикла;

КонецПроцедуры

Граница цикла вычисляется один раз при входе. Индекс Количество() - Сч берётся заново на каждом шаге, поэтому после удаления последней строки следующей становится предыдущая. Сначала чистим детей, потом проверяем родителя: так удаляются и группы, которые опустели после чистки.

Сортировка и отбор иерархических данных

Сортировка

Дерево.Строки.Сортировать("Сумма Убыв, Наименование", Истина);

Первый параметр — список колонок через запятую с направлением Возр или Убыв. Второй — Истина, чтобы сортировать и все вложенные уровни. Без него упорядочится только верхний уровень.

Сортировать можно и отдельную ветку: СтрокаОтдела.Строки.Сортировать("Наименование").

Поиск

МетодЧто делает
Строки.Найти(Значение, "Колонка", Истина)Первая строка с этим значением, Истина — искать во вложенных
Строки.НайтиСтроки(Структура, Истина)Массив всех строк по условию «колонка = значение»
Отбор = Новый Структура("Наименование", "Иванов И. И.");
Найденные = Дерево.Строки.НайтиСтроки(Отбор, Истина);

Отбор по условию

Готового отбора «сумма больше N» у дерева нет: НайтиСтроки проверяет только равенство. Варианты:

  • ✂️ Удалить лишнее из копии дерева процедурой обхода с конца, как выше. Копию делают методом Скопировать(), чтобы не портить исходник.
  • 🧱 Собрать новое дерево рекурсией: переносить строку, если она подходит под условие или у неё есть подходящие потомки.
  • 🗄️ Отобрать в запросе — самый быстрый вариант для больших объёмов: условие в ГДЕ или ИМЕЮЩИЕ, а дерево уже получается отфильтрованным.

Сборка отфильтрованного дерева:

Функция СкопироватьПоУсловию(Источник, Приемник, Порог)

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

КонецФункции

Приёмник — пустое дерево с теми же колонками. Его удобно получить так: Результат = Дерево.Скопировать(); Результат.Строки.Очистить();.

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

Когда дерево собирается неправильно, проще всего увидеть его содержимое прямо во время выполнения.

  1. Поставьте точку останова (F9) на строке после заполнения дерева.
  2. Запустите отладку (F5) и дождитесь остановки.
  3. Выделите переменную и нажмите Shift+F9 — откроется окно вычисления выражения.
  4. Раскрывайте свойство Строки, затем строки по индексу и их Строки — так видна вся вложенность.
  5. Чтобы следить за деревом на каждом шаге, добавьте переменную в табло отладки.

Быстрые проверки прямо в окне выражения:

  • Дерево.Строки.Количество() — сколько строк на верхнем уровне;
  • Дерево.Колонки.Количество() — создались ли колонки из запроса;
  • Дерево.Строки[0].Строки[0].Уровень() — правильная ли глубина.

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

Диагностика: выберите симптом

Вопросы по дереву значений в 1С

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