При кастомизации и улучшении юзабилити интерфейсов в среде 1С:Предприятие 7.7 разработчики часто используют замечательную внешнюю компоненту FormEx (ФормЭкс). Она позволяет динамически управлять свойствами элементов диалога, включая цвета, видимость и, конечно же, шрифты. Однако при попытке программно изменить шрифт у элемента формы (например, текстового поля или кнопки) программисты нередко сталкиваются со странным поведением: какие бы параметры жирности или размера ни передавались в код, визуально шрифт либо вообще не меняется, либо отображается жирным по умолчанию, либо искажает пропорции интерфейса.
В этой статье мы подробно разберем, почему управление шрифтами через FormEx может работать некорректно, исследуем механизм взаимодействия компоненты с графической подсистемой Windows GDI и научимся правильно применять и уничтожать шрифты без риска зависания программы.
Давайте рассмотрим типичный пример кода из практики разработчиков, в котором создается новый шрифт и назначается элементу формы (атрибуту "Комментарий") при ее открытии:
Процедура ПослеОткрытия()
РФ = СоздатьОбъект("РасширениеФормы");
Шрифт = Сервис.СоздатьШрифт(, 13, 1);
РФ.ПолучитьАтрибут("Комментарий").Шрифт = Шрифт;
Сервис.УдалитьШрифт(Шрифт);
КонецПроцедуры
На первый взгляд, логика абсолютно верная и чистая. Мы создали объект шрифта с помощью метода СоздатьШрифт, присвоили его свойству Шрифт нужного элемента формы и сразу же вызвали УдалитьШрифт, чтобы освободить оперативную память компьютера. Однако именно в строке немедленного удаления кроется критическая ошибка, из-за которой вся верстка элемента "ломается".
Выясним причину этого сбоя, заглянув под капот операционной системы Windows и графической библиотеки GDI (Graphics Device Interface), которую использует 1С 7.7 для прорисовки окон:
СоздатьШрифт не создает "готовые буквы" внутри элемента 1С. Он обращается к операционной системе Windows и регистрирует в ней новый системный графический объект. В ответ операционная система возвращает числовой идентификатор — дескриптор объекта (Handle). Именно это число хранится в переменной Шрифт.РФ.ПолучитьАтрибут("Комментарий").Шрифт = Шрифт;, мы просто передаем элементу управления этот числовой дескриптор. Элемент запоминает ссылку, но физический рендеринг (отрисовка) текста на экране происходит асинхронно — чуть позже, когда Windows начнет формировать картинку окна.Сервис.УдалитьШрифт(Шрифт); в следующей строке, мы принудительно уничтожаем созданный GDI-объект в оперативной памяти Windows. Дескриптор становится недействительным.Чтобы избежать этой проблемы, мы должны изменить подход к управлению временем жизни шрифта. Созданный графический объект должен существовать в памяти на протяжении всего времени, пока открыта форма и на ней отображаются элементы с этим шрифтом. Удалять объект нужно только тогда, когда форма закрывается.
Давайте разберем по шагам правильную реализацию этого алгоритма:
ПослеОткрытия (или ПослеСозданияФормы).ПриЗакрытии формы.Посмотрим на практический пример кода:
Перем глШрифтЭлемента; // Переменная модуля формы
Процедура ПослеОткрытия()
РФ = СоздатьОбъект("РасширениеФормы");
// Создаем шрифт: имя "MS Sans Serif", высота 13, жирность 4 (NORMAL)
глШрифтЭлемента = Сервис.СоздатьШрифт("MS Sans Serif", 13, 4);
// Назначаем шрифт элементу управления
РФ.ПолучитьАтрибут("Комментарий").Шрифт = глШрифтЭлемента;
КонецПроцедуры
Процедура ПриЗакрытии()
// Уничтожаем объект шрифта только при закрытии формы
Если ПустоеЗначение(глШрифтЭлемента) = 0 Тогда
Сервис.УдалитьШрифт(глШрифтЭлемента);
КонецЕсли;
КонецПроцедуры
Используя эту схему, мы гарантируем, что объект шрифта будет существовать ровно столько, сколько открыто окно, и Windows сможет корректно и без искажений отрисовывать интерфейс.
Давайте проанализируем ситуацию, когда программист меняет параметр жирности (третий параметр в функции СоздатьШрифт) от 1 до 6, но на экране ничего не меняется, и шрифт выглядит абсолютно одинаково. Разница становится заметна только при установке значения 7 (BOLD) и более.
Параметр "Жирность" в FormEx принимает значения от 1 (THIN — самый тонкий) до 9 (HEAVY — самый толстый), транслируя их в стандартные веса шрифтов Windows API (от 100 до 900 соответственно). Но тонкие градации часто не работают на практике по двум причинам:
MS Sans Serif, Tahoma, Arial) физически содержат в своих файлах только два начертания: Regular (обычный, вес 400) и Bold (жирный, вес 700). Если вы затребуете жирность 1 (Thin, вес 100), системный сопоставитель шрифтов Windows (Font Mapper) поймет, что такого начертания в системе нет, и автоматически подставит наиболее близкое доступное — то есть Regular (4).Если ваша форма открывается модально, использование предопределенного события компоненты FormEx ПослеОткрытия() может приводить к нестабильному результату. В некоторых версиях компоненты в модальном режиме это событие может вообще не вызываться из-за особенностей перехвата сообщений Windows.
Для настройки визуального отображения элементов в модальных окнах рекомендуется использовать альтернативное событие — ПослеСозданияФормы(). Данное событие срабатывает, когда контекст формы и все элементы управления уже созданы в оперативной памяти компьютера, но само окно еще не выведено на экран монитора. Это не только гарантирует стабильное применение шрифтов в любых режимах открытия окон, но и предотвращает раздражающий эффект "мигания" элементов при их перерисовке.
Стоит помнить, что в операционной системе Windows установлен строгий лимит на количество одновременно открытых графических дескрипторов GDI (по умолчанию — 10 000 объектов на один запущенный процесс). Если вы будете создавать шрифты с помощью метода СоздатьШрифт и забывать удалять их в процедуре ПриЗакрытии, то ресурсы начнут накапливаться (произойдет утечка памяти GDI).
При достижении лимита программа 1С:Предприятие 7.7 начнет искажать интерфейс: перестанут прорисовываться кнопки, исчезнут иконки меню, а затем программа аварийно закроется без предупреждений и сохранения данных. Всегда следите за тем, чтобы каждому вызову создания шрифта соответствовал вызов его удаления при закрытии контекста.
Если один и тот же шрифт используется у вас во многих формах конфигурации, гораздо эффективнее и безопаснее создать его один раз глобально при запуске системы, а удалять только при выходе из программы:
// В Глобальном модуле
Перем глШрифтДляИнтерфейса Экспорт;
Процедура ПриНачалеРаботыСистемы()
ЗагрузитьВнешнююКомпоненту("FormEx.dll");
Сервис = СоздатьОбъект("Сервис");
// Создаем глобальный шрифт для всей системы
глШрифтДляИнтерфейса = Сервис.СоздатьШрифт("Tahoma", 11, 4);
КонецПроцедуры
Процедура ПриЗавершенииРаботыСистемы()
// Освобождаем ресурсы операционной системы при выходе
Если ПустоеЗначение(глШрифтДляИнтерфейса) = 0 Тогда
Сервис = СоздатьОбъект("Сервис");
Сервис.УдалитьШрифт(глШрифтДляИнтерфейса);
КонецЕсли;
КонецПроцедуры
В локальных формах вам больше не придется писать код удаления — достаточно будет просто назначить глобальную переменную глШрифтДляИнтерфейса свойству Шрифт нужного элемента управления. Это полностью исключит риск утечки ресурсов и сделает работу приложения стабильной.