增强WPF DatePicker:通过SmartDate CustomControl解决的问题和解决方案

目录

识别WPF DatePicker的问题

了解WPF DatePicker

源代码下载和设置

项目结构

声明和使用SmartDate

CustomControl的定义和使用

设计一个新的DatePicker:SmartDate

选择对ContentControl的控制

使用DataTemplate

DataTemplate上的负面视图

SmartDate的关键绑定属性(DependencyProperty)

SmartDate模板设计

CustomControl不用于重用,仅供内部使用

了解PART_控制项及其角色

SmartDate.cs源代码

其他实现

SmartDate实现教程和源代码简介


识别WPF DatePicker的问题

WPF DatePickerWPF中的核心控件之一,已有近20年的历史。与ButtonTextBoxesCheckBoxes等更简单的控件相比,DatePicker具有更复杂的结构和阶段,由多个控件组成。这种复杂性需要高度的自定义专业知识,因此难以使用或修改提供的过时控件。

了解WPF DatePicker

分析和理解DatePicker的结构及其内部元素在模板中的交互,对于增强WPF中的基本设计和分析技能非常有益。这适用于所有WPF控件,而不仅仅是DatePicker。但是,由于DatePicker是根据过时的趋势设计的,因此基于基本Control实现新的CustomControl可能更有效。

源代码下载和设置

本文确定了使用基本DatePicker的问题,并演示了如何使用CustomControl方法重新设计它。通过GitHub下载源代码以直接检查结果并与本文一起阅读也是有益的。

首先,使用以下git命令下载源代码:

git clone https://github.com/vickyqu115/smartdate

接下来,要从源代码运行解决方案文件,您需要一个具有Windows 10或更高版本、Visual Studio 2022Rider以及.NET 8.0的环境。

SmartDate.sln

项目结构

SmartDate由两个项目组成:

  • SmartDateControl智能日期控制
  • SmartDat应用程序

SmartDateControl是一个CustomControl库,它包括SmartDate类以及所有其他从属CustomControl类。SmartDateApp是一个简单的应用程序项目,用于指导用户如何使用此控件。

声明和使用SmartDate

用法很简单。使用xmlns声明命名空间,并使用SmartDate,就像标准DatePicker一样。

<Window x:Class="SmartDateApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:smart="clr-namespace:SmartDateControl.UI.Units;assembly=SmartDateControl"
        xmlns:theme="https://jamesnet.dev/xaml/presentation/themeswitch"
        mc:Ignorable="d"
        x:Name="Window"
        Title="SmartDate" Height="450" Width="800" Background="#FFFFFF">
    <Viewbox Width="500">
        <UniformGrid Margin="20" Columns="1" VerticalAlignment="Top">
            <smart:SmartDate SelectedDate="{Binding Created}"/>
            <DatePicker SelectedDate="{Binding Created}"/>
        </UniformGrid>
    </Viewbox>
</Window>

SelectedDateDependencyProperty并使用相同的DateTimetype作为DatePickerSelectedDate

执行结果

CustomControl的定义和使用

我之前已经通过CodeProject上的四篇文章详细讨论了CustomControls背后的技术。如果您需要了解和掌握CustomControls,请参考这些文章。特别是,有关RiotSlider的文章深入探讨了WPF CustomControls的体系结构,因此,如果您还没有阅读它,我强烈建议您阅读它。

回到主要讨论,让我们定义CustomControl。通常,CustomControl以从Control派生的类为目标,但实际上,它包括从DependencyObject派生的所有类,而不仅仅是那些继承Control的类(如Panel)以及Visuals(如Animations)。但是,如前所述,只有在可以使用模板或至少使用DataContext的层中实现CustomControls才有意义。因此,在CustomControl样式中实现从FrameworkElement派生的类被视为明智的。

设计一个新的DatePicker:SmartDate

本文将详细介绍如何实现一个名为SmartDate的新CustomControl,该控件派生自最基本的类Control,而不使用现有的DatePicker

选择对ContentControl的控制

首先,让我们看看ContentControlControl之间的区别。ContentControl不仅提供基本的Template,还提供ContentContentTemplate的属性。这些属性通过ContentPresenter自动链接,从而自动设置ContentPresenterContentContentTemplate之间的关系。因此,建议根据DataTemplate的基本用法选择派生控件。

DatePicker从根本上说是利用DataTemplate的控件吗?虽然意见可能有所不同,但像DatePicker这样的复杂控件通常需要多个DataTemplate,并且与标准ContentControl不同。实际上,DatePicker派生自Control,类似类型的控件通常继承自Control。例如,ComboBox可能看起来类似于DatePicker,但它是具有ItemsSource属性的ItemsControl

因此,将SmartDate的实现基于Control是合适的,特别是因为SmartDate不提供自己的DataTemplate

使用DataTemplate

尽管SmartDate默认情况下不提供DataTemplate,但在控件的各个区域中,通过DataTemplate进行扩展的许多点可能会有所帮助。

例如,您可以扩展DayOfWeek控件的ContentPresenter以添加特定日期处理,这是客户端之间的常见要求。这允许各种扩展,例如特殊日期的触发器或转换器。

通过将SelectedDate绑定区域扩展到ContentPresenter,您可以灵活地使用它来选择日期,合并从简单的TextBlock到可编辑的TextBox甚至包含时间的格式。

DataTemplate上的负面视图

DataTemplate从根本上保持了多功能性,即使在复杂情况下也是如此,并且是自定义的重要模板区域。但是,应仔细考虑是否将此多功能性应用于日期选取器等特定控件。使用DataTemplate意味着必须将所有相关logic分离为交互式实现的组件。虽然这看起来很实用,但做出正确的判断至关重要。

SmartDate的关键绑定属性(DependencyProperty

此控件包括名为SelectedDateDateTime?类型的绑定属性。由于默认值可能为null,因此将其声明为可为null的类型,用于设置通过日历选择的日期值。

SmartDate模板设计

ControlTemplat设计中必须包含的基本组件如下:

  • Popup
  • ListBox
  • ToggleButton

Popup充当一个面板,其中包含ListBox(即日历),而ListBox使用内部ItemsPanel通过UniformGrid实现日历。ToggleButton用作日历图标,切换该按钮会更改PopupIsOpen属性以控制日历窗口。此设置在基本DatePicker控件中也类似,因此将其与DatePicker的实际开源代码进行比较非常有益。

现在,让我们看看SmartDate控件在其Template中的结构。

SmartDate:ControlTemplate

<ControlTemplate TargetType="{x:Type units:SmartDate}">
    <Border Background="{TemplateBinding Background}"
            BorderBrush="{TemplateBinding BorderBrush}"
            BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="4">
        <Grid>
            <units:CalendarSwitch x:Name="PART_Switch"/>
            <Popup x:Name="PART_Popup" StaysOpen="False" ...>
                <Border Background="{TemplateBinding Background}" ...>
                    <james:JamesGrid Rows="Auto,Auto,Auto" Columns="*">
                        <james:JamesGrid Rows="*" Columns="Auto,*,Auto">
                            <units:ChevronButton x:Name="PART_Left" Tag="Left"/>
                            <TextBlock Style="{StaticResource MonthStyle}"/>
                            <units:ChevronButton x:Name="PART_Right" Tag="Right"/>
                        </james:JamesGrid>
                        <UniformGrid Columns="7">
                            <units:DayOfWeek Grid.Column="0" Content="Su"/>
                            <units:DayOfWeek Grid.Column="1" Content="Mo"/>
                            <units:DayOfWeek Grid.Column="2" Content="Tu"/>
                            <units:DayOfWeek Grid.Column="3" Content="We"/>
                            <units:DayOfWeek Grid.Column="4" Content="Th"/>
                            <units:DayOfWeek Grid.Column="5" Content="Fr"/>
                            <units:DayOfWeek Grid.Column="6" Content="Sa"/>
                        </UniformGrid>
                        <units:CalendarBox x:Name="PART_ListBox"/>
                    </james:JamesGrid>
                </Border>
            </Popup>
        </Grid>
    </Border>
</ControlTemplate>

正如您在ControlTemplate中看到的那样,前面提到的所有组件都包含在内。Popup用作基本控件,而CalendarSwitch是从ToggleButton继承的日历切换按钮。最后,从ListBox继承的CalendarBox用作在日历上选择日期的列表控件。

此外,还包括用于导航到上个月和下个月的按钮、用于显示当前月份的TextBlock以及用于显示星期几的设计元素。

CustomControl不用于重用,仅供内部使用

SmartDate控件不仅由其自身使用,而且还在其Template中使用CustomControls。并非所有CustomControls都用于通用控件实现。在像SmartDate这样的情况下,它们是为特定目的实现的,从WPF体系结构的角度来看,这是一种常见的做法。

此类控件通常归类在命名空间“Primitives”下。此类别包括ToggleButtonThumbScrollBar等控件,这些控件通常不直接使用,而是在其他控件的内部使用。

根据有关WPF的这些体系结构事实,可以看出SmartDate控件的Template的结构与WPF的基本模式没有显著差异。

了解PART_控制项及其角色

CustomControl结构不会像UserControls那样自动连接代码和XAML。因此,两者之间的所有交互都由_PART控件完全管理。

预定义的_PART控件包括:

  • PART_Switch
  • PART_ListBox
  • PART_Left
  • PART_Right

这些是在覆盖SmartDate类的OnApplyTemplate方法期间分配的,其中实现了所有必要的过程,例如按钮事件和日期生成。最好在通过OnApplyTemplate传递时用PART_前缀命名控件。此外,在XAML中命名这些元素的方式,使开发人员能够根据PART_名称预测类中发生的进程,这将是示例。

SmartDate.cs源代码

接下来,我们将检查SmartDate.cs类文件中包含的核心实现。需要关注的关键领域包括:

  • 声明的DependencyProperty
  • 通过OnApplyTemplate定义PART_元素
  • 通过SelectedDate属性的日期选择控制逻辑
  • CalendarBox中SelectedItem/SelectedValue的利用率

SmartDate:CustomControl

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace SmartDateControl.UI.Units
{
    public class SmartDate : Control
    {
        private Popup _popup;
        private CalendarSwitch _switch;
        private CalendarBox _listbox;

        public bool KeepPopupOpen
        {
            get { return (bool)GetValue(KeepPopupOpenProperty); }
            set { SetValue(KeepPopupOpenProperty, value); }
        }

        public static readonly DependencyProperty KeepPopupOpenProperty =
            DependencyProperty.Register("KeepPopupOpen", typeof(bool), typeof(SmartDate), new PropertyMetadata(true));



        public DateTime CurrentMonth
        {
            get { return (DateTime)GetValue(CurrentMonthProperty); }
            set { SetValue(CurrentMonthProperty, value); }
        }

        public static readonly DependencyProperty CurrentMonthProperty =
            DependencyProperty.Register("CurrentMonth", typeof(DateTime), typeof(SmartDate), new PropertyMetadata(null));



        public DateTime? SelectedDate
        {
            get { return (DateTime?)GetValue(SelectedDateProperty); }
            set { SetValue(SelectedDateProperty, value); }
        }

        public static readonly DependencyProperty SelectedDateProperty =
            DependencyProperty.Register("SelectedDate", typeof(DateTime?), typeof(SmartDate), new PropertyMetadata(null));



        static SmartDate()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(SmartDate), new FrameworkPropertyMetadata(typeof(SmartDate)));
        }
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            _popup = (Popup)GetTemplateChild("PART_Popup");
            _switch = (CalendarSwitch)GetTemplateChild("PART_Switch");
            _listbox = (CalendarBox)GetTemplateChild("PART_ListBox");
            ChevronButton leftButton = (ChevronButton)GetTemplateChild("PART_Left");
            ChevronButton rightButton = (ChevronButton)GetTemplateChild("PART_Right");

            _popup.Closed += _popup_Closed;
            _switch.Click += _switch_Click;
            _listbox.MouseLeftButtonUp += _listbox_MouseLeftButtonUp;

            leftButton.Click += (s, e) => MoveMonthClick(-1);
            rightButton.Click += (s, e) => MoveMonthClick(1);

        }

        private void MoveMonthClick(int month)
        {
            GenerateCalendar(CurrentMonth.AddMonths(month));
        }

        private void _popup_Closed(object sender, EventArgs e)
        {
            _switch.IsChecked = IsMouseOver;
        }

        private void _switch_Click(object sender, RoutedEventArgs e)
        {
            if (_switch.IsChecked == true)
            {
                _popup.IsOpen = true;

                GenerateCalendar(SelectedDate ?? DateTime.Now);
            }
        }

        private void _listbox_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            if (_listbox.SelectedItem is CalendarBoxItem selected)
            {
                SelectedDate = selected.Date;
                GenerateCalendar(selected.Date);

                _popup.IsOpen = KeepPopupOpen; 
            }
        }
        private void GenerateCalendar(DateTime current)
        {
            if (current.ToString("yyyyMM") == CurrentMonth.ToString("yyyyMM")) return;

            CurrentMonth = current;
            _listbox.Items.Clear();
            DateTime fDayOfMonth = new(current.Year,current.Month,1);
            DateTime lDayOfMonth = fDayOfMonth.AddMonths(1).AddDays(-1);

            int fOffset = (int)fDayOfMonth.DayOfWeek;
            int lOffset = 6 - (int)lDayOfMonth.DayOfWeek;

            DateTime fDay = fDayOfMonth.AddDays(-fOffset);
            DateTime lDay = lDayOfMonth.AddDays(lOffset);

            for (DateTime day = fDay; day <= lDay; day = day.AddDays(1))
            {
                CalendarBoxItem boxItem = new();
                boxItem.Date = day;
                boxItem.DateFormat = day.ToString("yyyyMMdd");
                boxItem.Content = day.Day;
                boxItem.IsCurrentMonth = day.Month == current.Month;

                _listbox.Items.Add(boxItem);
            }
            if (SelectedDate != null)
            {
                _listbox.SelectedValue = SelectedDate.Value.ToString("yyyyMMdd");
            }
        }
    }
}

首先,仔细检查DependencyProperty,包括基本属性,例如维护所选日期的SelectedDateKeepPopupOpen属性确定在选择日期后是否使窗口保持打开状态,而CurrentMonth属性(标准DatePicker控件中看不到的DateTime属性)保留当前月份的位置,以便于在日历月份中导航。

GenerateCalendar方法包含根据所选日期重新创建日历的逻辑。偏移计算部分在这里值得注意。当前日期设置日历显示,并且要包括上个月和下个月的预览日期,需要一个简单但至关重要的计算。

DateTime fDayOfMonth = new(current.Year,current.Month,1);
DateTime lDayOfMonth = fDayOfMonth.AddMonths(1).AddDays(-1);

int fOffset = (int)fDayOfMonth.DayOfWeek;
int lOffset = 6 - (int)lDayOfMonth.DayOfWeek;

DateTime fDay = fDayOfMonth.AddDays(-fOffset);
DateTime lDay = lDayOfMonth.AddDays(lOffset);

在事件处理方面,日历选择事件利用MouseLeftButtonUp来与典型的按钮单击行为保持一致。这很恰当,因为如果再次选择selected值,则不会触发SelectionChanged事件,因此它不适合此上下文。

ToggleButtonIsChecked状态、PopupIsOpenClose功能之间的交互都是通过事件实现的,提供了一个全面的交互机制,有利于通过直接实现进行学习。

其他实现

此应用程序专为教程目的而设计,允许进一步的功能扩展,例如时间选择或手动值调整。在此框架内,实施针对特定客户要求量身定制的日历显示也是可行的。

SmartDate实现教程和源代码简介

实现SmartDate控件的整个过程可以在 YouTubeBilibili上的教程视频中找到,并且可以在GitHub上查看。这些视频时长只有50多分钟,历时两个月开发,同时平衡其他专业职责,使其成为免费提供的高质量教育资源。建议以充足的时间和耐心来完成这些教程,以确保全面学习。

https://www.codeproject.com/Articles/5381829/Enhancing-WPF-DatePicker-Issues-and-Solutions-via

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值