万普插件库

jQuery插件大全与特效教程

实现现代化 WPF 日期选择控件 SmartDate

实现现代化 WPF 日期选择控件 SmartDate

控件名称:SmartDate

作者:WPFDevelopersOrg - Vicky&James

源码链接[1]
:https://github.com/vickyqu115/smartdate

教学视频[2](【小李趣味多】https://bit.ly/3xI9DNh)

这篇文章是对 WPF SmartDate教程视频的技术回顾。

WPF DatePicker 的问题认知

WPF DatePickerWPF中历史悠久的基本控件之一,已经有近20年的历史。相比简单的Button、TextBox、CheckBox等控件,DatePicker内部结构和操作步骤更加复杂,由多个控件组成。因此,进行自定义需要高水平的技能和技术,直接使用或自定义这一老旧控件相当困难。

WPF DatePicker 的理解

分析和理解 DatePicker的结构及模板中各内部元素的交互,是提升WPF设计和分析能力的有益案例。这不仅适用于DatePicker,还适用于所有WPF控件。然而,DatePicker的设计是在很多年前,与现在更加推荐的编程方式有所不同,因此在这样的环境下,根据项目的具体需求,通过CustomControl重新构建一个DatePicker控件可能是更加有效的方式。

下载和准备源码

本文介绍了如何识别基础 DatePicker的使用问题,并通过CustomControl方法重新设计。你可以通过GitHub下载源码并查看结果,同时结合本文阅读将会更有帮助。

首先,通过以下命令从 GitHub下载源码:

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

接下来,要运行源码的解决方案文件,需要在 Windows 10以上的环境中使用Visual Studio 2022Rider以及.NET 8.0版本。

SmartDate.sln

项目结构

SmartDate由两个项目组成:

  • SmartDateControl:CustomControl库,包含SmartDate类及所有子CustomControl类。

  • SmartDateApp: 一个简单的应用程序项目,展示如何使用这个控件。

SmartDate 的声明与使用方法

使用方法非常简单。通过 xmlns声明命名空间,并像使用传统DatePicker一样使用SmartDate

<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>

SelectedDate是一个DependencyProperty,与DatePickerSelectedDate相同,类型为DateTime?

运行结果

CustomControl 的定义与应用

开始定义 CustomControl。通常,CustomControl是从Control派生的类,但实际上,所有从DependencyObject派生的类都可以包括在内。然而,只有那些可以利用Template或至少可以利用DataContext的层次结构才有意义。因此,从FrameworkElement派生的类更适合用于CustomControl的实现。

设计新的 DatePicker: SmartDate

本文详细说明了如何实现一个从基本类 Control派生的新的CustomControlSmartDate,而不是使用现有的DatePicker

选择 Control 而非 ContentControl 的原因

首先,了解 ContentControlControl的区别。ContentControl除了提供基本模板外,还提供ContentContentTemplate属性。ContentPresenter通过DataTemplate自动连接 Content 和 ContentTemplate,因此无需手动设置它们之间的关系。总结来说,根据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会将相关逻辑分离成独立的交互实现,看似实用,但需要慎重判断。

SmartDate 的主要绑定属性(DependencyProperty)

这个控件包括一个名为 SelectedDate的绑定属性,类型为DateTime?。由于默认值可以为空,因此声明为able类型,用于指定通过日历选择的日期值。

SmartDate 模板设计

ControlTemplate设计中必需的基本组成部分如下:

  • Popup

  • ListBox

  • ToggleButton

Popup用于包含ListBox,即日历;ListBox通过ItemsPanelTemplate使用UniformGrid实现日历;ToggleButton以日历图标表示,当按钮切换时,PopupIsOpen属性也会改变,从而控制日历窗口。这种结构不仅在SmartDate控件中适用,在基本的DatePicker控件中也类似,因此对比DatePicker的开源代码非常有益。

下面是 SmartDate控件的模板结构。

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继承的日历切换按钮。CalendarBox继承自ListBox,用于选择日历日期。

其他组成部分包括用于切换到上一个月或下一个月的按钮、显示当前月份的 TextBlock以及用于显示星期几的设计元素。

非重用性内部专用 CustomControl

SmartDate控件不仅可以独立使用,也可以在模板内部实现为CustomControl。并非所有CustomControl都以通用控件为目的。SmartDate具有特定的用途,这在WPF架构中是很常见的。

这种性质的控件通常归类为 'Primitives' 命名

空间。ToggleButtonThumbScrollBar等控件通常在其他控件的内部使用。

基于这种 WPF架构事实,可以看出SmartDate控件的模板设计与WPF基本模式没有太大区别。

理解 PART_ 控件项及其作用

CustomControl结构中,代码与XAML之间没有自动连接功能。两者的交互完全依赖于_PART控件。

常用的 _PART控件包括:

  • PART_Switch

  • PART_ListBox

  • PART_Left

  • PART_Right

这些控件在 SmartDate类的OnApplyTemplate方法中传递,处理按钮事件、日期生成等所有必要操作。通过OnApplyTemplate接收的控件名称最好使用PART_前缀命名,以便在XAML中预见类内部的处理逻辑。

SmartDate.cs 源代码

以下是包含 CustomControl核心实现的SmartDate.cs类文件,特别重要的部分包括:

  • 声明的 DependencyProperty

  • 通过 OnApplyTemplate 定义 PART_ 元素

  • 通过 SelectedDate 属性控制日历选择逻辑

  • 使用 CalendarBox 的
    SelectedItem/SelectedValue

  • CustomControl: SmartDate.cs

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);

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);

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 != )
{
_listbox.SelectedValue = SelectedDate.Value.ToString("yyyyMMdd");
}
}
}
}

首先,查看 DependencyProperty,包括最重要的SelectedDate,以及保持弹出窗口打开的KeepPopupOpen属性和记录当前月份的CurrentMonth属性。这些属性在基础DatePicker中是不存在的。

GenerateCalendar方法包含了根据选择日期生成新日历的逻辑。值得注意的是Offset计算部分。根据当前日期生成日历时包含前后月份的日期,这部分逻辑是日历生成的关键。

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处理日历选择事件,匹配一般按钮点击操作。相比SelectionChanged事件,选择相同值时不会触发事件,这样的处理方式更为适合。

ToggleButtonIsCheckedPopupIsOpen及其关闭相关的交互通过事件实现。这些复杂的交互最好通过实际实现进行学习。

关于扩展实现

这个应用程序是为教程制作的代码,可以通过添加功能进行扩展。比如添加时间选择功能或手动更改值。也可以根据客户需求实现自定义日历显示。

SmartDate 实现的 WPF 教程视频及源码介绍

SmartDate控件的全部实现过程可以通过BiliBili视频查看,也可以在GitHub上找到。这些视频时长约50分钟,制作耗时 近1个月。作为高质量的免费教学资源,建议大家花足够的时间慢慢反复练习和学习。

沟通与支持

我们随时保持沟通渠道开放。大家可以通过以下方式与我们互动:

  • GitHub[3]: 关注、Fork、Stars

  • BiliBili[4]: 一键三连

  • 邮箱: james@jamesnet.dev

参考资料

[1]

源码链接:
https://github.com/vickyqu115/smartdate

[2]

教学视频: https://bit.ly/3xI9DNh

[3]

GitHub: https://github.com/vickyqu115/smartdate

[4]

BiliBili: https://bit.ly/3xI9DNh

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言