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

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

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

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


相关推荐

2023年最新微信小程序抓包教程(微信小程序 抓包)

声明:本公众号大部分文章来自作者日常学习笔记,部分文章经作者授权及其他公众号白名单转载。未经授权严禁转载。如需转载,请联系开百。请不要利用文章中的相关技术从事非法测试。由此产生的任何不良后果与文...

测试人员必看的软件测试面试文档(软件测试面试怎么说)

前言又到了毕业季,我们将会迎来许多需要面试的小伙伴,在这里呢笔者给从事软件测试的小伙伴准备了一份顶级的面试文档。1、什么是bug?bug由哪些字段(要素)组成?1)将在电脑系统或程序中,隐藏着的...

复活,视频号一键下载,有手就会,长期更新(2023-12-21)

视频号下载的话题,也算是流量密码了。但也是比较麻烦的问题,频频失效不说,使用方法也难以入手。今天,奶酪就来讲讲视频号下载的新方案,更关键的是,它们有手就会有用,最后一个方法万能。实测2023-12-...

新款HTTP代理抓包工具Proxyman(界面美观、功能强大)

不论是普通的前后端开发人员,还是做爬虫、逆向的爬虫工程师和安全逆向工程,必不可少会使用的一种工具就是HTTP抓包工具。说到抓包工具,脱口而出的肯定是浏览器F12开发者调试界面、Charles(青花瓷)...

使用Charles工具对手机进行HTTPS抓包

本次用到的工具:Charles、雷电模拟器。比较常用的抓包工具有fiddler和Charles,今天讲Charles如何对手机端的HTTS包进行抓包。fiddler抓包工具不做讲解,网上有很多fidd...

苹果手机下载 TikTok 旧版本安装包教程

目前苹果手机能在国内免拔卡使用的TikTok版本只有21.1.0版本,而AppStore是高于21.1.0版本,本次教程就是解决如何下载TikTok旧版本安装包。前期准备准备美区...

【0基础学爬虫】爬虫基础之抓包工具的使用

大数据时代,各行各业对数据采集的需求日益增多,网络爬虫的运用也更为广泛,越来越多的人开始学习网络爬虫这项技术,K哥爬虫此前已经推出不少爬虫进阶、逆向相关文章,为实现从易到难全方位覆盖,特设【0基础学爬...

防止应用调试分析IP被扫描加固实战教程

防止应用调试分析IP被扫描加固实战教程一、概述在当今数字化时代,应用程序的安全性已成为开发者关注的焦点。特别是在应用调试过程中,保护应用的网络安全显得尤为重要。为了防止应用调试过程中IP被扫描和潜在的...

一文了解 Telerik Test Studio 测试神器

1.简介TelerikTestStudio(以下称TestStudio)是一个易于使用的自动化测试工具,可用于Web、WPF应用的界面功能测试,也可以用于API测试,以及负载和性能测试。Te...

HLS实战之Wireshark抓包分析(wireshark抓包总结)

0.引言Wireshark(前称Ethereal)是一个网络封包分析软件。网络封包分析软件的功能是撷取网络封包,并尽可能显示出最为详细的网络封包资料。Wireshark使用WinPCAP作为接口,直接...

信息安全之HTTPS协议详解(加密方式、证书原理、中间人攻击 )

HTTPS协议详解(加密方式、证书原理、中间人攻击)HTTPS协议的加密方式有哪些?HTTPS证书的原理是什么?如何防止中间人攻击?一:HTTPS基本介绍:1.HTTPS是什么:HTTPS也是一个...

Fiddler 怎么抓取手机APP:抖音、小程序、小红书数据接口

使用Fiddler抓取移动应用程序(APP)的数据接口需要进行以下步骤:首先,确保手机与计算机连接在同一网络下。在计算机上安装Fiddler工具,并打开它。将手机的代理设置为Fiddler代理。具体方...

python爬虫教程:教你通过 Fiddler 进行手机抓包

今天要说说怎么在我们的手机抓包有时候我们想对请求的数据或者响应的数据进行篡改怎么做呢?我们经常在用的手机手机里面的数据怎么对它抓包呢?那么...接下来就是学习python的正确姿势我们要用到一款强...

Fiddler入门教程全家桶,建议收藏

学习Fiddler工具之前,我们先了解一下Fiddler工具的特点,Fiddler能做什么?如何使用Fidder捕获数据包、修改请求、模拟客户端向服务端发送请求、实施越权的安全性测试等相关知识。本章节...

fiddler如何抓取https请求实现手机抓包(100%成功解决)

一、HTTP协议和HTTPS协议。(1)HTTPS协议=HTTP协议+SSL协议,默认端口:443(2)HTTP协议(HyperTextTransferProtocol):超文本传输协议。默认...