HandyControl WPF TimePicker 时间选择器完全指南:从下拉时钟到数据校验的实战用法
HandyControl WPF TimePicker 时间选择器完全指南:从下拉时钟到数据校验的实战用法
HandyControl 是面向 WPF 的一套常用控件库,其中 TimePicker(时间选择器)允许用户通过在文本框中直接键入时间,或点击右侧按钮展开下拉时钟控件来选择时间。本文以 doc/source/handycontrol/extend_controls/timePicker/index.md 文档为主线,结合 TimePicker 源码 与官方主题样式,系统讲解 TimePicker 的创建、时间选择、时钟样式切换、标题与占位符配置、核心属性/方法与底层实现原理,帮助你快速在实际项目中落地并自由定制。
TimePicker 控件概览与模板结构
TimePicker 本质上是一个「文本框 + 下拉按钮 + Popup 弹层」的组合控件。它在源码中的类声明如下(见 TimePicker.cs):
[TemplatePart(Name = ElementRoot, Type = typeof(Grid))]
[TemplatePart(Name = ElementTextBox, Type = typeof(WatermarkTextBox))]
[TemplatePart(Name = ElementButton, Type = typeof(Button))]
[TemplatePart(Name = ElementPopup, Type = typeof(Popup))]
public class TimePicker : Control
{
private const string ElementRoot = "PART_Root";
private const string ElementTextBox = "PART_TextBox";
private const string ElementButton = "PART_Button";
private const string ElementPopup = "PART_Popup";
}
四个模板部件分别承担不同职责:
| 模板部件 | 类型 | 作用 |
|---|---|---|
PART_Root |
Grid |
整体布局容器 |
PART_TextBox |
WatermarkTextBox |
可输入时间的文本框,支持水印占位文本 |
PART_Button |
Button |
右侧下拉按钮(时钟图标) |
PART_Popup |
Popup |
承载下拉时钟的弹层,默认 StaysOpen="False" |
其中 PART_TextBox 使用 HandyControl 的 WatermarkTextBox,意味着它天然支持水印(Placeholder)显示,这正是后文「占位符文本」功能的基础。OnApplyTemplate() 中会通过 GetTemplateChild 获取这四个部件并绑定事件(源码位置),若模板缺失对应命名部件会抛出异常。
创建 TimePicker
创建 TimePicker 只需一行代码。XAML 方式:
<hc:TimePicker />
C# 方式:
var timePicker = new TimePicker();
默认样式由 TimePicker.xaml 中的隐式样式提供,它基于 TimePickerBaseStyle(定义于 TimePickerBaseStyle.xaml)。基础模板由两列 Grid 构成:左侧为文本框、右侧为时钟图标按钮,外层 Border 根据「鼠标悬停 / 焦点 / 弹层打开」状态切换 SecondaryBorderBrush 与 PrimaryBrush 边框颜色。控件被禁用时整体透明度降至 0.4。
选择时间
TimePicker 提供两条选择时间的路径:
- 文本框键入:直接输入如
14:30:00的时间文本,回车或失焦后触发解析。 - 下拉时钟选择:点击右侧按钮打开 Popup 中的时钟控件,在时钟上选择并确认。
两条路径最终都会写入 SelectedTime 依赖属性。键入文本的解析与回写逻辑在 SetSelectedTime() 中完成:文本非空时尝试 DateTime.Parse,解析成功则同步更新 SelectedTime 与 DisplayTime,解析失败则回退到当前选中值或 DisplayTime;文本清空时则将 SelectedTime 置为 null(源码位置)。
除了鼠标操作,TimePicker 还内置了键盘快捷方式(源码位置):
Alt + ↓:切换下拉时钟的打开 / 关闭;Enter:提交当前文本框中的时间。
设置选中时间同样可以在 XAML 或代码中进行:
<hc:TimePicker SelectedTime="{x:Static system:DateTime.Now}"/>
timePicker.SelectedTime = DateTime.Now;
SelectedTime 类型为 DateTime?,默认值为 null,并且注册时带有 BindsTwoWayByDefault,因此它天然支持双向绑定(源码位置)。当它变化时,控件内部会把时间按 TimeFormat 格式化后写回文本框,并抛出 SelectedTimeChanged 路由事件(FunctionEventArgs<DateTime?> 类型),方便在 MVVM 或事件模型中监听。
更改时钟样式
TimePicker 的 Clock 属性类型为 ClockBase(定义于 ClockBase.cs),它是一个抽象控件基类,提供 SelectedTime、DisplayTime、TimeFormat 等公共依赖属性,以及 Confirmed 事件——用户在时钟上点击确认按钮时触发。仓库中提供了两个具体时钟实现:
Clock:默认的时钟样式(Clock.cs);ListClock:列表式时钟样式(ListClock.cs)。
当未显式设置 Clock 时,OnApplyTemplate 会通过 EnsureClock() 自动创建一个默认 Clock 实例注入(源码位置)。
替换时钟样式只需设置 Clock 属性,示例(配合 TimePickerPlus 样式):
<hc:TimePicker ShowClearButton="True" Style="{StaticResource TimePickerPlus}">
<hc:TimePicker.Clock>
<hc:ListClock/>
</hc:TimePicker.Clock>
</hc:TimePicker>
源码层面的时钟联动机制值得注意:Clock 依赖属性注册为 NotDataBindable,其变更回调 OnClockChanged 会先解除旧时钟的 SelectedTimeChanged / Confirmed 订阅,再为新时钟订阅事件、强制设置 ShowConfirmButton = true,并将其挂到 Popup 的 Child 上(源码位置)。也就是说,Clock 必须通过 XAML 属性或代码设置,而不能用绑定。时钟中选中的时间通过 SelectedTimeChanged 事件回传,确认后通过 Confirmed 事件让 TimePicker 收起弹层。
设置标题与占位符文本
TimePicker 支持通过 HandyControl 的附加属性 InfoElement.Title(标题)与 InfoElement.Placeholder(占位符)向用户说明输入用途。要使用这两个属性,必须应用 TimePickerExtend 或 TimePickerPlus 样式(默认基础样式不含标题与水印布局):
<hc:TimePicker hc:InfoElement.Placeholder="{x:Static langs:Lang.PlsEnterContent}"
hc:InfoElement.Title="{x:Static langs:Lang.TitleDemoStr2}"
Style="{StaticResource TimePickerExtend}"/>
TimePickerExtend 与 TimePickerPlus 的差异在于:TimePickerPlus 额外提供了一个清除按钮(ButtonClear),在鼠标悬停且 InfoElement.ShowClearButton="True" 时显示,点击后通过 ControlCommands.Clear 命令清空 SelectedTime 与文本框(模板定义见 TimePickerBaseStyle.xaml,命令绑定见 TimePicker.cs 构造函数)。
这两种「扩展样式」都支持标题的两种排版位置,通过 InfoElement.TitlePlacement 切换:
- 默认(
Top):标题显示在输入框上方,使用TimePickerExtendTopTemplate; Left:标题显示在输入框左侧,需配合InfoElement.TitleWidth指定标题列宽度。
官方 Demo(TimePickerDemo.xaml)中给出了非常完整的组合用法,包括必填标记、清除按钮、标题居左、紧凑尺寸等:
<!-- 必填 + 占位符 + 标题居左 + 清除按钮 -->
<hc:TimePicker Width="380"
hc:InfoElement.TitleWidth="140"
hc:InfoElement.TitlePlacement="Left"
hc:InfoElement.Placeholder="{ex:Lang Key={x:Static langs:LangKeys.PlsEnterContent}}"
hc:InfoElement.Title="{ex:Lang Key={x:Static langs:LangKeys.TitleDemoStr3}}"
hc:InfoElement.Necessary="True"
hc:InfoElement.ShowClearButton="True"
Style="{StaticResource TimePickerPlus}"/>
与标题排版相关的附加属性还包括 InfoElement.ContentHeight / MinContentHeight(控制输入区高度)和 TitleElement.MarginOnTheTop / MarginOnTheLeft(标题间距),这些都在扩展样式的模板中被绑定。
样式一览
HandyControl 为 TimePicker 提供了完整的样式族(定义于 TimePicker.xaml):
| 样式键 | 基于 | 说明 |
|---|---|---|
TimePicker(隐式) |
TimePickerBaseStyle |
默认样式 |
TimePicker.Small |
TimePickerBaseStyle |
紧凑尺寸,MinHeight=20、Padding=6,0 |
TimePickerExtend |
TimePickerExtendBaseStyle |
支持标题 / 占位符 / 必填标记 |
TimePickerExtend.Small |
TimePickerExtend |
紧凑版扩展样式 |
TimePickerPlus |
TimePickerPlusBaseStyle |
在扩展样式基础上增加清除按钮 |
TimePickerPlus.Small |
TimePickerPlus |
紧凑版 Plus 样式 |
在 Demo 中还可以看到「禁用状态」组合:IsEnabled="False" 时控件以 40% 透明度显示;配合 InfoElement.IsReadOnly="True" 时,下拉按钮被隐藏、文本框变为只读(触发逻辑见 TimePickerBaseStyle.xaml)。
属性详解
下表汇总 TimePicker 的核心属性,并补充了源码中的默认值与行为细节:
| 属性 | 类型 / 默认值 | 描述 |
|---|---|---|
SelectedTime |
DateTime? / null,双向绑定 |
获取或设置当前选中的时间;变化时同步文本框并触发 SelectedTimeChanged 事件 |
Clock |
ClockBase / 默认 Clock |
获取或设置下拉时钟;不可绑定,只能通过 XAML 属性或代码设置 |
TimeFormat |
string / "HH:mm:ss" |
获取或设置用于显示选中时间的格式 |
DisplayTime |
DateTime / DateTime.Now,双向绑定 |
获取或设置时钟上要显示的时间(打开弹层时的初始时间) |
IsDropDownOpen |
bool / false,双向绑定 |
指示下拉时钟的打开 / 关闭状态;控件禁用时强制关闭 |
Text |
string / "" |
获取 TimePicker 显示的文本,或设置选定的时间(设置文本会触发时间解析) |
VerifyFunc |
数据验证委托 | 获取或设置数据验证委托 |
IsError |
bool |
获取或设置数据是否错误 |
ErrorStr |
string |
获取或设置错误提示 |
TextType |
文本类型 | 获取或设置文本类型 |
ShowClearButton |
bool(附加属性 InfoElement.ShowClearButton) |
是否在悬停时显示清除按钮(需 TimePickerPlus 系列样式) |
其中 TimeFormat、DisplayTime、IsDropDownOpen、SelectedTime、Text 的实现细节都已在源码中确认(TimePicker.cs)。例如 DisplayTime 的强制值回调会把时间同步给 Clock.DisplayTime,保证打开弹层时时钟显示的是当前时间;IsDropDownOpen 打开时会记录 _originalSelectedTime 并将焦点移到时钟上。
说明:
VerifyFunc、IsError、ErrorStr、TextType是官方文档列出的数据校验相关属性。需要留意的是,当前仓库中 TimePicker 源码 的类声明仅继承Control,尚未包含这些校验成员的实现,实际使用前建议以你所引用版本的程序集 API 为准。
此外,TimePicker 还继承了 TextBoxBase 的文本编辑外观属性:SelectionBrush、SelectionOpacity、CaretBrush(以及非 .NET 4.x 下的 SelectionTextBrush),这些属性会在 OnApplyTemplate 中绑定到内部文本框(源码位置),可用于自定义选中文本与光标的颜色。
事件与方法
路由事件:
| 事件 | 参数类型 | 触发时机 |
|---|---|---|
SelectedTimeChanged |
FunctionEventArgs<DateTime?> |
选中时间发生变化时 |
ClockOpened |
RoutedEventArgs |
下拉时钟打开时 |
ClockClosed |
RoutedEventArgs |
下拉时钟关闭时 |
后两个事件由 Popup 的 Opened / Closed 内部处理触发,并会回调 ClockBase 的虚方法 OnClockOpened() / OnClockClosed()(见 ClockBase.cs),便于自定义时钟实现打开 / 关闭时的行为。
方法:
| 方法 | 描述 |
|---|---|
VerifyData() |
验证数据(与数据校验属性配套) |
VerifyData() 属于文档描述的数据验证接口能力。除数据校验外,TimePicker 重写了 ToString(),返回 SelectedTime 按 TimeFormat 格式化后的字符串,未选中时返回空字符串(源码位置)。键盘交互方面,控件将 TabNavigation 覆盖为 Once 模式,自身不作为 Tab 停靠点,聚焦时光标会进入文本框并全选文本(源码位置 与 OnGotFocus)。
完整实战示例
综合上述特性,一个包含标题、必填、占位符、清除按钮与 ListClock 时钟的完整示例:
<Window xmlns:hc="https://handyorg.github.io/handycontrol"
xmlns:system="clr-namespace:System;assembly=mscorlib">
<StackPanel>
<!-- 基础用法 + 默认时钟 -->
<hc:TimePicker SelectedTime="{x:Static system:DateTime.Now}"/>
<!-- 带标题、占位符、必填标记与清除按钮 -->
<hc:TimePicker hc:InfoElement.Title="会议开始时间"
hc:InfoElement.Placeholder="请选择时间"
hc:InfoElement.Necessary="True"
hc:InfoElement.ShowClearButton="True"
Style="{StaticResource TimePickerPlus}"/>
<!-- 切换为列表式时钟 -->
<hc:TimePicker Style="{StaticResource TimePickerExtend}">
<hc:TimePicker.Clock>
<hc:ListClock/>
</hc:TimePicker.Clock>
</hc:TimePicker>
</StackPanel>
</Window>
后台代码方式:
var timePicker = new TimePicker
{
Style = (Style) FindResource("TimePickerExtend"),
SelectedTime = DateTime.Now
};
timePicker.SelectedTimeChanged += (s, e) =>
Console.WriteLine($"选中时间:{e.Info:HH:mm:ss}");
小结
HandyControl 的 TimePicker 在标准的「文本框 + 下拉时钟」交互之上,提供了可替换的 ClockBase 时钟体系、TimeFormat 格式化、DisplayTime 初始显示时间、键盘快捷操作、SelectedTimeChanged 事件,以及通过 InfoElement 附加属性实现的标题 / 占位符 / 必填 / 清除按钮等表单能力。配合 TimePickerExtend、TimePickerPlus 及其 .Small 系列样式,可以快速搭建风格统一且交互完整的时间输入控件。深入阅读 TimePicker 源码、基础模板 与 官方 Demo,即可按需定制属于自己的时间选择器。