Technisch artikel

TDateTimePicker-formaat in Delphi 5: DateTime_SetFormat

TDateTimePicker heeft ergens tussen Delphi 5 en Delphi 6 een Format-eigenschap gekregen. De exacte versiegrens is zelden betrouwbaar gedocumenteerd, en wanneer u deze tegenkomt, meldt de compiler simpelweg dat de identifier niet bestaat. De regel die op elke nieuwere Delphi-versie compileert en werkt, faalt op de oude versie stilzwijgend of direct, afhankelijk van hoe het project is gestructureerd.

De onderliggende control is de Windows DATETIMEPICK_CLASS common control uit comctl32.dll, en deze heeft altijd al een formaat-string geaccepteerd via het DTM_SETFORMAT-bericht. De VCL van Delphi wrapt dat bericht in de Format-eigenschap op nieuwere versies. In Delphi 5 stuurt u het bericht zelf via de DateTime_SetFormat macro-wrapper die is gedeclareerd in CommCtrl.

Het patroon voor conditionele compilatie

De standaardaanpak is een conditioneel compilerblok op basis van een versiesymbool. Delphi 5 definieert VER130; u kunt hier direct op testen of uw eigen D5-symbool definiëren in de projectopties om het branch-label beter leesbaar te maken in de codebase. In beide gevallen roept de Delphi 5-branch DateTime_SetFormat aan met de handle van de control; elke andere branch wijst de eigenschap toe:

{$IFDEF D5}
  DateTime_SetFormat(DateTimePicker1.Handle, PChar('MM/dd/yyyy'));
{$ELSE}
  DateTimePicker1.Format := 'MM/dd/yyyy';
{$ENDIF}

Voeg CommCtrl toe aan de uses-clausule van de unit die de Delphi 5-branch bevat. Het is niet nodig om deze onvoorwaardelijk toe te voegen; als u de afhankelijkheid liever binnen de scope houdt, wikkel de uses-invoer dan in dezelfde conditional:

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

Syntaxis van de formaat-string

De formaat-string die aan DateTime_SetFormat wordt doorgegeven, volgt het Windows date-time picker-formaat, niet de tokens van Delphi's FormatDateTime. De twee lijken op elkaar maar zijn niet uitwisselbaar. Windows gebruikt d voor de dag van de maand (zonder voorloopnul), dd voor de dag met voorloopnul, M voor het maandnummer, MM voor de maand met voorloopnul, yy voor het jaartal in twee cijfers en yyyy voor het jaartal in vier cijfers. Tijdvelden gebruiken h/hh voor uren in 12-uursnotatie, H/HH voor 24-uursnotatie, m/mm voor minuten, s/ss voor seconden. Vaste tekst wordt tussen enkele aanhalingstekens binnen de formaat-string geplaatst, dus 'dd/MM/yyyy' is al correct, maar 'dd '<literal>' MM' vereist geneste koppels van enkele aanhalingstekens.

Delphi's FormatDateTime daarentegen gebruikt d/dd/ddd/dddd voor dagvormen met andere semantiek, en m voor de maand (niet M). Als u een formaat-string kopieert uit een FormatDateTime-aanroep en deze direct aan DateTime_SetFormat doorgeeft, worden de maand- en minuutvelden in de uitvoer waarschijnlijk omgewisseld. Schrijf de formaat-string van de control vanaf nul op basis van de Windows-documentatie in plaats van deze te converteren vanuit een Delphi-formaat.

Regionale instellingen en de globale ShortDateFormat

Het wijzigen van Delphi's globale variabele ShortDateFormat heeft geen invloed op een reeds gemaakte TDateTimePicker. De picker leest bij het maken de regionale instellingen van Windows en rendert daarna op basis van de berichten die hij ontvangt, niet de globale variabelen van de Delphi RTL. Dat betekent dat het instellen van ShortDateFormat in FormCreate niets verandert aan wat de control weergeeft. U hebt DateTime_SetFormat nodig (of de eigenschap Format in nieuwere Delphi-versies) om de eigen formaat-string van de control te overschrijven nadat de handle bestaat.

Voor applicaties die internationaal worden gedistribueerd, is het verstandig de formaat-string te testen op een machine met een niet-Amerikaanse regionale instelling. Windows kan een lege of nil formaat-string overschrijven met de standaardwaarde van de locale, maar een expliciete niet-nil string vervangt de regionale instelling volledig. Dat is meestal wat u wilt, maar het betekent wel dat een Amerikaans patroon zoals 'MM/dd/yyyy' ongewijzigd verschijnt voor gebruikers die dd/MM/yyyy verwachten. Als de datumkiezer gericht is op de gebruiker in plaats van een puur gegevensinvoerveld te zijn, overweeg dan om de control de standaardwaarde te laten gebruiken (geef nil door aan DateTime_SetFormat) zodat deze automatisch de locale van de gebruiker volgt.

Recreatie van de handle

De formaatinstelling hoort bij de window-handle, niet bij een VCL-objectveld. Als de handle van de control wordt vernietigd en opnieuw aangemaakt (wat de VCL kan doen wanneer u een control een nieuwe parent geeft of bepaalde stijleigenschappen tijdens runtime wijzigt), keert het formaat terug naar de Windows-standaard. De veilige plek om DateTime_SetFormat toe te passen is in een handler die na het maken van de handle wordt uitgevoerd: override CreateWnd in een afgeleide klasse en roep het daar aan, of pas het toe in de OnEnter- of OnShow-gebeurtenis nadat u hebt gecontroleerd of de handle actief is. Het eenmalig instellen in FormCreate is vaak voldoende voor eenvoudige formulieren, maar het is niet betrouwbaar als het formulier dynamisch van stijl wisselt of controls van parent veranderen.

Dezelfde beperking geldt voor de VCL-eigenschap Format in nieuwere Delphi-versies; intern roept deze hetzelfde Windows-bericht aan en heeft deze te maken met dezelfde levenscyclus van de handle. Het verschil is dat de VCL-property-setter controleert of de handle is toegewezen alvorens aan te roepen, en het formaat automatisch opnieuw toepast bij CreateWnd. De kale aanroep van DateTime_SetFormat heeft geen dergelijke bescherming, dus de Delphi 5-branch moet bewuster omgaan met het moment van uitvoering.

De workaround verwijderen

Wanneer een project de ondersteuning voor Delphi 5 beëindigt, wordt het conditionele blok ballast. Verwijder de {$IFDEF D5}-branch en de scoped CommCtrl-import, zodat alleen de directe toewijzing van de eigenschap Format overblijft. Voer deze verwijdering uit in één enkele commit, zodat het eenvoudig te identificeren is als er onverwacht iets kapotgaat op een compiler die u niet hebt getest. De formaat-string zelf blijft hetzelfde; alleen het overdrachtsmechanisme verandert.