WPF——自定义日历

文章目录

  • 一、引言
  • 二、自定义日历控件
    • 0. 历史
    • 1. 日历
    • 2. 为日历指定样式
    • 3. 设置新的日历模板
    • 4. 替换CalendarItem模板
    • ★5. 强大的CalendarDayButton模板
    • 6. 从Calendar派生
    • 7. DatePicker控件
  • 三、结尾

一、引言

想实现一个自定义的日历控件,
WPF——自定义日历_第1张图片
在每日方格中,可以加自定义的标记。比如当天任务完成打个√,没完成打个×。
但是看了看WPF自带的calendar控件,似乎没有办法简单地通过设置属性来搞定。用blend敲开后,发现结构很简单(感觉和Calendar不是一个东西),也没看出个所以然来。于是,去了网上搜了搜类似的控件,也没找到现成的包,网上修改的几个demo都是改的官方例子,动不动就一千行代码。想自己用ListBox或Grid写一个,感觉也比较麻烦,写出来还不稳定。

既然如此,还不如打开文档,一步步来学习Calendar,看看能否得到什么线索。这种时候,往往慢慢来,比较快。


二、自定义日历控件

0. 历史

先说说历史,当微软在2006年发布WPF时,所有人都认为它少了点什么,其中最明显的就是日历控件(calendar controls)。这很奇怪——毕竟,WinForms都有MonthCalendar和DateTimePicker控件,为什么不把它们直接移植到WPF上呢?

因为将控件移植到WPF并不是简单更改属性和事件名称就能达成的(如果这么简单,我现在也不用写这篇文章了)。WPF控件必须是“无外观的”(lookless)。

什么是无外观(lookless)?
如果你学过模板,就应该有这个概念,WPF的外观是通过控件模板(Control Template)来单独定义的。所以我只要将控件模板一换,外观就变了。所以不会说某个控件一定是什么样子的,也即控件是无(具体确定的)外观的。这种概念中,控件更像是一种更抽象的,具有某些功能的集合体,它的外观是可以由使用者来赋予的。

控件的可视化外观会分离到其他元素和控件的可视化树(Visual Tree)中去。该控件的默认模板必须以完全可替换的方式进行结构化和文档化(总之它是一种更抽象的、更上层的方式,不是说我要个钟,就画个钟就行了)。通过这种方式,开发人员和设计人员可以自定义控件,以赋予它们一个全新的外观。

设计一个带有可替换模板的控件是令人望而生畏的,正如某大佬在当年的文章《Templates for Uncommon Controls》中试图设计一个MonthCalendar控件时发现的那样。你希望使控件尽可能的一般化,以适应各种不同的外观,同时代码和模板之间的接口又不能过于复杂。而且Calendar还有另一个层次的复杂性,它严重依赖于文化习惯,而自定义模板不应该过分地去破坏这些习惯。

简单说,就是世界各地的日历是不一样的。我该怎么设计出一个通用的日历,使得世界各地的人都能接受,都能用它们的习惯来实现它的外观。

这可能就是WPF日历控件花费如此长时间的原因。好在后来,微软还是在WPF工具包(WPF Toolkit)中发布了Calendar和DatePicker控件(以及几个子类)。(还包括一个新的DataGrid,但这里不讨论它)。这些控件与Silverlight中的相同控件都兼容不错。

1. 日历

Calendar和DatePicker类(以及一些它们支持的枚举和委托)在Microsoft.Windows.Controls命名空间中。它的一些附属类(相关的,一起组成日历的类),主要是CalendarItem、CalendarButton、CalendarDayButton和DatePickerTextBox,在Microsoft.Windows.Controls.Primitives空间中。这些不是WPF控件惯用的命名空间(默认添加的),因此你可能需要在C#代码中添加新的using指令,在XAML文件中构造合适的XML命名空间声明(版本比较新的话应该是自带的,大概20年左右?)。和新的Visual State Manager相关的类有一个更普通的命名空间System.Windows。

Calendar类派生自Control。如果只是实例化Calendar,就会得到下图的效果。默认只显示一个月的信息——不会像WinForms的MonthCalendar那样显示多个月的。这个月总是包括含六周,即使只有四到五周。
WPF——自定义日历_第2张图片
点击顶部小标题(顶部表示月份和年份的部分),就会切换到下图内容。
WPF——自定义日历_第3张图片
你可以单击其中的某个月以返回该月的视图,或者再次单击标切换到下图。
WPF——自定义日历_第4张图片
这三张图的效果分别对应于Calendar的DisplayMode属性中的Month、Year和Decade枚举成员。

这三种模式中,标题两侧的两个按钮分别可按月、年或十年向前或向后导航。
在这里插入图片描述
DisplayDate属性的类型为DateTime,表示显示哪个月、年、十年。DisplayDateStart和DisplayDateEnd属性是可空的DateTime类型(nullable,DateTime?),它们会将日历的导航约束在特定范围内。

IsTodayHighlighted属性默认为true,它会在今天的日期后面绘制一个黑色的正方形。你也可以自己指定强调的日期。SelectionMode属性可以设置为CalendarSelectionMode枚举的成员:SingleDate(默认),SingleRange,MultipleRange或None。对于SingleDate模式,SelectedDate属性(类型为nullable DateTime)表示选择的日期;其他模式,SelectedDates属性的类型为SelectedDatesCollection,它派生自DateTime类型的ObservableCollection(ObservableCollection)。

BlackoutDates属性可以禁止所选日期段。该属性类型为CalendarBlackoutDateCollection,它派生自CalendarDateRange类型的ObservableCollection,该类定义了DateTime类型的Start和End属性(就是定义了开始和结束的时间,该时间段是被禁止的)。

FirstDayOfWeek属性是DayOfWeek类型的;默认值通常是周日(Sunday),但在某些地区(最著名的是法国,我还以为是中国),默认时间是周一(Monday)。
在这里插入图片描述
Calendar定义了四个事件:DisplayModeChanged,DisplayDateChanged,SelectionModeChanged和SelectedDatesChanged。

2. 为日历指定样式

Calendar类的其余公有属性为CalendarItemStyle、CalendarButtonStyle和CalendarDayButtonStyle,它们都是Style类型的,但要理解这些属性,你需要知道Calendar控件是如何构建和构造的(得知道这些样式分别修饰Calendar哪部分),为了更好地了解这些属性,你可以看一下XAML中的它们的代码。

Calendar控件的所有源码都可以从CodePlex站点下载,如果你对定制日历控件感兴趣,那么最重要的文件无疑是Generic.xaml,它位于其子目录Toolkit-release\Calendar\Themes中。

CodePlex是微软的一个开源站,可惜现在似乎没了。不过现在集成到WPF中的控件都可以用Blend来打开获取到XAML源码。
WPF——自定义日历_第5张图片

Generic.xaml文件包含了Calendar控件本身和其他组成Calendar控件的控件的默认样式(包括模板)。如果你需要自定义Calendar模板,那你就得好好学习Generic.xaml了。

尽管Calendar类定义了控件的所有属性和事件,并且还执行了一些用户输入处理(比如点标题,会进入年月选择界面就是处理了用户的输入),但是Generic.xaml中的Calendar模板显示,Calendar仅由一个不可见的StackPanel和一个CalendarItem控件组成。

我第一次用blend敲开Calendar后,看到的结构也非常简单,只有几十行XAML代码,完全不像Calendar类中描述的那么复杂。后来才知道,这是因为它的内部结构没有完全展开。

CalendarItem类也派生自Control,它包含了控件的整个可视化外观(也就是说,日历显示出来被你看到的东西都是在它里面定义的)。其中包括顶部的三个按钮(一般统称为导航按钮)。
在这里插入图片描述

它还包含两个网格。其中一个网格显示每月的天数(包括每个星期的天数)。另一个格子显示的是12个月或12年。在任何时候,这两个网格中只有一个是可见的。
WPF——自定义日历_第6张图片

// 所以CalendarItem的整体结构很简单
<Grid>
	<三个按钮/>
	
	<网格1 显示每月天数/>
	<网格2 显示12个月或12年/>
Grid>

在blend中继续敲开它还可以看到它的层次结构就如上面所描述那样,由三个顶部按钮和两个网格组成,
WPF——自定义日历_第7张图片
点开网格内容,发现是7*6的,这样看来日历的实现还是挺朴素的,只不过代码确实多了点,自己实现的话比较花时间。
WPF——自定义日历_第8张图片
CalendarItem不仅定义了两个网格,还会对它们进行填充。当显示月份的天数时,它会创建CalendarDayButton类型的对象;当显示12个月或年时,填充的对象类型为CalendarButton。CalendarDayButton和CalendarButton都派生自Button(由这段话也可知CalendarXXButton就是这些网格中的日子或者年月)。

Generic.xaml包含Calendar、CalendarItem、CalendarButton和CalendarDayButton的默认样式和模板。对于后三项,你可以通过设置Calendar定义的CalendarItemStyle、CalendarButtonStyle或CalendarDayButtonStyle属性来为它们设置新的样式(或模板),你可以直接设置或通过Calendar的Style属性来设置。

其实学到这里,如果你是想静态更改日历的样式,就可以通过更改这几个样式来实现了,但若要给某一天赋予特性,你还得知道怎么获取那一天的DateTime。

为日历本身设置样式对于日历专有(Calendar-specific)的属性(如DisplayMode或SelectionMode)或日历继承的属性(如HorizontalAlignment或Margin)是有用的,但是我们重点关注的往往是一些更有问题的属性:那些与字体和笔刷相关的属性(那些与日历外观有关的属性)。

Calendar控件应该从它附属的控件开始被精确地构建,如果你乱用字体,则该控件看起来会变得别扭。例如,你可以在日历上设置FontFamily属性,它会影响除了星期(day-of-week)标题外的所有文本(这些标题的FontFamily硬编码在CalendarItem的默认模板内的DateTemplate中)。然而,如果一个月的天数的文本变得小了一点,整个月看起来就会向左移动。

这段就是讲不要乱调属性,要注意控件整体的观感。里面某一个部分变动了会影响整体。

在Calendar上设置FontSize属性是无效的。如果你确实想改变字体大小,你必须在CalendarButton样式、CalendarDayButton样式和CalendarItem模板中的两个地方进行更改。所以,忘了它吧,不要改FontSize了。如果你想让Calendar控件大一点或小一点,就不要改FontSize了。我建议定义一个ScaleTransform并将其设置为Calendar的LayoutTransform属性,或将Calendar放在一个视图框(Viewbox)中。

Calendar用于标记当前日期、选中日期和禁止日期的颜色在各种模板中都是硬编码(hardcode)的。唯一可以轻松更改的笔刷是Calendar控件的Background属性和控件周围的BorderBrush。在Calendar的默认样式中,它们都被设置为线性渐变笔刷(linear gradient brushes),通过模板绑定传递给CalendarItem。如果你想替换默认的Background笔刷(后文会提到),要注意,它负责的是导航按钮后面的不同背景阴影。

3. 设置新的日历模板

为控件替换模板的第一步是查看类的定义,你可以从文档或源码中(如果有的话)查看定义。在里面你将看到许多TemplatePart属性。这些TemplatePart属性指明了一些控件或元素,你的新模板必须具有这些命名的元素。对于Calendar控件,一个新模板必须至少包含两个部分:名为PART_CalendarItem的CalendarItem和名为PART_Root的Panel(默认Calendar模板中,PART_Root面板是一个StackPanel)。
WPF——自定义日历_第9张图片
一般不太可能去替换Calendar的模板,但是也有例外。也许你想要在组成Calendar的可视化树中添加一些东西呢,比如一个可滚动的ItemsControl,它显示所有选定的日期。

下面是该场景(就是上面说的加个可滚动的条目控件)项目的MainWindow.xaml文件的摘录,其中包含了Calendar的新模板,

<ControlTemplate TargetType="toolkit:Calendar">
	<StackPanel Name="PART_Root" Orientation="Horizontal"> 
		<primitives:CalendarItem Name="PART_CalendarItem" Style="{TemplateBinding CalendarItemStyle}" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" VerticalAlignment="Center" />
		<Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Margin="4 4 0 4"> 
			<ScrollViewer VerticalScrollBarVisibility="Auto" Height="{Binding ElementName=PART_CalendarItem, Path=ActualHeight}" Width="100"> 
				<ItemsControl ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=toolkit:Calendar}, Path=SelectedDates}" />
			ScrollViewer> 
		Border> 
	StackPanel>
ControlTemplate>

下面是它的外观,
WPF——自定义日历_第10张图片
请注意应用CalendarItem控件所需的原语的XML命名空间前缀。在XAML文件的根元素中,该前缀与Microsoft.Windows.Controls.Primitives的.NET命名空间和WPFToolkit程序集相关。类似地,前缀工具包与同样的程序集中的Microsoft.Windows.Controls命名空间相关。Calendar在XAML中像下面这样被简单实例化:

<toolkit:Calendar />

包含新模板的样式将隐式应用。

这篇文档的内容是十几年前的,现在似乎已经没有这样的前缀了,或许合并了?

注意上面代码中,使用了Binding with RelativeSource来连接ItemsControl的ItemsSource属性和Calendar的SelectedDates属性。TemplateBinding不能用于此,因为SelectedDates没有依赖属性的支持。

那怎么知道我应该用TemplateBinding设置CalendarItem的哪些属性呢?查看Generic.xaml的默认Calendar模板。事实上,不仅应该查看它,还需要把它复制粘贴到你自己的源码中。复制粘贴恐怕是修改现有模板的最佳方式。

4. 替换CalendarItem模板

为了对Calendar控件的内部外观有更多的控制,就有必要替换CalendarItem的模板。默认模板是一大块XAML代码——在Generic.xaml文件中差不多有250行——它包含了在顶部的三个嵌入导航按钮模板。

经验丰富的WPF程序员如果还没有深入研究过Silverlight,会惊讶地发现这三个嵌入按钮模板中有一些陌生的东西:VisualStateManager、VisualStateGroup和VisualState类的引用。这些类是可视化状态管理器(Visual State Manager)的主要组件,它已经在Silverlight中了,并有望在下一个版本中成为WPF的一部分(不出意外,现在(2022)已经有了)。(WPF Toolkit中包含了这个初步可视化状态管理器的源码)

可视化状态管理器旨在替换模板中使用的许多触发器。模板触发器通常基于某些属性的值,如IsEnabled、IsMouseOver和IsSelected。相反,可视化状态管理器是基于可视化状态的一个更结构化的概念,具有像Disable、MouseOver和Selected等名称。乍一看,这些视觉状态似乎与触发器有一一对应的关系,但事实并非如此。在实际使用中,触发器会根据它们出现的顺序产生不同的视觉外观,当触发器需要相互协作时,定义多个触发器通常显得笨拙。视觉状态的替代识别使程序员的意图更加明显。属性设置和可视状态之间的关联发生在代码中。

要在XAML中引用Visual State Manager类,你需要定义一个附加的XML前缀(例如VSM),它与WPFToolkit程序集中的System.Windows命名空间相关联。

CalendarItem的自定义模板有八个命名部分:

Part Type
PART_Root FrameworkElement
PART_HeaderButton Button
PART_PreviousButton Button
PART_NextButton Button
DayTitleTemplate Key name of DataTemplate
PART_MonthView Grid
PART_YearView Grid
PART_DisabledVisual FrameworkElement

注意,这些名字中其中一个实际上是资源键名。CalendarItem模板的资源部分应该包含一个键名为“DayTitleTemplate”的DataTemplate,它包含一个TextBlock元素,Text属性设置如下:

Text="{Binding}"

该DateTemplate用于显示一周天数的标题。用于这些表头的CalendarDayButton项的Content设置为null,但是DataContext设置为文本表头(Su、Mo、Tu等等),因此出现了奇怪的绑定。

CalendarItem模板的默认可视化树由几个嵌套的网格(grid)组成。外部的单格网格(single-cell)名为PART_Root。该Grid包含一个名为PART_DisabledVisual的Border和Rectangle,用于在控件禁用时提供一个半透明的覆盖。

虽然这篇文章废话很多,大部分描述与文章开头的实现无关,但是这些废话中也可以学到一些小技巧。比如这边说的一个透明的Rectangle覆盖住,就形成了一个禁用的效果。官方的人也在这么用,平时一些常见的效果很多都由这样的小技巧实现。

Border包含了另一个Grid,该网格具有三列两行,提供控件的主要结构。三个命名的导航按钮占据最上面一行。底部一行包含两个网格(PART_MonthView和PART_YearView),它们横跨三列。(就如下图,1、2、3各一格,456合并为一格)
WPF——自定义日历_第11张图片
名为PART_MonthView的网格预计有七列(一周的七天)和七行(一列为星期标题,六行为每月的天数),而PART_YearView预计有四列和三行(一年中的12个月或“十年”视图中的12年)。当CalendarItem的代码部分创建CalendarDayButton和CalendarButton子元素来填充这些网格时,它显式地为每个子元素设置Grid.Row和Grid.Column附加属性。

由于这个原因,自定义CalendarItem模板不太可能与现有布局有显著差异。如果你想尝试一下修改它,BareBonesCalendarItem工程中的MainWindow.xaml文件有着许多你需要的最小的标记,你可以通过修改它们来实现你想要的功能。下图是一个修改后的效果,为了让它与默认不同,我用DockPanel构建了整体结构,因此previous-month和next-month的按钮出现在了两侧。
WPF——自定义日历_第12张图片
我没有给这些导航按钮应用模板,所以它们看起来像真正的原生按钮。即使进行了这样的简化,该日历模板的长度也接近80行。虽然效果看起来很奇怪,但它确实也能当日历用。

定义新的CalendarItem模板的更安全、更合理的方法是从Generic.xaml复制CalendarItem的默认模板,然后只修改它的某些部分

我在ColorfulCalendarItem工程中就是这么做的。MainWindow.xaml文件定义了一个local的XML命名空间前缀,用于引用Calendar类来与Generic.xaml中的命名空间声明保持一致。

在CalendarItem模板中,我为外部Border添加了一个新的Background画刷:

<LinearGradientBrush StartPoint="0 0" EndPoint="0 1"> <GradientStop Offset="0" Color="#FFFFC0" /> 
	<GradientStop Offset="0.5" Color="#FFE0B0" />
	<GradientStop Offset="1" Color="#FFD0A8" /> 
LinearGradientBrush>

我还在ContentPresenter的中心表头周围添加了一个新的Border,并给它一个新的Background:

<Border Padding="12 0" CornerRadius="6"> 
	<Border.Background> 
		<LinearGradientBrush StartPoint="0 0" EndPoint="0 1"> 
		<GradientStop Offset="0" Color="#FFC4A0" /> 
		<GradientStop Offset="1" Color="#FF9450" /> 
		LinearGradientBrush> 
	Border.Background>
Border>

生成的效果如下图:
WPF——自定义日历_第13张图片

★5. 强大的CalendarDayButton模板

CalendarItem模板包含了在顶部显示的三个导航按钮的嵌入模板,但是该模板没有对填充下层网格的CalendarButton和CalendarDayButton对象的引用。这些按钮是在代码中实例化的,你可以通过检查CalendarItem.cs文件注意到这一点。但是,你可以通过设置Style对象到CalendarButtonStyle和CalendarDayButtonStyle来为这些按钮定义新的模板

CalendarDayButton的自定义模板非常强大。在该模板中,你可以为每天显示(除基本的1-31这样的两位数外的)额外的信息。一开始,你似乎无法以一种聪明的方式来实现这一操作,因为添加到CalendarDayButton中的任何内容都可能依赖于该按钮表示的确切日期,而这个日期似乎不容易获得。

别担心。尽管CalendarDayButton的Content被设置为一个简单的文本字符串,例如24,但是每个CalendarDayButton的DataContext属性被设置为该日期的实际DateTime对象,包括正确的月和年。(该特性也有助于用于星期标题的CalendarDayButton对象;这些按钮的Content为空,但DataContext设置为文本字符串Su、Mo等。这就是为什么这些标题在CalendarItem中需要DataTemplate的原因)

DateTime对象的可用性拓展了整个日历可以变化空间,你只需要在新的CalendarDayButton模板中引入一些自定义的类就可以实现这些效果。因为默认的CalendarDayButton模板大约有120行XAML,所以你可能不会从头编写这样的模板。与CalendarItem模板一样,你可能希望从Generic.xaml复制默认的CalendarDayButton模板到你自己的项目中,然后修改它。

有种可能是在ToolTip中显示节假日和其他重要的日子,当你鼠标指针经过每天时,就会显示。将该特性与绑定转换器(binding converter)结合使用似乎是合理的:实际上,converter将DateTime对象转换为了String对象。

这正是我想要实现的,对某些日期添加特殊的标注。

下面代码展示了该场景下绑定转换器的重要部分。构造函数设置3月和6月之间的日期项。在实际程序中,它可能会从文件中访问这些日期和文本字符串。

public class RedLetterDayConverter: IValueConverter
{ 
	static Dictionary < DateTime, string > dict = new Dictionary < DateTime, string > ();
	static RedLetterDayConverter() 
	{ 
		dict.Add(new DateTime(2009, 3, 17), "St. Patrick's Day"); 
		dict.Add(new DateTime(2009, 3, 20), "First day of spring"); 
		dict.Add(new DateTime(2009, 4, 1), "April Fools"); 
		dict.Add(new DateTime(2009, 4, 22), "Earth Day"); 
		dict.Add(new DateTime(2009, 5, 1), "May Day"); 
		dict.Add(new DateTime(2009, 5, 10), "Mother's Day"); 
		dict.Add(new DateTime(2009, 6, 21), "First Day of Summer"); 
	} 
	public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 
	{ 
		string text; 
		if (!dict.TryGetValue((DateTime) value, out text)) 
			text = null; 
		return text; 
	} 
	... 
}

MainWindow.xaml文件包含了CalendarDayButton的模板,并添加了一些内容。第一个是存储为资源项(Resource)的绑定转换器。

<ControlTemplate.Resources> 
	<src:RedLetterDayConverter x:Key="conv" /> 
ControlTemplate.Resources>

在CalendarDayButton模板中的外部Grid,接着引用绑定转换器来显示ToolTip。

<Grid ToolTip="{Binding Converter={StaticResource conv}, Mode=OneWay}">

如果该绑定看起来缺少Path设置项,请记住按钮的DataContext是一个DateTime对象,并且DataContext是通过可视化树继承的。如果你只是想在ToolTip中显示日期时间,你可以简单地使用:

<Grid ToolTip="{Binding}">

该模板还有一个额外的矩形对象,用于强调显示当前日期和选定的日期:

<Rectangle x:Name="RedLetterDayBackground" IsHitTestVisible="False" Fill="#80FF0000" />

当绑定转换器返回null时,DataTrigger会使这个矩形不可见:

<DataTrigger Binding="{Binding Converter={StaticResource conv}}" Value="{x:Null}"> 
	<Setter TargetName="RedLetterDayBackground" Property="Visibility" Value="Hidden" /> DataTrigger>

在更一般的情况下,CalendarDayButton模板可视化树可以包含任何具有DateTime类型依赖属性的FrameworkElement的派生类,甚至可以包含任何其DataContext类型为DateTime的FrameworkElement派生类。

下面是大佬创建的一个月相日历。原始的MoonDisk类源自Viewport3D,用来模拟太阳照射下的月亮。他从这个类中删除了DateTime依赖属性,并对其稍加修改,将其DataContext转换为DateTime。MoonPhaseCalendar工程包含了MoonDisk类,并在CalendarDayButton模板的可视化树中引用它,如下所示:
WPF——自定义日历_第14张图片

6. 从Calendar派生

CalendarItem、CalendarButton和CalendarDayButton都是密封(sealed)类——你无法从他们派生,即使你可以从CalendarButton和CalendarDayButton派生,你也需要重写CalendarItem的部分内容来实例化你的新按钮。

但是,你可以从Calendar本身派生出一些额外的功能。例如,你可能希望增强Calendar控件,来在单击特定日期时弹出一个非模态(modeless)对话框,来让你输入和查看当天的日常安排。
WPF——自定义日历_第15张图片
要实现这个功能,派生自Calendar(我称之为DailyRemindersCalendar,有点日程表的意思)的类必须能够检测用户何时单击了CalendarDayButton。通常情况下,该实现是非常直接的:派生自Calendar的类的构造函数将调用AddHandler,它的参数为Mouse.ClickEvent和处理点击事件的方法。正如你所知道的那样,WPF实现了路由事件(routed event),因此来自任何子按钮的Click事件会沿着可视化树向上传播。该处理程序将通过检查RoutedEvent对象的Source参数,以及检查DataContext是否为DateTime类型的对象来区分不同的子按钮。

然而,这被证明是不可能的。CalendarItem类为CalendarDayButton的button-down和button-up事件安装处理程序,并禁用Click事件。

相反,我写了DailyRemindersCalendar来重写OnMouseDoubleClick方法。然而,这引发了另一个问题:对于Calendar派生的事件,MouseButtonEventArgs的Source和OriginalSource属性都不是任何类型的按钮。Source是null,OriginalSource是一个TextBlock或Path或Rectangle。

然而,通过可视化树继承的DataContext再次起到了拯救作用。在OnMouseDoubleClick重写中,如果OriginalSource对象具有DateTime类型的DataContext,则该方法知道正在点击一个CalendarDayButton,而DateTime对象将确切地告诉该方法是哪一个。该方法接着会实例化一个DailyRemindersDialog,它完全在代码中构建UI(一个包含TextBlock和TextBox控件的Grid)。(事实上,整个DailyReminders程序完全由代码组成,没有标记)。

DailyRemindersDialog是非模态调用的,因此你可以同时显示不同日期的对话框。不过,DailyRemindersDialog禁止同一天显示多个对话框。这将引发有关如何正确存储和检索这些日常提醒的问题。DailyReminderStorage类通过引用隔离存储(isolated storage)中的XML文件来处理这部分程序。每当DailyRemindersDialog关闭时,它会将文件与更新的信息一起保存。

7. DatePicker控件

我只关注新的Calendar控件而不是DatePicker,因为DatePicker主要由一个DatePickerTextBox和一个调用Calendar控件的下拉菜单组成。

这个DatePickerTextBox非常粗糙。它当然没有在WinForms的DateTimePicker中实现我喜欢的功能——单击日期或时间(年、月、日、小时等),然后使用光标键或数字键来更改值。

但是,可应用于独立Calendar控件的所有样式和模板也可以用于从DatePicker中的下拉菜单中调用的Calendar控件。DatePicker控件有一个类型为Style的名为CalendarStyle的属性,你设置到该属性的Style对象可以包含Calendar定义的任何属性设置器(setter),包括CalendarItemStyle、CalendarButtonStyle和CalendarDayButtonStyle属性。

通过一系列嵌入样式与模板,实际上你可以同时访问Calendar和DatePicker,并根据你自己的需要和喜好来对它们进行调整。

三、结尾

说实话,自定义日历是真的麻烦。
但是学过这篇文章之后发现如果只是去修改它的某个部分,其实也没有那么复杂。因为它是一个由多个部分组成的大整体。我想实现的功能只需要改它的CalendarDayButton模板即可。如果有空的话,单独出一篇修改CalendarDayButton模板的。

你可能感兴趣的:(.NET,#,WPF,wpf)