目录
SmartDate的关键绑定属性(DependencyProperty)
识别WPF DatePicker的问题
WPF DatePicker是WPF中的核心控件之一,已有近20年的历史。与Button、TextBoxes或CheckBoxes等更简单的控件相比,DatePicker具有更复杂的结构和阶段,由多个控件组成。这种复杂性需要高度的自定义专业知识,因此难以使用或修改提供的过时控件。
了解WPF DatePicker
分析和理解DatePicker的结构及其内部元素在模板中的交互,对于增强WPF中的基本设计和分析技能非常有益。这适用于所有WPF控件,而不仅仅是DatePicker。但是,由于DatePicker是根据过时的趋势设计的,因此基于基本Control实现新的CustomControl可能更有效。
源代码下载和设置
本文确定了使用基本DatePicker的问题,并演示了如何使用CustomControl方法重新设计它。通过GitHub下载源代码以直接检查结果并与本文一起阅读也是有益的。
首先,使用以下git命令下载源代码:
git clone https://github.com/vickyqu115/smartdate
接下来,要从源代码运行解决方案文件,您需要一个具有Windows 10或更高版本、Visual Studio 2022或Rider以及.NET 8.0的环境。
SmartDate.sln
项目结构
SmartDate由两个项目组成:
- SmartDateControl智能日期控制
- SmartDat应用程序
SmartDateControl是一个CustomControl库,它包括SmartDate类以及所有其他从属CustomControl类。SmartDateApp是一个简单的应用程序项目,用于指导用户如何使用此控件。
声明和使用SmartDate
用法很简单。使用xmlns声明命名空间,并使用SmartDate,就像标准DatePicker一样。
<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并使用相同的DateTime?type作为DatePicker的SelectedDate。
执行结果
CustomControl的定义和使用
我之前已经通过CodeProject上的四篇文章详细讨论了CustomControls背后的技术。如果您需要了解和掌握CustomControls,请参考这些文章。特别是,有关RiotSlider的文章深入探讨了WPF CustomControls的体系结构,因此,如果您还没有阅读它,我强烈建议您阅读它。
回到主要讨论,让我们定义CustomControl。通常,CustomControl以从Control派生的类为目标,但实际上,它包括从DependencyObject派生的所有类,而不仅仅是那些继承Control的类(如Panel)以及Visuals(如Animations)。但是,如前所述,只有在可以使用模板或至少使用DataContext的层中实现CustomControls才有意义。因此,在CustomControl样式中实现从FrameworkElement派生的类被视为明智的。
设计一个新的DatePicker:SmartDate
本文将详细介绍如何实现一个名为SmartDate的新CustomControl,该控件派生自最基本的类Control,而不使用现有的DatePicker。
选择对ContentControl的控制
首先,让我们看看ContentControl和Control之间的区别。ContentControl不仅提供基本的Template,还提供Content和ContentTemplate的属性。这些属性通过ContentPresenter自动链接,从而自动设置ContentPresenter、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意味着必须将所有相关logic分离为交互式实现的组件。虽然这看起来很实用,但做出正确的判断至关重要。
SmartDate的关键绑定属性(DependencyProperty)
此控件包括名为SelectedDate的DateTime?类型的绑定属性。由于默认值可能为null,因此将其声明为可为null的类型,用于设置通过日历选择的日期值。
SmartDate模板设计
ControlTemplat设计中必须包含的基本组件如下:
- Popup
- ListBox
- ToggleButton
Popup充当一个面板,其中包含ListBox(即日历),而ListBox使用内部ItemsPanel通过UniformGrid实现日历。ToggleButton用作日历图标,切换该按钮会更改Popup的IsOpen属性以控制日历窗口。此设置在基本DatePicker控件中也类似,因此将其与DatePicker的实际开源代码进行比较非常有益。
现在,让我们看看SmartDate控件在其Template中的结构。
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继承的日历切换按钮。最后,从ListBox继承的CalendarBox用作在日历上选择日期的列表控件。
此外,还包括用于导航到上个月和下个月的按钮、用于显示当前月份的TextBlock以及用于显示星期几的设计元素。
CustomControl不用于重用,仅供内部使用
SmartDate控件不仅由其自身使用,而且还在其Template中使用CustomControls。并非所有CustomControls都用于通用控件实现。在像SmartDate这样的情况下,它们是为特定目的实现的,从WPF体系结构的角度来看,这是一种常见的做法。
此类控件通常归类在命名空间“Primitives”下。此类别包括ToggleButton、Thumb和ScrollBar等控件,这些控件通常不直接使用,而是在其他控件的内部使用。
根据有关WPF的这些体系结构事实,可以看出SmartDate控件的Template的结构与WPF的基本模式没有显著差异。
了解PART_控制项及其角色
CustomControl结构不会像UserControls那样自动连接代码和XAML。因此,两者之间的所有交互都由_PART控件完全管理。
预定义的_PART控件包括:
- PART_Switch
- PART_ListBox
- PART_Left
- PART_Right
这些是在覆盖SmartDate类的OnApplyTemplate方法期间分配的,其中实现了所有必要的过程,例如按钮事件和日期生成。最好在通过OnApplyTemplate传递时用PART_前缀命名控件。此外,在XAML中命名这些元素的方式,使开发人员能够根据PART_名称预测类中发生的进程,这将是示例。
SmartDate.cs源代码
接下来,我们将检查SmartDate.cs类文件中包含的核心实现。需要关注的关键领域包括:
- 声明的DependencyProperty
- 通过OnApplyTemplate定义PART_元素
- 通过SelectedDate属性的日期选择控制逻辑
- CalendarBox中SelectedItem/SelectedValue的利用率
SmartDate:CustomControl
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(null));
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(null));
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 != null)
{
_listbox.SelectedValue = SelectedDate.Value.ToString("yyyyMMdd");
}
}
}
}
首先,仔细检查DependencyProperty,包括基本属性,例如维护所选日期的SelectedDate。KeepPopupOpen属性确定在选择日期后是否使窗口保持打开状态,而CurrentMonth属性(标准DatePicker控件中看不到的DateTime属性)保留当前月份的位置,以便于在日历月份中导航。
GenerateCalendar方法包含根据所选日期重新创建日历的逻辑。偏移计算部分在这里值得注意。当前日期设置日历显示,并且要包括上个月和下个月的预览日期,需要一个简单但至关重要的计算。
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来与典型的按钮单击行为保持一致。这很恰当,因为如果再次选择selected值,则不会触发SelectionChanged事件,因此它不适合此上下文。
ToggleButton的IsChecked状态、Popup的IsOpen和Close功能之间的交互都是通过事件实现的,提供了一个全面的交互机制,有利于通过直接实现进行学习。
其他实现
此应用程序专为教程目的而设计,允许进一步的功能扩展,例如时间选择或手动值调整。在此框架内,实施针对特定客户要求量身定制的日历显示也是可行的。
SmartDate实现教程和源代码简介
实现SmartDate控件的整个过程可以在 YouTube和Bilibili上的教程视频中找到,并且可以在GitHub上查看。这些视频时长只有50多分钟,历时两个月开发,同时平衡其他专业职责,使其成为免费提供的高质量教育资源。建议以充足的时间和耐心来完成这些教程,以确保全面学习。
https://www.codeproject.com/Articles/5381829/Enhancing-WPF-DatePicker-Issues-and-Solutions-via