使用标准的WPF .NET 4.0 DatePicker控件的一些技巧






4.88/5 (48投票s)
演示如何修改DatePicker,使其可以使用键盘的上下键进行更智能的日期选择。
引言
这篇文章其实相当简单,说实话,我反复犹豫是否要将其作为一篇文章发布。我曾认为它或许可以作为一篇不错的博客文章,但最终,我认为作为一篇已发布的文章会使更多人受益,所以我决定将这篇非常简单的文章作为文章发布,而不是博客文章;抱歉它的篇幅不长。我知道这不符合我平时的写作风格,但希望各位能够接受;我只是觉得它很有用。
那么,随附的代码实际上做了什么?嗯,它很简单;它向您展示了如何修改标准的 .NET 4.0 WPF DatePicker
控件,允许用户在使用鼠标位于 DatePicker
文本的正确区域时,通过键盘的上下键来跳过日期/月份/年份,这是标准控件实际上并不具备的功能。它还会考虑当前可能应用的任何已禁用日期,并向您展示如何创建自定义 Calendar
样式,以便在其中放置一个“跳转到今天”按钮。
这就是我们试图要做的事情。正如我所说,非常简单,但出奇地有用,而且不是开箱即用的功能,有些人可能不知道如何做到这一点,所以我认为值得写一篇帖子。
外观
那么,这一切看起来是什么样的呢?毫不意外,没有什么特别的,它就是一个 DatePicker
,还带有一个“跳转到今天”按钮。以下是它的全部荣耀。太棒了,不是吗(你们说不是,我说确实是代码)。
这是展开的 Calendar
请注意“跳转到今天”按钮。当点击“跳转到今天”按钮时,它会出乎意料地将 Calendar
和因此 DatePicker
导航到今天的日期,除非今天的日期是已禁用日期之一,在这种情况下,“跳转到今天”按钮将不可用。
下面是另一个 Calendar
,但这次我展示的是添加了一些已禁用日期到 Calendar
的情况,您可以看到它们在下面的图片中被标记为小叉号。
下面的图片显示的内容需要您发挥一点想象力来弄清楚;基本上,如果您的鼠标悬停在日的部分,然后按下向上键,DatePicker
日期的日部分将增加一,除非它遇到已禁用日期,在这种情况下,它将前进到下一个可用的非禁用日期。当按下向下键时,也应用了相同的逻辑。对于日/月/年部分,它也是这样工作的。
它是如何工作的
那么,它是如何工作的呢?嗯,这很简单,可以分解为几个步骤
步骤 1:创建一个专门的 DatePicker
这一步再简单不过了;只需继承自 DatePicker
,如下所示
public class DatePickerEx : DatePicker
{
/// <summary>
/// Allows us to hook into when a new style is applied, so we can call ApplyTemplate()
/// at the correct time to get the things we need out of the Template
/// </summary>
public static readonly DependencyProperty AlternativeCalendarStyleProperty =
DependencyProperty.Register("AlternativeCalendarStyle",
typeof(Style), typeof(DatePickerEx),
new FrameworkPropertyMetadata((Style)null,
new PropertyChangedCallback(OnAlternativeCalendarStyleChanged)));
public Style AlternativeCalendarStyle
{
get { return (Style)GetValue(AlternativeCalendarStyleProperty); }
set { SetValue(AlternativeCalendarStyleProperty, value); }
}
private static void OnAlternativeCalendarStyleChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
DatePickerEx target = (DatePickerEx)d;
target.ApplyTemplate();
}
}
我们还希望我们的专用 DatePicker
能够接受 Calendar
的新样式,这就是为什么我们包含一个新的 DependencyProperty
来实现这一点,这反过来又允许我们在需要时调用 ApplyTemplate()
。这基本上是为了让我们在知道所有必需的 ControlTemplate
部分都可用时,控制何时调用 ApplyTemplate()
。
步骤 2:创建一个 Calendar 样式来替换默认的
由于我们想要一个“跳转到今天”按钮,我们只需在 XAML 中创建一个新的 Calendar
样式来支持这个新按钮。这是 Calendar
的完整新样式
<Style x:Key="calendarWithGotToTodayStyle"
TargetType="{x:Type Calendar}">
<Setter Property="Foreground"
Value="#FF333333" />
<Setter Property="Background"
Value="White" />
<Setter Property="BorderBrush"
Value="DarkGray" />
<Setter Property="BorderThickness"
Value="1" />
<Setter Property="CalendarDayButtonStyle"
Value="{StaticResource CalendarDayButtonStyleEx}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Calendar}">
<Border BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}">
<StackPanel Orientation="Vertical">
<StackPanel x:Name="PART_Root"
HorizontalAlignment="Center">
<CalendarItem x:Name="PART_CalendarItem"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
Style="{TemplateBinding CalendarItemStyle}" />
<Button x:Name="PART_GoToTodayButton"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="10"
Content="Go To Today" />
</StackPanel>
</StackPanel>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
我们将其应用到我们的新 DependencyProperty
,如下所示
<local:DatePickerEx
AlternativeCalendarStyle="{StaticResource calendarWithGotToTodayStyle}" />
步骤 3:编写一些代码来完成工作
现在我们所要做的就是编写一些代码来提取整个 DatePicker
ControlTemplate
的相关部分,以及我们上面看到的应用于 Calendar
的新样式。
我们如何从 DatePicker ControlTemplate
中获取我们想要的东西?嗯,这是通过以下方式完成的
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
textBox = this.GetTemplateChild("PART_TextBox") as DatePickerTextBox;
popup = this.GetTemplateChild("PART_Popup") as Popup;
if (AlternativeCalendarStyle != null)
{
System.Windows.Controls.Calendar calendar = popup.Child as System.Windows.Controls.Calendar;
calendar.Style = AlternativeCalendarStyle;
calendar.ApplyTemplate();
goToTodayButton = calendar.Template.FindName("PART_GoToTodayButton", calendar) as Button;
if (goToTodayButton != null)
{
gotoTodayCommand = new SimpleCommand(CanExecuteGoToTodayCommand, ExecuteGoToTodayCommand);
goToTodayButton.Command = gotoTodayCommand;
}
}
textBox.PreviewKeyDown -= new KeyEventHandler(DatePickerTextBox_PreviewKeyDown); //unhook
textBox.PreviewKeyDown += new KeyEventHandler(DatePickerTextBox_PreviewKeyDown); //hook
}
现在我们所要做的就是响应向上/向下键,并确保我们在更新日期时考虑到任何已禁用日期;这段代码如下
private void DatePickerTextBox_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Up || e.Key == Key.Down)
{
int direction = e.Key == Key.Up ? 1 : -1;
string currentDateText = Text;
DateTime result;
if (!DateTime.TryParse(Text, out result))
return;
char delimeter = ' ';
switch (this.SelectedDateFormat)
{
case DatePickerFormat.Short: // dd/mm/yyyy
delimeter = '/';
break;
case DatePickerFormat.Long: // day month year
delimeter = ' ';
break;
}
int index = 3;
bool foundIt = false;
for (int i = Text.Length - 1; i >= 0; i--)
{
if (Text[i] == delimeter)
{
--index;
if (textBox.CaretIndex > i)
{
foundIt = true;
break;
}
}
}
if (!foundIt)
index = 0;
switch (index)
{
case 0: // Day
result = result.AddDays(direction);
break;
case 1: // Month
result = result.AddMonths(direction);
break;
case 2: // Year
result = result.AddYears(direction);
break;
}
while (this.BlackoutDates.Contains(result))
{
result = result.AddDays(direction);
}
DateTimeFormatInfo dtfi = DateTimeHelper.GetCurrentDateFormat();
switch (this.SelectedDateFormat)
{
case DatePickerFormat.Short:
this.Text = string.Format(CultureInfo.CurrentCulture,
result.ToString(dtfi.ShortDatePattern, dtfi));
//this.Text = result.ToShortDateString();
break;
case DatePickerFormat.Long:
this.Text = string.Format(CultureInfo.CurrentCulture,
result.ToString(dtfi.LongDatePattern, dtfi));
//this.Text = result.ToLongDateString();
break;
}
switch (index)
{
case 1:
textBox.CaretIndex = Text.IndexOf(delimeter) + 1;
break;
case 2:
textBox.CaretIndex = Text.LastIndexOf(delimeter) + 1;
break;
}
}
}
额外内容:显示已禁用日期的工具提示
我还决定向您展示如何显示已禁用日期的工具提示。为此,需要几个步骤。
步骤 1:创建一个附加 DP 查找
第一步是创建一个附加 DP,该 DP 首先可以应用于 DatePicker
,然后该 DP 会将值传递给 DatePicker
所拥有的 Calendar
。这是我构思的附加 DP
public static class CalendarProps
{
#region BlackOutDatesTextLookup
/// <summary>
/// BlackOutDatesTextLookup : Stores dictionary to allow lookup of
/// Calendar.BlackoutDates to reason for blackout dates string.
/// </summary>
public static readonly DependencyProperty BlackOutDatesTextLookupProperty =
DependencyProperty.RegisterAttached("BlackOutDatesTextLookup",
typeof(Dictionary<CalendarDateRange, string>), typeof(CalendarProps),
new FrameworkPropertyMetadata(new Dictionary<CalendarDateRange, string>()));
public static Dictionary<CalendarDateRange,
string> GetBlackOutDatesTextLookup(DependencyObject d)
{
return (Dictionary<CalendarDateRange, string>)
d.GetValue(BlackOutDatesTextLookupProperty);
}
public static void SetBlackOutDatesTextLookup(DependencyObject d,
Dictionary<CalendarDateRange, string> value)
{
d.SetValue(BlackOutDatesTextLookupProperty, value);
}
#endregion
}
步骤 2:确保在添加已禁用日期时填充此附加 DP
这很容易做到。演示应用程序是这样做的
public MainWindow()
{
InitializeComponent();
AddBlackOutDates(mdp, 2);
}
private void AddBlackOutDates(DatePicker dp, int offset)
{
Dictionary<CalendarDateRange, string> blackoutDatesTextLookup =
new Dictionary<CalendarDateRange, string>();
for (int i = 0; i < offset; i++)
{
CalendarDateRange range = new CalendarDateRange(DateTime.Now.AddDays(i));
dp.BlackoutDates.Add(range);
blackoutDatesTextLookup.Add(range,
string.Format("This is a simulated BlackOut date {0}",
range.Start.ToLongDateString()));
}
dp.SetValue(CalendarProps.BlackOutDatesTextLookupProperty, blackoutDatesTextLookup);
}
请注意,我们如何将此附加 DP 应用于我们的专用 DatePicker
。Calendar
如何知道这些?嗯,我们现在需要回溯一步,再次查看我们专用 DatePicker
的 ApplyTemplate()
方法。将附加 DP 值从专用 DatePicker
传播到 Calendar
的部分如下所示
public override void OnApplyTemplate()
{
....
....
if (AlternativeCalendarStyle != null)
{
....
....
System.Windows.Controls.Calendar calendar =
popup.Child as System.Windows.Controls.Calendar;
calendar.SetValue(CalendarProps.BlackOutDatesTextLookupProperty,
this.GetValue(CalendarProps.BlackOutDatesTextLookupProperty));
....
....
}
....
....
}
步骤 3:确保 Calendar 具有专门的 CalendarDayButtonStyle
现在我们需要确保专用 DatePicker
的嵌入式 Calendar
具有特殊的 CalendarDayButtonStyle
样式。这是这样做的
<Style x:Key="calendarWithGotToTodayStyle"
TargetType="{x:Type Calendar}">
....
....
<Setter Property="CalendarDayButtonStyle"
Value="{StaticResource CalendarDayButtonStyleEx}" />
....
....
</Style>
其中 CalendarDayButtonStyle
样式最重要的部分如下
<!-- CalendarDayButton Style -->
<Style x:Key="CalendarDayButtonStyleEx"
TargetType="{x:Type CalendarDayButton}">
<Setter Property="MinWidth"
Value="5" />
<Setter Property="MinHeight"
Value="5" />
<Setter Property="FontSize"
Value="10" />
<Setter Property="HorizontalContentAlignment"
Value="Center" />
<Setter Property="VerticalContentAlignment"
Value="Center" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type CalendarDayButton}">
<Grid x:Name="CalendarDayButtonGrid">
<Grid.ToolTip>
<MultiBinding Converter="{local:HighlightDateConverter}">
<MultiBinding.Bindings>
<Binding />
<Binding RelativeSource="{RelativeSource FindAncestor,
AncestorType={x:Type Calendar}}" />
</MultiBinding.Bindings>
</MultiBinding>
</Grid.ToolTip>
........
........
........
........
........
<Rectangle x:Name="TodayBackground"
Fill="#FFAAAAAA"
Opacity="0"
RadiusY="1"
RadiusX="1" />
<Rectangle x:Name="AccentBackground"
RadiusX="1"
RadiusY="1"
IsHitTestVisible="False"
Fill="{Binding RelativeSource=
{RelativeSource AncestorType=Calendar},
Path=DateHighlightBrush}" />
<Rectangle x:Name="SelectedBackground"
Fill="#FFBADDE9"
Opacity="0"
RadiusY="1"
RadiusX="1" />
<Border BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}" />
<Rectangle x:Name="HighlightBackground"
Fill="#FFBADDE9"
Opacity="0"
RadiusY="1"
RadiusX="1" />
<ContentPresenter x:Name="NormalText"
TextElement.Foreground="#FF333333"
HorizontalAlignment=
"{TemplateBinding HorizontalContentAlignment}"
Margin="5,1,5,1"
VerticalAlignment=
"{TemplateBinding VerticalContentAlignment}" />
<Path x:Name="Blackout"
Data="M8.1772461,11.029181 L10.433105,
11.029181 L11.700684,12.801641
L12.973633,11.029181 L15.191895,
11.029181 L12.844727,13.999395
L15.21875,17.060919 L12.962891,
17.060919 L11.673828,15.256231
L10.352539,17.060919 L8.1396484,
17.060919 L10.519043,14.042364 z"
Fill="#FF000000"
HorizontalAlignment="Stretch"
Margin="3"
Opacity="0"
RenderTransformOrigin="0.5,0.5"
Stretch="Fill"
VerticalAlignment="Stretch" />
<Rectangle x:Name="DayButtonFocusVisual"
IsHitTestVisible="false"
RadiusY="1"
RadiusX="1"
Stroke="#FF45D6FA"
Visibility="Collapsed" />
</Grid>
<ControlTemplate.Triggers>
<!-- No ToolTip highlighting if IValueConverter returned null -->
<DataTrigger Value="{x:Null}">
<DataTrigger.Binding>
<MultiBinding Converter=
"{local:HighlightDateConverter}">
<MultiBinding.Bindings>
<Binding />
<Binding RelativeSource=
"{RelativeSource FindAncestor,
AncestorType={x:Type Calendar}}" />
</MultiBinding.Bindings>
</MultiBinding>
</DataTrigger.Binding>
<Setter TargetName="AccentBackground"
Property="Visibility"
Value="Hidden" />
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
这里要注意的主要事项是应用了一个工具提示(位于样式顶部附近),它通过一个 HighlightDateConverter IValueConverter
获取其值,如果 HighlightDateConverter IValueConverter
的值为“{x:Null}”(请参阅上面的 Triggers 部分),则没有工具提示可见。
步骤 4:创建工具提示
拼图的最后一块是创建工具提示。这是通过将 Day 按钮的当前日期和整个 Calendar
传递给 HighlightDateConverter IValueConverter
来实现的。其中 HighlightDateConverter IValueConverter
会检查 Day 按钮的当前日期是否在 Calendar.BlackOutDates
范围内。如果找到,它将使用找到的范围项来索引我们之前设置的 Calendar
的 BlackOutDatesTextLookup
附加 DP。
这是 HighlightDateConverter IValueConverter
的完整列表
public class HighlightDateConverter : MarkupExtension, IMultiValueConverter
{
#region MarkupExtension Overrides
private static HighlightDateConverter converter = null;
public override object ProvideValue(IServiceProvider serviceProvider)
{
if (converter == null)
{
converter = new HighlightDateConverter();
}
return converter;
}
#endregion
#region IMultiValueConverter Members
/// <summary>
/// Gets a tool tip for a date passed in. Could also return null
/// </summary>
/// The 'values' array parameter has the following elements:
///
/// - values[0] = Binding #1: The date to be looked up.
/// This should be set up as a pathless binding;
/// the Calendar control will provide the date.
///
/// - values[1] = Binding #2: A binding reference
/// to the Calendar control that is invoking this converter.
/// </remarks>
public object Convert(object[] values, Type targetType,
object parameter, System.Globalization.CultureInfo culture)
{
// Exit if values not set
if ((values[0] == null) || (values[1] == null)) return null;
// Get values passed in
DateTime targetDate = (DateTime)values[0];
Calendar calendar = (Calendar)values[1];
var range = calendar.BlackoutDates.Where(x => x.Start.IsSameDateAs(targetDate));
if (range.Count() > 0)
{
Dictionary<CalendarDateRange, string> blackOutDatesTextLookup =
(Dictionary<CalendarDateRange, string>)
calendar.GetValue(CalendarProps.BlackOutDatesTextLookupProperty);
return blackOutDatesTextLookup[range.First()];
}
else
return null;
}
/// <summary>
/// Not used.
/// </summary>
public object[] ConvertBack(object value, Type[] targetTypes,
object parameter, System.Globalization.CultureInfo culture)
{
return new object[0];
}
#endregion
}
特别感谢
关于工具提示部分,我的一些内容基于 David Veeneman 的优秀文章,该文章的 URL 为:ExtendingWPFCalendar.aspx。
就这些
如果您觉得这篇文章有用,请投个票/留言表示支持。