Artigo Técnico

Formato do TDateTimePicker no Delphi 5: DateTime_SetFormat

TDateTimePicker ganhou uma propriedade Format algures entre o Delphi 5 e o Delphi 6. A fronteira exacta de versão raramente está documentada de forma fiável e, quando se dá de caras com ela, o compilador limita-se a dizer que o identificador não existe. A linha que compila e funciona em qualquer Delphi mais recente falha silenciosamente ou de forma direta no antigo, consoante a estrutura do projeto

O controlo subjacente é o common control DATETIMEPICK_CLASS do Windows, proveniente de comctl32.dll, e sempre aceitou uma string de formato através da mensagem DTM_SETFORMAT. A VCL do Delphi encapsula essa mensagem na propriedade Format nas versões mais recentes. No Delphi 5, envia-se a mensagem manualmente através do wrapper de macro DateTime_SetFormat declarado em CommCtrl

O padrão de compilação condicional

A abordagem habitual é um bloco condicional de compilação assente num símbolo de versão. O Delphi 5 define VER130; pode testar-se diretamente contra ele ou definir um símbolo próprio D5 nas opções do projeto para tornar o rótulo do ramo mais legível ao longo do código-fonte. De qualquer das formas, o ramo do Delphi 5 chama DateTime_SetFormat com o handle do controlo; todos os outros ramos atribuem a propriedade:

O ramo do Delphi 5 chama DateTime_SetFormat no handle do controlo, enquanto as versões VCL mais recentes atribuem a propriedade Format, convergindo ambos na mensagem DTM_SETFORMAT
A condicional do compilador mantém uma única árvore de código-fonte para o Delphi 5 e versões VCL modernas; ambos os ramos acabam por enviar a mesma mensagem DTM_SETFORMAT ao controlo
{$IFDEF D5}
  DateTime_SetFormat(DateTimePicker1.Handle, PChar('MM/dd/yyyy'));
{$ELSE}
  DateTimePicker1.Format := 'MM/dd/yyyy';
{$ENDIF}

Adicione CommCtrl à cláusula uses da unit que contém o ramo do Delphi 5. Não há necessidade de a adicionar incondicionalmente; se preferir manter a dependência limitada, envolva a entrada uses na mesma condicional:

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

Sintaxe da string de formato

A string de formato passada a DateTime_SetFormat segue o formato do date-time picker do Windows, não os tokens de FormatDateTime do Delphi. Os dois parecem-se mas não são intermutáveis. O Windows usa d para o dia do mês (sem zero à esquerda), dd para o dia com zero à esquerda, M para o número do mês, MM para o mês com zero à esquerda, yy para o ano com dois dígitos e yyyy para o ano com quatro dígitos. Os campos de hora usam h/hh para horas no formato de 12 horas, H/HH para o formato de 24 horas, m/mm para minutos e s/ss para segundos. O texto literal vai entre plicas dentro da string de formato, pelo que 'dd/MM/yyyy' já está correto, mas 'dd '<literal>' MM' exigiria pares de plicas embutidos

Já o FormatDateTime do Delphi usa d/dd/ddd/dddd para as formas do dia com semânticas diferentes, e m para o mês (não M). Se copiar uma string de formato de uma chamada a FormatDateTime e a entregar diretamente a DateTime_SetFormat, os campos de mês e minuto provavelmente ficarão trocados no resultado. Escreva a string de formato do controlo de raiz, a partir da documentação do Windows, em vez de a converter de um formato do Delphi

Tokens de formato do seletor Windows versus tokens de FormatDateTime do Delphi, em que mm significa minutos num e mês no outro
O seletor de data-hora do Windows e o FormatDateTime do Delphi usam tokens sobrepostos com significados diferentes, pelo que uma cadeia copiada transforma o campo do mês em minutos

Definições regionais e o ShortDateFormat global

Alterar a variável global ShortDateFormat do Delphi não afeta um TDateTimePicker já criado. O picker lê as definições regionais do Windows no momento da criação e, depois disso, renderiza de acordo com as mensagens que recebe, não com as globais da RTL do Delphi. Isso significa que definir ShortDateFormat em FormCreate não altera o que o controlo apresenta. É necessário DateTime_SetFormat (ou a propriedade Format nas versões mais recentes do Delphi) para substituir a string de formato do próprio controlo depois de o handle já existir

Para aplicações implementadas internacionalmente, teste a string de formato numa máquina com uma definição regional que não seja dos EUA. O Windows pode substituir uma string de formato vazia ou nil pela predefinição regional, mas uma string explícita e não nula sobrepõe-se inteiramente à definição regional. É normalmente isso que se pretende, mas significa que um padrão centrado nos EUA como 'MM/dd/yyyy' aparecerá tal e qual para utilizadores que esperam dd/MM/yyyy por convenção. Se o seletor de data for orientado ao utilizador em vez de um campo puramente de introdução de dados, considere deixar o controlo assumir a predefinição (passando nil a DateTime_SetFormat) para que siga automaticamente a definição regional do utilizador

Recriação do handle

A definição de formato pertence ao handle da janela, não a nenhum campo de objeto da VCL. Se o handle do controlo for destruído e recriado, o que a VCL pode fazer quando se reatribui o pai a um controlo ou se alteram certas propriedades de estilo em tempo de execução, o formato regressa à predefinição do Windows. O local seguro para aplicar DateTime_SetFormat é num manipulador que corre depois da criação do handle: sobreponha CreateWnd numa classe descendente e chame-o aí, ou aplique-o no evento OnEnter ou OnShow apenas depois de confirmar que o handle está ativo. Defini-lo uma única vez em FormCreate costuma bastar para formulários simples, mas não é fiável se o formulário fizer troca dinâmica de estilos ou reatribuição de pai a controlos

A mesma restrição aplica-se à propriedade Format da VCL nas versões mais recentes do Delphi; internamente, chama a mesma mensagem do Windows e enfrenta o mesmo ciclo de vida do handle. A diferença é que o setter da propriedade da VCL verifica se o handle está alocado antes de fazer a chamada, e reaplica o formato automaticamente em CreateWnd. A chamada direta a DateTime_SetFormat não tem essa proteção, pelo que o ramo do Delphi 5 precisa de ser mais criterioso quanto ao momento em que corre

Remover o contorno

Quando um projeto deixa de suportar o Delphi 5, o bloco condicional torna-se peso morto. Remova o ramo {$IFDEF D5} e a importação limitada de CommCtrl, deixando apenas a atribuição direta da propriedade Format. Mantenha a remoção num único commit, para ser fácil de identificar caso algo se parta inesperadamente num compilador que não tenha testado. A string de formato em si mantém-se igual; só muda o mecanismo de entrega