ДеревоЗначений в 1С 8.3: чтение строк, обход веток и поиск узлов на встроенном языке
Содержание
Чтобы получить значение из дерева значений в 1С, сначала доберитесь до нужной строки, а потом прочитайте колонку по имени: Строка.Сумма или Строка["Сумма"]. Строки верхнего уровня лежат в Дерево.Строки, подчинённые — в Строка.Строки у каждого узла. Поэтому всё «получение значения» сводится к двум задачам: найти узел и правильно обратиться к его ячейке.
На управляемой форме синтаксис другой. Там дерево хранится как ДанныеФормыДерево, вместо свойства Строки используется метод ПолучитьЭлементы(), а текущую строку дают Элементы.ИмяДерева.ТекущиеДанные. Ниже — оба варианта, рабочие примеры кода и ошибки, на которых чаще всего спотыкаются.
Как устроен объект ДеревоЗначений
Дерево значений — коллекция в памяти, похожая на таблицу значений, но с иерархией. У каждой строки могут быть свои дочерние строки, у тех — свои, и так на любую глубину.
Объект состоит из трёх частей:
- 🧱
Колонки— коллекция колонок. Одна на всё дерево: у вложенных строк те же колонки, что и у верхних. - 🌳
Строки— коллекция строк верхнего уровня (КоллекцияСтрокДереваЗначений). - 📄
СтрокаДереваЗначений— отдельный узел. В нём лежат значения колонок, ссылка на родителя и своя коллекцияСтроки.
| Объект | Что даёт | Ключевые свойства и методы |
|---|---|---|
ДеревоЗначений | всё дерево целиком | Колонки, Строки, Скопировать() |
КоллекцияСтрокДереваЗначений | строки одного уровня у одного родителя | Добавить(), Количество(), Найти(), НайтиСтроки(), Итог(), Сортировать(), ВыгрузитьКолонку(), Родитель |
СтрокаДереваЗначений | один узел | Родитель, Строки, Уровень(), доступ к колонкам по имени и индексу |
Создадим дерево для примеров: номенклатурные группы, внутри — товары с количеством и суммой.
Дерево = Новый ДеревоЗначений;
Дерево.Колонки.Добавить("Наименование", Новый ОписаниеТипов("Строка"));
Дерево.Колонки.Добавить("Количество", Новый ОписаниеТипов("Число"));
Дерево.Колонки.Добавить("Сумма", Новый ОписаниеТипов("Число"));
Группа = Дерево.Строки.Добавить();
Группа.Наименование = "Канцтовары";
Товар = Группа.Строки.Добавить();
Товар.Наименование = "Бумага А4";
Товар.Количество = 10;
Товар.Сумма = 3500;
Товар = Группа.Строки.Добавить();
Товар.Наименование = "Ручка шариковая";
Товар.Количество = 50;
Товар.Сумма = 1250;
Типизируйте колонки через ОписаниеТипов. Колонка без типа принимает что угодно, и ошибку «строка вместо числа» вы увидите только на этапе сложения или вывода в отчёт.
Доступ к строкам: индекс, перебор, текущая строка
Самый прямой способ получить узел — по индексу внутри его коллекции. Индексы начинаются с нуля.
ПерваяГруппа = Дерево.Строки[0];
ВторойТовар = ПерваяГруппа.Строки[1];
Сообщить(ВторойТовар.Наименование); // Ручка шариковая
Перед обращением по индексу проверяйте размер коллекции. Дерево.Строки[0] на пустом дереве вызовет ошибку Индекс находится за границами диапазона.
Если Дерево.Строки.Количество() > 0 Тогда
ПерваяГруппа = Дерево.Строки[0];
КонецЕсли;
Перебор одного уровня — обычный цикл Для Каждого. Он не заходит во вложенные строки, только в строки текущей коллекции:
Для Каждого Группа Из Дерево.Строки Цикл
Сообщить(Группа.Наименование + ": " + Группа.Строки.Количество() + " поз.");
КонецЦикла;
Номер строки внутри своей коллекции возвращает метод Индекс(): Группа.Строки.Индекс(Товар). Если строка не принадлежит этой коллекции, вернётся -1.
Родитель, подчинённые строки и уровень вложенности
Иерархию читают тремя инструментами:
Строка.Родитель— узел уровнем выше. У строк верхнего уровня он равенНеопределено.Строка.Строки— дочерние узлы. Пустая коллекция означает «лист», последний уровень.Строка.Уровень()— глубина узла. Верхний уровень — 0, его дети — 1 и так далее.
Так можно подняться от любого узла к корню и собрать полный путь:
Функция ПутьКУзлу(Строка)
Путь = Строка.Наименование;
Текущая = Строка.Родитель;
Пока Текущая <> Неопределено Цикл
Путь = Текущая.Наименование + " / " + Путь;
Текущая = Текущая.Родитель;
КонецЦикла;
Возврат Путь;
КонецФункции
Для обхода всех веток сверху вниз нужна рекурсия. Процедура принимает коллекцию строк и вызывает сама себя для каждой коллекции детей:
Процедура ОбойтиДерево(КоллекцияСтрок)
Для Каждого Строка Из КоллекцияСтрок Цикл
Отступ = СтрокаСимволов(" ", Строка.Уровень()); // своя функция, см. ниже
Сообщить(Отступ + Строка.Наименование);
Если Строка.Строки.Количество() > 0 Тогда
ОбойтиДерево(Строка.Строки);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Функция СтрокаСимволов(Символ, Количество)
Результат = "";
Для Сч = 1 По Количество Цикл
Результат = Результат + Символ;
КонецЦикла;
Возврат Результат;
КонецФункции
// Вызов:
ОбойтиДерево(Дерево.Строки);
Отличить группу от листа по пустой колонке ненадёжно: у группы тоже может быть заполнено «Количество». Проверяйте Строка.Строки.Количество() > 0 или храните признак в отдельной колонке ЭтоГруппа.
Чтение ячеек и работа с колонками
У строки дерева три равноценных способа прочитать значение:
| Способ | Пример | Когда удобен |
|---|---|---|
| Через точку | Товар.Сумма | имя колонки известно при написании кода |
| По имени в скобках | Товар["Сумма"] | имя приходит из переменной или настроек |
| По индексу колонки | Товар[2] | универсальный перебор всех колонок |
Перебор всех ячеек строки, не зная заранее, какие колонки есть:
Для Каждого Колонка Из Дерево.Колонки Цикл
Значение = Товар[Колонка.Имя];
Сообщить(Колонка.Имя + " = " + Строка(Значение));
КонецЦикла;
Проверить, есть ли колонка, можно до обращения к ней: Дерево.Колонки.Найти("Сумма") вернёт колонку или Неопределено. Это спасает от ошибки, когда дерево собирается в одном месте кода, а читается в другом.
Чтобы получить все значения одной колонки уровня сразу, используйте ВыгрузитьКолонку() — он вернёт массив:
Названия = Группа.Строки.ВыгрузитьКолонку("Наименование");
Сумму по колонке считает Итог(). Второй параметр включает подчинённые строки:
ИтогоПоГруппе = Группа.Строки.Итог("Сумма"); // только дети первого уровня
ИтогоВсего = Дерево.Строки.Итог("Сумма", Истина); // все строки всех уровней
Итог с подчинёнными строками суммирует и группы, и вложенные товары. Если вы уже записали в строку группы сумму её товаров, Итог("Сумма", Истина) посчитает каждую сумму дважды. В таком дереве берите итог только по верхнему уровню или только по листьям.
Поиск узла: Найти и НайтиСтроки
Метод Найти(Значение, Колонки, ВключатьПодчиненные) возвращает первую подходящую строку или Неопределено. По умолчанию он ищет только в той коллекции, у которой вызван.
// Ищем во всём дереве, в колонке "Наименование"
Узел = Дерево.Строки.Найти("Бумага А4", "Наименование", Истина);
Если Узел <> Неопределено Тогда
Сообщить("Сумма: " + Узел.Сумма);
КонецЕсли;
НайтиСтроки(Отбор, ВключатьПодчиненные) возвращает массив всех совпадений. Отбор — структура «колонка → значение», условия объединяются через «И»:
Отбор = Новый Структура("Количество", 10);
Найденные = Дерево.Строки.НайтиСтроки(Отбор, Истина);
Для Каждого Строка Из Найденные Цикл
Сообщить(ПутьКУзлу(Строка) + " — " + Строка.Сумма);
КонецЦикла;
| Метод | Что вернёт | Условие | Глубина |
|---|---|---|---|
Найти() | одну строку или Неопределено | одно значение в одной или нескольких колонках | по третьему параметру |
НайтиСтроки() | массив строк (может быть пустым) | точное равенство по всем полям структуры | по второму параметру |
| Своя рекурсия | что угодно | любые условия: «больше», «содержит», по типу | вся иерархия |
Оба метода сравнивают на точное равенство. Для условий вида «сумма больше 1000» или «наименование содержит подстроку» пишите обход вручную, как в предыдущем разделе, и проверяйте условие внутри цикла.
Дерево значений на управляемой форме
На форме дерево — это реквизит типа ДеревоЗначений, но в модуле формы на клиенте вы работаете с объектом ДанныеФормыДерево. Отличия от «серверного» дерева:
- 🔁 вместо свойства
Строки— методПолучитьЭлементы(); - ⬆️ вместо свойства
Родитель— методПолучитьРодителя(); - 🆔 у строки есть идентификатор
ПолучитьИдентификатор(), по нему строку находитНайтиПоИдентификатору(); - 🚫 методов
Найти(),НайтиСтроки()иИтог()у коллекции элементов формы нет.
Получить значение выделенной пользователем строки:
&НаКлиенте
Процедура ДеревоТоваровВыбор(Элемент, ВыбраннаяСтрока, Поле, СтандартнаяОбработка)
ТекДанные = Элементы.ДеревоТоваров.ТекущиеДанные;
Если ТекДанные = Неопределено Тогда
Возврат;
КонецЕсли;
Сообщить(ТекДанные.Наименование + ": " + ТекДанные.Сумма);
Родитель = ТекДанные.ПолучитьРодителя();
Если Родитель <> Неопределено Тогда
Сообщить("Группа: " + Родитель.Наименование);
КонецЕсли;
КонецПроцедуры
Элементы.ДеревоТоваров.ТекущаяСтрока — это не сама строка, а её идентификатор (число). Строку по нему получают так: ДеревоТоваров.НайтиПоИдентификатору(Элементы.ДеревоТоваров.ТекущаяСтрока).
Если нужен полноценный поиск или итоги, переведите реквизит в обычное дерево на сервере и обратно:
&НаСервере
Процедура ПересчитатьИтогиНаСервере()
Дерево = РеквизитФормыВЗначение("ДеревоТоваров");
Для Каждого Группа Из Дерево.Строки Цикл
Группа.Сумма = Группа.Строки.Итог("Сумма");
КонецЦикла;
ЗначениеВРеквизитФормы(Дерево, "ДеревоТоваров");
КонецПроцедуры
Развернуть ветки на форме можно методом элемента: Элементы.ДеревоТоваров.Развернуть(Идентификатор, Истина). Второй параметр раскрывает и всех потомков. Для раскрытия всего дерева пройдите по ПолучитьЭлементы() верхнего уровня и вызовите Развернуть для каждой строки.
Дерево из запроса и большие объёмы данных
Чаще всего дерево не собирают руками, а получают из запроса с итогами. Ключевое слово ИТОГИ ПО плюс обход по группировкам дают готовую иерархию:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Номенклатура.Родитель КАК Группа,
| Номенклатура.Ссылка КАК Номенклатура
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| НЕ Номенклатура.ЭтоГруппа
|ИТОГИ ПО
| Группа";
Дерево = Запрос.Выполнить().Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам);
Если итоги считать не по иерархии справочника, а по полю ИЕРАРХИЯ (ИТОГИ ПО Номенклатура ИЕРАРХИЯ), дерево повторит структуру групп справочника на всю глубину.
Когда в дереве десятки тысяч строк, скорость решают несколько приёмов:
- ⚡ Соответствие вместо повторного поиска. У дерева, в отличие от таблицы значений, нет индексов. Каждый
Найти(..., Истина)проходит всё дерево заново. Если ищете в цикле, один раз соберитеСоответствие«ключ → строка дерева» и берите узлы из него. - 🧮 Итоги в запросе, а не в коде.
ИТОГИ СУММА(...) ПОсчитает быстрее, чемИтог()по каждой ветке. - 📦 Выборка вместо выгрузки. Если дерево нужно только прочитать один раз,
Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам)с вложеннымиВыбрать()не держит в памяти весь результат. - 🔄 Меньше передач клиент-сервер.
РеквизитФормыВЗначениеиЗначениеВРеквизитФормыкопируют всё дерево. Делайте их один раз за серверный вызов, а не в цикле.
Пример быстрого доступа по ключу:
Карта = Новый Соответствие;
ЗаполнитьКарту(Дерево.Строки, Карта);
Процедура ЗаполнитьКарту(КоллекцияСтрок, Карта)
Для Каждого Строка Из КоллекцияСтрок Цикл
Карта.Вставить(Строка.Номенклатура, Строка);
ЗаполнитьКарту(Строка.Строки, Карта);
КонецЦикла;
КонецПроцедуры
// Дальше в цикле по документам — без поиска по дереву:
Узел = Карта.Получить(ТоварДокумента);
В модуле процедура ЗаполнитьКарту объявляется до основного кода или в общем модуле. Строки дерева в Соответствии — ссылки на те же объекты: изменение Узел.Сумма сразу меняет дерево.
Ошибки при работе с деревом значений
Правильное удаление строк по условию — обратным циклом, граница вычисляется один раз до цикла:
Всего = Группа.Строки.Количество();
Для Сч = 1 По Всего Цикл
Строка = Группа.Строки[Всего - Сч];
Если Строка.Количество = 0 Тогда
Группа.Строки.Удалить(Строка);
КонецЕсли;
КонецЦикла;
Ещё одна ловушка — Сортировать(). Без второго параметра Истина сортируется только тот уровень, у которого вызван метод: Дерево.Строки.Сортировать("Сумма Убыв", Истина) упорядочит все ветки.