Создание кастомного календаря на FireMonkey с нуля
Я Равиль Зарипов, разработчик из Алматы. В кроссплатформенных приложениях на Delphi и FireMonkey мне часто требовался календарь с фирменным оформлением, понятной работой с событиями и одинаковым поведением на пяти операционных системах.
Стандартный выбор даты решает лишь узкую задачу: открыть окно, выбрать день и закрыть диалог. Для приложения с расписанием, планированием или фильтрами нужен полноценный компонент, который отображает месяц, управляет выделением и предоставляет событиям понятный интерфейс.
Практичный подход предполагает разделение кода на календарную математику, модель состояния, отрисовку и обработку ввода. Каждый слой получает одну ответственность, поэтому поведение можно проверять независимо от графического интерфейса.
В результате получается календарь, который выглядит одинаково на Android, iOS, Windows, macOS и Linux, сохраняет выбранную дату и легко адаптируется под тему приложения.
Архитектура календаря
Основу компонента разумно построить на TControl и переопределить процедуру отрисовки. Так календарь остаётся обычным элементом FireMonkey: его можно размещать в TLayout, менять размеры, применять стили и накладывать поверх других элементов. Отдельные формы для каждого режима не нужны.
Календарная логика должна находиться в независимом классе. Он принимает выбранную дату, текущий отображаемый месяц и диапазон разрешённых значений, а также сообщает о навигации и выборе. Компонент графического интерфейса преобразует эти действия в события, не содержащие сведений о координатах или шрифтах.
Календарные вычисления и состояние
Для вычислений подходит TDateTime, дополненный функциями System.DateUtils. Разбор даты выполняется через DecodeDate, а переход к следующему дню — через IncDay. Количество дней в месяце, високосные годы и смещения начала недели рассчитываются отдельно, чтобы не зависеть от языковых настроек операционной системы.
Состояние включает отображаемый месяц, активную дату и выбранное значение. Эти поля нельзя смешивать: переход к соседнему месяцу не должен менять выделение, а выбор дня не должен неожиданно переводить календарь на другой год. Начало недели задаётся параметром локализации и одинаково применяется при расчёте сетки и генерации подсказок.
Отрисовка сетки и оформление
Месяц рисуется на TCanvas в координатах формы. Сначала вычисляются размеры ячеек, затем создаются прямоугольники дней, линии сетки и текстовые элементы. Область рисования ограничивается обрезанием, чтобы длинные подписи не выходили за границы компонента при изменении размера.
Текст требует отдельного измерения: шрифт, межстрочный интервал и доступная ширина ячейки влияют на число помещающихся символов. Названия месяцев и дней недели лучше получать из локализованного ресурса, а не хранить в коде. Цвета выбранного, активного, текущего и недоступного дня задаются через набор свойств, что позволяет связать календарь со светлой, тёмной или корпоративной темой.
Взаимодействие с пользователем
Каждое касание сначала преобразуется в локальные координаты компонента. Затем определяется строка и столбец сетки, после чего проверяется, находится ли точка внутри допустимого дня. Такой порядок предотвращает выбор даты за границами календаря и сохраняет правильную работу при разных размерах окна.
Короткое касание выбирает дату, горизонтальный свайп переключает месяц, а вертикальный перемещает фокус по неделям. Порог жеста должен учитывать масштаб экрана, иначе движение пальца на сенсорном устройстве трудно отличить от обычного нажатия. Дублирование и отмена жестов обрабатываются до вызова событий выбора.
Клавиатурная навигация реализуется через фокус компонента и направление клавиш. Enter и пробел подтверждают дату, стрелки меняют активный день, а Page Up и Page Down переключают месяцы. Для экранных читал важны правильный порядок фокуса, заметное состояние выделения и отсутствие зависимости выбора только от цвета.
Масштабирование, локализация и доступность
Размеры элементов лучше выражать долями доступной области, а фиксированные значения использовать лишь для минимальных отступов. При изменении ширины компонент пересчитывает сетку, а при изменении высоты регулирует число видимых строк. Так календарь сохраняет пропорции на телефоне, планшете и настольном компьютере.
Локализация влияет не только на подписи. В некоторых регионах неделя начинается с понедельника, в других — с воскресенья, а для части языков применяется другой порядок дней. Числа, разделители и направление текста также требуют настройки. Эти параметры следует передавать до первой отрисовки, чтобы изменить календарь без пересоздания формы.
Для доступных интерфейсов полезно предоставлять каждой дате текстовое описание и отдельное состояние ошибки. Контраст подписей следует проверять для обеих тем, а мелкие индикаторы не должны быть единственным способом сообщить о выбранном дне. FMX не скрывает базовые сложности платформ, но аккуратная структура управления делает их управляемыми.
События, сеть и обновление данных
Событие удобно представить отдельной моделью с устойчивым идентификатором, заголовком, интервалом и признаком доступности. Календарь отображает только события заданного диапазона, а выбор или перетаскивание передаёт наружу саму модель. Это позволяет менять способ отображения, не затрагивая код хранения данных.
Для загрузки календаря или списка событий применяют TNetHTTPClient; подходы к сокращению задержек и числа обращений разобраны в статье оптимизация HTTP-запросов. Сетевой ответ поступает в отдельном потоке, после чего обновление интерфейса выполняется в основном потоке приложения.
Долгие запросы необходимо отменять при закрытии формы или смене месяца. Полезный кеш сокращает повторные обращения, но для каждого ответа нужны метки актуальности и контроль ошибок. Если сервер временно недоступен, календарь показывает сохранённые события и отдельный индикатор, не подменяя их пустым состоянием.
Проверка, производительность и публикация
Проверка начинается с крайних дат: конец месяца, високосный год, переход через год и длинные названия локаций. Затем проверяются разные размеры формы, локализации, темы и способы ввода. Сенсорные жесты стоит тестировать отдельно, поскольку координаты касания и масштаб меняют пороговые значения.
Для производительности важно перерисовывать только изменившуюся область и не создавать новые объекты внутри каждого кадра. Геометрию месяца можно рассчитывать заранее, а результаты измерения текста сохранять до изменения шрифта или размеров. Журналирование ошибок и сетевых запросов помогает находить проблемные состояния в сборках для конкретных платформ.
Если карточка события содержит внешний адрес, его открывают отдельным действием, как в примере казино ночью рядом, не смешивая переход с навигацией между датами.
| Подход | Разработка | Межплатформенность | Назначение |
|---|---|---|---|
| Встроенный выбор даты | Низкие затраты | Различается на платформах | Прототип и простые формы |
| Сторонний компонент | Средние затраты | Зависит от поставщика | Готовые сложные сценарии |
| Собственный FireMonkey-компонент | Высокие затраты | Единая логика и внешний вид | Продуктовый календарь |
Собственная реализация оправдана, когда внешний вид, правила выбора и интеграция с событиями важнее скорости прототипирования. Разделение модели, расчётов и отрисовки при этом сохраняет функциональность компонента и позволяет постепенно расширять его поведение без переписывания основы.