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






4.90/5 (4投票s)
解释如何在 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 的工作方式不同。在这里,这些属性附加到特定的控件,例如 TextBlock
或 TextBox
。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 上,您可以找到一个变通方法。我想也可以派生自己的窗口类并在那里解决这个问题。
这允许您使所有窗口看起来相似,并且您可以轻松更改背景颜色。
其他控件的示例
为了展示一些稍微复杂的示例,我添加了三个样式,分别用于 TextBlock
、TextBox
和 Button
。
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
的重要性