技术文章

Delphi 5 中的 TDateTimePicker Format:DateTime_SetFormat

在 Delphi 5 和 Delphi 6 之间的某个版本,TDateTimePicker 获得了一个 Format 属性。确切的版本界限很少有你可以信赖的记录,而当你遇到它时,编译器只会告诉你该标识符不存在。在你拥有的每个较新的 Delphi 版本上都能编译和工作的代码行,在这个旧版本上会无声地或者直接失败,这取决于项目的结构。

底层控件是来自 comctl32.dll 的 Windows DATETIMEPICK_CLASS 公共控件,它一直通过 DTM_SETFORMAT 消息接受格式字符串。Delphi 的 VCL 在较新版本上将该消息包装在 Format 属性中。在 Delphi 5 上,你通过声明在 CommCtrl 中的 DateTime_SetFormat 宏包装器亲自发送该消息。

条件编译模式

标准的方法是基于一个版本符号的编译器条件块。Delphi 5 定义了 VER130;你可以直接针对它进行测试,或者在项目选项中定义你自己的 D5 符号,以使分支标签在整个代码库中更具可读性。无论采用哪种方式,Delphi 5 分支都会使用控件句柄调用 DateTime_SetFormat;其余所有分支则分配该属性:

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

CommCtrl 添加到包含 Delphi 5 分支的单元的 uses 子句中。不需要无条件添加;如果你更喜欢将依赖项限定在一定范围内,请将 uses 条目包裹在相同的条件中:

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

格式字符串语法

传递给 DateTime_SetFormat 的格式字符串遵循 Windows 日期时间选择器格式,而不是 Delphi 的 FormatDateTime 标记。两者看起来相似,但不可互换。Windows 使用 d 表示月中的某天(无前导零),dd 表示以零填充的天数,M 表示月份数字,MM 表示以零填充的月份,yy 表示两位数年份,yyyy 表示四位数年份。时间字段使用 h/hh 表示 12 小时制小时,H/HH 表示 24 小时制,m/mm 表示分钟,s/ss 表示秒。字面文本放在格式字符串内的单引号中,所以 'dd/MM/yyyy' 已经是正确的了,但 'dd '<literal>' MM' 将需要内嵌单引号对。

相比之下,Delphi 的 FormatDateTime 使用具有不同语义的 d/dd/ddd/dddd 表示日期形式,使用 m 表示月份(不是 M)。如果你从 FormatDateTime 调用中复制格式字符串并将其直接传递给 DateTime_SetFormat,在输出中月份和分钟字段可能会被调换。根据 Windows 文档从头编写控件的格式字符串,而不是从 Delphi 格式转换。

区域设置和全局 ShortDateFormat

更改 Delphi 的全局 ShortDateFormat 变量不会影响已经创建的 TDateTimePicker。该选择器在创建时从 Windows 读取区域设置,此后根据其收到的消息进行呈现,而不是 Delphi RTL 全局变量。这意味着在 FormCreate 中设置 ShortDateFormat 不会改变控件显示的内容。在句柄存在之后,你需要 DateTime_SetFormat(或较新 Delphi 上的 Format 属性)来覆盖控件自身的格式字符串。

对于在国际范围内部署的应用程序,请在具有非美国区域设置的机器上测试该格式字符串。Windows 可以用本地默认值覆盖空白或 nil 格式字符串,但明确的非 nil 字符串完全取代区域设置。这通常是你想要的,但这意味着像 'MM/dd/yyyy' 这样以美国为中心的模式将按原样显示给按照惯例期望使用 dd/MM/yyyy 的用户。如果日期选择器是面向用户的而不是纯粹的数据输入字段,请考虑让控件默认(传递 nilDateTime_SetFormat),这样它就能自动遵循用户的区域设置。

句柄重建

格式设置属于窗口句柄,不属于任何 VCL 对象字段。如果控件的句柄被销毁然后重建(当你在运行时重设控件的父级或改变某些样式属性时,VCL 可能会这样做),格式将恢复为 Windows 默认值。应用 DateTime_SetFormat 的安全位置是在句柄创建后运行的处理程序中:在后代类中重写 CreateWnd 并在那里调用它,或者仅在确认句柄有效之后,在 OnEnterOnShow 事件中应用它。在 FormCreate 中设置一次通常对简单的表单来说是够的,但如果该表单进行动态样式切换或控件父级重置,那是不可靠的。

同样的限制也适用于较新 Delphi 版本上的 VCL Format 属性;在内部它调用同一个 Windows 消息并面临同样的句柄生命周期。不同之处在于 VCL 属性设置器在继续调用之前会检查句柄是否已被分配,并在 CreateWnd 时自动重新应用格式。原始的 DateTime_SetFormat 调用没有此类保护,因此 Delphi 5 分支需要更加审慎地决定它何时运行。

移除变通方法

当一个项目放弃 Delphi 5 支持时,条件块就成了无用的负担。移除 {$IFDEF D5} 分支和限定范围的 CommCtrl 导入,只留下直接的 Format 属性分配。将这种移除保留在一个单一提交中,这样如果有东西在你未测试的编译器上意外崩溃,很容易识别出来。格式字符串本身保持不变;改变的只是交付机制。