首页 新闻 论坛 群组 Blog 文档 下载 读书 Tag 网摘 搜索 开源 FAQ 第二书店 博文视点 程序员
频道: 研发 数据库 中间件 信息化 视频 .NET Java 游戏 移动 服务: 人才 外包 培训
    图书品种:235680
       
热门搜索: ASP.NET Ajax Spring Hibernate Java

第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文件内,然后让多个应用程序使用相同的资源。


查看所有评论(0)条】

最近评论



正在载入评论列表...
热点评论