WPF通过资源来保存一些可以被重复利用的样式,对象定义以及一些传统的资源如二进制数据,图片等等,而在其支持上也更能体现出这些资源定义的优越性。比如通过ResourceDictionary的支持就可以通过资源来实现换肤功能,在ExpressionBlend中设计的酷炫造型也可以通过导出成资源来很容易的被程序员所引用,本地化的实现,访问另外程序集的嵌入式资源等等。这些都给我们提供了丰富的手段通过资源访问架构来构建丰富的富媒体应用程序。本文简单讲解了WPF Resources的分类及其常见用法,并简单触及用ResourceDictionary来管理多个Resources文件(这是换肤的基础)。
在WPF中的资源不仅依赖于核心.NET的资源系统,在其基础上也添加了对两种不同资源类型的支持:二进制资源和逻辑资源。而对于这些资源类型的构建动作也有了更多的支持选项。
1. 二进制资源
二进制资源其实是一些传统的资源项,比如位图,音频文件,视频文件,松散文件(Loose file)等等。对于这些资源项我们可以将其存储为松散文件,或者编译进程序集中。这与传统的.NET程序其实是相通的,但在WPF中提供了两种对二进制资源的构建选项:
· Resource: 将资源放入程序集中(如果是有本地化支持的话会编译到对应语言集的子程序集中。
· Content:将这个资源作为一个松散文件加入到程序集中,程序集会记录对应的文件是否存在及其路径。这就相当于我们web开发中常用的构建动作。
对于MSBuild来说这也是默认的构建类型,例如,
<Content Include="Images\Go.ico" /> <Content Include="Images\Go.jpg" /> <Content Include="Images\Go2.gif" /> <Content Include="Images\Go2.jpg" /> <Content Include="Images\information16.png" /> <Content Include="Images\pass16.png" /> <Content Include="Images\pass32.png" /> <Content Include="Images\unknown16.png" /> <Content Include="Images\warning.gif" /> <Content Include="Images\warning16.png" /> <EmbeddedResource Include="LoginForm.resx"> <SubType>Designer</SubType> <DependentUpon>LoginForm.cs</DependentUpon> </EmbeddedResource> <EmbeddedResource Include="OptionsForm.resx"> <SubType>Designer</SubType> <DependentUpon>OptionsForm.cs</DependentUpon> </EmbeddedResource> <EmbeddedResource Include="PageHistory.resx"> <SubType>Designer</SubType> <DependentUpon>PageHistory.cs</DependentUpon> </EmbeddedResource>
上面所示是一个普通的WinForm应用程序的项目文件,对于添加到其内部的二进制资源文件其默认的构建动作便是Content-表明其作为一个松散文件存储,只要保证其对应路径的文件存在则可以自动加载(而无需再你的打包文件中必须包含)。而你也会看到EmbeddedResource构建动作,这是WinForm的构建动作,它和Resource构建动作很相似,会在程序集中嵌入一个二进制资源,但是WPF中因为嵌入式资源比WPF还要优先,所以需要尽量避免使用。
之所以推荐使用Resource和Content构建类型是因为这样嵌入的资源可以很容易的在XAML中被引用,而且对于WPF的统一资源识别符也是专门针对这两种构建动作而设计的。相反地,对于EmbeddedResource构建动作嵌入的资源是不能在XAML中被引用的,除非自定义代码。
· 访问二进制资源
访问二进制资源最普通的就是对松散文件的访问,这和普通的.NET应用程序没什么两样,直接看例子吧:
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center"> <Button Background="Yellow" BorderBrush="Red" Margin="5"> <Image Height="21" Source="zoom.gif"/> </Button> <Button Background="Yellow" BorderBrush="Red" Margin="5"> <Image Height="21" Source="defaultThumbnailSize.gif"/> </Button> <Button Background="Yellow" BorderBrush="Red" Margin="5"> <Image Height="21" Source="previous.gif"/> </Button> </StackPanel>
上述的的松散文件只需要以Content构建加入项目即可. 这只是普通的访问方式,当然也是我们常用的,但也有很多你可能需要用到哦,不妨看看:
资源URI |
资源 |
zoom.gif | 存放于当前程序集;或是添加到项目的松散文件。 |
Folder/zoom.gif | 如果内部有目录结构时加入相对目录结构。 |
C:\Images\zoom.gif file://C:/Images/zoom.gif |
绝对路径的松散文件 |
\\FileServer\Images\zoom.gif | 共享目录的松散文件 |
http://my.net/zoom.gif | 位于某个站点上的松散文件 |
AssemblyReference;Component/ResourceName | 访问嵌入到另外一个程序集或EXE文件内的资源。Component是关键字,必须写。例如: MyDll;Component/Images/zoom.gif; |
pack://siteOfOrigin:,,,/Images/zoom.gif | 访问位于部署位置的资源。 |
2. 逻辑资源
逻辑资源是WPF特有的资源类型,它是存储在元素的Resources属性中的.NET对象,通常需要共享给多个子元素。换句话说,你可以声明一个SolidColorBrush对象当作一个逻辑资源,你也可以声明一个Style,然后再后续的XAML中简单通过{StaticResource ResourceName}来使用。资源定义需要有一个在ResourceDictionary中唯一的关键字x:Key(单独的ResourceDictionary中的键名不可以重复,多个ResourceDictionary中键名可以重复,会根据在逻辑数上的lookup的顺序来就近生效)。例如下边的逻辑资源声明:
<Window x:Class="WpfApplication1.Window2" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window2" Height="300" Width="400"> <Window.Resources> <SolidColorBrush x:Key="buttonBackground">Yellow</SolidColorBrush> <SolidColorBrush x:Key="borderBrush">Red</SolidColorBrush> <LinearGradientBrush x:Key="backgroundBrush" StartPoint="0,0" EndPoint="1,1"> <GradientStop Color="Blue" Offset="0"></GradientStop> <GradientStop Color="White" Offset="0.5"></GradientStop> <GradientStop Color="Red" Offset="1"></GradientStop> </LinearGradientBrush> </Window.Resources> <Window.Background> <StaticResource ResourceKey="backgroundBrush" /> </Window.Background> <Grid> <StackPanel Orientation="Horizontal" Horiz> <Button Background="{StaticResource buttonBackground}" BorderBrush="{StaticResource borderBrush}" Margin="5" Height="28"> <Image Height="21" Source="Images\zoom.gif"></Image> </Button> <Button Background="{StaticResource buttonBackground}" BorderBrush="{StaticResource borderBrush}" Margin="5" Height="28"> <Image Height="21" Source="Images\previous.gif"></Image> </Button> <Button Background="{StaticResource buttonBackground}" BorderBrush="{DynamicResource borderBrush}" Margin="5" Height="28"> <Image Height="21" Source="Images\next.gif"></Image> </Button> </StackPanel> </Grid> </Window>
一个资源可以被以StaticResource或者DynamicResource的方式来引用,这是以标记扩展(StaticResource/DynamicResource Markup Extension)来标注的。去以StaticResource还是以DynamicResource来引用一个资源,这取决于:
· 你是以什么样的方式来为你的应用创建资源的:每个Page?一个Application?一个松散XAML文件?还是一个只包含资源定义的程序集?
· 需要在运行时更新你的资源吗?
· 查找资源的行为—向前查询?
· 资源本身的行为和属性
3. Static Resource – 静态资源
StaticResource仅仅会被应用一次---在第一次需要资源时加载。而且这种引用方式不支持向前加载,所有的资源定义必须在引用之前定义。StaticResource通常用在:
· 设计的APP是将所有的资源放入Page或者App这个级别的Resource Dictionary中的,而且不需要在运行时重新计算—例如只保存一些松散文件,逻辑资源的声明等。
· 不需要给DependencyObject或者Freezable的对象设置属性。
· Resource Dictionary将被编译进DLL.
· 需要给很多的Dependency Property赋值。
将一个资源以Static Resource引用,需要用到Static Resource Markup Extension。 它在已经定义的资源中查询特定key的value为XAML的某个属性赋值。这个查询的行为与load-time查找类似,在当前Page的XAML中或者所有Application的Resources中查找,并在运生成运行时对象。
XAML Attribute Usage
<object property=”StaticResource key}” … />
XAML Object Element Usage
<object> <object.Property> <StaticResource ResourceKey = “key” …/> </object.Property> </object>
<Button Background="{StaticResource buttonBackground}" BorderBrush="{StaticResource borderBrush}" Margin="5" Height="28">
而后者我们在前面的例子中也看到了,用法稍微特殊,其实在这个情况下这个资源肯定是一个逻辑资源,相当于一段声明对象的代码。比如给window设背景:
<Window.Background> <StaticResource ResourceKey="backgroundBrush" /> </Window.Background>
Static Resource的查找行为
· 首先检查此对象本身的Resources集合内是否有匹配值(根据ResourceKey)
· 其次会在逻辑树中向上搜寻父元素的Resource Dictionary.
· 最后会检查Root级别的比如Page,Window,Application等。
4. Dynamic Resource – 动态资源
与Static Resource不同的是,Dynamic Resource可以在程序运行时重新评估/计算资源来生成对应的对象/值,它支持向前引用,只要请求的key在整个应用程序内的任何Resources Dictionary定义过就可以被加载。如果有多个相同的key存在,则最后搜索到的资源为有效。
动态资源常用于以下情况:
· 资源直到运行时才能被取定其值的。这些包含想系统资源,或者通过用户交互/用户可以设定的值。例如你可以用Setter Property语法来引用一些系统资源像SystemColors, SystemFonts等,这些是真正的Dynamic Resource,因为他们是来自用户的运行环境。
· 在Custom control中有创建/引用主题风格的需求.
· 在运行过程中调整(比如添加或者合并)ReourceDictionary.
· 需要向前引用的场景。
· 创建的Style的值与当前用户设定的主题或其他设定有关的。
· 运行过程中可能更改逻辑树的次序的。
下面的代码片段演示了如何在XAML中引用SystemFonts,这需要用DynamicResource标记:
<Style x:Key="SimpleFont" TargetType="{x:Type Button}"> <Setter Property = "FontSize" Value= "{DynamicResource {x:Static SystemFonts.IconFontSizeKey}}"/> <Setter Property = "FontWeight" Value= "{DynamicResource {x:Static SystemFonts.MessageFontWeightKey}}"/> <Setter Property = "FontFamily" Value= "{DynamicResource {x:Static SystemFonts.CaptionFontFamilyKey}}"/> </Style>
Dynamic Resource的查找行为
· 首先遍历请求对象本身定义的Resources集合。
· 然后遍历逻辑树上当前请求对象的父对象,直到遍历到Root(如Page.Reources, Window.Resources, UserControl.Resources等)
· 随后会遍历应用程序的Resources(即Application.Resources)
· 进而会Check当前激活的Theme的资源。
· 最后才会去遍历System Resources.
在程序中你可以通过myWindow.Resources[“key”]的方式来直接访问一个资源。另外,WPF还提供了TryFindResource(key)和FindResource(key)来支持资源搜索。FindResource方法在没找到资源的情况下会触发ResourceReferenceKeyNotFoundException异常。
其实通过上边的示例我们可以很清楚的看到,在使用静态资源的地方我们往往都可以使用动态资源,他们并没有什么合适与否之说,而选择它们中的哪一个,完全取决于你是否需要资源的使用者发现更新。我们可以再来比较一下二者的区别:
· 对于资源的更新会反映在那些使用了动态资源的元素上,这是他们最主要的区别。
· 性能上:因为动态资源要跟踪变化,所以需要占用更多的资源。而静态资源往往是在window或page加载之后来引用,动态资源会改善加载时间。但静态资源在使用时却会有些许性能的提升。
· 动态资源只能设置依赖属性只,而静态资源可以在任何地方使用。比如,我们可以声明一个逻辑资源把它当作一个元素来用,而动态资源却无法做到:
<Window x:Class="WpfApplication1.Window3" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window3" Height="300" Width="300"> <Window.Resources> <Button Background="Blue" Margin="5" Height="28" x:Key="prev"> <Image Height="21" Source="Images\previous.gif"></Image> </Button> </Window.Resources> <Grid> <Button Height="20" Width="70" C /> <StaticResource ResourceKey="prev" /> </Grid> </Window>
当你在XAML中使用StaticResource时,是不支持Forward Reference的,也就是说任何资源必须在XAML文件中声明之后才可以使用。如果是在同一个元素中定义,则只能使用Dynamic Resource。
<Window x:Class="WpfApplication1.Window3" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window3" Height="300" Width="300" Background="{DynamicResource winBackground}"> <Window.Resources> <Button Background="Blue" Margin="5" Height="28" x:Key="prev"> <Image Height="21" Source="Images\previous.gif"></Image> </Button> <LinearGradientBrush x:Key="winBackground" StartPoint="0,0" EndPoint="1,1"> <GradientStop Color="Blue" Offset="0"></GradientStop> <GradientStop Color="White" Offset="0.5"></GradientStop> <GradientStop Color="Red" Offset="1"></GradientStop> </LinearGradientBrush> </Window.Resources> <Grid> <Button Height="20" Width="70" C /> <StaticResource ResourceKey="prev" /> </Grid> </Window>
5. 资源的应用
前变也已经说过可以引用别的程序集的资源,那么到底如何引用呢?另外,我们都是在说XAML中引用资源,那么代码中又该如何去做呢?我们又通常会在什么地方定义资源呢?这里就来说一下这几个问题,以及某些特殊情况下的定义。
· 共享资源
默认情况下,当有一个资源被引用到多个地方是,使用的都是同一个对象实例,这通常是理想的行为。但你同样也可以把x:Shared=”False”来让每个引用资源的地方都生成一个不同的对象实例,这样可以独立进行修改。这通常用于多逻辑资源的声明。
· 程序代码中定义和应用资源
在代码中定义一个新的Resource,你需要首先得到一个ResourceDictionary的实例,然后再创建一个新的资源并将这个资源加入到ResourceDictionary的实例中。而在访问资源时,你需要用到myWindow.Resources[“key”]或者object.FindResource(key)函数。注意myWindows是你当前window的实例,而在用FindResource时,前边的object代表的是这个资源所在的ResourceDictionary的父对象。
private void Window_Loaded(object sender, RoutedEventArgs e) { Window3 window = new Window3(); window.Resources.Add("buttonBackground", new SolidColorBrush(Color.FromRgb(0,255,0))); window.Resources.Add("borderBrush", new SolidColorBrush(Color.FromRgb(255, 0, 0))); btnContent.Background = (Brush)window.FindResource("buttonBackground"); btnContent.BorderBrush = (Brush)window.FindResource("borderBrush"); }
注意在找不到资源时会抛出一个ResourceReferenceKeyNotFoundException异常,所以尽量调用TryFindResource方法更好些,如果失败将会返回null.
上边的例子是针对StaticResource来说的,它就相当于这段代码:
<Button x:Name="btnContent" Canvas.Left="50" Background="{StaticResource buttonBackground}" BorderBrush="{StaticResource borderBrush}" C />
但对于DynamicResource来说,需要调用这个元素的SetResourceReference方法来更新依赖属性的绑定。下边的两端代码是相等的:
<Button x:Name="btnContent" Canvas.Left="50" Background="{DynamicResource buttonBackground}" BorderBrush="{ DynamicResource borderBrush}" C /> btnContent.SetResourceReference(Button.BackgroundProperty, "buttonBackground"); btnContent.SetResourceReference(Button.BorderBrushProperty, "borderBrush");
SetResourceReference是可以在资源被加载到某个Resource Dictionary之前调用的,即便是FindResource会失败,但引用的建立仍然有效。
· 从另一个程序集中访问嵌入式资源
除了可以用特定的URI来访问别的程序中的二进制资源外,WPF可以从另外一个程序集中获取逻辑资源,这得用到ComponentResourceKey标记。要使用ComponentResourceKey,每个资源都必须有一个键名。然后你可以通过这样的方式访问:
<Button Background=”{DynamicResource {x:Static otherAssembly: MyClass.MyClassBrushKey }}” />
· Styles 和 Implicit Keys
样式是最常见的一种资源,而且它总是被定义在Resource Dictionary中,为了来重用。Style其实就是一系列分组的Setter的集合,用来设定逻辑资源的属性值,它有一种比较特殊的情形就是Implicit Keys,可以不声明一个x:Key的名字,而只设置x:TargetType的值,这样面对的就是对于所有这个类型的控件都使用这个样式。下边的示例中x:Key的值其实就是type-Button。
<Style TargetType="Button"> <Setter Property="Background"> <Setter.Value> <LinearGradientBrush> <GradientStop Offset="0.0" Color="AliceBlue"/> <GradientStop Offset="1.0" Color="Salmon"/> </LinearGradientBrush> </Setter.Value> </Setter> <Setter Property="FontSize" Value="18"/> </Style>
Style也是资源的一种---从某种意义上来说,它很类似于我们给普通HTML中的元素建立CSS. 对于Designer来说可能轻松一些哦。
6. Resource Dictionary –资源字典
所有的资源项在最终都会被整合到Resource Dictionary中的,也就是说无论是FrameworkElement的Resources,还是Window的Resources,还是Application的Resources,还是特定的ResourceDictionary中定义的resources在整个应用编译执行的时候实际上他们都在一起的作为可遍历集合共同存在于一个相对会话空间内的。
我们也提到过Resource的key是可以被允许有相同的,这样在遍历不同相对地址的Resource Dictionary时会根据StaticResource或者DynamicResource的lookup behavior来确定哪个有效。通常为了维护和灵活性的考虑,我们通常会将Resource Dictionary文件分成好几个,但在某些场合下我们只需要用其中某些资源,那么我么可以将资源从几个独立的文件中提取并合并,那么可以这么做:
<Window.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="Diction1.xaml"></ResourceDictionary> <ResourceDictionary Source="Diction2.xaml"></ResourceDictionary> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Window.Resources>
注意,在资源合并后,仍然会出现重复值的情况,那么最后取出的资源获胜。
7. Localization – 本地化
本地化和换肤其实都是在用ResourceDictionary来做文章的。说白了,Localization就是用不同语言下取不同事先设定好的资源来显示而已。要做到这些很容易,4步就可以轻松实现:
· 定义Resource Dictionary来包含不同语言下要显示的资源项。
创建单独的Resource Dictionary文件,并以语言本身名字来命名,并把en-US来作为默认语言环境(这里顺便就命名为default.xaml了)
Default.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:sys="clr-namespace:System;assembly=mscorlib"> <sys:String x:Key="Title_PM">Project Manager</sys:String> <sys:String x:Key="Title_PL">Project Lead</sys:String> <sys:String x:Key="Title_SD">Senior Developer</sys:String> <sys:String x:Key="Title_SA">System Architecture</sys:String> </ResourceDictionary>
zh-CN.xaml (注意对.NET命名空间的引用)
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:sys="clr-namespace:System;assembly=mscorlib"> <sys:String x:Key="Title_PM">项目经理</sys:String> <sys:String x:Key="Title_PL">项目主管</sys:String> <sys:String x:Key="Title_SD">资深开发工程师</sys:String> <sys:String x:Key="Title_SA">系统架构师</sys:String> </ResourceDictionary>
给应用程序添加默认资源:其实就是将默认的Resource Dictionary加入到Application的全局Resource里边。
<Application x:Class="WpfApplication1.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" StartupUri="Window1.xaml"> <Application.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="Language\default.xaml"/> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Application.Resources> </Application>
在Application启动时根据不同语言来加载以语言命名的XAML文件(Resource Dictionary)。因为对于重名的资源,后来加载的资源将会胜出,所以以当前语言名加载的XAML文件中的资源项将会被引用。这就是多语言的本质!
public partial class App : Application { protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); LoadLanguage(); } private void LoadLanguage() { CultureInfo currentCultureInfo = CultureInfo.CurrentCulture; ResourceDictionary langRd = null; try { langRd = Application.LoadComponent( new Uri(@"Language\" + currentCultureInfo.Name + ".xaml", UriKind.Relative)) as ResourceDictionary; } catch { } if (langRd != null) { if (this.Resources.MergedDictionaries.Count > 0) { this.Resources.MergedDictionaries.Clear(); } this.Resources.MergedDictionaries.Add(langRd); } } }
在XAML中引用资源。
<TextBlock Canvas.Top="50" Width="100" Height="24" Text="{StaticResource Title_PM}" />
· 大功告成,运行程序你会看到默认的语言的显示:Project Manager.当然如果你的默认文化是英语的话。用程序换成中文试试结果?没问题,在LoadLanguage()之前更改语言即可:
base.OnStartup(e); CultureInfo info = new CultureInfo("zh-CN"); Thread.CurrentThread.CurrentCulture = info; Thread.CurrentThread.CurrentUICulture = info; LoadLanguage();