В процессе разработки и сопровождения систем на платформе «1С:Предприятие 8» в режиме обычного приложения (Обычные Формы) программисты часто сталкиваются с аппаратными конфликтами. Типичный сценарий: кассир или менеджер открывает форму документа (например, «Чек ККМ» или «Реализация товаров и услуг»), отвлекается и оставляет окно открытым. При этом открытая форма монопольно захватывает COM-порт или USB-порт подключенного торгового оборудования — фискального регистратора, сканера штрихкодов или терминала сбора данных. В результате другие пользователи или другие сеансы работы не могут получить доступ к устройству.
В управляемом приложении (УФ) решить задачу закрытия окон достаточно просто с помощью штатного механизма ОкнаКлиентскогоПриложения и метода ПолучитьОкна(). Однако в обычном приложении (ОФ) таких инструментов «из коробки» не предусмотрено. В этой статье мы подробно разберем, как решить эту проблему, и рассмотрим несколько эффективных архитектурных и программных подходов.
Рассмотрим наиболее надежный и архитектурно правильный способ решения проблемы. Зачем пытаться принудительно закрывать окна пользователей, рискуя потерять несохраненные данные, если можно изменить логику работы с самим оборудованием? Мы можем научить систему захватывать порт устройства исключительно в момент совершения физического действия (например, печати чека или опроса весов) и мгновенно освобождать его сразу по завершении операции.
Проанализируем пример кода, который переписывает стандартное поведение системы. Вместо удержания порта в течение всего времени работы с формой, мы подключаем клиента непосредственно перед печатью и отключаем сразу после:
Процедура ВыполнитьПечатьЧекаНаСервереТО(ДанныеЧека) Экспорт
// Шаг 1. Динамически подключаем торговое оборудование
РезультатПодключения = ПолучитьСерверТО().ПодключитьКлиента(ЭтаФорма);
Если Не РезультатПодключения Тогда
Предупреждение("Не удалось занять порт фискального регистратора. Возможно, он заблокирован другим окном или сеансом.");
Возврат;
КонецЕсли;
// Шаг 2. Выполняем фискальную операцию внутри блока попытки
Попытка
ДрайверОборудования.ПечатьЧека(ДанныеЧека);
Исключение
ОбщегоНазначения.СообщитьОбОшибке(ОписаниеОшибки());
КонецПопытки;
// Шаг 3. Гарантированно отключаем клиента и освобождаем порт устройства
ПолучитьСерверТО().ОтключитьКлиента(ЭтаФорма);
КонецПроцедуры
При таком подходе форма документа может оставаться открытой сколь угодно долго — порт принтера чеков или весов будет свободен для других процессов и пользователей в системе.
Если специфика работы требует, чтобы оборудование было постоянно подключено во время интерактивной работы с формой (например, сканер штрихкодов должен мгновенно реагировать на сканирование), мы можем применить механизм отслеживания активности. Разберем по шагам, как настроить временное отключение оборудования, если пользователь бездействует.
Для этого мы задействуем метод платформы ПодключитьОбработчикОжидания. При любом изменении данных на форме или движении мыши мы будем обновлять время последней активности, а фоновый таймер будет проверять время простоя.
Рассмотрим пример реализации этой логики непосредственно в модуле обычной формы документа:
Перем ВремяПоследнейАктивности;
Процедура ПриОткрытии()
ВремяПоследнейАктивности = ТекущаяДата();
// Каждые 30 секунд проверяем, активен ли пользователь
ПодключитьОбработчикОжидания("ПроверитьАктивностьПользователя", 30);
КонецПроцедуры
Процедура ПроверитьАктивностьПользователя()
// Если пользователь бездействует более 5 минут (300 секунд)
Если ТекущаяДата() - ВремяПоследнейАктивности > 300 Тогда
// Проверяем, подключено ли оборудование в данный момент
Если ПолучитьСерверТО().КлиентПодключен(ЭтаФорма) Тогда
// Отключаем устройство и освобождаем физический порт
ПолучитьСерверТО().ОтключитьКлиента(ЭтаФорма);
Сообщить("Оборудование отключено в связи с отсутствием активности в форме.");
КонецЕсли;
КонецЕсли;
КонецПроцедуры
// Обработчик интерактивных действий на форме (например, при получении фокуса или изменении реквизитов)
Процедура ПриАктивизацииФормы()
ВремяПоследнейАктивности = ТекущаяДата();
// Если кассир вернулся к работе, мгновенно восстанавливаем подключение к порту
Если Не ПолучитьСерверТО().КлиентПодключен(ЭтаФорма) Тогда
ПолучитьСерверТО().ПодключитьКлиента(ЭтаФорма);
КонецЕсли;
КонецПроцедуры
Этот метод хорош тем, что он не закрывает саму форму документа, сохраняя все введенные пользователем данные, но при этом эффективно решает проблему блокировки портов.
Если бизнес-логика требует именно закрытия неактивных окон документов определенного типа, нам понадобится механизм отслеживания открытых форм. Поскольку в обычном приложении нет системного метода ПолучитьОкна(), мы разработаем собственный программный реестр.
Для реализации этого метода нам потребуется объявить глобальную переменную в Модуле обычного приложения и использовать общие процедуры конфигурации.
Шаг 1. Объявим глобальную переменную в Модуле обычного приложения:
Перем глОткрытыеФормы Экспорт;
Инициализируем её как коллекцию при старте системы:
Процедура ПриНачалеРаботыСистемы()
глОткрытыеФормы = Новый Соответствие;
КонецПроцедуры
Шаг 2. Напишем методы регистрации в общем модуле (например, РаботаСФормамиКлиент):
Процедура ЗарегистрироватьОткрытуюФорму(Форма) Экспорт
Если глОткрытыеФормы = Неопределено Тогда
глОткрытыеФормы = Новый Соответствие;
КонецЕсли;
// В качестве ключа используем строковое представление или уникальный идентификатор
КлючФормы = Строка(Форма);
глОткрытыеФормы.Вставить(КлючФормы, Форма);
КонецПроцедуры
Процедура РазрегистрироватьЗакрытуюФорму(Форма) Экспорт
Если глОткрытыеФормы <> Неопределено Тогда
КлючФормы = Строка(Форма);
глОткрытыеФормы.Удалить(КлючФормы);
КонецЕсли;
КонецПроцедуры
Шаг 3. Внедрим вызовы этих процедур в формы документов, которые подлежат контролю (это можно сделать централизованно через общие процедуры интерфейса, такие как РаботаСДиалогами в типовых решениях УТ 10.3 или УПП 1.3):
Процедура ПриОткрытии()
РаботаСФормамиКлиент.ЗарегистрироватьОткрытуюФорму(ЭтаФорма);
КонецПроцедуры
Процедура ПриЗакрытии()
РаботаСФормамиКлиент.РазрегистрироватьЗакрытуюФорму(ЭтаФорма);
КонецПроцедуры
Шаг 4. Теперь мы можем в любой момент обойти коллекцию открытых форм и закрыть нужные нам документы программно:
Процедура ЗакрытьВсеФормыДокументовОпределенногоТипа(ИмяМетаданных) Экспорт
СписокНаУдаление = Новый Массив;
Для Каждого Элемент Из глОткрытыеФормы Цикл
ТекущаяФорма = Элемент.Значение;
Если ТекущаяФорма.Открыта() Тогда
Попытка
// Проверяем тип объекта, связанного с формой
Если ТекущаяФорма.ДокументОбъект.Метаданные().Имя = ИмяМетаданных Тогда
ТекущаяФорма.Закрыть();
СписокНаУдаление.Добавить(Элемент.Ключ);
КонецЕсли;
Исключение
// Форма может не иметь свойства ДокументОбъект (например, это общая форма)
Продолжить;
КонецПопытки;
Иначе
СписокНаУдаление.Добавить(Элемент.Ключ);
КонецЕсли;
КонецЦикла;
// Очищаем наш реестр от закрытых форм
Для Каждого Ключ Из СписокНаУдаление Цикл
глОткрытыеФормы.Удалить(Ключ);
КонецЦикла;
КонецПроцедуры
Выясним, как быть, если конфигурация находится на жесткой поддержке поставщика и мы не можем добавлять туда глобальные переменные и переписывать события форм. Существует неочевидный платформенный лайфхак.
Если мы получаем форму конкретного существующего документа через метод ПолучитьФорму(), платформа 1С возвращает ссылку на уже открытый экземпляр этой формы (если он запущен у текущего пользователя). Мы можем сделать выборку документов за последний день и проверить их состояние:
Процедура ЗакрытьОткрытыеФормыДокументовЗаСегодня()
// Выбираем документы нужного типа, созданные за текущие сутки
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЧекККМ.Ссылка КАК Ссылка
|ИЗ
| Документ.ЧекККМ КАК ЧекККМ
|ГДЕ
| ЧекККМ.Дата МЕЖДУ &НачалоДня И &КонецДня";
Запрос.УстановитьПараметр("НачалоДня", НачалоДня(ТекущаяДата()));
Запрос.УстановитьПараметр("КонецДня", КонецДня(ТекущаяДата()));
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
// Метод вернет существующую форму, если она открыта на клиенте
ФормаДокумента = Выборка.Ссылка.ПолучитьФорму();
Если ФормаДокумента.Открыта() Тогда
// Закрываем форму программно с отработкой штатных процедур
ФормаДокумента.Закрыть();
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Важное ограничение метода: данный способ поможет закрыть только те документы, которые уже записаны в базу данных (имеют ссылку). Если пользователь открыл форму нового, еще не записанного документа, этот способ его не обнаружит. Тем не менее, для проведения регламентных процедур в конце дня этот метод отлично подходит.
Некоторые разработчики пытаются решить проблему имитацией действий пользователя с помощью операционной системы. Например, отправкой сочетания клавиш Ctrl + F4 (закрытие вкладки MDI) через Windows Script Host.
Проанализируем пример такого кода:
Процедура ЗакрытьОкноЧерезWShell()
Попытка
Шелл = Новый COMОбъект("WScript.Shell");
// Активируем окно программы 1С
Шелл.AppActivate("1С:Предприятие");
// Посылаем команду эмуляции нажатия комбинации клавиш Ctrl + F4
Шелл.SendKeys("^{F4}");
Исключение
Сообщить("Не удалось отправить команду закрытия окна: " + ОписаниеОшибки());
КонецПопытки;
EndProcedure
Критический анализ метода: Использование метода SendKeys() крайне не рекомендуется для стабильных бизнес-систем. Если в момент выполнения скрипта пользователь переключится на другое приложение (например, откроет интернет-браузер или почтовый клиент), скрипт отправит сочетание клавиш туда, что может привести к непредсказуемому закрытию важных программ пользователя.
Мы подробно разобрали различные способы борьбы с удержанием портов оборудования в обычном приложении 1С. Подведем итог и определим приоритетность решений: