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

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

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

实现现代化 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


相关推荐

GCI: Another key public good for international community

MembersofadelegationofhighschoolstudentsfromtheU.S.stateofWashingtonposeforaphotoa...

kube on kube 实现思路分享(kube-scheduler)

这里的kubeonkube,是指建立K8s元集群,纳管其他业务K8s集群,通过声明式API管理集群的创建、增删节点等。参考https://github.com/kubean-i...

China and India hold the key to a more inclusive global future

ByMayaMajueranLead:AsChinaandIndiamark75yearsofdiplomaticties,theircooperationcouldse...

日本真子公主的婚礼又要提上日程了吗?未婚夫:债务问题已解决

日本明仁天皇将于今年3月31日退位,德仁皇太子即将成为新一任的天皇。在平成时代最后的倒计时中,明仁天皇的孙女真子公主的婚事却又一次进入了人们的视野。(viaTheTelegraph)关注日本皇室的...

kratos源码分析系列(1)(kvm源码解析与应用 pdf)

https://github.com/go-kratos/kratos是b站开源的一个微服务框架,整体来看它结合grpc生态中的grpc-gateway,以及wire依赖注入和众多常用的trace,m...

【2.C#基础】6.循环语句(c#循环语句例子)

6.循环语句当需要多次执行同一个处理时,就需要用到循环语句。一般情况下,循环的流程图如下:6.1while循环C#中的while循环语句在给定的条件为真的情况下会重复执行目标语句。格式如下:...

使用 Google Wire 在 Go 中进行依赖注入

关注点分离、松耦合系统和依赖反转原则等概念在软件工程中是众所周知的,并且在创建良好的计算机程序过程中至关重要。在本文中,我们将讨论一个同时应用了这三个原则的技术,称为依赖注入。我们将尽可能地实践,更加...

用 Golang封装你的API(golang封装dll)
用 Golang封装你的API(golang封装dll)

每日分享最新,最流行的软件开发知识与最新行业趋势,希望大家能够一键三连,多多支持,跪求关注,点赞,留言。@头条创作挑战赛本文探讨了在用Golang封装你的API的过程以及几个不同的编程步骤。我做了一个非常有限的时间来证明如何为客户正在开...

2025-05-09 20:03 liuian

Terraform 实战 | 万字长文(terrify是什么意思中文)

Terraform是什么Terraform(https://www.terraform.io/)是HashiCorp旗下的一款开源(Go语言开发)的DevOps基础架构资源管理运维工具,可...

Go 语言入门:环境安装(go语言安装 window)

一、前言这里不同于其他人的Go语言入门,环境安装我向来注重配置,比如依赖包、缓存的默认目录。因为前期不弄好,后面要整理又影响这影响那的,所以就干脆写成文章,方便后期捡起。二、安装1.安装包htt...

Go语言进阶之Go语言高性能Web框架Iris项目实战-项目结构优化EP05

前文再续,上一回我们完成了用户管理模块的CURD(增删改查)功能,功能层面,无甚大观,但有一个结构性的缺陷显而易见,那就是项目结构过度耦合,项目的耦合性(Coupling),也叫耦合度,进而言之,模块...

如何将Go项目与Docker结合实现高效部署

在现代软件开发中,使用Docker部署应用程序已经成为一种标准实践。本文将深入探讨如何将Go项目与Docker结合,实现高效、可靠的部署过程。通过详细的步骤和丰富的示例,你将能够迅速掌握这一流程。准备...

五分钟轻松熟悉一个k8s Operator应用制作

简介:operator是一种kubernetes的扩展形式,可以帮助用户以Kubernetes的声明式API风格自定义来管理应用及服务,operator已经成为分布式应用在k8s集群部...

程序员的副业秘籍!一款可以快速搭建各类系统的后台管理系统

系统简介这是一个基于Gin+Vue+ElementUI(或ArcoDesign、AntDesign)的系统快速开发平台,采用了前后端分离,旨在帮助用户快速完成各类系统的基础功能搭建。平...

使用 Go 语言开发区块链钱包的项目目录结构设计

在开发区块链钱包时,项目的目录结构应该清晰、模块化,确保代码的可维护性和扩展性。基于Go的惯例,结合区块链钱包的功能需求,以下是一个较为合理的目录结构示例:1.目录结构blockchain-wa...