273 lines
9.2 KiB
C#
273 lines
9.2 KiB
C#
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;
|
||
|
||
/// <summary>
|
||
/// 自定义日期时间选择器。
|
||
/// 弹出层布局:左侧 <see cref="Calendar"/> 日历 + 右侧时/分/秒 三列 <see cref="ListBox"/>,
|
||
/// 底部「此刻 / 确定」按钮,用于替代 HandyControl 默认的「圆盘表盘 + 上午下午」风格 DateTimePicker。
|
||
/// </summary>
|
||
public partial class DateTimeListPicker : UserControl
|
||
{
|
||
public DateTimeListPicker()
|
||
{
|
||
InitializeComponent();
|
||
Loaded += (_, _) => SyncDisplay();
|
||
}
|
||
|
||
#region 公共依赖属性
|
||
|
||
/// <summary>选择的日期时间,双向绑定。</summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>占位符(空值时显示在 TextBox 上的提示文字)。</summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>显示格式,默认 yyyy-MM-dd HH:mm:ss。</summary>
|
||
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 内部依赖属性
|
||
|
||
/// <summary>弹出层是否打开。</summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>TextBox 当前显示的文字。用户可手工编辑,回车/失焦时尝试解析。</summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>弹出层内未确认的日期值。</summary>
|
||
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,且不会再有变更通知,导致弹出层右侧三列为空。
|
||
|
||
/// <summary>0-23 小时列表。</summary>
|
||
public List<int> Hours { get; } = Enumerable.Range(0, 24).ToList();
|
||
|
||
/// <summary>0-59 分钟列表。</summary>
|
||
public List<int> Minutes { get; } = Enumerable.Range(0, 60).ToList();
|
||
|
||
/// <summary>0-59 秒列表。</summary>
|
||
public List<int> 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);
|
||
}
|
||
}
|
||
|
||
/// <summary>将 <see cref="SelectedDateTime"/> 同步到 TextBox 显示文本。</summary>
|
||
private void SyncDisplay()
|
||
{
|
||
_suppressTextSync = true;
|
||
try
|
||
{
|
||
DisplayText = SelectedDateTime.HasValue
|
||
? SelectedDateTime.Value.ToString(DateTimeFormat, CultureInfo.InvariantCulture)
|
||
: string.Empty;
|
||
}
|
||
finally
|
||
{
|
||
_suppressTextSync = false;
|
||
}
|
||
}
|
||
|
||
/// <summary>弹出层打开时,把当前选中值(或当前时间)拷贝到 Pending 字段。</summary>
|
||
private void SyncPendingFromSelected()
|
||
{
|
||
var dt = SelectedDateTime ?? DateTime.Now;
|
||
PendingDate = dt.Date;
|
||
PendingHour = dt.Hour;
|
||
PendingMinute = dt.Minute;
|
||
PendingSecond = dt.Second;
|
||
}
|
||
|
||
/// <summary>将三列时间 ListBox 滚动到当前选中项。</summary>
|
||
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();
|
||
}
|
||
|
||
/// <summary>解析当前 TextBox 文本到 <see cref="SelectedDateTime"/>,失败则回滚显示。</summary>
|
||
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();
|
||
}
|
||
}
|
||
}
|