在WPF中,资源的含义和处理方式与传统的Win32和Windows Forms资源有所区别。首先,不需要创建.resx文件,只需要在工程中指出资源即可,其它所有的工作都由WPF完成。其次,WPF中的资源不再像.NET中有资源ID,在XAML中引用资源需要使用Uri。最后,在WPF的资源中,几乎可以包含所有的任意CLR对象,只要对象有一个默认的构造函数和独立的属性。在WPF本身的对象中,可以声明如下四种对象:Style、Brushes、Templates和DataSource。
在定义具体的资源之前,我们先考虑如下几个相关的问题:
1、资源的有效范围:
在WPF中,所有的框架级元素(FrameworkElement或者FrameworkContentElement)都有一个Resource属性。也就是说。我们可以在所有这类元素的Resource子元素中定义属性。在实践中,最常用的三种就是三种根元素所对应的资源:Application、Page和Window。顾名思义,在Application根元素下定义的资源将在当前整个应用程序中可见,都可以访问。在Page和Window中定义的元素只能在对应的Page和Window中才能访问。
2、资源加载形式:
WPF提供了两种资源类型:Static资源和Dynamic资源。
两种的区别主要有两点:A)、Static资源在编译时决议,而Dynamic资源则是在运行时决议。B)、Static资源在第一次编译后即确定了相应的对象或者值。此后不能对其进行修改,即使修改成功也是没有任何意义的,因为其它使用资源的对象不会得到通知。Dynamic资源不同,它只有在运行过程中真正需要时,才会在资源目标中查找。所以我们可以动态的修改Dynamic资源。显而易见,Static资源的运行效率高。
3、不管是Static资源还是Dynamic资源,所有的资源都需要设置Key属性x:Key=”KeyName”。因为WPF中的资源没有资源ID,需要通过资源Key来标识以方便以后访问资源。范围资源时我们根据资源的类型使用StaticResource或者DynamicResource标记扩展。
好了,对WPF中的资源所有了解后,我们看一些简单的例子:
1 <Window 2 3 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 4 5 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> 6 7 <StackPanel> 8 9 <StackPanel.Resources> 10 11 <SolidColorBrush x:Key="MyBrush" Color="gold"/> 12 13 </StackPanel.Resources> //我们在StackPanel元素的Resource子元素中定义了一个SolidColorBrush资源 14 15 <TextBlock Foreground="{StaticResource MyBrush}" Text="Text"/> 16 17 </StackPanel> 18 19 </Window>
在这个例子中,我们在StackPanel元素的Resource子元素中定义了一个SolidColorBrush资源。然后在后面通过StaticResouce标记扩展,利用前面的x:Key属性访问定义好的资源。
资源除了可以在XAML声明外,还可以通过代码进行访问控制。支持Resource属性的对象都可以通过FindResource、以及Resource.Add和Resource.Remove进行控制:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Window.Resouce>
<SolidColorBrush x:Key="MyBrush" Color="gold"/>
</Window.Resouce>
</Window>
我们先在代码XAML的Window.Resource中定义了一个MyBrush。在代码中可以如下对其进行访问:
SolidColorBrush brush = this.FindResource("MyBrush") as SolidColorBrush;
如果需要进一步修改或者删除资源时,可如下编码:
this.Resouce.Remove(“MyBrush”); //删除MyBrush资源
this.Resouce.Add(“MyBrush”); //重新动态添加资源
说明:以上三处的this引用都是特指我们定义MyBrush的元素Window。读者朋友可根据实际情况修改。
在本系列的之十三中简单介绍了WPF中资源的资源。但是,没有给出任何具体的实例,在这个Post中将给出一个动态资源的例子,也算是响应daxian110的请求。并适当的扩展在前一个Post当中没有涉及的知识。
我们先看一个例子程序:
1 <Window x:Class="WindowsApplication1.Window1" 2 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 3 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 4 Title="WindowsApplication1" Height="150" Width="100" Loaded="OnLoaded" 5 > 6 <Canvas> 7 <Button Click="OnClick" Canvas.Left="10" Canvas.Top="20" 8 Width="80" Height="30" Content="{DynamicResource TestRes1}"/> 9 <Button Canvas.Left="10" Canvas.Top="60" Width="80" 10 Height="30" Content="{DynamicResource TestRes2}"/> 11 </Canvas> 12 </Window>
程序很简单,在窗口中添加了两个按钮,我们需要关注的是这个ContentControl控件(实际就是Button)其中的Content属性。我们在Content属性中可以包含其它任何你想显示的内容。不止是字符串文本。这种抽象的处理使我们可以把所有的内容等同对待,减少了很多处理上的麻 烦。在本例子中,Content属性被和一个TestRes1和TestRes2关联起来。这个TestRes到底是什么呢?这就是动态资源的名称。具体的内容在显示按钮的时候决定。
注意上面Window中的Loaded属性,通过它我们可以设置一个函数名称,它将Window加载完成后被调用。下面就看看如何用代码控制TestRes:
private void OnLoaded(object sender, RoutedEventArgs e) { string szText1 = "Res Text1"; this.Resources.Add("TestRes1", szText1); string szText2 = "Res Text2"; this.Resources.Add("TestRes2", szText2); }
OnLoaded是Window1类中的一个成员函数,在这个函数里,我们需要添加资源,因为我们的XAML中需要使用TestRes1和TestRes2,运行时如果找不到对应资源,程序将失败。
现在,我们调用Add方法添加资源。第一个参数是资源的名称,第二个参数是添加的资源对象。
程序的运行效果如图1:
图1 图2
接下来我们看看修改资源的方法。在上面XAML的第一个按钮的Click属性中我们指定了一个OnClick事件方法。它将在点击按钮时调用,现在我们通过这个事件来修改另一个按钮的Content资源:
private void OnClick(object sender, RoutedEventArgs e)
{
string szText = "New Res Text";
this.Resources.Remove("TestRes2");
this.Resources.Add("TestRes2", szText);
}
OnLoaded实现同样的简单,先调用Remove方法删除已有的TestRes2资源,然后重新添加一个新的TestRes2资源对象。点击第一个按钮后,下面按钮的文本将自动修改为新的资源对象。运行效果如图2。
XAML加载器在分析XAML文件时,发现StaticResource,将会在当前Element的资源中查找指定的Key,如果查找失败,将沿 着逻辑树向上查找,直到Root元素。如果还没有找到资源,再查找Application下定义的资源。在Application中定义的资源适用于整个 应用程序。类似于全局对象。注意:使用Static资源时,不能向前引用。即使偶尔程序运行成功,向前引用的效率将非常低,因为它需要查找所有的 ResourceDictionay。对于这种情况,使用DynamicResource将更适合。
另一方面,XAML加载器发现DynamicResource时,将根据当前的属性设置创建一个表达式,直到运行过程中资源需要,才根据表达式从资 源中查找相关内容进行计算,返回所需的对象。注意,DynamicResource的查找于StaticResource基本类似,除了在定义了 Style和Template时,会多一个查找目标。具体的细节可参数MSDN。
首先建立一个默认的WPF工程,然后向工程中添加一个ICON资源。在添加资源后,我们可以选择资源的类型,如下图所示:
<ItemGroup>
<Resource Include="WTL2007.ico" />
</ItemGroup>
<ItemGroup>
<Content Include="WTL2007.ico" />
</ItemGroup>
那么,这两者之间有什么区别呢?我们先看Resource类型。如果某个文件在工程文本中被标识为Resource,这个文件将被嵌入到应用程序所在的Assembly。
工程编译后,工程中的所有指定资源文件一起创建一个.resources文件。对于本地化应用程序,将加载对应的卫星Assembly。
如果文件标识为Content,并且将CopyToOutputDirectory设置为Always或者PerserveNewest。这个文件被拷贝到编译输出目录与应用程序Assembly一起。
<Content Include="WTL2007.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
编译时,标识为Content的文件都会被创建一个文件映射关系。运行时,根据指定的Uri,WPF的加载机制将根据实际情况加载资源。
不管我们所引用的类型是Resource还是Content,在代码中,我们可以通过简单的相关Uri来访问这些资源:
<Object Property=”Ico/WTL2007.ico”/>
下面有几个比较好的建议,可以帮助我们选择资源的编译Action。对于下面的这些需求,应该选择Resource:
1、文件是本地化文件
2、应用程序部署后不再希望文件被修改
3、如果不希望对文件进行单独的部署或者管理,移动应用程序时不必担心资源的位置
对于下面的一些资源文件需求,我们应该选择Content:
1、文件不是本地化文件
2、希望文件在部署后可以被替换
3、希望文件可以被下载更新等等(注意不能是包含在应用程序Assembly)。
4 、某些不能被设置为 Resource 类型的文件,否则 WPF 不能识别。WPF将应用程序的起源地点进行概念上的抽象。如果我们的应用程序位于http://yilinglai.cnblogs.com/testdir/test.application。我们应用程序的起源地点是http://yilinglai.cnblogs.com/testdir/,那么我们就可以在应用程序中这样指定资源位置:
<Image Source=”pack://siteoforigin:,,,/Images/Test.JPG”/>
通过这种包装的Uri,使用资源的引用更加灵活。那么,这种类似Internet应用程序的资源包装Uri指定方式有什么优点呢?
1)、应用程序Assembly建立后,文件也可被替代。
2)、可以使文件只在需要使才被下载。
3)、编译应用程序时,我们不需要知道文件的内容(或者文件根本不存在)。
4)、某些文件如果被嵌入到应用程序的Assembly后,WPF将不能加载。比如Frame中的HTML内容,Media文件。
这里的pack://其实是一种URI(Uniform Resource Identifiers)语法格式。pack://<authority><absolute_path>,其中的authority部分是一个内嵌的URI。注意这个URI也是遵守RFC 2396文档声明的。由于它是被嵌入到URI当中,因此一些保留字符必须被忽略。在我们前面的例子中,斜线(”/”)被逗号(”,”)代替。其它的字符如”%”、”?”都必须忽略。
前面例子中的siteoforigin可以理解为一种authority的特例。WPF利用它抽象了部署应用程序的原始站点。比如我们的应用程序在C:\App,而在相同目录下有一个Test.JPG文件,访问这个文件我们可以用硬编码URI file:///c:/App/Test.JPG。另外一种方法就是这种抽象性:pack://siteoforigin:,,/Test.JPG。这种访问方法的便利是不言而喻的!在XPS文档规范中,对URI有更好的说明。有兴趣朋友可以在此下载。
也许你看到现在对此的理解有些问题。不用太着急,随着你对WPF越来越熟悉,会有更多的体会。对于WPF的新手(我也是),对于此点不必过度纠缠。因为WPF的Application类中提供了一些有用的方法:
Application.GetResourceStream (Uri relativeUri);
Application.GetContentStream(Uri relativeUri);
Application.GetRemoteStream (Uri relativeUri);
通过使用这些函数,隐藏了URI定位的细节。从这些函数的名称我们可以看出,它们分别对应于我在前面介绍的三种类型:Content、Resource和SiteofOrigin。
最后,简单的说明一下另一种使用资源的方式,直接定义资源,不使用任何的属性,具体的用法看例子就明白了:
<StackPanel Name="sp1"> <StackPanel.Resources> <Ellipse x:Key="It1" Fill="Red" Width="100" Height="50"/> <Ellipse x:Key="It2" Fill="Blue" Width="200" Height="100"/> </StackPanel.Resources> <StaticResource ResourceKey="It1" /> <StaticResource ResourceKey="It2" /> </StackPanel>