WPF 实现时间选择控件
控件名:TimePicker
作 者:WPFDevelopersOrg - 驚鏵
原文链接[1]:https://github.com/WPFDevelopersOrg/WPFDevelopers
码云链接[2]:https://gitee.com/WPFDevelopersOrg/WPFDevelopers
框架使用
.NET4 至 .NET6
;Visual Studio 2022
;
1)代码TimePicker.cs
如下:
TimePicker
控件依赖属性,SelectedTimeFormatProperty、MaxDropDownHeightProperty、SelectedTimeProperty
和IsCurrentTimeProperty
。在静态构造函数中,使用
DefaultStyleKeyProperty.OverrideMetadata
方法来指定控件的默认样式。控件的模板部分定义了三个重要的部件:
PART_TimeSelector
(列表框用于选择时间)、PART_EditableTextBox
(可编辑的文本框)和PART_Popup
(弹出窗口)。在应用模板时,我们获取并保存了这些模板部件的引用。并且订阅了时间选择器的
SelectedTimeChanged
事件,以及弹出窗口的Opened
事件。当时间选择器的
SelectedTimeChanged
事件发生时,我们会更新文本框中的文本,并将选中的时间赋值给SelectedTime
属性。当弹出窗口的
Opened
事件发生时,我们会调用时间选择器的SetTime
方法,以根据SelectedTime
的值更新列表框的选择项。
using System;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
namespace WPFDevelopers.Controls
{
[TemplatePart(Name = TimeSelectorTemplateName, Type = typeof(ListBox))]
[TemplatePart(Name = EditableTextBoxTemplateName, Type = typeof(TextBox))]
[TemplatePart(Name = PopupTemplateName, Type = typeof(Popup))]
public class TimePicker : Control
{
private const string TimeSelectorTemplateName = "PART_TimeSelector";
private const string EditableTextBoxTemplateName = "PART_EditableTextBox";
private const string PopupTemplateName = "PART_Popup";
public static readonly DependencyProperty SelectedTimeFormatProperty =
DependencyProperty.Register("SelectedTimeFormat", typeof(string), typeof(TimePicker),
new PropertyMetadata("HH:mm:ss"));
public static readonly DependencyProperty MaxDropDownHeightProperty =
DependencyProperty.Register("MaxDropDownHeight", typeof(double), typeof(TimePicker),
new UIPropertyMetadata(SystemParameters.PrimaryScreenHeight / 3.0, OnMaxDropDownHeightChanged));
public static readonly DependencyProperty SelectedTimeProperty =
DependencyProperty.Register("SelectedTime", typeof(DateTime?), typeof(TimePicker),
new PropertyMetadata(null, OnSelectedTimeChanged));
public static readonly DependencyProperty IsCurrentTimeProperty =
DependencyProperty.Register("IsCurrentTime", typeof(bool), typeof(TimePicker), new PropertyMetadata(false));
private DateTime _date;
private Popup _popup;
private TextBox _textBox;
private TimeSelector _timeSelector;
static TimePicker()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(TimePicker),
new FrameworkPropertyMetadata(typeof(TimePicker)));
}
public string SelectedTimeFormat
{
get => (string) GetValue(SelectedTimeFormatProperty);
set => SetValue(SelectedTimeFormatProperty, value);
}
public DateTime? SelectedTime
{
get => (DateTime?) GetValue(SelectedTimeProperty);
set => SetValue(SelectedTimeProperty, value);
}
public bool IsCurrentTime
{
get => (bool) GetValue(IsCurrentTimeProperty);
set => SetValue(IsCurrentTimeProperty, value);
}
private static void OnMaxDropDownHeightChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var ctrl = d as TimePicker;
if (ctrl != null)
ctrl.OnMaxDropDownHeightChanged((double) e.OldValue, (double) e.NewValue);
}
protected virtual void OnMaxDropDownHeightChanged(double oldValue, double newValue)
{
}
private static void OnSelectedTimeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var ctrl = d as TimePicker;
if (ctrl != null && e.NewValue != null)
{
var dateTime = (DateTime) e.NewValue;
if (ctrl._timeSelector != null && dateTime > DateTime.MinValue)
ctrl._timeSelector.SelectedTime = dateTime;
else
ctrl._date = dateTime;
}
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_textBox = GetTemplateChild(EditableTextBoxTemplateName) as TextBox;
_timeSelector = GetTemplateChild(TimeSelectorTemplateName) as TimeSelector;
if (_timeSelector != null)
{
_timeSelector.SelectedTimeChanged -= TimeSelector_SelectedTimeChanged;
_timeSelector.SelectedTimeChanged += TimeSelector_SelectedTimeChanged;
if (!SelectedTime.HasValue && IsCurrentTime)
{
SelectedTime = DateTime.Now;
}
else
{
SelectedTime = null;
SelectedTime = _date;
}
}
_popup = GetTemplateChild(PopupTemplateName) as Popup;
if (_popup != null)
{
_popup.Opened -= Popup_Opened;
_popup.Opened += Popup_Opened;
}
}
private void Popup_Opened(object sender, EventArgs e)
{
if (_timeSelector != null)
{
_timeSelector.SetTime();
}
}
private void TimeSelector_SelectedTimeChanged(object sender, RoutedPropertyChangedEventArgs<DateTime?> e)
{
if (_textBox != null && e.NewValue != null)
{
_textBox.Text = e.NewValue.Value.ToString(SelectedTimeFormat);
SelectedTime = e.NewValue;
}
}
}
}
2)TimeSelector.cs
代码如下:
三个列表框,分别用于选择小时、分钟和秒。
代码中定义了一些依赖属性,例如
SelectedTimeProperty
用于获取或设置选中的时间,ItemHeightProperty
用于设置列表项的高度,SelectorMarginProperty
用于设置选择器的边距。还定义了一个SelectedTimeChangedEvent
事件,用于在选中时间发生变化时触发。控件的模板部分包含了三个列表框,分别命名为
PART_ListBoxHour、PART_ListBoxMinute
和PART_ListBoxSecond
。在应用模板时,会将数据源绑定到列表框上,并为每个列表框添加SelectionChanged
事件的处理程序。在列表框的
SelectionChanged
事件处理程序中,会根据选择的项更新_hour、_minute
和_second
等字段,并通过SetSelectedTime
方法设置SelectedTime
属性的值。最后在
SetTime
方法中会根据SelectedTim
e的值来更新列表框的选择项,并调用SetSelectedTime
方法设置SelectedTime
属性的值。小时集合设置为
_listBoxHour
的ItemsSource
。这个集合由上下四个空字符串、从0到23的小时数(格式为两位数)。
""
""
""
"00"
"01"
"02"
...
"21"
"22"
"23"
""
""
""
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
namespace WPFDevelopers.Controls
{
[TemplatePart(Name = ListBoxHourTemplateName, Type = typeof(ListBox))]
[TemplatePart(Name = ListBoxMinuteTemplateName, Type = typeof(ListBox))]
[TemplatePart(Name = ListBoxSecondTemplateName, Type = typeof(ListBox))]
public class TimeSelector : Control
{
private const string ListBoxHourTemplateName = "PART_ListBoxHour";
private const string ListBoxMinuteTemplateName = "PART_ListBoxMinute";
private const string ListBoxSecondTemplateName = "PART_ListBoxSecond";
public static readonly RoutedEvent SelectedTimeChangedEvent =
EventManager.RegisterRoutedEvent("SelectedTimeChanged", RoutingStrategy.Bubble,
typeof(RoutedPropertyChangedEventHandler<DateTime?>), typeof(TimeSelector));
public static readonly DependencyProperty SelectedTimeProperty =
DependencyProperty.Register("SelectedTime", typeof(DateTime?), typeof(TimeSelector),
new PropertyMetadata(null, OnSelectedTimeChanged));
public static readonly DependencyProperty ItemHeightProperty =
DependencyProperty.Register("ItemHeight", typeof(double), typeof(TimeSelector), new PropertyMetadata(0d));
public static readonly DependencyProperty SelectorMarginProperty =
DependencyProperty.Register("SelectorMargin", typeof(Thickness), typeof(TimeSelector),
new PropertyMetadata(new Thickness(0)));
private int _hour, _minute, _second;
private ListBox _listBoxHour, _listBoxMinute, _listBoxSecond;
static TimeSelector()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(TimeSelector),
new FrameworkPropertyMetadata(typeof(TimeSelector)));
}
public DateTime? SelectedTime
{
get => (DateTime?)GetValue(SelectedTimeProperty);
set => SetValue(SelectedTimeProperty, value);
}
public double ItemHeight
{
get => (double)GetValue(ItemHeightProperty);
set => SetValue(ItemHeightProperty, value);
}
public Thickness SelectorMargin
{
get => (Thickness)GetValue(SelectorMarginProperty);
set => SetValue(SelectorMarginProperty, value);
}
public event RoutedPropertyChangedEventHandler<DateTime?> SelectedTimeChanged
{
add => AddHandler(SelectedTimeChangedEvent, value);
remove => RemoveHandler(SelectedTimeChangedEvent, value);
}
public virtual void OnSelectedTimeChanged(DateTime? oldValue, DateTime? newValue)
{
var args = new RoutedPropertyChangedEventArgs<DateTime?>(oldValue, newValue, SelectedTimeChangedEvent);
RaiseEvent(args);
}
private static void OnSelectedTimeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var ctrl = d as TimeSelector;
if (ctrl != null)
ctrl.OnSelectedTimeChanged((DateTime?)e.OldValue, (DateTime?)e.NewValue);
}
private double GetItemHeight(ListBox listBox)
{
if (listBox.Items.Count > 0)
{
var listBoxItem = listBox.ItemContainerGenerator.ContainerFromIndex(0) as ListBoxItem;
if (listBoxItem != null) return listBoxItem.ActualHeight;
}
return 0;
}
private int GetFirstNonEmptyItemIndex(ListBox listBox)
{
for (var i = 0; i < listBox.Items.Count; i++)
{
var item = listBox.Items[i] as string;
if (!string.IsNullOrWhiteSpace(item))
return i;
}
return -1;
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
var minuteSecondList = Enumerable.Range(0, 60).Select(num => num.ToString("D2"));
var emptyData = Enumerable.Repeat(string.Empty, 4);
var result = emptyData.Concat(minuteSecondList).Concat(emptyData);
_listBoxHour = GetTemplateChild(ListBoxHourTemplateName) as ListBox;
if (_listBoxHour != null)
{
var hours = Enumerable.Range(0, 24).Select(num => num.ToString("D2"));
_listBoxHour.SelectionChanged -= ListBoxHour_SelectionChanged;
_listBoxHour.SelectionChanged += ListBoxHour_SelectionChanged;
_listBoxHour.ItemsSource = emptyData.Concat(hours).Concat(emptyData);
_listBoxHour.Loaded += (sender, args) =>
{
var h = GetItemHeight(_listBoxHour);
if (h <= 0) return;
ItemHeight = h;
Height = h * 10;
var YAxis = GetFirstNonEmptyItemIndex(_listBoxHour) * h;
SelectorMargin = new Thickness(0, YAxis, 0, 0);
};
}
_listBoxMinute = GetTemplateChild(ListBoxMinuteTemplateName) as ListBox;
if (_listBoxMinute != null)
{
_listBoxMinute.SelectionChanged -= ListBoxMinute_SelectionChanged;
_listBoxMinute.SelectionChanged += ListBoxMinute_SelectionChanged;
_listBoxMinute.ItemsSource = result;
}
_listBoxSecond = GetTemplateChild(ListBoxSecondTemplateName) as ListBox;
if (_listBoxSecond != null)
{
_listBoxSecond.SelectionChanged -= ListBoxSecond_SelectionChanged;
_listBoxSecond.SelectionChanged += ListBoxSecond_SelectionChanged;
_listBoxSecond.ItemsSource = result;
}
}
private void ListBoxSecond_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (string.IsNullOrWhiteSpace(_listBoxSecond.SelectedValue.ToString())) return;
_second = Convert.ToInt32(_listBoxSecond.SelectedValue.ToString());
SetSelectedTime();
}
private void ListBoxMinute_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (string.IsNullOrWhiteSpace(_listBoxMinute.SelectedValue.ToString())) return;
_minute = Convert.ToInt32(_listBoxMinute.SelectedValue.ToString());
SetSelectedTime();
}
private void ListBoxHour_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (string.IsNullOrWhiteSpace(_listBoxHour.SelectedValue.ToString())) return;
_hour = Convert.ToInt32(_listBoxHour.SelectedValue.ToString());
SetSelectedTime();
}
private void SetSelectedTime()
{
var dt = new DateTime(DateTime.Today.Year, DateTime.Today.Month, DateTime.Today.Day, _hour, _minute,
_second);
SelectedTime = dt;
}
public void SetTime()
{
if (!SelectedTime.HasValue)
return;
_hour = SelectedTime.Value.Hour;
_minute = SelectedTime.Value.Minute;
_second = SelectedTime.Value.Second;
_listBoxHour.SelectionChanged -= ListBoxHour_SelectionChanged;
var hour = _hour.ToString("D2");
_listBoxHour.SelectedItem = hour;
//(_listBoxHour as TimeSelectorListBox).Positioning();
_listBoxHour.SelectionChanged += ListBoxHour_SelectionChanged;
_listBoxMinute.SelectionChanged -= ListBoxMinute_SelectionChanged;
var minute = _minute.ToString("D2");
_listBoxMinute.SelectedItem = minute;
//(_listBoxMinute as TimeSelectorListBox).Positioning();
_listBoxMinute.SelectionChanged += ListBoxMinute_SelectionChanged;
_listBoxSecond.SelectionChanged -= ListBoxSecond_SelectionChanged;
var second = _second.ToString("D2");
_listBoxSecond.SelectedItem = second;
//(_listBoxSecond as TimeSelectorListBox).Positioning();
_listBoxSecond.SelectionChanged += ListBoxSecond_SelectionChanged;
SetSelectedTime();
}
}
}
3)代码TimeSelectorListBox.cs
如下:
TimeSelectorListBox
是一个自定义的ListBox
控件,继承自ListBox
类。scrollViewer
是一个ScrollViewer
对象,用于滚动 ListBox 中的项。lastIndex
用于存储上次滚动时ListBox
中选中项的索引位置。isFirst
用于判断是否为第一次滚动。IsItemItsOwnContainerOverride
方法重写了ListBox
的方法,用于指定 ListBox 的项是否为指定的容器类型(TimeSelectorItem
)。GetContainerForItemOverride
方法重写了ListBox
的方法,用于创建 ListBox 的项所使用的容器(TimeSelectorItem
)。构造函数
TimeSelectorListBox
对控件进行初始化,设置Loaded
和PreviewMouseWheel
事件的处理一次滚动一条内容。TimeSelectorListBox_Loaded
方法用于在控件加载完成后执行一些初始化操作,如获取ScrollViewer
对象并监听其ScrollChanged
事件。ScrollListBox_PreviewMouseWheel
方法在鼠标滚轮滚动时处理滚动逻辑,根据滚动方向和选择项的位置来调整ListBox
的选中项,并防止滚动穿透。ScrollViewer_ScrollChanged
方法在ScrollViewer
的滚动位置发生变化时更新lastIndex
的值。OnSelectionChanged
方法重写了ListBox
的方法,在选中项发生改变时执行自定义操作,通过计算选中项与上次滚动位置之间的索引差来调整ScrollViewer
的滚动位置。FindVisualChild
方法用递归的方式在Visual
树中查找指定类型的子元素,并返回找到的第一个匹配项。
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using WPFDevelopers.Helpers;
namespace WPFDevelopers.Controls
{
public class TimeSelectorListBox : ListBox
{
private bool isFirst = true;
private double lastIndex = 4;
private ScrollViewer scrollViewer;
public TimeSelectorListBox()
{
Loaded += TimeSelectorListBox_Loaded;
PreviewMouseWheel -= ScrollListBox_PreviewMouseWheel;
PreviewMouseWheel += ScrollListBox_PreviewMouseWheel;
}
protected override bool IsItemItsOwnContainerOverride(object item)
{
return item is TimeSelectorItem;
}
protected override DependencyObject GetContainerForItemOverride()
{
return new TimeSelectorItem();
}
private void ScrollListBox_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
if (Items != null && Items.Count > 0)
{
var delta = e.Delta;
var scrollCount = delta > 0 ? -1 : 1;
ItemPositioning(scrollCount);
e.Handled = true;
}
}
void ItemPositioning(int scrollCount)
{
var itemCount = Items.Count;
var newIndex = SelectedIndex + scrollCount;
if (newIndex < 4)
newIndex = 5;
else if (newIndex >= itemCount - 4)
newIndex = itemCount;
SelectedIndex = newIndex;
}
void Positioning()
{
if (SelectedIndex <= 0 || scrollViewer == null) return;
var index = SelectedIndex - (int)lastIndex;
var offset = scrollViewer.VerticalOffset + index;
scrollViewer.ScrollToVerticalOffset(offset);
}
private void TimeSelectorListBox_Loaded(object sender, RoutedEventArgs e)
{
scrollViewer = ControlsHelper.FindVisualChild<ScrollViewer>(this);
if (scrollViewer != null)
{
scrollViewer.ScrollChanged -= ScrollViewer_ScrollChanged;
scrollViewer.ScrollChanged += ScrollViewer_ScrollChanged;
}
}
private void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
var offset = e.VerticalOffset;
if (isFirst == false)
lastIndex = offset + 4;
else
{
lastIndex = offset == 0 ? 4 : offset + 4;
isFirst = false;
}
}
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
if (SelectedIndex != -1 && lastIndex != -1)
{
if (SelectedIndex <= 0) return;
Positioning();
}
}
}
}
4)代码TimePicker.xaml
如下:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:WPFDevelopers.Controls"
xmlns:helpers="clr-namespace:WPFDevelopers.Helpers">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Basic/ControlBasic.xaml" />
</ResourceDictionary.MergedDictionaries>
<ControlTemplate x:Key="WD.TimePickerToggleButton" TargetType="{x:Type ToggleButton}">
<Border
x:Name="PART_Border"
Padding="6,0"
Background="Transparent"
BorderThickness="0"
SnapsToDevicePixels="true">
<controls:PathIcon
x:Name="PART_PathIcon"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{DynamicResource WD.PlaceholderTextSolidColorBrush}"
IsHitTestVisible="False"
Kind="Time" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="PART_PathIcon" Property="Foreground" Value="{DynamicResource WD.PrimaryNormalSolidColorBrush}" />
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="PART_PathIcon" Property="Foreground" Value="{DynamicResource WD.PrimaryNormalSolidColorBrush}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<Style
x:Key="WD.TimeSelectorItem"
BasedOn="{StaticResource WD.DefaultListBoxItem}"
TargetType="{x:Type controls:TimeSelectorItem}">
<Setter Property="BorderThickness" Value="0" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type controls:TimeSelectorItem}">
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="Background" Value="Transparent" />
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{DynamicResource WD.BaseSolidColorBrush}" />
</Trigger>
<DataTrigger Binding="{Binding}" Value="">
<Setter Property="IsEnabled" Value="False" />
</DataTrigger>
</ControlTemplate.Triggers>
<controls:SmallPanel>
<Border
Name="PART_Border"
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
SnapsToDevicePixels="True">
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
</Border>
</controls:SmallPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style
x:Key="WD.TimeListStyle"
BasedOn="{StaticResource WD.DefaultListBox}"
TargetType="{x:Type controls:TimeSelectorListBox}">
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Hidden" />
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Hidden" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="VirtualizingStackPanel.IsVirtualizing" Value="False" />
<Setter Property="ItemContainerStyle" Value="{StaticResource WD.TimeSelectorItem}" />
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto" />
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" />
</Trigger>
</Style.Triggers>
</Style>
<Style
x:Key="WD.TimeSelector"
BasedOn="{StaticResource WD.ControlBasicStyle}"
TargetType="{x:Type controls:TimeSelector}">
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Padding" Value="{StaticResource WD.DefaultPadding}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type controls:TimeSelector}">
<Border
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"
BorderThickness="{TemplateBinding BorderThickness}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
UseLayoutRounding="{TemplateBinding UseLayoutRounding}">
<controls:SmallPanel SnapsToDevicePixels="True">
<UniformGrid Rows="1">
<controls:TimeSelectorListBox x:Name="PART_ListBoxHour" Style="{StaticResource WD.TimeListStyle}" />
<controls:TimeSelectorListBox x:Name="PART_ListBoxMinute" Style="{StaticResource WD.TimeListStyle}" />
<controls:TimeSelectorListBox x:Name="PART_ListBoxSecond" Style="{StaticResource WD.TimeListStyle}" />
</UniformGrid>
<Line />
<Path />
<Border
Height="{TemplateBinding ItemHeight}"
Margin="{TemplateBinding SelectorMargin}"
VerticalAlignment="Top"
BorderBrush="{DynamicResource WD.BaseSolidColorBrush}"
BorderThickness="0,1"
IsHitTestVisible="False" />
</controls:SmallPanel>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style
x:Key="WD.TimePicker"
BasedOn="{StaticResource WD.ControlBasicStyle}"
TargetType="{x:Type controls:TimePicker}">
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="BorderBrush" Value="{DynamicResource WD.BaseSolidColorBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Background" Value="{DynamicResource WD.BackgroundSolidColorBrush}" />
<Setter Property="Padding" Value="{StaticResource WD.DefaultPadding}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type controls:TimePicker}">
<ControlTemplate.Resources>
<Storyboard x:Key="OpenStoryboard">
<DoubleAnimation
EasingFunction="{StaticResource WD.ExponentialEaseOut}"
Storyboard.TargetName="PART_DropDown"
Storyboard.TargetProperty="(Grid.RenderTransform).(ScaleTransform.ScaleY)"
To="1"
Duration="00:00:.2" />
</Storyboard>
<Storyboard x:Key="CloseStoryboard">
<DoubleAnimation
EasingFunction="{StaticResource WD.ExponentialEaseOut}"
Storyboard.TargetName="PART_DropDown"
Storyboard.TargetProperty="(Grid.RenderTransform).(ScaleTransform.ScaleY)"
To="0"
Duration="00:00:.2" />
</Storyboard>
</ControlTemplate.Resources>
<controls:SmallPanel SnapsToDevicePixels="True">
<Grid Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Border
Name="PART_Border"
Grid.ColumnSpan="2"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{Binding Path=(helpers:ElementHelper.CornerRadius), RelativeSource={RelativeSource TemplatedParent}}"
SnapsToDevicePixels="True" />
<TextBox
x:Name="PART_EditableTextBox"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Background="{TemplateBinding Background}"
Focusable="True"
Foreground="{DynamicResource WD.PrimaryTextSolidColorBrush}"
SelectionBrush="{DynamicResource WD.WindowBorderBrushSolidColorBrush}"
Style="{x:Null}"
Template="{StaticResource WD.ComboBoxTextBox}" />
<TextBlock
x:Name="PART_Watermark"
Margin="{TemplateBinding Padding}"
Padding="1,0"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Background="Transparent"
FontSize="{StaticResource WD.NormalFontSize}"
Foreground="{DynamicResource WD.RegularTextSolidColorBrush}"
IsHitTestVisible="False"
Text="{Binding Path=(helpers:ElementHelper.Watermark), RelativeSource={RelativeSource TemplatedParent}}"
TextTrimming="CharacterEllipsis"
Visibility="Collapsed" />
<ToggleButton
x:Name="PART_ToggleButton"
Grid.Column="1"
Background="{TemplateBinding Background}"
ClickMode="Release"
Focusable="False"
Style="{x:Null}"
Template="{StaticResource WD.TimePickerToggleButton}" />
<Popup
x:Name="PART_Popup"
AllowsTransparency="True"
IsOpen="{Binding Path=IsChecked, ElementName=PART_ToggleButton}"
Placement="Bottom"
PlacementTarget="{Binding ElementName=PART_Border}"
StaysOpen="False">
<controls:SmallPanel
x:Name="PART_DropDown"
MinWidth="{TemplateBinding FrameworkElement.ActualWidth}"
MaxHeight="{TemplateBinding MaxDropDownHeight}"
Margin="24,2,24,24"
RenderTransformOrigin=".5,0"
SnapsToDevicePixels="True">
<controls:SmallPanel.RenderTransform>
<ScaleTransform ScaleY="0" />
</controls:SmallPanel.RenderTransform>
<Border
Name="PART_DropDownBorder"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{Binding Path=(helpers:ElementHelper.CornerRadius), RelativeSource={RelativeSource TemplatedParent}}"
Effect="{StaticResource WD.PopupShadowDepth}"
SnapsToDevicePixels="True"
UseLayoutRounding="True" />
<controls:TimeSelector x:Name="PART_TimeSelector" />
</controls:SmallPanel>
</Popup>
</Grid>
</controls:SmallPanel>
<ControlTemplate.Triggers>
<Trigger SourceName="PART_ToggleButton" Property="IsChecked" Value="True">
<Trigger.EnterActions>
<BeginStoryboard x:Name="BeginStoryboardOpenStoryboard" Storyboard="{StaticResource OpenStoryboard}" />
</Trigger.EnterActions>
<Trigger.ExitActions>
<StopStoryboard BeginStoryboardName="BeginStoryboardOpenStoryboard" />
</Trigger.ExitActions>
</Trigger>
<Trigger SourceName="PART_ToggleButton" Property="IsChecked" Value="False">
<Trigger.EnterActions>
<BeginStoryboard x:Name="BeginStoryboardCloseStoryboard" Storyboard="{StaticResource CloseStoryboard}" />
</Trigger.EnterActions>
<Trigger.ExitActions>
<StopStoryboard BeginStoryboardName="BeginStoryboardCloseStoryboard" />
</Trigger.ExitActions>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="PART_Border" Property="BorderBrush" Value="{DynamicResource WD.PrimaryNormalSolidColorBrush}" />
</Trigger>
<Trigger SourceName="PART_Popup" Property="AllowsTransparency" Value="True">
<Setter TargetName="PART_DropDownBorder" Property="Margin" Value="0,2,0,0" />
</Trigger>
<Trigger Property="SelectedTime" Value="">
<Setter TargetName="PART_Watermark" Property="Visibility" Value="Visible" />
</Trigger>
<Trigger Property="SelectedTime" Value="{x:Null}">
<Setter TargetName="PART_Watermark" Property="Visibility" Value="Visible" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style BasedOn="{StaticResource WD.TimeSelector}" TargetType="{x:Type controls:TimeSelector}" />
<Style BasedOn="{StaticResource WD.TimePicker}" TargetType="{x:Type controls:TimePicker}" />
</ResourceDictionary>
5)示例代码TimePickerExample.xaml
如下:
<UniformGrid>
<wd:TimePicker
Width="200"
VerticalAlignment="Center"
wd:ElementHelper.Watermark="请选择任意时间" />
<wd:TimePicker
Width="200"
VerticalAlignment="Center"
IsCurrentTime="True" />
<wd:TimePicker
Width="200"
VerticalAlignment="Center"
SelectedTime="2023-05-06 23:59:59" />
</UniformGrid>
![2c6fce7ba24882dd17582badde7f0e0c.gif](https://img-blog.csdnimg.cn/img_convert/2c6fce7ba24882dd17582badde7f0e0c.gif)
参考资料
[1]
原文链接: https://github.com/WPFDevelopersOrg/WPFDevelopers
[2]码云链接: https://gitee.com/WPFDevelopersOrg/WPFDevelopers