Почему зависает весь интерфейс 1С при печати из оповещения и как это исправить?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

В процессе разработки на платформе 1С:Предприятие 8.3 (начиная с версий 8.3.12 и 8.3.13) программисты часто сталкиваются с крайне неприятным поведением системы: при выполнении вполне стандартного кода тонкий клиент полностью «застывает». При этом приложение перестает реагировать на любые клики мыши, клавиатурные сочетания, не закрывается по нажатию на крестик и лишь издает монотонный системный звук при попытке нажать на любое место экрана. Единственным способом закрыть программу остается принудительное завершение процесса через Диспетчер задач Windows.

В этой статье мы подробно разберем, почему возникает эта ошибка, проанализируем внутренние механизмы взаимодействия оконной системы платформы 1С с операционной системой и реализуем надежное архитектурное решение этой проблемы без использования сомнительных костылей.

Сценарий возникновения проблемы

Давайте рассмотрим классический пример, в котором воспроизводится зависание. Предположим, у нас есть основная форма, из которой мы открываем вспомогательное окно оплаты (например, форму авторизации банковского терминала или выбора фискального регистратора). Это вспомогательное окно открывается с режимом блокировки всего интерфейса или окна владельца. После успешного завершения операции вспомогательное окно закрывается, возвращает результат в процедуру оповещения, и система должна сразу же распечатать чек или квитанцию.

Ниже представлен исходный программный код, который гарантированно приводит к зависанию интерфейса на платформе 1С:


&НаКлиенте
Процедура Повесить(Команда)    
    
    // Открываем блокирующее окно формы оплаты
    ОткрытьForm = ОткрытьФорму(
        "ВнешняяОбработка.Вешаем8313.Форма.ФормаОплаты", 
        , 
        ЭтотОбъект, 
        , 
        , 
        ,
        Новый ОписаниеОповещения("ЗавершениеОплаты", ЭтотОбъект, Неопределено),
        РежимОткрытияОкнаФормы.БлокироватьОкноВладельца
    );
    
КонецПроцедуры

&НаКлиенте
Процедура ЗавершениеОплаты(РезультатОплаты, ДополнительныеПараметры) Экспорт
    
    // Формируем табличный документ для печати
    ТабДок = Новый ТабличныйДокумент;
    ТабДок.Область("R1C1").Текст = "Печать чека ККМ... Успешно!";
    ТабДок.ИмяПринтера = "Microsoft Print to PDF";
    
    // Отправляем документ напрямую на принтер
    Список = Новый СписокЗначений();
    Список.Добавить(TabДок);
    УправлениеПечатьюКлиент.РаспечататьТабличныеДокументы(Список, Новый СписокЗначений(), Ложь);
    
КонецПроцедуры

Код самой вспомогательной формы оплаты предельно прост:


&НаКлиенте
Процедура КомандаЗакрыть(Команда)
    
    Закрыть();
    
КонецПроцедуры

При запуске этого кода форма оплаты закрывается, но в ту же секунду все окна 1С становятся недоступными. Давайте разберемся, почему это происходит.

Выясняем глубокую техническую причину зависания

Чтобы понять природу мертвой блокировки интерфейса, нам необходимо заглянуть под капот оконного менеджера платформы 1С:Предприятие 8.3. Начиная с версии 8.3.12, разработчики платформы существенно переработали механизмы отображения окон и блокировок для обеспечения корректной работы немодального интерфейса и улучшения стабильности веб-клиента.

Когда мы открываем форму с параметром РежимОткрытияОкнаФормы.БлокироватьОкноВладельца или РежимОткрытияОкнаФормы.БлокироватьВесьИнтерфейс, платформа накладывает на родительские окна специальный блокирующий слой (overlay), который перехватывает все события ввода пользователя.

Далее происходит следующая цепочка событий:

  1. Пользователь нажимает кнопку закрытия на вспомогательной форме.
  2. Платформа инициирует закрытие текущего окна и немедленно, в том же самом потоке выполнения, вызывает процедуру обратного вызова, указанную в объекте ОписаниеОповещения (в нашем случае — ЗавершениеОплаты).
  3. Важный нюанс: на момент выполнения кода внутри процедуры ЗавершениеОплаты вспомогательное окно уже визуально исчезло, но физическое снятие блокировки с основного интерфейса еще не завершено! Сообщение о снятии блокирующего слоя добавлено в очередь системных сообщений операционной системы, но поток выполнения 1С еще до него не добрался, так как он занят выполнением нашей процедуры оповещения.
  4. Внутри процедуры ЗавершениеОплаты мы вызываем метод ПакетОтображаемыхДокументов.Напечатать(РежимИспользованияДиалогаПечати.НеИспользовать) (который вызывается неявно внутри функций библиотеки стандартных подсистем).
  5. Метод прямого вывода на печать является тяжелой синхронной операцией. Он обращается к графической подсистеме операционной системы (GDI/дровам принтера), осуществляет рендеринг страниц и отправляет их в спулер печати ОС. На время выполнения этой операции поток обработки интерфейса 1С замораживается.
  6. В результате возникает классическая взаимная блокировка (deadlock): системное сообщение о разблокировке основного окна «зависает» в очереди сообщений, ожидая освобождения потока интерфейса. А сам поток интерфейса застревает на выполнении синхронного процесса печати, который, в свою очередь, пытается обратиться к параметрам родительского (все еще заблокированного) окна. Оконный менеджер 1С входит в ступор — интерфейс остается навсегда заблокированным невидимым слоем модальности.

В пользу этой гипотезы говорят результаты практических экспериментов, проведенных разработчиками:

Анализ решений и «костылей»

На просторах форумов часто предлагают использовать промежуточное предупреждение с таймаутом для решения этой проблемы. Выглядит этот «костыль» следующим образом:


&НаКлиенте
Процедура ЗавершениеОплатыПромежуток(РезультатОплаты, ДополнительныеПараметры) Экспорт
    
    // Вывод предупреждения на 1 секунду для искусственной разблокировки
    ПоказатьПредупреждение(
        Новый ОписаниеОповещения("ЗавершениеОплаты", ЭтотОбъект, Неопределено),
        "Выполняется печать...", 
        1, 
        "Печать"
    );
    
КонецПроцедуры

Почему этот метод работает? Метод ПоказатьПредупреждение является асинхронным. Его вызов заставляет платформу принудительно запустить внутренний цикл обработки сообщений для показа нового диалогового окна. Во время этого микро-цикла платформа успевает обработать зависшее в очереди сообщение о разблокировке родительского интерфейса от предыдущей закрытой формы. Когда управление передается в процедуру ЗавершениеОплаты, основной интерфейс уже полностью разблокирован, и прямая печать проходит успешно.

Недостатки костыля с предупреждением:

Профессиональное решение проблемы: отложенный обработчик ожидания

Наиболее изящный, надежный и архитектурно правильный способ обойти взаимную блокировку оконного менеджера — это использование однократного обработчика ожидания с минимальной задержкой.

Идея метода заключается в том, чтобы позволить процедуре оповещения завершиться штатно и максимально быстро. Сразу после этого платформа вернет управление операционной системе, которая мгновенно обработает накопившуюся очередь сообщений и полностью снимет блокирующий слой с интерфейса. И только на следующем «тике» системных событий выполнится наш обработчик ожидания, который безопасно произведет отправку документа на печать.

Давайте реализуем эту схему на практике. Перепишем наш исходный код правильно:


&НаКлиенте
Процедура Повесить(Команда)    
    
    // Открываем форму с блокировкой интерфейса
    ПараметрыОповещения = Новый ОписаниеОповещения("ЗавершениеОплаты", ЭтаФорма);
    
    ОткрытьФорму(
        "ВнешняяОбработка.Вешаем8313.Форма.ФормаОплаты", 
        , 
        ЭтотОбъект, 
        , 
        , 
        ,
        ПараметрыОповещения,
        РежимОткрытияОкнаФормы.БлокироватьВесьИнтерфейс
    );
    
КонецПроцедуры

&НаКлиенте
Процедура ЗавершениеОплаты(РезультатОплаты, ДополнительныеПараметры) Экспорт
    
    // Вместо мгновенного запуска печати подключаем однократный обработчик ожидания.
    // Интервал в 0.1 секунды визуально неощутим для пользователя,
    // но этого времени платформе гарантированно хватит для разблокировки окон.
    ПодключитьОбработчикОжидания("ВыполнитьПечатьПослеРазблокировкиОкон", 0.1, Истина);
    
КонецПроцедуры

&НаКлиенте
Процедура ВыполнитьПечатьПослеРазблокировкиОкон() Экспорт
    
    // Данный метод выполняется в абсолютно чистом контексте,
    // когда все блокирующие интерфейсы уже успешно сняты платформой.
    
    ТабДок = Новый ТабличныйДокумент;
    ТабДок.Область("R1C1").Текст = "Печать чека ККМ... Успешно!";
    ТабДок.ИмяПринтера = "Microsoft Print to PDF";
    
    Список = Новый СписокЗначений();
    Список.Добавить(TabДок);
    
    // Безопасно отправляем табличный документ на печать
    УправлениеПечатьюКлиент.РаспечататьТабличныеДокументы(Список, Новый СписокЗначений(), Ложь);
    
КонецПроцедуры

Этот подход гарантирует стабильную работу программы на любых релизах платформы «1С:Предприятие 8.3», начиная с самых ранних и заканчивая современными версиями.

Где еще может выстрелить эта проблема?

Описанный баг взаимной блокировки потоков в процедурах оповещения закрытия окон актуален не только для печати табличных документов. Нам, как разработчикам, необходимо придерживаться правила: любые тяжелые синхронные операции на клиенте внутри обработчиков закрытия блокирующих окон должны выполняться через отложенный вызов.

Рассмотрим список типовых операций, которые могут намертво повесить интерфейс при вызове из процедур оповещения:

  1. Интеграция с внешним оборудованием. Подключение и отправка команд на торговое оборудование (эквайринговые терминалы, ККТ, весы, сканеры штрихкодов), особенно если используются внешние компоненты (ActiveX, Native API).
  2. Работа с COM-объектами. Вызов внешних приложений через COM-соединение (например, выгрузка данных в MS Excel, заполнение шаблонов MS Word).
  3. Файловые операции. Использование синхронных методов работы с локальными файлами на диске пользователя или вызов диалогов выбора файлов (хотя современные стандарты разработки 1С и требуют использования асинхронных методов вроде НачатьПомещениеФайла, в старых конфигурациях все еще можно встретить синхронный код).
  4. Длительные вычисления на клиенте. Запуск ресурсоемких циклов обработки данных непосредственно на стороне клиента (например, парсинг больших текстовых файлов или XML-выгрузок).

Использование однократного подключения обработчика ожидания с помощью метода ПодключитьОбработчикОжидания с интервалом 0.1 секунды является универсальным и наиболее надежным паттерном проектирования интерфейса в управляемом приложении 1С, позволяющим навсегда забыть о зависании интерфейса клиентских приложений.

← На главную