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

在 WPF 资源库中组织和使用低级样式元素

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (4投票s)

2020年4月2日

CPOL

10分钟阅读

viewsIcon

11387

解释如何在 WPF 应用程序中设置和使用颜色、字体和尺寸以实现可重用性

引言

当我开始创建 WPF 应用程序时,我只是将所有标记直接混合在 WPF 代码中。渐渐地,我发现这不是一个好方法。

  • 样式信息的重用很困难。
  • 改变应用程序外观需要大量工作。

本文旨在帮助您创建一个样式库,将标记与 WPF 功能布局分离,并使重用变得更容易一些。我并不声称拥有完美的解决方案,并希望邀请您提出您的想法和改进建议。

您可以从 Github 下载一个演示项目。

您需要一些 WPF 知识(基础)以及资源字典的基本知识才能理解本文。

演示应用程序设置

演示应用程序有两个项目

  • StyleDemo.DesktopUI 是一个 WPF .NET Core 3.1 项目。这仅用于演示和测试目的。
  • Styles.Library 是一个 WPF 用户控件库,.NET Core 3.1。请务必使用用户控件库而不是普通的类库。

这也应该适用于 .NET framework。

Desktop UI 依赖于 Styles.Library 项目,所以不要忘记包含这些依赖项。

您不需要任何 Nuget 包。我使用 Visual Studio Community Edition 2019,版本 16.5。

设置 Styles.Library

基本概念是创建一些资源字典。为了避免创建对每个单独字典的引用,首先创建一个收集所有其他字典的字典。我使用约定在每个资源字典的名称中包含“Dictionary”一词。

在演示中,我将其命名为 StylesDictionary

为了测试这一点,您至少需要一个要包含在用户控件库中的字典。为此,我创建了三个空字典

  • ColorSchemaDictionary 将包含应用程序中使用的所有颜色。
  • SizeSchemaDictionary 用于我们希望赋予标准值的所有尺寸。
  • FontDictonary 用于定义字体。

Styles.Dictionary 的代码如下所示

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
  <ResourceDictionary.MergedDictionaries>

    <!-- Basic markup -->
    <ResourceDictionary Source="ColorSchemaDictionary.xaml"/>
    <ResourceDictionary Source="SizeSchemaDictionary.xaml"/>
    <ResourceDictionary Source="FontDictionary.xaml"/>
  </ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

使 Styles.Library 在 UI 中可用

现在,我们可以使这个 StylesDictionary 在桌面应用程序中可用。为此,请修改 App.xaml,以引用此资源字典

<Application x:Class="StyleDemo.DesktopUI.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
      <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
          <ResourceDictionary
            Source="pack://application:,,,/Styles.Library;component/StylesDictionary.xaml"/>
        </ResourceDictionary.MergedDictionaries>
      </ResourceDictionary>
    </Application.Resources>
</Application>

确保复杂 URI 中的所有元素都正确。对此有一个复杂的解释,但我可以在不完全理解其逻辑的情况下生活。我从 Code Project 的这篇文章中获得了这个。

确保您的桌面项目已注册对 Styles.Library dll 的依赖。

现在应用程序应该像以前一样运行,但它将使用您的(空的)资源字典库。

添加低级资源

现在可以设置常见的低级资源。这包括颜色、基本尺寸和字体。基本思想是我们希望通过名称和功能而不是实际值来引用它们,因此如果您以后想修改配色方案,只需在一个地方更改它即可。

设置颜色

ColorSchemaDictionary 旨在收集所有颜色。为了给出语法的概念,创建了一个非常简单的方案,刚好足以展示它是如何工作的。您需要将颜色设置为画刷。在此示例中,我只使用纯色画刷,但它也适用于其他画刷类型。

  <!--  Window colors  -->
  <SolidColorBrush x:Key="WindowBackground" Color="LightBlue" />
  <SolidColorBrush x:Key="WindowBorderBrush" Color="CornflowerBlue" />
  <SolidColorBrush x:Key="ControlBackground" Color="LightBlue" />
  <SolidColorBrush x:Key="TextBoxBackground" Color="Oldlace" />
  <SolidColorBrush x:Key="HeaderBackground" Color="DarkGray" />

  <!--  Border colors  -->
  <SolidColorBrush x:Key="BorderDefault" Color="DarkBlue" />
  <SolidColorBrush x:Key="BorderAlert" Color="OrangeRed" />

  <!--  Text colors  -->
  <SolidColorBrush x:Key="LabelText" Color="DarkBlue" />
  <SolidColorBrush x:Key="DataText" Color="Black" />
  <SolidColorBrush x:Key="AlertText" Color="OrangeRed" />

  <!--  Button colors  -->
  <SolidColorBrush x:Key="ButtonBackground" Color="DarkBlue" />
  <SolidColorBrush x:Key="ButtonText" Color="Lavender" />
  <SolidColorBrush x:Key="ButtonDisabled" Color="Gray" />
  <SolidColorBrush x:Key="ButtonHover" Color="CornflowerBlue" />
  <SolidColorBrush x:Key="ButtonPressed" Color="LightBlue" />

如果您在 Visual Studio 中查看此代码,它将显示小的颜色样本。最大的优点是您将所有颜色都放在一个地方,这使得您可以轻松地检查是否有漂亮的平衡。

您应该仔细考虑命名。Intellisense 将起作用,但如果您开始键入 B,它将只显示所有以字符 B 开头的内容。因此,我更喜欢先在名称中提及控件类型,然后是资源功能的描述性文本。这似乎是显而易见的,但我仍然后悔那些我以错误顺序执行此操作的项目,例如,设置 DefaultWindowBackgroundColor 之类的东西。然后您会有一长串 Default 要搜索。

您可以直接从您想要使用的控件中引用这些颜色

<Button Background="{StaticResource ButtonBackground}"
        Foreground="{StaticResource ButtonText}">Test button</Button>

如果您将此代码放入主窗口,您将看到一个填充蓝色的按钮,上面有淡紫色文本。如您在上面的代码中看到的,这种使用命名资源的方式非常繁琐,所以我们将做得更好。按钮仍然使用整个窗口大小。这不是您通常想要的,所以下一步是定义一些默认尺寸。

设置尺寸

为了保持清晰的概览,尺寸与颜色分离。应使用 SizeSchemaDictionary。它可能看起来像这样

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:system="clr-namespace:System;assembly=System.Runtime">
 
  <Thickness
    x:Key="MarginDefault"
    Bottom="5"
    Left="5"
    Right="5"
    Top="5" />

  <Thickness
    x:Key="MarginSmall"
    Bottom="3"
    Left="3"
    Right="3"
    Top="3" />

  <Thickness
    x:Key="PaddingDefault"
    Bottom="2"
    Left="2"
    Right="2"
    Top="2" />

  <Thickness
    x:Key="PaddingSmall"
    Bottom="1"
    Left="1"
    Right="1"
    Top="1" />

  <Thickness
    x:Key="ThinBorderWidth"
    Bottom="1"
    Left="1"
    Right="1"
    Top="1" />

  <CornerRadius
    x:Key="CornersDefault"
    BottomLeft="5"
    BottomRight="5"
    TopLeft="5"
    TopRight="5" />

  <!-- Button dimensions -->
  <system:Double x:Key="ButtonDefaultWidth">100</system:Double>
  <system:Double x:Key="ButtonWideWidth">120</system:Double>
  <system:Double x:Key="ButtonDefaultHeight">30</system:Double>
  <system:Double x:Key="TextBoxDefaultHeight">30</system:Double>
</ResourceDictionary>

通过这种方式,您的标记更具可重用性,但您的 XAML 规范会变得很长,而且仍然需要大量输入。

一个评论。我倾向于以这种方式将此 MarginDefault 应用于每个控件。可能有其他选项,例如,仅在右侧和底部应用边距。我注意到,如果您应用边距的方式不一致,您的标记很快就会变得难看。然后您可能需要应用手动修复,以使控件正确对齐,这可能会导致更多不一致,从而在您更改任何内容时导致标记损坏。因为我对所有控件应用相同的边距,所以一切总是看起来很好对齐。这与使用上述工作方式的其他尺寸的标准化相结合。在我们将其应用于更高一级之前,我们仍然需要学习如何设置字体。

设置字体

很长一段时间,我找不到很多关于如何制作可重用字体设置的信息。这并不是真的简单,但这就是您可以做到的方式。

您可以像这样定义一个字体系列

<FontFamily x:Key="FontFamilyDefault">Consolas, Arial</FontFamily> 

在这种情况下,定义了两个字体系列,如果 Consolas 不可用,则可以使用 Arial 作为替代。如果您想使用非标准字体,您需要确保它们以某种方式包含在解决方案或安装程序中。

您可以在样式中使用 setter 来使用它

<Setter Property="FontFamily" Value="{DynamicResource FontFamilyDefault}"/>

定义字体大小稍微复杂一些。问题是默认情况下,您不能在大小中传递单位,例如,您不能指定 12pt 字体。如果您不指定单位,WPF 默认为 1/96 英寸。这有效,但您需要设置比在 Word 中使用的尺寸大得多的尺寸。

我在 StackOverFlow 上找到了一个解决方案

您需要创建一个新类。我在这里使用 C#,您可以将其包含在您的代码中,即使您通常使用其他语言。

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Markup;

namespace Styles.Library
  {
  // Usage: <local:FontSize Size="11pt" x:Key="ElevenPoint"/>
  public class FontSizeExtension : MarkupExtension
    {
    [TypeConverter(typeof(FontSizeConverter))]
    public double Size { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider)
      {
      return Size;
      }
    }
  }

这个解决方案背后有很多技术,我不会尝试解释它。现在您可以像这样定义大小

<local:FontSize Size="20pt" x:Key="FontSizeDefault"/>

Visual Studio 通常会自动在资源字典中为您设置“using”语句

xmlns:local="clr-namespace:Styles.Library"

这种复杂性将隐藏在您的样式定义中

<Setter Property="FontFamily" Value="{DynamicResource FontFamilyDefault}" />

字体粗细和字体样式很简单

 <!-- FontWeight -->
  <FontWeight x:Key="FontWeightDefault">Black</FontWeight>
  <FontWeight x:Key="FontWeightTextBlock">Normal</FontWeight>
  <FontWeight x:Key="FontWeightTextBox">Normal</FontWeight>

  <!-- FontStyle -->
  <FontStyle x:Key="FontStyleDefault">Italic</FontStyle>
  <FontStyle x:Key="FontStyleTextBlock">Italic</FontStyle>
  <FontStyle x:Key="FontStyleTextBox">Normal</FontStyle>

最后,有一些样式,如下划线、删除线,在 Word 中被视为字体设置的一部分,但 WPF 的工作方式不同。在这里,这些属性附加到特定的控件,例如 TextBlockTextBox。Window 控件不支持它,因此您不能全局下划线所有文本。

这就是您可以将它们定义为样式资源的方式

<Style x:Key="TextBlockUnderlined">
    <Setter Property="TextBlock.TextDecorations" Value="Underline" />
  </Style>

  <Style x:Key="TextBoxUnderlined">
    <Setter Property="TextBox.TextDecorations" Value="Underline" />
  </Style>

另请参阅 https://www.manongdao.com/q-204646.html,我在这里找到了解决此问题的方法。它们的用法符合一般模式。

在控件样式中的应用

我们已经涵盖了基础知识,因此我们可以进入下一步,为控件定义一些样式。

标记往往很大且可读性不高。为了改善这一点,您可以为控件定义样式。因为本教程侧重于设置工作方式,所以不会涵盖定义复杂样式。

可以更改每个控件的默认样式。因为我很懒,我尝试这样做,但在很多情况下,您会遇到麻烦。原因是控件可能在其他控件中使用。如果您开始应用花哨的默认样式,这些样式也会应用到您真正不希望应用的地方。因此,我使用的 99% 的样式都有一个键,并且必须明确应用。我在这里吸取了教训……

这篇文章给出了一些很好的例子:https://ikriv.com/dev/wpf/TextStyle/

因此,作为一项规则,请始终指定 x:Key 以保持对样式使用的控制。

它有助于在样式文件中进行划分。对于此演示,将创建并连接四个附加的样式字典

  • WindowDictionary 用于窗口样式
  • ButtonDictionary 用于按钮
  • TextBlockDictionary 用于 TextBlock
  • TextBoxDictionary 用于 TextBox

设置 WindowDictionary

这个例子很简单,所以是一个很好的起点。创建一个名为 WindowDictionary 的资源字典,其中包含此代码

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

  <!--  Window style  -->

  <Style x:Key="WindowDefault" TargetType="Window">
    <Setter Property="Background" Value="{DynamicResource WindowBackground}" />
  </Style>
</ResourceDictionary>

这定义了一个窗口 Style,它将设置背景颜色。在演示应用程序中,我还设置了字体,但为了简单起见,我在这里省略了。

需要注意的重要事项:如果您在项目内部创建资源字典,您可以使用 StaticResource 作为资源类型。对于在单独的库项目中创建的字典,请始终使用 DynamicResource,如示例所示。否则您的资源将无法识别。我花了相当长的时间才发现您需要这样做,所以请注意。

现在您必须确保库接口知道该资源,因此请修改 StylesDictionary

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
   
  <ResourceDictionary.MergedDictionaries>
    <!-- Basic markup -->
    <ResourceDictionary Source="ColorSchemaDictionary.xaml" />
    <ResourceDictionary Source="SizeSchemaDictionary.xaml" />
    <ResourceDictionary Source="FontDictionary.xaml" />
    <ResourceDictionary Source="WindowDictionary.xaml" />
    <ResourceDictionary Source="ButtonDictionary.xaml" />
    <ResourceDictionary Source="TextBlockDictionary.xaml" />
    <ResourceDictionary Source="TextBoxDictionary.xaml" />
  </ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

最后,将样式应用于窗口(在这里,您可以使用静态资源)

<Window x:Class="StyleDemo.DesktopUI.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Style="{StaticResource WindowDefault}"
        Title="MainWindow" Height="450" Width="800">

    <Grid>
        <Button Background="{StaticResource ButtonBackground}"
                Foreground="{StaticResource ButtonText}"
                Width="{StaticResource ButtonDefaultWidth}"
                Height="{StaticResource ButtonDefaultHeight}"
                Margin="{StaticResource MarginDefault}"
                Padding="{StaticResource PaddingDefault}">Test button</Button>
    </Grid>
</Window>

结果应该是一个漂亮的蓝屏,中心有一个深蓝色按钮。

创建此样式时,我也希望为 WindowStartupLocation 设置默认值。这不起作用,因为它不是 XAML 依赖属性。在 stackoverflow 上,您可以找到一个变通方法。我想也可以派生自己的窗口类并在那里解决这个问题。

这允许您使所有窗口看起来相似,并且您可以轻松更改背景颜色。

其他控件的示例

为了展示一些稍微复杂的示例,我添加了三个样式,分别用于 TextBlockTextBoxButton

TextBlock 很简单

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
 
  <Style x:Key="TextBlockDefault" TargetType="{x:Type TextBlock}">
    <Setter Property="Foreground" Value="{DynamicResource LabelText}" />
    <Setter Property="Background" Value="{DynamicResource ControlBackground}" />
    <Setter Property="Margin" Value="{DynamicResource MarginDefault}" />
    <Setter Property="HorizontalAlignment" Value="Stretch" />
    <Setter Property="VerticalAlignment" Value="Center" />
    <Setter Property="FontSize" Value="{DynamicResource FontSizeTextBlock}" />
    <Setter Property="FontFamily" Value="{DynamicResource FontFamilyTextBlock}" />
    <Setter Property="FontWeight" Value="{DynamicResource FontWeightTextBlock}" />
  </Style>

</ResourceDictionary>

您可以在相同或单独的资源字典中定义其他变体。

对于 TextBox,我修改了默认的 TextBox(未定义 x:Key 属性)。在这种情况下,这有效。演示中未显示,但我创建了一些变体,例如只读 textbox 和多行 textbox。这取决于您的喜好和需求如何做。

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:Styles.Library">

  <!--  Textbox  -->

  <Style TargetType="{x:Type TextBox}">
    <Setter Property="FontSize" Value="{DynamicResource FontSizeTextBox}" />
    <Setter Property="FontFamily" Value="{DynamicResource FontFamilyTextBox}" />
    <Setter Property="FontWeight" Value="{DynamicResource FontWeightTextBox}" />
    <Setter Property="FontStyle" Value="{DynamicResource FontStyleTextBox}" />

    <Setter Property="Foreground" Value="{DynamicResource DataText}" />
    <Setter Property="Background" Value="{DynamicResource TextBoxBackground}" />
    <Setter Property="Margin" Value="{DynamicResource MarginDefault}" />
    <Setter Property="Padding" Value="{DynamicResource PaddingDefault}" />
    <Setter Property="Height" Value="{DynamicResource TextBoxDefaultHeight}" />
    <Setter Property="HorizontalAlignment" Value="Stretch" />
    <Setter Property="TextAlignment" Value="Left" />
    <Setter Property="VerticalContentAlignment" Value="Center" />
  </Style>
</ResourceDictionary>

如果您想为鼠标悬停、按下或禁用状态使用标记变体,按钮会复杂得多。我在这里展示一个示例,为您提供一个起点。

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:Styles.Library">
 
  <!--  Standard button layout  -->

  <Style x:Key="ButtonDefault" TargetType="{x:Type Button}">
    <Setter Property="HorizontalContentAlignment" Value="Center" />
    <Setter Property="VerticalContentAlignment" Value="Center" />
    <Setter Property="Margin" Value="{DynamicResource MarginDefault}" />
    <Setter Property="Width" Value="{DynamicResource ButtonDefaultWidth}" />
    <Setter Property="Height" Value="{DynamicResource ButtonDefaultHeight}" />
    <Setter Property="Background" Value="{DynamicResource ButtonBackground}"/>
    <Setter Property="Foreground" Value="{DynamicResource ButtonText}"/>
    <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="{x:Type Button}">
          <Grid x:Name="grid">
            <Border
              x:Name="border"
              Background="{DynamicResource ButtonBackground}"
              BorderBrush="{DynamicResource LabelText}"
              Padding="{DynamicResource PaddingDefault}"
              BorderThickness="{DynamicResource ThinBorderWidth}">
              <ContentPresenter
                HorizontalAlignment="Center"
                VerticalAlignment="Center"
                TextElement.FontWeight="Bold" />
            </Border>
          </Grid>
          <ControlTemplate.Triggers>
            <Trigger Property="IsPressed" Value="True">
              <Setter TargetName="border"
                      Property="Background"
                      Value="{DynamicResource ButtonPressed}" />
            </Trigger>
            <Trigger Property="IsMouseOver" Value="True">
              <Setter TargetName="border"
                      Property="Background"
                      Value="{DynamicResource ButtonHover}" />
            </Trigger>
            <Trigger Property="IsEnabled" Value="False">
              <Setter TargetName="border"
                      Property="Background"
                      Value="{DynamicResource ButtonDisabled}" />
            </Trigger>
          </ControlTemplate.Triggers>
        </ControlTemplate>
      </Setter.Value>
    </Setter>
 </Style>
 
</ResourceDictionary>

这允许您显示一个带有灰色背景的禁用按钮

<Button Style="{StaticResource ButtonDefault}" IsEnabled="False">
Disabled button
</Button>

如果您不完全理解此示例也没关系,但您可以通读代码并根据需要找到有关自定义按钮的更多背景信息。

按钮的字体继承自窗口级别的字体设置。您可能希望或不希望这样做,具体取决于您所需的控制级别。

最后的 remarks

在本文中,我介绍了一种工作方式,我在很长一段时间的试错过程中发现,并得到了慷慨的开发人员的许多帮助,他们通过他们的解决方案和问题答案做出了贡献。该解决方案并非旨在作为解决所有标记问题的复制粘贴解决方案。它取决于您的具体需求如何设置细节。我的选择是相对低级别地进行,但广泛重用低级组件。您可以选择对多个控件仅使用一种字体。这工作量较少,但如果您想更改设计,这可能会导致更多工作。

我期待听到更好的解决方案。在很多方面,我都是一个初学者,但这对我现在来说有效,我希望它能帮助您创建一些对您有效的东西。

历史

  • 版本 1.0:本文的初始版本
  • 版本 1.1:小更新,改进文本以强调使用 DynamicResource 的重要性
© . All rights reserved.