Article technique

Format de TDateTimePicker dans Delphi 5 : DateTime_SetFormat

TDateTimePicker a acquis une propriété Format quelque part entre Delphi 5 et Delphi 6. La frontière exacte entre les versions est rarement documentée de manière fiable, et lorsqu'on y est confronté, le compilateur signale simplement que l'identifiant n'existe pas. La ligne qui compile et fonctionne sur toutes les versions récentes de Delphi échoue silencieusement ou complètement sur l'ancienne, selon la structure du projet.

Le contrôle sous-jacent est le contrôle commun Windows DATETIMEPICK_CLASS de comctl32.dll, qui a toujours accepté une chaîne de format via le message DTM_SETFORMAT. La VCL de Delphi encapsule ce message dans la propriété Format sur les versions récentes. Sous Delphi 5, il faut envoyer le message directement via le macro-wrapper DateTime_SetFormat déclaré dans CommCtrl.

Le modèle de compilation conditionnelle

L'approche standard consiste en un bloc de compilation conditionnelle basé sur un symbole de version. Delphi 5 définit VER130 ; on peut tester ce symbole directement ou définir son propre symbole D5 dans les options du projet pour rendre la branche plus lisible dans tout le code source. Dans tous les cas, la branche Delphi 5 appelle DateTime_SetFormat avec le handle du contrôle ; toutes les autres branches affectent la propriété directement :

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

Ajoutez CommCtrl à la clause uses de l'unité qui contient la branche Delphi 5. Il n'est pas nécessaire de l'ajouter de façon inconditionnelle ; si l'on préfère limiter la dépendance à sa portée, on peut envelopper l'entrée uses dans la même directive conditionnelle :

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

Syntaxe des chaînes de format

La chaîne de format transmise à DateTime_SetFormat suit la syntaxe du date-time picker Windows, et non les jetons de FormatDateTime propres à Delphi. Les deux se ressemblent mais ne sont pas interchangeables. Windows utilise d pour le jour du mois sans zéro initial, dd pour le jour avec zéro, M pour le numéro de mois, MM pour le mois avec zéro, yy pour l'année sur deux chiffres et yyyy pour l'année sur quatre chiffres. Les champs de temps utilisent h/hh pour les heures en format 12 heures, H/HH pour le format 24 heures, m/mm pour les minutes, s/ss pour les secondes. Le texte littéral se place entre guillemets simples à l'intérieur de la chaîne de format, ainsi 'dd/MM/yyyy' est déjà correct, mais 'dd '<littéral>' MM' nécessiterait des paires de guillemets simples imbriqués.

En revanche, FormatDateTime de Delphi utilise d/dd/ddd/dddd pour les formes de jour avec des sémantiques différentes, et m pour le mois (et non M). Si l'on copie une chaîne de format d'un appel FormatDateTime et qu'on la passe directement à DateTime_SetFormat, les champs de mois et de minutes seront probablement inversés dans le résultat. Il vaut mieux rédiger la chaîne de format du contrôle de zéro en se référant à la documentation Windows, plutôt que de convertir depuis un format Delphi.

Paramètres régionaux et la variable globale ShortDateFormat

Modifier la variable globale ShortDateFormat de Delphi n'a aucun effet sur un TDateTimePicker déjà créé. Le sélecteur lit les paramètres régionaux de Windows au moment de sa création et se rend ensuite selon les messages qu'il reçoit, et non selon les variables globales du RTL Delphi. Cela signifie que définir ShortDateFormat dans FormCreate ne changera pas ce qu'affiche le contrôle. Il faut utiliser DateTime_SetFormat (ou la propriété Format sur les versions récentes de Delphi) pour remplacer la chaîne de format propre au contrôle une fois que le handle existe.

Pour les applications déployées à l'international, testez la chaîne de format sur une machine avec un paramètre régional non américain. Windows peut remplacer une chaîne de format vide ou nil par la valeur par défaut de la locale, mais une chaîne non-nil explicite supplante entièrement le paramètre régional. C'est généralement le comportement souhaité, mais cela signifie qu'un modèle américain comme 'MM/dd/yyyy' apparaîtra tel quel pour les utilisateurs qui s'attendent à dd/MM/yyyy par convention. Si le sélecteur de date est orienté utilisateur plutôt qu'un simple champ de saisie, envisagez de laisser le contrôle utiliser sa valeur par défaut (passer nil à DateTime_SetFormat) afin qu'il suive automatiquement la locale de l'utilisateur.

Recréation du handle

Le paramètre de format appartient au handle de fenêtre, et non à un champ d'objet VCL. Si le handle du contrôle est détruit et recréé, ce que la VCL peut faire lors d'un reparentage de contrôle ou d'un changement de certaines propriétés de style à l'exécution, le format revient à la valeur par défaut Windows. L'endroit approprié pour appeler DateTime_SetFormat est dans un gestionnaire qui s'exécute après la création du handle : il faut surcharger CreateWnd dans une classe dérivée et l'appeler là, ou l'appliquer dans l'événement OnEnter ou OnShow uniquement après avoir confirmé que le handle est actif. Un seul appel dans FormCreate est souvent suffisant pour les formulaires simples, mais n'est pas fiable si le formulaire effectue une permutation de style dynamique ou un reparentage de contrôle.

La même contrainte s'applique à la propriété VCL Format sur les versions récentes de Delphi ; en interne elle appelle le même message Windows et est soumise au même cycle de vie du handle. La différence est que le setter de la propriété VCL vérifie si le handle est alloué avant d'effectuer l'appel, et réapplique le format automatiquement dans CreateWnd. L'appel brut à DateTime_SetFormat n'offre pas cette protection, la branche Delphi 5 doit donc être plus prudente quant au moment de son exécution.

Suppression du contournement

Lorsqu'un projet abandonne le support de Delphi 5, le bloc conditionnel devient du code mort. Supprimez la branche {$IFDEF D5} et l'import conditionnel de CommCtrl, en ne conservant que l'affectation directe de la propriété Format. Faites cette suppression dans un seul commit afin qu'elle soit facilement identifiable si quelque chose se casse inopinément sur un compilateur qui n'a pas été testé. La chaîne de format elle-même reste la même ; seul le mécanisme de transmission change.