Artykuł techniczny

Format TDateTimePicker w Delphi 5: DateTime_SetFormat

TDateTimePicker zyskał właściwość Format gdzieś między Delphi 5 a Delphi 6. Dokładna granica wersji jest rzadko udokumentowana w sposób, któremu można ufać, a kiedy na nią trafisz, kompilator po prostu poinformuje cię, że identyfikator nie istnieje. Nie ma tu żadnej niespodzianki w runtime, której trzeba by szukać: linia, która kompiluje się i działa w każdej nowszej wersji Delphi, jaką masz, w Delphi 5 kończy się twardym błędem kompilacji, więc prawdziwe pytanie brzmi, jak utrzymać jedno drzewo źródeł, które buduje się w obu

Podstawową kontrolką jest wspólna kontrolka Windows DATETIMEPICK_CLASS z pliku comctl32.dll, która od zawsze akceptowała ciąg formatu poprzez komunikat DTM_SETFORMAT. VCL w nowszych wersjach Delphi opakowuje ten komunikat we właściwość Format. W Delphi 5 musisz wysłać ten komunikat samodzielnie za pomocą makra DateTime_SetFormat zadeklarowanego w module CommCtrl

Wzorzec kompilacji warunkowej

Standardowym podejściem jest blok kompilacji warunkowej oparty na symbolu wersji. Delphi 5 definiuje VER130; możesz sprawdzić to bezpośrednio lub zdefiniować własny symbol D5 w opcjach projektu, aby uczynić etykietę gałęzi bardziej czytelną w całym kodzie. W obu przypadkach gałąź dla Delphi 5 wywołuje DateTime_SetFormat z uchwytem kontrolki; każda inna gałąź przypisuje właściwość:

Gałąź Delphi 5 wywołuje DateTime_SetFormat na uchwycie kontrolki, podczas gdy nowsze wersje VCL przypisują właściwość Format, oba zbiegając się w komunikacie DTM_SETFORMAT
Warunek kompilatora utrzymuje jedno drzewo źródłowe dla Delphi 5 i nowoczesnych wersji VCL; obie gałęzie kończą, wysyłając tę samą wiadomość DTM_SETFORMAT do kontrolki
{$IFDEF D5}
  DateTime_SetFormat(DateTimePicker1.Handle, PChar('MM/dd/yyyy'));
{$ELSE}
  DateTimePicker1.Format := 'MM/dd/yyyy';
{$ENDIF}

Dodaj CommCtrl do sekcji uses w module, który zawiera gałąź dla Delphi 5. Nie ma potrzeby dodawania tego bezwarunkowo; jeśli wolisz ograniczyć zasięg zależności, opakuj wpis w uses w ten sam warunek:

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

Składnia ciągu formatu

Ciąg formatu przekazywany do DateTime_SetFormat jest zgodny z formatem kontrolki wyboru daty i godziny w systemie Windows, a nie z tokenami FormatDateTime w Delphi. Oba wyglądają podobnie, ale nie można ich stosować zamiennie. Windows używa d dla dnia miesiąca (bez zera wiodącego), dd dla dnia z zerem wiodącym, M dla numeru miesiąca, MM dla miesiąca z zerem wiodącym, yy dla dwucyfrowego roku i yyyy dla czterocyfrowego roku. Pola czasu używają h/hh dla godzin w formacie 12-godzinnym, H/HH dla 24-godzinnego, m/mm dla minut, s/ss dla sekund. Tekst dosłowny umieszcza się w pojedynczych cudzysłowach wewnątrz ciągu formatu, więc 'dd/MM/yyyy' jest poprawne, ale 'dd '<literal>' MM' wymagałoby wbudowanych par pojedynczych cudzysłowów

Dla porównania, FormatDateTime w Delphi używa d/dd/ddd/dddd dla formatów dni o różnej semantyce i m dla miesiąca (nie M). Jeśli skopiujesz ciąg formatu z wywołania FormatDateTime i przekażesz go bezpośrednio do DateTime_SetFormat, pola miesiąca i minuty prawdopodobnie zostaną zamienione na wyjściu. Napisz ciąg formatu kontrolki od podstaw zgodnie z dokumentacją systemu Windows, zamiast konwertować go z formatu Delphi

Tokeny formatu selektora Windows kontra tokeny FormatDateTime Delphi, gdzie mm znaczy minuty w jednym i miesiąc w drugim
Windowsowy wybieracz daty i czasu oraz FormatDateTime z Delphi używają nakładających się tokenów o różnych znaczeniach, więc skopiowany ciąg zamienia pole miesięcy na minuty

Ustawienia regionalne a globalna zmienna ShortDateFormat

Zmiana globalnej zmiennej ShortDateFormat w Delphi nie wpływa na utworzony już komponent TDateTimePicker. Kontrolka odczytuje ustawienia regionalne z systemu Windows w momencie tworzenia, a następnie renderuje zgodnie z otrzymywanymi komunikatami, a nie ze zmiennymi globalnymi RTL Delphi. Oznacza to, że ustawienie ShortDateFormat w FormCreate nie zmieni tego, co wyświetla kontrolka. Potrzebujesz funkcji DateTime_SetFormat (lub właściwości Format w nowszych wersjach Delphi), aby nadpisać własny ciąg formatu kontrolki po utworzeniu jej uchwytu

W przypadku aplikacji wdrażanych międzynarodowo przetestuj ciąg formatu na maszynie z ustawieniami regionalnymi innymi niż amerykańskie. Windows może nadpisać pusty (blank lub nil) ciąg formatu domyślnym dla danych ustawień regionalnych, ale jawny, niepusty ciąg całkowicie zastępuje ustawienia regionalne. Zazwyczaj jest to pożądane zachowanie, ale oznacza to, że amerykański wzorzec, taki jak 'MM/dd/yyyy', pojawi się w niezmienionej formie u użytkowników, którzy zwyczajowo oczekują dd/MM/yyyy. Jeśli kontrolka wyboru daty jest skierowana do użytkownika, a nie jest czystym polem wprowadzania danych, rozważ pozostawienie ustawienia domyślnego kontrolki (przekaż nil do DateTime_SetFormat), aby automatycznie dostosowywała się do ustawień regionalnych użytkownika

Ponowne tworzenie uchwytu

Ustawienie formatu należy do uchwytu okna, a nie do żadnego pola obiektu VCL. Jeśli uchwyt kontrolki zostanie zniszczony i utworzony ponownie, co VCL może zrobić podczas zmiany rodzica kontrolki lub zmiany niektórych właściwości stylu w czasie wykonywania, format powróci do domyślnego dla systemu Windows. Bezpiecznym miejscem na zastosowanie DateTime_SetFormat jest procedura obsługi uruchamiana po utworzeniu uchwytu: nadpisz CreateWnd w klasie pochodnej i wywołaj ją tam, lub zastosuj w zdarzeniu OnEnter lub OnShow dopiero po upewnieniu się, że uchwyt jest aktywny. Jednorazowe ustawienie tego w FormCreate często wystarcza dla prostych formularzy, ale nie jest niezawodne, jeśli formularz wykonuje dynamiczne przełączanie stylów lub zmianę rodzica kontrolki

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)            // powrót do domyślnego formatu z ustawień regionalnych
  else
    DateTime_SetFormat(Handle, PChar(FDisplayFormat));
end;

procedure TFormattedDatePicker.CreateWnd;
begin
  inherited CreateWnd;
  ApplyDisplayFormat;                          // wykonuje się ponownie po każdym odtworzeniu uchwytu
end;

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

Ciąg formatu siedzi w polu obiektu Delphi, więc przeżywa samo okno, a CreateWnd wpycha go do każdego nowego uchwytu. Właściwość nazywa się DisplayFormat, a nie Format, żeby nie kolidowała z odziedziczonym Format, gdy ten sam moduł buduje się w nowszym Delphi

To samo ograniczenie dotyczy właściwości Format w VCL w nowszych wersjach Delphi; wewnętrznie wywołuje ona ten sam komunikat systemu Windows i podlega temu samemu cyklowi życia uchwytu. Różnica polega na tym, że setter właściwości w VCL sprawdza, czy uchwyt jest przydzielony przed wykonaniem wywołania, i automatycznie ponownie stosuje format podczas CreateWnd. Surowe wywołanie DateTime_SetFormat nie ma takiego zabezpieczenia, więc gałąź dla Delphi 5 musi być bardziej przemyślana pod kątem momentu uruchomienia

Usunięcie obejścia

Gdy projekt rezygnuje ze wsparcia dla Delphi 5, blok warunkowy staje się martwym kodem. Usuń gałąź {$IFDEF D5} i powiązany import CommCtrl, pozostawiając tylko bezpośrednie przypisanie właściwości Format. Zachowaj usunięcie w jednym commicie, aby łatwo było zidentyfikować problem, jeśli coś nieoczekiwanie zepsuje się w kompilatorze, którego nie testowałeś. Sam ciąg formatu pozostaje ten sam; zmienia się tylko mechanizm jego dostarczania