从日历派生
在 DatePicker 控件
Microsoft 发布了 Windows Presentation Foundation (WPF) 在 2006 年,每个人都同意缺少几个部分,而最明显日历控件。它似乎付息日不固定的省略) 别是忘 Windows 窗体有 MonthCalendar DateTimePicker 控件,那么为什么不只是端口到 WPF?
但移植 WPF 控件不是一个简单问题只更改属性和事件的名称。WPF 控件必须是"lookless。 必须将该控件的外观分成其他元素和控件的可视树。此默认模板必须结构,并为完全可替换这样一种方式中介绍。在这种方式开发人员和设计人员可以自定义控件使其具有全新的外观。
我发现我试图设计为我的项目的 MonthCalendar 控件时,设计一个带有可替换的模板的控件可以是 intimidating"
不常见的控件的模板
"2008 年 1 月发布。要使控件为通用尽可能为适合各种不同的外观还不进行 inordinately 复杂的代码和模板之间的接口。日历引入另一级别的复杂性的因为大量依靠文化约定,它并不明显应程度允许违反这些规则自定义模板。
这可能是为什么 WPF Calendar 控件需要很长。但后期最后一年 Microsoft (带有几个子类) 的日历和 DatePicker 控件在一个
WPF 工具包
. (新的 DataGrid,还包含,但我不会讨论的此处)。 这些控件与在 Silverlight 中, 相同的控件非常兼容,而且它们可能最终会在 WPF 的下一个版本中。
有关本文的其余部分我假设您下载 WPF 工具包源代码和二进制文件并运行安装程序注册 WPFToolkit.dll。一旦注册此 DLL,就可以使用 Visual Studio 中的 WPF 项目中通过右键单击项目中的引用标记,选择添加的引用,然后选择.NET 选项卡。WPF 工具包 DLL 将找到列表底部。
A Brief 方向的日历:
(与某些支持枚举和委托) 在日历和 DatePicker 类具有 Microsoft.Windows.controls 的命名空间。某些子类主要是值得注意的是 CalendarItem、 CalendarButton、 CalendarDayButton 和 DatePickerTextBox,位于 Microsoft.Windows.controls.primitives 命名空间。这些不用于 WPF 控件在自定义命名空间因此您可能需要添加新 using C# 代码中的指令并构建正确的 XML 命名空间声明 XAML 文件中。与新 Visual 状态我将讨论本文后面) 管理器的类具有更普通的命名空间 System.Windows。
日历类派生自控件。如果只实例化日历,您获得
图 1
中上显示的视图。显示仅一个月,永远不会多个月与 Windows 窗体 MonthCalendar。即使如果唯一的四个或五个,月份始终将包含六个的周数。单击标题 (在顶部月和年部分),将切换到资源中年视图。可以单击月份返回到该月份的视图之一或单击标题再次以获取十年视图显示在右侧。单击要返回年视图在年之一。
图 1
的三个日历显示模式
)
以下三个视图对应于在 DisplayMode 属性设置为三个 CalendarMode 枚举的成员之一的日历: 月、 年或十年。
每一侧标头的两个按钮定位向前或向后按月、 年,或十年。
DisplayDate 属性 DateTime 类型的月、 年或十年显示。类型为空的 DateTime,并且限制在特定范围内日历的导航,DisplayDateStart 和 DisplayDateEnd 属性都。
在 IsTodayHighlighted 属性 true 默认情况下,并导致深正方形今天的日期之后绘制。此外可以选择日期。将 SelectionMode 属性设置为 CalendarSelectionMode 枚举的成员: SingleDate (默认) SingleRange,MultipleRange,或无。为 SingleDate 模式,SelectedDate 属性 (的类型为空的 DateTime) 指示所选内容 ; SelectedDates 属性属于类型派生的 DateTime 类型的 ObservableCollection 的 SelectedDatesCollection 的否则。
BlackoutDates 属性,可以禁止的日期的不同区域的选择。该属性是类型的类型派生 CalendarDateRange,定义 DateTime 类型的开始和结束属性的类的 ObservableCollection 的 CalendarBlackoutDateCollection。
FirstDayOfWeek 属性的类型 DayOfWeek ; 默认值通常是星期日,但对于某些区域设置 (最值得注意的是法国) 默认值是星期一。
日历定义了四个事件: DisplayModeChanged,DisplayDateChanged,SelectionModeChanged,和 SelectedDatesChanged。
日历类的剩余的公共属性是 CalendarItemStyle、 CalendarButtonStyle,和 CalendarDayButtonStyle,所有类型样式,但要理解这些属性您需要知道如何 Calendar 控件是生成和结构,并为该知识库它有助于检查某些 XAML。
日历控件的所有源代码从该 CodePlex 站点都是可下载但如果您想自定义该控件最重要文件一定 generic.xaml,位于子目录 Toolkit release\Calendar\Themes 中。generic.xaml 文件包含日历控件本身和构成 Calendar 控件在其他控件的默认样式 (包括模板)。如果您将模板化日历,您需要非常仔细研究 generic.xaml。
尽管日历类定义所有属性和该的控件事件,并且还执行一些的用户输入处理,generic.xaml 了日历模板显示的日历包括仅不可见的 StackPanel 和 CalendarItem 控件。
此外 CalendarItem 类派生自控件,并包含控件的整个 Visual 的外观。这包括三个按钮在顶部我将引用统称为导航按钮)。它还包括两个网格。一个网格显示月份 (一周的天包括标头) 的天数。在其他网格显示 12 个月或 12 年。在任何时候这些两个网格之一是可见。
CalendarItem 程序还负责填充这些两个网格。当显示该月的天,它创建 CalendarDayButton 类型的对象 ; 在 12 月或年类型 CalendarButton。这两个类都派生从按钮。
在 generic.xaml 包括日历、 CalendarItem、 CalendarButton,和 CalendarDayButton 的默认样式和模板。这些后的三个项目可以通过设置该 CalendarItemStyle、 CalendarButtonStyle 或直接或通过日历的 Style 属性定义日历,CalendarDayButtonStyle 属性设置新的样式 (包括模板)。
设置样式本身日历非常有用日历特定属性 (如 DisplayMode 或 SelectionMode) 或由日历 (如 HorizontalAlignment 或页边距) 继承的属性但我要重点的属性的更多问题类: 这些字体和画笔。
Calendar 控件非常精确地生成其附属的控件并如果启动与字体 messing 周围,控件可以迅速成为直观地 lopsided。是例如您可以设置 FontFamily 属性的日历上和它将影响除外天的周标题的所有文本其他设置。(这些标题 FontFamily 是在默认模板中 CalendarItem 的 DateTemplate 中硬编码。 但是,如果月份的天数的文本结果变为一点小,整个月份将似乎向左移。
在日历上设置 FontSize 属性不会产生任何效果。如果您确实要更改该 FontSize 必须 CalendarButton 样式、 CalendarDayButton 样式和 CalendarItem 模板内的两个位置中更改它。忘记它。如果您想使日历控件略有大于或小于普通,不要执行它使用 FontSize。我建议定义一个 ScaleTransform 和设置日历的 LayoutTransform 属性,或可能将该日历放入一个 Viewbox。
日历用于在当前日期、 所选的日期和中断日期将标记的颜色是不同的模板中的硬编码。仅画笔,您可以方便地更改是背景财产日历控件和在 BorderBrush 周围控件。这些是将同时设置为默认样式的日历中的线性渐变画笔,然后传递给 CalendarItem 通过模板绑定。如果您希望替换默认背景画笔我将演示下文中, 记住的负责能够看到在
图 1
中的导航按钮后所不同的背景底纹。
设置新的日历模板
替换控件的模板,首先会查找在类定义在文档或实际的源代码 (如果可用)。有您将看到 TemplatePart 属性的集合。这些 TemplatePart 属性指示指定的控件或新模板必须具有完全正常工作的元素。对于日历控件,新模板都必须有至少两个部分: 名 PART_CalendarItem 的 CalendarItem 和同名 PART_Root 的面板。(在默认日历模板中 PART_Root 面板是一个 StackPanel。
很不您将会在的日历替换模板中,但不应尝试第二个猜测您的需要。也许您希望将内容添加到该 Visual 树,日历,如一个可滚动 ItemsControl,显示所有选定的日期所构成的。
这样的应用程序进行了说明由 SelectedDateLister 项目包含为该列可下载的源代码。
图 2
显示为日历的 MainWindow.xaml 文件具有新的模板的项目的摘录,并
图 3
显示了其外观。
图 2 A 日历的新模板
<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>
图 3 使用所选的日期列表的日历
请注意 XML 命名空间前缀的用于引用 CalendarItem 控件的基元。 在 XAML 文件的根元素中的前缀是与 Microsoft.Windows.controls.primitives 和 WPFToolkit 程序集的.NET 命名空间。 同样,前缀工具包是与在同一程序集中 Microsoft.Windows.controls 命名空间。 在 XAML 中只实例化该日历就像这样:
<toolkit:Calendar />
包含新的模板将样式应用隐式。
此外请注意 图 2 中使用 RelativeSource ItemsControl 将 ItemsSource 属性连接到日历的将 SelectedDates 属性的绑定的使用。TemplateBinding 不能用于此作业,因为 SelectedDates 不备份具有依赖关系属性。
我如何知道 CalendarItem 的哪些属性我应设置使用 TemplateBinding?我将探讨 generic.xaml 默认日历模板。实际上,我不仅看它: 我复制并粘贴到我的源代码鼠标右键。复制和粘贴是,我很担心,最好的方法修改现有模板。
替换 CalendarItem 模板
若要将更多控制 Calendar 控件的内部外观有必要为 CalendarItem 替换模板。默认模板是大量块的 XAML generic.xaml 文件中的几乎 250 个行,并且包含三个嵌入式的模板显示在顶部导航按钮。
没有还 delved 到 Silverlight 的经验丰富的 WPF 程序员会看到三个嵌入式的按钮模板中不熟悉的内容感到惊讶: 对类的引用名为 VisualStateManager、 VisualStateGroup 和 VisualState。这些类是 WPF 的 Visual 状态管理器已经在 Silverlight 中并且希望成为下一个主要的版本中的一部分的主要组件。(此初步 Visual 状态管理器的源代码包含 WPF 工具包中)。
Visual 状态管理器用于替换模板中当前使用的触发器的许多。模板触发器通常基于 IsEnabled、 IsMouseOver 和 IsSelected 等的特定属性的值。Visual 状态管理器而是基于名称之类的 Visual 状态的更多的结构化概念如被禁用 MouseOver,并且选择。在第一次不易这些 Visual 状态似乎与触发器,一一对应,但这不确实如此。在实际使用,触发器会导致根据在其中显示,和通常就粗心触发器需要彼此协同工作时定义多个触发器顺序的不同 Visual 的外观。Visual 状态的其他标识使程序员的目的更明显。关联属性设置和 Visual 状态之间发生代码中。
若要引用 Visual 状态管理器类在 XAML 中的,需要定义一个其他 XML 前缀 (vsm,例如) 与 WPFToolkit 集中在 System.Windows 命名空间相关联。
自定义模板的 CalendarItem 应 8 的名为的部分中所示图 4 .
图 4 CalendarItem 模板的命名部件
请注意这些名称之一是实际资源项名称。CalendarItem 模板的资源部分应包括键名为"DayTitleTemplate"文本属性设置如包含在 TextBlock 元素的 DataTemplate 这样:
Text="{Binding}"
此 DateTemplate 用于显示星期日期的标题。 用于这些标头 CalendarDayButton 项具有设置为空,但它的 DataContext 设为文本标头 (su、 Mo、 Tu,等),因此奇怪绑定其内容。 (我将拥有更多地说更高版本在 CalendarDayButton 的 DataContext 属性。
默认 CalendarItem 模板的可视树包含多个嵌套网格。 一个外部的单个单元格网格名为 PART_Root。 此网格包含边框,名为 PART_DisabledVisual 矩形适合提供禁用该控件时涉及一个 semi-opaque。
边框包含另一个网格,提供主结构,到控件的两行三列与此项。 三个名为的导航按钮将占用最上面一行。 底部行中包含两个网格 (PART_MonthView 和 PART_YearView) 跨越三列。
网格名为的 PART_MonthView 预期有七列 (的一周中的七个工作日) 和七个行 (一天的周标题) 六个月的 PART_YearView 时应使用四个列和三行 (一年的或"十年"视图中的 12 年的 12 个月)。 当 CalenderItem 代码部分创建 CalendarDayButton 和 CalendarButton 子级来填充这些网格时,它显式设置 grid.row 和附加的 Grid.column 属性与每个子表。
图 5 一个新的裸 bones CalendarItem 模板
由于这个原因太自定义的 CalendarItem 模板将显著不同于现有的布局。 如果您希望获得它的一个快照时,在 BareBonesCalendarItem 项目 MainWindow.xaml 文件将具有几乎您需要获取内容,完整且功能最小标记。 在结果 Visual 是图 5 所示。 只是以使其略有不同,我生成整体结构从一个的 DockPanel 上一个月和下一个月的这些按钮将在面上。 将我未模板应用于这些导航按钮这是为什么在类似于实际的按钮。 即使有这些简化,此模板是几乎 80 行长度。 它看起来有点的奇数页,但它的工作方式。
定义新的 CalendarItem 模板了更安全和更合理的方法是将只是默认模板为 CalendarItem 从复制 generic.xaml 到您自己的 XAML 文件,然后修改只是它的某些部分。
这是我做 ColorfulCalendarItem 项目中。 MainWindow.xaml 文件定义本地而不是工具包的 XML 命名空间的前缀引用日历类与 generic.xaml 中该命名空间声明保持一致。
CalendarItem 模板中,我添加外部边框的新背景画笔:
<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 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>
结果显示在 图 6 .
图 6 CalendarItem 模板中定义的新颜色
功能强大的 CalendarDayButton 模板
CalendarItem 模板包含三个导航按钮在的日历的顶部显示嵌入式的模板,但该模板有不对用于填充较低的网格 CalendarButton 和 CalendarDayButton 对象的引用。 如您可以通过检查 CalendarItem.cs 文件注意,这些按钮是实例化代码中,。 但是,您可以通过设置 Style 对象的日历 CalendarButtonStyle 和 CalendarDayButtonStyle 属性定义为这些按钮的新模板。
CalendarDayButton 的自定义模板是可能非常强大的。 此处可以显示每一天超过该自定义两位数数字介于 1 和 31 之间的其他信息。 在最初它不会似乎,您可以这样做以智能的方式,因为在 CalendarDayButton 的内容要添加的任何可能需要将取决于由该按钮的确切日期,该信息不似乎随时可。
不要烦恼。 尽管在 CalendarDayButton 的内容将设置为一个简单的文本字符串,如 24,在 DataContext 属性的每个 CalendarDayButton 为实际的 DateTime 对象包括正确的月和年的该日期。 (此功能还有助于区分用于天的周标题 CalendarDayButton 对象 ; 这些按钮有一个空的内容但一个 DataContext 设置为文本字符串 su、 Mo,等。 此操作在 DataTemplate CalendarItem 中需要这些标题的原因。
DateTime 对象的可用性就可以增强功能可以实现只是通过引用新的 CalendarDayButton 模板中的一些自定义类的一个整个区域。 因为默认 CalendarDayButton 模板是大约 120 行 XAML,您可能不会在编写这样的模板从零开始。 为用 CalendarItem 模板您将可能希望将 generic.xaml 中的默认 CalendarDayButton 模板复制到自己的项目,然后修改它。
一种可能是显示在鼠标指针经过每日项目 RedLetterDays 所示时显示工具提示中的假日和其他重要的时间。 它看起来合理实现此功能为一个绑定转换器: 转换器实际上,将 DateTime 对象转换成字符串对象。
图 7 显示了此绑定转换器的重要部分。 构造函数为 6 月 (时它将被发布) 三月 (当我编写这篇文章) 之间的日期设置项目。 在实际的程序中它将可能访问这些日期和文本字符串从一个文件。
为 RedLetterDays 图 7 大部分的绑定转换器
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 与只有几个内容的包含该模板。 第一个是作为资源存储绑定转换器:
<ControlTemplate.Resources>
<src:RedLetterDayConverter x:Key="conv" />
</ControlTemplate.Resources>
外部网格 CalendarDayButton 模板中的然后引用该绑定转换器,以显示工具提示:
<Grid ToolTip="{Binding Converter={StaticResource conv},
Mode=OneWay}">
如果它丢失路径设置看起来的绑定,请注意按钮的 DataContext 一个 DateTime 对象并且的 DataContext 继承通过可视树。 如果您只希望显示工具提示将 DateTime,使用只是:
<Grid ToolTip="{Binding}">
该模板还具有其他用于突出显示当前日期和所选的日期的其他的 Rectangle 对象:
<Rectangle x:Name="RedLetterDayBackground"
IsHitTestVisible="False"
Fill="#80FF0000" />
一个 DataTrigger 使此矩形不可见绑定转换器返回 Null 时:
<DataTrigger
Binding="{Binding
Converter={StaticResource conv}}"
Value="{x:Null}">
<Setter TargetName="RedLetterDayBackground"
Property="Visibility"
Value="Hidden" />
</DataTrigger>
图 8 显示三个假日三月的月份。
图 8 </a0>-RedLetterDays 显示
在更多一般种情况下 CalendarDayButton 模板可视树可以具有 DateTime,类型的依赖关系属性的任何 FrameworkElement 派生,或者甚至任何 FrameworkElement 派生,假定其 DataContext 不 DateTime 类型。
本专栏的几个读者可能记得我创建 2008 年 1 月问题收的各个阶段的日历。 原始的 MoonDisk 类是从模拟收的太阳亮起的视图的 Viewport3D 派生的。 我消除 DateTime 依赖属性,从此类并修改稍有将转换为 DateTime 的 DataContext。 MoonPhaseCalendar 项目包含的 MoonDisk 类和 CalendarDayButton 模板的可视树中引用它就像这样:
<Grid Width="48" Height="48">
<ContentPresenter
x:Name="NormalText"
HorizontalAlignment="Left"
VerticalAlignment="Top">
<TextElement.Foreground>
<SolidColorBrush x:Name="selectedText"
Color="#FF333333"/>
</TextElement.Foreground>
</ContentPresenter>
<src:MoonDisk Margin="6" />
</Grid>
请注意还有未绑定 MoonDisk 因为它继承 DateTime 类型的 DataContext。 为 7 月 2009 月份结果显示在 图 9 。
图 9 </a0>-MoonPhaseCalendar 显示
从日历派生
CalendarItem、 CalendarButton,和 CalendarDayButton 是所有的密封类,您不能从这些,派生,并且即使可以派生 CalendarButton 和 CalendarDayButton,就需要重写的 CalendarItem 来实例化新的按钮而不是旧的部分。
可以,但是,派生从日历本身提供一些附加功能。 是例如可以增强日历控件,以便当您单击特定的日期上的时, 无模式对话框弹出,如 图 10 ,所示使您可以输入和查看当天的计划。 这是 DailyReminders 项目背后。
图 10 </a0>-DailyReminders 对话框
若要实现此功能,从日历我称为 DailyRemindersCalendar) 派生,类必须能够检测用户单击一个 CalendarDayButton。 通常这是非常简单: 从日历派生的类的该构造函数将调用 AddHandler 在 mouse.ClickEvent 和处理这些事件的方法的参数。 如您所知 WPF 实现路由的事件,以便从任何子按钮的 Click 事件传输设置可视树。 通过检查 RoutedEvent 对象的 Source 参数并还检查是否在 DataContext DateTime 类型的对象,该处理程序将 discriminate 不同的子按钮之间。
但是,此证明不希望成为可能。 CalendarItem 类安装 CalendarDayButton button-down 和按钮向上事件处理程序,并禁用 Click 事件。
相反,我编写了 DailyRemindersCalendar 重写 OnMouseDoubleClick 方法。 但是的引发另一个问题: 在一个日历派生源既 MouseButtonEventArgs OriginalSource 属性中的该事件为任何类型的按钮。 源为空,OriginalSource TextBlock 或路径或矩形。
再一次但是,提供通过可视树 DataContext 继承了要在恢复。 在此 OnMouseDoubleClick 覆盖如果 OriginalSource 对象具有一个 DataContext DateTime,类型的方法知道单击一个 CalendarDayButton,并 DateTime 对象通知该方法的一个。 该方法然后可以实例在 DailyRemindersDialog 生成其用户界面 (一个网格包含 TextBlock 和 TextBox 控件) 的化完全在代码中。 (在实际上,整个 DailyReminders 程序完全由包含没有标记代码)。
因此可以为不同的天同时显示对话框 modelessly,调用在 DailyRemindersDialog。 但是,此 DailyRemindersDialog 禁止为同一天显示多个对话框。 将引发问题涉及正确的存储和检索这些每日的提醒。 DailyRemindersStorage 类通过引用独立存储中的 XML 文件处理程序的该部分。 将关闭一个 DailyRemindersDialog 时它会导致该文件与更新的信息一起保存。
在 DatePicker 控件
我有重点以独占方式新的日历控件而不是在 DatePicker 因为 DatePicker 很大程度上的一个 DatePickerTextBox 和调用日历控件的一个下拉。
此 DatePickerTextBox 是非常粗略的。 当然它不实现功能,我喜欢,许多 Windows 窗体中 DateTimePicker 控件中,单击日期或时间 (年、 月、 日、 小时和等) 的各个组件,若要更改值中使用光标键的数字键。
但所有样式和可应用于独立 Calendar 控件的模板还可都适用对日历控件调用从 DatePicker 中下拉列表。 DatePicker 控件有一个名为 CalendarStyle 类型样式的属性,并且将设置为该属性的样式对象可以包含由日历,包括 CalendarItemStyle、 CalendarButtonStyle 和 CalendarDayButtonStyle 属性定义的任何属性的 setter。
尽管嵌入式的样式和模板一系列,可以实际上到达 Calendar 控件和 DatePicker 控件并调整其自己的需求和首选项。
将您的问题和提出的意见发送至 mmnet30@Microsoft.com.
Charles Petzold 是 longtime 到 MSDN Magazine 特约编辑。 他最新的著作是 The Annotated Turing: A Guided Tour through Alan Turing's Historic Paper on Computability and the Turing Machine (Wiley,2008)。 他的网站是 www.charlespetzold.com.