对于喜欢将常用控件转变为非常用可视对象的程序员而言,Windows® Presentation Foundation (WPF) 提供了一种令人兴奋不已的功能,即模板。控件的功能及其可视外观一向是由复杂的控件代码控制。在 WPF 中,控件的功能仍通过代码实现,但视觉效果与该代码分离开来,并以 XAML 中定义的模板形式存在。通过创建一个新模板(通常在 XAML 中,不用编写任何代码),程序员和设计师无需更改控件代码就能彻底修改控件的可视外观。
在 一年前的开篇专栏中,我讲述了如何为 ScrollBar、ProgressBar 和 Slider 控件设计模板。但模板化功能有利有弊:在设计新的自定义控件时,您要为控件的可视外观提供一个默认模板,并允许该模板由使用控件的程序员替换。您并不完全 一定要这样构造控件——事实上,在拙作“Applications = Code + Markup”(应用程序 = 代码 + 标记)(Microsoft Press®, 2006) 中,没有任何自定义控件定义了可替换模板——但如果这么做的话,需要使用该控件的人(包括您)会省事得多。
本 专栏的目的不是为了创建功能完备、外观漂亮的控件,而是为了建立一种机制,为分布在动态链接库中的控件定义默认可替换模板。我在此讨论的许多模板化技术都 是通过研究现有 WPF 控件上的模板学到的。如果您也想这么做,“Applications = Code + Markup”(应用程序 = 代码 + 标记)第 25 章中的 DumpControlTemplate 程序能让您以方便的 XAML 格式从所有标准 WPF 控件中提取默认模板。
元素和控件
体 验过以前的 Windows 客户端编程环境的程序员很快就会在 WPF 类层次结构中发现一个有趣的现象。例如,在本机 Windows API 中,任何具有屏幕上可视外观的东西都被归类为“窗口”,而在 Windows 窗体中,所有东西都是“控件”。但在 WPF 中,Control 类和许多其他可视对象(尤其是 TextBlock、Image、Decorator 和 Panel),都从 FrameworkElement 派生。那么,元素与控件到底有何区别呢?
首先,Control 类将一组非常简单的属性添加到 FrameworkElement 类,包括 Foreground、Background 和五个与字体相关的属性。Control 并不直接使用这些属性,它们只是为了方便从 Control 派生的类。
其次,Control 类添加了 IsTabStop 属性和 TabIndex 属性,这意味着控件在 tab 键导航链中一般是停留点,而元素则不是。总而言之,元素用于观看,而控件则用于交互(但元素仍能获取焦点并对键盘、鼠标和笔针输入作出响应)。
第三,Control 类定义 ControlTemplate 类型的 Template 属性。此模板一般是元素的可视树和构成控件可视外观的其他控件,通常还包含根据属性变化和事件而更改此可视外观的触发器。
第 三个特征意味着从 Control 派生的类有一个可自定义的可视外观,而从 FrameworkElement 派生的其他类则没有。TextBlock 和 Image 当然都有可视外观,但自定义这些视觉效果没有任何意义,因为这些元素不会给它们显示的格式化文本或位图增添任何东西。在另一方面,ScrollBar 可有多种外观,而功能则仍然相同。这就是模板的用途。
对 于程序员来说,以下可能是元素和控件之间最大的差别:如果从 FrameworkElement 派生,为了在屏幕上呈现元素的可视元素及其子项,您很可能需要覆盖 MeasureOverride、ArrangeOverride 和 OnRender。如果从 Control 派生,通常情况下并不需要覆盖这些方法,因为控件的视觉效果由 Template 属性的 ControlTemplate 对象中的可视树定义。
WPF 包括一个名为 UserControl 的类,它通过 ContentControl 从 Control 派生。通常推荐将此 UserControl 作为简单自定义控件的基类,其用途广泛。例如,拙作第 25 章中的 DatePicker 控件即从 UserControl 派生。但请记住 Control 与 UserControl 之间的如下显著区别:当从 UserControl 派生时,您可以在 XAML 中定义可视树,但此可视树是 UserControl 的 Content 属性的子项。UserControl 自有其简单的默认模板,您可能不会替换该模板,因为它将 ContentPresenter 嵌套在 Border 内部。
从 UserControl 所派生类的可视树并不是用来被替换的,因此该类的代码及其可视树可以更紧密地耦合。相反,如果您打算从 Control 派生并提供一个可替换的默认模板,代码和可视树之间的交互则应该既简单又记录完备。
默认模板和 DLL
我决定在此专栏中再研究一下日历控件。本专栏的源代码是一个名为 CalendarTemplateDemo 的单一 Visual Studio® 解决方案。它包含一个库项目,创建名为 CalendarControls 的 DLL 以及使用这些控件的四个演示程序。
CalendarControls 库包含名为 CalendarMonth、CalendarDay 和 CalendarDayNotes 的 CalendarControls 命名空间中的三个控件的代码和默认模板。这三个类都在各自的 C# 文件中定义。前两个类从 Control 派生,CalendarDayNotes 则从 CalendarDay 派生。
为 使默认模板能被使用控件的应用程序所替换,在 DLL 中定义默认模板的规则非常严格。如果不遵循这些规则,创建出的控件可能没有默认模板或模板不能由应用程序替换。DLL 项目必须有一个名为 Themes 的目录,其中包含一个名为 generic.xaml 的文件,根元素为 ResourceDictionary。资源为指向 DLL 中控件的 Style 元素。我的 CalendarControls 库中的 generic.xaml 文件类似图 1 中所示。

Figure 1 Generic.xaml for CalendarControls
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cc="clr-namespace:CalendarControls">
<Style TargetType="cc:CalendarMonth" />
...
</Style>
<Style TargetType="cc:CalendarDay" />
...
</Style>
<Style TargetType="cc:CalendarDayNotes" />
...
</Style>
</ResourceDictionary>
此文件中的每个 Style 元素至少有一个 Setter 元素,用于设置控件的默认模板:
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="cc:CalendarMonth">
...
</ControlTemplate>
</Setter.Value>
</Setter>
ControlTemplate 包含控件的可视树。它还包含一个可选的 Resources 部分,用于定义模板中使用的一些资源。通常会有一个 Triggers 部分,用于定义模板如何对其他属性或事件中的变化作出响应。
或 者,您可以将这些默认模板分成单独的文件,我就是这么做的。CalendarControls 项目的 Themes 目录包含三个文件,分别名为 CalendarMonthStyle.xaml、CalendarDayStyle.xaml 和 CalendarDayNotesStyle.xaml。每个文件都有一个 ResourceDictionary 根元素和 Style 类型的单一子项(指向特定控件)。generic.xaml 文件引用这三个资源文件,如图 2 所示。

Figure 2 Referencing Themes from Generic.xaml
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cc="clr-namespace:CalendarControls">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary
Source="CalendarControls;Component/themes/
CalendarMonthStyle.xaml" />
<ResourceDictionary
Source="CalendarControls;Component/themes/
CalendarDayStyle.xaml" />
<ResourceDictionary
Source="CalendarControls;Component/themes/CalendarDayNotesStyle.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
为 确保这些样式能与 CalendarMonth、CalendarDay 和 CalendarDayNotes 类型的对象成功结合,这些类必须从其 DefaultStyleKey 属性返回一个正确的值。由于该原因,这些类的静态构造函数改变了该属性的默认值,从而返回类的类型。此过程通过覆盖 DefaultStyleKey 依赖属性的元数据完成:
static CalendarMonth()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(CalendarMonth),
new FrameworkPropertyMetadata(typeof(CalendarMonth)));
...
}
OverrideMetadata 的第一个参数是修改元数据的类的类型;给 FrameworkPropertyMetadata 构造函数的参数表示 DefaultStyleKey 属性的新默认值,它也是类的类型。
DLL 的程序集信息文件(通常命名为 AssemblyInfo.cs)是存放以下程序集属性的好地方:
[assembly: ThemeInfo(ResourceDictionaryLocation.None,
ResourceDictionaryLocation.SourceAssembly)]
这意味着没有主题特定的模板集,但控件的通用模板所在的程序集 (DLL) 与控件本身的相同。
控件派生基础知识
从 Control 派生时,您可能需要定义一些新的公共属性。在大多数情况下,您应该使用依赖属性支持这些属性,以使它们成为数据绑定和动画的目标。即使它们是只读属性,不 能成为绑定目标,但是使用依赖属性能提供一种通知机制,这样当这些属性改变时,其他对象就会收到通知。
如果您需要定义一个已经由其他类定义但并未从 Control 继承的属性,不要完全重新定义该属性。而是对现有的依赖属性调用 AddOwner。将 AddOwner 返回的值保存为公共静态只读字段,并在 CLR 属性中使用该值。
如 果需要更改继承属性的默认值,有多种方法。直接在类的构造函数中设置属性值的做法并不可取,因为这种所谓的本地设置有非常高的优先权,不能被样式或属性覆 盖。低优先权方法是调用依赖属性上的 OverrideMetadata,正如我在上文中使用 DefaultStyleKey 属性演示的一样。Control 类本身以这种方式设置 IsTabStop 的默认 true 值。
另 一种低优先权方法是使用与控件的默认模板在同一个 Style 元素中的 Setter 设置属性。许多 WPF 控件使用该技术设置与控件的可视外观关联的属性,如 SnapsToDevicePixels、MinWidth 或 MinHeight。如果类需要在每次继承属性的值改变时都收到通知,则可以使用 OverrideMetadata 安装一种附加回调方法。
从 Control 派生的最大挑战是以一种方式定义代码和模板之间的交互,能够满足您的所有需要而又不阻止他人编写替代模板。一般而言,控制代码能以几种方式适应模板:
- 代码定义模板可通过 TemplateBinding 标记扩展访问的属性。
- 代码定义模板可将之用作触发器的属性和事件。
- 代码定义模板中的按钮可以触发的 RoutedCommand 属性或字段。
- 代码假设某些帮助器元素存在于模板中;它有时通过预定义的名称引用这些元素。
我将为这几项技术一一举例。
在 某些情况下,替换模板时需要注意从 Control 派生的类,使它能访问新模板并建立指向模板的链接。Control 类定义一个虚拟 OnTemplateChanged 方法,您可以覆盖该方法,以便在 Template 属性改变时收到通知。然而,从我的经验来看,OnTemplateChanged 方法实际上并不是执行任何必需链接的好地方,因为模板尚未得到应用(以使用 WPF 语言)。一种高明得多的策略是覆盖由 FrameworkElement 定义但没有默认实现(非常奇怪)的 OnApplyTemplate 方法。
元素和控件的层次结构
CalendarControls 库中的 MonthCalendar 控件显示一年中的单个月份。TwoCalendars 程序显示两个 MonthCalendar 实例,一个用英语,一个用法语,如图 3 所示。
Figure 3 Two Instances of MonthCalendar (单击该图像获得较大视图)
正是因为遵循了 WPF 设计原理,默认模板才与 Win32® 的对应体如此相似——它们通常都有些单调,您现在看到的日历确实如此。但从项目伊始,我的目标之一就是正确实现不一定要从星期日(法语为 dimanche)开始的日历。然而,我并没有大胆地打算超出公历。
为了使这些示例相对简单,我决定不实现选择日期的概念。不过,正如您所见,名为 IsToday 的属性导致某个特殊日期被突出显示。
我从 WPF 学到的非常重要的经验之一就是,复杂的控件可通过较简单的控件和元素构建。您在图 3 中看到的都不是由 CalendarMonth 的代码部件所定义。它完全是默认的 XAML 模板的一部分:一个边框环绕四周,并提供背景。顶部的按钮是 RepeatButton 控件,他们以一个月或一年向前或向后导航。按钮之间是 TextBlock。星期几是 StatusBar 上的 StatusBarItem 对象,使用 UniformGrid 作为 ItemsPanel。日期也在 UniformGrid 中显示。
由 于一些原因(在下文将很明确),我决定将单个日期设为独立的控件,并命名为 CalendarDay。CalendarDay 也从 Control 派生,默认模板是一个包含 TextBlock 的 Border。当 CalendarMonth 控件显示月份时,会生成 28、29、30 或 31 个 CalendarDay 对象。显示这两个日历的 TwoCalendars.xaml 文件如图 4 所示。

Figure 4 TwoCalendars.xaml
<!-- TwoCalendars.xaml by Charles Petzold, Sept. 2007 -->
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cc="clr-namespace:CalendarControls;assembly=CalendarControls"
x:Class="Petzold.TwoCalendars.TwoCalendars"
Title="Two Calendars Demonstration">
<Window.Resources></Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<cc:CalendarMonth Grid.Column="0" Margin="24" />
<cc:CalendarMonth Grid.Column="1" Margin="24"
Culture="fr-FR" />
</Grid>
</Window>
代码和 XAML
由于 CalendarDay 比 CalendarMonth 简单得多,而且足够短(能完整显示),因此这是我希望在此项目中首先重点关注的部分。图 5 显示了 CalendarDay.cs。注意这并不是一个部分类。默认模板并不是 CalendarDay 类的一部分;相反,它自动应用于 CalendarDay 类型的对象。

Figure 5 CalendarDay.cs
// CalendarDay.cs by Charles Petzold, Sept. 2007
using System;
using System.Windows;
using System.Windows.Controls;
namespace CalendarControls
{
public class CalendarDay : Control
{
// Static constructor: Change defaults.
static CalendarDay()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(CalendarDay),
new FrameworkPropertyMetadata(typeof(CalendarDay)));
IsTabStopProperty.OverrideMetadata(typeof(CalendarDay),
new FrameworkPropertyMetadata(false));
}
// Date dependency property and property.
public static readonly DependencyProperty DateProperty =
DependencyProperty.Register("Date",
typeof(DateTime),
typeof(CalendarDay),
new PropertyMetadata(DateChangedCallback));
public DateTime Date
{
set { SetValue(DateProperty, value); }
get { return (DateTime)GetValue(DateProperty); }
}
// IsToday dependency property and property.
static readonly DependencyProperty IsTodayProperty =
DependencyProperty.Register("IsToday",
typeof(bool),
typeof(CalendarDay),
new PropertyMetadata(false));
public bool IsToday
{
set { SetValue(IsTodayProperty, value); }
get { return (bool)GetValue(IsTodayProperty); }
}
// Day read-only dependency property and property.
static readonly DependencyPropertyKey DayKey =
DependencyProperty.RegisterReadOnly("Day",
typeof(string),
typeof(CalendarDay),
new PropertyMetadata());
public static readonly DependencyProperty DayProperty =
DayKey.DependencyProperty;
public string Day
{
protected set { SetValue(DayKey, value); }
get { return (string)GetValue(DayProperty); }
}
// DateChangedCallback method
static void DateChangedCallback(DependencyObject obj,
DependencyPropertyChangedEventArgs args)
{
CalendarDay calday = obj as CalendarDay;
calday.Day = calday.Date.Day.ToString();
}
}
}
类 定义三个新属性,所有属性都由依赖属性支持:DateTime 类型的日期,bool 类型的 IsToday 以及字符串类型、名为 Day 的只读属性。Day 属性完全为了方便模板。每当 Date 属性改变时,类就将 DateTime 对象的 Day 属性转换为字符串,并将它设置为自己的 Day 属性。
CalendarDayStyle.xaml 文件包含 CalendarDay 控件的模板。模板通常以 Border 元素开头,该元素的属性通过 TemplateBinding 扩展设置为由 Control 类定义的同名属性。如果不在模板中显式引用这些属性,它们将不会对控件的可视外观产生任何影响!
通 过可视树继承的 Control 属性无需在模板中引用。这些继承属性为 Foreground 和五个与字体相关的属性。对这些属性的任何更改都会由 TextBlock 自动继承;TextBlock 显示由 CalendarDay 类定义的 Day 属性。HorizontalContentAlignment 和 VerticalContentAlignment 属性决定文本在单元格中的对齐方式。模板以一个 Triggers 部分结束,如下文所示:
<Trigger Property="IsToday" Value="True">
<Setter Property="Background"
Value="{DynamicResource
{x:Static SystemColors.HighlightBrushKey}}" />
<Setter Property="Foreground"
Value="{DynamicResource
{x:Static SystemColors.HighlightTextBrushKey}}" />
</Trigger>
当 CalendarDay 定义的 IsToday 属性为 true 时,Background 和 Foreground 属性将设置为突出显示项目的系统画笔。此 Triggers 部分也可以包含 MultiTrigger 元素和 EventTrigger 元素,后者能触发动画。
虽 然 CalendarDay 包含一个 Border 元素,但却没有可见的边框。CalendarMonth 模板的定义类似,整个月份周围也没有可见边框。边框不可见是因为 Control 将 BorderBrush 的默认值定义为空值,将 BorderThickness 的默认值定义为零,就像 Border 元素本身一样。您可能会强烈地认为,应将每一天都圈在一个可见的方框内。为什么不将模板中的属性设置为更合理的值呢?
<Border BorderThickness="1"
BorderBrush="Black" ...
此 方法的问题是,将来使用该控件的任何程序员都必须更改默认模板才能更改这些值。模板的硬编码属性值应尽可能地少。如果您是控件及其默认模板的设计者,您可 以使用资源文件中的 Style 元素将这些属性设置为更合理的默认值。如果您是控件的使用者,则可以在使用该控件时设置这些属性。
例如,在 TwoCalendars.xaml 文件中,您可以按如下方法更改第一个日历:
<cc:CalendarMonth BorderThickness="1"
BorderBrush="Black" ...
这么做会给整个日历加上一个可见的边框。但如何设置 CalendarDay 边框呢?CalendarDay 元素甚至不在 TwoCalendars.xaml 文件中显示,因为 CalendarMonth 在内部生成所有 CalendarDay 对象!
解决方案是使用以 CalendarDay 为目标的样式。尝试将它插入 TwoCalendars.xaml 的 Resources 部分:
<Style TargetType="cc:CalendarDay">
<Setter Property="BorderThickness" Value="1" />
<Setter Property="BorderBrush"
Value="{DynamicResource
{x:Static SystemColors.ControlTextBrushKey}}" />
<Setter Property="HorizontalContentAlignment"
Value="Center" />
<Setter Property="VerticalContentAlignment"
Value="Center" />
<Setter Property="FontStyle" Value="Italic" />
</Style>
此样式不仅会设置边框,也会设置内容对齐方式属性,使单元格中的日期居中,并使数字斜体。
默 认的 CalendarMonth 模板包含在 CalendarMonthStyle.xaml 文件中。它以 Border 开头,随后是包含三个垂直单元格的 Grid,用于按钮和月份名称、星期几以及日历网格本身。Grid 的顶部单元格是另一个 Grid,包含五个水平单元格,用于按钮和月份名称。
CalendarMonth 类准备了几种属性,模板可用这些属性显示文字信息,如 MonthName、AbbreviatedMonthName 和格式化的 AbbreviatedYearMonth。(CalendarMonth 包含许多来自 DateTimeFormatInfo 的这类信息。)CalendarMonth 也在两个数组中存储星期几的名称,即 DayNames 属性和 AbbreviatedDayNames 属性。它们与 DateTimeFormatInfo 类中的同名属性有些差别,因为 DateTimeFormatInfo 中的数组通常以星期日开头。
由 CalendarMonth 定义的 FirstDayInWeek 属性基于当月第一天为星期几,FirstDayOfWeek 属性来自 DateTimeFormatInfo。
CalendarMonthStyle.xaml 中的模板拥有自己的 Resources 部分。这对于在模板内设置样式非常方便,对标记中未明确显示的元素类型尤为如此。例如,注意星期几如何显示。将 AbbreviatedDayNames 属性分配给用 UniformGrid 作为其 ItemsPanel 的 StatusBar。AbbreviatedDaysNames 数组的字符串在内部成为 StatusBarItem 控件的 Content 属性。StatusBarItem 实际上不会在此标记中显示,但 Style 能指向 StatusBarItem 控件,将内容居中,并在名称之间插入一些空格。
访问命名元素
到 目前为止,我们已经介绍了类如何定义 XAML 文件通过 TemplateBinding 扩展和触发器引用的属性的示例。但有些时候,类访问模板中的特定元素更为方便。在这个示例中,CalendarMonth 类需要为一个特定月份生成所有 CalendarDay 对象,且这些对象最终必须在某种面板中。看起来最简便的方法是为此面板指定一个代码引用的名称。
您 会发现,CalendarMonth 模板中的第二个 UniformGrid 被命名为 PART_Panel。此名称与几个 WPF 控件的默认模板中定义的名称类似。CalendarMonth 类定义之前是一个属性,该属性指明此名称以及代码期望它标识的元素类型:
[TemplatePart(Name = "PART_Panel", Type = typeof(Panel))]
此属性是为了方便可视化设计器,不是必需的。但请注意,我已暗示模板中的此元素可以是 Panel 的任何派生物。
作为对调用 ApplyTemplate 的响应,CalendarMonth.cs 中的此语句获取此面板并将其存储为字段:
pnl = Template.FindName("PART_Panel", this) as Panel;
注 意这并不是普通的 FindName 方法。这是由派生 ControlTemplate(以及 ItemsPanelTemplate、DataTemplate 和 HierarchicalDataTemplate)的 FrameworkTemplate 定义的 FindName 方法,因此可通过 CalendarMonth 对象的 Template 属性访问它。
如果此 pnl 对象为空会发生什么情况?这意味着模板不包括任何名为 PART_Panel 的内容,或者可能是名为 PART_Panel 的元素不是从 Panel 派生。如果某个类在模板中找不到一个命名元素,它会根据现有情况默认继续。
在 将名称和类型与模板中的帮助器元素相关联时,尽可能使之通用。在这种情况下,MonthCalendar 需要一个带 Children 属性的元素,因此,Panel 似乎比较合适。虽然 UniformGrid 用起来显然不错,但 MonthCalendar 不需要特殊类型的面板。
生成命令
CalendarMonth 模板必须包含向前导航和向后导航的按钮。如何编写对这些按钮按下作出响应的代码?最简便的方法是由类定义公共静态只读字段或 RoutedCommand 类型的只读属性。现有的 WPF 控件在将这些 RoutedCommand 对象定义为字段或属性时存在不一致。ScrollBar 类将它们定义为只读字段,并将它们命名为 LineDownCommand 和 LineLeftCommand 等。Slider 类将它们定义为只读属性,命名为 DecreaseLarge、DecreaseSmall、IncreaseLarge 和 IncreaseSmall。我选择了 ScrollBar 方法,并将我的 RoutedCommand 对象定义为字段,如下所示:
public static readonly RoutedCommand NextMonthCommand =
new RoutedCommand("NextMonth",
typeof(CalendarMonth));
注意这些都是静态的!模板以指定给控件的 Command 属性的完全限定名称引用它们:
<ToggleButton Command="
CalendarMonth.NextMonthCommand" ...
并不是许多控件都可以如此定义 Command 属性,即您可以将其设置为 RoutedCommand 类型的对象。只有 ButtonBase、MenuItem 和 Hyperlink 可以。
通过将对象添加到其 CommandBindings 集合,CalendarMonth 构造函数将这些 RoutedCommand 对象与一个执行方法链接:
CommandBindings.Add(new
CommandBinding(NextMonthCommand,
NextMonthExecuted));
此外,您也可以指定一个可执行的方法,它可让代码设置一个 Boolean,表明控件是否有效。如果无效,它将自动被禁用。
CalendarMonth.cs 中的 NextMonthExecuted 方法如下所示:
void NextMonthExecuted(object sender,
ExecutedRoutedEventArgs args)
{
Date = Date.AddMonths(1);
}
使 用 RoutedCommand 对象的一个优势是您可以通过调用 Execute 方法相当轻松地从代码触发它们。例如,您可以使用某些键盘输入触发命令。但在许多情况下,通过将 InputGesture 类型的对象(派生 KeyGesture 和 MouseGesture 的抽象类)与 RoutedCommand 相关联,您无需显式的键盘处理便能完成这一工作。CalendarMonth 的静态构造函数将 KeyGesture 对象添加到所有四个 RoutedCommand 字段中,如下所示:
NextMonthCommand.InputGestures.Add(
new KeyGesture(Key.PageDown));
这些笔势本应添加到 RoutedCommand 对象的原始字段定义中,但这个方法有点笨,因为您需要在构造函数中提供整个 InputGestureCollection。
替换模板
当您编写一个应用程序来试图替换默认模板并使日历完全改观时,真正考验这整个练习的时候来临了。NewCalendar 程序为垂直显示日期的 CalendarMonth 定义一个新模板。该程序创建 12 个月历,将它们并排显示(请参见图 6)。
Figure 6 The NewCalendar Display
也 可以从 CalendarDay 派生新类,以增强日历的功能,但最初这似乎不可行:CalendarMonth 负责生成 28、29、30 或 31 个 CalendarDay 实例。似乎如果您希望从 CalendarDay 派生,则必须同时从 CalendarMonth 派生才能更改该逻辑。为避免这种情况,我为 Type 类型定义了另一个名为 DayType 的 CalendarMonth 属性。默认情况下,此属性等于 typeof(CalendarDay),但它也可以设置为派生自 CalendarDay 的类的类型。
CalendarDayNotes 从 CalendarDay 派生,并假定其模板包含一个名为 PART_TextBox 的 TextBox 类型控件。您向这些 TextBox 控件键入的任何内容都由 CalendarDayNotes 保存在小文件中,由派生自日期的文件名标识。下次您再运行该程序时,它会加载所有内容。CalendarDayNotes 的默认模板非常单调,而 ReminderCalendar 程序的模板则稍微漂亮一些,它将日期显示为狭长的形状,并用颜色表示渐变,如图 7 所示。
Figure 7 The ReminderCalendar
为 了使日期更漂亮一些,甚至都不需要从 CalendarDay 派生。CalendarControls 库还有一个从 Viewport3D 派生的名为 MoonDisk 的类,以及一个 DateTime 属性,以模拟引导月亮球面上的光线。MoonPhaseCalendar 创建日历的速度并不是世界上最快的——它需要为每个月生成多达 31 个 Viewport3D 对象——但它确实与普通日历不一样,如图 8 所示。
Figure 8
The MoonPhaseCalendar Display
代码下载 (784 KB)