При разработке сложных отчетов, печатных форм и обработок в системе 1С:Предприятие программисты часто прибегают к программному формированию табличных документов. Одним из ключевых инструментов для манипулирования ячейками является метод ВставитьОбласть() объекта ТабличныйДокумент. Он позволяет копировать произвольные прямоугольные области ячеек и вставлять их в нужное место результирующего документа.
Однако на практике разработчики часто сталкиваются с загадочным поведением платформы: один и тот же код, написанный для формирования печатной формы, ведет себя абсолютно по-разному в зависимости от режима запуска приложения (тонкий клиент, толстый клиент, файловый или клиент-серверный вариант работы). В этой статье мы подробно разберем, почему возникает такая разница, проанализируем внутренние механизмы платформы и выработаем надежные архитектурные решения.
Обратимся к официальной документации платформы. В Синтакс-Помощнике для метода ВставитьОбласть() указано следующее примечание: «Если размеры исходной области не соответствуют результирующей области, то при выполнении метода возникает ошибка». На уровне платформы эта ошибка прерывает исполнение кода и выбрасывает исключение с системным текстом: Выделение не соответствует вставляемому блоку.
Казалось бы, поведение задокументировано и предопределено. Но на практике это строгое правило соблюдается далеко не всегда. Проведем эксперимент и посмотрим, как реагирует платформа в различных средах выполнения.
Рассмотрим пример кода управляемой формы внешней обработки. Мы создадим процедуру, которая пытается вставить область размером в 3 колонки в область-приемник размером в 5 колонок. Логично ожидать, что из-за несоответствия геометрии метод должен вызвать ошибку.
&НаСервере
Функция ТестНаСервере()
Сообщение = Новый СообщениеПользователю;
ТабДокИсходный = Новый ТабличныйДокумент;
ТабДокНовый = Новый ТабличныйДокумент;
// Заполним ячейки исходными данными
ТабДокИсходный.Область(1,1).Текст = "ячейка1";
ТабДокИсходный.Область(1,3).Текст = "ячейка3";
// Источник имеет размер 1 строка на 3 колонки (1,1,1,3)
ОбластьИсходная = ТабДокИсходный.Область(1,1,1,3);
// Приемник имеет больший размер — 1 строка на 5 колонок (1,1,1,5)
ОбластьПриемник = ТабДокНовый.Область(1,1,1,5);
Попытка
ТабДокНовый.ВставитьОбласть(ОбластьИсходная, ОбластьПриемник);
Сообщение.Текст = "Получилось вставить 3 колонки в 5 (без ошибок)";
Исключение
Сообщение.Текст = "Ошибка: разный размер областей!";
КонецПопытки;
Сообщение.Сообщить();
Возврат ТабДокНовый;
КонецФункции
&НаКлиенте
Процедура Тест(Команда)
ТД = ТестНаСервере();
ТД.Показать();
КонецПроцедуры
Результаты запуска этого теста в разных условиях удивляют:
Исключение. Выводится сообщение "Ошибка: разный размер областей!". Если убрать попытку, выполнение аварийно завершится с ошибкой Выделение не соответствует вставляемому блоку.Выясним причину, почему одно и то же действие приводит к совершенно разным результатам на уровне ядра платформы.
Разница в поведении метода напрямую связана с особенностями работы движка табличного документа на стороне клиента и на стороне сервера приложений. Разработчики платформы 1С:Предприятие используют две принципиально разные реализации обработки табличных документов:
Выделение не соответствует вставляемому блоку.rphost (на сервере приложений) или при работе Тонкого клиента. Серверная часть платформы не имеет прямой связи с интерактивным интерфейсом операционной системы. Главная задача серверного движка — максимальная скорость генерации отчетов и экономия ресурсов процессора. Проверка геометрического соответствия областей при вставке в этом режиме отключена или сильно упрощена. Серверный движок «на лету» подгоняет данные (автоматически дополняя недостающие ячейки пустыми значениями или отсекая лишние), завершая метод без генерации ошибок.Таким образом, когда мы вызываем метод ВставитьОбласть() в Тонком клиенте, процедура выполняется на стороне сервера, где проверка отключена. В Толстом клиенте файловой базы выполнение происходит локально в рамках единого UI-процесса, что и вызывает жесткую валидацию.
Описанное различие долгое время являлось источником труднодиагностируемых проблем в типовых решениях от фирмы 1С (в частности, в конфигурации «1С:Зарплата и управление персоналом 3.1» при выводе на печать расчета пособий к больничным листам или генерации штрихкодов документов).
Сценарий выглядел следующим образом:
Выделение не соответствует вставляемому блоку, и программа аварийно прекращала формирование печатной формы.В качестве временного обходного пути (workaround) техническая поддержка рекомендовала принудительный перевод всех пользователей на работу исключительно под управлением Тонкого клиента. В последующих релизах разработчики конфигураций исправили этот недочет, приведя размеры областей к строгому соответствию в самом коде формирования отчетов.
Помимо архитектурных особенностей платформы, к ошибке несоответствия выделения часто приводит классическая ошибка при работе со свойствами макета табличного документа. Разберем этот сценарий подробнее.
Часто программисты путают возвращаемые типы методов ПолучитьОбласть() и Область():
ПолучитьОбласть("ИмяОбласти") возвращает новый изолированный объект типа ТабличныйДокумент.Область() возвращает объект типа ОбластьЯчеекТабличногоДокумента.Если передать результат вызова ПолучитьОбласть() напрямую в метод ВставитьОбласть() в качестве источника, платформа попытается вставить весь табличный документ целиком, что гарантированно приведет к ошибке геометрического несоответствия. Чтобы этого избежать, полученную область необходимо явно приводить к типу области ячеек. Рассмотрим правильный и неправильный варианты кода.
// НЕПРАВИЛЬНО: Приведет к ошибке геометрии в Толстом клиенте
ОбластьМакета = Макет.ПолучитьОбласть("СтрокаТаблицы");
ТабДокПриемник.ВставитьОбласть(ОбластьМакета, ОбластьНазначения);
// ПРАВИЛЬНО: Извлекаем область ячеек из полученного табличного документа
ОбластьМакета = Макет.ПолучитьОбласть("СтрокаТаблицы");
ТабДокПриемник.ВставитьОбласть(ОбластьМакета.Область(), ОбластьНазначения);
Вызов метода Область() без параметров у возвращенного макета позволяет получить точную прямоугольную область ячеек, с которой метод ВставитьОбласть() сможет работать корректно.
Разработчикам также важно знать о сопутствующем баге платформы 1С, который присутствовал в нескольких релизах веток 8.3.13–8.3.16. При использовании метода ВставитьОбласть() в результирующем табличном документе полностью ломалось и переставало визуально работать свойство ФиксацияСверху (закрепление строк при прокрутке).
Даже если в коде было явно прописано программное закрепление строк, после выполнения вставки областей визуальное разделение пропадало. При сохранении такого документа в формат Excel закрепление строк также не переносилось. Для обхода этого бага рекомендуется устанавливать свойства фиксации (ФиксацияСверху, ФиксацияСлева) в самый последний момент — строго после завершения всех операций по компоновке и вставке областей в документ.
Чтобы ваш код формирования отчетов и печатных форм работал стабильно в любых режимах запуска и на любых версиях платформы 1С, необходимо придерживаться принципа оборонительного программирования. Нам нужно гарантировать, что размеры области-приемника всегда будут в точности совпадать с размерами области-источника.
Для этого напишем универсальную вспомогательную функцию, которая автоматически рассчитывает координаты области-приемника на основе переданной области-источника и координат левой верхней ячейки вставки.
// Функция вычисляет точные координаты области-приемника
// и возвращает объект ОбластьЯчеекТабличногоДокумента
Функция ПолучитьОбластьПриемникТочногоРазмера(ТабДокПриемник, ОбластьИсточник, НачальнаяСтрока, НачальнаяКолонка) Экспорт
// Вычисляем высоту и ширину области-источника (в ячейках)
ВысотаОбласти = ОбластьИсточник.Низ - ОбластьИсточник.Верх;
ШиринаОбласти = ОбластьИсточник.Право - ОбластьИсточник.Лево;
// Рассчитываем конечные координаты для приемника
КонечнаяСтрока = НачальнаяСтрока + ВысотаОбласти;
КонечнаяКолонка = НачальнаяКолонка + ШиринаОбласти;
// Возвращаем область-приемник, идеально соответствующую источнику
Возврат ТабДокПриемник.Область(НачальнаяСтрока, НачальнаяКолонка, КонечнаяСтрока, КонечнаяКолонка);
КонецФункции
Посмотрим, как изящно и безопасно применить эту функцию в реальном коде при компоновке печатной формы:
Процедура ВывестиДанныеВТабличныйДокумент(ИтоговыйТабДок, Макет)
// Получаем область-источник из макета и приводим ее к правильному типу
ОбластьИсточник = Макет.ПолучитьОбласть("ШапкаТаблицы").Область();
// Задаем начальную точку вставки в итоговом документе
СтрокаНачала = 5;
КолонкаНачала = 1;
// Получаем область-приемник гарантированно совпадающего размера
ОбластьПриемник = ПолучитьОбластьПриемникТочногоРазмера(ИтоговыйТабДок, ОбластьИсточник, СтрокаНачала, КолонкаНачала);
// Выполняем вставку. Ошибка исключена в любом режиме клиента!
Попытка
ИтоговыйТабДок.ВставитьОбласть(ОбластьИсточник, ОбластьПриемник);
Исключение
ЗаписатьЛогОшибок(ОписаниеОшибки());
ВызватьИсключение "Не удалось скомпоновать форму из-за геометрического несоответствия областей.";
КонецПопытки;
КонецПроцедуры
Использование этого подхода полностью защищает ваше приложение от непредвиденных сбоев на стороне клиентов. Мы уходим от жесткой зависимости от архитектурных особенностей платформы, делая процесс формирования документов стабильным и предсказуемым.