using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Input; namespace YY.Admin.Controls; /// /// 自定义日期时间选择器。 /// 弹出层布局:左侧 日历 + 右侧时/分/秒 三列 , /// 底部「此刻 / 确定」按钮,用于替代 HandyControl 默认的「圆盘表盘 + 上午下午」风格 DateTimePicker。 /// public partial class DateTimeListPicker : UserControl { public DateTimeListPicker() { InitializeComponent(); Loaded += (_, _) => SyncDisplay(); } #region 公共依赖属性 /// 选择的日期时间,双向绑定。 public static readonly DependencyProperty SelectedDateTimeProperty = DependencyProperty.Register( nameof(SelectedDateTime), typeof(DateTime?), typeof(DateTimeListPicker), new FrameworkPropertyMetadata( null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedDateTimeChanged)); public DateTime? SelectedDateTime { get => (DateTime?)GetValue(SelectedDateTimeProperty); set => SetValue(SelectedDateTimeProperty, value); } /// 占位符(空值时显示在 TextBox 上的提示文字)。 public static readonly DependencyProperty PlaceholderProperty = DependencyProperty.Register( nameof(Placeholder), typeof(string), typeof(DateTimeListPicker), new PropertyMetadata("请选择日期时间")); public string Placeholder { get => (string)GetValue(PlaceholderProperty); set => SetValue(PlaceholderProperty, value); } /// 显示格式,默认 yyyy-MM-dd HH:mm:ss。 public static readonly DependencyProperty DateTimeFormatProperty = DependencyProperty.Register( nameof(DateTimeFormat), typeof(string), typeof(DateTimeListPicker), new PropertyMetadata("yyyy-MM-dd HH:mm:ss", OnDateTimeFormatChanged)); public string DateTimeFormat { get => (string)GetValue(DateTimeFormatProperty); set => SetValue(DateTimeFormatProperty, value); } #endregion #region 内部依赖属性 /// 弹出层是否打开。 public static readonly DependencyProperty IsDropDownOpenProperty = DependencyProperty.Register( nameof(IsDropDownOpen), typeof(bool), typeof(DateTimeListPicker), new PropertyMetadata(false, OnIsDropDownOpenChanged)); public bool IsDropDownOpen { get => (bool)GetValue(IsDropDownOpenProperty); set => SetValue(IsDropDownOpenProperty, value); } /// TextBox 当前显示的文字。用户可手工编辑,回车/失焦时尝试解析。 public static readonly DependencyProperty DisplayTextProperty = DependencyProperty.Register( nameof(DisplayText), typeof(string), typeof(DateTimeListPicker), new PropertyMetadata(string.Empty)); public string DisplayText { get => (string)GetValue(DisplayTextProperty); set => SetValue(DisplayTextProperty, value); } /// 弹出层内未确认的日期值。 public static readonly DependencyProperty PendingDateProperty = DependencyProperty.Register( nameof(PendingDate), typeof(DateTime?), typeof(DateTimeListPicker), new PropertyMetadata(null)); public DateTime? PendingDate { get => (DateTime?)GetValue(PendingDateProperty); set => SetValue(PendingDateProperty, value); } public static readonly DependencyProperty PendingHourProperty = DependencyProperty.Register(nameof(PendingHour), typeof(int), typeof(DateTimeListPicker), new PropertyMetadata(0)); public int PendingHour { get => (int)GetValue(PendingHourProperty); set => SetValue(PendingHourProperty, value); } public static readonly DependencyProperty PendingMinuteProperty = DependencyProperty.Register(nameof(PendingMinute), typeof(int), typeof(DateTimeListPicker), new PropertyMetadata(0)); public int PendingMinute { get => (int)GetValue(PendingMinuteProperty); set => SetValue(PendingMinuteProperty, value); } public static readonly DependencyProperty PendingSecondProperty = DependencyProperty.Register(nameof(PendingSecond), typeof(int), typeof(DateTimeListPicker), new PropertyMetadata(0)); public int PendingSecond { get => (int)GetValue(PendingSecondProperty); set => SetValue(PendingSecondProperty, value); } #endregion // 注意:这三个集合必须用字段初始化器赋值,而不是放到构造函数体内。 // 原因:XAML 中通过 {Binding Hours, ElementName=Root} 绑定到普通 CLR 只读属性, // 绑定在 InitializeComponent() 执行时就建立。若赋值滞后到构造函数体内, // 绑定第一次求值时 Hours 仍为 null,且不会再有变更通知,导致弹出层右侧三列为空。 /// 0-23 小时列表。 public List Hours { get; } = Enumerable.Range(0, 24).ToList(); /// 0-59 分钟列表。 public List Minutes { get; } = Enumerable.Range(0, 60).ToList(); /// 0-59 秒列表。 public List Seconds { get; } = Enumerable.Range(0, 60).ToList(); private bool _suppressTextSync; private static void OnSelectedDateTimeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((DateTimeListPicker)d).SyncDisplay(); } private static void OnDateTimeFormatChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((DateTimeListPicker)d).SyncDisplay(); } private static void OnIsDropDownOpenChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var picker = (DateTimeListPicker)d; if ((bool)e.NewValue) { picker.SyncPendingFromSelected(); picker.Dispatcher.BeginInvoke(new Action(picker.ScrollListBoxesToSelection), System.Windows.Threading.DispatcherPriority.Loaded); } } /// 同步到 TextBox 显示文本。 private void SyncDisplay() { _suppressTextSync = true; try { DisplayText = SelectedDateTime.HasValue ? SelectedDateTime.Value.ToString(DateTimeFormat, CultureInfo.InvariantCulture) : string.Empty; } finally { _suppressTextSync = false; } } /// 弹出层打开时,把当前选中值(或当前时间)拷贝到 Pending 字段。 private void SyncPendingFromSelected() { var dt = SelectedDateTime ?? DateTime.Now; PendingDate = dt.Date; PendingHour = dt.Hour; PendingMinute = dt.Minute; PendingSecond = dt.Second; } /// 将三列时间 ListBox 滚动到当前选中项。 private void ScrollListBoxesToSelection() { PART_HourList?.ScrollIntoView(PendingHour); PART_MinuteList?.ScrollIntoView(PendingMinute); PART_SecondList?.ScrollIntoView(PendingSecond); } private void OnNowClick(object sender, RoutedEventArgs e) { var now = DateTime.Now; PendingDate = now.Date; PendingHour = now.Hour; PendingMinute = now.Minute; PendingSecond = now.Second; ScrollListBoxesToSelection(); } private void OnConfirmClick(object sender, RoutedEventArgs e) { var date = PendingDate ?? DateTime.Today; SelectedDateTime = new DateTime( date.Year, date.Month, date.Day, ClampTime(PendingHour, 23), ClampTime(PendingMinute, 59), ClampTime(PendingSecond, 59)); IsDropDownOpen = false; } private static int ClampTime(int value, int max) { if (value < 0) return 0; if (value > max) return max; return value; } private void OnTextBoxKeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.Enter) { TryParseDisplayText(); e.Handled = true; } } private void OnTextBoxLostFocus(object sender, RoutedEventArgs e) { TryParseDisplayText(); } /// 解析当前 TextBox 文本到 ,失败则回滚显示。 private void TryParseDisplayText() { if (_suppressTextSync) return; if (string.IsNullOrWhiteSpace(DisplayText)) { SelectedDateTime = null; return; } if (DateTime.TryParseExact(DisplayText, DateTimeFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out var exact)) { SelectedDateTime = exact; } else if (DateTime.TryParse(DisplayText, CultureInfo.CurrentCulture, DateTimeStyles.None, out var fallback)) { SelectedDateTime = fallback; } else { // 输入非法,恢复为原值 SyncDisplay(); } } }