Техническая статья

Формат TDateTimePicker в Delphi 5: DateTime_SetFormat

Элемент TDateTimePicker получил свойство Format где-то между Delphi 5 и Delphi 6. Точная граница версий редко документируется так, чтобы этому можно было доверять, и когда вы сталкиваетесь с этим, компилятор просто сообщает, что идентификатор не существует. Сюрпризов во время выполнения здесь ждать не приходится: строка, которая компилируется и работает в любой более новой версии Delphi, на Delphi 5 даёт жёсткую ошибку компиляции, так что настоящий вопрос в том, как собрать одно дерево исходников сразу под обе версии

Лежащий в основе элемент управления — это общий элемент управления Windows DATETIMEPICK_CLASS из comctl32.dll, и он всегда принимал строку формата через сообщение DTM_SETFORMAT. VCL в Delphi оборачивает это сообщение в свойство Format в новых версиях. В Delphi 5 вы отправляете сообщение самостоятельно через макрос-обертку DateTime_SetFormat, объявленную в CommCtrl

Паттерн условной компиляции

Стандартный подход — это блок условной компиляции, привязанный к символу версии. Delphi 5 определяет VER130; вы можете проверять его напрямую или определить собственный символ D5 в параметрах проекта, чтобы сделать метку ветвления более читаемой во всей кодовой базе. В любом случае, ветка Delphi 5 вызывает DateTime_SetFormat с дескриптором элемента управления; любая другая ветка назначает свойство:

Ветка Delphi 5 вызывает DateTime_SetFormat на дескрипторе контрола, а более новые версии VCL присваивают свойство Format — обе сходятся на сообщении DTM_SETFORMAT
Компиляторная условная директива держит одно дерево исходников для Delphi 5 и современных версий VCL; обе ветки в итоге отправляют контролу одно и то же сообщение DTM_SETFORMAT
{$IFDEF D5}
  DateTime_SetFormat(DateTimePicker1.Handle, PChar('MM/dd/yyyy'));
{$ELSE}
  DateTimePicker1.Format := 'MM/dd/yyyy';
{$ENDIF}

Добавьте CommCtrl в раздел uses модуля, который содержит ветку Delphi 5. Нет необходимости добавлять его безусловно; если вы предпочитаете ограничить область действия зависимости, оберните запись в uses в такое же условие:

uses
  ...,
{$IFDEF D5}
  CommCtrl,
{$ENDIF}
  ...;

Синтаксис строки формата

Строка формата, передаваемая в DateTime_SetFormat, следует формату элемента выбора даты и времени Windows, а не токенам FormatDateTime Delphi. Они выглядят похоже, но не взаимозаменяемы. Windows использует d для дня месяца (без ведущего нуля), dd для дня с нулем, M для номера месяца, MM для месяца с нулем, yy для двузначного года и yyyy для четырехзначного года. Поля времени используют h/hh для 12-часового формата, H/HH для 24-часового, m/mm для минут, s/ss для секунд. Буквенный текст помещается в одинарные кавычки внутри строки формата, поэтому 'dd/MM/yyyy' уже правильный, но 'dd '<literal>' MM' потребует встроенных пар одинарных кавычек

Напротив, FormatDateTime в Delphi использует d/dd/ddd/dddd для форматов дней с другой семантикой, и m для месяца (не M). Если вы скопируете строку формата из вызова FormatDateTime и передадите ее напрямую в DateTime_SetFormat, поля месяца и минут, вероятно, поменяются местами в выводе. Пишите строку формата элемента управления с нуля по документации Windows, а не конвертируйте из формата Delphi

Токены формата Windows-пикера против токенов FormatDateTime Delphi, где mm означает минуты в одном и месяц в другом
Выборщик даты-времени Windows и FormatDateTime в Delphi используют пересекающиеся токены с разными значениями, поэтому скопированная строка превращает поле месяца в минуты

Региональные настройки и глобальная переменная ShortDateFormat

Изменение глобальной переменной ShortDateFormat в Delphi не влияет на TDateTimePicker, который уже создан. Элемент выбора считывает региональные настройки из Windows во время создания и в дальнейшем отображается в соответствии с получаемыми сообщениями, а не глобальными переменными RTL Delphi. Это означает, что установка ShortDateFormat в FormCreate не изменит то, что отображает элемент управления. Вам нужен DateTime_SetFormat (или свойство Format в новых версиях Delphi), чтобы переопределить собственную строку формата элемента управления после того, как дескриптор будет существовать

Для приложений, развертываемых на международном уровне, протестируйте строку формата на машине с неамериканскими региональными настройками. Windows может переопределить пустую строку формата значением по умолчанию для локали, но явная непустая строка полностью заменяет региональные настройки. Обычно это то, что нужно, но это означает, что американоцентричный шаблон вроде 'MM/dd/yyyy' будет отображаться как есть для пользователей, которые ожидают dd/MM/yyyy по соглашению. Если элемент выбора даты ориентирован на пользователя, а не является чистым полем ввода данных, рассмотрите возможность использования настроек по умолчанию элемента управления (передайте nil в DateTime_SetFormat), чтобы он автоматически следовал локали пользователя

Пересоздание дескриптора

Настройка формата принадлежит дескриптору окна, а не какому-либо полю объекта VCL. Если дескриптор элемента управления уничтожается и пересоздается, что VCL может делать при смене родителя элемента управления или изменении определенных свойств стиля во время выполнения, формат возвращается к значению по умолчанию Windows. Безопасное место для применения DateTime_SetFormat — это обработчик, который запускается после создания дескриптора: переопределите CreateWnd в классе-потомке и вызовите его там, или примените его в событии OnEnter или OnShow только после подтверждения того, что дескриптор активен. Установка один раз в FormCreate часто достаточна для простых форм, но это ненадежно, если форма выполняет динамическое переключение стилей или смену родителя элементов управления

uses
  ComCtrls, CommCtrl;

type
  TFormattedDatePicker = class(TDateTimePicker)
  private
    FDisplayFormat: string;
    procedure ApplyDisplayFormat;
    procedure SetDisplayFormat(const Value: string);
  protected
    procedure CreateWnd; override;
  public
    property DisplayFormat: string read FDisplayFormat write SetDisplayFormat;
  end;

procedure TFormattedDatePicker.ApplyDisplayFormat;
begin
  if FDisplayFormat = '' then
    DateTime_SetFormat(Handle, nil)            // возврат к локали по умолчанию
  else
    DateTime_SetFormat(Handle, PChar(FDisplayFormat));
end;

procedure TFormattedDatePicker.CreateWnd;
begin
  inherited CreateWnd;
  ApplyDisplayFormat;                          // выполняется снова после каждого пересоздания дескриптора
end;

procedure TFormattedDatePicker.SetDisplayFormat(const Value: string);
begin
  FDisplayFormat := Value;
  if HandleAllocated then
    ApplyDisplayFormat;
end;

Строка формата хранится в поле объекта Delphi, поэтому переживает окно, и CreateWnd передаёт её в каждый новый дескриптор. Свойство называется DisplayFormat, а не Format, чтобы не конфликтовать с унаследованным Format, когда тот же модуль собирается на более новых версиях Delphi

Это же ограничение применяется к свойству Format VCL в новых версиях Delphi; внутренне оно вызывает то же сообщение Windows и сталкивается с тем же жизненным циклом дескриптора. Разница в том, что сеттер свойства VCL проверяет, выделен ли дескриптор перед вызовом, и автоматически применяет формат снова при CreateWnd. Прямой вызов DateTime_SetFormat не имеет такой защиты, поэтому ветка Delphi 5 должна быть более осторожной в отношении того, когда она выполняется

Удаление обходного пути

Когда проект прекращает поддержку Delphi 5, блок условной компиляции становится мертвым грузом. Удалите ветку {$IFDEF D5} и импорт CommCtrl с ограниченной областью видимости, оставив только прямое присвоение свойства Format. Оставьте удаление в одном коммите, чтобы было легко определить, если что-то неожиданно сломается на компиляторе, который вы не тестировали. Сама строка формата остается прежней; меняется только механизм доставки