目录
WPF资源系统是一种保管一系列有用对象的简单方法,从而可以更容易地重用这些对象。尽管可在代码中创建和操作资源,但通常在XAML标记中定义资源。一旦定义资源,就可以在窗口标记的所有其他部分使用资源。
10.1 资源基础
WPF允许在代码中以及在标记中的各个位置定义资源。资源具有以下优点:
- 高效。可以通过资源定义对象,并在标记中的多个地方使用。这会精简代码,使其更高搞笑。
- 可维护性。可通过资源使用低级的格式化袭击,并将它们移到便于对齐及逆行修改的中央位置。在XAML中创建资源相当于在代码中创建常量。
- 适应性。一旦特定信息与应用程序的其他部分分离开来,并放置到资源部分中,就可以动态地修改这些信息。
10.1.1 资源集合
每个元素都有Resources属性,该属性存储了一个资源字典集合(它是ResourceDictionary类的实例)。尽管每个元素都提供了Resources属性(该属性作为FrameworkElement类的一部分定义),但通常在窗口级别定义资源。这是因为每个元素都可以访问各自资源集合中的资源,也可访问所有父元素的资源集合中的资源。
下面的标记显示了如何定义与使用画刷:
<Window x:Class="Resources.WindowResource"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Resources" Height="300" Width="300">
<Window.Resources>
<ImageBrush x:Key="TileBrush" TileMode="Tile"
ViewportUnits="Absolute" Viewport="0 0 32 32"
ImageSource="happyface.jpg" Opacity="0.3">
</ImageBrush>
</Window.Resources>
<StackPanel Margin="5">
<Button Background="{StaticResource TileBrush}" Padding="5" FontWeight="Bold" FontSize="14" Margin="5">A Tiled Button</Button>
<Button Padding="5" Margin="5" FontWeight="Bold" FontSize="14">A Normal Button</Button>
<Button Background="{StaticResource TileBrush}" Padding="5" Margin="5" FontWeight="Bold" FontSize="14">Another Tiled Button</Button>
</StackPanel>
</Window>
定义画刷资源时,图像画刷的细节不是非常重要。重要的是Key特性(以名称空间前缀x:开头,这会将该画刷放置到XAML名称空间中,而不是WPF名称空间中)。该特性制定了在Window.Resoures.Collection集合中编写画刷索引的名称。当需要检索资源时,只要使用相同名称,就可以使用需要的任何内容。
为使用XAML标记中的资源,需要一种引用资源的方法。这是通过标记扩展完成的。实际上有两个标记扩展可工使用:一个用于动态资源,另一个用于静态资源。静态资源在首次创建窗口时一次性地设置完毕。而对于动态资源,如果发生了改变,就会重新应用资源。
10.1.2 资源的层次
每个元素都有自己的资源集合,为了找到期望的资源,WPF在元素树中进行递归搜索。在当前示例中,可将图像画刷从窗口的资源集合移到包含这三个按钮StackPanel面板的资源集合中,而不必改变应用程序的工作方式。也可将图像画刷放到Button.Resources集合中,不过需要定义画刷两次——为每个按钮分别定义一次。
需要考虑另外一个问题是,当使用静态资源是,必须总是在引用资源之前在标记中定义资源。这意味着尽管从标记角度看,将Window.Resources部分放在窗口的主要内容(包含所有按钮的StackPanel面板)之后是完全合法的,但这回破坏当前示例。当XAML解析器遇到它不知道的资源的静态引用时,会抛出异常。
10.1.3
上面的示例使用了静态资源(在该例中是图形画刷),所以可能认为对于资源的任何改变都不会有什么反应。然而,事实并非如此。例如,设想在应用了资源并且显示了窗口子厚,执行下面的代码:
ImageBrush brush = (ImageBrush)this.Resource["TileBrush"];
brush.ViewPort = new Rect(0, 0, 5, 5);
上面的代码从Window.Resources集合中检索画刷,并对它进行操作。当运行此代码时,可能不希望用户界面有任何反应——毕竟它是静态资源。但这一变化会传播给两个按钮。实际上,会使用新设置的ViewPort属性进行更新,而不管是通过静态资源还是动态资源使用画刷。
这是因为Brush类集成自Freezable类。Freezable类有一个基本的变化跟踪特性。这意味着,无论何时在WPF中改变画刷,所有使用该画刷的控件都会自动更新。
现在,可能想弄清楚静态资源和动态资源之间到底有什么区别。区别在于静态资源只从资源集合中获取对象一次。根据对象的类型以及使用对象的方式,对象的任何变化都可能被立即注意到。然而,动态资源再每次需要对象时都会重新从集合中查找对象。这意味着可再同一键下放置一个全新对象,而且动态资源会应用该变化。
作为一般规则,只有再下列情况才需要使用动态属性:
- 资源具有依赖于系统设置的属性
- 准备通过编程方式替换资源对象
然而,不应过度使用动态资源。主要问题是对资源的修改未必会触发对用户界面的更新。
10.1.4 非共享资源
通常,再多个地方使用吗欧中资源时,使用的时同一个对象实例。这种行为——称为共享——通常这也正是所希望的。然而,也可能希望告诉解析器再每次使用时创建单独的对象实例。为了关闭共享行为,需要使用Shared特性,如下所示:
<ImageBrush x:key="TilBrush" x:Shared="False" .../>
10.1.5 通过代码访问资源
可使用FrameworkElement.FindResource()方法查找资源。下面是一个实例,当引发Click事件时,会查找按钮资源。
private void cmdChanged_Click(object sender, RoutedEventArgs e)
{
Button cmd = (Button)sender;
ImageBrush brush = (ImageBrush)sender.FindResource("TiteBrush");
...
}
可使用TryFindResource()方法代替FindResource()方法。如果找不到资源,该方法会返回null引用,而不是抛出异常。
10.1.6 应用程序资源
窗口不是查找资源的最后一站。如果再控件或其他容器中找不到指定的资源,WPF会继续检查为应用程序定义的资源集合。在VS中,这些资源是在App.xaml文件中的标记中定义的资源,如下所示:
<Application x:Class="Resources.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="Menu.xaml">
<Application.Resources>
<ImageBrush x:Key="TileBrush" TileMode="Tile"
ViewportUnits="Absolute" Viewport="0 0 32 32"
ImageSource="happyface.jpg" Opacity="0.3">
</ImageBrush>
</Application.Resources>
</Application>
当某个元素查找资源时,应用程序资源任然不是最后一站。如果没有在应用程序资源中找到所需的资源,元素还会继续查找系统资源。
10.1.7 系统资源
常使用的系统资源主要包含SystemColors、SystemFonts和SystemParameters,这些类都位于System.Windows名称空间中。
- SystemColors类用于访问颜色设置
- SystemFonts类用于访问字体设置
- SystemParameters类封装了大量的设置列表,这些设置描述了更重屏幕元素的标准尺寸、键盘和鼠标设置、屏幕尺寸以及各种图形效果是否已经打开。
在WPF中,可使用静态标记扩展访问静态属性。例如,下面的标记演示了如何使用XAML设置同一标签的前景设:
<Label Foreground="{x:Static SystemColors.WindowTextBrush}">Ordinary text</Lable>
上面的实例没有使用资源,这可能引发一个小问题——当解析窗口并创建标签时,会根据当前窗口文本颜色的“快照”创建会刷。如果在应用程序运行时,该变了Windows颜色,Label空间不会更新自身。
为解决这个问题,不能将Foreground属性直接设置为画刷对象,而是需要将它设置为封装了该系统资源的DymamicResource对象。幸运的是,所有SystemXxx类都提供了可返回ResourceKey对象应用的补充属性集,使用这些引用可从系统资源集合中提取资源。这些属性与直接返回对象的普通属性同名,后面加上单词Key。
<Label Foreground="{DymamicResource {x:Static SystemColors.WindowTextBrushKey}}">Ordinary text</Lable>
10.2 资源字典
如果希望在多个项目之间共享资源,可创建资源字典。资源字典只是XAML文档,除了存储希望使用的资源外,不做任何其他事情。
10.2.1 创建资源字典
下面是一个资源字典示例,它包含一个资源:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ImageBrush x:Key="TitleBrush"
TileMode="Tile"
ViewportUnits="Absolute"
Viewport="0 0 32 32"
ImageSource="happyface.jpg"
Opacity="0.3">
</ImageBrush>
</ResourceDictionary>
当为应用程序添加资源字典时,务必将Build Action设置为Page。这样可保证了获得最佳性能而将资源字典编译为BAML。不过,将资源字典的Build Action设置为Resource也是非常完美的,这样它会被嵌入到程序集中,但是不会被编译。当然,在运行时解析它的速度要稍慢一些。
10.2.2 使用资源字典
为了使用资源字典,需要将其合并到应用程序某些位置的资源集合中。例如,可在特定窗口中执行此操作,但通常将其合并到应用程序的资源集合中,如下所示:
<Application x:Class="Resources.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="Menu.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MegerdDictionaries>
<ResourceDictionary Source="AppBrushes.xaml"/>
<ResourceDictionary Source="WinzardBrushes.xaml"/>
</ResourceDictionary.MegerdDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
10.2.3 在程序集之间共享资源
当共享包含一个或多个资源自带你的编译过的程序集时,还需要面对另一种挑战——需要有一种方法来提取所希望的资源并在应用程序种使用资源。为此,可使用两种方法。最直观的解决方法是使用代码创建合适的ResourceDictionary对象。例如,如果类库程序集中有名为ReuseableDictionary.xaml的资源字典,那么可使用下面的代码手动创建该资源字典:
ResourceDictionary resourceDictionary = new ResourceDictionary();
resourceDictionary.Source = new Uri("ResourceLibrary;componet/ReuseableDictionary.xaml", UriKind.Relative);
cmd.Background = (Brush)resourceDictionary["TileBrush"];
然而,不必手动指定资源。当加载新的资源字典时,窗口中的所有DynamicResource引用都会被自动重新评估。
如果不想编写任何代码,还有另外一种选择。可使用ComponentResourceKey标记扩展,该编辑扩展时专门针对这种情况而设计的。使用ComponentResourceKey为资源创建键名。通过执行这一步骤,告知WPF准备在程序集之间共享资源。
在继续执行任何操作前,需要确保已经为资源字典提供了正确的名称。为了让这种技巧生效,必须将资源字典放置到generic.xaml文件中,并且必须将该文件放到应用程序文件夹的Themes子文件夹种。generic.xaml文件种的资源被认为是默认主题的一部分,并且它们总是可用的。
下一步是为存储在ResourceLibrary程序集中希望共享的资源创建键名。当使用ComponentResourceKey时,需要提供两部分信息:类库程序集中的引用和描述性的资源ID。类引用是WPF允许和其他程序集共享资源的关键部分。放使用资源时,需要提供相同的类引用和资源ID.
该类的实际外观并不重要,它不需要包含代码.定义该类型的程序集就是ComponentResourceKey将要从中查找资源的程序集。
public class CustomResources
{
}
现在可以使用该类和资源ID创建键名:
x:Key="{ComponentResourceKey TypeInTargetAssembly={x:Type local:CustomResource}}, ResourceId=SadTitleBrush}"
下面时generic.xaml文件的完整标记,它包含了一个单独资源——一个使用不同图形的ImageBrush对象:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ResourceLibrary">
<ImageBrush
x:Key="{ComponentResourceKey TypeInTargetAssembly={x:Type local:CustomResources}, ResourceId=SadTileBrush}"
TileMode="Tile"
ViewportUnits="Absolute" Viewport="0 0 32 32"
ImageSource="ResourceLibrary;component/sadface.jpg" Opacity="0.3">
</ImageBrush>
</ResourceDictionary>
使用库资源:
<Window x:Class="Resources.ResourceFromLibrary"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:res="clr-namespace:ResourceLibrary;assembly=ResourceLibrary"
Title="ResourceFromLibrary" Height="300" Width="300"
>
<Window.Resources>
<ImageBrush x:Key="TileBrush" TileMode="Tile"
ViewportUnits="Absolute" Viewport="0 0 32 32"
ImageSource="happyface.jpg" Opacity="0.3"></ImageBrush>
</Window.Resources>
<StackPanel Margin="5">
<Button Background="{StaticResource TileBrush}" Padding="5"
FontWeight="Bold" FontSize="14" Margin="5"
>A Resource From This Assembly</Button>
<Button Background="{DynamicResource {ComponentResourceKey TypeInTargetAssembly={x:Type res:CustomResources}, ResourceId=SadTileBrush}}"
Padding="5" Margin="5"
FontWeight="Bold" FontSize="14">
A Resource From ResourceLibrary</Button>
<Button Background="{DynamicResource {x:Static res:CustomResources.SadTileBrush}}"
Padding="5" Margin="5"
FontWeight="Bold" FontSize="14">
A Resource From ResourceLibrary
</Button>
</StackPanel>
</Window>