资源基础

  WPF允许在代码中以及在标记中的各个位置定义资源(和特定的控件、窗口一起定一,或在整个应用程序中定义)。资源具有很多重要的优点,如下所述:

  • 高效:可以通过资源定义对象,并在标记中的多个地方使用。这会精简代码,使其更高效
  • 可维护性:可通过资源使用低级的格式化细节(如字号),并将它们移到便于对其进行修改的中央位置。在XAML中创建资源相当于在代码中创建常量。
  • 适应性:一旦特定信息与应用程序的其他部分分离开来,并放置到资源部分中,就可以动态地修改这些信息。例如,可能希望根据用户的个人喜好或当前语言修改资源的细节。

资源集合

  每个元素都有Resources属性,该属性存储了一个资源字典集合(它是ResourceDIctionary类的示例)。资源集合可包含任意类型的对象,并根据字符串编写索引。

  尽管每个元素都提供了 Resources 属性(该属性作为 FrameworkElement 类的一部分定义),但通常在窗口级别定义资源。这是因为每个元素都可以访问各自资源集合中的资源,也可以访问所有父元素的资源集合中的资源。

1
2
3
4
5
6
7
8
<Window.Resources>
<Style TargetType="{x:Type Button}">
<Setter Property="Background"
Value="Pink" />
<Setter Property="Foreground"
Value="White" />
</Style>
</Window.Resources>

资源的层次

  每个元素都有自己的资源集合,为了找到期望的资源,WPF在元素树中进行递归搜索。在使用静态资源时,必须中是在引用资源之前在标记中定义资源。一般采用就近原则

静态资源和动态资源

  • 静态资源只从资源集合中获取对象一次。根据对象的类型(以及使用对象的方式),对象的任何变化都可能被立即注意到。
  • 动态资源在每次需要对象时都会重新从资源集合中查找对象。这意味着可在同一键下放置一个全新对象,而且动态资源会应用看变化

  作为一般规则,只有在下列情况下才需要使用动态资源:

  • 资源具有依赖于系统设置的属性(如当前Windows操作系统的颜色或字体)
  • 准备通过编程方式替换资源对象(例如,实现几类动态皮肤特性)

  然而,不应过度使用动态资源。主要问题时对资源的修改未必会触发对用户界面的更新。许多情况下,需要在控件中显示动态内容,而且控件需要随着内容的改变调整自身。对于这种情况,使用数据绑定更合理。

在极少数情况下,动态资源还用于提高第一次加载窗口时的性能。这是因为动态资源中是在创建窗口时加载,而动态资源在第一次使用时加载。然而,除非资源非常大并且非常复杂(在这这种情况下,解析资源标记会耗时较长时间),否则否则这样做没有任何溢出。

非共享资源

  通常,在多个地方使用某种资源时,使用的是同一个对象实例。这种行为——称为共享通常这也正是所希望的。然而,也可能希望告诉解析器在每次使用时创建单独的对象实例。为关闭共享行为,需要使用Shared特性,如下所示:

1
<ImageBrush x:Key="TileBrush" x:Shared="False"/>

  很少有理由需要使用非共享的资源。如果希望以后分别修改资源实例,可考虑使用非共享资源。例如,可创建包含几个使用同一画刷按钮的窗口,并关闭共享行为,从而可以分别改变每个画刷。由于效率低下,这种方式不常见。在这个示例中,开始时告诉所有按钮使用同一个画刷,当需要时再创建并应用新的画刷,这样可能更好。这样,只有当确实需要时才承担额外的画刷对象开销。

  使用非共享资源的另一个原因是,可能希望以一种原本不允许的方式重用某个对象。例如,使用该技术,可将某个元素(如一幅图像或一个按钮)定义为资源,然后在窗口的多个不同位置显示该元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
<Window.Resources>
<Border x:Key="border"
x:Shared="False"
BorderBrush="Pink"
BorderThickness="1"
Width="50"
Height="50"></Border>
</Window.Resources>
<StackPanel>
<ContentControl Content="{StaticResource border}" />
<ContentControl Content="{StaticResource border}" />
<ContentControl Content="{StaticResource border}" />
</StackPanel>

通过代码访问资源

  通常在标记中定义和使用资源。如有必要,也可在代码中使用资源集合。正如已经看到的,可通过名称从资源集合中提取资源。为此,需要使用正确元素的资源集合

1
var brush = (ImageBrush)FindResource("TileBrush");

可使用TryFindResource()方法替代FindResource()方法。如果找不到资源,该方法会会返回null引用,而不是抛出异常。

  此外,还可通过编写代码添加资源。选择希望放置资源的元素,并使用资源集合的Add()方法。然而,通常在标记中定义资源。

应用程序资源

  窗口不是查找资源的最后一站。如果在控件或其容器中(直到包含窗口或页面)找不到指定的资源,WPF 会继续检査为应用程序定义的资源集合。在 VisualStudio 中,这些资源是在 App.xaml文件的标记中定义的资源,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<Application x:Class="WpfApp.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp"
StartupUri="MainWindow.xaml">
<Application.Resources>
<ImageBrush x:Key="TileBrush"
TileMode="Tile"
ViewboxUnits="Absolute"
Viewport="0 0 32 32"
ImageSource="happyface.jpg"
Opacity="0.3" />
</Application.Resources>
</Application>

  在创建应用程序资源之前,需要考虑在复杂性和重用性之间取得平衡。添加应用程序资源可提高重用性,但会增加复杂性,因为没有立即明确哪些窗口使用给定的资源(从概念上讲,这与使用太多全局变量的旧式 C++程序一样)。一条正确的指导原则是:如果对象需要被广泛重用(例如,在许多窗口中重用),可使用应用程序资源;如果只是在两三个窗口中使用,可考虑在每个窗口中分别定义资源。

系统资源

  动态资源主要用于辅助应用程序对系统环境设置的变化做出响应,为此需要使用三个类,分别时SystemColorsSystemFontsSystemParameters,这些类都位于System.Windows名称空间中。SystemColors类用于访问颜色设置;SystemFonts类用于访问字体设置;而SystemParameters类封装了大量的设置列表,这些设置描述了各种屏幕元素的标准尺寸、键盘和鼠标设置、屏幕尺寸以及各种图形效果(如热跟踪、阴影以及当拖动窗口时显示窗口内容)是否已经打开。

SystemColorsSystemFonts 类有两个版本,它们分别位于 System.Windows 名称空间和System.Drawing名称空间。System.Windows名称空间中的版本是 WPF 的一部分,它们使用正确的数据类型并且支持资源系统。位于System.Drawing名称空间中的版本是 Windows 窗体的一部分,对于WPF应用程序,它们没有用处。

  SystemColorsSystemFontsSystemParameters类通过静态属性公开了它们的所有细节,可通过如下方式使用:

1
2
3
4
// 使用颜色
txt.Foreground = new SolidBrush(SYstemColors.WindowTextColor);
// 使用现成画刷
txt.Foreground = SystemColors.WindowTextBrush;
1
<TextBlock Foreground="{x:Static SystemColors.WindowTextBrush}" Text="text"/>

  上面的示例没有使用资源,这可能会引发一个小问题——当解析窗口并创建标签时,会根据当前窗口文本颜色的“快照”创建画刷。如果在应用程序运行时(在显示了包含标签的窗口后)改变了Windows颜色,TextBlock控件不会更新自身。为了解决这个问题,不能讲Foreground属性直接设置为画刷对象,而是需要将它设置为封装了该系统资源的DynamicResource对象

所有SystemXXX类都提供了可返回ResourceKey对象引用的补充属性集,使用这些引用可从系统资源集合中提取资源。这些属性于直接返回对象的普通属性同名,后面加上单词Key

1
2
<TextBlock Foreground="{DynamicResource {x:Static SystemColors.WindowTextBrushKey}}"
Text="Ordinary text" />

资源字典

  如果希望在多个项目之间共享资源,可创建资源字典。资源字典只是XAML文档,除了存储希望使用的资源外,不做其他任何事情。

创建资源字典

  当为应用程序添加资源字典时,务必将Build Action 设置为 Page(与其他任意 XAML 文件一样)。这样可保证为了获得最佳性能而将资源字典编译为 BAML。不过,将资源字典的 BuildAction 设置为 Resource 也是非常完美的,这样它会被嵌入到程序集中,但是不会被编译。当然,在运行时解析它的速度要稍慢一些。

1
2
3
4
5
6
7
8
9
10
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="DefaultButtonStyle"
TargetType="{x:Type Button}">
<Setter Property="Background"
Value="Purple" />
<Setter Property="Foreground"
Value="White" />
</Style>
</ResourceDictionary>

使用资源字典

  为了使用资源字典,需要将其合并到应用程序某些位置的资源集合中。例如,可在特定窗口中执行此操作,但通常将其合并到应用程序的资源集合中,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<Application x:Class="WpfApp.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp"
StartupUri="MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="ButtonStyles.xaml" />
</ResourceDictionary.MergedDictionaries>
<!-- 如果希望添加自己的资源并合并到资源字典中,只需要在MergedDictionaries部分之前或之后放置资源就可以了 -->
</ResourceDictionary>
</Application.Resources>
</Application>

在相互重叠的不同资源集合中存储同名的资源是完全合理的。然而,不允许合并使用相同资源名称的资源字典。如果使用重复的资源名称,当编译应用程序时将收到XamlParseException异常。

  使用资源字典的一个原因时为了定义一个活多个可重用的应用程序“皮肤”,可将“皮肤”应用到控件上。另一个原因时为了存储需要被本地化的内容(如错误消息字符串)

在程序集之间共享资源

  当共享包含一个或多个资源字典的编译过的程序集时,还需要面对另一种挑战-- 需要有一种方法来提取所希望的资源并在应用程序中使用资源。为此,可使用两种方法。最直观的解决方法是使用代码创建合适的ResourceDictionary对象。

1
2
3
4
5
ResourceDictionary resourceDictionary = new ResourceDictionary();
// pack URI语法
resourceDictionary.Source = new Uri(
"ResourceLibrary;component/ReusablkeDictionary.xaml",UriKind.Relative
);

  上面的代码片段使用了在本章前面介绍的packURI语法。它构造了一个相对URI,该URI指向另一个程序集中名为 ReusableDictionary.xaml 的编译过的 XAML 资源。一旦创建ResourceDictionary对象,就可以从集合中手动检索所需的资源了:

1
cmd.Background = (Brush)resourceDictionary["TileBrush"];

  然而,不必手动指定资源。当加载新的资源字典时,窗口中的所有 DynamicResource 引用都会被自动重新评估。当构建动态皮肤特性时,将看到使用这种技术。

  如果不想编写任何代码,还有另一种选择。可使用ComponentResourceKey 标记扩展,该标记扩展是专门针对这种情况而设计的。使用ComponentResourceKey为资源创建键名。通过执行这一步骤,告知 WPF 您准备在程序集之间共享资源。

命名资源的最常用方式就是使用字符串。然而,WPF还提供了一些聪明的资源扩展功能,当使用特定类型的非字符串键名时,它们会自动起作用。例如,在下一章将看到,对于样式,可使用Type对象作为键名。这会告诉 WPF 自动将样式应用到相应类型的元素上。同样,可为希望在程序集之间共享的任何资源使用 ComponentResourceKey实例作为键名。

  在继续执行任何操作前,需要确保已经为资源字典提供了正确的名称。为了让这种技巧生效,必须将资源字典放置到generic.xaml文件中,并且必须将该文件放到应用程序文件夹的Themes 子文件夹中。generic.xaml 文件中的资源被认为是默认主题的一部分,并且它们总是可用的

使用类库共享资源

  下一步是为存储在ResourceLibrary程序集中希望共享的资源创建键名。当使用ComponentResourceKey时,需要提供两部分信息:类库程序集中类的引用和描述性的资源ID。类引用是 WPF允许和其他程序集共享资源的关键部分。当使用资源时,需要提供相同的类引用和资源 ID。
  该类的实际外观并不重要,它不需要包含代码。定义该类型的程序集就是ComponentResourceKey 将要从中查找资源的程序集。如下CustomResources所示

1
2
3
public class CustomResources
{
}

  现在可以使用该类和资源ID创建键名了:

1
2
3
4
5
6
7
8
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ResourceLibrary">

<SolidColorBrush x:Key="{ComponentResourceKey TypeInTargetAssembly={x:Type local:CustomResources},ResourceId=PrimarySolidBrush}"
Color="Green" />

</ResourceDictionary>

  使用包含ComponentResourceKeyDynamicaResource(这是合理的,因为ComponentResourceKey时资源名)。在使用资源字典的应用程序中国使用的ComponentResourceKey,就是在类库中使用的ComponentResourceKey,再次提供了对同一个类的引用和相同的资源ID.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<Window x:Class="WpfApp.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"
xmlns:local="clr-namespace:WpfApp"
xmlns:res="clr-namespace:ResourceLibrary;assembly=ResourceLibrary"
mc:Ignorable="d"
Title="MainWindow"
Height="450"
Width="800">
<StackPanel>
<Button Background="{DynamicResource {ComponentResourceKey TypeInTargetAssembly={x:Type res:CustomResources},ResourceId=PrimarySolidBrush}}">
A Resource From ResourceLinrary
</Button>
</StackPanel>
</Window>

当使用ComponentResourceKey时,必须使用动态资源,不能使用静态资源

  还可以采取一个附加步骤,使资源更容易使用。可定义一个静态属性,让它返回需要使用的正确ComponentResourceKey。通常在组件的类中定义该属性,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class CustomResources
{
public static ComponentResourceKey PrimarySolidBrushKey
{
get
{
return new ComponentResourceKey(
typeInTargetAssembly: typeof(CustomResources),
resourceId: "PrimarySolidBrush"
);
}
}
}

  现在可使用Static标记扩展访问该属性并应用资源,而不必在标记中使用很长的ComponentResourceKey

1
2
3
<Button Background="{DynamicResource {x:Static res:CustomResources.PrimarySolidBrushKey}}">
A Resource From ResourceLinrary
</Button>

  在本质上,这种便捷方法与前面介绍的Systemxcx类使用相同的技术。例如,当检索SystemColors.WindowTextBrushKey 时,所接收的也是正确的资源键对象。唯一的区别是,它是私有 SystemResourceKey(而不是 ComponentResourceKey)的一个实例。这两个类都继承自ResourceKey 抽象类。