第21章 资源
假设你正为一个窗口或对话框编写XAML,而且你决定你要为各种控件使用两个不同的font size。在窗口内的某些控件会有较大的font size,某些则会取得较小的font size。你大概知道哪个控件会取得哪种font size,但是你不太确定实际的font size会是多少。或许你想要在决定值之前先实验一下。
天真的做法是将FontSize的值硬编码在XAML内,像这样:
FontSize="14pt"
如果你稍后决定想要更大或更小的值,你可以用“查找并替代”的功能。虽然“查找并替代”的方法对于小项目可行,但是身为编程员,你一定知道这不是通用的好方法。假设你正处理复杂的渐变画刷,而不是简单的字体尺寸,你可能一开始会复制和粘贴渐变画刷,遍布整个程序。但是如果你需要改变此画刷,你需要改的地方可就相当多了。
如果你是在C# 中面对此问题,你不会用复制渐变画刷的方式,或者硬编码font size的方式。你会定义变量,或者(为了清楚地表达意图与提高效率)你可以在窗口类中定义一些常数字段:
const double fontsizeLarge = 14 / 0.75;
const double fontsizeSmall = 11 / 0.75;
你也可以改将它们定义成静态的只读值:
static readonly double fontsizeLarge = 14 / 0.75;
static readonly double fontsizeSmall = 11 / 0.75;
不同之处在于,常数是在编译期间计算的,并且在编译期间做值的替代,而静态变量是在运行期间计算的。
在编程语言中,此技巧实在太常用,也太有用,如果在XAML中也有类似的用法,会相当有价值。幸运的是,真的有。你可以先将对象定义成资源,然后就可以在XAML中复用它们。
本章所要讨论的“资源”(resource)和本书之前所提到的资源,差异相当大。我之前向你展示过如何使用Microsoft Visual Studio,来指示工程中某些文件要被编译成资源(Build Action设定为Resource)。这些资源更正确的称呼方式是“组件资源”(assembly resource)。常常,组件资源是二进制文件(binary file),像是icon和位图。但是在第19章,我也向你展示过如何对XML文件使用此技术。这些组件资源被储存在组件中(EXE或DLL),并且可以利用Uri对象来存取。
本章的资源,有时候被称为“局部定义的资源”(locally defined resource),因为它们是定义在XAML中(有时候是在C# 程序代码中),而且它们通常会和应用程序中的某element、控件、页面或窗口有关联。对于资源来说,只有在定义此资源的element内,以及在该element的孩子内,此资源才是可用的。你可以把这种资源想成是XAML中用来“弥补不具有C# 静态只读字段”的替代品。就和静态只读字段一样,资源对象在运行时被建立一次,而且被引用它们的element所共享。
所有的资源储存在一个ResourceDictionary类型的对象中,而且3个非常基本的类(FrameworkElement、FrameworkContentElement、Application)都定义了一个property,名为Resources,类型为ResourceDictionary。ResourceDictionary对象内的每个项目都具有一个key,用来识别该对象。通常这些key只是文字字符串。为了定义资源的key,XAML定义了一个x:Key attribute。
继承自FrameworkElement的element可以有一个Resources collection。此Resources section几乎总是以property element的语法定义在此element的最前面:
<StackPanel>
<StackPanel.Resources>
...
</StackPanel.Resources>
...
</StackPanel>
定义在此Resources section内的资源,可以在整个StackPanel内使用,也可以被StackPanel的任何孩子所使用。Resources section内的每个资源都具有下面的形式:
<SomeType x:Key="mykey" ...>
...
</SomeType>
你可以用attribute语法或property element语法,设定该对象的property。XAML element然后可以利用“标记扩充”(markup extension),引用到“某个key对应的资源”。顾名思义,markup extension是特别的关键词,是XAML专用的。搭配资源所使用的markup extension,名为StaticResource。
本章一开始描述的问题牵涉到两个不同的font size。下面的独立XAML文件展示了如何在一个StackPanel的Resources collection内定义两个font-size资源,然后StackPanel的child element如何存取这些资源。
FontSizeResources.xaml
<!-- ====================================================
FontSizeResources.xaml (c) 2006 by Charles Petzold
==================================================== -->
<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="clr-namespace:System;assembly=mscorlib">
<StackPanel.Resources>
<s:Double x:Key="fontsizeLarge">
18.7
</s:Double>
<s:Double x:Key="fontsizeSmall">
14.7
</s:Double>
</StackPanel.Resources>
<Button HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="24">
<Button.FontSize>
<StaticResource ResourceKey="fontsizeLarge" />
</Button.FontSize>
Button with large FontSize
</Button>
<Button HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="24"
FontSize="{StaticResource fontsizeSmall}" >
Button with small FontSize
</Button>
</StackPanel>
注意StackPanel element tag定义了一个XML命名空间的前缀s,代表System命名空间(clr-namespace:System;assembly=mscorlib),这允许引用此Resources collection中的Double结构。
StackPanel的Resources section包含两个Double对象的定义,它们的key分别是“fontsizeLarge”和“fontsizeSmall”。在任何Resources dictionary内,key都必须独一无二,不能有相同的两个key。18.7和14.7的值等价于14 pt和11 pt。
此StackPanel或StackPanel的child element都可以使用这些资源,使用方式有两种,都牵涉到StaticResource markup extension。第一个Button使用property element语法,存取FontSize资源,使用一个StaticResource的element和一个ResourceKey的attribute,来表示此项目的key:
<Button.FontSize>
<StaticResource ResourceKey="fontsizeLarge" />
</Button.FontSize>
第二个Button的语法更常见。FontSize attribute被设定成一个字符串,将“StaticResource”和key的名字放在大括号(curly bracket)内:
FontSize="{StaticResource fontsizeSmall}"
仔细看看此语法,你将会在本章看到许多这样的语法;在第23章,当我开始讨论数据绑定时,此语法也会一再出现。大括号表示此表达式(expression)是在一个markup extension中。没有StaticResource类。然而,有一个类名为StaticResourceExtension,继承自MarkupExtension,而且具有ResourceKey property。StaticResource被归类为markup extension,因为它让我们可以在XAML内做某些“原本只有在编程语言中才有可能”的事。此StaticResourceExtension类负责根据指定的key提供dictionary内的对应值。
在本章中,你还会看到两个其他的markup extension,分别为x:Static与DynamicResource,它们也都会放在大括号内。此大括号用来告诉XAML解析器,“这里出现的是markup extension”。在此大括号内,不可以出现引号。
少数时候,你可能会需要在文字字符串内用到一些大括号,但这和markup extension无关:
<!-- Won't work right! -->
<TextBlock Text="{just a little text in here}" />
为了让XAML解析器不要将它误认为名为just的markup extension,而开始做无谓的(而且会失败的)查找,我们在一组大括号前,插入一组空的大括号,作为escape sequence:
<!-- Works just fine! -->
<TextBlock Text="{}{just a little text in here}" />
Resources section几乎总是定义在一个element的最顶端,因为任何资源必须在文件中被引用之前定义。对于资源来说,向前引用(forward reference,也就是引用时尚未定义)是不允许的。
虽然特定Resources collection内,所有的key都不能重复,但是相同的key可以出现在两个Resources collection内。当一个资源必须被定位时,会先从element所引用的Resources collection开始查找,然后继续沿着这个树状结构往上找,直到找到此key为止。下面的独立XAML文件展示了这一过程:
ResourceLookupDemo.xaml
<!-- =====================================================
ResourceLookupDemo.xaml (c) 2006 by Charles Petzold
===================================================== -->
<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Orientation="Horizontal">
<StackPanel.Resources>
<SolidColorBrush x:Key="brushText" Color="Blue" />
</StackPanel.Resources>
<StackPanel>
<StackPanel.Resources>
<SolidColorBrush x:Key="brushText" Color="Red" />
</StackPanel.Resources>
<Button HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="24"
Foreground="{StaticResource brushText}">
Button with Red text
</Button>
</StackPanel>
<StackPanel>
<Button HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="24"
Foreground="{StaticResource brushText}">
Button with Blue text
</Button>
</StackPanel>
</StackPanel>
这里定义了3个StackPanel element。第一个是水平方向;另外两个StackPanel elements是第一个的孩子。为了简单起见,这两个StackPanel孩子都只包含一个按钮。
作为父亲的StackPanel具有一个Resources collection,此collection含有蓝色的SolidColorBrush(其key是“brushText”)。此StackPanel的第一个孩子也具有Resources collection,含有另一个SolidColorBrush对象,其key也是“brushText”,但颜色是红色。两个按钮都用key为“brushText”的StaticResource extension来设定Foreground property。第一个按钮(在红色画刷的StackPanel内)具有红色的文字,第二个按钮在不具有“brushText”资源的StackPanel内,所以“brushText”资源会用父亲StackPanel的,也就是蓝色。
定义具有相同名称的资源,是一个很有威力的技巧,特别是对于style来说(第24章的主题正是style)。Style让你可以定义property,适合多个element使用,甚至可以定义这些element如何对特定的事件和property的改变产生反应。在真实的WPF程序中,大多数的Resources collections都是用来定义style或改变style的定义。因为style是如此地重要,我认为最好先介绍资源,让你对style底层的技术建立良好的基础。记得一件事,如果本章似乎缺少什么(主要是,使用资源为特定的element和控件定义许多property),这些内容都会在第24章补齐。
资源是共享的,每个资源只需要建立一个对象。如果该资源没有被引用到,甚至不会建立对象。
你可能会怀疑,“我可以将一个element或控件定义成资源吗?”是的,你可以。比方说,你可以将下面的内容包含在ResourceLookupDemo.xaml的Resources section:
<Button x:Key="btn"
FontSize="24">
Resource Button
</Button>
然后你可以使用下面的语法,将Button作为“父亲StackPanel”的孩子(或者“某个孩子StackPanel”的孩子,这取决于你将此资源定义在何处):
<StaticResource ResourceKey="btn" />
这样是行得通的,但是你不能做两次。此Button对象被当作资源建立,只是一个对象,如果该Button是一个面板的孩子,就不可以是同一个面板的另一个孩子,或者另一个面板的孩子。请注意,当你在StaticResource element中引用此Button时,你无法改变此Button。将Button变成资源,你其实没有因此得到什么好处。那又何必这么做呢?
如果你认为你需要将控件和其他的element定义成资源,你可能真正需要的是,使用资源来定义此element的某些property,而非全部的property。很有可能你真正需要的是style,你应该去看看第24章。
虽然,资源几乎总是定义在XAML中,而非定义在程序代码中,你还是可以用C# 程序代码,将对象加入一个element的Resources collection中:
stack.Resources.Add("brushText", new SolidColorBrush(Colors.Blue));
显然,资源只是一个可以在多个element或控件之间共享的对象。此Add方法是由ResourceDictionary类所定义的,第一个参数是key,类型是object,但是最常用的是字符串。主要的3个定义了Resources collection的类(FrameworkElement、Framework- ContentElement、Application)也都定义了一个方法,名为FindResource,用来找出特定key的资源。StaticResourceExtension正是使用此方法来找出资源的。
有趣的是,调用某个element的FindResource方法,可能会从此element的Resources collection中找到资源,但是不会就停在此element。它也可能从此element的祖先(element tree中的祖先)找到资源。我想,FindResource是像这样实现的:
public object FindResource(object key)
{
object obj = Resources[key];
if (obj != null)
return obj;
if (Parent != null)
return Parent.FindResource(key);
return Application.Current.FindResource(key);
}
能够递归地(recursive)查找element tree,正是FindResource有价值的原因,因为直接使用key对Resources property进行索引,是做不到这一点的。也请注意,当element tree已经完全查找过,FindResource还会检查Application的Resources dictionary。你可以(而且应该)使用Application的Resources collection,来放置整个应用程序都可以使用的设定、style和主题(theme)。
一直到现在之前,我都建议你在Visual Studio中建立空的工程,这样你可以对WPF本身具有更好的理解,而不会为Visual Studio提供的一切所分心。现在你已经知道资源了,让Visual Studio帮我们处理编程风格(programming style),应该不会有问题了,至少我们可以试验性地做做看。
让我们使用Visual Studio来建立一个“Windows Presentation Foundation Application”工程,并且将名称指定为GradientBrushResourceDemo。Visual Studio会建立一个名为MyApp.xaml的文件,其Resources section已经有定义了,等着你输入:
MyApp.xaml
<Application x:Class="GradientBrushResourceDemo.MyApp"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="Window1.xaml"
>
<Application.Resources>
</Application.Resources>
</Application>
这就是为什么应用程序的Resources section被认为在WPF编程中相当重要的原因!让我们使用该Resources section来定义一个适用于整个应用程序的渐变画刷,现在MyApp.xaml变成这样:
MyApp.xaml
<Application x:Class="GradientBrushResourceDemo.MyApp"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="Window1.xaml"
>
<Application.Resources>
<LinearGradientBrush x:Key="brushGradient"
StartPoint="0, 0"
EndPoint="1, 1">
<LinearGradientBrush.GradientStops>
<GradientStop Offset="0" Color="Black" />
<GradientStop Offset="0.5" Color="Green" />
<GradientStop Offset="1" Color="Gold" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Application.Resources>
</Application>
Visual Studio也为MyApp.xaml建立一个MyApp.xaml.cs code-behind文件,但是此文件做的事不多。Visual Studio所建立的Window1.xaml文件,默认定义了一个Window element和一个Grid。
Window1.xaml
<Window x:Class="GradientBrushResourceDemo.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="GradientBrushResourceDemo" Height="300" Width="300"
>
<Grid>
</Grid>
</Window>
此原始的Window1.xaml.cs code-behind文件只是去调用InitializeComponent。
Window1.xaml.cs
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace GradientBrushResourceDemo
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}
}
}
对此程序代码,我使用一行C#语句加了一个资源到该窗口。
Window1.xaml.cs
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace GradientBrushResourceDemo
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1 : Window
{
public Window1()
{
Resources.Add("thicknessMargin", new Thickness(24, 12, 24, 23));
InitializeComponent();
}
}
}
在Window1.xaml文件中,StackPanel可以替代Grid,然后4个TextBlock element可以使用LinearGradientBrush资源与Thickness资源。
Window1.xaml
<Window x:Class="GradientBrushResourceDemo.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="GradientBrushResourceDemo" Height="300" Width="300"
>
<StackPanel>
<TextBlock Margin="{StaticResource thicknessMargin}"
Foreground="{StaticResource brushGradient}">
Gradient text
</TextBlock>
<TextBlock Margin="{StaticResource thicknessMargin}"
Foreground="{StaticResource brushGradient}">
Of black, green, and gold
</TextBlock>
<TextBlock Margin="{StaticResource thicknessMargin}"
Foreground="{StaticResource brushGradient}">
Makes an app pretty,
</TextBlock>
<TextBlock Margin="{StaticResource thicknessMargin}"
Foreground="{StaticResource brushGradient}">
Makes an app bold.
</TextBlock>
</StackPanel>
</Window>
我不喜欢使用Visual Studio预先构建的工程来建立我书上的例子,这一点都不是秘密。其中有一个麻烦是,我觉得有必要为一切改名,好让我的文件名不是MyApp和Window1。但是现在你已经看到此Application.Resource tag意味着什么,以及如何使用它,如果你想要使用Visual Studio工程和XAML 设计工具,应该不会有问题了。
在本章一开始,我描述了如何在程序中将两个不同的font size定义为静态只读字段。有趣的是,XAML定义一个markup extension,名为x:Static,专门用来引用静态的property或字段,也适合用于枚举成员。
比方说,假设你想要将一个Button的Content property设定成SomeClass类的静态property,名为SomeStaticProp。此markup extension语法是:
Content="{x:Static SomeClass:SomeStaticProp}"
或者,你可以在property element语法内使用一个x:Static element:
<Button.Content>
<x:Static Member="SomeClass:SomeStaticProp" />
</Button.Content>
静态字段或property的类型应该要符合你正在设定的property类型,或者可以被转成该类型。(当然,对object类型的Content property来说,任何东西都行)。比方说,如果你想要让特定element具有和“标题栏”相同的高度,你可以这么写:
Height="{x:Static SystemParameters.CaptionHeight}"
你没有被限制为只能使用WPF定义的静态property或字段,但是如果你存取非WPF类,你需要对类所在的CLR命名空间作XML命名空间的声明。
下面是一个独立的XAML程序,System命名空间被关联到XML前缀s。此程序显示Environment类的静态property的信息。
EnvironmentInfo.xaml
<!-- ==================================================
EnvironmentInfo.xaml (c) 2006 by Charles Petzold
================================================== -->
<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="clr-namespace:System;assembly=mscorlib">
<TextBlock>
<Label Content="Operating System Version: " />
<Label Content="{x:Static s:Environment.OSVersion}" />
<LineBreak />
<Label Content=".NET Version: " />
<Label Content="{x:Static s:Environment.Version}" />
<LineBreak />
<Label Content="Machine Name: " />
<Label Content="{x:Static s:Environment.MachineName}" />
<LineBreak />
<Label Content="User Name: " />
<Label Content="{x:Static s:Environment.UserName}" />
<LineBreak />
<Label Content="User Domain Name: " />
<Label Content="{x:Static s:Environment.UserDomainName}" />
<LineBreak />
<Label Content="System Directory: " />
<Label Content="{x:Static s:Environment.SystemDirectory}" />
<LineBreak />
<Label Content="Current Directory: " />
<Label Content="{x:Static s:Environment.CurrentDirectory}" />
<LineBreak />
<Label Content="Command Line: " />
<Label Content="{x:Static s:Environment.CommandLine}" />
</TextBlock>
</StackPanel>
此程序只想显示这些property的文字说明。其中大多数的property都是返回字符串,但是有两个例外:OSVersion property(目前正在运行的Microsoft Windows版本)类型为OperatingSystem ;Version property(.NET的版本)类型为Version。幸好,这两个类的ToString方法会将信息格式化成适合阅读的文字。
这两个x:Static标记表达式(markup expression)不能简单地设定给TextBlock的Text property(比方说)。没有自动的转换,可以将这些非字符串的对象转成字符串。取而代之,我将x:Static表达式设定给Label控件的Content property。此Content property可以被设定成任何对象,而且此对象将会通过其ToString方法显示出来。让这些Label element成为一个TextBlock的孩子(这么一来,它们会变成InlineUIContainer element的一部分),这允许在其中散布LineBreak element,以将输出分成多行。
虽然你可以在XAML Cruncher中运行此文件,但是不能在IE内运行。因为所有的项目(OSVersion和Version除外)都需要程序具有“安全权限”(security permission)才行,而在IE内运行XAML的做法,是没有这种安全权限的。
另一种使用x:Static的做法需要在你的C# 源代码中定义静态字段或property,然后从工程的XAML文件中存取它们。我将要展示的下一个工程名为AccessStaticFields。此工程包含一个XAML文件和一个C# 文件,它们都对应着相同的Window类,但是此工程也包含一个
C# 文件,名为Constants.cs,用来定义Constants类,此类具有3个静态只读的字段和property。这是一个全静态的类:
Constants.cs
//------------------------------------------
// Constants.cs (c) 2006 by Charles Petzold
//------------------------------------------
using System;
using System.Windows;
using System.Windows.Media;
namespace Petzold.AccessStaticFields
{
public static class Constants
{
// Public static members.
public static readonly FontFamily fntfam =
new FontFamily("Times New Roman Italic");
public static double FontSize
{
get { return 72 / 0.75; }
}
public static readonly LinearGradientBrush brush =
new LinearGradientBrush(Colors.LightGray, Colors.DarkGray,
new Point(0, 0), new Point(1, 1));
}
}
我定义这些项目的其中两个为静态字段,另一个为静态只读property。这只是为了有变化,对此例子来说其实效果都一样。(它们不需要是只读的,只是XAML文件无法设定这些字段,所以如果不让它们只读,也不会带来什么额外的好处。)因为这些静态字段和property都被定义成你的源代码的一部分,而不是属于标准的WPF组件,所以此XAML文件需要一个XML命名空间的声明来定义一个前缀(prefix),让此前缀和字段和property所属的类的命名空间产生关联。
下面的XAML文件,将XML前缀src和CLR命名空间Petzold.AccessStaticFields关联起来。此文件然后使用x:Static markup extension来存取静态字段和property。这些markup extension其中两个使用attribute语法,而第三个使用property element语法(纯粹只是为了有变化)。
AccessStaticFields.xaml
<!-- =====================================================
AccessStaticFields.xaml (c) 2006 by Charles Petzold
===================================================== -->
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:src="clr-namespace:Petzold.AccessStaticFields"
x:Class="Petzold.AccessStaticFields.AccessStaticFields"
Title="Access Static Fields"
SizeToContent="WidthAndHeight">
<TextBlock Background="{x:Static src:Constants.brush}"
FontSize="{x:Static src:Constants.FontSize}"
TextAlignment="Center">
<TextBlock.FontFamily>
<x:Static Member="src:Constants.fntfam" />
</TextBlock.FontFamily>
Properties from<LineBreak />Static Fields
</TextBlock>
</Window>
此code-behind文件没什么特别的。
AccessStaticFields.cs
//---------------------------------------------------
// AccessStaticFields.cs (c) 2006 by Charles Petzold
//---------------------------------------------------
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace Petzold.AccessStaticFields
{
public partial class AccessStaticFields : Window
{
[STAThread]
public static void Main()
{
Application app = new Application();
app.Run(new AccessStaticFields());
}
public AccessStaticFields()
{
InitializeComponent();
}
}
}
当然,此静态字段和property不需要在单独的文件内。在此工程的早期版本,我将它们放在AccessStaticFields类的C# 中,并且让x:Static markup extension引用src:Access- StaticFields类,而不是src:Constants类。但是我比较喜欢准备一个全静态的类,专门用来放置应用程序会用到的所有常数。
现在你知道如何把对象定义成资源,并利用StaticResource markup extension引用这些对象。你也知道如何使用x:Static引用静态property和类字段。这里所缺乏的是引用某对象实例(instance)的property和字段的能力。这项工作需要指定对象和该对象的某property,而且语
法已经超过StaticResource与x:Static的能力。这是“数据绑定”(data binding)的工作,是第23章的内容。
下面是另一个x:Static的例子。此独立的XAML文件使用x:Static表达式来设定一个Label控件的Content与Foreground attribute。
DisplayCurrentDateTime.xaml
<!-- =========================================================
DisplayCurrentDateTime.xaml (c) 2006 by Charles Petzold
========================================================= -->
<Label xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="clr-namespace:System;assembly=mscorlib"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="48"
Content="{x:Static s:DateTime.Now}"
Foreground="{x:Static SystemColors.ActiveCaptionBrush}" />
DateTime.Now对象是属于DateTime类型,Content property显示的是此结构的ToString方法所返回的字符串,所以结果具有良好的格式。XAML文件中最后一行设定此Foreground property为一个Brush对象(取自静态的SystemColors.ActiveCaptionBrush property),所以文字将会和窗口的标题栏(caption bar)具有一样的颜色。
我希望你不要预期时间会每秒自动更新!如果你使用XAML Cruncher来运行DisplayCurrentDateTime.xaml,那么DateTime.Now property会被取用一次,也就是在此Label控件被建立的时候。如果你在此XAML文件内键入一个没有害处的空格,或者按下F6,则XAML Cruncher会再次将此“新”版本传递到XamlReader.Load,而时间就会被更新了。
当系统颜色改变时,会发生什么事?你认为程序会自动更新此Label的前景颜色吗?去试试看吧!在桌面按下鼠标右键,从菜单中选择Properties,调出控制面板(Control Panel)的显示小程序(Display applet)。选择外观(Appearance)页,然后将Color Scheme改变成别的。默认是蓝色;还有别的选择,橄榄绿或银色。点击Apply或OK。(如果你的OS是Microsoft Windows Vista,鼠标右键点击桌面,选择Personalize,然后从列表中选择Desktop Colors。)
当窗口采用新的系统颜色时,你将会看到所有正在运行的程序的标题栏都会改变颜色。连XAML Cruncher的标题栏也改变颜色了,但是Label所显示的文字颜色依然保持不变。
你失望吗?你或许不应该惊讶。这和日期与时间的内容,是一样的情况。此静态的SystemColors.ActiveCaptionBrush property只会被取用一次,也就是在Label建立时。没有机制可以在property改变时自动更新此控件。当然,就和日期与时间一样,如果你在DisplayCurrentDateTime.xaml文件内键入无害的空格或按下F6,此文件会被重载,而Label就会重新建立,使用新的系统颜色了。
你希望Label的前景颜色会在系统颜色改变时随之自动更新吗?如果是的话,下面的内容就是你必须掌握的。
SystemColors、SystemParameters以及SystemFonts类都具有一大群静态property,XAML文件可以利用x:Static markup extension来取用这些property。如果你看过这3个类,你可能会注意到很奇怪的一件事:所有的静态property都是成对存在。比方说,如果有一个property名为Whatever,就会有一个property名为WhateverKey。所有以“Key”作为名称结尾的property,都会返回ResourceKey类型的对象。
静态SystemColors.ActiveCaptionBrush property返回一个SolidColorBrush类型的对象,所以我们自然会把此对象指定给Label的Foreground property,这需要Brush类型的对象。
SystemColors.ActiveCaptionBrushKey property返回一个对象,类型为ResourceKey。此ResourceKey对象应该也可以让我们取得和SystemColors.ActionCaptionBrush相同的画刷。此ResourceKey是一个dictionary的key,就像是你使用x:Key属性定义在XAML资源的key。此ResourceKey对象最大的差别是它包含一个Assembly类型的property,这使得XAML parser可以知道此dictionary是储存在哪一个组件(assembly)里。
使用StaticResource markup extension搭配SystemColors.ActiveCaptionBrushKey所返回的key,应该可以取得此画刷。或许下面这样可行:
Foreground="{StaticResource SystemColors.ActiveCaptionBrushKey}"
但结果却是不行。如果你试图用这一行替代DisplayCurrentDateTime.xaml的Foreground attribute,你会得到错误信息,告诉你“找不到资源”。如果你仔细想想就会恍然大悟。解析器在寻找的资源,是key为“SystemColors.ActiveCaptionBrushKey”的资源,但我们真正要的却是key为“静态SystemColors.ActiveCaptionBrushKey property的返回值”的资源。
其实没有表面上看起来这么难。下面的markup extension返回SolidColorBrush类型的对象:
{x:Static SystemColors.ActiveCaptionBrush}
所以此markup extension肯定返回ResourceKey类型的对象:
{x:Static SystemColors.ActiveCaptionBrushKey}
ResourceKey类型的对象正是StaticResource所需要的。所以,有了这样的洞察力和胆量,你试着将一个x:Static表达式嵌套在StaticResource表达式的内部:
Foreground="{StaticResource {x:Static SystemColors.ActiveCaptionBrushKey}}"
而这么做就成功了!请注意,一组大括号被嵌套在另一组大括号内,所以整个表达式结束的地方有两个大括号。此新的Foreground设定功能上等同于最初的设定:
Foreground="{x:Static SystemColors.ActiveCaptionBrush}"
SystemColors.ActiveCaptionBrushKey所引用到的资源是SolidColorBrush,这和SystemColors.ActiveCaptionBrush所返回的,是同一个画刷。
然而,采用此另类的语法似乎没有好处。如果你运行此新的XAML文件,并改变系统颜色,Label的前景颜色还是不受影响。
现在让我们再多做一点小改变。让我们把StaticResource改成DynamicResource,这是本章介绍的第三个也是最后一个markup extension:
Foreground="{DynamicResource {x:Static SystemColors.ActiveCaptionBrushKey}}"
而这却奏效了!现在如果你改变系统颜色,你将会看到此Label的文字颜色也会改变,和标题栏的颜色一致。
为了完整起见,你可能会对DynamicResource的property element语法感兴趣。在DisplayCurrentDateTime.xaml中,Label的end tag需要被加入,好让此 Foreground property变成一个property element:
<Label ... >
<Label.Foreground>
<DynamicResource>
<DynamicResource.ResourceKey>
<x:Static Member="SystemColors.ActiveCaptionBrushKey" />
</DynamicResource.ResourceKey>
</DynamicResource>
</Label.Foreground>
</Label>
StaticResource与DynamicResource代表存取资源的两种不同做法。两者都需要key,而且使用这些key来存取对象。如果是StaticResource,key被用来存取对象一次,然后对象会被保留。当你使用DynamicResource,此key会被保留,而对象需要的时候就会被取用。
当用户改变系统颜色,Windows操作系统会广播消息,告诉大家颜色改变了。应用程序如果想对此消息有所反应,做法就是invalidate(失效)自己的窗口。在WPF中,这种“失效”会被翻译成对InvalidateVisual的调用,这意味着每个element都会调用OnRender。这个时候,如果一个element的Foreground property(比方说)引用到一个动态的资源,保留的key就会被用来存取画刷。然而,这不像整个element被重新建立,实际上差远了。当你改变系统颜色而且Label文字颜色改变时,此Label内容会维持相同:此控件的日期与时间不会被更新。
DynamicResource的主要目的是用来存取系统资源,比如系统颜色。不要对DynamicResource寄予太多期望。当资源改变时,不会有通知。如果你需要控件和element可以依据其他对象的property改变而更新自己,你需要使用数据绑定(data binding),这是第23章的内容。
通常,你无法对资源使用向前引用(forward reference)。换句话说,用StaticResource markup extension所引用到的资源必须已经定义在此文件中,或定义在祖先element中。但是可能有时候,如果能引用到尚未定义的资源,会很方便。比方说,面板的start tag可能会包含一个Background attribute,它引用到一个资源,被定义在随后的一个Resources section中:
<StackPanel Background="{StaticResource mybrush}">
<StackPanel.Resources>
<SolidColorBrush x:Key="mybrush" ... />
</StackPanel.Resources>
...
这是行不通的。你可以从start tag移除Background attribute,然后改用property element语法:
<StackPanel>
<StackPanel.Resources>
<SolidColorBrush x:Key="mybrush" ... />
</StackPanel.Resources>
<StackPanel.Background>
<StaticResource ResourceKey="mybrush" />
</StackPanel.Background>
...
或者,你可以将StaticResource改变成DynamicResource,这使得资源的存取被延后,当此资源真正需要被显示在面板上的时候,才去存取资源。
你建立成为资源的画刷,本身可以使用系统颜色。下面的独立XAML将两个画刷定义为资源。LinearGradientBrush定义在“active标题颜色”和“inactive标题颜色”之间的渐变。第二个画刷是SolidColorBrush,类似地使用DynamicResource搭配SystemColors. ActiveCaptionColorKey。
DynamicResourceDemo.xaml
<!-- ======================================================
DynamicResourceDemo.xaml (c) 2006 by Charles Petzold
====================================================== -->
<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Background="{DynamicResource
{x:Static SystemColors.InactiveCaptionBrushKey}}">
<StackPanel.Resources>
<LinearGradientBrush x:Key="dynabrush1"
StartPoint="0 0" EndPoint="1 1">
<LinearGradientBrush.GradientStops>
<GradientStop Offset="0"
Color="{DynamicResource
{x:Static SystemColors.ActiveCaptionColorKey}}" />
<GradientStop Offset="1"
Color="{DynamicResource
{x:Static SystemColors.InactiveCaptionColorKey}}" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
<SolidColorBrush x:Key="dynabrush2"
Color="{DynamicResource
{x:Static SystemColors.ActiveCaptionColorKey}}" />
</StackPanel.Resources>
<Label HorizontalAlignment="Center"
FontSize="96"
Content="Dynamic Resources"
Background="{StaticResource dynabrush1}"
Foreground="{StaticResource dynabrush2}" />
</StackPanel>
请注意这两个资源使用DynamicResource去引用SystemColors.ActiveCaption- ColorKey与SystemColors.InactiveCaptionColorKey。这些是key(因为它们是搭配DynamicResource使用),但是这些key引用到的是颜色,而非画刷,因为它们是用来设定两个GradientStop对象的Color property。
此程序为自己着色的方式有3种。在上面,你看到StackPanel的背景是“基于SystemColors.InactiveCaptionBrushKey”的DynamicResource。下面的Label使用两种“局部定义资源”(locally defined resource)来为其背景和前景着色。然而,在此例中,这两个资源画刷是静态资源。
当系统的颜色改变,这两个“被定义成局部资源的”画刷也会跟着改变,取得新的Color property。然而,LinearGradientBrush和SolidColorBrush对象并没有被替代。它们是相同的对象。Label element引用这两个对象,所以当这些对象改变时,Label的背景和前景的property会反应出系统的颜色。
如果你将Label的Background和Foreground attribute改变成DynamicResource,此程序会停止响应系统颜色的改变!问题出在DynamicResource希望“重新建立”一个被key所
引用的对象。此画刷对象没有被重新建立,所以DynamicResource也就不会去更新Foreground与Background property。(至少,这是我目前找到的最合理的解释。)
你可以在你自己的资源定义中,使用“和系统颜色与其他系统设定相关联”的key。如果这么做的话,局部资源定义就会覆盖(override)系统设定,除非找不到局部资源。此独立的XAML文件是本章稍早ResourceLookupDemo.xaml程序的变化。
AnotherResourceLookupDemo.xaml
<!-- ============================================================
AnotherResourceLookupDemo.xaml (c) 2006 by Charles Petzold
============================================================ -->
<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Orientation="Horizontal">
<StackPanel>
<StackPanel.Resources>
<SolidColorBrush
x:Key="{x:Static SystemColors.ActiveCaptionBrushKey}"
Color="Red" />
</StackPanel.Resources>
<Button HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="24"
Foreground="{DynamicResource
{x:Static SystemColors.ActiveCaptionBrushKey}}">
Button with Red text
</Button>
</StackPanel>
<StackPanel>
<Button HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="24"
Foreground="{DynamicResource
{x:Static SystemColors.ActiveCaptionBrushKey}}">
Button with Blue text
</Button>
</StackPanel>
</StackPanel>
只有第一个内嵌的StackPanel含有Resources section,这里使用来自SystemColors. ActiveCaptionBrushKey的key,定义了一个红色的画刷。在StackPanel内的Button取得红色的画刷,但是其他的Button取得“active caption”画刷,而且当系统颜色改变时会跟着改变。
当你使用资源越来越多时,你可能会想要在多个应用程序之间共享资源。特别是,如果你开发了一个自定义style的collection,以让你公司的应用程序具有独特的外观与感觉时,尤其如此。
你想要在多个工程之间共享的资源,可以被集中在XAML文件中,其root element是ResourceDictionary。每个资源都是root element的一个孩子。下面是一个可能的resource dictionary,只有一个资源(不过,想要有多个资源也行)。
MyResources1.xaml
<!-- ===============================================
MyResources1.xaml (c) 2006 by Charles Petzold
=============================================== -->
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<LinearGradientBrush x:Key="brushLinear">
<LinearGradientBrush.GradientStops>
<GradientStop Color="Pink" Offset="0" />
<GradientStop Color="Aqua" Offset="1" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</ResourceDictionary>
下面是另一个resource dictionary,可以包含许多资源,但是这里只包含一个资源:
MyResources2.xaml.
<!-- ===============================================
MyResources2.xaml (c) 2006 by Charles Petzold
=============================================== -->
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<RadialGradientBrush x:Key="brushRadial">
<RadialGradientBrush.GradientStops>
<GradientStop Color="Pink" Offset="0" />
<GradientStop Color="Aqua" Offset="1" />
</RadialGradientBrush.GradientStops>
</RadialGradientBrush>
</ResourceDictionary>
你现在正在准备一个名为UseCommonResources的工程,而且你想要使用MyResources1.xaml和MyResources2.xaml所定义的资源。你可以让这两个文件成为此项目的一部分,将“Build Action”设定成“Page”或“Resource”。(不过设定成Page比较好,因为某些初步的处理会发生在编译期,将此文件从XAML转成BAML。)在此工程的“应用程序定义文件”(application definition file)中,你可以加上一个Resources section,语法如下面的文件所示。
UseCommonResourcesApp.xaml
<!-- ========================================================
UseCommonResourcesApp.xaml (c) 2006 by Charles Petzold
======================================================== -->
<Application xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
StartupUri="UseCommonResourcesWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="MyResources1.xaml" />
<ResourceDictionary Source="MyResources2.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
在文件的Resources section内,是一个ResourceDictionary element。Resource- Dictionary定义了一个名为MergedDictionaries的property,这是其他ResourceDictionary对象的collection,而这些对象是根据文件名来引用的。如果你只有一个resource dictionary,你可以从一个ResourceDictionary对象直接引用它,不需要使用ResourceDictionary. MergedDictionaries property element。
多个resource dictionary真的会被合并(merge)。如果你碰巧在多个文件中使用相同的key,那么当resource dictionary被合并时,早先出现的资源会被后来出现的相同key的资源替代。
除了可以将ResourceDictionary放在应用程序定义文件中,也可以放在某个XAML文件的Resources section中,但是这么做的话,该资源只能被该文件使用,无法在整个应用程序中使用。
最后,下面是Window element,它使用了定义在MyResources1.xaml与MyResources2.xaml文件内的资源。
UseCommonResourcesWindow.xaml
<!-- ===========================================================
UseCommonResourcesWindow.xaml (c) 2006 by Charles Petzold
=========================================================== -->
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Title="Use Common Resources"
Background="{StaticResource brushLinear}">
<Button FontSize="96pt"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="{StaticResource brushRadial}">
Button
</Button>
</Window>
接触像XAML这样的新语言,焦虑可能会伴随而来。这个语言真的定义得那么充分,让我们不会在路上跌得鼻青脸肿吗?很重要的是,程序代码要尽量少地重复,而资源可以帮助我们达到此目标。不只对象可以被定义一次,然后在整个应用程序中使用多次,资源也可以被存储在它们自己的ResourceDictionary文件内,然后让多个应用程序使用相同的资源。







