资源基础
WPF允许在代码中以及在标记中的各个位置定义资源(和特定的控件、窗口一起定一,或在整个应用程序中定义)。资源具有很多重要的优点,如下所述:
- 高效:可以通过资源定义对象,并在标记中的多个地方使用。这会精简代码,使其更高效
- 可维护性:可通过资源使用低级的格式化细节(如字号),并将它们移到便于对其进行修改的中央位置。在XAML中创建资源相当于在代码中创建常量。
- 适应性:一旦特定信息与应用程序的其他部分分离开来,并放置到资源部分中,就可以动态地修改这些信息。例如,可能希望根据用户的个人喜好或当前语言修改资源的细节。
资源集合
每个元素都有Resources
属性,该属性存储了一个资源字典集合(它是ResourceDIctionary
类的示例)。资源集合可包含任意类型的对象,并根据字符串编写索引。
尽管每个元素都提供了 Resources 属性(该属性作为 FrameworkElement 类的一部分定义),但通常在窗口级别定义资源。这是因为每个元素都可以访问各自资源集合中的资源,也可以访问所有父元素的资源集合中的资源。
1 | <Window.Resources> |
资源的层次
每个元素都有自己的资源集合,为了找到期望的资源,WPF在元素树中进行递归搜索。在使用静态资源时,必须中是在引用资源之前在标记中定义资源。一般采用就近原则
静态资源和动态资源
- 静态资源只从资源集合中获取对象一次。根据对象的类型(以及使用对象的方式),对象的任何变化都可能被立即注意到。
- 动态资源在每次需要对象时都会重新从资源集合中查找对象。这意味着可在同一键下放置一个全新对象,而且动态资源会应用看变化
作为一般规则,只有在下列情况下才需要使用动态资源:
- 资源具有依赖于系统设置的属性(如当前Windows操作系统的颜色或字体)
- 准备通过编程方式替换资源对象(例如,实现几类动态皮肤特性)
然而,不应过度使用动态资源。主要问题时对资源的修改未必会触发对用户界面的更新。许多情况下,需要在控件中显示动态内容,而且控件需要随着内容的改变调整自身。对于这种情况,使用数据绑定更合理。
在极少数情况下,动态资源还用于提高第一次加载窗口时的性能。这是因为动态资源中是在创建窗口时加载,而动态资源在第一次使用时加载。然而,除非资源非常大并且非常复杂(在这这种情况下,解析资源标记会耗时较长时间),否则否则这样做没有任何溢出。
非共享资源
通常,在多个地方使用某种资源时,使用的是同一个对象实例。这种行为——称为共享通常这也正是所希望的。然而,也可能希望告诉解析器在每次使用时创建单独的对象实例。为关闭共享行为,需要使用Shared
特性,如下所示:
1 | <ImageBrush x:Key="TileBrush" x:Shared="False"/> |
很少有理由需要使用非共享的资源。如果希望以后分别修改资源实例,可考虑使用非共享资源。例如,可创建包含几个使用同一画刷按钮的窗口,并关闭共享行为,从而可以分别改变每个画刷。由于效率低下,这种方式不常见。在这个示例中,开始时告诉所有按钮使用同一个画刷,当需要时再创建并应用新的画刷,这样可能更好。这样,只有当确实需要时才承担额外的画刷对象开销。
使用非共享资源的另一个原因是,可能希望以一种原本不允许的方式重用某个对象。例如,使用该技术,可将某个元素(如一幅图像或一个按钮)定义为资源,然后在窗口的多个不同位置显示该元素。
1 | <Window.Resources> |
通过代码访问资源
通常在标记中定义和使用资源。如有必要,也可在代码中使用资源集合。正如已经看到的,可通过名称从资源集合中提取资源。为此,需要使用正确元素的资源集合
1 | var brush = (ImageBrush)FindResource("TileBrush"); |
可使用TryFindResource()
方法替代FindResource()
方法。如果找不到资源,该方法会会返回null引用,而不是抛出异常。
此外,还可通过编写代码添加资源。选择希望放置资源的元素,并使用资源集合的Add()
方法。然而,通常在标记中定义资源。
应用程序资源
窗口不是查找资源的最后一站。如果在控件或其容器中(直到包含窗口或页面)找不到指定的资源,WPF 会继续检査为应用程序定义的资源集合。在 VisualStudio 中,这些资源是在 App.xaml文件的标记中定义的资源,如下所示:
1 | <Application x:Class="WpfApp.App" |
在创建应用程序资源之前,需要考虑在复杂性和重用性之间取得平衡。添加应用程序资源可提高重用性,但会增加复杂性,因为没有立即明确哪些窗口使用给定的资源(从概念上讲,这与使用太多全局变量的旧式 C++程序一样)。一条正确的指导原则是:如果对象需要被广泛重用(例如,在许多窗口中重用),可使用应用程序资源;如果只是在两三个窗口中使用,可考虑在每个窗口中分别定义资源。
系统资源
动态资源主要用于辅助应用程序对系统环境设置的变化做出响应,为此需要使用三个类,分别时SystemColors
、SystemFonts
和SystemParameters
,这些类都位于System.Windows
名称空间中。SystemColors
类用于访问颜色设置;SystemFonts
类用于访问字体设置;而SystemParameters
类封装了大量的设置列表,这些设置描述了各种屏幕元素的标准尺寸、键盘和鼠标设置、屏幕尺寸以及各种图形效果(如热跟踪、阴影以及当拖动窗口时显示窗口内容)是否已经打开。
SystemColors
和SystemFonts
类有两个版本,它们分别位于 System.Windows
名称空间和System.Drawing
名称空间。System.Windows
名称空间中的版本是 WPF 的一部分,它们使用正确的数据类型并且支持资源系统。位于System.Drawing
名称空间中的版本是 Windows 窗体的一部分,对于WPF应用程序,它们没有用处。
SystemColors
、SystemFonts
和SystemParameters
类通过静态属性公开了它们的所有细节,可通过如下方式使用:
1 | // 使用颜色 |
1 | <TextBlock Foreground="{x:Static SystemColors.WindowTextBrush}" Text="text"/> |
上面的示例没有使用资源,这可能会引发一个小问题——当解析窗口并创建标签时,会根据当前窗口文本颜色的“快照”创建画刷。如果在应用程序运行时(在显示了包含标签的窗口后)改变了Windows颜色,TextBlock
控件不会更新自身。为了解决这个问题,不能讲Foreground属性直接设置为画刷对象,而是需要将它设置为封装了该系统资源的DynamicResource对象
。
所有SystemXXX
类都提供了可返回ResourceKey
对象引用的补充属性集,使用这些引用可从系统资源集合中提取资源。这些属性于直接返回对象的普通属性同名,后面加上单词Key
1 | <TextBlock Foreground="{DynamicResource {x:Static SystemColors.WindowTextBrushKey}}" |
资源字典
如果希望在多个项目之间共享资源,可创建资源字典。资源字典只是XAML文档,除了存储希望使用的资源外,不做其他任何事情。
创建资源字典
当为应用程序添加资源字典时,务必将Build Action 设置为 Page(与其他任意 XAML 文件一样)。这样可保证为了获得最佳性能而将资源字典编译为 BAML。不过,将资源字典的 BuildAction 设置为 Resource 也是非常完美的,这样它会被嵌入到程序集中,但是不会被编译。当然,在运行时解析它的速度要稍慢一些。
1 | <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
使用资源字典
为了使用资源字典,需要将其合并到应用程序某些位置的资源集合中。例如,可在特定窗口中执行此操作,但通常将其合并到应用程序的资源集合中,如下所示:
1 | <Application x:Class="WpfApp.App" |
在相互重叠的不同资源集合中存储同名的资源是完全合理的。然而,不允许合并使用相同资源名称的资源字典。如果使用重复的资源名称,当编译应用程序时将收到XamlParseException异常。
使用资源字典的一个原因时为了定义一个活多个可重用的应用程序“皮肤”,可将“皮肤”应用到控件上。另一个原因时为了存储需要被本地化的内容(如错误消息字符串)
在程序集之间共享资源
当共享包含一个或多个资源字典的编译过的程序集时,还需要面对另一种挑战-- 需要有一种方法来提取所希望的资源并在应用程序中使用资源。为此,可使用两种方法。最直观的解决方法是使用代码创建合适的ResourceDictionary对象。
1 | ResourceDictionary resourceDictionary = new ResourceDictionary(); |
上面的代码片段使用了在本章前面介绍的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 | public class CustomResources |
现在可以使用该类和资源ID创建键名了:
1 | <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
使用包含ComponentResourceKey
的DynamicaResource
(这是合理的,因为ComponentResourceKey
时资源名)。在使用资源字典的应用程序中国使用的ComponentResourceKey
,就是在类库中使用的ComponentResourceKey
,再次提供了对同一个类的引用和相同的资源ID.
1 | <Window x:Class="WpfApp.MainWindow" |
当使用ComponentResourceKey
时,必须使用动态资源,不能使用静态资源
还可以采取一个附加步骤,使资源更容易使用。可定义一个静态属性,让它返回需要使用的正确ComponentResourceKey
。通常在组件的类中定义该属性,如下所示:
1 | public class CustomResources |
现在可使用Static
标记扩展访问该属性并应用资源,而不必在标记中使用很长的ComponentResourceKey
:
1 | <Button Background="{DynamicResource {x:Static res:CustomResources.PrimarySolidBrushKey}}"> |
在本质上,这种便捷方法与前面介绍的Systemxcx
类使用相同的技术。例如,当检索SystemColors.WindowTextBrushKey
时,所接收的也是正确的资源键对象。唯一的区别是,它是私有 SystemResourceKey
(而不是 ComponentResourceKey
)的一个实例。这两个类都继承自ResourceKey 抽象类。