百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

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

liebian365 2025-01-26 23:11 18 浏览 0 评论

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


相关推荐

4万多吨豪华游轮遇险 竟是因为这个原因……

(观察者网讯)4.7万吨豪华游轮搁浅,竟是因为油量太低?据观察者网此前报道,挪威游轮“维京天空”号上周六(23日)在挪威近海发生引擎故障搁浅。船上载有1300多人,其中28人受伤住院。经过数天的调...

“菜鸟黑客”必用兵器之“渗透测试篇二”

"菜鸟黑客"必用兵器之"渗透测试篇二"上篇文章主要针对伙伴们对"渗透测试"应该如何学习?"渗透测试"的基本流程?本篇文章继续上次的分享,接着介绍一下黑客们常用的渗透测试工具有哪些?以及用实验环境让大家...

科幻春晚丨《震动羽翼说“Hello”》两万年星间飞行,探测器对地球的最终告白

作者|藤井太洋译者|祝力新【编者按】2021年科幻春晚的最后一篇小说,来自大家喜爱的日本科幻作家藤井太洋。小说将视角放在一颗太空探测器上,延续了他一贯的浪漫风格。...

麦子陪你做作业(二):KEGG通路数据库的正确打开姿势

作者:麦子KEGG是通路数据库中最庞大的,涵盖基因组网络信息,主要注释基因的功能和调控关系。当我们选到了合适的候选分子,单变量研究也已做完,接着研究机制的时便可使用到它。你需要了解你的分子目前已有哪些...

知存科技王绍迪:突破存储墙瓶颈,详解存算一体架构优势

智东西(公众号:zhidxcom)编辑|韦世玮智东西6月5日消息,近日,在落幕不久的GTIC2021嵌入式AI创新峰会上,知存科技CEO王绍迪博士以《存算一体AI芯片:AIoT设备的算力新选择》...

每日新闻播报(September 14)_每日新闻播报英文

AnOscarstatuestandscoveredwithplasticduringpreparationsleadinguptothe87thAcademyAward...

香港新巴城巴开放实时到站数据 供科技界研发使用

中新网3月22日电据香港《明报》报道,香港特区政府致力推动智慧城市,鼓励公私营机构开放数据,以便科技界研发使用。香港运输署21日与新巴及城巴(两巴)公司签署谅解备忘录,两巴将于2019年第3季度,开...

5款不容错过的APP: Red Bull Alert,Flipagram,WifiMapper

本周有不少非常出色的app推出,鸵鸟电台做了一个小合集。亮相本周榜单的有WifiMapper's安卓版的app,其中包含了RedBull的一款新型闹钟,还有一款可爱的怪物主题益智游戏。一起来看看我...

Qt动画效果展示_qt显示图片

今天在这篇博文中,主要实践Qt动画,做一个实例来讲解Qt动画使用,其界面如下图所示(由于没有录制为gif动画图片,所以请各位下载查看效果):该程序使用应用程序单窗口,主窗口继承于QMainWindow...

如何从0到1设计实现一门自己的脚本语言

作者:dong...

三年级语文上册 仿写句子 需要的直接下载打印吧

描写秋天的好句好段1.秋天来了,山野变成了美丽的图画。苹果露出红红的脸庞,梨树挂起金黄的灯笼,高粱举起了燃烧的火把。大雁在天空一会儿写“人”字,一会儿写“一”字。2.花园里,菊花争奇斗艳,红的似火,粉...

C++|那些一看就很简洁、优雅、经典的小代码段

目录0等概率随机洗牌:1大小写转换2字符串复制...

二年级上册语文必考句子仿写,家长打印,孩子照着练

二年级上册语文必考句子仿写,家长打印,孩子照着练。具体如下:...

一年级语文上 句子专项练习(可打印)

...

亲自上阵!C++ 大佬深度“剧透”:C++26 将如何在代码生成上对抗 Rust?

...

取消回复欢迎 发表评论: