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 |
絕對路徑的鬆散文件 |
\\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" HorizontalAlignment="Center"> <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="{DynamicResourceborderBrush}" 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" Content="Content" /> <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" Content="Content" /> <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}" Content="Content" /> |
|
但對於DynamicResource來說,需要調用這個元素的SetResourceReference方法來更新依賴屬性的綁定。下邊的兩端代碼是相等的:
<Button x:Name="btnContent" Canvas.Left="50" Background="{DynamicResource buttonBackground}" BorderBrush="{ DynamicResource borderBrush}" Content="Content" /> |
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(); |
簡單吧?呵呵。有關Resource的東西基本上就這麼多了,換膚我們再開闢另一個話題來談吧。這可是WPF夠炫的Feature之一哦。。。。
一些與本文有關的小代碼片段及Localization的示例請點擊這裏下載。