При разработке отчетов и рабочих мест на управляемых формах 1С программисты часто сталкиваются с задачей вывода многоуровневых данных. Обычный объект типа ТаблицаЗначений отображает данные линейно и не поддерживает интерактивное сворачивание и разворачивание строк по аналогии с отчетами. Для решения этой задачи в платформе предусмотрен специальный объект — Дерево значений.
В этой статье мы подробно разберем, как правильно получить данные с группировками из запроса, оптимально заполнить ими реквизит формы и настроить интерфейс для удобной работы пользователя. Мы рассмотрим как стандартные подходы с обходом выборки, так и более продвинутые и быстрые методы платформы, включая динамическое конструирование колонок и использование компоновки данных.
Предположим, нам необходимо вывести на форму остатки по счетам учета в разрезе организаций, контрагентов (Субконто1) и договоров (Субконто2). Данные должны быть сгруппированы иерархически: на верхнем уровне находится Организация, далее идет Контрагент, а на самом нижнем уровне — Договор.
Для реализации такой иерархии на форме обычная таблица значений не подойдет. Нам потребуется использовать реквизит формы с типом ДанныеФормыДерево, который визуализируется на форме в виде таблицы с кнопками развертывания («плюсиками»). На стороне сервера этот реквизит конвертируется в объект типа ДеревоЗначений.
Рассмотрим детально пример, предложенный разработчиками на форуме. Сначала мы пишем запрос, в котором используем предложение ИТОГИ ПО для построения иерархии результатов. Затем мы программно обходим выборку с помощью вложенных циклов и переносим данные в дерево значений.
Давайте разберем серверную процедуру заполнения дерева значений:
&НаСервере
Процедура ЗаполнитьДеревоНаФормеРучнымОбходом()
// Получаем объект дерева значений из реквизита формы
ДеревоЗначОбъект = РеквизитФормыВЗначение("ДеревоЗнач");
ДеревоЗначОбъект.Строки.Очистить(); // Очищаем старые данные
Сп = Новый СписокЗначений;
Сп.Добавить(ПланыСчетов.Хозрасчетный.РасчетыСПоставщикамиИПодрядчиками);
ЗапросДанные = Новый Запрос;
ЗапросДанные.Текст =
"ВЫБРАТЬ
| ХозрасчетныйОстатки.Организация КАК Организация,
| ХозрасчетныйОстатки.Субконто1 КАК Субконто1,
| ХозрасчетныйОстатки.Субконто2 КАК Субконто2,
| СУММА(ХозрасчетныйОстатки.СуммаОстатокДт) КАК СуммаОстатокДт,
| СУММА(ХозрасчетныйОстатки.КоличествоОстатокДт) КАК КоличествоОстатокДт
|ИЗ
| РегистрБухгалтерии.Хозрасчетный.Остатки(&ТекДата, , , ) КАК ХозрасчетныйОстатки
|ГДЕ
| ХозрасчетныйОстатки.Счет В ИЕРАРХИИ(&Сп)
|
|СГРУППИРОВАТЬ ПО
| ХозрасчетныйОстатки.Субконто1,
| ХозрасчетныйОстатки.Субконто2,
| ХозрасчетныйОстатки.Организация
|ИТОГИ
| СУММА(СуммаОстатокДт),
| СУММА(КоличествоОстатокДт)
|ПО
| Организация,
| Субконто1,
| Субконто2";
ЗапросДанные.УстановитьПараметр("ТекДата", ТекущаяДата());
ЗапросДанные.УстановитьПараметр("Сп", Сп);
РезЗапроса = ЗапросДанные.Выполнить();
// Выбираем первый уровень группировки (Организация)
ВыборкаОрг = РезЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаОрг.Следующий() Цикл
НоваяСтрОрг = ДеревоЗначОбъект.Строки.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрОрг, ВыборкаОрг);
// Выбираем второй уровень группировки (Субконто1 - Контрагент)
ВыборкаСуб1 = ВыборкаОрг.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаСуб1.Следующий() Цикл
НоваяСтрСуб1 = НоваяСтрОрг.Строки.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрСуб1, ВыборкаСуб1);
// Выбираем третий уровень группировки (Субконто2 - Договор)
ВыборкаСуб2 = ВыборкаСуб1.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаСуб2.Следующий() Цикл
НоваяСтрСуб2 = НоваяСтрСуб1.Строки.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрСуб2, ВыборкаСуб2);
КонецЦикла;
КонецЦикла;
КонецЦикла;
// Возвращаем измененный объект в реквизит формы
ЗначениеВРеквизитФормы(ДеревоЗначОбъект, "ДеревоЗнач");
КонецПроцедуры
Этот способ интуитивно понятен и наглядно показывает, как распределяются подчиненные строки. Однако ручной обход выборки с большим уровнем вложенности имеет существенный минус — он работает медленно при обработке десятков тысяч записей из-за накладных расходов на создание объектов строк на стороне сервера 1С.
В платформе 1С существует встроенный высокооптимизированный механизм автоматического преобразования иерархического результата запроса в дерево значений. Мы можем заменить многострочные циклы вызовом всего одного метода Выгрузить() с указанием типа обхода.
Проанализируем пример оптимизированного кода:
&НаСервере
Процедура ЗаполнитьДеревоНаФормеОптимально()
ЗапросДанные = Новый Запрос;
// ... (Текст запроса с конструкцией ИТОГИ ПО аналогичен первому примеру) ...
РезЗапроса = ЗапросДанные.Выполнить();
// Выгружаем результат запроса сразу в структуру дерева значений
// Параметр ОбходРезультатаЗапроса.ПоГруппировкам указывает на необходимость
// автоматического построения иерархических веток на основе итогов запроса
ДеревоЗначений = РезЗапроса.Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам);
// Переносим данные напрямую в реквизит формы
// ВНИМАНИЕ: Колонки дерева значений должны в точности соответствовать реквизитам Дерева на форме!
ЗначениеВРеквизитФормы(ДеревоЗначений, "ДеревоЗнач");
КонецПроцедуры
Важное правило: При использовании метода ЗначениеВРеквизитФормы() имена и типы колонок в выгруженном объекте ДеревоЗначений должны строго совпадать с реквизитами формы типа ДанныеФормыДерево. Если в запросе окажутся лишние колонки или типы данных будут отличаться от настроенных в конфигураторе, система вызовет ошибку выполнения.
Если источником иерархии служит не просто группировка по разным полям, а сам справочник (например, справочник "Номенклатура" с группами и элементами), то в качестве параметра выгрузки следует использовать значение ОбходРезультатаЗапроса.ПоГруппировкамСИерархией.
Иногда структура выводимых данных заранее неизвестна. Например, количество и состав группировок определяются настройками пользователя непосредственно в режиме "1С:Предприятие". В такой ситуации мы не можем заранее настроить реквизиты дерева в конфигураторе. Нам поможет программное создание реквизитов и элементов формы.
Выполним эту операцию по шагам:
ИзменитьРеквизиты() для изменения структуры данных в памяти сервера.Элементы.Добавить().Пример программного добавления колонок в Дерево значений:
&НаСервере
Процедура СоздатьКолонкиДереваДинамически(ИменаКолонок)
ДобавляемыеРеквизиты = Новый Массив;
СозданныеЭлементы = Новый Массив;
// 1. Описываем колонки, которые хотим добавить в дерево "ДинамическоеДерево"
Для Каждого ИмяКолонки Из ИменаКолонок Цикл
// Указываем имя, тип и путь к родительскому реквизиту дерева
НовыйРеквизит = Новый РеквизитФормы(ИмяКолонки, Новый ОписаниеТипов("Строка"), "ДинамическоеДерево", ИмяКолонки);
ДобавляемыеРеквизиты.Добавить(НовыйРеквизит);
КонецЦикла;
// 2. Добавляем реквизиты на форму
ИзменитьРеквизиты(ДобавляемыеРеквизиты);
// 3. Создаем элементы формы (визуальные колонки в таблице)
Для Каждого ИмяКолонки Из ИменаКолонок Цикл
ИмяЭлементаКолонки = "ДинамическоеДерево" + ИмяКолонки;
НовыйЭлемент = Элементы.Добавить(ИмяЭлементаКолонки, Тип("ПолеФормы"), Элементы.ДинамическоеДерево);
НовыйЭлемент.Вид = ВидПоляФормы.ПолеВвода;
НовыйЭлемент.ПутьКДанным = "ДинамическоеДерево." + ИмяКолонки;
НовыйЭлемент.Заголовок = ИмяКолонки;
КонецЦикла;
КонецПроцедуры
После вывода иерархических данных на форму пользователю часто требуется раскрыть все ветки до нижнего уровня или, наоборот, быстро свернуть их. Эти действия выполняются исключительно на стороне клиента с помощью методов элемента формы Таблица.
Рассмотрим, как реализовать функции развертывания и свертывания всех узлов программно:
&НаКлиенте
Процедура РазвернутьВсеУзлыДерева(Команда)
// Получаем коллекцию строк верхнего уровня
КоллекцияСтрок = ДеревоЗнач.ПолучитьЭлементы();
Для Каждого СтрокаДерева Из КоллекцияСтрок Цикл
// Получаем уникальный идентификатор строки дерева
ИдентификаторСтроки = СтрокаДерева.ПолучитьИдентификатор();
// Разворачиваем узел. Второй параметр "Истина" указывает на рекурсивное раскрытие всех подчиненных уровней
Элементы.ДеревоЗнач.Развернуть(ИдентификаторСтроки, Истина);
КонецЦикла;
КонецПроцедуры
&НаКлиенте
Процедура СвернутьВсеУзлыДерева(Команда)
КоллекцияСтрок = ДеревоЗнач.ПолучитьЭлементы();
Для Каждого СтрокаДерева Из КоллекцияСтрок Цикл
ИдентификаторСтроки = СтрокаДерева.ПолучитьИдентификатор();
// Метод Свернуть скрывает подчиненные элементы выбранного узла
Элементы.ДеревоЗнач.Свернуть(ИдентификаторСтроки);
КонецЦикла;
КонецПроцедуры
Если задача заключается только в удобном выводе иерархических данных для просмотра, анализа и последующей печати, то ручное программирование дерева значений на форме часто является избыточным.
Более мощной альтернативой выступает Система компоновки данных (СКД) с выводом результата в Табличный документ на форме:
ПолеТабличногоДокумента.Табличный документ по умолчанию поддерживает группировки ("плюсики" сбоку или сверху), автоматический расчет итоговых строк, гибкую настройку условного оформления (например, выделение цветом итогов по организациям) и нативную выгрузку в Excel без потери структуры группировок.
Важно различать понятия группировки строк и визуального объединения колонок, о котором шла речь в сообщениях форума:
ДеревоЗначений.ГруппаКолонок). После этого перетащите нужные колонки внутрь этой группы. На форме они отобразятся под красивой объединенной шапкой.Используйте описанные методики в зависимости от конкретных требований к вашему интерфейсу: быстро выгружайте готовые деревья из запросов методом Выгрузить() при фиксированной структуре данных, конструируйте форму динамически при плавающих настройках, либо доверяйте сложную аналитику механизмам СКД.