using System.Collections.ObjectModel; using System.Windows; using System.Windows.Controls; using System.Windows.Input; namespace XFEToolBox.WpfCore.Controls; /// /// 可按需显示小时、分钟和秒钟的 24 小时时间选择器。 /// public class TimePicker : Control { public static readonly RoutedUICommand SelectNowCommand = new( "选择当前时间", nameof(SelectNowCommand), typeof(TimePicker)); public static readonly RoutedUICommand ClearTimeCommand = new( "清除时间", nameof(ClearTimeCommand), typeof(TimePicker)); public static readonly RoutedUICommand ConfirmTimeCommand = new( "确认时间", nameof(ConfirmTimeCommand), typeof(TimePicker)); private static readonly DependencyPropertyKey DisplayTextPropertyKey = DependencyProperty.RegisterReadOnly( nameof(DisplayText), typeof(string), typeof(TimePicker), new PropertyMetadata("选择时间")); public static readonly DependencyProperty DisplayTextProperty = DisplayTextPropertyKey.DependencyProperty; public static readonly DependencyProperty SelectedTimeProperty = DependencyProperty.Register( nameof(SelectedTime), typeof(TimeSpan?), typeof(TimePicker), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedTimeChanged)); public static readonly DependencyProperty HourProperty = DependencyProperty.Register( nameof(Hour), typeof(int), typeof(TimePicker), new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnPartChanged)); public static readonly DependencyProperty MinuteProperty = DependencyProperty.Register( nameof(Minute), typeof(int), typeof(TimePicker), new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnPartChanged)); public static readonly DependencyProperty SecondProperty = DependencyProperty.Register( nameof(Second), typeof(int), typeof(TimePicker), new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnPartChanged)); public static readonly DependencyProperty MinuteIncrementProperty = DependencyProperty.Register( nameof(MinuteIncrement), typeof(int), typeof(TimePicker), new PropertyMetadata(5, OnMinuteIncrementChanged)); public static readonly DependencyProperty SecondIncrementProperty = DependencyProperty.Register( nameof(SecondIncrement), typeof(int), typeof(TimePicker), new PropertyMetadata(1, OnSecondIncrementChanged)); public static readonly DependencyProperty ShowHourProperty = DependencyProperty.Register( nameof(ShowHour), typeof(bool), typeof(TimePicker), new PropertyMetadata(true, OnDisplayedPartsChanged)); public static readonly DependencyProperty ShowMinuteProperty = DependencyProperty.Register( nameof(ShowMinute), typeof(bool), typeof(TimePicker), new PropertyMetadata(true, OnDisplayedPartsChanged)); public static readonly DependencyProperty ShowSecondProperty = DependencyProperty.Register( nameof(ShowSecond), typeof(bool), typeof(TimePicker), new PropertyMetadata(false, OnDisplayedPartsChanged)); public static readonly DependencyProperty PlaceholderTextProperty = DependencyProperty.Register( nameof(PlaceholderText), typeof(string), typeof(TimePicker), new PropertyMetadata("选择时间", OnPlaceholderChanged)); public static readonly DependencyProperty IsDropDownOpenProperty = DependencyProperty.Register( nameof(IsDropDownOpen), typeof(bool), typeof(TimePicker), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); public static readonly RoutedEvent SelectedTimeChangedEvent = EventManager.RegisterRoutedEvent( nameof(SelectedTimeChanged), RoutingStrategy.Bubble, typeof(RoutedPropertyChangedEventHandler), typeof(TimePicker)); private bool synchronizing; public TimePicker() { Hours = Enumerable.Range(0, 24).ToArray(); Minutes = []; Seconds = []; RebuildMinutes(); RebuildSeconds(); CommandBindings.Add(new CommandBinding(SelectNowCommand, (_, _) => SelectNow())); CommandBindings.Add(new CommandBinding(ClearTimeCommand, (_, _) => ClearTime())); CommandBindings.Add(new CommandBinding(ConfirmTimeCommand, (_, _) => SetCurrentValue(IsDropDownOpenProperty, false))); } public IReadOnlyList Hours { get; } public ObservableCollection Minutes { get; } public ObservableCollection Seconds { get; } public TimeSpan? SelectedTime { get => (TimeSpan?)GetValue(SelectedTimeProperty); set => SetValue(SelectedTimeProperty, value); } public int Hour { get => (int)GetValue(HourProperty); set => SetValue(HourProperty, value); } public int Minute { get => (int)GetValue(MinuteProperty); set => SetValue(MinuteProperty, value); } public int Second { get => (int)GetValue(SecondProperty); set => SetValue(SecondProperty, value); } public int MinuteIncrement { get => (int)GetValue(MinuteIncrementProperty); set => SetValue(MinuteIncrementProperty, value); } public int SecondIncrement { get => (int)GetValue(SecondIncrementProperty); set => SetValue(SecondIncrementProperty, value); } public bool ShowHour { get => (bool)GetValue(ShowHourProperty); set => SetValue(ShowHourProperty, value); } public bool ShowMinute { get => (bool)GetValue(ShowMinuteProperty); set => SetValue(ShowMinuteProperty, value); } public bool ShowSecond { get => (bool)GetValue(ShowSecondProperty); set => SetValue(ShowSecondProperty, value); } public string PlaceholderText { get => (string)GetValue(PlaceholderTextProperty); set => SetValue(PlaceholderTextProperty, value); } public bool IsDropDownOpen { get => (bool)GetValue(IsDropDownOpenProperty); set => SetValue(IsDropDownOpenProperty, value); } public string DisplayText => (string)GetValue(DisplayTextProperty); public event RoutedPropertyChangedEventHandler SelectedTimeChanged { add => AddHandler(SelectedTimeChangedEvent, value); remove => RemoveHandler(SelectedTimeChangedEvent, value); } private static void OnSelectedTimeChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e) { if (dependencyObject is not TimePicker picker) return; picker.UpdateFromSelectedTime((TimeSpan?)e.NewValue); picker.RaiseEvent(new RoutedPropertyChangedEventArgs( (TimeSpan?)e.OldValue, (TimeSpan?)e.NewValue, SelectedTimeChangedEvent)); } private static void OnPartChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e) { if (dependencyObject is not TimePicker picker || picker.synchronizing) return; picker.SetCurrentValue(SelectedTimeProperty, new TimeSpan( Math.Clamp(picker.Hour, 0, 23), Math.Clamp(picker.Minute, 0, 59), Math.Clamp(picker.Second, 0, 59))); } private static void OnMinuteIncrementChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e) { if (dependencyObject is TimePicker picker) picker.RebuildMinutes(); } private static void OnSecondIncrementChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e) { if (dependencyObject is TimePicker picker) picker.RebuildSeconds(); } private static void OnDisplayedPartsChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e) { if (dependencyObject is TimePicker picker) picker.UpdateDisplayText(picker.SelectedTime); } private static void OnPlaceholderChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e) { if (dependencyObject is TimePicker picker && picker.SelectedTime is null) picker.SetValue(DisplayTextPropertyKey, picker.PlaceholderText); } private void UpdateFromSelectedTime(TimeSpan? time) { synchronizing = true; try { if (time is { } value) { SetCurrentValue(HourProperty, Math.Clamp(value.Hours, 0, 23)); SetCurrentValue(MinuteProperty, Math.Clamp(value.Minutes, 0, 59)); SetCurrentValue(SecondProperty, Math.Clamp(value.Seconds, 0, 59)); } } finally { synchronizing = false; } UpdateDisplayText(time); } private void UpdateDisplayText(TimeSpan? time) { if (time is null || (!ShowHour && !ShowMinute && !ShowSecond)) { SetValue(DisplayTextPropertyKey, PlaceholderText); return; } var value = time.Value; var parts = new List(3); if (ShowHour) parts.Add($"{value.Hours:00}"); if (ShowMinute) parts.Add($"{value.Minutes:00}"); if (ShowSecond) parts.Add($"{value.Seconds:00}"); SetValue(DisplayTextPropertyKey, string.Join(':', parts)); } private void RebuildMinutes() => RebuildPartCollection(Minutes, MinuteIncrement, Minute, MinuteProperty); private void RebuildSeconds() => RebuildPartCollection(Seconds, SecondIncrement, Second, SecondProperty); private void RebuildPartCollection(ObservableCollection collection, int requestedIncrement, int selectedValue, DependencyProperty selectedProperty) { var increment = Math.Clamp(requestedIncrement, 1, 30); collection.Clear(); for (var value = 0; value < 60; value += increment) collection.Add(value); if (!collection.Contains(selectedValue)) { var nearest = collection.OrderBy(value => Math.Abs(value - selectedValue)).FirstOrDefault(); SetCurrentValue(selectedProperty, nearest); } } private void SelectNow() { var now = DateTime.Now; var minuteIncrement = Math.Clamp(MinuteIncrement, 1, 30); var secondIncrement = Math.Clamp(SecondIncrement, 1, 30); var hour = now.Hour; var minute = ShowMinute ? (int)Math.Round(now.Minute / (double)minuteIncrement) * minuteIncrement : now.Minute; var second = ShowSecond ? (int)Math.Round(now.Second / (double)secondIncrement) * secondIncrement : now.Second; if (second >= 60) { second = 0; minute++; } if (minute >= 60) { minute = 0; hour = (hour + 1) % 24; } SetCurrentValue(SelectedTimeProperty, new TimeSpan(hour, minute, second)); } private void ClearTime() { SetCurrentValue(SelectedTimeProperty, null); SetCurrentValue(IsDropDownOpenProperty, false); } }