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

Visual Studio 2012 WPF 的 Metro 样式

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.77/5 (74投票s)

2012年8月21日

CPOL

8分钟阅读

viewsIcon

368519

downloadIcon

60915

用于 Button、ListBox、Menu、ScrollBar、TabControl、TextBox、ComboBox、DataGrid 和 GroupBox 的黑色 Metro 样式

目录

介绍

尽管关于微软的“Metro”设计概念是好是坏仍然存在激烈争论,但WPF开发人员,如果追随这一趋势,现在可以在设计样式时将这一概念铭记于心。但尽管许多人一听到“Metro”这个词就想到Windows 8的磁贴墙,但这个概念本身实际上远不止于此。其核心思想是保持事物简单。不再有渐变,不再有花哨、闪烁、玻璃质感、炫目的表面。毫无疑问,最能体现这一概念的应用之一是新的Visual Studio 2012。本文将介绍一些类似Visual Studio的WPF样式。

“类似 Visual Studio 的样式”意味着,我当然没有复制样式(使用反编译器或其他工具),所以它们实际上并不完全相同,但我使用了相同的颜色,并且我的演示应用程序的结构也像 Visual Studio。我稍后会讨论一些主要区别。

截图

首先是一些最终效果的截图,向你展示我所说的

使用样式

为了稍微组织一下样式,我将每个样式都放在一个不同的 ResourceDictionary 中,并以要设置样式的控件命名。如果您不想使用所有样式,则必须通过 MergedDictionaries 自己嵌入每个 ResourceDictionary。在这种情况下,还需要明确引用样式,因为它们都有一个键。如果您想使用完整的样式集,只需将 Styles.xaml ResourceDictionary 添加到 MergedDictionaries 中。

使用完整的样式集

App.xaml

<Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary 
                Source="pack://application:,,,/Selen.Wpf.SystemStyles;component/Styles.xaml"/>
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Application.Resources>

样式将自动应用于每个控件

使用其中一部分

App.xaml

<Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary 
                Source="pack://application:,,,/Selen.Wpf.SystemStyles;component/ButtonStyles.xaml"/>
            <ResourceDictionary 
                Source="pack://application:,,,/Selen.Wpf.SystemStyles;component/TextBoxStyles.xaml"/>
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Application.Resources>

在您想要使用样式的地方

<Button Content="blastallenemies.cmd" Style="{StaticResource LinkButton}"/>

所有样式列表

为方便将来参考,以下是所有样式及其对应键的完整列表

目标类型样式键
ButtonStandardButton
ButtonLinkButton
TabControlStandardTabControl
MenuStandardMenu
ListBoxStandardListBox
滚动条StandardScrollBar
文本框StandardTextBox
文本框SearchTextBox
DataGridStandardDataGrid
ComboBoxStandardComboBox
GroupBoxStandardGroupBox

概念

关于Metro的一些话

在我们讨论WPF实现之前,我想总结一下我们追求的总体目标

  • 简而言之,Metro 意味着专注于内容
  • 只有锐利的边缘
  • 没有模糊的线条
  • 没有渐变
  • 只有少数几种颜色(并非总是如此,但我喜欢它,因为它再次强调了内容的重要性)

由于我专注于黑色风格,我们可以添加以下两条规则

  • 深色背景
  • 白色前景

这意味着什么?

现在我们知道我们想要什么了,那么让我们看看这在 XAML 中意味着什么

颜色

由于颜色种类不多,我决定将它们全部放在一个独立的 ResourceDictionary (Selen.Wpf.Core 中的 Resources.xaml) 中,以便于访问。我们基本上有:

  • 窗口 Background (我通过一个单独的窗口样式应用了它,之前我认为不值得提及,因为它除了这个什么也没做)
  • 所有元素的 Foreground
  • 普通、高亮和选定状态的 BackgroundBorderBrush (许多控件相同)

边框

边框可以在(几乎)所有样式中找到。由于它们都看起来相似,我们可以对它们说一些通用的话:

  • BorderThickness 应等于 1 或 0(大边框不适合 Metro 风格)
  • CornerRadius 应始终为 0(无圆角)
  • BackgroundBorderBrush 应根据需要设置为提到的颜色
  • SnapsToDevicePixels 应设置为 true。对于那些不了解此属性的人来说:它基本上告诉 WPF 关闭抗锯齿,从而在边框及其环境之间进行硬切割。这可以防止我们的边框显得模糊。

注意:这些规则有一些例外,因为我有时使用 Border 来做除了创建实际边框之外的其他事情,我们稍后会看到。

示例

到目前为止最好的例子是 Button 样式,因为它只由一个 Border 组成。所以,就是这样,没什么好惊讶的

<Style x:Key="StandardButton" TargetType="Button">
    <Setter Property="Visibility" Value="Visible"/>
    <Setter Property="Foreground" Value="{StaticResource Foreground}"/>
    <Setter Property="Background" Value="{StaticResource BackgroundNormal}"/>
    <Setter Property="BorderBrush" Value="{StaticResource BorderBrushNormal}"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="Button">
                <Border SnapsToDevicePixels="True"
                        BorderThickness="1"
                        BorderBrush="{TemplateBinding BorderBrush}" 
                        Background="{TemplateBinding Background}">
                    <Grid>
                        <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
                    </Grid>
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsMouseOver" Value="True">
                        <Setter Property="BorderBrush" Value="{StaticResource BorderBrushHighlighted}" />
                        <Setter Property="Background" Value="{StaticResource BackgroundHighlighted}" />
                    </Trigger>
                    <Trigger Property="IsPressed" Value="True">
                        <Setter Property="Background" Value="{StaticResource BackgroundSelected}"/>
                        <Setter Property="BorderBrush" Value="{StaticResource BorderBrushSelected}"/>
                    </Trigger>
                    <Trigger Property="IsEnabled" Value="False">
                        <Setter Property="Visibility" Value="Hidden"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

实现

我现在不想讨论样式中的每一行,因为很多东西和我们已经讨论过的相同,或者只是逻辑上的,不需要太多解释就能理解(例如通过 MouseEnter 上的 Trigger 切换背景)。但我想指出一些有趣的地方,一方面给你一些自己的工作想法,另一方面让你更好地理解我所做的事情。

链接按钮

首先是两个“非标准”样式——LinkButtonSearchTextBoxLinkButton 的想法很简单,它只是一个包裹着 ContentPresenterTextBlock

<ControlTemplate TargetType="Button">
    <TextBlock><ContentPresenter/></TextBlock>
</ControlTemplate>

Button 将其 Foreground 转发给子元素,因此我们可以通过这种方式在普通 Trigger 中更改 TextBlock 的 Foreground

<Style.Triggers>
    <Trigger Property="IsMouseOver" Value="true">
        <Setter Property="Foreground" Value="{StaticResource LinkButtonHighlightedForeground}" />
    </Trigger>
</Style.Triggers>

搜索文本框

SearchTextBox 基本上是一个普通的 TextBox,带有一个特殊的 ControlTemplate,该模板包含文本本身和一个额外的 TextBlock,用于显示“搜索...”文本。我们的第二个任务是扩展 Triggers 的功能,除了切换背景颜色之外,还要隐藏/显示“搜索...”文本。

<ControlTemplate TargetType="{x:Type TextBox}">
    <Grid Background="{TemplateBinding Background}" SnapsToDevicePixels="true">
        <TextBlock Foreground="{StaticResource SearchTextForeground}" Margin="5,0,0,0" 
                   VerticalAlignment="Center" Name="search" Text="Search ..." Visibility="Hidden"/>
        <ScrollViewer x:Name="PART_ContentHost" Margin="1"/>
    </Grid>
    <ControlTemplate.Triggers>
        <Trigger Property="TextBox.Text" Value="">
            <Setter TargetName="search" Property="Visibility" Value="Visible"/>
        </Trigger>
        <Trigger Property="TextBox.Text" Value="{x:Null}">
            <Setter TargetName="search" Property="Visibility" Value="Visible"/>
        </Trigger>
        <Trigger Property="IsMouseOver" Value="true">
            <Setter Property="Background" Value="{StaticResource TextBoxBackgroundSelected}" />
            <Setter TargetName="search" Property="Foreground" Value="{StaticResource Foreground}" />
        </Trigger>
        <Trigger Property="IsFocused" Value="true">
            <Setter Property="Background" Value="{StaticResource TextBoxBackgroundSelected}" />
            <Setter TargetName="search" Property="Visibility" Value="Hidden"/>
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

Tab 控件

TabControl ControlTemplate 可能值得一说。这主要是因为通常只对 TabItems 进行样式设置,所以您可能会对一些不同的东西感兴趣。ControlTemplate 需要更改有两个主要原因:

  • TabControl 的头部和内容部分之间添加蓝色水平线(我们为此“滥用”了一个 Border
  • 将标题的背景更改为透明(与 TabControl 本身的背景不同)。这是因为我们希望通过为选项卡内容提供另一个背景来将其与环境分离。

正如你所看到的,与默认模板相比,主要的变化是两个 Borders,一个用于水平线,一个用于选项卡内容的背景。

<ControlTemplate TargetType="{x:Type TabControl}">
    <Grid KeyboardNavigation.TabNavigation="Local">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Border Background="Transparent" BorderThickness="0,0,0,3" 
                BorderBrush="{StaticResource BackgroundSelected}">
            <TabPanel Name="HeaderPanel" Panel.ZIndex="1" Margin="0,0,4,-1" 
                IsItemsHost="True" KeyboardNavigation.TabIndex="1"/>
        </Border>
        <Border Grid.Row="1" Background="{StaticResource Background}"/>
        <ContentPresenter Grid.Row="1" Name="PART_SelectedContentHost" 
                          ContentSource="SelectedContent"/>
    </Grid>
</ControlTemplate>

TabItem 样式再次非常直接。

在展开的 MenuItem 周围添加边框

现在这是一个有点奇怪的地方,它是一个非常小的细节,也许没有人会注意到,但我在 VS 中看到了它并为此感到高兴:它是在展开的 MenuItem 周围的边框。

你可能会问自己,这有什么奇怪的,让我们先看看这张图片(我指的边框用红色突出显示)

问题很明显:我们需要这种自定义形状的边框。但由于我们实际上有一个 MenuItem 和其下的 Popup,我们被迫创建两个 Borders。这可能会给你一个想法,是的,它就是那么脏。我们正在创建第三个 Border,它部分覆盖了两个第一个边框汇合处的下边框。因此,我们将第三个边框的 Width 绑定到第一个边框的 ActualWidth。在将 SnapsToDevicePixels 设置为 true 的情况下执行此操作会导致一些非常奇怪的效果,因为 WPF 有时会将 MenuItem “捕捉”到与我们的第三个边框不同的物理像素,所以我们必须对此规则做出例外。这是准备好的 ControlTemplate

<ControlTemplate TargetType="{x:Type MenuItem}">
    <!--Border 1-->
    <Border x:Name="Border" Background="Transparent" BorderBrush="Transparent" 
            BorderThickness="1" SnapsToDevicePixels="False">
        <Grid x:Name="Grid">
            <Grid.ColumnDefinitions>
                <ColumnDefinition x:Name="Col0" MinWidth="17" Width="Auto" 
                                  SharedSizeGroup="MenuItemIconColumnGroup"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="MenuTextColumnGroup"/>
                <ColumnDefinition Width="Auto" SharedSizeGroup="MenuItemIGTColumnGroup"/>
                <ColumnDefinition x:Name="Col3" Width="14"/>
            </Grid.ColumnDefinitions>
            <ContentPresenter Grid.Column="0" x:Name="Icon" VerticalAlignment="Center" 
                              ContentSource="Icon"/>
            <ContentPresenter Grid.Column="1" Margin="{TemplateBinding Padding}" 
                              x:Name="HeaderHost" RecognizesAccessKey="True" 
                              ContentSource="Header" VerticalAlignment="Center"/>
            <ContentPresenter Grid.Column="2" Margin="8,1,8,1" x:Name="IGTHost" 
                              ContentSource="InputGestureText" VerticalAlignment="Center"/>
            <Grid Grid.Column="3" Margin="4,0,6,0" x:Name="ArrowPanel" VerticalAlignment="Center">
                <Path x:Name="ArrowPanelPath" HorizontalAlignment="Right" VerticalAlignment="Center" 
                      Fill="{TemplateBinding Foreground}" Data="M0,0 L0,8 L4,4 z"/>
            </Grid>
            <Popup IsOpen="{Binding Path=IsSubmenuOpen, RelativeSource={RelativeSource TemplatedParent}}" 
                   Placement="Right" HorizontalOffset="-1" x:Name="SubMenuPopup" Focusable="false"
                   PopupAnimation="{DynamicResource {x:Static SystemParameters.MenuPopupAnimationKey}}"
                   AllowsTransparency="True">
                <Grid Margin="0,0,5,5">
                    <!--Border 2-->
                    <Border x:Name="SubMenuBorder" 
                            BorderBrush="{StaticResource MenuSeparatorBorderBrush}"
                            BorderThickness="1" Background="{StaticResource SubmenuItemBackground}" 
                            SnapsToDevicePixels="True">
                        <Grid x:Name="SubMenu" Grid.IsSharedSizeScope="True" Margin="2">
                            <StackPanel IsItemsHost="True" 
                                        KeyboardNavigation.DirectionalNavigation="Cycle"/>
                        </Grid>
                        <Border.Effect>
                            <DropShadowEffect ShadowDepth="2" Color="Black"/>
                        </Border.Effect>
                    </Border>
                    <!--Border 3-->
                    <Border Margin="1,0,0,0" x:Name="TransitionBorder" Width="0" Height="2" 
                            VerticalAlignment="Top" HorizontalAlignment="Left" 
                            Background="{StaticResource SubmenuItemBackground}" SnapsToDevicePixels="False"
                            BorderThickness="1" BorderBrush="{StaticResource SubmenuItemBackground}"/>
                </Grid>
            </Popup>
        </Grid>
    </Border>
    <ControlTemplate.Triggers>
    <!--A whole bunch of triggers-->
</ControlTemplate>

由于 Triggers 只做常规工作(改变背景等),而且数量相当多,我决定省略它们。如果你有兴趣,可以下载源代码并查看它们。

滚动条

由于VS仍然使用(在我看来不恰当的)标准ScrollBar样式,我决定自己创建一个。这个想法很简单。为了适应样式集,我们需要一个矩形的ScrollBarThumb和矩形的ScrollBarLineButtons。实现方式再次非常直接:一个边框和相应的触发器。例如,如果我们看ScrollBarThumb,它看起来与我们之前讨论过的Button样式完全一样。

<Style x:Key="ScrollBarThumb" TargetType="{x:Type Thumb}">
    <Setter Property="SnapsToDevicePixels" Value="true"/>
    <Setter Property="IsTabStop" Value="false"/>
    <Setter Property="Focusable" Value="false"/>
    <Setter Property="Background" Value="{StaticResource BackgroundNormal}"/>
    <Setter Property="BorderBrush" Value="{StaticResource BorderBrushNormal}"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type Thumb}">
                <Border Background="{TemplateBinding Background}" 
                        BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="1" />
                <ControlTemplate.Triggers>
                    <Trigger Property="IsMouseOver" Value="True">
                        <Setter Property="Background" Value="{StaticResource BackgroundHighlighted}"/>
                        <Setter Property="BorderBrush" Value="{StaticResource BorderBrushHighlighted}"/>
                    </Trigger>
                    <Trigger Property="IsDragging" Value="True">
                        <Setter Property="Background" Value="{StaticResource BackgroundSelected}"/>
                        <Setter Property="BorderBrush" Value="{StaticResource BorderBrushSelected}"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

演示应用程序

出于演示目的,我组装了一个小型应用程序。别指望它能做任何有意义的事情,它只是为了展示样式,并且没有任何逻辑。即使 TabControl 的关闭按钮也无法工作,因为我不想在项目中包含任何一行 C# 代码,以免你费力地从代码中找出样式绑定,从而让你尽可能轻松地在自己的应用程序中使用这些样式。

MetroWindow 更新

现在演示使用了 MahApps.Metro 项目中的 MetroWindow (你可以在 lib 目录中找到 dll)。为了让 MetroWindow 看起来像 Visual Studio,我们需要在 Selen.Wpf.Core 中的 Resources.xaml 中添加以下内容:

<Color x:Key="AccentColor">#2D2D30</Color>
<Color x:Key="WhiteColor">#2D2D30</Color>
<Color x:Key="BlackColor">#FFFFFFFF</Color>

AccentColor 定义了 Titlebar 的背景(关闭、最小化等按钮所在的位置),而 WhiteColorMetroWindow 的背景,它与第一个相同,以获得 VS 外观。BlackColor 为所有元素设置了前景。

请注意,您不能将大多数其他 MahApps.Metro 控件与此 AccentColor 一起使用,因为它们也依赖于它。不幸的是,没有选项可以仅设置 TitleBar 的背景,但由于在此示例中我们只使用 MetroWindow,因此可以通过 AccentColor 来实现。

兴趣点

我想你们很多人都知道这一点,但在我看来,将任何 TabItemListBoxItemFocusVisualStyle 设置为 null 至关重要,以防止它们在通过键盘选中时出现那些难看的虚线边框,所以我只是想提一下,永远不要忘记这一点!

<Setter Property="FocusVisualStyle" Value="{x:Null}"/>

总结

好的,既然你已经坚持到这里,我想你对 Metro 概念并没有完全不喜欢(至少如果你没有跳过整篇文章的话),所以我希望你也喜欢我的样式。当然,设计是一个有争议的话题,但心中有一个设计概念能为你的应用程序带来一些结构,而且我认为一个每个控件都以相同方式设计的应用程序,尽管你可能会称之为无聊,但比每个控件都有另一个酷炫、华丽效果的应用程序更用户友好。

历史

  • 2012年12月9日 - TabItems的关闭逻辑,TreeView样式
  • 2012年10月10日 - 增加了GroupBox样式,修复了水平滚动条问题
  • 2012年10月3日 - 增加了ComboBox样式
  • 2012年9月30日 - 增加了DataGrid样式
  • 2012年8月28日 - 演示使用MahApps.Metro MetroWindow
  • 2012年8月22日 - 首次发布
© . All rights reserved.