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