创建 WPF 中的 Microsoft Outlook 约会视图 - 第一部分





5.00/5 (14投票s)
一系列文章的第一部分,
文章系列
本文是系列文章的第一部分,介绍如何开发一个高级WPF控件,该控件旨在模仿Microsoft Outlook中的日历视图,如下所示。

- 第1部分 - 介绍、基本类设计与自定义项模板
- 第2部分 - 单个约会项的布局
- 第3部分 - 附加的日历视图布局和单个时段的布局
引言
大约六个月前,我开始了一项学习Windows Presentation Foundation (WPF) 的计划。作为该计划的一部分,我决定需要一个项目来运用我的技能,一个有足够多样的挑战需要解决,但同时又能最大限度地减少我需要编写的非WPF代码的项目。我决定基于Microsoft Outlook编写一个WPF应用程序,利用Outlook COM API作为底层的领域层。在某个时候,我决定构建必要的控件和组件来显示日历视图。在本系列文章中,我将提供该控件的设计,既为了发布代码,也通过解释过程中所做的设计决策来帮助其他开发者开发自定义控件。
要求
在开发该控件时,我确定了一系列需求:
- 支持约会数据的多种视图(单日、单周等)
- 支持将底层数据绑定到任意对象集合,而不是强制使用预定义的
Appointment
类 - 支持“无外观”控件设计
- 支持对控件所有视觉方面进行自定义样式设置
- 支持约会重叠(例如,一个约会从下午1点到3点,另一个约会从下午2点到4点)
- 支持在单个视图中显示多天(类似于Microsoft Outlook中的“周视图”)
- 支持在其他适用的场景中重用底层类(以避免所有代码都针对
Appointment
视图) - 支持显示的日历时段的任意开始和结束
例如,而不是支持显示从午夜到午夜的某一天,而是允许显示上午9点到下午6点,甚至跨越2天的48小时时段。
背景
本系列文章需要读者对Windows Presentation Foundation有深入的了解,因为它将涉及项目容器生成器、自定义布局控件等主题。
第1步 - 基本控件类设计
第一步是确定控件本身的基本类设计以及需要哪些属性。我有两个主要选择:使用ListView
/ViewBase
组合,或者继承自Selector
(ListBox
和ComboBox
等控件的父类)。为了支持多种视图,我决定利用ListView
组件,该组件通过ListView.View
属性(类型为ViewBase
)支持底层数据视图的多种视图。ListView
最常用的视图是GridView
。MSDN库中有一个关于创建自定义ViewBase
类的示例在这里。由于使用了ListView
/ViewBase,这在某些方面与继承自Selector
之类的类不同,因此并非所有设计点都直接适用于更标准的自定义控件创建。
下一步是定义该类所需的属性。为了满足在单个视图中显示多天的需求,我遵循了类似于GridView
的设计,它有一个包含单个GridViewColumn
实例的Columns
集合。类似地,CalendarView
类将需要一个CalendarViewPeriod
数据类和一个CalendarViewPeriodCollection
集合类。同样,我使用了GridView
辅助类来确定要使用的适当基类。将CalendarViewPeriod
继承自DependencyObject
是允许数据绑定到类的各种属性所必需的。将CalendarViewPeriodCollection
继承自ObservableCollection
会自动提供监听列表修改(添加/删除/插入/替换等)的能力,这将在以后需要。
using System;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace OutlookWpfCalendar.Windows.Controls
{
public class CalendarView: ViewBase
{
private CalendarViewPeriodCollection periods =
new CalendarViewPeriodCollection();
public CalendarViewPeriodCollection Periods
{
get { return periods; }
}
}
public class CalendarViewPeriod: DependencyObject
{
}
public class CalendarViewPeriodCollection: ObservableCollection<CalendarViewPeriod>
{
}
}
每个时段都需要属性来定义其开始和结束时间,以及标题(此时不会使用)。其中每个都将被定义为依赖属性以支持数据绑定。
public class CalendarViewPeriod: DependencyObject
{
public static readonly DependencyProperty BeginProperty =
DependencyProperty.Register("Begin", typeof(DateTime),
typeof(CalendarViewPeriod));
public static readonly DependencyProperty EndProperty =
DependencyProperty.Register("End", typeof(DateTime),
typeof(CalendarViewPeriod));
public static readonly DependencyProperty HeaderProperty =
DependencyProperty.Register("Header", typeof(object),
typeof(CalendarViewPeriod));
public DateTime Begin
{
get { return (DateTime)this.GetValue(BeginProperty); }
set { this.SetValue(BeginProperty, value); }
}
public DateTime End
{
get { return (DateTime)this.GetValue(EndProperty); }
set { this.SetValue(EndProperty, value); }
}
public object Header
{
get { return (object)this.GetValue(HeaderProperty); }
set { this.SetValue(HeaderProperty, value); }
}
}
为了支持绑定到任意集合而不是固定约会类的集合,有必要提供一种方法来询问ItemsSource
中的每个对象该约会的开始和结束时间。这将通过请求绑定来完成,就像GridViewColumn
类需要绑定(DisplayMemberBinding
)来从特定项中提取值以便在GridView
的该列中显示一样。
public class CalendarView: ViewBase
{
// Existing code excluded to emphasize changes
public BindingBase ItemBeginBinding { get; set; }
public BindingBase ItemEndBinding { get; set; }
}
第2步 - 基本视觉树
下一步是定义渲染ViewBase
的底层视觉树所使用的样式,以及渲染每个ListViewItem
(每个ListViewItem
是一个用于渲染ListView
的ItemsSource
中包含的单个数据对象的控件)所使用的样式。这可以通过在CalendarView
类中重写两个ViewBase
属性,并在Themes/Generic.xaml文件中添加两个样式来完成。请注意ComponentResourceKey
类的使用。虽然直接返回一个原始string
是合法的,但将来可能会发生名称冲突。ComponentResourceKey
本质上添加了一个Type
实例的限定符,允许具有相同“名称”(或在此例中为资源ID)的多个样式。第一个样式针对整个ListView
,目前只渲染一个StackPanel
,并将各个ListViewItem
实例包含在其中(通过将IsItemsHost
设置为true
)。第二个样式针对每个ListViewItem
,并渲染一个绑定到底层数据对象(在此例中为约会)的TextBlock
,这将渲染调用数据对象的ToString()
方法的结果。
public class CalendarView: ViewBase
{
// Existing code excluded to emphasize changes
protected override object DefaultStyleKey
{
get { return new ComponentResourceKey(this.GetType(), "DefaultStyleKey"); }
}
protected override object ItemContainerDefaultStyleKey
{
get { return new ComponentResourceKey(this.GetType(),
"ItemContainerDefaultStyleKey"); }
}
}
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:OutlookWpfCalendar.Windows.Controls">
<Style x:Key="{ComponentResourceKey TypeInTargetAssembly=
{x:Type controls:CalendarView}, ResourceId=DefaultStyleKey}"
TargetType="{x:Type ListView}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListView}">
<StackPanel IsItemsHost="True" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="{ComponentResourceKey TypeInTargetAssembly=
{x:Type controls:CalendarView},
ResourceId=ItemContainerDefaultStyleKey}"
TargetType="{x:Type ListViewItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListViewItem}">
<TextBlock Text="{Binding}" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
第3步 - 使用自定义项模板显示初始视图
下载中包含一个示例应用程序,其中包含一个简单的Appointment
类和以下CalendarView
类实例(以及底层结果)。
<Window x:Class="OutlookWpfCalendar.UI.CalendarViewWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:OutlookWpfCalendar.UI"
xmlns:controls="clr-namespace:OutlookWpfCalendar.Windows.Controls;
assembly=OutlookWpfCalendar"
Title="Calendar View Window" Height="500" Width="400">
<Grid>
<ListView>
<ListView.View>
<controls:CalendarView ItemBeginBinding="{Binding Path=Start}"
ItemEndBinding="{Binding Path=Finish}">
<controls:CalendarView.Periods>
<controls:CalendarViewPeriod Header="Monday"
Begin="03/02/2009 12:00 AM" End="03/03/2009 12:00 AM" />
<controls:CalendarViewPeriod Header="Tuesday"
Begin="03/03/2009 12:00 AM" End="03/04/2009 12:00 AM" />
<controls:CalendarViewPeriod Header="Wednesday"
Begin="03/04/2009 12:00 AM" End="03/05/2009 12:00 AM" />
<controls:CalendarViewPeriod Header="Thursday"
Begin="03/05/2009 12:00 AM" End="03/06/2009 12:00 AM" />
<controls:CalendarViewPeriod Header="Friday"
Begin="03/06/2009 12:00 AM" End="03/07/2009 12:00 AM" />
</controls:CalendarView.Periods>
</controls:CalendarView>
</ListView.View>
<ListView.Items>
<local:Appointment Start="03/02/2009 2:00 AM"
Finish="03/02/2009 3:00 AM" Subject="Meet with John"
Location="Southwest Meeting Room" Organizer="Jim Smith" />
<local:Appointment Start="03/03/2009 4:00 AM"
Finish="03/03/2009 5:00 AM" Subject="Meet with Rick"
Location="Southwest Meeting Room" Organizer="Jim Smith" />
<local:Appointment Start="03/04/2009 6:00 AM"
Finish="03/04/2009 6:30 AM" Subject="Meet with Dave"
Location="Southwest Meeting Room" Organizer="Jim Smith" />
<local:Appointment Start="03/02/2009 1:30 AM"
Finish="03/02/2009 5:00 AM" Subject="Meet with Larry"
Location="Southwest Meeting Room" Organizer="Jim Smith" />
<local:Appointment Start="03/03/2009 4:30 AM"
Finish="03/03/2009 7:30 AM" Subject="Meet with Jim"
Location="Southwest Meeting Room" Organizer="Jim Smith" />
</ListView.Items>
</ListView>
</Grid>
</Window>
显然,约会还需要更好的样式,这需要为包含每个Appointment
的各个ListViewItem
控件定义一个样式。这可以是一个全局样式,也可以像下面那样,在ListView
的上下文内通过ItemContainerStyle
属性来定义。
<ListView.ItemContainerStyle>
<Style TargetType="{x:Type ListViewItem}">
<Style.Setters>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListViewItem}">
<Border BorderBrush="#5076A7" BorderThickness="1,1,1,1"
CornerRadius="4,4,4,4">
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#FFFFFF" Offset="0.0" />
<GradientStop Color="#C0D3EA" Offset="1.0" />
</LinearGradientBrush>
</Border.Background>
<StackPanel TextElement.FontFamily="Segoe UI"
TextElement.FontSize="12">
<TextBlock FontWeight="Bold" Padding="3,0,0,0"
Text="{Binding Path=Subject}" />
<TextBlock Padding="3,0,0,0"
Text="{Binding Path=Location}" />
<TextBlock Padding="3,0,0,0"
Text="{Binding Path=Organizer}" />
</StackPanel>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style.Setters>
</Style>
</ListView.ItemContainerStyle>
后续步骤
在本文中,我们设计并实现了初始类结构。重点更多地放在类设计而不是视觉布局上。除了单个约会项的外观,这只是一个可以在普通ListBox
控件中应用的样式,实际上并没有什么能表明这是Microsoft Outlook日历UI。在接下来的文章中,我们将构建创建真实外观和感觉所需的实际控件。这与我开发控件的方式相符,首先关注提供必要灵活性的XAML(这驱动了类和属性设计),然后确定构成整体的视觉布局的各个部分。
历史
- 2009年4月15日:初始版本
- 2009年4月21日:更新文章