创建无过程代码的 WPF Outlook 栏






4.78/5 (46投票s)
在本文中,

引言
WPF 的一个优点是它将控件的行为与其呈现分离开来。您可以选择任何控件,通过更改模板和一些样式,使其看起来完全不同。在本教程中,我们将介绍其原理,并创建一个控件模板,将 TabControl
转换为 Office 2007 中的 Outlook Bar。这都是纯 XAML,无需代码!
背景
假定您对 XAML 和 WPF 有基本了解,包括对不同布局面板、资源和绑定的知识。
开始
让我们从在 XAML 中创建选项卡控件开始
<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" >
<TabControl Name="monkey">
<TabItem Header="Mail" IsSelected="True">
<ListBox BorderThickness="0">
<ListBoxItem>Your mail here.</ListBoxItem>
</ListBox>
</TabItem>
<TabItem Header="Calendar" />
<TabItem Header="Tasks" />
</TabControl>
</Page>
正如您所看到的,我们有一个非常基本的 TabControl
。目前没有什么特别或令人兴奋的。
基本控件模板
控件模板是 WPF 如此强大的原因。您可以将控件模板视为控件的整个“外观”。通过替换默认控件模板,我们可以完全改变其外观,同时保持行为不变。让我们为我们的 TabControl
添加一个新的模板
<Page.Resources>
<ControlTemplate x:Key="OutlookBar" TargetType="{x:Type TabControl}">
<ControlTemplate.Resources>
<SolidColorBrush x:Key="BorderBrush" Color="#6593CF" />
</ControlTemplate.Resources>
<Border BorderBrush="{StaticResource BorderBrush}" BorderThickness="1"
SnapsToDevicePixels="True" >
<DockPanel>
<StackPanel IsItemsHost="True" DockPanel.Dock="Bottom" />
<ContentPresenter Content="{TemplateBinding SelectedContent}" />
</DockPanel>
</Border>
</ControlTemplate>
</Page.Resources>
现在我们可以通过将 Template
属性设置为我们的新控件模板来将其连接到 TabControl
<TabControl Name="monkey" Template="{StaticResource OutlookBar}">
现在我们的 TabControl
有了一个新的控件模板。如果您在 Visual Studio 或 XAMLPad 中查看它,您会看到一个惊人的转变已经发生。它已经呈现出 Outlook Bar 的形状。诚然,它不是一个非常好看的 Outlook Bar,但或多或少是一个功能齐全的 Outlook Bar。运行它并尝试单击按钮/选项卡。内容窗格将在每个选项卡的内容之间切换。

让我们看看这个模板的两个关键方面
<StackPanel IsItemsHost="True" DockPanel.Dock="Bottom" />
TabControl
继承自 ItemsControl
。这意味着,像 ListBox
、Menu
或 TreeView
一样,它以某种方式显示项目列表,在本例中是 TabItems
。为了让 ControlTemplate
呈现列表,我们在模板中放置一个容器面板,并将 IsItemsHost
属性设置为 True
。(我使用了 StackPanel
,但它也可以是任何其他容器)。
<ContentPresenter Content="{TemplateBinding SelectedContent}" />
我们需要在 Outlook Bar 顶部的内容窗格中显示所选选项卡的内容。幸运的是,TabControl
上有一个依赖属性允许我们这样做 - SelectedContent
。为了连接它,我们使用模板绑定。模板绑定将模板连接到应用模板的对象的属性。语法就是 {TemplateBinding PropertyName}
。这是一种令人愉快地简单而优雅的方法。
添加样式
我们已经完善了布局,但是我们的 Outlook bar 看起来像一个从丑陋的树上掉下来的 TabControl
,一路撞到了每个树枝。我们需要让选项卡看起来像带有适当字体、背景和高亮的 Outlook bar 按钮。为此,我们需要使用样式。
样式所做的就是设置控件上的一组属性。它们旨在让您只需更改一个属性,就能使一组控件看起来或行为相同。
让我们添加更多可用于样式的资源。
<ControlTemplate.Resources>
<SolidColorBrush x:Key="CaptionBrush" Color= "#15428B" />
<SolidColorBrush x:Key="BorderBrush" Color="#6593CF" />
<LinearGradientBrush x:Key="LabelBrush" StartPoint="0, 0" EndPoint="0,1">
<GradientStop Color="#E3EFFF" Offset="0" />
<GradientStop Color="#AFD2FF" Offset="1" />
</LinearGradientBrush>
</ControlTemplate.Resources>
现在让我们重新设置 TabItems
的样式
<Style TargetType="{x:Type TabItem}">
<Setter Property="Background" Value="{StaticResource ButtonNormalBrush}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabItem}">
<Grid Background="{TemplateBinding Background}" MinHeight="32">
<Line Stroke="{StaticResource BorderBrush}" VerticalAlignment="Top"
Stretch="Fill" X2="1" SnapsToDevicePixels="True" />
<ContentPresenter Margin="5,0,5,0" TextBlock.FontFamily="Tahoma"
TextBlock.FontSize="8pt" TextBlock.FontWeight="Bold"
TextBlock.Foreground="{StaticResource CaptionBrush}"
Content="{TemplateBinding Header}" VerticalAlignment="Center"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
对于我们的样式,我们指定了 TabItem
的 TargetType
- 这意味着它将影响当前范围内的所有 TabItems
(即,在任何使用我们模板的 TabControl
中的 TabItems
)。
如果我们此时查看,选项卡控件将看起来像这样

因此,此时我们的 TabControl
开始看起来像一个 Outlook bar。但是,当我们将鼠标悬停在 Outlook 中的按钮上时,按钮会改变颜色以指示它。我们还需要一种方法来突出显示哪个按钮被选中。我们在 Windows Form 控件中执行此操作的传统方法是使用事件。使用 WPF 仍然可以选择该选项,它具有非常丰富的事件模型。但是,本文的目标是无需使用代码即可完成此操作,WPF 为我们提供了一种非常简洁的无需代码即可完成此操作的方法 - 触发器。
触发器
您可以将触发器视为条件样式。让我们看看一个触发器
<Trigger Property="IsSelected" Value="False">
<Setter Property="TextElement.Foreground" Value="{StaticResource CaptionBrush}" />
</Trigger>
如您所见,当 IsSelected
属性设置为 false
时,此触发器会设置文本颜色。就这么简单。
您可以使用 MultiTrigger
将触发器设置为依赖于多个条件
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsSelected" Value="False" />
<Condition Property="IsMouseOver" Value="False" />
</MultiTrigger.Conditions>
<MultiTrigger.Setters>
<Setter Property="Background" Value="{StaticResource ButtonNormalBrush}" />
</MultiTrigger.Setters>
</MultiTrigger>
所有条件都需要满足触发器才能生效,因此对于上面那个,如果按钮未选中并且鼠标未悬停在其上方,则会设置背景画刷。
对于我们的 TabItem
,我们将触发器放在 ControlTemplate.Triggers
元素中,如下所示
<ControlTemplate TargetType="{x:Type TabItem}">
<ControlTemplate.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsSelected" Value="False" />
<Condition Property="IsMouseOver" Value="False" />
</MultiTrigger.Conditions>
<MultiTrigger.Setters>
<Setter Property="Background" Value="{StaticResource ButtonNormalBrush}" />
</MultiTrigger.Setters>
</MultiTrigger>
<!-- More Triggers here... -->
</ControlTemplate.Triggers>
<Grid Background="{TemplateBinding Background}"
MinHeight="32" SnapsToDevicePixels="True">
<Line Stroke="{StaticResource BorderBrush}"
VerticalAlignment="Top" Stretch="Fill" X2="1" SnapsToDevicePixels="True" />
<ContentPresenter Margin="5,0,5,0" TextBlock.FontFamily="Tahoma"
TextBlock.FontSize="8pt" TextBlock.FontWeight="Bold"
TextBlock.Foreground="{StaticResource CaptionBrush}"
Content="{TemplateBinding Header}" VerticalAlignment="Center"/>
</Grid>
</ControlTemplate>
为清晰起见,我没有包含所有上述触发器。其余触发器包含在源文件中。
至此,我们差不多完成了。剩下的就是添加顶部的标签,这非常简单
<Border BorderBrush="{StaticResource BorderBrush}"
BorderThickness="1" SnapsToDevicePixels="True" >
<DockPanel>
<StackPanel DockPanel.Dock="Bottom" IsItemsHost="True" />
<!-- Top label -->
<Grid DockPanel.Dock="Top" MinHeight="28"
Background="{StaticResource ButtonNormalBrush}" SnapsToDevicePixels="True">
<TextBlock FontFamily="Tahoma" Foreground="{StaticResource CaptionBrush}"
VerticalAlignment="Center" Margin="5,0" FontSize="18" FontWeight="Bold" />
<Line Stroke="{StaticResource BorderBrush}"
VerticalAlignment="Bottom" X2="1" Stretch="Fill"/>
</Grid>
<ContentPresenter Content="{TemplateBinding SelectedContent}" />
</DockPanel>
</Border>
至此,我们完成了!
下一步
你们中间的 UI 严谨者(我将自己也归入此类)会注意到我们刚才完成的模型存在一些缺点。我已经在下面包含了注释,并将其留给感兴趣的读者作为练习。
- 原始的 Outlook Bar 包括底部的溢出面板和可用于更改全尺寸按钮数量的抓取器。这是一个行为修改,因此几乎不可能使用纯 XAML 来实现,这超出了本文的范围。也许可以通过一些代码和重新设计样式的
ToolBar
(ToolBar
提供类似功能)来完成一些事情。我很乐意听到任何尝试过此操作的人的反馈。 - 在 Outlook Bar 中,您需要单击并释放按钮才能更改顶部的内容窗格,但是我们的控件在鼠标按下时更改窗格。这再次是使用
TabControl
作为我们的基础的结果。也许可以通过一些代码来改变这一点,我再次很想听听任何已在TabControl
中成功实现此功能的人的反馈。 - 内容窗格上方的文本未设置。很容易连接一个事件,用于从代码设置此内容。
- 完成这项工作的最佳方法几乎肯定是一个基于
ItemsControl
的自定义控件。您应该能够重用上面几乎所有的模板,只需稍作修改。 - 如果要更改颜色,只需更改
ControlTemplate.Resource
元素中的画刷即可。
关注点
形状元素和布局系统
我在组装示例时,Line
元素带来了一些麻烦。我最初尝试在按钮模板顶部显示一条线完全失败了,看起来像这样
<Line Stroke="Navy" StrokeThickness="1" />
布局引擎为我的线条保留了空间,但没有绘制任何内容。我尝试了各种方法,直到我阅读了 MSDN 关于形状的帮助。为了使任何形状使用布局引擎中为其保留的整个空间,您需要设置 Stretch
属性。这似乎正是我所需要的,所以我适当地添加了 Stretch="Fill"
,但仍然没有帮助,直到我重新阅读了帮助并顿悟了。
形状使用它们自己的布局空间,然后设置 Stretch
属性意味着它会扩展该空间以填充分配给它的空间,同时尊重 HorizontalAlignment
和 VerticalAlignment
属性。但是,我没有在线的布局空间中设置任何属性,因此虽然它有一个布局空间,但其中没有绘制任何内容,因此没有什么可以拉伸的。一个小的改变产生了巨大的不同
<Line Stroke="Navy" StrokeThickness="1" Stretch="Fill" X2="1">
这在线条的布局空间中绘制了一条介于 0 和 1 之间的线条,Stretch
属性将其扩展以覆盖整个宽度。
像素对齐
您会注意到我大量使用了 SnapsToDevicePixels
属性。当您有水平或垂直的直线时,您会注意到抗锯齿通常会使它们看起来模糊。通过使用 SnapsToDevicePixels
,您可以确保边缘始终落在完整的设备像素上,因此边缘保持清晰,就像在 Windows Forms 中使用经典的 GDI 样式绘图一样。对于对角线和曲线,通常最好使用正常的抗锯齿设置。
历史
- 2008年7月6日 - 第一版