2025-08-20 12:10:13 +08:00
|
|
|
|
using System.Globalization;
|
|
|
|
|
|
using System.Windows.Controls.Primitives;
|
|
|
|
|
|
|
2026-01-02 17:30:41 +08:00
|
|
|
|
namespace Melskin.Controls;
|
2025-08-20 12:10:13 +08:00
|
|
|
|
|
|
|
|
|
|
// 添加一个新的 TemplatePart
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// TimePicker 控件允许用户从一个时间选择器中选取时间。此控件支持12小时制和24小时制的时间显示,并且可以自定义分钟的增量。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[TemplatePart(Name = VbPartToggleButton, Type = typeof(ToggleButton))]
|
|
|
|
|
|
[TemplatePart(Name = VbPartPopup, Type = typeof(Popup))]
|
|
|
|
|
|
[TemplatePart(Name = VbPartHourTextBlock, Type = typeof(TextBlock))]
|
|
|
|
|
|
[TemplatePart(Name = VbPartMinuteTextBlock, Type = typeof(TextBlock))]
|
|
|
|
|
|
[TemplatePart(Name = VbPartPeriodTextBlock, Type = typeof(TextBlock))]
|
|
|
|
|
|
[TemplatePart(Name = VbPartHourSelector, Type = typeof(ListBox))]
|
|
|
|
|
|
[TemplatePart(Name = VbPartMinuteSelector, Type = typeof(ListBox))]
|
|
|
|
|
|
[TemplatePart(Name = VbPartPeriodSelector, Type = typeof(ListBox))]
|
|
|
|
|
|
[TemplatePart(Name = VbPartAcceptButton, Type = typeof(Button))]
|
|
|
|
|
|
[TemplatePart(Name = VbPartDismissButton, Type = typeof(Button))]
|
|
|
|
|
|
public class TimePicker : Control
|
|
|
|
|
|
{
|
|
|
|
|
|
private const string VbPartToggleButton = "PART_ToggleButton"; // 使用PART_前缀是WPF的惯例
|
|
|
|
|
|
private const string VbPartPopup = "TimePickerPopup";
|
|
|
|
|
|
private const string VbPartHourTextBlock = "HourTextBlock";
|
|
|
|
|
|
private const string VbPartMinuteTextBlock = "MinuteTextBlock";
|
|
|
|
|
|
private const string VbPartPeriodTextBlock = "PeriodTextBlock";
|
|
|
|
|
|
private const string VbPartHourSelector = "HourSelector";
|
|
|
|
|
|
private const string VbPartMinuteSelector = "MinuteSelector";
|
|
|
|
|
|
private const string VbPartPeriodSelector = "PeriodSelector";
|
|
|
|
|
|
private const string VbPartAcceptButton = "AcceptButton";
|
|
|
|
|
|
private const string VbPartDismissButton = "DismissButton";
|
|
|
|
|
|
|
|
|
|
|
|
private ToggleButton? toggleButton;
|
|
|
|
|
|
private Popup? popup;
|
|
|
|
|
|
private TextBlock? hourTextBlock;
|
|
|
|
|
|
private TextBlock? minuteTextBlock;
|
|
|
|
|
|
private TextBlock? periodTextBlock;
|
|
|
|
|
|
private ListBox? hourSelector;
|
|
|
|
|
|
private ListBox? minuteSelector;
|
|
|
|
|
|
private ListBox? periodSelector;
|
|
|
|
|
|
private Button? acceptButton;
|
|
|
|
|
|
private Button? dismissButton;
|
|
|
|
|
|
|
|
|
|
|
|
private bool isUpdatingSelection;
|
|
|
|
|
|
private TimeSpan? pendingTime;
|
|
|
|
|
|
|
|
|
|
|
|
#region Dependency Properties (与之前相同)
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 用于标识 TimePicker 控件中时钟类型的依赖属性。此属性决定了时间选择器是以12小时制还是24小时制显示时间。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static readonly DependencyProperty ClockIdentifierProperty = DependencyProperty.Register(
|
|
|
|
|
|
nameof(ClockIdentifier),
|
|
|
|
|
|
typeof(TimeClockIdentifier),
|
|
|
|
|
|
typeof(TimePicker),
|
|
|
|
|
|
new PropertyMetadata(TimeClockIdentifier.Clock24Hour, OnClockIdentifierChanged));
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 表示 TimePicker 控件的头部内容。此属性允许用户自定义显示在时间选择器顶部的信息或控件。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static readonly DependencyProperty HeaderProperty = DependencyProperty.Register(
|
|
|
|
|
|
nameof(Header),
|
|
|
|
|
|
typeof(object),
|
|
|
|
|
|
typeof(TimePicker),
|
|
|
|
|
|
new PropertyMetadata(null));
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 用于设置或获取 TimePicker 控件中分钟选择器的增量值的依赖属性。此属性定义了用户在选择时间时,分钟部分可选值的步长。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static readonly DependencyProperty MinuteIncrementProperty = DependencyProperty.Register(
|
|
|
|
|
|
nameof(MinuteIncrement),
|
|
|
|
|
|
typeof(int),
|
|
|
|
|
|
typeof(TimePicker),
|
|
|
|
|
|
new PropertyMetadata(1, OnMinuteIncrementChanged));
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 用于获取或设置 TimePicker 控件中用户选择的时间的依赖属性。此属性支持双向数据绑定,默认值为 null。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static readonly DependencyProperty SelectedTimeProperty = DependencyProperty.Register(
|
|
|
|
|
|
nameof(SelectedTime),
|
|
|
|
|
|
typeof(TimeSpan?),
|
|
|
|
|
|
typeof(TimePicker),
|
|
|
|
|
|
new FrameworkPropertyMetadata(
|
|
|
|
|
|
null,
|
|
|
|
|
|
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
|
|
|
|
|
|
OnSelectedTimeChanged));
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
static TimePicker()
|
|
|
|
|
|
{ DefaultStyleKeyProperty.OverrideMetadata(typeof(TimePicker), new FrameworkPropertyMetadata(typeof(TimePicker))); }
|
|
|
|
|
|
|
|
|
|
|
|
#region Public Properties (与之前相同)
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取或设置一个值,该值指示 TimePicker 控件中时钟的类型。此属性用于确定时间选择器是以12小时制还是24小时制显示时间。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public TimeClockIdentifier ClockIdentifier
|
|
|
|
|
|
{
|
|
|
|
|
|
get => (TimeClockIdentifier)GetValue(ClockIdentifierProperty);
|
|
|
|
|
|
set => SetValue(ClockIdentifierProperty, value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 用于设置或获取 TimePicker 控件顶部的标题内容。此属性允许开发者自定义显示在时间选择器上方的文字或其他类型的 UI 元素,以提供额外的信息或指示。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public object Header
|
|
|
|
|
|
{
|
|
|
|
|
|
get => GetValue(HeaderProperty); set => SetValue(HeaderProperty, value); }
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 用于设置或获取 TimePicker 控件中分钟选择器的增量值。此属性决定了分钟选择器列表中每项之间的间隔时间,单位为分钟。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public int MinuteIncrement
|
|
|
|
|
|
{
|
|
|
|
|
|
get => (int)GetValue(MinuteIncrementProperty);
|
|
|
|
|
|
set => SetValue(MinuteIncrementProperty, value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取或设置 TimePicker 控件中选定的时间。此属性为双向绑定,并且可以为空,表示没有选定时间。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public TimeSpan? SelectedTime
|
|
|
|
|
|
{
|
|
|
|
|
|
get => (TimeSpan?)GetValue(SelectedTimeProperty);
|
|
|
|
|
|
set => SetValue(SelectedTimeProperty, value);
|
|
|
|
|
|
}
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
|
public override void OnApplyTemplate()
|
|
|
|
|
|
{
|
|
|
|
|
|
base.OnApplyTemplate();
|
|
|
|
|
|
|
|
|
|
|
|
// 解绑旧模板事件
|
|
|
|
|
|
if(toggleButton != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
toggleButton.Checked -= OnToggleButtonChecked;
|
|
|
|
|
|
}
|
|
|
|
|
|
if(acceptButton != null)
|
|
|
|
|
|
acceptButton.Click -= OnAcceptButtonClick;
|
|
|
|
|
|
if(dismissButton != null)
|
|
|
|
|
|
dismissButton.Click -= OnDismissButtonClick;
|
|
|
|
|
|
if(hourSelector != null)
|
|
|
|
|
|
hourSelector.SelectionChanged -= OnSelectorSelectionChanged;
|
|
|
|
|
|
if(minuteSelector != null)
|
|
|
|
|
|
minuteSelector.SelectionChanged -= OnSelectorSelectionChanged;
|
|
|
|
|
|
if(periodSelector != null)
|
|
|
|
|
|
periodSelector.SelectionChanged -= OnSelectorSelectionChanged;
|
|
|
|
|
|
|
|
|
|
|
|
// 获取新模板部件
|
|
|
|
|
|
toggleButton = GetTemplateChild(VbPartToggleButton) as ToggleButton;
|
|
|
|
|
|
popup = GetTemplateChild(VbPartPopup) as Popup;
|
|
|
|
|
|
hourTextBlock = GetTemplateChild(VbPartHourTextBlock) as TextBlock;
|
|
|
|
|
|
minuteTextBlock = GetTemplateChild(VbPartMinuteTextBlock) as TextBlock;
|
|
|
|
|
|
periodTextBlock = GetTemplateChild(VbPartPeriodTextBlock) as TextBlock;
|
|
|
|
|
|
hourSelector = GetTemplateChild(VbPartHourSelector) as ListBox;
|
|
|
|
|
|
minuteSelector = GetTemplateChild(VbPartMinuteSelector) as ListBox;
|
|
|
|
|
|
periodSelector = GetTemplateChild(VbPartPeriodSelector) as ListBox;
|
|
|
|
|
|
acceptButton = GetTemplateChild(VbPartAcceptButton) as Button;
|
|
|
|
|
|
dismissButton = GetTemplateChild(VbPartDismissButton) as Button;
|
|
|
|
|
|
|
|
|
|
|
|
// 绑定新模板事件
|
|
|
|
|
|
if(toggleButton != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
toggleButton.Checked += OnToggleButtonChecked;
|
|
|
|
|
|
}
|
|
|
|
|
|
if(acceptButton != null)
|
|
|
|
|
|
acceptButton.Click += OnAcceptButtonClick;
|
|
|
|
|
|
if(dismissButton != null)
|
|
|
|
|
|
dismissButton.Click += OnDismissButtonClick;
|
|
|
|
|
|
if(hourSelector != null)
|
|
|
|
|
|
hourSelector.SelectionChanged += OnSelectorSelectionChanged;
|
|
|
|
|
|
if(minuteSelector != null)
|
|
|
|
|
|
minuteSelector.SelectionChanged += OnSelectorSelectionChanged;
|
|
|
|
|
|
if(periodSelector != null)
|
|
|
|
|
|
periodSelector.SelectionChanged += OnSelectorSelectionChanged;
|
|
|
|
|
|
|
|
|
|
|
|
PopulateSelectors();
|
|
|
|
|
|
UpdateDisplayedTime(SelectedTime);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 当ToggleButton被选中 (即用户点击控件) 时触发
|
|
|
|
|
|
private void OnToggleButtonChecked(object sender, RoutedEventArgs e)
|
|
|
|
|
|
{
|
|
|
|
|
|
if(popup == null)
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
// 用当前已选时间或当前系统时间来初始化弹窗内的选择器
|
|
|
|
|
|
pendingTime = SelectedTime ?? DateTime.Now.TimeOfDay;
|
|
|
|
|
|
|
|
|
|
|
|
UpdateSelectorFromTime(pendingTime);
|
|
|
|
|
|
// Popup的IsOpen状态已通过XAML绑定到ToggleButton的IsChecked,所以无需手动设置
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void OnAcceptButtonClick(object sender, RoutedEventArgs e)
|
|
|
|
|
|
{
|
|
|
|
|
|
SelectedTime = pendingTime;
|
|
|
|
|
|
ClosePopup();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void OnDismissButtonClick(object sender, RoutedEventArgs e) { ClosePopup(); }
|
|
|
|
|
|
|
|
|
|
|
|
private void ClosePopup()
|
|
|
|
|
|
{
|
|
|
|
|
|
if(toggleButton != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
toggleButton.IsChecked = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void OnSelectorSelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
|
|
|
|
{
|
|
|
|
|
|
if(isUpdatingSelection || toggleButton == null || toggleButton.IsChecked == false)
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
if(hourSelector?.SelectedItem == null ||
|
|
|
|
|
|
minuteSelector?.SelectedItem == null ||
|
|
|
|
|
|
(ClockIdentifier == TimeClockIdentifier.Clock12Hour && periodSelector?.SelectedItem == null))
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if(int.TryParse(hourSelector.SelectedItem.ToString(), out var hour) &&
|
|
|
|
|
|
int.TryParse(minuteSelector.SelectedItem.ToString(), out var minute))
|
|
|
|
|
|
{
|
|
|
|
|
|
if(ClockIdentifier == TimeClockIdentifier.Clock12Hour)
|
|
|
|
|
|
{
|
|
|
|
|
|
var period = periodSelector?.SelectedItem.ToString();
|
|
|
|
|
|
if(string.Equals(period, "PM", StringComparison.OrdinalIgnoreCase) && hour < 12)
|
|
|
|
|
|
hour += 12;
|
|
|
|
|
|
else if(string.Equals(period, "AM", StringComparison.OrdinalIgnoreCase) && hour == 12)
|
|
|
|
|
|
hour = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
pendingTime = new TimeSpan(hour, minute, 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#region Helper and Callback Methods (与之前基本相同)
|
|
|
|
|
|
private void PopulateSelectors()
|
|
|
|
|
|
{
|
|
|
|
|
|
if(hourSelector == null || minuteSelector == null || periodSelector == null)
|
|
|
|
|
|
return;
|
|
|
|
|
|
hourSelector.ItemsSource = ClockIdentifier == TimeClockIdentifier.Clock24Hour
|
|
|
|
|
|
? Enumerable.Range(0, 24).Select(h => h.ToString("D2"))
|
|
|
|
|
|
: Enumerable.Range(1, 12).Select(h => h.ToString());
|
|
|
|
|
|
minuteSelector.ItemsSource = Enumerable.Range(0, 60 / MinuteIncrement)
|
|
|
|
|
|
.Select(i => (i * MinuteIncrement).ToString("D2"));
|
|
|
|
|
|
periodSelector.ItemsSource = new[] { "AM", "PM" };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void UpdateDisplayedTime(TimeSpan? time)
|
|
|
|
|
|
{
|
|
|
|
|
|
if(hourTextBlock == null || minuteTextBlock == null)
|
|
|
|
|
|
return;
|
|
|
|
|
|
if(time.HasValue)
|
|
|
|
|
|
{
|
|
|
|
|
|
var dt = DateTime.Today.Add(time.Value);
|
|
|
|
|
|
var hourFormat = ClockIdentifier == TimeClockIdentifier.Clock24Hour ? "HH" : "%h";
|
|
|
|
|
|
hourTextBlock.Text = dt.ToString(hourFormat);
|
|
|
|
|
|
minuteTextBlock.Text = dt.ToString("mm");
|
|
|
|
|
|
if(periodTextBlock != null)
|
|
|
|
|
|
periodTextBlock.Text = dt.ToString("tt", CultureInfo.InvariantCulture);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
hourTextBlock.Text = "--";
|
|
|
|
|
|
minuteTextBlock.Text = "--";
|
|
|
|
|
|
if(periodTextBlock != null)
|
|
|
|
|
|
periodTextBlock.Text = "";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void UpdateSelectorFromTime(TimeSpan? time)
|
|
|
|
|
|
{
|
|
|
|
|
|
isUpdatingSelection = true;
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
if(time.HasValue && hourSelector != null && minuteSelector != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var dt = DateTime.Today.Add(time.Value);
|
|
|
|
|
|
if(ClockIdentifier == TimeClockIdentifier.Clock12Hour)
|
|
|
|
|
|
{
|
|
|
|
|
|
var hour12 = dt.Hour % 12;
|
|
|
|
|
|
if(hour12 == 0)
|
|
|
|
|
|
hour12 = 12;
|
|
|
|
|
|
hourSelector.SelectedItem = hour12.ToString();
|
|
|
|
|
|
if(periodSelector != null)
|
|
|
|
|
|
periodSelector.SelectedItem = dt.Hour >= 12 ? "PM" : "AM";
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
hourSelector.SelectedItem = dt.Hour.ToString("D2");
|
|
|
|
|
|
}
|
|
|
|
|
|
var roundedMinute = (int)(Math.Round((double)dt.Minute / MinuteIncrement) * MinuteIncrement) % 60;
|
|
|
|
|
|
minuteSelector.SelectedItem = roundedMinute.ToString("D2");
|
|
|
|
|
|
|
|
|
|
|
|
if(hourSelector.SelectedItem != null)
|
|
|
|
|
|
hourSelector.ScrollIntoView(hourSelector.SelectedItem);
|
|
|
|
|
|
if(minuteSelector.SelectedItem != null)
|
|
|
|
|
|
minuteSelector.ScrollIntoView(minuteSelector.SelectedItem);
|
|
|
|
|
|
}
|
|
|
|
|
|
} finally
|
|
|
|
|
|
{
|
|
|
|
|
|
isUpdatingSelection = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void OnSelectedTimeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
|
|
|
|
{
|
|
|
|
|
|
if(d is TimePicker picker)
|
|
|
|
|
|
picker.UpdateDisplayedTime(e.NewValue as TimeSpan?);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void OnClockIdentifierChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
|
|
|
|
{
|
|
|
|
|
|
if(d is TimePicker picker)
|
|
|
|
|
|
{
|
|
|
|
|
|
picker.PopulateSelectors();
|
|
|
|
|
|
picker.UpdateDisplayedTime(picker.SelectedTime);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void OnMinuteIncrementChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
|
|
|
|
{
|
|
|
|
|
|
if(d is TimePicker picker)
|
|
|
|
|
|
picker.PopulateSelectors();
|
|
|
|
|
|
}
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// TimeClockIdentifier 枚举定义了时间选择器中时钟的类型。它用于区分12小时制和24小时制的时间显示方式。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public enum TimeClockIdentifier
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
///
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
Clock12Hour,
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 表示24小时制的时间显示方式。在这种模式下,时间选择器将不区分上午和下午,直接以0-23小时的形式展示时间。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
Clock24Hour,
|
|
|
|
|
|
}
|