WPF 工具包 DatePicker 仅限月/年

Posted

技术标签:

【中文标题】WPF 工具包 DatePicker 仅限月/年【英文标题】:WPF Toolkit DatePicker Month/Year Only 【发布时间】:2010-12-20 09:20:24 【问题描述】:

我正在使用上面的 Toolkit 的 Datepicker,但我想将其限制为仅选择月份和年份,因为在这种情况下,用户不知道或不关心确切的日期。显然存储的数据在 Datetime 格式中,将存储一天,但这与我无关。有没有简单的方法来解决这个问题?

谢谢

【问题讨论】:

【参考方案1】:

感谢@Fernando García 提供的基础。

我已经为 DatePicker 编写了 DateFormat 和 IsMonthYear 附加属性以启用月/年选择。

IsMonthYear 附加属性将 DatePicker 的 Calendar.DisplayMode 限制为 Year 和 Decade,以防止从 CalendarMode.Month 中进行选择。它还将 DatePicker.SelectedDate 的日期部分设置为 1。

DateFormat 附加属性不限于此场景,它也可以与 IsMonthYear 分开使用,为 DatePicker 的显示和输入提供格式字符串。

附加属性的示例用法:

<DatePicker my:DatePickerCalendar.IsMonthYear="True"
            my:DatePickerDateFormat.DateFormat="MM/yyyy"/>

IsMonthYear 附加属性是:

public class DatePickerCalendar

    public static readonly DependencyProperty IsMonthYearProperty =
        DependencyProperty.RegisterAttached("IsMonthYear", typeof(bool), typeof(DatePickerCalendar),
                                            new PropertyMetadata(OnIsMonthYearChanged));

    public static bool GetIsMonthYear(DependencyObject dobj)
    
        return (bool)dobj.GetValue(IsMonthYearProperty);
    

    public static void SetIsMonthYear(DependencyObject dobj, bool value)
    
        dobj.SetValue(IsMonthYearProperty, value);
    

    private static void OnIsMonthYearChanged(DependencyObject dobj, DependencyPropertyChangedEventArgs e)
    
        var datePicker = (DatePicker) dobj;

        Application.Current.Dispatcher
            .BeginInvoke(DispatcherPriority.Loaded,
                         new Action<DatePicker, DependencyPropertyChangedEventArgs>(SetCalendarEventHandlers),
                         datePicker, e);
    

    private static void SetCalendarEventHandlers(DatePicker datePicker, DependencyPropertyChangedEventArgs e)
    
        if (e.NewValue == e.OldValue)
            return;

        if ((bool)e.NewValue)
        
            datePicker.CalendarOpened += DatePickerOnCalendarOpened;
            datePicker.CalendarClosed += DatePickerOnCalendarClosed;
        
        else
        
            datePicker.CalendarOpened -= DatePickerOnCalendarOpened;
            datePicker.CalendarClosed -= DatePickerOnCalendarClosed;
        
    

    private static void DatePickerOnCalendarOpened(object sender, RoutedEventArgs routedEventArgs)
    
        var calendar = GetDatePickerCalendar(sender);
        calendar.DisplayMode = CalendarMode.Year;

        calendar.DisplayModeChanged += CalendarOnDisplayModeChanged;
    

    private static void DatePickerOnCalendarClosed(object sender, RoutedEventArgs routedEventArgs)
    
        var datePicker = (DatePicker) sender;
        var calendar = GetDatePickerCalendar(sender);
        datePicker.SelectedDate = calendar.SelectedDate;

        calendar.DisplayModeChanged -= CalendarOnDisplayModeChanged;
    

    private static void CalendarOnDisplayModeChanged(object sender, CalendarModeChangedEventArgs e)
    
        var calendar = (Calendar) sender;
        if (calendar.DisplayMode != CalendarMode.Month)
            return;

        calendar.SelectedDate = GetSelectedCalendarDate(calendar.DisplayDate);

        var datePicker = GetCalendarsDatePicker(calendar);
        datePicker.IsDropDownOpen = false;
    

    private static Calendar GetDatePickerCalendar(object sender)
    
        var datePicker = (DatePicker) sender;
        var popup = (Popup) datePicker.Template.FindName("PART_Popup", datePicker);
        return ((Calendar) popup.Child);
    

    private static DatePicker GetCalendarsDatePicker(FrameworkElement child)
    
        var parent = (FrameworkElement) child.Parent;
        if (parent.Name == "PART_Root")
            return (DatePicker) parent.TemplatedParent;
        return GetCalendarsDatePicker(parent);
    

    private static DateTime? GetSelectedCalendarDate(DateTime? selectedDate)
    
        if (!selectedDate.HasValue)
            return null;
        return new DateTime(selectedDate.Value.Year, selectedDate.Value.Month, 1);
    

而 DateFormat 附加属性是:

public class DatePickerDateFormat

    public static readonly DependencyProperty DateFormatProperty =
        DependencyProperty.RegisterAttached("DateFormat", typeof (string), typeof (DatePickerDateFormat),
                                            new PropertyMetadata(OnDateFormatChanged));

    public static string GetDateFormat(DependencyObject dobj)
    
        return (string) dobj.GetValue(DateFormatProperty);
    

    public static void SetDateFormat(DependencyObject dobj, string value)
    
        dobj.SetValue(DateFormatProperty, value);
    

    private static void OnDateFormatChanged(DependencyObject dobj, DependencyPropertyChangedEventArgs e)
    
        var datePicker = (DatePicker) dobj;

        Application.Current.Dispatcher.BeginInvoke(
            DispatcherPriority.Loaded, new Action<DatePicker>(ApplyDateFormat), datePicker);
    

    private static void ApplyDateFormat(DatePicker datePicker)
    
        var binding = new Binding("SelectedDate")
            
                RelativeSource = new RelativeSource AncestorType = typeof (DatePicker),
                Converter = new DatePickerDateTimeConverter(),
                ConverterParameter = new Tuple<DatePicker, string>(datePicker, GetDateFormat(datePicker))
            ;
        var textBox = GetTemplateTextBox(datePicker);
        textBox.SetBinding(TextBox.TextProperty, binding);

        textBox.PreviewKeyDown -= TextBoxOnPreviewKeyDown;
        textBox.PreviewKeyDown += TextBoxOnPreviewKeyDown;

        datePicker.CalendarOpened -= DatePickerOnCalendarOpened;
        datePicker.CalendarOpened += DatePickerOnCalendarOpened;
    

    private static TextBox GetTemplateTextBox(Control control)
    
        control.ApplyTemplate();
        return (TextBox) control.Template.FindName("PART_TextBox", control);
    

    private static void TextBoxOnPreviewKeyDown(object sender, KeyEventArgs e)
    
        if (e.Key != Key.Return)
            return;

        /* DatePicker subscribes to its TextBox's KeyDown event to set its SelectedDate if Key.Return was
         * pressed. When this happens its text will be the result of its internal date parsing until it
         * loses focus or another date is selected. A workaround is to stop the KeyDown event bubbling up
         * and handling setting the DatePicker.SelectedDate. */

        e.Handled = true;

        var textBox = (TextBox) sender;
        var datePicker = (DatePicker) textBox.TemplatedParent;
        var dateStr = textBox.Text;
        var formatStr = GetDateFormat(datePicker);
        datePicker.SelectedDate = DatePickerDateTimeConverter.StringToDateTime(datePicker, formatStr, dateStr);
    

    private static void DatePickerOnCalendarOpened(object sender, RoutedEventArgs e)
    
        /* When DatePicker's TextBox is not focused and its Calendar is opened by clicking its calendar button
         * its text will be the result of its internal date parsing until its TextBox is focused and another
         * date is selected. A workaround is to set this string when it is opened. */

        var datePicker = (DatePicker) sender;
        var textBox = GetTemplateTextBox(datePicker);
        var formatStr = GetDateFormat(datePicker);
        textBox.Text = DatePickerDateTimeConverter.DateTimeToString(formatStr, datePicker.SelectedDate);
    

    private class DatePickerDateTimeConverter : IValueConverter
    
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        
            var formatStr = ((Tuple<DatePicker, string>) parameter).Item2;
            var selectedDate = (DateTime?) value;
            return DateTimeToString(formatStr, selectedDate);
        

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        
            var tupleParam = ((Tuple<DatePicker, string>) parameter);
            var dateStr = (string) value;
            return StringToDateTime(tupleParam.Item1, tupleParam.Item2, dateStr);
        

        public static string DateTimeToString(string formatStr, DateTime? selectedDate)
        
            return selectedDate.HasValue ? selectedDate.Value.ToString(formatStr) : null;
        

        public static DateTime? StringToDateTime(DatePicker datePicker, string formatStr, string dateStr)
        
            DateTime date;
            var canParse = DateTime.TryParseExact(dateStr, formatStr, CultureInfo.CurrentCulture,
                                                  DateTimeStyles.None, out date);

            if (!canParse)
                canParse = DateTime.TryParse(dateStr, CultureInfo.CurrentCulture, DateTimeStyles.None, out date);

            return canParse ? date : datePicker.SelectedDate;
        
    

【讨论】:

谢谢,IsMonthYear 附加属性对我帮助很大。我把它翻译成VB,看我的帖子。 很好的答案,但有一个小错误。当您打开下拉菜单并选择日期时,日期框中的文本会瞬间闪烁为原始格式化字符串(例如 dd/MM/yyyy)和新格式化字符串(例如 MM/yyyy)。为了纠正这个问题,我不得不在 DatePickerdateFormat 类中做一些hackery。解决方案如下 嗨@Simon,将此代码嵌入我的wpf窗口的位置,我试图将您的.cs代码复制到窗口.cs,我试图从.xaml调用它。但它显示错误。我是 wpf 的新手。你能帮我理解一下吗? @simon 这有一个错误。单击下拉菜单并选择月份。文本框将显示年份和月份,现在再次单击日历图标并且不要更改月份,尝试单击外部。弹出窗口没有关闭。如何解决这个问题?【参考方案2】:

如果你可以使用日历控件,你可以这样做

<toolkit:Calendar x:Name="_calendar" DisplayModeChanged="_calendar_DisplayModeChanged" DisplayMode="Year" />

使用此代码隐藏

Private Sub _calendar_DisplayModeChanged(ByVal sender As System.Object, ByVal e As Microsoft.Windows.Controls.CalendarModeChangedEventArgs)

    If _calendar.DisplayMode = Microsoft.Windows.Controls.CalendarMode.Month Then
        _calendar.DisplayMode = Microsoft.Windows.Controls.CalendarMode.Year
    End If

End Sub

【讨论】:

这对我不起作用。它从未填充 SelectedDate。 DisplayDate 不可信,因为它可能会随着用户移动鼠标点击另一个按钮而改变。【参考方案3】:

这是Simon's 和Dr. ABT's 答案的组合,包括所有额外的必需代码,并且修复了WindowsMicrosoft 控件之间的引用冲突。

这很荒谬,这需要一个 345 行的类来实现,但如果您包含 DatePickerCalendar.cs(与您的命名空间),构建您的项目并使用以下 XAML,它应该可以工作。

<DatePicker local:DatePickerCalendar.IsMonthYear="True" 
            local:DatePickerDateFormat.DateFormat="MMM-yyyy"
            Text="MMM-yyyy"></DatePicker>

DatePickerCalendar.cs

using System;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
using Calendar = System.Windows.Controls.Calendar;
using CalendarMode = System.Windows.Controls.CalendarMode;
using CalendarModeChangedEventArgs = System.Windows.Controls.CalendarModeChangedEventArgs;
using DatePicker = System.Windows.Controls.DatePicker;

namespace <YourProject>

    public class DatePickerCalendar
    
        public static readonly DependencyProperty IsMonthYearProperty =
            DependencyProperty.RegisterAttached("IsMonthYear", typeof(bool), typeof(DatePickerCalendar),
                                                new PropertyMetadata(OnIsMonthYearChanged));

        public static bool GetIsMonthYear(DependencyObject dobj)
        
            return (bool)dobj.GetValue(IsMonthYearProperty);
        

        public static void SetIsMonthYear(DependencyObject dobj, bool value)
        
            dobj.SetValue(IsMonthYearProperty, value);
        

        private static void OnIsMonthYearChanged(DependencyObject dobj, DependencyPropertyChangedEventArgs e)
        
            var datePicker = (DatePicker)dobj;

            Application.Current.Dispatcher
                .BeginInvoke(DispatcherPriority.Loaded,
                             new Action<DatePicker, DependencyPropertyChangedEventArgs>(SetCalendarEventHandlers),
                             datePicker, e);
        

        private static void SetCalendarEventHandlers(DatePicker datePicker, DependencyPropertyChangedEventArgs e)
        
            if (e.NewValue == e.OldValue)
                return;

            if ((bool)e.NewValue)
            
                datePicker.CalendarOpened += DatePickerOnCalendarOpened;
                datePicker.CalendarClosed += DatePickerOnCalendarClosed;
            
            else
            
                datePicker.CalendarOpened -= DatePickerOnCalendarOpened;
                datePicker.CalendarClosed -= DatePickerOnCalendarClosed;
            
        

        private static void DatePickerOnCalendarOpened(object sender, RoutedEventArgs routedEventArgs)
        
            var calendar = GetDatePickerCalendar(sender);
            calendar.DisplayMode = CalendarMode.Year;

            calendar.DisplayModeChanged += CalendarOnDisplayModeChanged;
        

        private static void DatePickerOnCalendarClosed(object sender, RoutedEventArgs routedEventArgs)
        
            var datePicker = (DatePicker)sender;
            var calendar = GetDatePickerCalendar(sender);
            datePicker.SelectedDate = calendar.SelectedDate;

            calendar.DisplayModeChanged -= CalendarOnDisplayModeChanged;
        

        private static void CalendarOnDisplayModeChanged(object sender, CalendarModeChangedEventArgs e)
        
            var calendar = (Calendar)sender;
            if (calendar.DisplayMode != CalendarMode.Month)
                return;

            calendar.SelectedDate = GetSelectedCalendarDate(calendar.DisplayDate);

            var datePicker = GetCalendarsDatePicker(calendar);
            datePicker.IsDropDownOpen = false;
        

        private static Calendar GetDatePickerCalendar(object sender)
        
            var datePicker = (DatePicker)sender;
            var popup = (Popup)datePicker.Template.FindName("PART_Popup", datePicker);
            return ((Calendar)popup.Child);
        

        private static DatePicker GetCalendarsDatePicker(FrameworkElement child)
        
            var parent = (FrameworkElement)child.Parent;
            if (parent.Name == "PART_Root")
                return (DatePicker)parent.TemplatedParent;
            return GetCalendarsDatePicker(parent);
        

        private static DateTime? GetSelectedCalendarDate(DateTime? selectedDate)
        
            if (!selectedDate.HasValue)
                return null;
            return new DateTime(selectedDate.Value.Year, selectedDate.Value.Month, 1);
        
    

    public class DatePickerDateFormat
    
        public static readonly DependencyProperty DateFormatProperty =
            DependencyProperty.RegisterAttached("DateFormat", typeof(string), typeof(DatePickerDateFormat),
                                                new PropertyMetadata(OnDateFormatChanged));

        public static string GetDateFormat(DependencyObject dobj)
        
            return (string)dobj.GetValue(DateFormatProperty);
        

        public static void SetDateFormat(DependencyObject dobj, string value)
        
            dobj.SetValue(DateFormatProperty, value);
        

        private static void OnDateFormatChanged(DependencyObject dobj, DependencyPropertyChangedEventArgs e)
        
            var datePicker = (DatePicker)dobj;

            Application.Current.Dispatcher.BeginInvoke(
                DispatcherPriority.Loaded, new Action<DatePicker>(ApplyDateFormat), datePicker);
        
        private static void ApplyDateFormat(DatePicker datePicker)
        
            var binding = new Binding("SelectedDate")
            
                RelativeSource = new RelativeSource  AncestorType = typeof(DatePicker) ,
                Converter = new DatePickerDateTimeConverter(),
                ConverterParameter = new Tuple<DatePicker, string>(datePicker, GetDateFormat(datePicker)),
                StringFormat = GetDateFormat(datePicker) // This is also new but didnt seem to help
            ;

            var textBox = GetTemplateTextBox(datePicker);
            textBox.SetBinding(TextBox.TextProperty, binding);

            textBox.PreviewKeyDown -= TextBoxOnPreviewKeyDown;
            textBox.PreviewKeyDown += TextBoxOnPreviewKeyDown;

            var dropDownButton = GetTemplateButton(datePicker);

            datePicker.CalendarOpened -= DatePickerOnCalendarOpened;
            datePicker.CalendarOpened += DatePickerOnCalendarOpened;

            // Handle Dropdownbutton PreviewMouseUp to prevent issue of flickering textboxes
            dropDownButton.PreviewMouseUp -= DropDownButtonPreviewMouseUp;
            dropDownButton.PreviewMouseUp += DropDownButtonPreviewMouseUp;
        

        private static ButtonBase GetTemplateButton(DatePicker datePicker)
        
            return (ButtonBase)datePicker.Template.FindName("PART_Button", datePicker);
        


        /// <summary>
        ///     Prevents a bug in the DatePicker, where clicking the Dropdown open button results in Text being set to default formatting regardless of StringFormat or binding overrides
        /// </summary>
        private static void DropDownButtonPreviewMouseUp(object sender, MouseButtonEventArgs e)
        
            var fe = sender as FrameworkElement;
            if (fe == null) return;

            var datePicker = fe.TryFindParent<DatePicker>();
            if (datePicker == null || datePicker.SelectedDate == null) return;

            var dropDownButton = GetTemplateButton(datePicker);

            // Dropdown button was clicked
            if (e.OriginalSource == dropDownButton && datePicker.IsDropDownOpen == false)
            
                // Open dropdown
                datePicker.SetCurrentValue(DatePicker.IsDropDownOpenProperty, true);

                // Mimic everything else in the standard DatePicker dropdown opening *except* setting textbox value 
                datePicker.SetCurrentValue(DatePicker.DisplayDateProperty, datePicker.SelectedDate.Value);

                // Important otherwise calendar does not work
                dropDownButton.ReleaseMouseCapture();

                // Prevent datePicker.cs from handling this event 
                e.Handled = true;
            
        



        private static TextBox GetTemplateTextBox(Control control)
        
            control.ApplyTemplate();
            return (TextBox)control?.Template?.FindName("PART_TextBox", control);
        

        private static void TextBoxOnPreviewKeyDown(object sender, KeyEventArgs e)
        
            if (e.Key != Key.Return)
                return;

            /* DatePicker subscribes to its TextBox's KeyDown event to set its SelectedDate if Key.Return was
             * pressed. When this happens its text will be the result of its internal date parsing until it
             * loses focus or another date is selected. A workaround is to stop the KeyDown event bubbling up
             * and handling setting the DatePicker.SelectedDate. */

            e.Handled = true;

            var textBox = (TextBox)sender;
            var datePicker = (DatePicker)textBox.TemplatedParent;
            var dateStr = textBox.Text;
            var formatStr = GetDateFormat(datePicker);
            datePicker.SelectedDate = DatePickerDateTimeConverter.StringToDateTime(datePicker, formatStr, dateStr);
        

        private static void DatePickerOnCalendarOpened(object sender, RoutedEventArgs e)
        
            /* When DatePicker's TextBox is not focused and its Calendar is opened by clicking its calendar button
             * its text will be the result of its internal date parsing until its TextBox is focused and another
             * date is selected. A workaround is to set this string when it is opened. */

            var datePicker = (DatePicker)sender;
            var textBox = GetTemplateTextBox(datePicker);
            var formatStr = GetDateFormat(datePicker);
            textBox.Text = DatePickerDateTimeConverter.DateTimeToString(formatStr, datePicker.SelectedDate);
        

        private class DatePickerDateTimeConverter : IValueConverter
        
            public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
            
                var formatStr = ((Tuple<DatePicker, string>)parameter).Item2;
                var selectedDate = (DateTime?)value;
                return DateTimeToString(formatStr, selectedDate);
            

            public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
            
                var tupleParam = ((Tuple<DatePicker, string>)parameter);
                var dateStr = (string)value;
                return StringToDateTime(tupleParam.Item1, tupleParam.Item2, dateStr);
            

            public static string DateTimeToString(string formatStr, DateTime? selectedDate)
            
                return selectedDate.HasValue ? selectedDate.Value.ToString(formatStr) : null;
            

            public static DateTime? StringToDateTime(DatePicker datePicker, string formatStr, string dateStr)
            
                DateTime date;
                var canParse = DateTime.TryParseExact(dateStr, formatStr, CultureInfo.CurrentCulture,
                                                      DateTimeStyles.None, out date);

                if (!canParse)
                    canParse = DateTime.TryParse(dateStr, CultureInfo.CurrentCulture, DateTimeStyles.None, out date);

                return canParse ? date : datePicker.SelectedDate;
            


        

    



    public static class FEExten
    
        /// <summary>
        /// Finds a parent of a given item on the visual tree.
        /// </summary>
        /// <typeparam name="T">The type of the queried item.</typeparam>
        /// <param name="child">A direct or indirect child of the
        /// queried item.</param>
        /// <returns>The first parent item that matches the submitted
        /// type parameter. If not matching item can be found, a null
        /// reference is being returned.</returns>
        public static T TryFindParent<T>(this DependencyObject child)
            where T : DependencyObject
        
            //get parent item
            DependencyObject parentObject = GetParentObject(child);

            //we've reached the end of the tree
            if (parentObject == null) return null;

            //check if the parent matches the type we're looking for
            T parent = parentObject as T;
            if (parent != null)
            
                return parent;
            
            else
            
                //use recursion to proceed with next level
                return TryFindParent<T>(parentObject);
            
        

        /// <summary>
        /// This method is an alternative to WPF's
        /// <see cref="VisualTreeHelper.GetParent"/> method, which also
        /// supports content elements. Keep in mind that for content element,
        /// this method falls back to the logical tree of the element!
        /// </summary>
        /// <param name="child">The item to be processed.</param>
        /// <returns>The submitted item's parent, if available. Otherwise
        /// null.</returns>
        public static DependencyObject GetParentObject(this DependencyObject child)
        
            if (child == null) return null;

            //handle content elements separately
            ContentElement contentElement = child as ContentElement;
            if (contentElement != null)
            
                DependencyObject parent = ContentOperations.GetParent(contentElement);
                if (parent != null) return parent;

                FrameworkContentElement fce = contentElement as FrameworkContentElement;
                return fce != null ? fce.Parent : null;
            

            //also try searching for parent in framework elements (such as DockPanel, etc)
            FrameworkElement frameworkElement = child as FrameworkElement;
            if (frameworkElement != null)
            
                DependencyObject parent = frameworkElement.Parent;
                if (parent != null) return parent;
            

            //if it's not a ContentElement/FrameworkElement, rely on VisualTreeHelper
            return VisualTreeHelper.GetParent(child);
        
    

截图

【讨论】:

很棒的方法。我建议更新这一行中的类: return (TextBox)control?.Template?.FindName("PART_TextBox", control);删除问号。比我只复制和粘贴我的代码,似乎工作正常。谢谢 我可以用datatrigger动态改变这个类的属性吗? 这有一个错误。单击下拉菜单并选择月份。文本框将显示年份和月份,现在再次单击日历图标并且不要更改月份,尝试单击外部。弹出窗口没有关闭。如何解决这个问题? @AbdulsalamElsharif 你能检查一下你是否从日历中选择了一个月,然后再次打开它,如果你点击外面没有更改日期,它会为你关闭弹出窗口吗?【参考方案4】:

我将Simon的答案翻译成VB,我把结果贴在这里(请给他学分)

Imports System.Windows.Threading
Imports System.Windows.Controls.Primitives
Imports System.Windows.Controls

''' <summary>     Allows the Date Picker Calendar to select month.
'''                      Use with the attached property IsMonthYear Property.
''' </summary>
''' <remarks> Source : https://***.com/questions/1798513/wpf-toolkit-datepicker-month-year-only 
'''           Author : </remarks>
Public Class DatePickerCalendar

Public Shared IsMonthYearProperty As DependencyProperty = DependencyProperty.RegisterAttached("IsMonthYear", GetType(System.Boolean), GetType(DatePickerCalendar), New PropertyMetadata(AddressOf OnIsMonthYearChanged))

Public Shared Function GetIsMonthYear(ByVal dobj As DependencyObject) As Boolean
    Return CType(dobj.GetValue(IsMonthYearProperty), Boolean)
End Function

Public Shared Sub SetIsMonthYear(ByVal dobj As DependencyObject, ByVal value As Boolean)
    dobj.SetValue(IsMonthYearProperty, value)
End Sub

Private Shared Sub OnIsMonthYearChanged(ByVal dobj As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
    Dim MyDatePicker = CType(dobj, DatePicker)
    Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Loaded, _
                    New Action(Of DatePicker, DependencyPropertyChangedEventArgs)(AddressOf SetCalendarEventHandlers), MyDatePicker, e)
End Sub

Private Shared Sub SetCalendarEventHandlers(ByVal datePicker As DatePicker, ByVal e As DependencyPropertyChangedEventArgs)
    If (e.NewValue = e.OldValue) Then
        Return
    End If
    If CType(e.NewValue, Boolean) Then
        AddHandler datePicker.CalendarOpened, AddressOf DatePickerOnCalendarOpened
        AddHandler datePicker.CalendarClosed, AddressOf DatePickerOnCalendarClosed
    Else
        RemoveHandler datePicker.CalendarOpened, AddressOf DatePickerOnCalendarOpened
        RemoveHandler datePicker.CalendarClosed, AddressOf DatePickerOnCalendarClosed
    End If
End Sub

Private Shared Sub DatePickerOnCalendarOpened(ByVal sender As Object, ByVal routedEventArgs As RoutedEventArgs)
    Dim MyCalendar = GetDatePickerCalendar(sender)
    MyCalendar.DisplayMode = CalendarMode.Year
    AddHandler MyCalendar.DisplayModeChanged, AddressOf CalendarOnDisplayModeChanged
End Sub

Private Shared Sub DatePickerOnCalendarClosed(ByVal sender As Object, ByVal routedEventArgs As RoutedEventArgs)
    Dim MyDatePicker = CType(sender, DatePicker)
    Dim MyCalendar = GetDatePickerCalendar(sender)
    MyDatePicker.SelectedDate = MyCalendar.SelectedDate
    RemoveHandler MyCalendar.DisplayModeChanged, AddressOf CalendarOnDisplayModeChanged
End Sub

Private Shared Sub CalendarOnDisplayModeChanged(ByVal sender As Object, ByVal e As CalendarModeChangedEventArgs)
    Dim MyCalendar = CType(sender, Calendar)
    If (MyCalendar.DisplayMode <> CalendarMode.Month) Then
        Return
    End If
    MyCalendar.SelectedDate = GetSelectedCalendarDate(MyCalendar.DisplayDate)
    Dim MyDatePicker = GetCalendarsDatePicker(MyCalendar)
    MyDatePicker.IsDropDownOpen = False
End Sub

Private Shared Function GetDatePickerCalendar(ByVal sender As Object) As Calendar
    Dim MyDatePicker = CType(sender, DatePicker)
    Dim MyPopup = CType(MyDatePicker.Template.FindName("PART_Popup", MyDatePicker), Popup)
    Return CType(MyPopup.Child, Calendar)
End Function

Private Shared Function GetCalendarsDatePicker(ByVal child As FrameworkElement) As DatePicker
    Dim MyParent = CType(child.Parent, FrameworkElement)
    If (MyParent.Name = "PART_Root") Then
        Return CType(MyParent.TemplatedParent, DatePicker)
    End If
    Return GetCalendarsDatePicker(MyParent)
End Function

Private Shared Function GetSelectedCalendarDate(ByVal selectedDate As DateTime?) As DateTime?
    If Not selectedDate.HasValue Then
        Return Nothing
    End If
    Return New DateTime(selectedDate.Value.Year, selectedDate.Value.Month, 1)
End Function
End Class

但是对于格式类,我无法让它工作。 我使用并稍微修改了 petrycol 的(更简单的)答案来显示月/年。来源:Changing the string format of the WPF DatePicker

    <DatePicker   SelectedDate="Binding FromDate"  
                  l:DatePickerCalendar.IsMonthYear="True"
                  x:Name="MonthCalendar" HorizontalAlignment="Center"                   
                 >
        <DatePicker.Resources>
            <!--Source : https://***.com/questions/3819832/changing-the-string-format-of-the-wpf-datepicker
                Author : petrycol -->
            <Style TargetType="x:Type DatePickerTextBox">
                <Setter Property="Control.Template">
                    <Setter.Value>
                        <ControlTemplate>
                            <TextBox Width="60"    TextAlignment="Center" x:Name="PART_TextBox"
                                     Text="Binding Path=SelectedDate, StringFormat='MM yy', 
                                     RelativeSource=RelativeSource AncestorType=x:Type DatePicker,FallbackValue='-- --'" />
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>
        </DatePicker.Resources>
        <!--CalendarOpened="DatePicker_CalendarOpened"-->
    </DatePicker>

【讨论】:

【参考方案5】:

为了添加到Simon's brilliant answer,我对 DatePickerDateFormat.cs 进行了一些更改,以防止文本框在 dd/MM/yyyy(原始字符串格式)和 MM/yyyy(被覆盖的字符串格式)之间短暂闪烁的错误。

这是由于 DatePicker 在打开下拉列表之前立即在内部设置了 PART_Textbox 文本。它将它设置为字符串格式的文本,您对此无能为力 - 它会覆盖您的绑定。

为了防止这种行为,我将此代码添加到DatePickerDateFormat class from above。

    在ApplyDateFormat中,获取下拉按钮并处理PreviewMouseUp

    private static void ApplyDateFormat(DatePicker datePicker)
    
        var binding = new Binding("SelectedDate")
                      
                          RelativeSource = new RelativeSource  AncestorType = typeof(DatePicker) ,
                          Converter = new DatePickerDateTimeConverter(),
                          ConverterParameter = new Tuple<DatePicker, string>(datePicker, GetDateFormat(datePicker)),
                          StringFormat = GetDateFormat(datePicker) // This is also new but didnt seem to help
                      ;
    
        var textBox = GetTemplateTextBox(datePicker);
        textBox.SetBinding(TextBox.TextProperty, binding);
    
        textBox.PreviewKeyDown -= TextBoxOnPreviewKeyDown;
        textBox.PreviewKeyDown += TextBoxOnPreviewKeyDown;
    
        var dropDownButton = GetTemplateButton(datePicker);
    
        datePicker.CalendarOpened -= DatePickerOnCalendarOpened;
        datePicker.CalendarOpened += DatePickerOnCalendarOpened;
    
        // Handle Dropdownbutton PreviewMouseUp to prevent issue of flickering textboxes
        dropDownButton.PreviewMouseUp -= DropDownButtonPreviewMouseUp;
        dropDownButton.PreviewMouseUp += DropDownButtonPreviewMouseUp;
    
    
    private static ButtonBase GetTemplateButton(DatePicker datePicker)
    
        return (ButtonBase)datePicker.Template.FindName("PART_Button", datePicker);
    
    

当 PreviewMouseUp 触发时,如果存在 Selected 日期,则覆盖 On Dropdown Shown 行为(我们模仿 Datepicker 所做的一切,但我们不设置 PART_TextBox 文本)

    /// <summary>
    ///     Prevents a bug in the DatePicker, where clicking the Dropdown open button results in Text being set to default formatting regardless of StringFormat or binding overrides
    /// </summary>
    private static void DropDownButtonPreviewMouseUp(object sender, MouseButtonEventArgs e)
    
        var fe = sender as FrameworkElement;
        if (fe == null) return;

        var datePicker = fe.TryFindAncestorOrSelf<DatePicker>();
        if (datePicker == null || datePicker.SelectedDate == null) return;            

        var dropDownButton = GetTemplateButton(datePicker);

        // Dropdown button was clicked
        if (e.OriginalSource == dropDownButton && datePicker.IsDropDownOpen == false)
                                        
            // Open dropdown
            datePicker.SetCurrentValue(DatePicker.IsDropDownOpenProperty, true);

            // Mimic everything else in the standard DatePicker dropdown opening *except* setting textbox value 
            datePicker.SetCurrentValue(DatePicker.DisplayDateProperty, datePicker.SelectedDate.Value);

            // Important otherwise calendar does not work
            dropDownButton.ReleaseMouseCapture();

            // Prevent datePicker.cs from handling this event 
            e.Handled = true;                
        
    

扩展方法 TryFindAncestorOrSelf 沿着可视化树向上查找类型 T 的对象。您可以在此处找到它的实现:http://www.hardcodet.net/2008/02/find-wpf-parent

希望这对某人有所帮助!

【讨论】:

方法的原始名称是TryFindParent :) 博士。 ABT,感谢与原始格式闪烁相关的修复。我已经添加了您的更改,但是现在,如果日历已打开并且您单击远离控件,则日历将保持打开状态。正常的 DatePicker 行为是在您单击远离控件时关闭日历控件。知道为什么会这样吗? 天哪,恐怕不是,我在 3 年前就参与了这个项目! @user1202134 我遇到了同样的问题。你能找到解决办法吗?如果是,请分享【参考方案6】:

最近我对日历有一个特定要求,可以选择仅选择月份和年份。所以我之前有一篇关于 wpf datepicker 的自定义格式的帖子,但在这种情况下,我们需要做一个有点棘手的答案。首先你需要像这样创建一个特定的控件:

public class DatePickerCo : DatePicker

一旦你已经这样做了。当其他人存在另一个帖子时,我不记得该帖子的网址,但无论如何,认为您需要像这样覆盖 OnCalendarOpened 方法:

 
 protected override void OnCalendarOpened(RoutedEventArgs e)
        
            var popup = this.Template.FindName(
                "PART_Popup", this) as Popup;
            if (popup != null && popup.Child is System.Windows.Controls.Calendar)
            
                ((System.Windows.Controls.Calendar)popup.Child).DisplayMode = CalendarMode.Year;
            

            ((System.Windows.Controls.Calendar)popup.Child).DisplayModeChanged += new EventHandler(DatePickerCo_DisplayModeChanged);
        

请注意,我们在最后一行添加了事件 DisplayModeChanged 的​​处理程序。这对于接下来的步骤非常重要。好的,下一步是定义 DisplayModeChanged


 private void DatePickerCo_DisplayModeChanged(object sender, CalendarModeChangedEventArgs e)
        
            var popup = this.Template.FindName(
                "PART_Popup", this) as Popup;
            if (popup != null && popup.Child is System.Windows.Controls.Calendar)
            
                var _calendar = popup.Child as System.Windows.Controls.Calendar;
                if (_calendar.DisplayMode == CalendarMode.Month)
                
                    _calendar.DisplayMode = CalendarMode.Year;

                    if (IsDropDownOpen)
                    
                        this.SelectedDate = GetSelectedMonth(_calendar.DisplayDate);
                        this.IsDropDownOpen = false;
                        ((System.Windows.Controls.Calendar)popup.Child).DisplayModeChanged -= new EventHandler(DatePickerCo_DisplayModeChanged);
                    
                

            
        

 private DateTime? GetSelectedMonth(DateTime? selectedDate)
        
            if (selectedDate == null)
            
                selectedDate = DateTime.Now;
            

            int monthDifferenceStart = DateTimeHelper.CompareYearMonth(selectedDate.Value, DisplayDateRangeStart);
            int monthDifferenceEnd = DateTimeHelper.CompareYearMonth(selectedDate.Value, DisplayDateRangeEnd);

            if (monthDifferenceStart >= 0 && monthDifferenceEnd  0, "monthDifferenceEnd should be greater than 0!");
                    _selectedMonth = DateTimeHelper.DiscardDayTime(DisplayDateRangeEnd);
                
            

            return _selectedMonth;
        

这里有几件事,首先你需要选择一个月份,这就是创建函数 GetSelectedMonth 的原因。第二件事是该方法使用名为 DateTimeHelper.cs 的 WPFToolkit 类。好的 到目前为止,我们刚刚创建了获取月份的功能。但是,一旦您单击月历,我们就会继续以月/年格式显示所选日期。为此,我们只需要为我们的自定义控件创建一个特定的样式,如下所示:

在我的第一个答案中 How to change format (e.g. dd/MMM/yyyy) of DateTimePicker in WPF application

我用日期选择器的自定义格式解释了所有内容。我希望这对你有帮助。如有任何意见或建议,请告诉我,问候!!。

【讨论】:

你知道silverlight工具包版本有什么类似的方法吗?【参考方案7】:

我想提供一个不需要大量代码的替代解决方案。 您可以绑定到 DatePickers 的 CalendarOpened 事件。在这种情况下,您可以将Calendar 的显示模式设置为Decade,并为Calendar 上的DisplayModeChanged 事件添加事件处理程序。然后在 DisplayModeChanged 事件处理程序中,如果模式是月份,您可以关闭日历,并将 SelectedDate 设置为当前的 DisplayDate。这对我的目的很有效

XAML

<DatePicker
Name="YearPicker"
Grid.Column="1"
SelectedDate="Binding SelectedDate, Mode=TwoWay"
CalendarOpened="DatePicker_Opened">
<DatePicker.Resources>
    <Style TargetType="DatePickerTextBox">
        <Setter Property="Control.Template">
            <Setter.Value>
                <ControlTemplate>
                    <TextBox x:Name="PART_TextBox"
                            Text="Binding Path=SelectedDate, StringFormat = 0:MM-yyyy, 
                            RelativeSource=RelativeSource AncestorType=x:Type DatePicker" />
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</DatePicker.Resources>

然后是后面的代码

private void DatePicker_Opened(object sender, RoutedEventArgs e)

    DatePicker datepicker = (DatePicker)sender;
    Popup popup = (Popup)datepicker.Template.FindName("PART_Popup", datepicker);
    Calendar cal = (Calendar)popup.Child;
    cal.DisplayModeChanged += Calender_DisplayModeChanged;
    cal.DisplayMode = CalendarMode.Decade;


private void Calender_DisplayModeChanged(object sender, CalendarModeChangedEventArgs e)

    Calendar calendar = (Calendar)sender;
    if (calendar.DisplayMode == CalendarMode.Month)
    
        calendar.SelectedDate = calendar.DisplayDate;
        YearPicker.IsDropDownOpen = false;
    

我希望这会有所帮助!

【讨论】:

一半时间单击日历图标不会启动下拉菜单以供选择。可以查一下吗? 你能重新看看这个问题吗

以上是关于WPF 工具包 DatePicker 仅限月/年的主要内容,如果未能解决你的问题,请参考以下文章

更改 WPF DatePicker 年/月标题区域背景颜色

编辑 WPF 工具包以仅获取 DatePicker

WPF 工具包 DatePicker 更改默认值“显示日历”

WPF DatePicker 多个日期

WPF DatePicker 是不是允许您指定时区?

WPF DatePicker IsEnabled 属性不改变外观