百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT知识 > 正文

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

liuian 2025-04-08 16:02 39 浏览

实现现代化 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.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 派生的新的 CustomControl SmartDate,而不是使用现有的 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


相关推荐

优酷视频免费下载并安装(下载优酷视频安装)

很高兴回答你的问题!1.首先在电脑上打开优酷。2.点击右上角的三道杠点击“设置”。3.点击“转码设置”。4.将转码格式改成mp4。5.打开要下载的视频,点击下载按钮。6.勾选“下载完后自动转码”。优酷...

u盘变成系统盘怎么变回来(u盘变成系统盘还能用吗)
  • u盘变成系统盘怎么变回来(u盘变成系统盘还能用吗)
  • u盘变成系统盘怎么变回来(u盘变成系统盘还能用吗)
  • u盘变成系统盘怎么变回来(u盘变成系统盘还能用吗)
  • u盘变成系统盘怎么变回来(u盘变成系统盘还能用吗)
360浏览器官网电脑版(360浏览器 官网)

网页版的入口,你的意思是说如何打开嘛?它安装完了之后。就会在桌面上出现一个图标,你直接点击不就可以了吗?1、首先打开电脑浏览器。2、然后在浏览器上搜索搜索360网盘。3、然后就可以看到网页版登录入口。...

服务器重装系统(服务器重装系统按什么键)

进入服务器之后选择清除系统重新安装即可如果确实忘记了服务器电脑密码,可以尝试使用重装系统的方式来解决问题。首先需要准备一个可启动的系统安装盘或U盘,然后在服务器开机时进入BIOS设置,将启动设备设为安...

win11下载一半可以取消吗(win11下载两次)

1.首先回到桌面,右键单击鼠标开始win徽标,右击菜单点击运行,或者直接WIN+R组合键。  2.跳出运行对话框,输入services.msc并单击OK按钮。3.转到服务列表,找到Windowsup...

windows7怎么进入bios(windows7怎么进入管理员界面)

1、开机时按F2键或者DEL键,进入BIOS系统;注:机器型号不同,进入BIOS的按键可能有所不同,具体可参看左下角的屏幕提示。2、选择Security选项卡,选择SecureBoot,按回车键——...

截图快捷键ctrl加什么电脑截图

ctrl+alt+a是qq的截图快捷键;台式电脑还可以使用的截图方式:方法一:按PrtScSysRq键,然后在文档中右击选择粘贴就可以看见截图,将截图另存为即可获得截图;方法二:按win+PrtScS...

手机怎样提高网速(手机怎样提高网速华为)
  • 手机怎样提高网速(手机怎样提高网速华为)
  • 手机怎样提高网速(手机怎样提高网速华为)
  • 手机怎样提高网速(手机怎样提高网速华为)
  • 手机怎样提高网速(手机怎样提高网速华为)
ios官方网站(苏州晶体公司ios官方网站)
ios官方网站(苏州晶体公司ios官方网站)

方法/步骤1,点击下方的【safari】图标。2,搜索苹果官网,点击进入3,进入苹果的页面,点击左上角二道横4.查询苹果的相关产品。1.打开苹果官网:http://www.apple.com.cn/并点击页面右上角的技术支持选项。2.选择您...

2025-11-10 09:55 liuian

手机突然无法识别u盘(手机突然无法识别u盘怎么办呢)

1、手机不支持OTG功能,所以将U盘连接到手机后,手机无法识别U盘的内容,因此显示不了;这种情况只能换台支持OTG功能的手机来连接U盘才行。2、手机支持OTG功能,但是使用的OTG线质量有问题导致无...

windows10更新不了一直重试(window10一直更新失败)

可能是以下几个原因导致的:1.可能是硬盘剩余空间太少或者碎片太多,队伍用文件进行清理并清理碎片即可。2.可能是windows10版本不支持软件进行运行。3.没有权限,打开相应的权限后重启即可情况说明你...

联想笔记本怎么进入安全模式
联想笔记本怎么进入安全模式

联想笔记本电脑进入安全模式的方法如下:1、第一步,按下【windows+R】,打开【运行】,输入【msconfig】后,点击【确定】。2、第二步,打开【系统配置】窗口后,点击【引导】。3、第三步,勾选【安全引导】后,选择需要的安全模式,通常...

2025-11-10 08:05 liuian

winxp升级包下载(xp 升级)

题主你好,XP系统要升级成WIN7很简单,方法如下:1,下载win7系统iso镜像到本地硬盘,右键使用WinRAR等工具解压出来2,将最大的win7.gho文件和Onekeyghost工具放到同一目...

windows 7电脑配置要求(windows7电脑配置要求)

官方推荐最低配置:处理器:1GHz32位或者64位处理器内存:1GB及以上显卡:支持DirectX9128M及以上(开启AERO效果)硬盘空间:32位16G以上(主分区,NTFS格式)...

ie主页被360锁定不能修改(ie浏览器首页被锁定360导航怎么取消百度知道)

法一、点击开始,运行,键入msconfig点击“确定”,在弹出的窗口中切换到“启动”选项卡,禁用可疑程序启动项。法二、1、打开360安全卫士进入“更多”;2、主页防护;3、在打开的对话框中进行设...