資源這個詞具有非常廣泛的意義。任何對象都可以是一個資源。一個在用戶 界面中經常使用的Brush或者Color可以是一個資源。一段文本或者一個圖形也可 以是一個資源。沒有什麼特殊的對象不可以成為一個資源。資源的底層處理機制 確保了獲取你所需要的資源成為可能,而不閉關心這個資源是什麼;同時,這套 機制可以簡單的識別和定位對象。
資源管理的核心是ResourceDictionary這個類。這是一個相當簡單的集合類 ,就像一個普通的Hashtable,允許以關鍵字聚合對象,同時提供一個索引器, 從而獲取到使用了這些關鍵字的對象。因此,原則上,使用ResourceDictionary 就像使用一個Hashtable一樣,正如范例6-1所示:
示例6-1
ResourceDictionary myDictionary = new ResourceDictionary( );
myDictionary.Add("Foo", "Bar");
myDictionary.Add("HW", "Hello, world");
Console.WriteLine (myDictionary["Foo"]);
Console.WriteLine(myDictionary["HW"]);
實際上,沒有必要如此創建一個ResourceDictionary對象。代替的,你可以 正常
使用由WPF提供的一些資源字典。例如,FrameworkElement基類——用戶界面 的大部分元素都從中派生,對外暴露一個名為Resources的屬性,這就是一個資 源字典。此外,在Xaml標記中,也可以看到這些資源字典的,正如示例6-1所示 :
示例6-2
<?Mapping XmlNamespace="urn:System" ClrNamespace="System" Assembly="mscorlib" ?>
<Window x:Class="ResourcePlay.Window1" Text="ResourcePlay"
xmlns="http://schemas.microsoft.com/winfx/avalon/2005"
xmlns:x="http://schemas.microsoft.com/winfx/xaml/2005"
xmlns:s="urn:System">
<Window.Resources>
<SolidColorBrush x:Key="Foo" Color="Green" />
<s:String x:Key="HW">Hello, world</s:String>
</Window.Resources>
<Grid Name="myGrid">
</Grid>
</Window>
x:Key這個屬性指定了資源字典中的關鍵字。原則上,你可以使用任何類型的 對象作為關鍵字,然而實際上,String是最普遍的選擇,盡管資源字典中使用的 是存儲在公有屬性中的截然不同的對象實例。
當使用xaml導入資源字典時,WPF可能會選擇推遲資源的生成。在特定的環境 下,這種做法可以使得xaml資源樹的一部分仍然以二進制形式(BAML)存在。只 有在需要的時候,才會還原這部分需要的資源。如果在界面出現時並不需要所有 的對象,這樣做可以明顯加快用戶界面的啟動時間。除了加快速度,這種優化在 極大程度上對代碼行為並沒有直接的效果。盡管如此,如果你的xaml標簽出錯了 ,因延遲創建而產生的錯誤,其浮現的時間可能會超過你的預計。
#譯者注:微軟推薦XAML被編譯成BAML(Binary Application ...二進制語言 程序標記語言)。
示例6-3展示了如何獲取示例6-2中定義的資源
示例6-3
Brush b = (Brush) this.Resources["Foo"];
String s = (String) this.Resources["HW"];
注意到,這段代碼使用this.Resources訪問ResourceDictionary,這對於在 同一個xaml中,由後台代碼訪問前台中的資源是非常適當的。盡管如此,這種方 式並不總是方便。如何讓一個xaml文件中的資源可以被應用程序中的所有窗體訪 問?我們所有窗體中復制同樣的資源代碼,這是乏味和效率低下的方法。此外, 如何獲取一個自定義控件的資源,而這個資源是由控件的父窗體指定的;而不是 在這個控件中對外暴露這些資源?為了解決這些問題,以及更容易通過用戶界面 取得一致性,FrameworkElement通過一個有層次的資源范圍(resource scope) ,擴展了ResourceDictionary機制
6.1.1 資源范圍
FrameworkElement不光為每個用戶界面元素提供了ResourceDictionary,還 提供了FindResource方法來獲取資源。示例6-4展示了如何使用該方法獲取和示 例6-4同樣的資源。
示例6-4
Brush b = (Brush) this.FindResource("Foo");
String s = (String) this.FindResource("HW");
這看起來毫無意思:在示例6-3中,我們已經有this.Resource[“Foo”]這樣 的方法獲取資源,為什麼還要有FindResource方法呢?這是因為,如果在指定位 置找不到資源,FindResource將繼續它的搜索,而不會停止。
示例6-5 使用了示例6-2中myGrid元素,以之取代this,此時,Grid沒有任何 資源,第一行代碼將變量b1設置為null。然而,因為b2是通過FindResource設置 的,WPF將考慮一定范圍內的所有資源,並不是那些直接設置給Grid的。這個范 圍是這麼確定的:從Grid元素開始,如果沒有找到資源,就會檢測它的父元素, 父元素的父元素。。。一直向上,直至根元素(有可能,父元素恰巧就是根元素 ——這將是一個很短的搜索。但是通常意義說,將要盡其所能的搜索)。結果, 變量b2被設為與示例6-3與中同樣的對象。
示例6-5
// Returns null
Brush b1 = (Brush) myGrid.Resources["Foo"];
// Returns SolidColorBrush from Window.Resources
Brush b2 = (Brush) myGrid.FindResource("Foo");
到此並未停止。如果FindResource方法直到UI根節點也沒有找到指定的資源 ,這時,將要在application中尋找。不僅所有的framework元素都有一個 Resource屬性,Application對象也是如此。示例6-6展示了如何在xaml標簽中定 義應用程序級范圍的資源(如果你使用的是Visual Studio 2005的項目模板,你 要將這段代碼放在MyApp.xaml中)。
示例6-6
<Application x:Class="ResourcePlay.MyApp"
xmlns="http://schemas.microsoft.com/winfx/avalon/2005"
xmlns:x="http://schemas.microsoft.com/winfx/xaml/2005"
StartingUp="AppStartingUp"
>
<Application.Resources>
<LinearGradientBrush x:Key="shady" StartPoint="0,0" EndPoint="1,1">
<LinearGradientBrush.GradientStops>
<GradientStop Offset="0" Color="Red"/>
<GradientStop Offset="1" Color="Black"/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Application.Resources>
</Application>
應用程序級范圍對於程序中的任何對象而言都是很方便的。例如,如果你使 用樣式和控件模板,肯定要將其放在應用程序的資源中,從而確保應用程序外觀 上的一致性。
對資源的搜索並未終止到應用程序級別。如果一個資源在UI樹上以及應用程 序中都不存在,FindResource方法最後將要在系統級范圍搜索,這個范圍內的設 置都是針對系統級而言的,例如被選中項顏色的配置,以及滾動條的寬度。
圖6-1展示了一個典型的資源層次。每個應用程序在運行時,都會有一些窗體 ,而這些窗體內都有一顆由很多元素組成的樹。如果FindResource在圖中標記為 1的元素上被調用,它首先會檢索改元素的資源字典。如果沒有找到,將會按照 途中數字標記的順序逐層向上搜索,直至達到系統資源這一層。
圖6-1
WPF在系統范圍定義了筆刷,字體,以及度量單位;用戶也可以在這一級別對 其進行配置。這是由SystemColors,SystemFonts以及SystemParameters這些類 的靜態屬性提供的(在這些類中,定義了400以上的資源,這裡並未列出,具體 語法請參閱SDK文檔)。示例6-7從系統范圍的資源獲取一個Brush對象,並將其 設置給toolTipBackgroud對象(參考第7章獲取更多Brush的信息)。
示例6-7
Brush toolTipBackground = (Brush) myGrid.FindResource (SystemColors.InfoBrushKey);
這些系統資源的類使用對象作為資源的關鍵字,而不是字符串。這樣避免了 命名沖突的風險,因為一個明確的對象可以唯一的標志系統資源。從而系統資源 和自定義資源之間不會有沖突。系統資源的類同時定義了一些靜態屬性,允許你 直接得到相關的對象,而不必從使用FindResource方法。例如,SystemColors類 定義了一個InfoBrush屬性,可以得到與示例6-7同樣的Brush對象,見示例6-8:
示例6-8
Brush toolTipBackground = SystemColors.InfoBrush;
使用這些屬性比通過FindResource方法獲取系統資源在編碼上要簡單的多。 盡管如此,使用FindResource方法仍然具有3個優勢:
第一個優點,如果用戶要改變應用程序的配色方案,而不受限於系統的配色 方案,可以通過將這些資源放在應用程序級別,從而覆蓋系統級別的設置。示例 6-9展示了一個應用程序級別的資源片斷,其中重新定義了InfoBrushKey資源的 值,這個資源是基於應用程序級別的。
示例6-9
// (Hypothetical function for retrieving settings)
Color col = GetColorFromUserSettings( );
Application.Current.Resources[SystemColors.InfoBrushKey] = new SolidColorBrush(Colors.Red);
替換過的值只會影響到示例6-7,但不會影響到示例6-8。這是因為後者是直 接在系統級別獲取資源,而前者,是通過FindResource方法向上搜索,在應用程 序級別找到了資源就返回,而不再繼續向上搜索了。
第二個優點,通過資源關鍵字,直接使用在xaml標簽中定義的系統資源。
第三個優點,可以使應用程序自動響應系統資源的變更。
後面兩個優點使用到了資源引用。
6.1.2 資源引用
目前為止,我們已經看到如何在代碼中獲取已命名資源的值。既然我們經常 使用資源值設置元素屬性,我們將著眼於如何為元素屬性設置資源值。這看起來 想一個可笑的試驗步驟。你可能希望代碼如同示例6-10:
示例6-10
this.Background = (Brush) this.FindResource (SystemColors.ControlBrushKey);
我們在逐步接近一個目標:將Background屬性設置為一個Brush對象,這個 Brush將使用當前控件背景選中的顏色。盡管如此,如果用戶在Windows控制面板 -顯示中改變設置,這個Background屬性並不會自動更新。示例6-10有效獲取了 一個資源值的快照。
示例6-11並未受上述問題所苦。並不是得到一個快照,而是將資源連接到 Background屬性上。
示例6-11
this.SetResourceReference(Window.BackgroundProperty, SystemColors.ControlBrushKey);
不同於示例6-10,如果系統資源值有所改變,這個Background屬性將會自動 獲取到新的值。這樣的實際結果時,如果用戶在Windows控制面板-顯示中改變 顏色配置,示例6-11將會確保你的用戶界面自動更新。
WPF定義了xaml語法,提供了與上述兩個示例同樣的功能(附錄A有關於XAML 更多的信息)。可以使用StaticResource和DynamicResource這兩個標簽。如果 使用系統資源或者其他運行時會發生改變的資源,請選則DynamicResource。如 果已知資源不會發生改變,選擇StaticResource,此時會進行一個快照,從而避 免了在連接跟蹤資源改變上的性能損失(這些損失雖然很小,但你也要為那些不 改變的資源盡量避免)。示例6-12展示了如何同時使用這兩個標簽。
示例6-12
<Window x:Class="ResourcePlay.Window1" Text="ResourcePlay"
xmlns="http://schemas.microsoft.com/winfx/avalon/2005"
xmlns:x="http://schemas.microsoft.com/winfx/xaml/2005">
<Window.Resources>
<SolidColorBrush x:Key="Foo" Color="LightGreen" />
</Window.Resources>
<Grid Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}">
<TextBlock FontSize="36" Width="200" Height="200"
Background="{StaticResource Foo}">Hello!</TextBlock>
</Grid>
</Window>
頂級的Window標簽中,定義了一個brush作為一個資源。TextBlock的 Backgroud屬性使用StaticResource引入了這個資源。這樣做與示例6-10很類似 :獲取一個快照,而且在應用程序運行時不會改變資源。
Grid的Backgroud屬性被設置為系統的“控件”顏色(典型的戰艦的灰白色, 這種顏色經常用於對話框的背景色)。既然這是一種用戶可配置的顏色,所以可 以在運行期進行改變,於是我們使用DynamicResource,達到了與示例6-10同樣 的效果。
這裡,DynamicResource語法比StaticResource有一點復雜。這是因為我們希 望使用的資源是通過SystemColors.ControlBruhKey對象確定其唯一性的,我們 可以嘗試使用如下語法:
<!-- This will not work as intended -->
<Grid Background="{DynamicResource SystemColors.ControlBrushKey}">
以上代碼看上去是對的,其實並未按照我們的設想執行,而是被解釋為一個 對資源動態的引用,這個資源是以字符串“SystemColors.ControlBruhKey”命 名,這樣的資源是不存在的,從而導致背景色沒有被設置。為了獲取到真正需要 的資源(按靜態屬性而不是按字符串形式),我們必須使用x:Static標簽,正如 示例6-11中的代碼所示。
6.1.3對圖形的復用
將圖片和圖形放到資源中是很有用的。這樣做有兩個主要原因。一個原因是 圖形可能非常復雜,將其直接放入xaml中會導致代碼難於閱讀。通過將其放入資 源標簽中,所有UI的結構將會更加清晰。另一個原因是支持復用,我們可以在多 個地方使用同樣的圖形。
有很多方法用於表示圖片和圖形,這將在第7章介紹。所有這些圖形都可以作 為資源,雖然對一些特定的類型有很多限制。尤其是,如果你使用從 FrameworkElement派生的任何元素作為一個資源,只能使用這個引用一次。這種 約束的原因是FrameworkElement是用戶界面樹的基礎。一個元素知道它的父元素 以及它的子元素,所以這個元素不可能出現在這棵樹的多個地方(當你使用資源 的時候,只能使用資源對象,而不能使用其copy副本)
所以雖然在資源中放置了一個Ellipse,或者是Canvas畫布中的所有圖形,你 都只能一次這個圖形,因為Ellipse和Canvas都是從FrameworkElement派生的。 為了減少混亂,將圖形放入資源而從xaml的主要標簽部分移除,這樣做有時是很 有用的。既然這樣,使用一次的約束就不是一個大的問題了。示例6-13就是使用 了名為Shape的Ellipse資源,演示了如何使用這個資源。
示例6-13
<Window.Resources>
<Ellipse x:Key="shape" Fill="Blue" Width="100" Height="80" />
</Window.Resources>
<StackPanel>
<Button>Foo</Button>
<StaticResource ResourceKey="shape" />
<Button>Bar</Button>
</StackPanel>
StaticResource標簽元素可以通過替換ResourceKey,在運行期動態修改。該 示例的結果如圖6-2所示:
圖6-2
一些繪圖類,像GeometryDrawing,DrawingGroup,是作為資源存儲圖形的最 好候選者。既然繪圖類不是從FrameworkElement派生的,我們可以自由的使用這 些類的示例在任何地方。如果需要,DrawingGroup可以把任意多的圖形圖片放到 一個單獨的資源圖形中,從Drawing類派生的各種不同類型都提供了訪問所有WPF 圖形的機制。參見第7章獲取更詳細信息。
示例6-14展示了如何定義並使用一個圖形資源,典型地使用了Drawing,並配 合以DrawingBrush。圖6-3顯示了相應的結果。
示例6-14
<Window.Resources>
<GeometryDrawing x:Key="drawing" Brush="Green">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="200" RadiusY="10" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
</Window.Resources>
<Rectangle Width="250" Height="50">
<Rectangle.Fill>
<DrawingBrush Drawing="{StaticResource drawing}" />
</Rectangle.Fill>
</Rectangle>
圖6-3
可選則地,我們可以定義一個DrawingBrush資源,從而將復雜的片斷移動到 Resource標簽內,從而在你使用資源的標簽位置,代碼變得相當地簡單,正如示 例6-15所示,執行結果與示例6-14相同(見圖6-3),但是使用資源的標簽則由 五行變成了一行。
示例6-15
<Window.Resources>
<DrawingBrush x:Key="dbrush" Drawing="{StaticResource drawing}" />
<GeometryDrawing x:Key="drawing" Brush="Green">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="200" RadiusY="10" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
</Window.Resources>
...
<Rectangle Width="250" Height="50" Fill="{StaticResource dbrush}" />
如果你想在同一個圖形出現在多個圖像中,你可能想降低一下成為資源的級 別,同時使用獨立的幾何對象作為資源。這一點接下來將會在圖形中被提及。示 例6-16展示了DrawingBrush標簽的用法,在其中嵌套了使用了Geometry資源的 GeometryDrawing子標簽。(這只是另一個橢圓,和圖6-3一樣,只不過是青色的 。)
示例6-16
<Window.Resources>
<EllipseGeometry x:Key="geom" RadiusX="200" RadiusY="30" />
</Window.Resources>
...
<Rectangle Width="250" Height="50">
<Rectangle.Fill>
<DrawingBrush>
<DrawingBrush.Drawing>
<GeometryDrawing Brush="Cyan" Geometry=" {StaticResource geom}" />
</DrawingBrush.Drawing>
</DrawingBrush>
</Rectangle.Fill>
</Rectangle>
在這個特定的例子中,資源的使用可能看上去有點極端。少許成效可能僅僅 是從無到有創建了一個新的幾何體。盡管如此,一些幾何體,例如PathGeometry ,可能會變得相當復雜,但是這種類型的復用,會變得更加有意義。
當圖形和幾何體功能強大而且可復用而且輕量級的時候,就會產生一個缺點 。因為它們並不是恰當的框架元素,從而不能利用WPF的系統布局。你可以使用 Brush的比例樣式(見第7章)按比例縮放這些元素,但是不能使用面板智能地適 應元素的布局(見第7章);因為所有的面板都是框架元素。你可能會想,從中 選擇一種既有使用框架元素特性(如布局)的能力,又有復用資源的能力—— ControlTemplate可以符合你的需要。
示例6-17展示了使用ControlTemplate資源的標簽。這個例子使用了Ellipse 元素多次,有時,我們不能將Ellipse元素成為一個資源。正如你在圖6-4中看到 的,每個Ellipse都被Grid獨立定位並且設置了大小。更多的模板技術見第5章。
示例6-17
<Window.Resources>
<ControlTemplate x:Key="shapeTemplate">
<Ellipse Fill="Blue" Margin="3" />
</ControlTemplate>
</Window.Resources>
...
<Grid Width="300" Height="150">
<Grid.RowDefinitions>
<RowDefinition Height="2*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Control Template="{StaticResource shapeTemplate}"
Grid.Row="0" Grid.Column="0" />
<Control Template="{StaticResource shapeTemplate}"
Grid.Row="0" Grid.Column="1" />
<Control Template="{StaticResource shapeTemplate}"
Grid.Row="1" Grid.Column="0" />
<Control Template="{StaticResource shapeTemplate}"
Grid.Row="1" Grid.Column="1" />
</StackPanel>
圖6-4
控件模板提供了一個復用任意標記的方法,但是如果 你不在圖形中使用基類FrameworkElement,使用更輕量級的DrawingBrush類會更 有效。而且如果你要創建大量包含相似圖形的繪圖,你甚至可以將獨立的幾何圖 形全都共享為資源。所有這些繪圖機制將要在第7章介紹。