创建 Microsoft Outlook 约会视图(WPF)- 第三部分





5.00/5 (9投票s)
一个系列文章的第 3 部分,描述了使用 Outlook 的外观和感觉创建基于 WPF 的约会控件。
文章系列
本文是关于开发一个旨在实现 Microsoft Outlook 中日历视图功能的先进 WPF 控件系列文章的第一部分。
- 第 1 部分 - 简介、基本类设计和自定义项模板
- 第 2 部分 - 单个约会项的布局
- 第 3 部分 - 附加的 CalendarView 布局和单个时段的布局
引言
上一篇文章介绍了用于布局约会的一个关键部分的`RangePanel`的设计,该部分可以根据控件级别的附加属性和面板级别的标准依赖属性来按比例调整子控件的大小和排列。下一部分将在`CalendarView`控件模板周围添加一些额外的次要定义,以及单个时段的布局。
步骤 6 - 附加的 CalendarView 控件模板部分
为了更好地可视化未来部分的位置,下一步是通过更改其默认样式来向`CalendarView`控件模板添加一些内容。控件模板分为三个部分:标题、时间刻度和内容。
<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}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border Grid.Row="0" Grid.Column="1"
BorderBrush="Black"
BorderThickness="1,1,1,1">
<TextBlock x:Name="PART_Header"
Text="Header"
HorizontalAlignment="Center" />
</Border>
<Border Grid.Row="1" Grid.Column="0"
BorderBrush="Black"
BorderThickness="1,1,1,1">
<TextBlock x:Name="PART_TimeScale"
Text="Time Scale"
VerticalAlignment="Center">
<TextBlock.LayoutTransform>
<RotateTransform Angle="270" />
</TextBlock.LayoutTransform>
</TextBlock>
</Border>
<TextBlock x:Name="PART_ContentPresenter"
Grid.Row="1" Grid.Column="1"
Text="Content" HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
注意模板中那些按照“PART_[名称]”格式命名的部分。这是 WPF 模板组件命名的通用标准。命名模板部分的好处是可以稍后通过 `FrameworkTemplate.FindName` 方法在代码中引用这些部分。当控件的控件模板被替换时,这特别有用。例如,假设有一个基本的登录控件,需要用户 ID、密码和一个按钮。如果代码假定这些控件存在,那么其他人就可以通过重新设置控件样式来更改它,只要他们实现了基本契约。但是,除了文档之外,人们如何知道契约是什么呢?解决方案是提供类本身的元数据,形式为 `TemplatePartAttribute` 属性。像 `ListBox`、`ComboBox` 等类上存在类似的属性来提供契约信息。`CalendarView` 的属性在下面的代码中显示
[TemplatePart(Name="PART_Header", Type=typeof(TextBlock))]
[TemplatePart(Name="PART_TimeScale", Type=typeof(TextBlock))]
[TemplatePart(Name="PART_ContentPresenter", Type=typeof(TextBlock))]
public class CalendarView : ViewBase
{
}
步骤 7 - CalendarViewContentPresenter/CalendarViewPeriodPresenter 组件
本步骤将开始两个部分。第一个是 `CalendarViewContentPresenter`,它将替换模板中的“Content”`TextBox`。其目的是为 `CalendarView` 中的每个时段包含一个 `CalendarViewPeriodPresenter`。这带来了一个挑战。内容演示器将是 `ListViewItem` 控件的逻辑父级,但视觉上,它将是时段演示器控件的父级。这需要通过代码覆盖控件的视觉树。此过程的核心是覆盖 `Visual` 类中的 `VisualChildCount` 属性和 `GetVisualChild` 方法。
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using OutlookWpfCalendar.Windows.Controls;
namespace OutlookWpfCalendar.Windows.Primitives
{
public class CalendarViewContentPresenter : Panel
{
private UIElementCollection visualChildren;
private bool visualChildrenGenerated;
protected CalendarView CalendarView
{
get { return this.ListView.View as CalendarView; }
}
protected ListView ListView
{
get { return this.TemplatedParent as ListView; }
}
protected override Size ArrangeOverride(Size finalSize)
{
int columnCount = this.CalendarView.Periods.Count;
Size columnSize = new Size(finalSize.Width / columnCount,
finalSize.Height);
double elementX = 0;
foreach (UIElement element in this.visualChildren)
{
element.Arrange(new Rect(new Point(elementX, 0), columnSize));
elementX = elementX + columnSize.Width;
}
return finalSize;
}
protected override Size MeasureOverride(Size constraint)
{
this.GenerateVisualChildren();
return constraint;
}
protected override int VisualChildrenCount
{
get
{
if (this.visualChildren == null)
return base.VisualChildrenCount;
return this.visualChildren.Count;
}
}
protected override Visual GetVisualChild(int index)
{
if ((index < 0) || (index >= this.VisualChildrenCount))
throw new ArgumentOutOfRangeException("index",
index, "Index out of range");
if (this.visualChildren == null)
return base.GetVisualChild(index);
return this.visualChildren[index];
}
protected void GenerateVisualChildren()
{
if (this.visualChildrenGenerated)
return;
if (this.visualChildren == null)
visualChildren = this.CreateUIElementCollection(null);
else
visualChildren.Clear();
foreach (CalendarViewPeriod period in CalendarView.Periods)
this.visualChildren.Add(new CalendarViewPeriodPresenter()
{ Period = period });
this.visualChildrenGenerated = true;
}
}
}
要覆盖视觉树,将创建一个单独的 `UIElementCollection`,并通过 `GenerateVisualChildren` 方法进行填充。上述描述的 `Visual` 类属性和方法将被重定向到这个新集合(在不覆盖集合不存在的情况下使用默认实现)。生成视觉集合会创建一个分配给指定时段的 `CalendarViewPeriodPresenter`。视觉子项的生成与 `MeasureOverride` 方法绑定,以便在控件渲染时触发。但是,由于此方法在诸如大小调整之类的事件上被调用,因此使用一个标志来避免视觉子项的过多重新创建。将来可能的一个添加是监听 `Periods` 以在 `Periods` 集合更改时强制重新生成。在 `ArrangeOverride` 中,每个演示器控件都被排列成相等的列,填充控件的可用实际空间。
using System.Windows;
using System.Windows.Controls;
using OutlookWpfCalendar.Windows.Controls;
namespace OutlookWpfCalendar.Windows.Primitives
{
public class CalendarViewPeriodPresenter: Control
{
static CalendarViewPeriodPresenter()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(CalendarViewPeriodPresenter),
new FrameworkPropertyMetadata(typeof(CalendarViewPeriodPresenter)));
}
public CalendarViewPeriod Period { get; set; }
}
}
XAML
<Style TargetType="{x:Type primitives:CalendarViewPeriodPresenter}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type primitives:CalendarViewPeriodPresenter}">
<Border BorderBrush="Black" BorderThickness="1,1,1,1">
<TextBlock Text="{Binding RelativeSource={RelativeSource
TemplatedParent}, Path=Period.Header}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
此时,`CalendarViewPeriodPresenter` 控件只是一个带有 `Period` 属性的空控件。新的一部分是静态构造函数,它覆盖了控件的默认样式。如果没有这一行,将 `Generic.xaml` 中的样式放入就不会生效,因为时段演示器将使用其父级 `Control` 的默认样式。目前,该控件将只显示一个边框来勾勒每个时段所占的空间,并带有一个 `TextBlock` 来标识控件代表的时段。由于控件中现在已经以某种形式使用了日历时段,因此可以修改 `CalendarViewWindow` 以包含下面定义的 `CalendarView`,并附带一张显示更改结果的屏幕截图。
<controls:CalendarView ItemBeginBinding="{Binding Path=Start}"
ItemEndBinding="{Binding Path=Finish}">
<controls:CalendarView.Periods>
<controls:CalendarViewPeriod Begin="03/02/2009 12:00 AM"
End="03/02/2009 8:00 AM" Header="Monday" />
<controls:CalendarViewPeriod Begin="03/03/2009 12:00 AM"
End="03/03/2009 8:00 AM" Header="Tuesday" />
<controls:CalendarViewPeriod Begin="03/04/2009 12:00 AM"
End="03/04/2009 8:00 AM" Header="Wednesday" />
<controls:CalendarViewPeriod Begin="03/05/2009 12:00 AM"
End="03/05/2009 8:00 AM" Header="Thursday" />
<controls:CalendarViewPeriod Begin="03/06/2009 12:00 AM"
End="03/06/2009 8:00 AM" Header="Friday" />
</controls:CalendarView.Periods>
</controls:CalendarView>
步骤 8 - 生成和放置 ListViewItem 控件(忽略约会重叠)
下一步是将 `ListViewItem` 生成并放置在它们合适的位置。暂时,我们将忽略约会重叠问题。此步骤将相当冗长,需要进行大量更改。
首先,我们将修改 `CalendarViewContentPresenter` 以生成 `ListViewItem` 控件。通常,在自定义控件中不需要此步骤,因为它们已经生成(在 `ArrangeOverride` 等方法中设置一个断点,并检查 `base.Children` 属性来查看它们)。但是,这些 `Visual` 控件的视觉父级已经被设置为 `CalendarViewContentPresenter` 本身,这不是我们想要的。之所以在此处(而不是在每个时段演示器中生成它们,例如)执行此代码,是因为此步骤需要在设置为 `ItemsHost` 的控件内完成。
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using OutlookWpfCalendar.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Collections.Generic;
namespace OutlookWpfCalendar.Windows.Primitives
{
public class CalendarViewContentPresenter : Panel
{
// Pre-existing code removed to isolate changes
private List<UIElement> listViewItemVisuals;
private bool listViewItemVisualsGenerated;
internal List<UIElement> ListViewItemVisuals
{
get { return listViewItemVisuals; }
}
protected override Size MeasureOverride(Size constraint)
{
this.GenerateVisualChildren();
this.GenerateListViewItemVisuals();
return constraint;
}
protected void GenerateListViewItemVisuals()
{
if (this.listViewItemVisualsGenerated)
return;
IItemContainerGenerator generator =
((IItemContainerGenerator)
ListView.ItemContainerGenerator).GetItemContainerGeneratorForPanel(this);
generator.RemoveAll();
if (this.listViewItemVisuals == null)
this.listViewItemVisuals = new List<UIElement>();
else
this.listViewItemVisuals.Clear();
using (generator.StartAt(new GeneratorPosition(-1, 0),
GeneratorDirection.Forward))
{
UIElement element;
while ((element = generator.GenerateNext() as UIElement) != null)
{
this.listViewItemVisuals.Add(element);
generator.PrepareItemContainer(element);
}
this.listViewItemVisualsGenerated = true;
}
}
}
}
生成后,每个 `ListViewItem` 都需要设置 `RangePanel` 的 `Begin`/`End` 附加属性。这可以通过创建 `DateTime` 附加属性来完成,这些属性将链接到每个 `ListViewItem`。通过这种方式,就可以询问每个 `ListViewItem` 约会的开始和结束时间。这如何被评估?通过使用绑定到 `CalendarView` 实例(`ItemBeginBinding`、`ItemEndBinding`)的绑定。
public class CalendarView : ViewBase
{
public static DependencyProperty BeginProperty =
DependencyProperty.RegisterAttached("Begin",
typeof(DateTime), typeof(ListViewItem));
public static DependencyProperty EndProperty =
DependencyProperty.RegisterAttached("End",
typeof(DateTime), typeof(ListViewItem));
// Pre-existing code removed to isolate changes
public static DateTime GetBegin(DependencyObject item)
{
return (DateTime)item.GetValue(BeginProperty);
}
public static DateTime GetEnd(DependencyObject item)
{
return (DateTime)item.GetValue(EndProperty);
}
public static void SetBegin(DependencyObject item, DateTime value)
{
item.SetValue(BeginProperty, value);
}
public static void SetEnd(DependencyObject item, DateTime value)
{
item.SetValue(EndProperty, value);
}
protected override void PrepareItem(ListViewItem item)
{
item.SetBinding(BeginProperty, ItemBeginBinding);
item.SetBinding(EndProperty, ItemEndBinding);
}
}
在对内容演示器和 `CalendarView` 进行更改后,可以修改 `CalendarViewPeriodPresenter`。这是对现有占位符(它只是一个包含 `TextBlock` 的控件模板)的替换。修改的一部分包括从 `Generic.xaml` 中删除现有的默认样式,因为视觉树将通过代码创建。代码的许多部分看起来会很熟悉,因为与内容演示器中使用的“通过代码覆盖视觉树”的概念相同。主要有两个更改。首先,此时唯一的视觉子项是单个 `RangePanel`。暂时忽略约会重叠,但由于将来的更改可能需要每个时段不止一个 `RangePanel`,因此将使用集合而不是单个实例。第二个添加是 `RangePanel` 本身的初始化。`RangePanel` 的 `Minimum` 和 `Maximum` 将绑定到提供的 `Period` 实例的 `Begin` 和 `End`(使用 `Ticks` 属性提供一个 `double` 值)。此外,会检查每个 `ListViewItem` 是否属于该 `Period`,如果是,则将其添加到 `RangePanel`。定义项的 `Begin` 和 `End` 的 `RangePanel` 附加属性将根据上面创建的 `CalendarView` 附加属性进行设置。
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using OutlookWpfCalendar.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Collections.Generic;
namespace OutlookWpfCalendar.Windows.Primitives
{
public class CalendarViewPeriodPresenter: Panel
{
private bool visualChildrenGenerated;
private UIElementCollection visualChildren;
static CalendarViewPeriodPresenter()
{
DefaultStyleKeyProperty.OverrideMetadata(
typeof(CalendarViewPeriodPresenter),
new FrameworkPropertyMetadata(typeof(CalendarViewPeriodPresenter)));
}
public CalendarViewPeriod Period { get; set; }
public ListView ListView { get; set; }
public CalendarView CalendarView { get; set; }
private CalendarViewContentPresenter ContentPresenter
{
get
{
return (CalendarViewContentPresenter)this.Parent;
}
}
protected override int VisualChildrenCount
{
get
{
if (this.visualChildren == null)
return base.VisualChildrenCount;
return this.visualChildren.Count;
}
}
protected override Visual GetVisualChild(int index)
{
if ((index < 0) || (index >= this.VisualChildrenCount))
throw new ArgumentOutOfRangeException("index",
index, "Index out of range");
if (this.visualChildren == null)
return base.GetVisualChild(index);
return this.visualChildren[index];
}
protected override Size ArrangeOverride(Size finalSize)
{
foreach (UIElement element in this.visualChildren)
element.Arrange(new Rect(new Point(0, 0), finalSize));
return finalSize;
}
protected override Size MeasureOverride(Size constraint)
{
this.GenerateVisualChildren();
return constraint;
}
protected void GenerateVisualChildren()
{
if (visualChildrenGenerated)
return;
if (this.visualChildren == null)
this.visualChildren = this.CreateUIElementCollection(null);
else
this.visualChildren.Clear();
RangePanel panel = new RangePanel();
panel.SetBinding(RangePanel.MinimumProperty,
new Binding("Begin.Ticks") { Source = Period });
panel.SetBinding(RangePanel.MaximumProperty,
new Binding("End.Ticks") { Source = Period });
foreach (ListViewItem item in this.ContentPresenter.ListViewItemVisuals)
{
if (this.CalendarView.PeriodContainsItem(item, this.Period))
{
item.SetValue(RangePanel.BeginProperty,
Convert.ToDouble(((DateTime)item.GetValue(
CalendarView.BeginProperty)).Ticks));
item.SetValue(RangePanel.EndProperty,
Convert.ToDouble(((DateTime)item.GetValue(
CalendarView.EndProperty)).Ticks));
panel.Children.Add(item);
}
}
Border border = new Border() { BorderBrush = Brushes.Blue,
BorderThickness = new Thickness(1.0) };
border.Child = panel;
visualChildren.Add(border);
this.visualChildrenGenerated = true;
}
}
}
public class CalendarView : ViewBase
{
// Pre-existing code removed to isolate changes
public bool PeriodContainsItem(ListViewItem item, CalendarViewPeriod period)
{
DateTime itemBegin = (DateTime)item.GetValue(BeginProperty);
DateTime itemEnd = (DateTime)item.GetValue(EndProperty);
return (((itemBegin <= period.Begin) &&
(itemEnd >= period.Begin)) ||
((itemBegin <= period.End) &&
(itemEnd >= period.Begin)));
}
}
所做的更改将产生下面显示的如下结果,表明控件现在开始成形。
后续步骤
前面两个部分涵盖了许多高级 WPF 主题。控件开发的后续步骤将更加直接;更侧重于视觉效果,而较少侧重于功能,因为控件的标题和时间刻度部分将得到填充。
历史
- 2009/04/21:初始版本。