65.9K
CodeProject 正在变化。 阅读更多。
Home

创建无过程代码的 WPF Outlook 栏

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.78/5 (46投票s)

2008 年 7 月 11 日

CPOL

7分钟阅读

viewsIcon

122845

downloadIcon

3687

在本文中,我们将通过将 TabControl 转换为 OutlookBar,来展示 XAML 和 Control Templates 的强大功能,并且无需任何过程代码。

Outlook Bar - Before Styling (tab control)

Outlook Bar - After Styling

引言

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。运行它并尝试单击按钮/选项卡。内容窗格将在每个选项卡的内容之间切换。

A TabControl laid out like an Outlook bar

让我们看看这个模板的两个关键方面

<StackPanel IsItemsHost="True" DockPanel.Dock="Bottom" />

TabControl 继承自 ItemsControl。这意味着,像 ListBoxMenuTreeView 一样,它以某种方式显示项目列表,在本例中是 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>

对于我们的样式,我们指定了 TabItemTargetType - 这意味着它将影响当前范围内的所有 TabItems(即,在任何使用我们模板的 TabControl 中的 TabItems)。

如果我们此时查看,选项卡控件将看起来像这样

Outlook Bar - Before adding triggers

因此,此时我们的 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 严谨者(我将自己也归入此类)会注意到我们刚才完成的模型存在一些缺点。我已经在下面包含了注释,并将其留给感兴趣的读者作为练习。

  1. 原始的 Outlook Bar 包括底部的溢出面板和可用于更改全尺寸按钮数量的抓取器。这是一个行为修改,因此几乎不可能使用纯 XAML 来实现,这超出了本文的范围。也许可以通过一些代码和重新设计样式的 ToolBarToolBar 提供类似功能)来完成一些事情。我很乐意听到任何尝试过此操作的人的反馈。
  2. 在 Outlook Bar 中,您需要单击并释放按钮才能更改顶部的内容窗格,但是我们的控件在鼠标按下时更改窗格。这再次是使用 TabControl 作为我们的基础的结果。也许可以通过一些代码来改变这一点,我再次很想听听任何已在 TabControl 中成功实现此功能的人的反馈。
  3. 内容窗格上方的文本未设置。很容易连接一个事件,用于从代码设置此内容。
  4. 完成这项工作的最佳方法几乎肯定是一个基于 ItemsControl 的自定义控件。您应该能够重用上面几乎所有的模板,只需稍作修改。
  5. 如果要更改颜色,只需更改 ControlTemplate.Resource 元素中的画刷即可。

关注点

形状元素和布局系统

我在组装示例时,Line 元素带来了一些麻烦。我最初尝试在按钮模板顶部显示一条线完全失败了,看起来像这样

<Line Stroke="Navy" StrokeThickness="1" />

布局引擎为我的线条保留了空间,但没有绘制任何内容。我尝试了各种方法,直到我阅读了 MSDN 关于形状的帮助。为了使任何形状使用布局引擎中为其保留的整个空间,您需要设置 Stretch 属性。这似乎正是我所需要的,所以我适当地添加了 Stretch="Fill",但仍然没有帮助,直到我重新阅读了帮助并顿悟了。

形状使用它们自己的布局空间,然后设置 Stretch 属性意味着它会扩展该空间以填充分配给它的空间,同时尊重 HorizontalAlignmentVerticalAlignment 属性。但是,我没有在线的布局空间中设置任何属性,因此虽然它有一个布局空间,但其中没有绘制任何内容,因此没有什么可以拉伸的。一个小的改变产生了巨大的不同

<Line Stroke="Navy" StrokeThickness="1" Stretch="Fill" X2="1">

这在线条的布局空间中绘制了一条介于 0 和 1 之间的线条,Stretch 属性将其扩展以覆盖整个宽度。

像素对齐

您会注意到我大量使用了 SnapsToDevicePixels 属性。当您有水平或垂直的直线时,您会注意到抗锯齿通常会使它们看起来模糊。通过使用 SnapsToDevicePixels,您可以确保边缘始终落在完整的设备像素上,因此边缘保持清晰,就像在 Windows Forms 中使用经典的 GDI 样式绘图一样。对于对角线和曲线,通常最好使用正常的抗锯齿设置。

历史

  • 2008年7月6日 - 第一版
© . All rights reserved.