При разработке различных систем планирования, интеграций с таск-трекерами (например, Битрикс24) или систем управления проектами в 1С часто возникает задача визуального разделения задач и подзадач. Входящие данные обычно поступают в плоском виде: каждая строка содержит уникальный идентификатор ID и ссылку на родительский элемент IDРодителя. Если просто вывести эти строки в стандартную табличную часть документа, пользователю будет крайне сложно визуально отделить подчиненные задачи от основных.
В рамках этой статьи мы подробно разберем, как решить эту проблему на платформе 1С:Предприятие 8.3 при использовании обычных форм (на примере конфигураций УТП, УПП или КА 1.1). Мы проанализируем различные подходы — от правильного архитектурного разделения хранения и отображения данных до простых способов визуальной стилизации «на лету» без изменения структуры метаданных.
Прежде чем перейти к коду, давайте выясним ключевую причину сложности. В метаданных документа 1С табличные части всегда хранятся в «плоском» виде. Платформа не позволяет создать иерархическую структуру непосредственно внутри физической табличной части документа. Попытка принудительно выводить иерархию в стандартное табличное поле, связанное с табличной частью, не увенчается успехом — у вас просто не появятся механизмы сворачивания и разворачивания строк (значки «плюс» и «минус»).
Правильное архитектурное решение заключается в разделении слоев:
Расшифровка), куда записываются все колонки: Задание, ID, IDРодителя, Исполнитель, КоличествоЧасов и т.д. Это гарантирует бесперебойную работу механизмов проведения документа, формирования отчетов и печати.ДеревоЗначений (назовем его РасшифровкаДерево). Именно его мы выводим на форму в качестве элемента управления «Табличное поле». При открытии формы или при нажатии кнопки заполнения мы программно переносим плоские данные из табличной части в дерево значений, выстраивая правильную иерархию. Перед записью документа мы выполняем обратную операцию — «сворачиваем» дерево обратно в плоскую табличную часть.Рассмотрим пошагово реализацию этого и других альтернативных вариантов.
Этот способ является наиболее профессиональным, так как предоставляет пользователю полноценный интерактивный интерфейс дерева с возможностью сворачивать и разворачивать группы задач.
Выполним предварительную настройку формы:
РасшифровкаДерево и типом данных ДеревоЗначений.Задание, ID, IDРодителя, Исполнитель, КоличествоЧасов, Цена).Задание и установим для нее свойство ОтображатьИерархию в значение Истина. Без этого флажка дерево будет выглядеть как плоский список.Теперь разберем алгоритм заполнения дерева значений. Поскольку данные могут приходить из внешнего источника (например, API) хаотично, нам потребуется быстрый способ сопоставления родителей и детей. Использование объекта Соответствие позволит избежать ресурсоемких вложенных циклов.
Пропишем процедуру заполнения дерева в модуле формы документа:
Процедура ЗаполнитьДеревоФормыИзТабличнойЧасти()
// Очищаем дерево перед заполнением, чтобы избежать дублирования
РасшифровкаДерево.Строки.Очистить();
// Выгружаем плоские данные из табличной части документа в ТЗ
ТЧРасшифровка = ЭтотОбъект.Расшифровка.Выгрузить();
// Соответствие нужно для быстрого поиска родительских строк в дереве по их ID
СоответствиеРодителей = Новый Соответствие;
// Первый проход: добавляем в корень дерева только основные задачи (у которых нет родителя)
Для Каждого СтрокаТЧ Из ТЧРасшифровка Цикл
Если Не ЗначениеЗаполнено(СтрокаТЧ.IDРодителя) Тогда
НоваяСтрокаРодитель = РасшифровкаДерево.Строки.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрокаРодитель, СтрокаТЧ);
// Запоминаем связь: ID задачи -> Ссылка на строку дерева
СоответствиеРодителей.Вставить(СтрокаТЧ.ID, НоваяСтрокаРодитель);
КонецЕсли;
КонецЦикла;
// Второй проход: распределяем подчиненные подзадачи по их родителям
Для Каждого СтрокаТЧ Из ТЧРасшифровка Цикл
Если ЗначениеЗаполнено(СтрокаТЧ.IDРодителя) Тогда
// Ищем строку родителя в нашем соответствии
РодительскаяСтрока = СоответствиеРодителей.Получить(СтрокаТЧ.IDРодителя);
Если РодительскаяСтрока <> Неопределено Тогда
// Добавляем строку на уровень ниже найденного родителя
НоваяСтрокаПодзадача = РодительскаяСтрока.Строки.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрокаПодзадача, СтрокаТЧ);
Иначе
// Если по какой-то причине родитель не найден (например, не попал в выборку),
// выводим строку в корень дерева, чтобы данные не потерялись
НоваяСтрокаПодзадача = РасшифровкаДерево.Строки.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрокаПодзадача, СтрокаТЧ);
КонецЕсли;
КонецЕсли;
КонецЦикла;
// Программное раскрытие всех ветвей дерева для удобства пользователя
Для Каждого СтрокаКорня Из ЭлементыФормы.РасшифровкаДерево.Значение.Строки Цикл
ЭлементыФормы.РасшифровкаДерево.Развернуть(СтрокаКорня, Истина);
КонецЦикла;
КонецПроцедуры
Для того чтобы данные корректно сохранялись при записи документа, нам необходимо совершить обратное преобразование — выгрузить дерево значений обратно в плоскую табличную часть объекта. Для этого настроим предопределенный обработчик события формы ПередЗаписью:
Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
// Очищаем физическую табличную часть документа
ЭтотОбъект.Расшифровка.Очистить();
// Рекурсивно выгружаем все элементы дерева обратно в ТЧ
ВыгрузитьВеткиДереваВТабличнуюЧасть(РасшифровкаДерево.Строки);
КонецПроцедуры
Процедура ВыгрузитьВеткиДереваВТабличнуюЧасть(КоллекцияСтрокДерева)
Для Каждого СтрокаДерева Из КоллекцияСтрокДерева Цикл
// Добавляем строку в плоскую ТЧ документа
НоваяСтрокаТЧ = ЭтотОбъект.Расшифровка.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрокаТЧ, СтрокаДерева);
// Если у текущей строки дерева есть подчиненные строки, обрабатываем их рекурсивно
Если СтрокаДерева.Строки.Количество() > 0 Тогда
ВыгрузитьВеткиДереваВТабличнуюЧасть(СтрокаДерева.Строки);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Не забудьте также вызвать процедуру ЗаполнительДеревоФормыИзТабличнойЧасти() в обработчике события формы ПриОткрытии(), чтобы при открытии ранее сохраненного документа пользователь сразу видел структурированное дерево задач.
Если вы хотите избежать ручного написания циклов перебора строк с использованием объектов Соответствие, в платформе 1С есть очень мощный и лаконичный встроенный инструмент — ПостроительЗапроса. Он умеет за доли секунды преобразовывать любую плоскую таблицу значений в полноценное дерево с заданной иерархией. Этот способ работает быстрее стандартных циклов на больших объемах данных.
Проанализируем пример кода преобразования плоской таблицы в дерево с помощью построителя:
Функция ПолучитьДеревоИзТаблицыЗначений(ПлоскаяТаблица)
Построитель = Новый ПостроительЗапроса;
// Назначаем плоскую таблицу значений источником данных
Построитель.ИсточникДанных = Новый ОписаниеИсточникаДанных(ПлоскаяТаблица);
// Указываем колонку, по которой необходимо построить иерархию
Построитель.ИсточникДанных.Колонки.IDРодителя.Измерение = Истина;
// Заполняем настройки построителя на основании источника данных
Построитель.ЗаполнитьНастройки();
// Выполняем построение дерева
Построитель.Выполнить();
// Выгружаем результат в формате дерева с обходом по группировкам и иерархии
Возврат Построитель.Результат.Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкамСИерархией);
КонецФункции
Этот метод изящен, однако у него есть ограничение: структура полученного дерева будет жестко привязана к группировочному полю, что потребует дополнительной адаптации колонок для вывода в интерфейс формы. Тем не менее, для быстрого анализа данных и построения сложных структур этот метод незаменим.
Рассмотрим ситуацию, когда разработчик по каким-то причинам не хочет связываться с деревом значений. Например, в документе уже реализована сложная логика проведения, завязаны десятки печатных форм, переписано множество модулей, и переход на реквизит формы «Дерево значений» грозит нарушением работоспособности всей системы. Или же вы боитесь появления дублей строк при печати, о которых упоминал автор темы.
В таком случае мы можем оставить табличное поле формы связанным напрямую со стандартной плоской табличной частью Расшифровка, но визуально оформить его так, чтобы подзадачи выглядели сдвинутыми вправо, создавая иллюзию вложенности.
Для этого мы задействуем мощный механизм обычных форм — обработчик события ПриВыводеСтроки для элемента управления «Табличное поле». Данное событие вызывается платформой при отрисовке каждой строки на экране и позволяет гибко менять оформление ячеек.
Разберем пример реализации данного подхода:
Процедура ТабличноеПолеРасшифровкаПриВыводеСтроки(Элемент, ОформлениеСтроки, ДанныеСтроки)
// Защита от пустых строк
Если ДанныеСтроки = Неопределено Тогда
Возврат;
КонецЕсли;
// Проверяем, является ли текущая строка подзадачей
Если ЗначениеЗаполнено(ДанныеСтроки.IDРодителя) Тогда
// 1. Делаем визуальный отступ (сдвиг) в колонке "Задание".
// Мы добавляем несколько неразрывных пробелов перед наименованием задачи.
// Это меняет только текст на экране, не изменяя саму строку в базе данных!
Отступ = Символы.НПП + Символы.НПП + Символы.НПП + Символы.НПП + "» ";
ОформлениеСтроки.Ячейки.Задание.УстановитьТекст(Отступ + ДанныеСтроки.Задание);
// 2. Дополнительно стилизуем подзадачи: делаем их текст серым и курсивным
ОформлениеСтроки.Ячейки.Задание.ЦветТекста = WebЦвета.Серый;
ОформлениеСтроки.Ячейки.Задание.Шрифт = Новый Шрифт(ОформлениеСтроки.Ячейки.Задание.Шрифт,,, Истина);
Иначе
// Для основных родительских задач сделаем шрифт жирным, чтобы привлечь внимание
ОформлениеСтроки.Ячейки.Задание.Шрифт = Новый Шрифт(ОформлениеСтроки.Ячейки.Задание.Шрифт,, Истина);
КонецЕсли;
КонецПроцедуры
Преимущества данного метода оформления:
Однако помните о производительности: обработчик ПриВыводеСтроки вызывается очень часто при прокрутке списка. Если строк в табличной части больше нескольких сотен, постоянное создание нового объекта Новый Шрифт(...) внутри процедуры может привести к замедлению интерфейса. Для оптимизации рекомендуется кэшировать создаваемые шрифты в переменных модуля формы или использовать более производительное событие ПриПолученииДанных.
Мы проанализировали три основных подхода к группировке задач и подзадач в 1С на обычных формах. Каждый метод имеет свои особенности применения: