出處:http://www.cnblogs.com/free722/archive/2011/11/12/2238654.html 邏輯樹與可視樹 XAML天生就是用來呈現用戶界面的,這是由於它具有層次化的特性。在WPF中,用戶界面由一個對象樹構建而成,這棵樹叫作邏輯樹。 WPF用戶界面的邏輯樹也並
出處:http://www.cnblogs.com/free722/archive/2011/11/12/2238654.html
邏輯樹與可視樹
XAML天生就是用來呈現用戶界面的,這是由於它具有層次化的特性。在WPF中,用戶界面由一個對象樹構建而成,這棵樹叫作邏輯樹。
WPF用戶界面的邏輯樹也並不一定用XAML創建,它完全可能用過程式代碼來實現。
邏輯樹的概念很直觀,但為什麼要關註它呢?因為幾乎WPF的每一方面(屬性、資源、事件等)都有與邏輯樹相關聯的行為。如,屬性值有時會沿著樹自動傳遞給子元素,而觸發的事件可以自底向上或自頂向下遍歷樹。
與邏輯樹類似的一個概念是可視樹。可視樹基本上是邏輯樹的擴展,在可視樹中,節點都被打散,分放到核心可視組件中。可視樹提供了一些詳細的可視化實現,而不是把每個元素當作一個“黑盒”。如,雖然ListBox從邏輯上講是一個單獨的控制項,但它的預設可視呈現是由更多的原始WPF元素組成的:一個Border對象、兩個ScrollBar及其他一些元素。
並非所有的邏輯樹節點都會出現在可視樹中,只有從System.Windows.Media.Visual或System.Windows.Media.Visual3D派生的元素才會被包含進去。其他元素不會包含在內,因為它們自己並沒有與生俱來的呈現行為。
使用System.Windows.LogicTreeHelper和System.Windows.Media.VisualTreeHelper這兩個有些對象的類可以方便地遍歷邏輯樹和可視樹。
註意:不要根據具體的可視樹寫代碼。邏輯樹是靜態的,不會受到程式員的干擾(例如動態添加/刪除)元素,但只要用戶切換不同的Windows主題,可視樹就會改變。
遍歷和列印邏輯樹和可視樹的示例代碼:
using System;
using System.Diagnostics;
using System.Windows;
using System.Windows.Media;
public parital class AboutDialog : Window
{
public AboutDialog()
{
IntializeComponent();
PrintLogicalTree(0, this);
}
protected override void OnContentRendered(EventArgs e)
{
base.OnContentRendered(e);
PrintVisualTree(0, this);
}
void PrintLogicalTree(int depth, object obj)
{
//列印對象,使用前置空格表示深度
Debug.WriteLine(new string('', depth) + obj);
//有時,葉子節點不是DependencyObject,如string
if(!(obj is DependencyObject)) return;
//遞歸調用每個邏輯子節點
foreach(object child in LogicalTreeHelper.GetChildren(obj as DependencyObject))
PrintLogicalTree(depth + 1, child);
}
void PrintVisualTree(int depth, DependencyObject obj)
{
//列印對象,使用前置空格表示深度
Debug.WriteLine(new string('', depth) + obj);
//遞歸調用每個可視子節點
for(int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
PrintVisualTree(depth + 1, VisualTreeHelper.GetChild(obj, i));
}
}
雖然在Window的構造函數中就可以遍歷邏輯樹,但可視樹真到Window完成至少一次佈局後才會有節點,否則是空的。這也是為什麼PrintVisualTree是在On-ContentRendered中調用的,因為OnContentRendered是在佈局完成後才被調用的。
依賴屬性
WPF引入了一個新的屬性類型叫作依賴屬性,整個WPF平臺中都會使用到它,用來實現樣式化、自動數據綁定、動畫等。
依賴屬性在任何時刻都是依靠多個提供程式來判斷它的值的。這些提供程式可以是一段一直在改變值的動畫,或者一個父元素的屬性值從上慢慢傳遞給子元素等。依賴屬性的最大特征是其內建的傳遞變更通知的能力。
添加這樣的智能給屬性,其動力在於能夠聲明標記中直接啟用富功能。WPF友好聲明設計的關鍵在於它使用了很多屬性。例如,Button控制項有96個公共屬性。屬性可以方便地在XAML中設置而不用程式代碼。但如果依賴屬性沒有額外的垂直傳遞,在不寫額外代碼的情況下,很難在設置屬性這樣簡單的動作中獲得想要的結果。
依賴屬性的實現
實際上,依賴屬性僅僅是普通的.NET屬性,只不過它已融入到了WPF架構中。它完全是由WPF API實現的,沒有一種.NET語言天生就能理解依賴屬性。
下例展示了一個Button如何有效地實現一個叫IsDefault的依賴屬性:
public class Button : ButtonBase
{
//依賴屬性
public static readonly DependencyProperty IsDefaultProperty;
static Button()
{
//註冊屬性
Button.IsDefaultProperty = DependencyProperty.Register("IsDefault", typeof(bool),
typeof(Button),
new FrameworkPropertyMetadata(false, new PropertyChangedCallback(OnIsDefaultChanged)));
...
}
//.NET屬性包裝器(可選)
public bool IsDefault
{
get { return (bool)GetValue(Button.IsDefaultProperty); }
set { SetValue(Button.IsDefaultProperty, value); }
}
//屬性改變的回調(可選)
private static void OnIsDefaultChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{ ... }
}
IsDefaultProperty靜態成員是真正的依賴屬性,類型為System.Windows.DependencyProperty。按規則,所有的DependencyProperty成員都必須是public、static,並且有一個Property作為尾碼。依賴屬性通常調用DependencyProperty.Register靜態方法創建,這樣的方法需要一個名稱(IsDefault)、一個屬性類型(bool)及擁有這個屬性的類(Button類)。通過不同的Register方法重載,你可以傳入metadata(元數據)來告訴WPF如何處理該屬性、如何處理屬性值改變的回調、如何處理強制值轉換、及如何驗證值。Button會在它的靜態構造函數中調用Register的重載,給依賴屬性一個預設值false,併為變更通知添加一個委托。
最後,那個叫作IsDefault的傳統.NET屬性會調用繼承自System.Windows.DependencyObject的GetValue和SetValue方法來實現自己的訪問器,System.-Windows.DependencyObject是底層基類,這是擁有依賴屬性的類必須繼承的。GetValue返回最後一次由SetValue設置的值,如果SetValue從未被調用過,那麼就是該屬性註冊時的預設值。IsDefault .NET屬性並不是必需的,Button的使用者可能會直接調用GetValue/SetValue方法,因為它們是公開的。
註意:在運行時,繞過了.NET屬性包裝器在XAML中設置依賴屬性。
雖然XAML編譯器在編譯時是依靠該屬性包裝器的,但在運行時WPF是直接調用GetValue和SetValue的。因此,為讓使用XAML設置屬性與使用過程式代碼設置屬性保持一致,在屬性包裝器中除了GetValue/SetValue調用外,不應該包含任何其他邏輯,這是至關重要的。如果要添加自定義邏輯,應該在註冊的回調函數中添加。
錶面上看,上例代碼像是一種冗長的呈現簡單布爾屬性的方式。然而,因為GetValue和SetValue內部使用了高效的稀疏存儲系統,而IsDefaultProperty是一個靜態成員(而不是一個實例成員),與典型的.NET屬性相比,依賴屬性的實現節省了保存每個實現所需要的記憶體。
依賴屬性的好處遠不止節約記憶體而已。它把相當一部分代碼集中起來,並做標準化處理。
變更通知
無論何時,只要依賴屬性的值變了,WPF就會自動根據屬性的元數據(metadata)觸發一系列動作。內建的變更通知最有趣的特性之一是屬性觸發器,它可以在屬性值改變時執行自定義動作,而不用更改任何過程式代碼。
例如,你想讓Button在滑鼠移上去時變為藍色。如果沒有屬性觸發器的話,你要為每個Button添加兩個事件處理程式,一個為MouseEvent事件準備,一個為MouseLeave事件準備。
<Button MouseEnter="Button_MouseEnter" MouseLeave="Button_MouseLeave"
MinWidth="75" Margin="10">
Help</Button>
<Button MouseEnter="Button_MouseEnter" MouseLeave="Button_MouseLeave"
MinWidth="75" Margin="10">
OK</Button>
下麵的代碼實現了這兩個事件處理程式:
void Button_MouseEnter(object sender, MouseEventArgs e)
{
Button b = sender as Button;
if(b != null) b.Foreground = Brushed.Blue;
}
void Mouse_MouseLeave(object sender, MouseEventArgs e)
{
Button b = sender as Button;
if(b != null) b.Foreground = Brushed.Black;
}
然而有了屬性觸發器,完全可以在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支持的3種觸發器之一。數據觸發器是屬性觸發器的另一種形式,它可以在任何.NET屬性中工作(而不僅僅是依賴屬性)。事件觸發器會通過聲明方式指定動作,該動作在路由事件觸發時生效。
屬性值繼承
術語“屬性值繼承”並不是指傳統的面向對象的類繼承,而是指屬性值自頂向下沿著元素樹傳遞。
屬性值的繼承行為由以下兩個因素決定:
- 並不是每個依賴屬性都參與屬性值繼承。(從內部來講,依賴屬性會通過傳遞FrameworkPropertyMetadataOptions.Inherits給DependencyProperty-.Register方法來完成繼承。
- 有其他一些優先順序更高的源來設置這些屬性值。
對多個提供程式的支持
WPF有許多強大的機制可以獨立地去嘗試設置依賴屬性的值。如果沒有設計良好的機制來處理這些完全不同的屬性值提供程式,這個系統會變得混亂,屬性值會變得不穩定。當然,正如它們的名字所表達的,依賴屬性就是設計為以一致的、有序的方式依靠這些提供程式。
下圖展示了這5步流程,通過該流程,WPF運行每個依賴屬性並最終計算出它的值。依靠依賴屬性中內嵌的變更通知,這個流程才可自動發生。
判斷基礎值
大多數屬性值提供程式會把基礎值的計算納入考慮範疇。下麵的清單顯示了8個提供程式,它們可以設置大多數依賴屬性的值,優先順序順序從高到低為:
本地值->樣式觸發器->模板觸發器->樣式設置程式->主題樣式觸發器->主題樣式設置程式->屬性值繼承->預設值
本地值技術上的含義是任何對DependencyObject.SetValue的調用,但它通常會有一個簡單的屬性賦值,這是用XAML或過程式代碼完成的。
預設值指的是依賴屬性註冊時使用的初始值。
計算
如果第一步中的值是表達式(派生自System.Windows.Expression的一個對象),那麼WPF會執行一種特殊的演算步驟--把表達式轉換為具體的結果。在WPF 3.0中,表達式僅在使用動態資源或數據綁定時起作用。
應用動畫
如果一個或多個動畫在運行,它們有能力改變當前的屬性值或完全替代當前的屬性值。
限制
在所有屬性值提供程式處理過後,WPF將拿到一個幾乎是終值的屬性值,如果依賴屬性已經註冊了CoerceValueCallback,還會把這個屬性值傳遞給Coerce-ValueCallback委托。該回調函數負責返回一個新的值,它是基於自定義邏輯實現的。
驗證
最後,如果依賴屬性已經註冊了ValidateValueCallback,之前的限制中的值將被傳入ValidateValueCallback委托。如果輸入值有效,該回調函數返回true,否則返回false。返回false將會導致拋出一個異常,並使整個流程被取消。
如果沒辦法判斷依賴屬性從哪裡獲得當前值,那麼可以得到靜態方法DependencyPropertyHelper.GetValueSource作為調試助手。該方法將返回一個ValueSource結構,其中包含以下一些數據:一個BaseValueSource枚舉值,它反映的是基礎值從哪裡來的(流程中的第一步);IsExpression、IsAnimated和IsCoerced幾個布爾類型屬性,它反映了第二步到第四步的信息。
請不要在程式代碼中使用這個方法,WPF以後的版本中將打破值計算的假設,會根據它的源類型採用不同的方式處理屬性值,而不是根據假設WPF應用程式中的方式來處理。
你很可能需要清除本地值,並讓WPF從下一個最高優先順序的提供程式中獲得值,然後使用這個值來設置最終的屬性值。DependencyObject提供了這樣的機制,可通過調用ClearValue方法來實現。
//b為Button實例
b.ClearValue(Button.ForegroundProperty);
Button.ForegroundProperty是一個DependencyProperty靜態成員,在調用ClearValue後,會重新計算基礎值,並把本地值從方程式中刪除。
附加屬性
附加屬性是依賴屬性的一種特殊形式,可以被有效地添加到任何對象中。這可能聽上去很奇怪,但這個機制在WPF中有多種應用。
類似於WinForm那樣的技術,許多WPF類定義了一個Tag屬性(類型是System.Object),目的是為了存儲每一個實例的自定義數據。但要添加自定義數據給任何一個派生自DependencyObject的對象,附加屬性是一種更加強大、更加靈活的機制。通常我們會忽略一點,即可以用附加屬性高效的向密封類(sealed class)的實例添加自定義數據。
另外,大家對附加屬性有一個曲解,雖然在XAML中設置它們依賴於SetXXX靜態方法,但可在過程式代碼中繞過這個方法,直接去調用DependencyObject-.SetValue方法。這意味著在過程式代碼中,可以把任何一個依賴屬性作為一個附加屬性。如,下麵的代碼把ListBox的IsTextSearchEnabled屬性添加到了Button控制項上,並賦予該屬性一個值:
//向Button添加一個不相關的屬性,並把它的值設置為true
okButton.SetValue(ListBox.IsTextSearchEnabledProperty, true);
雖然這似乎沒有任何意義,但你可以用一種對應用程式或組件有意義的方式來隨意使用這個屬性值。
路由事件
正如WPF在簡單的.NET屬性概念上添加了許多基礎的東西一樣,它也為.NET事件添加了許多基礎的東西。路由事件是專門設計用於在元素樹中使用的事件。當路由事件觸發後,它可以向上或向下遍歷可視樹和邏輯樹,用一種簡單而且持久的方式在每個元素上觸發,而不需要使用任何定製代碼。
事件路由讓許多程式不去留意可視樹的細節(對於樣式重置來說這是很不錯的),並且對於成功的WPF元素創作至關重要。
以前一章中,對於VCR樣式的Stop按鈕來說,一個用戶可能在Rectangle邏輯子元素上直接按下滑鼠左鍵。由於事件遍歷了邏輯樹,Button元素還是會發現這個事件,並處理該事件。因此,你可以在一個元素(如Button)中嵌入任何複雜內容或設置一棵複雜的可視樹,滑鼠左鍵單擊其中任何一個內部元素,仍然會觸發父元素Button的Click事件。如果沒有路由事件,內部內容的創造者或按鈕的使用者不得不編寫代碼來把事件串起來。
路由事件的實現和行為與依賴屬性有許多相同的地方。
路由事件的實現
與依賴屬性一樣,沒有一種.NET語言(除XAML外)天生具有理解路由指派的能力。
就像依賴屬性是由公共的靜態DependencyProperty成員加上一個約定的Property尾碼名構成的一樣,路由事件也是由公共的靜態RoutedEvent成員加上一個約定的Event尾碼名構成的。路由事件的註冊很像靜態構建器中註冊依賴屬性,它會定義一個普通的.NET事件或一個事件包裝器,這樣可以保證在過程式代碼中使用起來更加熟悉,並且可以在XAML中用事件特性語法添加一個事件處理程式。與屬性包裝器一樣,事件包裝器在訪問器中只能調用AddHandler和RemoveHandler,而不應該做其他事件。
public class Button : ButtonBase
{
//路由事件
public static readonly RoutedEvent ClickEvent;
static Button()
{
//註冊事件
Button.ClickEvent = 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));
...
}
...
}
這些AddHandler和RemoveHandler方法沒有從DependencyObject繼承,而是從System.Windows.UIElement繼承的,UIElement是一個更高層的供元素(如Button元素)繼承的基類。這些方法可以向一個適當的路由事件添加一個委托或從路由事件移除一個委托。在OnMouseLeftButtonDown中,它使用適當的RoutedEvent成員調用RaiseEvent來觸發Click事件。當前的Button實例(this)被傳遞給事件的源元素(source element)。在代碼清單中沒有列出,但是作為對KeyDown事件的響應,Button的Click事件將被觸發,這樣就可以處理由空格健或回車鍵完成點擊動作的情況。
路由策略和事件處理程式
當註冊完成後,每個路由事件將選擇3個路由策略中的一個。所謂路由策略就是事件觸發遍歷整棵元素樹的方式,這些策略由RoutingStategy枚舉值提供。
Tunneling(管道傳遞)----事件首先在根元素上被觸發,然後從每一個元素向下沿著樹傳遞,直至到達源元素為止(或者直至處理程式把事件標記為已處理為止)。
Bubbling(冒泡)----事件首先在源元素上被觸發,然後從每一個元素向上沿著樹傳遞,直至到達根元素為止(或者直至處理程式把事件標記為已處理為止)。
Direct(直接)----事件僅在源元素上觸發。這與普通.NET事件的行為相同,不同的是這樣的事件仍然會參與一些路由事件的特定機制,如事件觸發器。
路由事件的事件處理程式有一個簽名,它與通用.NET事件處理程式的模式匹配:第一個參數是一個System.Object對象,名為sender,第二個參數(一般命名為e)是一個派生自System.EventArgs的類。傳遞給事件處理程式的sender參數就是該處理程式被添加到的元素。參數e是RoutedEventArgs的一個實例(或者派生自RoutedEventArgs),RoutedEventArgs是EventArgs的一個子類,它提供了4個有用的屬性:
Source----邏輯樹中一開始觸發該事件的元素。
OriginalSource----可視樹中一開始觸發該事件的元素(例如,TextBlock或標準的Button元素的ButtonChrome子元素)。
Handled----布爾值,設置為true表示標記事件為已處理,這就是用於停止Tunneling或Bubbling的標記。
RoutedEvent----真正的路由事件對象(如Button.ClickEvent),當一個事件處理程式同時被用於多個路由事件時,它可以有效的識別被觸發的事件。
Source和OriginalSource的存在允許使用更高級別的邏輯樹或更低級別的可視樹。然而,這種區別僅對於像滑鼠事件這樣的物理事件有效。對於更抽象的事件來說,不需要與可視樹中的某個元素建立直接關係(就像由於鍵盤支持的Click),WPF會傳遞相同的對象給Source和OriginalSource。
路由事件的實現
UIElement類為鍵盤、滑鼠、指示筆輸入定義了許多路由事件。大多數路由事件是冒泡事件,但許多事件與管道事件是配對的。管道事件很容易被識別,因為按照慣例,它們的名字中都有一個Preview首碼,在它們的配對冒泡事件發生前,這些事件會立即被觸發。例如,PreviewMouseMove就是一個管道事件,在MouseMove冒泡事件前被觸發。
為許多不同的行為提供一對事件是為了給元素一個有效地取消事件或在事件即將發生前修改事件的機會。根據慣例,(當定義了冒泡和管道的事件對後)WPF的內嵌元素只會在響應一個冒泡事件時採取行動,這樣可以保證管道事件能夠名副其實的做到“預覽”。例如,在TextBox控制項的Preview事件中對錄入的文本進行校驗,過濾不符合規範的文本。
處理單擊滑鼠中鍵的事件在哪裡?
如果瀏覽一遍UIElement或ConentElement提供的所有滑鼠事件,可以找到MouseLeftButtonDown、MouseLeftButtonUp、MouseRightButtonDown、MouseRightButtonUp事件,但有些滑鼠上出現的附加按鍵該怎麼辦呢?
這一信息可以通過更加通用的MouseDown和MouseUp事件獲得。傳入這樣的事件處理程式的參數包括一個MouseButton枚舉值,它表示滑鼠狀態Left、Right、Midle、XButton1、XButton2,還有一個MouseButtonState枚舉值,表示這個按鈕是Pressed還是Released。
中止路由事件是一種假象
雖然在事件處理程式中設置RoutedEventArgs參數的Handled屬性為true,可以終止管道傳遞或冒泡,但是進一步沿著樹向上或向下的每個處理程式還是可以收到這些事件。這隻能在代碼中完成。在任何時候,都應該儘可能地避免處理已處理過的事件,因為事件應該是在第一時間被處理的。
總之,終止管理傳遞或冒泡僅僅是一種假像而已。更加準確的說法應該是,當一個路由事件標記為已處理時,管道傳遞和冒泡仍然會繼續,但預設情況下,事件處理程式只會處理沒有處理過的事件。
附加事件
通過附加事件,WPF可以通過一個沒有定義過該事件的元素來完成路由事件的管道傳遞和冒泡。
附加事件與附加屬性操作起來很像。每個路由事件都可以被當作附加事件使用。
由於需要傳遞許多信息給路由事件,可以用上層的“megahandler”來處理每一個管道或冒泡事件。這個處理程式通過分析RoutedEvent對象判斷哪個事件被觸發了,並把RoutedEventArgs參數轉換為一個合適的子類,然後繼續。
命令
WPF提供了內建的命令支持,這是一個更為抽象且松耦合的事件版本。儘管事件是與某個用戶動作相關聯的,但命令表示的是那些與用戶界面分離的動作,最標準的命令示例是剪切(Cut)、複製(Copy)、粘貼(Paste)。應用程式總能通過許多同步的機制提供這些動作:Menu控制項中的MenuItem、ContextMenu控制項中的MenuItem、ToolBar控制項中的Button、鍵盤快捷方式等。
內建命令
命令是任何一個實現了ICommand介面(位於System.Windows.Input命名空間)的對象,每個對象定義了3個簡單的成員:
Execute:執行特定命令的邏輯的方法。
CanExecute:如果命令允許被執行,則返回true,否則返回false。
CanExecuteChanged:無論何時,只要CanExecute的值改變,該事件就會觸發。
如果需要創建剪切、複製和粘貼命令,可以定義3個實現ICommand介面的類,找一個地方存儲這3個類(如放在主視窗的靜態成員中),從相關的事件處理程式中調用Execute(當CanExecute返回true時),處理CanExecuteChanged事件,改變相關用戶界面中的IsEnabled屬性。
像Button、CheckBox、MenuItem這樣的控制項有相關的邏輯會與任何命令做交互。它們會有一個簡單的Command屬性(類型為ICommand),當設置了Command屬性後,無論何時Click事件觸發,這些控制項會自動調用命令的Execute方法(只要CanExecute返回true時)。另外,它們會自動保持IsEnabled的值與CanExecute的值同步,這是通過CanExecuteChanged事件實現的。通過這種給屬性賦值的方式,任何邏輯在XAML下都是可以實現的。
同時,WPF已經定義了一系列命令,因此不需要為Cut、Copy和Paste命令實現ICommand對象,也不用擔心在哪裡保存這些命令。WPF有5個類的靜態屬性實現了WPF的內建命令:
ApplicationCommands----Close、Copy、Cut、Delete、Find、Help、New、Open、Paste、Print、PrintPreview、Properties、Redo、Replace、Save、SaveAs、SelectAll、Stop、Undo等。
其他4個類為ComponentCommands、MediaCommands、NavigationCommands、EditCommands。
每個屬性返回RoutedUICommand的實例,RoutedUIElement類不僅實現了ICommand介面,還可以像路由事件一樣支持冒泡。