WPF允许在代码中以及在标记中的各个位置定义资源(和特定的控件、窗口一起定义,或在整个应用程序中定义)。
资源具有许多重要的优点,如下所述:
- 高效。可以通过资源定义对象,并在标记中的多个地方使用。这会精简代码,使其更加高效。
- 可维护性。可通过资源使用低级的格式化细节(如字号),并将它们移到便于对其进行修改的*位置。在XAML中创建资源相对于在代码中创建常量。
- 适应性。一旦特定信息与应用程序的其他部分分离开来,并放置到资源部分中,就可以动态地吸怪这些信息。例如,可能希望根据用户的个人喜好或当语言修改资源的细节。
一、资源集合
每个元素都有Resources属性,该属性存储了一个资源字典集合(他是ResourceDictionary类的实例)。资源集合可包含任意类型的对象,并根据字符串编写索引。
尽管每个元素都提供了Resources属性(该属性作为FrameworkElement类的一部分定义),但通常在窗口级别定义资源。这是因为每个元素都可以访问各自资源集合中的资源,也可以访问所有父元素的资源集合中的资源。
例如,分析下图中显示的包含三个按钮的窗口。其中的两个按钮使用了相同的画刷——绘制笑脸图像的平铺式的图像画刷。
在该例中,显然希望顶部和底部的两个按钮具有相同的样式。不过,以后可能希望改变图像画刷的特征。因此,在窗口的资源中定义图像画刷并在需要时重用该画刷是合理的。
下面的标记显示了如何定义画刷:
<Window.Resources> <ImageBrush x:Key="TileBrush" TileMode="Tile" ViewportUnits="Absolute" Viewport="0 0 32 32" ImageSource="happyface.jpg" Opacity="0.3"></ImageBrush> </Window.Resources>
为使用XAML标记中的资源,需要一种引用资源的方法。这是通过标记扩展完成的。实际上有两个标记扩展可提供使用:一个用于动态资源,另一个用于静态资源。静态资源在首次创建窗口时一次性地设置完毕。而对于动态资源,如果发生了改变,就会重新应用资源。在该例中,图像画刷永远不会改变,所以使用静态资源是合适的。
下面是一个使用该资源的按钮:
<Button Background="{StaticResource TileBrush}" Padding="5" FontWeight="Bold" FontSize="14" Margin="5" >A Tiled Button</Button>
上面的代码检索资源并将资源指定给Button.Background属性。可使用动态资源执行相同的操作(但开销稍大些):
<Button background="{DynamicResource TileBrush}"></Button>
二、资源的层次
每个元素都有自己的资源集合,为了找到期望的资源,WPF在元素树中进行递归搜索。在当前示例中,可将图像画刷从窗口的资源集合移到包含这三个按钮的StackPanel面板的资源集合中,而不必改变应用程序的工作方式。也可将图像画刷放到Button.Resources集合中,不过,需要定义画刷两次——为每个按钮分别定义一次。
需要考虑的另一个问题是,当使用静态资源时,必须总是在引用资源之前的标记中定义资源。这意味着尽管从标记角度看,将Window.Resources部分放在窗口的主要内容(包含所有按钮的StackPanel面板)之后是完全合法的,但这会破坏当前示例。当XAML解析器遇到它不知道的资源的静态引用时,会抛出异常(可是使用动态资源避免这一问题,但没必要增加额外的开销)。
因此,如果希望在按钮元素中放置资源,需要稍微重新排列标记,从而在设置背景之前定义资源。下面的实现该操作的一种方法:
<Button Margin="5" Padding="5" FontWeight="Bold" FontSize="14"> <Button.Resources> <ImageBrush x:Key="TileBrush1" TileMode="Tile" ViewportUnits="Absolute" Viewport="0 0 32 32" ImageSource="happyface.jpg" Opacity="0.3"></ImageBrush> </Button.Resources> <Button.Background> <StaticResource ResourceKey="TileBrush1"/> </Button.Background> <Button.Content>Another Tiled Button</Button.Content> </Button>
这个示例中的静态资源标记扩展语法稍有不同,因为资源被放在嵌套元素中(而不是特性中),为指向正确的资源,使用ResourceKey属性指定资源键。
有趣的是,只要不在同一集合中多次使用相同的资源名,就可以重用资源名称。这意味着可使用如下所示的标记创建窗口,该标记在两个地方创建图像画刷。
<Window x:Class="Resources.TwoResources" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="TwoResources" 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 Padding="5" Margin="5" FontWeight="Bold" FontSize="14" > <Button.Resources> <ImageBrush x:Key="TileBrush" TileMode="Tile" ViewportUnits="Absolute" Viewport="0 0 10 10" ImageSource="happyface.jpg" Opacity="0.3"></ImageBrush> </Button.Resources> <Button.Background> <StaticResource ResourceKey="TileBrush" /> </Button.Background> <Button.Content>Another Tiled Button</Button.Content> </Button> </StackPanel> </Window>
效果图如下所示:
在上面的代码中,按钮使用找到的第一个资源。因为是从自己的资源集合开始查找,所以第二个按钮使用按钮内部定义的资源,而第一个按钮从包含窗口获取画刷。
三、静态资源和动态资源
因为上面的示例使用了静态资源(在该例中是图形画刷),所以你可能认为对于资源的任何改变都不会有什么反应。然而,事实并非如此。
例如,设想在应用了资源并且显示了窗口之后,执行下面的代码:
ImageBrush brush = (ImageBrush)this.Resources["TileBrush"]; brush.Viewport = new Rect(0, 0, 5, 5);
上面的代码从Window.Resources集合中检索画刷,并对它进行操作(从技术角度看,代码改变了每个平铺图像的尺寸,缩小了笑脸图像并压缩图像模式使其更加紧凑)。当运行此代码时,可能不希望用户界面有任何反应——毕竟,它是静态资源。但这一变化会传播给两个按钮。实际上,会使用新设置的Viewport属性进行更新,而不管是通过静态资源还是动态资源使用画刷。
这是因为Brush类继承自Freezable类。Freezable类有一个基本的变化跟踪特性(如果不需要改变,能被“冻结”为只读状态)。这意味着,无论何时在WPF中改变画刷,所有使用该画刷的控件都会自动更新。控件是否是通过资源获取其画刷无关紧要。
现在,你可能想弄清楚静态资源和动态资源之间到底有什么区别。区别在于静态资源只从资源集合中获取对象一次。根据对象的类型(以及使用对象的方式),对象的任何便哈都可能被立即注意到。然而,动态资源在每次需要对象时都会重新从资源集合中查找对象。这意味着可在同一键下放置一个全新对象,而且动态资源会应用该变化。
下面通过一个示例演示它们之间的区别,分析下面的代码,这段代码用全新的(并且有些乏味的)存蓝色画刷替换了当前的图像画刷:
this.Resources["TileBrush"] = new SolidColorBrush(Colors.LightBlue);
动态资源会应用该变化,而静态资源不知道它的画刷已在Resources集合中被其他内容替换了,它仍然继续使用原来的ImageBrush。
下图显示了该示例,该窗口包含动态资源(顶部按钮)和静态资源(底部资源)。
通常不需要使用动态资源,使用静态资源应用程序也能够很完美地工作。创建依赖于Windows设置(例如系统颜色)的资源明显属于例外情况,对于这种情况,如果希望能够响应当前颜色方案的任何改变,就需要使用动态资源(否则,如果使用静态资源,将仍使用原来的颜色方案,直到用户重新启动应用程序为止)。在稍后介绍系统资源时,将讨论动态资源工作原理的更多相关内容。
作为一般规则,只有在下列情况下才需要使用动态属性。
- 资源具有依赖于系统设置的属性(如当前Windows操作系统的颜色或字体)。
- 准备通过编程方式替换资源对象(例如,实现几类动态皮肤特性)
然而,不应该过度使用动态资源。主要问题是对资源的修改未必会触发对用户界面的更新(在画刷示例中,因为构造画刷对象的方式——画刷具有内置的通知支持,确实更新了用户界面)。许多情况下,需要在控件中显示动态内容,而且控件需要随着内容的改变调整自身。对于这种情况,使用数据绑定更合理。
四、非共享资源
通常,在多个地方使用某种资源时,使用的是同一个对象实例。这种行为——称为共享——通常这也正是所希望的。然而,也可能希望告诉解析器在每次使用时创建单独的对象实例。
为关闭共享行为,需要使用Shared特性,如下所示:
<ImageBrush x:Key="TileBrush" x:Shared="False" ...></ImageBrush>
很少有理由需要使用非共享的资源。如果希望以后分别修改资源实例,可考虑使用非共享资源。例如,可创建包含几个使用同一画刷按钮的窗口,并关闭共享行为,从而可以分别改变每个画刷。由于效率底下,这种方式不常见。在这个示例中,开始时告诉所有按钮使用同一个画刷,当需要时在创建并应用新的画刷,这样可能更好。这样,只有当确定需要时才承担额外的画刷对象开销。
使用非共享资源的另一个原因是,可能希望以一种原本不允许的方式重用某个对象。例如,使用该技术,可将某个元素(如一幅图像或一个按钮)定义为资源,然后再窗口的多个不同位置显示该元素。
同样,通常这不是最佳方法。例如,如果希望重用Image元素,再合理的做法是存储相关信息(例如,用于指定图像源的BitmapImage对象)并在多个Image元素之间共享。如果只是希望标准化控件,让他们共享相同的属性,最好使用样式。通过样式可为任意元素创建相同或几乎相同的副本,当属性值还没有被应用时,可以重用他们而且可以关联不同的事件处理程序。如果简单地使用非共享资源克隆元素,就会丢失这两个特性。
五、通过代码访问资源
通常在标记中定义和使用资源。如有必要,也可在代码中使用资源集合。
正如已经看到的,可通过名称从资源集合中提取资源。为此,需要使用正确元素的资源集合。如前所述,对于标记没有这一限制。控件(如按钮)能够检索资源,而不需要知道定义资源的确位置。当尝试为Background属性指定画刷时,WPF会在按钮的资源集合中检索名为TileBrush的资源,然后检查包含StackPanel的资源集合,接下来检查包含窗口(这个过程实际上海会继续检查应用程序资源和系统资源)。
可使用FrameworkElement.FindResource()方法以相同的方式查找资源。下面是一个示例,当引发Click事件时,会查找按钮资源(或它的一个更高级的容器):
private void cmdChange_Click(object sender,RoutedEventArgs e) { Button cmd=(Button)sender; ImageBrush brush=(ImageBrush)sender.FindResource("TileBrush"); ... }
可使用TryFindResource()方法代替FindResource()方法,如果找不到资源,该方法会返回null引用,而不是抛出异常。
此外,还可通过编写代码添加资源。选择希望放置资源的元素,并使用资源集合的Add()方法。然而,通常在标记中定义资源。
六、应用程序资源
窗口不是查找资源的最后一站。如果在控件或其容器中(直到包含窗口或页面)找不到指定的资源,WPF会继续检查为应用程序定义的资源集合。在Visual Studio中,这些资源是在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" x:Name="DynamicBrush" TileMode="Tile" ViewportUnits="Absolute" Viewport="0 0 32 32" ImageSource="happyface.jpg" Opacity="0.3"></ImageBrush> </Application.Resources> </Application>
应用程序资源为在整个应用程序中重用对象提供了一种极佳的方法。在这个示例中,如果计划在多个窗口中使用图像画刷,这是一种很好的选择。
当某个元素查找资源时,应用程序资源仍然不是最后一站。如果没有在应用程序资源中找到所需的资源,元素还会继续查找系统资源。
七、系统资源
动态资源主要用于辅助应用程序对系统环境设置的变化做出响应。但这会导致一个问题——开始时如何检索系统环境设置并在代码中使用它们。
为此需要使用三个类,分别是SystemColors、SystemFonts和SystemParameters,这些类都位于System.Windows名称空间中。SystemColors类用于访问颜色设置;SystemFonts类用于访问字体设置;而SystemParameters类封装了大量的设置列表,这些设置描述了各种屏幕元素的标准尺寸、键盘和鼠标设置、屏幕尺寸以及各种图形效果(如热跟踪、阴影以及当拖动窗口时显示窗口内容)是否已经打开。
SystemColors、SystemFonts和SystemParameters类通过静态属性公开了它们的所有细节。例如,SystemColors.WindowTextColor属性提供了Color结构,您可方便地使用该结构。下面的示例使用该属性创建一个画刷,并填充元素的前景色:
label.Foreground=new SolidBrush(SystemColors.WindowTextColor);
或者为了提高效率,可使用现成的画刷属性:
label.Foreground=SystemColors.WindowTextBrush;
在WPF中,可使用静态标记扩展访问静态属性。例如,下面的标记演示了如何使用XAML设置同一标签的前景色:
<Label Foreground="{x:Static SystemColors.WindowTextBrush}"> Ordinary Text </Label>
上面的示例没有使用资源,这可能会引发一个小问题——当解析窗口并创建标签时,会根据当前窗口文本颜色的“快照”创建画刷。如果在应用程序运行时(在显示了包含标签的窗口后)改变了Windows颜色,Label控件不会更新自身。具有这种行为的应用程序被认为是不太合理的。
为解决这个问题,不能将Foreground属性直接设置为画刷对象,而是需要将它设置为封装了该系统资源的DynamicResource对象。幸运的是,所有SystemXxx类都提供可返回ResourceKey对象引用的补充属性集,使用这些引用可从系统资源集合中提取资源。这些属性与直接返回对象的普通属性同名,后面加上单词Key。例如,SystemColors.WindowTextBrush的资源键时SystemColors.WindowTextBrushKey。
下面的标记显示了如何使用来自SystemXxx类的资源:
<Label Foreground="{DynamicResource {x:Static SystemColors.WindowTextBrushKey}}"> Ordinary Text </Label>
上面的标记比前面的示例复杂一些。首先定义了一个动态资源,但该动态资源不是从应用程序的资源集合中提取资源,而是使用了一个由SystemColors.WindowTextBrushKey属性定义的键。该属性是静态属性,因此还需要使用静态标记扩展,从而让解析器理解正在尝试执行什么操作。
现已完成修改,当系统设置变化时,Label控件能够无缝地更新自身。