[-]javascript
閒話WPF之一(WPF的結構) html
WPF進入咱們的生活已經不少年。(寫這句話讓我想起來了「我不作大哥好多年」。) 我的認爲在UI的實踐中,用戶須要的是易於操做的,更加絢麗的界面。這兩個應該是最基本、也是最重要的宗旨。而對於開發人員就是要用最簡單的方法開發出儘量漂亮的界面,而且效率也不能太差。(要求是否是有些過度啦!)除了在一些Web開發和特殊的應用中,不多有開發組配備單獨的美工,至少目前是這樣吧!根據本身目前對WPF的瞭解程度,感受WPF在其中某些方面確實有超強的震撼力。
客觀上講,Vista操做系統確實給咱們帶來了無可比擬的視覺效果。我本身深有體會,在近2個月的時間裏天天都是在Vista下的開發,回家後看到XP系統,始終有些不爽的感受。
WPF能夠認爲是MS利用原有.NET框架的一些特點,加上DirextX的產物。從下圖的WPF組件中,咱們能夠看出最底層仍然是一些內核API。(如下兩張圖片都來自互聯網。)
其中紅色顯示的組件是WPF的核心。Milcore是一個和DirectX交互的非託管組件,非託管代碼能帶給咱們更高效的處理,能更好的和DirextX交互。WPF的全部顯示都是由Dirext完成的。milcore中一個很是重要的功能就是Composition引擎,這個引擎對效率的要求很高,它的具體做用稍後介紹。因此milcore放棄了一些CLR的特徵來換取效率。而另外兩個紅色的組件都是創建在CLR基礎之上,利用了.NET的優點。 java
至於其中的User32組件有什麼做用,偶目前的知道的就是在WPF的某些應用場景中爲了某些兼容須要使用User32,其中就有DWM(桌面窗口管理)。DWM的內容又能夠寫上一大堆,感興趣的朋友能夠看SDK文檔。git
咱們除了關心WPF的基本結構外,更重要的 是WPF提供了什麼功能,請看下圖:web
圖中的每一個黃色塊都是一種媒體類型。這就表示WPF能夠處理幾乎全部的媒體類型:位圖、3D、音頻、視頻和文本等等。經過WPF,它集成了如今的GDI/GDI+、D3D/OPENGL以及多媒體的DSHOW等等。全部的東西都是等同對象,無論的3D仍是2D,或者文本。windows
結構圖中的Animate塊貫串了整個的結構,由於在WPF中咱們能夠對全部的可視內容進行動畫操做。這是很是讓人期待的功能。Animate下面咱們再次看到了Composition引擎,前面提到過它是位於milcore組件中。開發過程當中,咱們的界面元素功能有多種,好比圖片,視頻等等,最後顯示到窗口的內容能夠認爲只是一張圖片(準確說是Surface)。這個引擎的做用就是合成這些圖片和視頻元素最後進行提交顯示。緩存
怎麼感受是廢話一堆啊!我準備好了,你們的西紅柿、雞蛋不用吝嗇的,儘管雜吧!安全
在我開始看WPF文檔開始的幾天裏,腦子裏造成了一種錯誤的想法:WPF不就是XAML碼?當時的感受就是鬱悶啦,我學習WPF還得弄這個東西。給人的第一感受就是WPF很複雜。雖然對WPF的熟悉和了解還不是特別多,但如今已經知道這確實是一種錯誤的想法。性能優化
Charles Petzold先生曾有一篇文章介紹了WPF、XAML的一些關係(The Two APIs)。文章中說明了WPF爲何很複雜:由於WPF有兩套API,一套用於普通的編碼訪問(好比C#、VB.NET等其中.NET支持的語言。而另一套就是基於XML的API,被稱爲XAML(Extensible Application Markup Language)。服務器
XAML實現UI代碼和應用程序邏輯代碼的分離。在.NET 3.0和Windows Vista中,XAML與WPF一塊兒創建整個的UI。因爲XAML是基於XML的,因此每一個XAML代碼都確定是一個完整的XML文件。XAML繼承了XML全部的定義和規則。XAML與其餘XML擴展不一樣之處就是他所表示的意義。每一個XAML元素是一個.NET CLR類。基於XML使得咱們很是容易擴展和操做XAML。利用XAML的WPF這種關係,開發人員能夠單獨的設計漂亮的UI,也許真正的美工會更多的出現。咱們能夠把程序邏輯寫在單獨的文件或者是內聯嵌入到XML文件。
在XAML中使用得最多的XML功能應該有三個:命名空間、屬性和子元素。
先看一個簡單的XAML的例子:
<Window x:Class="FirstXAML.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="FirstXAML" Height="200" Width="300"
>
<Canvas>
</Canvas>
</Window>
其中的xmlns就是XML中的名字空間,在W3C中xmlns是以下定義的:
XML namespaces provide a simple method for qualifying element and attribute names used in Extensible Markup Language documents by associating them with namespaces identified by URI references.
簡單地說就是xmlns提供了一種方法把URI引用的名字空間定義爲當前XML文件的元素和屬性的默認命名空間。這裏表示當前這個XML文檔,也就是咱們的XAML文件,它的默認的命名空間就是http://schemas.microsoft.com/winfx/2006/xaml/presentation。
而後是屬性和子元素,XML對屬性的表示除了能夠用Property外,還能夠用子元素,在XAML中也是如此,看一個簡單的例子:
<Button Width="6">
<Button.Background>White</Button.Background>
</Button>
例子當中就使用了屬性和子元素兩種方式來指定屬性。其中的Width是直接用屬性表示,Background屬性是用子元素表示。在多數時候,但不是全部,你能夠自由選擇這兩種表示方式之一。
XAML被編譯爲BAML(Binary Application Markup Language)文件。一般,BAML文件比XAML更小,編譯後的BAML都是Pre-tokenized的,這樣在運行時能更快速的加載、分析XAML等等。這些BAML文件被以資源的形式嵌入到Assembly當中。同時生成相應的代碼(文件名稱是**.g.cs或者**.g.vb),這些代碼根據XAML元素分別生成命名的 Attribute字段。以及加載BAML的構造函數。
最後,關於XAML的優勢,我附上一點翻譯過來的條款,可能更直觀:
XAML除了有標記語言、XML的優勢外,還有以下一些優勢:
用XAML設計UI更簡單
XAML比其餘的UI設計技術所需編碼更少。
XAML設計的UI方便轉移、方便在其餘環境提交。好比在Web或Windows Client。
用XAML設計動態UI很是容易
XAML給UI設計人員帶來新的革命,如今全部的設計人員再也不須要.NET開發的知識一樣能夠設計UI。在不遠的未來,終端用戶能夠看到更漂亮的UI。
在前一篇文章中,指出xmlns的做用是設置XML文件的命名空間。相似的,xmlns:x的做用也是指定命名空間。這裏爲何是x而不是其餘的,咱們能夠簡單的理解爲其只是MS的一個命名而已,沒有任何特殊的意義,固然,爲了不和它的衝突,咱們定義本身的命名空間的時候不能是x。
而另外一個x:Class的做用就是支持當前Window所對應的類,前面已經說過每一個XAML元素都是一個CLR類型,這裏的x:Class是Window的一個屬性,屬性的內容指出當前的窗口類是FirstXAML名字空間下的Windows1。爲何須要類,而不所有用XAML實現?XAML的主要做用仍是編寫UI部分,咱們仍然須要用代碼對程序邏輯進行更深層次的控制。
好了,這是兩個最基本的名字空間。一樣地,名字空間也能夠自定義,而且這個自定義會給咱們帶來很大的方便。咱們定義以下的一個類:
namespace DataBind4Image { public class GroupData { //具體的細節忽略 } }
若是想在XAML文件中使用這個GroupData類對象,咱們就能夠經過自定義的名字空間引入這個類:
xmlns:local="clr-namespace:DataBind4Image"
這裏的後綴local只是一個標識,你能夠設置爲任何你喜歡的惟一標識。經過這個引入定義咱們就能夠在XAML文件中用local來標識DataBind4Image當中的任何類。訪問GroupData類時只須要加上local就能夠識別了:<local:DrawingGroupData/>
利用名字空間,除了能夠引入咱們定義的當前工程的類,還能夠引入任何的Assembly。直接看例子是最簡單的:
<Window x:Class="WindowsApplication1.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:sys="clr-namespace:System;assembly=System" > <ListBox> <sys:String>One</sys:String> </ListBox> </Window>
例子當中引入.NET的System Assembly,經過它咱們就能夠直接使用System的任何類。利用這種相似的方式,咱們能夠在XAML中使用幾乎全部的DOTNET框架類。
最後說明一下在XAML中inline嵌入程序邏輯處理代碼的狀況。利用<CDATA[…]]>關鍵字引入處理代碼。這種狀況在實際當中不太合適,咱們不該該採用UI和邏輯混合的方式。詳細的解釋能夠參數Windows SDK文檔。
<![CDATA[ void Clicked(object sender, RoutedEventArgs e) { button1.Content = "Hello World"; } ]]></x:Code>
前面提到過每一個XAML元素表示一個.NET CLR類。多數的XAML元素都是從System.Windows.UIElement, System.Windows.FrameworkElement, System.Windows.FrameworkContentElement和System.Windows.ContentElement繼承。沒有任何的XAML元素與.NET CLR的抽象類對應。可是不少元素都有一個抽象類的派生類對應。
一般有以下四種通用的XAML元素:
Root元素:Windows和Page是最經常使用的根元素。這些元素位於XAML文件的根元素,幷包含其餘元素。
Panel元素:幫助佈置UI位置。經常使用的是StackPanel, DockPanel, Grid和Canvas。
Control元素:定義XAML文件的控件類型。容許添加控件並自定義。 Document元素:幫助實現文檔提交。主要分爲Inline和Block元素組,幫助設計的外觀相似文檔。一些有名的Inline元素有Bold,LineBreak, Italic。Block元素有Paragraph, List, Block, Figure和Table。
XAML元素的屬性與.NET類對象的屬性相似,XAML的面向對象特徵使得它的行爲與以前的HTML相似。每一個屬性(其實是類屬性)繼承了父元素的屬性或者重載(若是從新設置了屬性)。
說明:這裏的Win32特指Vista操做系統以前的全部圖形系統:GDI、GDI+、Direct3D。
GDI是當今應用程序的主流圖形庫,GDI圖形系統已經造成了不少年。它提供了2D圖形和文本功能,以及受限的圖像處理功能。雖然在一些圖形卡上支持部分GDI的加速,可是與當今主流的Direct3D加速相比仍是很弱小。GDI+開始出現是在2001年,它引入了2D圖形的反走樣,浮點數座標,漸變以及單個象素的Alpha支持,還支持多種圖像格式。可是,GDI+沒有任何的加速功能(所有是用軟件實現)。
當前版本的WPF中,對一些Win32功能尚未很好的支持,好比WMF/EMF文件,單個象素寬度的線條等等。對於這些需求還須要使用GDI/GDI+來實現。
在Windows Vista中,GDI和GDI+仍然支持,它們與WPF並行存在,可是基本上沒有任何功能性的改進。對GDI和GDI+的改進主要集中在安全性和客戶相關問題上。WPF的全部提交都不依賴於GDI和GDI+,而是Direct3D。而且全部的Primitive都是經過Direct3D的本地接口實現的。還記得我前面隨筆中提到過的Milcore嗎?它就是和Direct3D交互的非託管代碼組件。因爲WPF的大部分代碼都是以託管代碼的形式存在的,因此WPF中有不少託管、非託管的交互。固然,在一些圖形卡不支持WPF所須要的功能時,WPF也提供了稍微低效的軟件實現,以此來支持在某些PC上運行WPF應用程序。
在Windows Vista中,Direct3D的關鍵改進就是引入了新的顯示驅動模型。VDDM驅動模型虛擬化了顯卡上的資源(主要是顯示內存),提供了一個調度程序,所以多個基於Direct3D的應用程序能夠共享顯卡(好比WPF應用程序和基於WPF的Windows Vista桌面窗口管理)。VDDM的健壯性、穩定性也獲得了提升,大量的驅動操做從內核(Kernel)模式移動到了用戶(User)模式,這樣提升了安全性,也簡化了顯示驅動的開發過程。
在Windows Vista中存在兩個版本的Direct3D:Direct3D 9和Direct3D 10。WPF依賴於Direct3D 9,這樣能更普遍的解決兼容性問題。另一個很是重要的緣由就是爲Vista的服務器版本提升方便,由於服務器版本的Vista對顯卡和Direct3D基本上沒有任何的要求。同時WPF也支持Direct3D 10。Direct3D 10依賴與VDDM,只能在Windows Vista上使用。因爲Windows XP沒有VDDM,雖然Microsoft作了很大的努力來改善XP中Direct3D 9相關驅動,提升內容的顯示質量,可是因爲XP中沒有對顯卡資源的虛擬化,強制全部的應用程序都用軟件提交。
WPF對某些多媒體的功能支持還須要依賴老的技術,好比DirectShow。當咱們進行音頻視頻的捕捉或者其它任務時,只能直接用DirectShow實現,而後再用HwndHost嵌入到WPF內容當中。
利用相似的技術,咱們能夠在WPF應用程序中顯示自定義格式的內容。經過提供自定義的DirectShow CODEC,而後用Media元素實現和WPF內容毫無限制的集成。
另外,WPF對XPS等文檔的打印輸出也獲得了極大的改善。XPS文檔自己的規範也極大的提升了其打印的質量,XPS文檔的規範能夠參考MSDN的資料。除了打印,Vista操做系統中對遠程的改進也部分依賴於WPF,好比有遠程協助、遠程桌面和終端服務等等。它們的實現過程是經過發送一系列的「遠程」命名到客戶端,客戶根據本身PC的性能和命名進行顯示,這樣顯示的質量能獲得極大的提升。
在WPF中,對Direct3D進行各類封裝。固然,若是你自己對Direct3D/OpenGL很熟悉,也能夠直接在WPF中使用。封裝後的Direct3D更容易使用。而且在Web應用程序(XBAP)也可使用Direct3D。在WPF中使用的Direct3D,沒有直接用非託管代碼控制所擁有的靈活性,也不能直接對硬件進行底層控制。
WPF中全部的提交都是矢量形式的,咱們能夠對圖像或窗口進行任意級的放縮,而圖像的質量不會有任何的損耗。
在前面關於XAML的Post當中,簡單說明了XAML若是引入自定義名稱空間。還提到過XAML基本上也是一種對象初始化語言。XAML編譯器根據XAML建立對象而後設置對象的值。好比:
<Button Width=」100」/>
很明顯,咱們設置Button的寬度屬性值爲100。可是,這個「100」的字符串爲何能夠表示寬度數值呢?在XAML中的全部屬性值都是用文本字符串來描述,而它們的目標值能夠是double等等。WPF如何將這些字符串轉換爲目標類型?答案是類型轉換器(TypeConverter)。WPF之因此知道使用Double類型是由於在FrameworkElement類中的WidthProperty字段被標記了一個TypeConverterAttribute,這樣就能夠知道在類型轉換時使用何種類型轉換器。TypeConverter是一種轉換類型的標準方法。.NET運行時已經爲標準的內建類型提供了相應的TypeConverter。因此咱們能夠用字符串值指定元素的屬性。
然而並非全部的屬性都標記了一個TypeConverterAttribute。這種狀況下,WPF將根據屬性值的目標類型,好比Brush,來判斷使用的類型轉換器。雖然屬性自己沒有指定TypeConverterAttribute,可是目標類型Brush本身標記了一個TypeConverterAttribute來指定它的類型轉換器:BrushConverter。因此在轉換這種屬性時將自動使用目標值類型的BrushConverter將文本字符串類型的屬性值轉換爲Brush類型。
類型轉換器對開發人員有什麼做用呢?經過它咱們能夠實現自定義的類型轉換。下面一個例子演示瞭如何從Color類型轉換爲SolidColorBrush。
[ValueConversion(typeof(Color), typeof(SolidColorBrush))] public class ColorBrushConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { Color color = (Color)value; return new SolidColorBrush(color); }
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { return null; } }
而後咱們能夠在資源中定義一個ColorBrushConverter 實例(src是一個自定義命名空間,引入了ColorBrushConverter 類所在的Assembly)。
<Application.Resources> <src:ColorBrushConverter x:Key="ColorToBrush"/> </Application.Resources>
最後使用這個自定義的類型轉換器:
<DataTemplate DataType="{x:Type Color}"> <Rectangle Height="25" Width="25" Fill="{Binding Converter={StaticResource ColorToBrush}}"/> </DataTemplate>
其實WPF所使用的這種類型轉換從.Net Framework1.0已經開始並普遍應用。有興趣的朋友能夠參考MSDN的介紹:通用類型轉換(Generalized Type Conversion)
閒話WPF之六(XAML的標記兼容性(Markup Compaibility))
繼續XAML的話題,在前一個Post當中簡單介紹了XAML的類型轉換器(TypeConverters)。此次介紹一些XAML標記兼容性(Markup Compatibility)的相關內容。
利用XAML標記兼容性實現更增強大的註釋功能
寫過XAML的朋友應該都知道:在XAML中能夠經過<!--****-->標記來實現註釋。可是,利用XAML標記兼容性,還提供了其它更增強大的註釋功能。 <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:mc=http://schemas.openxmlformats.org/markup-compatibility/2006 xmlns:c="Comment" mc:Ignorable="c"> <Canvas> <Button c:Width="100" Height="50">Hello</Button> </Canvas> </Window>
看見了Width前面的c前綴嗎?它的做用就是註釋掉Width屬性。是否是感受比標記註釋的方法簡單。並且這個c前面不但能夠應用在屬性上,也能夠直接應用在實例上,以下:
<Window xmlns=http://schemas.microsoft.com/winfx/2006/xaml/presentation xmlns:mc=http://schemas.openxmlformats.org/markup-compatibility/2006 xmlns:c="Comment" mc:Ignorable="c"> <Canvas> <c:Button Width="100" Height="50">Hello</c:Button> </Canvas> </Window>
上面的代碼就所有註釋掉了Button實例。固然,這種方法不建議在最後的發佈XAML文檔中出現。只適合在XAML文檔的開發過程當中使用。
XAML標記的向後兼容性
XAML支持XAML文檔的向前和向後兼容性。爲了幫助說明XAML標記的向後兼容性,咱們看一個自定義的View類,其中定義了一個Color類型的顏色屬性Color_Prop。
public class CLYLView { Color _color; public Color Color_Prop { get { return _color; } set { _color = value; } } }
很簡單,在XAML中,咱們能夠以下使用這個CLYLView類:
<CLYLView Color=」Red」 xmlns=」… assembly-V1-uri…」>
注意其中的xmlns=」… assembly-V1-uri…」,這就是一個所謂的XmlnsCompatibleWith屬性。經過它咱們指定了包含CLYLView的特定Assembly。
如今,咱們向V2版本的CLYLView添加了一個Content屬性。以下所示:
public class CLYLView { Color _color; Content _content; public Color Color_Prop { get { return _color; } set { _color = value; } } public Content Content_Prop { get { return _content; } set { _content = value; } }
}
如今咱們能夠這樣使用V2版本的CLYLView實例:
<CLYLView Color=」Red」 Content=」Unknown」 xmlns=」... assembly-v2-uri…」/>
可是,咱們仍然但願在V2版本的CLYLView支持V1版本。知足這種需求,咱們能夠用XmlnsCompatableWith聲明一個新的Assembly與老的Assembly兼容。XAML加載器看到了XmlnsCompatableWith屬性,就會把默認地把全部對V1的引用處理爲V2的引用。
向後兼容最大的一個好處就是:當咱們只有新版的Assembly時,全部對老版Assembly的引用仍然是可讀的,不會出現任何的錯誤。
前一個Post當中,咱們簡單介紹了XAML的向後兼容性,以及利用標記兼容性實現註釋的功能。如今,咱們接着討論XAML的向前兼容性問題。
一樣地,咱們用一個簡單的例子來幫助說明XAML的向前兼容性。假設有一個自定義的CLYLButton,實現了一個Light屬性。在V1版本它的默認屬性值是Blue(藍光)。在V2版本中支持屬性值Green(綠光)。假設咱們在程序中利用Light屬性實現了綠光效果。可是,若是剛好目標機器上的V2版本意外地被替換爲了V1版本。此時,程序的行爲應該怎麼樣呢?崩潰,不,咱們但願它在沒有V2的狀況下能利用V1版本的默認值實現藍光效果。如何實現且看XAML標記的向前兼容性。向前兼容性表示經過標記兼容性名字空間的Ignorable屬性標識元素、屬性和類,使它們能夠動態的支持向前版本。
<CLYLButton V2:Light="Green" xmlns="...assembly-v1-uri..." xmlns:V2="...assembly-V2-uri..." xmlns:mc=http://schemas.micrsoft.com/winfx/2006/markup-compatibility mc:Ignorable="V2" />
這就利用了標記兼容性名字空間的Ignorable屬性。mc:Ignorable=」V2」表示全部用V2前綴關聯的名字空間中元素或者屬性都是能夠忽略的。若是如今只有V1版本的CLYLButton,上面的代碼就被XAML加載器解釋爲:
<CLYLButton Light=」Blue」 xmlns=」… assembly-V1-uri …」/>
若是如今有V2版本的CLYLButton,上面的代碼將被XAML加載器解釋爲:
<CLYLButton Light=」Green」 xmlns=」… assembly-V2-uri …」/>
XMAL標記兼容性除了可應用在屬性上,還能夠應用在元素之上。仍然經過例子進行說明,定義以下的一個類:
[ContentProperty("Buttons")] public class CElement { List<CLYLButton> _buttons = new List<CLYLButton>(); public List<CLYLButton> Buttons { get { return _buttons; } }
關於ContentProperty的用法能夠參考MSDN文檔ContentPropertyAttribute Class
一樣,咱們能夠以下編寫XAML代碼,使其能夠同時兼容兩個版本的CElement。
<CElement mc:Ignorable="V2" xmln="...assembly-v1-uri..." xmlns:V2="...assembly-V2-uri..." xmlns:mc="http://schemas.micrsoft.com/winfx/2006/markup-compatibility"> <CLYLButton Light="Blue" /> <V2:CLYLButton Light="Green"/> </CElement>
這樣,若是加載器有V2版本,則Green屬性值生效。若是沒有則被忽略。相似地,咱們還能夠徹底自動地處理名字空間的類:
<CElement mc:Ignorable="v2" xmln="...assembly-v1-uri..." xmlns:V2="...assembly-v2-uri..." xmlns:mc="http://schemas.micrsoft.com/winfx/2006/markup-compatibility"> <V2:Favor/> </CElement>
加載時,若是沒有V2版本存在,Favor類實例一樣將被忽略。
在Markup Compatibility中,除了有前面介紹的Comment、Ignorable屬性修飾外,另外一個有趣的就是AlternateContent。利用AlternateContent,咱們能方便的實現可選內容。好比,咱們的程序使用了V2版本Assembly的CLYLButton類,可是,若是沒有找到這個Assembly,那麼它對應的內容自動用另外一個指定版本V1替換,而不是兼容性體現的忽略。看下面的例子:
<CElement mc:Ignorable="v2" xmln="...assembly-v1-uri..." xmlns:v2="...assembly-v2-uri..." xmlns:mc="http://schemas.micrsoft.com/winfx/2006/markup-compatibility"> <mc:AlternateContent> <mc:Choice Requires="V2"> <CLYLButton Light="Green" Shape="Dog" /> <V2:Favor/> </mc:Choice> <mc:Fallback> <CLYLButton Light="Blue"/> </mc:Fallback> </mc:AlternateContent> </CElement>
這一段XAML代碼在有V1版本的Assembly時將被視爲:
<CElement xmln="...assembly-v1-uri..."> <CLYLButton Light="Blue"/> </CElement>
若是有V2版本的Assembly,編譯的結果以下:
<CElement xmln="...assembly-v1-uri..."> <CLYLButton Light="Green"/> <Favor/> </CElement>
這部分的內容來自於即將出版的新書《WPF Unleashed》的第三章樣章。關於什麼是邏輯樹,咱們先看下面的一個僞XAML代碼的例子:
<Window ......> <StackPanel> <Label>LabelText</Lable> </StackPanel> </Window>
在這樣一個簡單UI中,Window是一個根結點,它有一個子結點StackPanel。而StackPanel有一個子結點Label。注意Label下還有一個子結點string(LabelText),它同時也是一個葉子結點。這就構成了窗口的一個邏輯樹。邏輯樹始終存在於WPF的UI中,無論UI是用XAML編寫仍是用代碼編寫。WPF的每一個方面(屬性、事件、資源等等)都是依賴於邏輯樹的。
視覺樹基本上是邏輯樹的一種擴展。邏輯樹的每一個結點都被分解爲它們的核心視覺組件。邏輯樹的結點對咱們而言基本是一個黑盒。而視覺樹不一樣,它暴露了視覺的實現細節。下面是Visual Tree結構就表示了上面四行XAML代碼的視覺樹結構:
並非全部的邏輯樹結點均可以擴展爲視覺樹結點。只有從System.Windows.Media.Visual和System.Windows.Media.Visual3D繼承的元素才能被視覺樹包含。其餘的元素不能包含是由於它們自己沒有本身的提交(Rendering)行爲。
在Windows Vista SDK Tools當中的XamlPad提供查看Visual Tree的功能。須要注意的是XamlPad目前只能查看以Page爲根元素,而且去掉了SizeToContent屬性的XAML文檔。以下圖所示:
注意圖中工具欄特別標記的地方。咱們能夠看到Visual Tree確實比較複雜,其中還包含有不少的不可見元素,好比ContentPresenter。Visual Tree雖然複雜,可是在通常狀況下,咱們不須要過多地關注它。咱們在從根本上改變控件的風格、外觀時,須要注意Visual Tree的使用,由於在這種狀況下咱們一般會改變控件的視覺邏輯。
WPF中還提供了遍歷邏輯樹和視覺樹的輔助類:System.Windows.LogicalTreeHelper和System.Windows.Media.VisualTreeHelper。注意遍歷的位置,邏輯樹能夠在類的構造函數中遍歷。可是,視覺樹必須在通過至少一次的佈局後才能造成。因此它不能在構造函數遍歷。一般是在OnContentRendered進行,這個函數爲在佈局發生後被調用。
其實每一個Tree結點元素自己也包含了遍歷的方法。好比,Visual類包含了三個保護成員方法VisualParent、VisualChildrenCount、GetVisualChild。經過它們能夠訪問Visual的父元素和子元素。而對於FrameworkElement,它一般定義了一個公共的Parent屬性表示其邏輯父元素。特定的FrameworkElement子類用不一樣的方式暴露了它的邏輯子元素。好比部分子元素是Children Collection,有是有時Content屬性,Content屬性強制元素只能有一個邏輯子元素。
WPF引入了一種新的屬性:Dependency屬性。Dependency屬性的應用貫串在整個WPF當中。Dependency屬性根據多個提供對象來決定它的值。而且是及時更新的。提供對象能夠是動畫,不斷地改變它的值。也能夠是父元素,它的屬性值被繼承到子元素。毫無疑問,Dependency屬性最大的特色就是內建的變化通知功能。提供Dependency屬性功能主要是爲了直接從聲明標記提供豐富的功能。WPF聲明的友好設計的關鍵是大量的使用屬性。若是沒有Dependency屬性,咱們將不得不編寫大量的代碼。關於WPF的Dependency屬性,咱們將重點研究以下三個方面:
1、變化通知功能:屬性的值被改變後,通知界面進行更新。
2、屬性值的繼承功能:子元素將繼承父元素中對應屬性名的值。
3、支持多個提供對象:咱們能夠經過多種方式來設置Dependency屬性的值。
先看如何實現一個標準的Dependency屬性。
public class Button : ButtonBase
{
// The dependency property
public static readonly DependencyProperty IsDefaultProperty;
static Button()
{
// Register the property
Button.IsDefaultProperty = DependencyProperty.Register(「IsDefault」, typeof(bool), typeof(Button),
new FrameworkPropertyMetadata(false,
new PropertyChangedCallback(OnIsDefaultChanged)));
…
}
// A .NET property wrapper (optional)
public bool IsDefault
{
get { return (bool)GetValue(Button.IsDefaultProperty); }
set { SetValue(Button.IsDefaultProperty, value); }
}
// A property changed callback (optional)
private static void OnIsDefaultChanged(
DependencyObject o, DependencyPropertyChangedEventArgs e) { … }
…
}
在上面的實現代碼中,System.Windows.DependencyProperty類表示的靜態字段IsDefaultProperty纔是真正的Dependency屬性。爲了方便,全部的Dependency屬性都是公有、靜態的,而且還有屬性後綴。一般建立Dependency屬性可用靜態方法DependencyProperty.Register。參數的屬性名稱、類型、使用這個屬性的類。而且能夠根據重載的方法提供其餘的通知事件處理和默認值等等。這些相關的信息可參考FrameworkPropertyMetadata類的多個重載構造函數。
最後,實現了一個.NET屬性,其中調用了從System.Windows.DependencyObject繼承的GetValue、SetValue方法。全部具備Dependency屬性的類都確定會繼承這個類。GetValue方法返回最後一次設置的屬性值,若是尚未調用一次SetValue,返回的將是Register方法所註冊的默認值。並且,這種.NET樣式的屬性封裝是可選的,由於GetValue/SetValue方法自己是公有的。咱們能夠直接調用這兩個函數,可是這樣的封裝使代碼更可讀。
雖然這種實現方式比較麻煩,可是,因爲GetValue/SetValue方法使用了一種高效的小型存儲系統,以及真正的Dependency屬性是靜態字段(不是實例字段),Dependency屬性的這種實現能夠大大的減小每一個屬性實例的存儲空間。想象一下,若是Button有50個屬性,而且所有是非靜態的實例字段,那麼每一個Button實例都含有這樣50個屬性的空間,這就存在很大的空間浪費。除了節省空間,Dependency屬性的實現還集中、而且標準化了屬性的線程訪問檢查、提示元素從新提交等等。
在前一個Post中,曾提到將要重點研究Dependency屬性的三個方面:變化通知;屬性值的繼承;支持多個提供對象。下面,我將分別就這三個內容進行簡單地說明。
【變化通知】
在任什麼時候候,只要Dependency屬性的值發生了變化,WPF能夠自動地根據屬性的元數據觸發不一樣的行爲。前面提到過:Dependency屬性最大的特色就是內建的變化通知功能。這種內建變化通知所提供的最值得注意的就是屬性觸發器(Property Trigger),就是它使用咱們不須要編寫任何的程序代碼就能在屬性變化使執行自定義行爲。請看下面XAML編碼的一個屬性觸發器例子:
<Trigger Property=」IsMouseOver」 Value=」True」>
<Setter Property=」Foreground」 Value=」Blue」/>
</Trigger>
它的功能就是在屬性值IsMouseOver變爲True的時,將屬性Foreground的值設置爲Blue。並且,它會在IsMouseOver變爲False時自動將Foreground的值設置爲原來的值。就是這樣簡單的三行代碼完成了咱們曾經須要多個函數、變量才能實現的功能。
使用屬性觸發器時須要注意:觸發器默認適用於每一個類對象。 並且,在WPF 3.0中因爲人爲的限制,Property Trigger將不能應用在單獨的元素。只能應用在某個Style對象之中。所以,若是想在某個單獨對象上實現Property Trigger。必須用以下的XAML進行封裝:
<Button MinWidth=」75」 Margin=」10」>
<Button.Style>
<Style TargetType=」{x:Type Button}」>
<Style.Triggers>
<Trigger Property=」IsMouseOver」 Value=」True」>
<Setter Property=」Foreground」 Value=」Blue」/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
OK
</Button>
【屬性值繼承】
屬性值繼承是指在設置邏輯樹某個結點元素的屬性後,它的全部之結點都繼承這個屬性值(固然,前提是子元素必須支持這個屬性)。咱們仍然利用閒話WPF之八中的一個例子進行說明:
<Window FontSize=」30」>
<StackPanel>
<Label>LabelText</Lable>
</StackPanel>
</Window>
咱們修改了Window是FontSize屬性爲30。經過實際觀察將發現它的子元素Label的FontSize也變爲了30。注意這裏的StackPanel是一個容器元素,它自己並不支持FontSize屬性。
如今咱們給上面的Window添加一個狀態欄。XAML代碼以下:
<Window ......>
<StackPanel>
<Label>LabelText</Lable>
<StatusBar>This is a Statusbar</StatusBar>
</StackPanel>
</Window>
這時你會發現:雖然StatusBar支持這個FontSize這個屬性,它也是Window的子元素,可是它的字體大小卻沒有變化。爲何呢?由於並非全部的元素都支持屬性值繼承。還存在以下兩種例外的狀況:
1、部分Dependency屬性在用Register註冊時能夠指定Inherits爲不可繼承。
2、若是有其餘更高優先級方法設置了其餘的值。(關於優先級的介紹且看下面分解。)
部分控件如StatusBar、Menu和Tooptip內部設置它們的字體屬性值以匹配當前系統的設置。這樣用戶經過控制面板能夠修改它們的外觀。這種方法存在一個問題:StatusBar等截獲了從父元素繼承來的屬性,而且不影響其子元素。好比,若是咱們在StatusBar中添加了一個Button。這個Button的字體屬性會由於StatusBar的截斷沒沒有改變,將保留其默認值。
附加說明:屬性值繼承的最初設計只適用於元素Tree,如今已經進行多個方面的擴展。好比,值能夠傳遞下級看起來像Children,但在邏輯或者視覺Tree中並非Children的某些元素。這些假裝的子元素能夠是觸發器、屬性的值,只要它是從Freezable繼承的對象。對於這些內容沒有很好的文檔說明。咱們只須要能使用就行沒必要過多關心。
在前一個Post中,重點說明了Dependency屬性的變化通知和屬性值的繼承兩個方面,下面咱們再看看Dependency屬性所支持的多個提供對象。
【支持多個提供對象】
WPT提供了獨立的、強大的機制來設置Dependency屬性的值。因爲支持多個提供對象,若是沒有很好的機制來處理這些提供對象,那麼Dependency屬性的值將是不可預測的、系統也將變得混亂。Dependency屬性的值取決於這些提供對象,它們以必定的順序和優先級排列。
下圖說明了WPF在計算Dependency屬性最終值的5個步驟:
基本的計算過程是:
肯定基礎值====>計算值(若是是表達式)===>應用動畫====>強制值===>值驗證
1、肯定基礎值
多數的提供對象都會影響基礎值,下面以優先級順序列出了能夠設置多數Dependency屬性值的八個提供對象:
1、Local Value 2、Style Triggers 3、Template Triggers 4、Style Setters 5、Theme Style Triggers 6、Theme Style Setters 7、Property Value Inheritance 8、Default Value
Local Value技術上表示任何對DependencyObject.SetValue的調用。它的最多見形式就是在XAML或者代碼中的屬性賦值。由於咱們一般用.NET的屬性方式封裝了對GetValue/SetValue的調用。Regitser註冊時指定的默認值位於處理過程的最後一步。關於其它的提供對象,如Style、Template將在之後介紹,敬請關注後續內容。
2、計算值
若是第一步獲得的是一個表達式,WPF將計算表達式以獲得一個具體的值。在3.0版本的WPF中,只有動態資源或者是數據綁定纔可能有表達式。也許未來版本的WPF會支持其它類型的表達式。
3、應用動畫
若是當前有一個或者多個動畫在運行,它們具備修改當前屬性值、或者徹底替代它的能力。所以,動畫的級別比其它屬性提供對象都高,甚至是Local Value,咱們必須記住這一點。
4、強制值
在處理完全部的提供對象後,WPF將最終的屬性值傳遞到CoerceValueCallback委派。若是Dependency屬性在註冊時提供了這樣的委派,那麼就應該根據自定義邏輯返回一個新的值。好比ProgressBar,當全部提供對象最後所提供的值超出了其定義的最大、最小值範圍時,ProgressBar將利用這個CoerceValueCallback委派限制在這個範圍以內。
5、值驗證
最後,前綴的強制值將傳遞給ValidateValueCallback委派,若是Dependency屬性註冊了這個委派。當值有效時委派必須返回True,不然返回False。返回False將拋出異常,終止整個進程。
附加說明:若是咱們不知道給定的Dependency屬性的值來源於何處,能夠調用靜態的DependencyPropertyHelper.GetValueSource方法。它做爲調試時的輔助工具,有時能給咱們提供幫助。方法會返回一個ValueSource結構。ValueSource結構中的屬性成員BaseValueSource、IsExpression、IsAnimated、IsCoerced分別表示了前面列出的八個提供對象的相應類型。注意:請不要在最後的發佈產品中使用這個方法,由於在未來版本的WPF中可能有不一樣的行爲。只應該將其做爲調試工具。
在前面,我用三篇短小的Post對Dependency屬性進行了說明。如今,咱們再繼續看一種特殊的Dependency屬性:Attached屬性。Attached屬性能夠很是高效地Attach到其餘的對象中。
咱們仍然用前面的一個簡單XAML代碼爲例:
<Window>
<StackPanel>
<Label>LabelText</Lable>
</StackPanel>
</Window>
如今,若是須要對StackPanel及其子元素設置字體大小,應該如何作呢?在Window元素中,它有一個屬性FontSize,能夠直接設置。可是,StackPanel本身自己並無FontSize這樣的屬性。這就該Attached屬性出場了。這裏咱們須要用定義在TextElement元素中的Attached屬性FontSize來設置StackPanel的字體。
<Window>
<StackPanel TextElement.FontSize=」30」>
<Label>LabelText</Lable>
</StackPanel>
</Window>
這樣,StackPanel的子元素就能經過屬性值繼承獲得新的FontSize屬性。對於這樣的XAML代碼,XAML編譯器或者解析器看到這種語法時,就要求TextElement(有時也稱爲Attached屬性提供者)有相應的靜態方法SetFontSize來設置對應的屬性值。所以,上面的Attached屬性設置代碼,能夠以下用C#實現:
StackPanel panel = new StackPanel();
TextElement.SetFontSize(panel, 30);
從這裏的代碼能夠看出,Attached屬性並不神祕。只是調用方法把元素和不相關的屬性關聯起來。而SetFontSize實現也比較簡單。它只是調用了Dependency屬性訪問函數所調用的DependencyObject.SetValue方法。注意調用的對象是傳入的DependencyObject,而不是當前的實例:
public static void SetFontSize(DependencyObject element, double value)
{
element.SetValue(TextElement.FontSizeProperty, value);
}
一樣地,Attached屬性也定義了對應的GetXXX函數。它調用的DependencyObject.GetValue方法:
public static double GetFontSize(DependencyObject element)
{
return (double)element.GetValue(TextElement.FontSizeProperty);
}
與普通的Dependency屬性同樣,這些GetXXX和SetXXX方法除了實現對GetValue和SetValue的調用,不能作任何其餘額外的工做。
其實,在WPF應用中,Attached屬性更多的用來控制UI的佈局。除了前面的StackPanel,還有Grid等等。
補充說明:上面的代碼還有一個問題須要說明。咱們設置StackPanel的字體屬性時用的是TextElement元素。爲何不用其餘的元素Control、Button呢?
這個問題的關鍵之處在於Dependency屬性的註冊方法。我曾在Dependency屬性[1]作過簡單的說明。咱們看看Element的FontSizeProperty屬性的註冊代碼:
TextElement.FontSizeProperty = DependencyProperty.RegisterAttached(
「FontSize」, typeof(double), typeof(TextElement), new FrameworkPropertyMetadata(
SystemFonts.MessageFontSize, FrameworkPropertyMetadataOptions.Inherits |
FrameworkPropertyMetadataOptions.AffectsRender |
FrameworkPropertyMetadataOptions.AffectsMeasure),
new ValidateValueCallback(TextElement.IsValidFontSize));
這裏與咱們前面的IsDefault屬性相似,只是RisterAttached方法優化了Attached屬性須要的屬性元數據的處理過程。
另外一方面,Control的FontSize屬性是在TextElement元素已經註冊的屬性之上調用AddOwner方法,獲取一個徹底相同的實例引用:
Control.FontSizeProperty = TextElement.FontSizeProperty.AddOwner(
typeof(Control), new FrameworkPropertyMetadata(SystemFonts.MessageFontSize,
FrameworkPropertyMetadataOptions.Inherits));
因此,在實現Attached屬性時咱們使用的是TextElement,而不是Control等等。
資源是保存在可執行文件中的一種不可執行數據。經過資源咱們能夠包含圖像、字符串等等幾乎是任意類型的數據。如此重要的功能,.NET Framework固然也是支持的,其中內建有資源建立、定位、打包和部署的工具。在.NET中能夠建立.resx和.resources文件。其中.resx由XML項組成。.resx只是一種中間格式,不能被應用程序直接使用,它必須用工具轉換爲.resource格式。
在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資源。顯而易見,Dynamic資源的運行效率將比Static資源低。
3、無論是Static資源仍是Dynamic資源,全部的資源都須要設置Key屬性:x:Key=」KeyName」。由於WPF中的資源沒有資源ID,須要經過資源Key來標識以方便之後訪問資源。範圍資源時咱們根據資源的類型使用StaticResource或者DynamicResource標記擴展。
好了,對WPF中的資源全部瞭解後,咱們看一些簡單的例子:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel>
<StackPanel.Resources>
<SolidColorBrush x:Key="MyBrush" Color="gold"/>
</StackPanel.Resources>
<TextBlock Foreground="{StaticResource MyBrush}" Text="Text"/>
</StackPanel>
</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提供的一個真正的優勢。除了能夠用在傳統的綁定環境中,數據綁定已經被擴展應用到控件屬性上。學習應用數據綁定,也能真正的體現XAML的好處。到底什麼是數據綁定呢?也許你從字面上已經理解的很不錯了。經過數據綁定,咱們在應用程序UI和程序邏輯之間創建了一種聯繫。正常創建綁定後,在數據的值發生改變後,綁定到數據的元素將自動更新、體現出數據的變化。
一樣,咱們先看幾個相關的知識點:
1、DataContext屬性。設置DataContext屬性,其實就是指定數據上下文。那麼數據上下文又是什麼呢?又是一個新的概念:數據上下文容許元素從它的父元素繼承數據綁定的數據源。很簡單,在某個元素的DataContext中指定的值,那麼在這個元素的子元素也可使用。注意,若是咱們修改了FrameworkElement或者FrameworkContentElement元素的DataContext屬性,那麼元素將再也不繼承DataContext值。也就是說新設置的屬性值將覆蓋父元素的設置。如何設置DataContext屬性,稍後說明。
2、數據源的種類。也許,WPF提供的數據綁定只是實現了一項普通的功能而已,可是,WPF中所支持的多種數據源使得它的數據綁定功能將更增強大。如今,WPF支持以下四種綁定源:
(1)、任意的CLR對象:數據源能夠是CLR對象的屬性、子屬性以及Indexers。對於這種類型的綁定源,WPF採用兩種方式來獲取屬性值:A)、反射(Reflection);B)、CustomTypeDescriptor,若是對象實現了ICustomTypeDescriptor,綁定將使用這個接口來獲取屬性值。
(2)、XML結點:數據源能夠是XML文件片段。也能夠是XMLDataProvider提供的整個XML文件。
(3)、ADO.NET數據表。我對ADO.NET的瞭解不夠,在此不作過多評論。
(4)、Dependency對象。綁定源能夠是其它DependencyObject的DependencyProperty屬性。
3、數據綁定的方式:(1)、OneWay,單一方向的綁定,只有在數據源發生變化後纔會更新綁定目標。(2)、TwoWay,雙向綁定,綁定的兩端任何一端發生變化,都將通知另外一端。(3)、OneTime,只綁定一次。綁定完成後任何一端的變化都不會通知對方。
在上面的第二點我介紹了數據源的種類,注意這裏的概念和下面要說明的指定數據源的方式的區別。目前,指定數據源有三種方式,咱們能夠經過任何一種方式來指定上述的任何一種數據源:
(1)、經過Source標記。咱們能夠在使用Binding使用Source標記顯式指定數據源。
(2)、經過ElementName標記。這個ElementName指定了一個已知的對象名稱,將使用它做爲綁定數據源。
(3)、經過RelativeRource標記。這個標記將在後面說明ControlTemplate和Style時再進行說明。
如今咱們說明了不少和數據源相關的內容。可是再綁定的時候,咱們還須要指定綁定對象的屬性名稱。因此WPT提供了一個Path標記。它被用來指定數據源的屬性。也便是數據源將在數據源對象的Path所指定的屬性上尋找屬性值。
如今,理論的東西講了一堆,我將在後面用一些簡單的例子進行說明。
在上一個Post當中,我敘述了WPF中的數據綁定相關的一堆理論知識。如今,咱們將對其中的某些方面經過實例作進一步的分析。
在介紹WPF數據綁定源的種類時,第一種就是任意的CLR對象。這裏須要注意的是WPF雖然支持任意的CLR對象,可是一個普通的CLR對象類卻不行。咱們還須要在CLR對象類上實現一種變化通知機制。
WPF把這種通知機制封裝在了INotifyPropertyChanged接口當中。咱們的CLR對象類只要實現了這個接口,它就具備了通知客戶的能力,一般是在屬性改變後通知綁定的目標。
下面是一個簡單的例子,實現了一個支持通知功能的Camera類:
using System;
using System.ComponentModel;
using System.Windows.Media.Media3D;
namespace LYLTEST
{
public class Camera : INotifyPropertyChanged
{
private PerspectiveCamera m_Camera;
public event PropertyChangedEventHandler PropertyChanged;
public Camera()
{
m_Camera = new PerspectiveCamera();
}
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
public PerspectiveCamera CameraProp
{
get { return m_Camera; }
set
{
if (value != m_Camera)
{
this.m_Camera = value;
NotifyPropertyChanged("CameraProp");
}
}
}
}
}
這一段代碼很簡單,首先引入類中使用的INotifyPropertyChanged和PerspectiveCamera須要的名字空間。這裏與普通CLR類的區別在於首先有一個公有的PropertyChangedEventHandler事件類型。而後咱們在.NET屬性包裝CameraProp判斷屬性是否發生了變化,若是是,則用當前是屬性名稱字符串「CameraProp」調用另外一個私有函數NotifyPropertyChanged。由它根據屬性的名稱構造一個PropertyChangedEventArgs對象,並完成對PropertyChanged的調用。它纔是屬性變化時真正應該調用的一個通知事件。
最後一點,若是咱們須要通知因此的屬性都發生了變化,則將上面的屬性字符串「CameraProp」用參數NULL替代便可。
在本系列的之十三中簡單介紹了WPF中資源的資源。可是,沒有給出任何具體的實例,在這個Post中將給出一個動態資源的例子,也算是響應daxian110的請求。並適當的擴展在前一個Post當中沒有涉及的知識。
咱們先看一個例子程序:
<Window x:Class="WindowsApplication1.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WindowsApplication1" Height="150" Width="100" Loaded="OnLoaded" > <Canvas> <Button Click="OnClick" Canvas.Left="10" Canvas.Top="20" Width="80" Height="30" Content="{DynamicResource TestRes1}"/> <Button Canvas.Left="10" Canvas.Top="60" Width="80" Height="30" Content="{DynamicResource TestRes2}"/> </Canvas> </Window>
程序很簡單,在窗口中添加了兩個按鈕,咱們須要關注的是其中對Content屬性。這個屬性的做用就是設置按鈕的內容。爲何這裏的名稱不是Text,而是Content?如此命名的緣由和WPF中控件一個很是重要的概念有關:WPF中幾乎任何的控件(也就是Element)均可以做爲一個容器存在。也就是說咱們在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中的資源。此次我將嘗試從另一個角度來分析WPF中的資源:資源編譯行爲,以及如何根據應用程序的須要選擇適當的類型。
首先創建一個默認的WPF工程,而後向工程中添加一個ICON資源。在添加資源後,咱們能夠選擇資源的類型,以下圖所示:
從圖中的下拉列表咱們能夠看到資源所支持的各類類型。主要支持的編譯行爲是Resource和Content。若是選擇爲Resource,再用文本方式打開C#工程文件(*.csproj文件),其中咱們爲發現以下的內容:
<ItemGroup>
<Resource Include="WTL2007.ico" />
</ItemGroup>
若是選擇爲Content,看到的資源項內容應該是:
<ItemGroup>
<Content Include="WTL2007.ico" />
</ItemGroup>
那麼,這二者之間有什麼區別呢?咱們先看Resource類型。若是某個文件在工程文本中被標識爲Resource,這個文件將被嵌入到應用程序所在的Assembly。若是咱們設置了Localizable元數據,對應的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=」WTL2007.ico」/>
下面有幾個比較好的建議,能夠幫助咱們選擇資源的編譯Action。對於下面的這些需求,應該選擇Resource:
1、文件是本地化文件
2、應用程序部署後再也不但願文件被修改
3、若是不但願對文件進行單獨的部署或者管理,移動應用程序時沒必要擔憂資源的位置
對於下面的一些資源文件需求,咱們應該選擇Content:
1、文件不是本地化文件
2、但願文件在部署後能夠被替換
3、但願文件能夠被下載更新等等(注意不能是包含在應用程序Assembly)。
4、某些不能被設置爲Resource類型的文件,不然WPF不能識別。
在前一個Post當中,我從資源編譯行爲的角度討論了WPF中的資源。可是,無論是Resource仍是Content都是在編譯時聲明資源。若是咱們打破這個限制,不但願指定徹底確認的資源地址。WPF提供了一種相似IE地址定位的抽象,它根據應用程序部署的位置決議。
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>
【傳遞事件】
WPF在.NET簡單事件通知之上添加了不少基礎結構。傳遞事件的設計使得事件能夠與元素樹一塊兒很好的工做。事件發生後,能夠在視覺樹和邏輯樹自動地進行上下傳遞,咱們不須要添加任何額外的代碼。
傳遞事件使得咱們不須要過多關注於視覺樹,這樣封裝對於咱們理解WPF的元素合成很是重要。好比,咱們點擊一個按鈕的事件,在點擊的時候咱們實際上點擊的是一個ButtonChrome或者TextBlock,也就是說咱們點擊的是Button的內容元素。正是由於事件能夠沿視覺樹傳遞,Button才發現這個事件,而且能夠處理。所以,咱們能夠給Button的Content當中添加任意的元素,而不會對事件有任何的影響。若是沒有這樣的事件傳遞,咱們點擊Button內的元素時,必須手動編寫代碼觸發Button點擊事件。
傳遞事件的的實現和行爲與Dependency屬性相似。一樣,咱們看看如何實現簡單的傳遞事件。多數時候,傳遞事件並不比普通的.NET事件難。與Dependency屬性同樣,.NET語言(除了XAML)自己並不明白傳遞目標。這些支持都是基於WPF API。
public class Button { // 傳遞的事件 public static readonly RoutedEvent ClickEvent;
static Button() { // 註冊事件 Button.DoubleClickEvent = EventManager.RegisterRoutedEvent(「Click」, RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(Button)); … } // .NET事件保證 (可選的) public event RoutedEventHandler Click { add { AddHandler(Button.ClickEvent, value); } remove { RemoveHandler(Button.ClickEvent, value); } }
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) { … // 激發事件 RaiseEvent(new RoutedEventArgs(Button.ClickEvent, this)); … } … }
從上面的實現能夠看出,事件與Dependency屬性有不少類似之處。也是定義一個靜態的RoutedEvent成員,一樣在靜態構造函數裏註冊事件。爲了方便,也包裝了一個普通的.NET事件。這裏的AddHandler/RemoveHandler不是從DependencyObject派生,而是更高一級的基類System.Windows.UIElement。這兩個方法爲相應的事件添加/刪除一個委派。在OnMouseLeftButtonDown中,咱們構造一個事件參數,傳入事件源對象this,而後調用RaiseEvent函數。
【事件策略和處理函數】
註冊WPF事件時,咱們須要爲傳遞事件選擇一種策略,這個策略指定了事件在元素樹中傳遞的方式。WPF支持這樣三種策略:
Tunneling:事件首先在根元素激發,而後到達樹下的每一個元素直到源元素(或者有處理函數處理這個事件終止了傳遞)。
Bubbling:事件首先在源元素激發,而後向上直到根元素(或者有處理函數處理這個事件終止了傳遞。
Direct:事件只在源元素激發。這與普通的.NET事件同樣,除了參與事件觸發器。
在上面的例子中,咱們註冊的事件策略就是Bubbling。
傳遞事件的處理函數的參數與普通.NET事件同樣。第一個參數System.Object表示處理函數依附的元素。第二個的System.EventArgs派生類,提供了以下四個有用的屬性:
Source:邏輯樹中激發事件的原始元素。
OriginalSource:視覺樹中激發事件的原始元素。
Handled:布爾值,表示事件是否被處理。
RoutedEvent:實際的傳遞事件對象(好比Button.ClickEvent)。這個對於相同的處理函數處理多個傳遞事件時很是有用,能夠用來區別傳遞事件。
Source和OriginalSource表明了邏輯樹和視覺樹對象。這有利於咱們進行一些低級控制,可是對於有的事件,不須要區別它們,這兩個的值是相同的。
前一個Post當中介紹了WPF如何處理事件的傳遞過程。如何定義傳遞事件,而且對事件進行了分類。如今,咱們看看WPF究竟是如何處理Bubbling和Tunneling事件的。最後介紹了Attached事件。
在UIElement類,預約義了不少的傳遞事件,好比鍵盤、鼠標等等。其中大多數是Bubbling事件,其中不少的事件都還有一個對應的Tunneling事件。全部的Tunneling事件都是Preview前綴命名,它們都在對應的Bubbling事件以前激發。好比PreviewMouseMove這個Tunneling事件是在MouseMove這個Bubbling事件以前激發的。
Tunneling事件的好處就是能夠有機會改變或者取消後面的Bubbling事件。WPF內建的響應事件只會對Bubbling事件進行響應,固然,前提了Bubbling和Tunneling同時定義。這種行爲有什麼好處呢?看下面的一個例子:好比,咱們想實現一種特殊的編輯框,只容許輸入一些特定的字符。之前的實現方法在處理編輯框的KeyDown或者編輯框的WM_CHAR事件,而後判斷新輸入的字符是否知足條件,若是不知足,咱們再把編輯框的值設置爲原來的值。這種實現技術會有字符的一個回退過程。而在WPF中,實現方法不一樣,直接在PrevewKeyDown等Tunneling事件中處理,若是是不須要的字符,把事件設置爲已經處理過。這樣這個事件就不會進入到後面的Bubbling事件KeyDown中,WPF也根本不會顯式這個字符。這種方法的效果將比以前的回退處理好不少。
雖然咱們能夠經過RoutedEventArgs參數的Handled屬性爲True來終止事件的傳遞。可是,有時候咱們須要某個事件始終被接受處理,這能夠經過程序代碼實現。使用重載的AddHanlder方法。好比,咱們給窗口添加一個鼠標右鍵的處理方法(其中MRBD_Handler是類的一個事件方法):
public AboutDialog() { InitializeComponent(); this.AddHandler(Window.MouseRightButtonDownEvent, new MouseButtonEventHandler(MRBD_Handler), true); }
這樣,任何條件下,MRBD_Handler均可以接收到窗口的鼠標右鍵事件。即便鼠標右鍵是點擊在窗口中的某個子控件之上。
【Attached事件】
與Attached屬性相似,WPF的Element在事件沒有定義的狀況下也支持Tunneling或者Bubbling事件。好比,咱們能夠在一個簡單的窗口程序中這樣指定事件函數:
<Window xmlns=」http://schemas.microsoft.com/winfx/2006/xaml/presentation」 xmlns:x=」http://schemas.microsoft.com/winfx/2006/xaml」 x:Class=」Window1」 Button.Click=」Button_Click」 <Button Text="TestButton" Width="50" Height="30"> </Window>
例子中,由於Window自己沒有定義Click事件,因此咱們必須指定Click事件屬性的名稱前綴,也就是定義事件的類名。通過這樣的定義後,點擊在Window中的TestButton,也會激發屬性聲明的Click事件,調用對應的Button_Click方法。
爲何這樣的定義能夠經過呢?首先編譯時,XAML會看到Button類確實定義了一個Click的.NET事件。在運行時,會直接調用AddHandler把這兩個事件依附到Window對應的類當中。因此上面用XAML屬性聲明的事件代碼與下面的程序代碼等效:
public Window1 { InitializeComponent(); this.AddHandler(Button.ClickEvent, new RoutedEventHandler(Button_Click)); }
最近比較忙些,好多天沒有寫WPF了。今天,咱們繼續回到前面的話題:WPF中的數據處理。前面講過,經過實現INotifyPropertyChanged,咱們能夠改變使任意的CLR對象支持WPF的綁定源。可是,INotifyPropertyChanged一般只應用在單個的類屬性上。在現實應用中,咱們還會遇到另一種狀況:咱們須要監視某一堆的數據是否發生變化。也就是說咱們綁定的數據源再也不是一個單獨數據對象。好比,綁定源是一個數據表時,咱們但願在表中任何一條數據發生變化就能獲得通知。(這裏暫不考慮WPF綁定對ADO.NET的支持。)
WPF提供了一個ObservableCollection類,它實現了一個暴露了INotifyPropertyChanged的數據集合。也就是說咱們不須要本身對每一個單獨的數據實現INotifyPropertyChanged結構。咱們先看看如何實現一個簡單的綁定數據集合。
namespace NSLYL { public class LYLDataObj { public LYLDataObj(string name, string description) { this.name = name; this.description = description; }
public string Name { get { return name; } set { name = value; } }
public string Description { get { return description; } set { description = value; } } private string name; private string description; }
public class LYLDataObjCol : ObservableCollection<LYLDataObj> { public LYLDataObjCol() { this.Add(new LYLDataObj("Microsot", "Operating System")); this.Add(new LYLDataObj("Google", "Search")); } } }
代碼很簡單,基本上就是這樣的一個模板。而後,咱們就能夠把LYLDataObjCol綁定到一個須要多項數據的Element之上,好比ListBox、ComboBox等等。
<ListBox ItemsSource="{StaticResource dataObj}" .../>
綁定以後,只要個人LYLDataObjCol對象發送了變化,ListBox、ComboBox的數據也會有對應的變化。
到如今,咱們已經知道在綁定的時候有兩種指定數據源的方式:1、DataContext,關於它咱們在這個Post有簡單介紹。2、直接用Binding類的Source屬性。那麼,咱們在使用的時候如何區別呢?首先,Source的優先級比DataContext高,只有Source不存在,或者在當前Source到不到須要的屬性時纔會查找DataContext。除此以外,這二者沒有真正的區別,只是建議使用Source,它能有助於咱們調試應用程序。由於經過它能夠明確的獲得Source的信息。而DataContext支持一種繼承。能夠在父Element指定Source源。這同時也成爲了DataContext的一個優勢:若是多個Element須要綁定同一個Source源,那麼咱們只須要在一個地方指定DataContext,就能夠在其子Element使用。
Style是一種修改屬性值是方法。咱們能夠將其理解爲對屬性值的批處理。對批處理你們應該不會感到默認。對,經過Style咱們能夠批量修改屬性的值。先從一個簡單的Style例子開始:
<Window x:Class="Viewer3D.WindowSettins" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Viewer3D Settings" > <Window.Resources> <Style TargetType="CheckBox"> <Setter Property="Height" Value="20"/> <Setter Property="Width" Value="50"/> <EventSetter Event="Checked" Handler="Checked_Click"/> <Setter Property="VerticalAlignment" Value="Center"/> </Style> </Window.Resources> </Window>
第一感受你可能會奇怪,爲何Style在資源裏呢?我我的直接將理解爲「批處理」的緣故。所以Style是修改多個對象的屬性值,它不從屬於單獨的元素對象。另外一個疑惑的問題是Style沒有設置x:Key屬性。這是一個很是關鍵的設置。若是咱們設置了Style的x:Key屬性,至關於在當前Window是資源中定義了一個名稱爲x:Key設定值的Style對象。記住定義的效果至關於對象。若是沒有設置x;Key,那麼這個Style將對屬於這個Window中全部CheckBox生效。這就起到了批處理的效果。
首先設定的是Style的TargetType屬性,它表示咱們但願修改的目標類型。而後定義一個Setters的集合。每一個Setter都表示修改的一個屬性或者事件。Property設置屬性名稱,Value設置屬性值。Event設置事件名稱,Handler設置事件的響應函數名稱。只要你在Resource作了相似的定義,在此Window中所使用的任何ChekcBox都會默認這些屬性值。是否是很方便呢?咱們在此定義一次,能夠節省不少代碼。
也許你還會問:這樣的統一修改屬性太武斷、霸道了吧!也許是的。咱們只修改部分Element的屬性值,而但願對某些特殊的Element作特殊處理。這樣的需求WPF固然也是支持的。看看下面的代碼:
<Style BasedOn="{StaticResource {x:Type CheckBox}}" TargetType="CheckBox" x:Key="WiderCheckBox"> <Setter Property="Width" Value="70"/> </Style>
WPT經過BasedOn對這種特殊的Style提供了支持。很明顯,BasedOn的意思是咱們當前的Style基於在資源的CheckBox。這裏又看到了x;Key擴展標記。由於咱們須要的是一個特例,一個特殊的Style對象。爲了之後引用這個Style,咱們須要x:Key的標識做用。其它的代碼與前面相似。
定義後,引用這個特殊Style的CheckBox的代碼是這樣的:
<CheckBox Style="{StaticResource WiderCheckBox}">Win</CheckBox>
你已經看到,咱們在CheckBox中指定了Style屬性,並引用前面的StaticResource標記。
經過前面的介紹,咱們已經知道WPF支持用Style Setters修改控件的屬性值,以改變控件的外觀。咱們知道,WPF的任何控件都有視覺樹和邏輯樹。可是Style有它本身的侷限性:它只能修改控件已有樹型結構的屬性,不能修改控件的樹型層次結構自己。而在實際運用中,咱們經常須要對控件進行更高級的自定義。此時,能夠須要使用ControlTemplate才能實現。
在WPF中,ControlTemplate用來定義控件的外觀。咱們能夠爲控件定義新的ControlTemplate來實現控件結構和外觀的修改。一樣,咱們先看一個例子:
<Style TargetType="Button"> <Setter Property="OverridesDefaultStyle" Value="True"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="Button"> <Grid> <Ellipse Fill="{TemplateBinding Background}"/> <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style>
從例子代碼咱們能夠看出,ControlTemplate含有模板的語義。也就是說它影響的應該是多個控件。而這個功能剛好能夠利用Style實現。因此,在理解了Style以後,這樣的代碼應該不會感到陌生。首先把OverridesDefaultStyle設置爲True,表示這個控件不使用當前Themes的任何屬性。而後用Setters修改控件的Template屬性。咱們定義了一個新的ControlTemplate來設置新的值。
一樣地,ControlTemplate也使用TargetType屬性,其意義與Style的TargetType同樣。它的x:Key屬性也是如此。而後,由一個Grid來表示控件的視覺內容。其中的TemplateBinding與Binding相似,表示當前Ellipse的顯示顏色與Button的Background屬性保持同步。TemplateBinding能夠理解爲Binding在模板中的特例。而另外一個ContentPresenter與WPF的基本控件類型有關,一種是ContentControl,一個是ItemControl。在上面的例子中定義的是基於ContentControl的Button。因此使用ContentPresenter來表示內容的顯示。
WPF中每一個預約義的控件都有一個默認的模板,所以,在咱們學習自定義模板(也就是自定義控件)以前,能夠先熟悉瞭解WPF的默認模板。爲了便於查看模板的樹形結構層次,咱們能夠將模板輸出爲XML文件格式,這樣能有助於理解。
XmlWriterSettings settings = new XmlWriterSettings(); settings.Indent = true; settings.IndentChars = new string(' ', 4); settings.NewLineOnAttributes = true; StringBuilder strbuild = new StringBuilder(); XmlWriter xmlwrite = XmlWriter.Create(strbuild, settings); XamlWriter.Save(ctrl.Template, xmlwrite);
這裏的ctrl是一個實例化的Control類。而且Control須要已經顯示在屏幕上,不然Control.Template可能爲NULL。
前面關於ControlTempalte的Post當中,只說明瞭如何定義的外觀。若是對於很複雜的自定義控件,一般咱們還須要在ControlTemplate使用Resource。很顯然,Resource的目的是便於實現元素的重用。另外,咱們的自定義模板一般是在XAML中完成的,由於用代碼實現是很是煩瑣的。對於小的應用程序,這個ControlTemplate通常直接定義在XAML的根元素。對於大的應用程序,一般應該定義在專門的資源XAML文件中,根元素是ResourceDictionary。
無論定義在什麼地方,除了前面用Style定義外觀,以及用Resource實現元素重用外,ControlTemplate包括一個Trigger元素,它描述在控件屬性發生變化時控件的外觀如何變化。好比自定義Button時須要考慮鼠標在Button上移動時控件的外觀。Trigger元素也是可選的,好比文本標籤元素,它通常不包括Trigger。
在ControlTemplate中使用資源很簡單,與其餘元素中的資源同樣: <ControlTemplate x:Key="templateThermometer" TargetType="{x:Type ProgressBar}"> <ControlTemplate.Resources> <RadialGradientBrush x:Key="brushBowl" GradientOrigin="0.3 0.3"> <GradientStop Offset="0" Color="Pink" /> <GradientStop Offset="1" Color="Red" /> </RadialGradientBrush> </ControlTemplate.Resources> <!-- 忽略其餘相關內容--> </ControlTemplate>
接下來是Trigger的使用。利用Trigger對象,咱們能夠接收到屬性變化或者事件發生,並據此作出適當的響應。Trigger自己也是支持多種類型的,下面是一個屬性Trigger的例子:
<Style TargetType="ListBoxItem"> <Setter Property="Opacity" Value="0.5" /> <Style.Triggers> <Trigger Property="IsSelected" Value="True"> <Setter Property="Opacity" Value="1.0" /> <!--其餘的Setters-> </Trigger> </Style.Triggers> </Style>
這段代碼設置ListBoxItem的Opacity屬性的默認值爲0.5。可是,在IsSelected屬性爲True時,ListBoxItem的Opacity屬性值爲1。從上面的代碼還能夠看出,在知足一個條件後,能夠觸發多個行爲(定義多個Setters)。一樣地,上面的Triggers也是一個集合,也能夠添加多個Trigger。
注意上面的多個Trigger是相互獨立的,不會互相影響。另外一種狀況是須要知足多個條件時才觸發某種行爲。爲此,WPF提供了MultiTrigger以知足這種需求。好比:
<Style TargetType="{x:Type Button}"> <Style.Triggers> <MultiTrigger> <MultiTrigger.Conditions> <Condition Property="IsMouseOver" Value="True" /> <Condition Property="Content" Value="{x:Null}" /> </MultiTrigger.Conditions> <Setter Property="Background" Value="Yellow" /> </MultiTrigger> </Style.Triggers> </Style>
這就表示只有IsMouseOver爲True、Content爲NULL的時候纔將Background設置爲Yellow。
以上的Trigger都是基於元素屬性的。對於鼠標移動等事件的處理,WPF有專門的EventTrigger。但因EventTrigger多數時候是和Storyboard配合使用的。所以,我將在後面介紹動畫的時候詳細說明EventTrigger。
另外一方面,如今所討論的Trigger都是基於屬性的值或者事件的。WPF還支持另外一種Trigger:DataTrigger。顯然,這種數據Trigger用於數據發生變化時,也就是說觸發條件的屬性是綁定數據的。相似地,數據Trigger也支持多個條件:MultiDataTrigger。他們的基於用法和前面的Trigger相似。
在實際應用中,ControlTemplate是一個很是重要的功能。它幫助咱們快速實現很Cool的自定義控件。下面我以Windows Vista SDK中的例子ControlTemplateExamples爲基礎,簡單地分析ControlTemplate的使用。這個例子工程很是豐富,幾乎包含了全部的標準控件。因此,在實現自定義控件時,能夠先參考這樣進行適當的學習研究。
首先是App.xaml文件,這裏它把Application.StartupUri屬性設置爲Window1.xaml。而後把工程目錄Resource下全部的控件xaml文件都合成爲了應用程序範圍內的資源。
<Application.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="Resources\Shared.xaml" /> <!-- 這裏省略 --> <ResourceDictionary Source="Resources\NavigationWindow.xaml" /> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Application.Resources>
這樣的用法頗有借鑑意義。在WPF中實現Skin框架也隨之變得很是簡單。值須要動態使用不一樣的XAML文件便可。而後是Window1.xaml文件。它裏面幾乎把全部的控件都顯示了一遍。沒有什麼多說的。重點看Resource目錄下的自定義控件文件。這裏的控件太多,不可能每一個都說說。我只挑選其中的Button.xaml爲例:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" >
<ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="Shared.xaml"/> </ResourceDictionary.MergedDictionaries>
<!-- Focus Visual -->
<Style x:Key="ButtonFocusVisual"> <Setter Property="Control.Template"> <Setter.Value> <ControlTemplate> <Border> <Rectangle Margin="5" StrokeThickness="3" Stroke="#60000000" StrokeDashArray="4 2"/> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style> <!--...............--> </ResourceDictionary>
由於這個XAML文件做爲資源使用,因此其根元素是ResourceDictionary,而再也不是Window/Application等等。同時,資源文件也能夠相互的嵌套,好比上面的包含的Shared.xaml文件。而後定義了一個Style,注意這裏的目標類型爲Control.Template,也就是針對全部的控件模板有效,因此Style添加了一個x:Key屬性。這樣就阻止Style適用於當前的全部控件。咱們必須顯式的引用這個Style。相關內容,能夠參考我前面的Style文章。
另外一個須要說明的是<ControlTemplate>的子元素,能夠是任何的VisualTree。好比這裏的Border,也能夠是Grid等等。好了,如今定義了一個名爲ButtonFocusVisual的模板,下面只須要引用它便可。
<Style TargetType="Button"> <!--.............--> <Setter Property="FocusVisualStyle" Value="{StaticResource ButtonFocusVisual}"/> <!--.............--> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="Button">
<Border x:Name="Border" ......./>
<ControlTemplate.Triggers> <Trigger Property="IsKeyboardFocused" Value="true"> <Setter TargetName="Border" Property="BorderBrush" Value="{StaticResource DefaultedBorderBrush}" /> </Trigger> </ControlTemplate.Triggers>
</ControlTemplate> </Setter.Value> </Setter> </Style>
這是真正影響控件外觀的代碼。由於在定義Style的時候沒有指定具體的x:Key,因此將影響全部的Button。如你所見,在FocusVisualStyle這個屬性(類型是Style)上咱們用資源方式引用了前面定義的命名Style:ButtonFocusVisual。接下來是定義Template,併爲其子元素Border定義了一個名稱。而後就是ControlTemplate的觸發器。在IsKeyboardFocused屬性知足條件的狀況下,咱們把Border(注意這個Border不是類型,而是具體的某個對象)的BorderBrush修改成另外一個靜態資源。結合前面的Post,理解也就不難了。
最後,咱們還會發現一個有趣的問題:這個例子雖然是ControlTempalte,但工程名稱倒是SimpleStyle,從這一點咱們也能夠看出:Style和Template一般是配合使用才能真正的實現豐富的自定義功能。
在創建漂亮UI的同時,咱們還須要關注應用程序的性能,WPF尤爲如此。下面從MS的文檔中總結出了一些有用的性能優化點。在實際編寫的過程當中,能夠參考。這個Post非徹底原創,是根據一些文檔總結出來的。
1、創建邏輯樹的時候,儘可能考慮從父結點到子結點的順序構建。由於當邏輯樹的一個結點發生變化時(好比添加或刪除),它的父結點和全部的子結點都會激發Invalidation。咱們應該避免沒必要要的Invalidation。
2、當咱們在列表(好比ListBox)顯示了一個CLR對象列表(好比List)時,若是想在修改List對象後,ListBox也動態的反映這種變化。此時,咱們應該使用動態的ObservableCollection對象綁定。而不是直接的更新ItemSource。二者的區別在於直接更新ItemSource會使WPF拋棄ListBox已有的全部數據,而後所有從新從List加載。而使用ObservableCollection能夠避免這種先所有刪除再重載的過程,效率更高。
3、在使用數據綁定的過程當中,若是綁定的數據源是一個CLR對象,屬性也是一個CLR屬性,那麼在綁定的時候對象CLR對象所實現的機制不一樣,綁定的效率也不一樣。
A、數據源是一個CLR對象,屬性也是一個CLR屬性。對象經過TypeDescriptor/PropertyChanged模式實現通知功能。此時綁定引擎用TypeDescriptor來反射源對象。效率最低。 B、數據源是一個CLR對象,屬性也是一個CLR屬性。對象經過INotifyPropertyChanged實現通知功能。此時綁定引擎直接反射源對象。效率稍微提升。 C、數據源是一個DependencyObject,並且屬性是一個DependencyProperty。此時不須要反射,直接綁定。效率最高。
4、訪問CLR對象和CLR屬性的效率會比訪問DependencyObject/DependencyProperty高。注意這裏指的是訪問,不要和前面的綁定混淆了。可是,把屬性註冊爲DependencyProperty會有不少的優勢:好比繼承、數據綁定和Style。因此有時候咱們能夠在實現DependencyProperty的時候,利用緩存機制來加速訪問速度:看下面的緩存例子:
public static readonly DependencyProperty MagicStringProperty = DependencyProperty.Register("MagicString", typeof(string), typeof(MyButton), new PropertyMetadata(new PropertyInvalidatedCallback(OnMagicStringPropertyInvalidated),new GetValueOverride(MagicStringGetValueCallback)));
private static void OnMagicStringPropertyInvalidated(DependencyObject d) { // 將緩存的數據標識爲無效 ((MyButton)d)._magicStringValid = false; }
private static object MagicStringGetValueCallback(DependencyObject d) { // 調用緩存的訪問器來獲取值 return ((MyButton)d).MagicString; }
// 私有的CLR訪問器和本地緩存 public string MagicString { get { // 在當前值無效時,獲取最新的值保存起來 if (!_magicStringValid) { _magicString = (string)GetValueBase(MagicStringProperty); _magicStringValid = true; }
return _magicString; } set { SetValue(MagicStringProperty, value); } }
private string _magicString; private bool _magicStringValid;
另外,由於註冊的DependencyProperty在默認是不可繼承的,若是須要繼承特性,也會下降DependencyProperty值刷新的效率。註冊DependencyProperty屬性時,應該把DefaultValue傳遞給Register方法的參數來實現默認值的設置,而不是在構造函數中設置。
5、使用元素TextFlow和TextBlock時,若是不須要TextFlow的某些特性,就應該考慮使用TextBlock,由於它的效率更高。
6、在TextBlock中顯式的使用Run命令比不使用Run命名的代碼要高。
7、在TextFlow中使用UIElement(好比TextBlock)所需的代價要比使用TextElement(好比Run)的代價高。
8、把Label(標籤)元素的ContentProperty和一個字符串(String)綁定的效率要比把字符串和TextBlock的Text屬性綁定的效率低。由於Label在更新字符串是會丟棄原來的字符串,所有從新顯示內容。
9、在TextBlock塊使用HyperLinks時,把多個HyperLinks組合在一塊兒效率會更高。看下面的兩種寫法,後一種效率高。
A、 <TextBlock Width="600" > <Hyperlink TextDecorations="None">MSN Home</Hyperlink> </TextBlock> <TextBlock Width="600" > <Hyperlink TextDecorations="None">My MSN</Hyperlink> </TextBlock>
B、 <TextBlock Width="600" > <Hyperlink TextDecorations="None">MSN Home</Hyperlink> <Hyperlink TextDecorations="None">My MSN</Hyperlink> </TextBlock>
10、任與上面TextDecorations有關,顯示超連接的時候,儘可能只在IsMouseOver爲True的時候顯示下劃線,一直顯示下劃線的代碼高不少。
11、在自定義控件,儘可能不要在控件的ResourceDictionary定義資源,而應該放在Window或者Application級。由於放在控件中會使每一個實例都保留一份資源的拷貝。
12、若是多個元素使用相同的Brush時,應該考慮在資源定義Brush,讓他們共享一個Brush實例。
13、若是須要修改元素的Opacity屬性,最後修改一個Brush的屬性,而後用這個Brush來填充元素。由於直接修改元素的Opacity會迫使系統建立一個臨時的Surface。
14、在系統中使用大型的3D Surface時,若是不須要Surface的HitTest功能,請關閉它。由於默認的HitTest會佔用大量的CPU時間進行計算。UIElement有應該IsHitTestVisible屬性能夠用來關閉HitTest功能。