65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (48投票s)

2010年11月11日

CPOL

6分钟阅读

viewsIcon

216917

downloadIcon

2998

演示如何修改DatePicker,使其可以使用键盘的上下键进行更智能的日期选择。

引言

这篇文章其实相当简单,说实话,我反复犹豫是否要将其作为一篇文章发布。我曾认为它或许可以作为一篇不错的博客文章,但最终,我认为作为一篇已发布的文章会使更多人受益,所以我决定将这篇非常简单的文章作为文章发布,而不是博客文章;抱歉它的篇幅不长。我知道这不符合我平时的写作风格,但希望各位能够接受;我只是觉得它很有用。

那么,随附的代码实际上做了什么?嗯,它很简单;它向您展示了如何修改标准的 .NET 4.0 WPF DatePicker 控件,允许用户在使用鼠标位于 DatePicker 文本的正确区域时,通过键盘的上下键来跳过日期/月份/年份,这是标准控件实际上并不具备的功能。它还会考虑当前可能应用的任何已禁用日期,并向您展示如何创建自定义 Calendar 样式,以便在其中放置一个“跳转到今天”按钮。

这就是我们试图要做的事情。正如我所说,非常简单,但出奇地有用,而且不是开箱即用的功能,有些人可能不知道如何做到这一点,所以我认为值得写一篇帖子。

外观

那么,这一切看起来是什么样的呢?毫不意外,没有什么特别的,它就是一个 DatePicker,还带有一个“跳转到今天”按钮。以下是它的全部荣耀。太棒了,不是吗(你们说不是,我说确实是代码)。

这是展开的 Calendar

WPFDatePickerEx/Open.png

请注意“跳转到今天”按钮。当点击“跳转到今天”按钮时,它会出乎意料地将 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 应用于我们的专用 DatePickerCalendar 如何知道这些?嗯,我们现在需要回溯一步,再次查看我们专用 DatePickerApplyTemplate() 方法。将附加 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 范围内。如果找到,它将使用找到的范围项来索引我们之前设置的 CalendarBlackOutDatesTextLookup 附加 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

就这些

如果您觉得这篇文章有用,请投个票/留言表示支持。

© . All rights reserved.