由上一章可知,WPF中的許多控件都是內容控件,而內容控件可包含任何類型以及大量的嵌套內容。例如,可構建包含圖形的按鈕,創建混合了文本和圖片內容的標簽,或者為了實現滾動或折疊的顯示效果而在特定容器中放置內容。設置可以多次重復嵌套,直至達到你所希望的層次深度。如下所示:
<Window x:Class="RouteEvent.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <Grid> <Label BorderThickness="1" BorderBrush="Black"> <StackPanel> <TextBlock Margin="3">Image and text label</TextBlock> <Image Source="face.jpg" Stretch="Fill" Width="64" Height="64"></Image> <TextBlock Margin="3">Courtesy of the StackPanel</TextBlock> </StackPanel> </Label> </Grid> </Window>
正如上面所看到的,放在WPF窗口中的所有要素都在一定層次上繼承自UIElement類,包括Label、StackPanel、TextBlock和Image。UIElement定義了一些核心事件。例如,每個繼承自UIElement的類都提供了MouseDown事件和MouseUp事件。
但當單擊上面這個特殊標簽中的圖像部分時,想一想會發生什么事情。很明顯,引發Image.MouseDown事件和Image.MouseUp事件是合情合理的。但如果希望采用相同的方式來處理標簽上的所有單擊事件,該怎么辦呢?此時,不管單擊了圖像、某塊文本還是標簽內的空白處,都應當使用相同的代碼進行相應。
顯然,可為每個元素的MouseDown或MouseUp事件關聯同一個事件處理程序,但這樣會是標記變得雜亂無章且難以維護。WPF使用路由事件模型提供了一個更好的解決方案。
路由事件實際上以下列三種方式出現:
- 與普通.NET事件類似的直接路由事件(direct event)。它們源於一個元素,不傳遞給其他元素。例如,MouseEnter事件(當鼠標指針移到元素上時發生)是直接路由事件。
- 在包含層次中向上傳遞的冒泡路由事件(bubbling event)。例如,MouseDown事件就是冒泡路由事件。該事件首先由被單擊的元素引發,接下來被該元素的父元素引發,然后被父元素的父元素引發,依此類推,直到WPF到達元素樹的頂部為止。
- 在包含層次中向下傳遞的隧道路由事件(tunneling event)。隧道路由事件在事件到達恰當的控件之前為預覽事件(甚至終止事件)提供了機會。例如,通過PreviewKeyDown事件可截獲是否按下了某個鍵。首先在窗口級別上,然后是更具體的容器,直至到達當按下鍵時具有焦點的元素。
當使用EventManager.RegisterEvent()方法注冊路由事件時,需要傳遞一個RoutingStrategy枚舉值,該值用於指示希望應用於事件的事件行為。
MouseUp事件和MouseDown事件都是冒泡路由事件,因此現在可以確定在上面特殊的標簽示例中會發生什么事情。當單擊標簽上的圖像部分時,按一下順序觸發MouseDown事件:
(1)Image.MouseDown事件
(2)StackPanel.MouseDown事件
(3)Label.MouseDown事件
為標簽引發了MouseDown事件后,該事件會傳遞到下一個控件(在本例中是位於窗口中的Grid控件),然后傳遞到Grid控件的父元素(窗口)。窗口時整個層次中的頂級元素,並且是事件冒泡順序的最后一站,它是處理冒泡路由事件(如MouseDown事件)的最后機會。如果用戶釋放了鼠標按鍵,就會按相同的順序觸發MouseUp事件。
沒有限制要在某個位置處理冒泡路由事件。實際上,完全可在任意層次上處理MouseDown事件或MouseUp事件。但通常選擇最合適的事件路由層次完成這一任務。
一、RoutedEventArgs類
在處理冒泡路由事件時,sender參數提供了對整個鏈條上最后那個鏈接的引用。例如,在上面的示例中,如果事件在處理之前,從圖像向上冒泡到標簽,sender參數就會引用標簽對象。
有些情況下,可能希望確定事件最初發生的位置。可從RoutedEventArgs類的屬性(如下表所示)獲得這一信息以及其他細節。由於所有WPF事件參數類繼承自RoutedEventArgs,因此任何事件處理程序都可以使用這些屬性。
表 RoutedEventArgs類的屬性
二、冒泡路由事件
如下圖顯示了一個簡單窗口,該窗口演示了事件的冒泡過程。當單擊標簽中的一部時,在列表框中顯示事件發生的順序。圖中顯示了單擊標簽中的圖像之后窗口的情況。MouseUp事件傳遞了5級,在窗體中停止向上傳遞。
圖 冒泡的圖像單擊事件
要創建該測試窗口,將元素層次結構中的圖像以及它上面的每個元素都關聯到同一個事件處理程序——名為SomethingClicked()的方法。下面是所需的XAML標記:
<Window x:Class="RouteEvent.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="359" Width="329" MouseUp="SomethingClicked"> <Grid Margin="3" MouseUp="SomethingClicked"> <Grid.RowDefinitions> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="*"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> </Grid.RowDefinitions> <Label Margin="5" Grid.Row="0" HorizontalAlignment="Left" Background="AliceBlue" BorderThickness="1" BorderBrush="Black" MouseUp="SomethingClicked"> <StackPanel MouseUp="SomethingClicked"> <TextBlock Margin="3" MouseUp="SomethingClicked">Image and text label</TextBlock> <Image Source="face.jpg" Stretch="Fill" Width="16" Height="16" MouseUp="SomethingClicked"></Image> <TextBlock Margin="3" MouseUp="SomethingClicked">Courtesy of the StackPanel</TextBlock> </StackPanel> </Label> <ListBox Grid.Row="1" Margin="5" Name="lstMessages"></ListBox> <CheckBox Grid.Row="2" Margin="5" Name="chkHandle">Handle first event</CheckBox> <Button Grid.Row="3" Margin="5" Padding="3" HorizontalAlignment="Right" Name="cmdClear" Click="cmdClear_Click">Clear list</Button> </Grid> </Window>
SomethingClicked()方法簡單地檢查RoutedEventArgs對象的屬性,並且給列表框添加消息:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; namespace RouteEvent { /// <summary> /// MainWindow.xaml 的交互邏輯 /// </summary> public partial class MainWindow : Window { protected int eventCounter = 0; public MainWindow() { InitializeComponent(); } private void SomethingClicked(object sender, RoutedEventArgs e) { eventCounter++; string message = "#" + eventCounter.ToString() + ":\r\n" + " Sender: " + sender.ToString() + "\r\n" + " Source: " + e.Source + "\r\n" + " Original Source: " + e.OriginalSource + "\r\n"; lstMessages.Items.Add(message); e.Handled = (bool)chkHandle.IsChecked; } private void cmdClear_Click(object sender, RoutedEventArgs e) { eventCounter = 0; lstMessages.Items.Clear(); } } }
在本例中還有一個細節。如果選中chkHandle復選框,SomethingClicked()方法就將RoutedEventArgs.Handled屬性設為true,從而在事件第一次發生時就終止事件的冒泡過程。因此,這時在列表框中就只能看到第一個事件,如下圖所示:
因為SomethingClicked()方法處理由Window對象引發的MouseUp事件,所以也能截獲在列表框和窗口表面空白處的鼠標單擊事件。但當單擊Clear按鈕時(這會刪除所有列表框條目)不會引發MouseUp事件,這時因為按鈕包含了一些有趣的代碼,這些代碼會掛起MouseUp事件,並引發更高級的Click事件。同時,Handled標記被設置為true,從而會阻止MouseUp事件繼續傳遞。
三、處理掛起的事件
有一種方法可接受被標記處理過的事件。不是通過XAML關聯事件處理程序,而是必須使用前面介紹的AddHandler()方法。AddHandler()方法提供了一個重載版本,該版本可以接收一個Boolean值作為它的第三個參數。如果將該參數設置為true,那么即使設置了Handled標記,也將接收到事件:
cmdClear.AddHandler(UIElement.MouseUpEvent,new MouseButtonEventHandler(cmdClear_MouseUp),true);
這通常並不是正確的設計決策。為防止可能造成的困惑,按鈕被設計為會掛起MouseUp事件。畢竟,可采用多種方式使用鍵盤“單擊”按鈕,這是Windows中非常普遍的約定。如果為按鈕錯誤地處理了MouseUp事件,而沒有處理Click事件,那么事件處理代碼就只能對鼠標單擊做出相應,而不能對相應的鍵盤操作做出相應。
四、附加事件
上面這個有趣的標簽示例是一個非常簡單的事件冒泡示例,因為所有的元素都支持MouseUp事件。然而,許多控件有各自的特殊事件。按鈕便是一個例子——它添加了Click事件,而其他任何基類都沒有定義該事件。
這導致兩難的境地。假設在StackPanel面板中封裝了一堆按鈕,並希望在一個事件處理程序中處理所有這些按鈕的單擊事件。粗略的方法是將每個按鈕的Click事件關聯到同一個事件處理程序。但Click事件支持事件冒泡,從而提供了一種更好的選擇。可通過處理更高層次元素的Click事件(如包含按鈕的StackPanel面板)來處理所有按鈕的Click事件。
但看似淺顯的代碼卻不能工作:
<StackPanel Click="DoSomething" Margin="5"> <Button Name="cmd1">Command 1</Button> <Button Name="cmd2">Command 2</Button> <Button Name="cmd3">Command 3</Button> ... </StackPanel>
問題在於StackPanel面板沒有Click事件,所以XAML解析器會將其解釋錯誤。解決方案是以“類名.事件名"的形式使用不同的關聯事件語法。下面是更正后的示例:
<StackPanel Button.Click="DoSomething" Margin="5"> <Button Name="cmd1">Command 1</Button> <Button Name="cmd2">Command 2</Button> <Button Name="cmd3">Command 3</Button> ... </StackPanel>
現在,事件處理程序可以接收到StackPanel面板包含的所有按鈕的單擊事件了。
可在代碼中關聯附加事件,但需要使用UIElement.AddHandler()方法,而不能使用+=運算符語法。下面是一個示例(該例假定StackPanel面板已被命名為pnlButtons):
pnlButtons.AddHandler(Button.Click,new RoutedEventHandler(DoSomething));
在DoSomething()事件處理程序中,可使用多種方法確定是哪個按鈕引發了事件。可以比較按鈕的文本(對與本地化這可能會引起問題),也可以比較按鈕的名稱(這是脆弱的方法,因為當構建應用程序時無法捕獲輸入錯誤的名稱)。最好確保每個按鈕在XAML中都有Name屬性設置,從而可以通過窗口類的一個字段訪問相應的對象,並使用事件發送者比較應用。下面列舉一個示例:
private void DoSomething(object sender,RoutedEventArgs e) { if(sender==cmd1) { ... } else if(sender==cmd2) { ... } else if(sender==cmd3) { ... } ... }
另一個選擇是簡單地隨按鈕傳遞一段可以在代碼中使用的信息。比如設置每個按鈕的Tag屬性。在此不列舉出具體實例。
五、隧道路由事件
隨着路由事件的工作方式和冒泡路由事件相同,當方向相反。例如,如果MouseUp事件是隧道路由事件(實際上不是),在特殊的標簽示例中單擊圖形將導致MouseUp事件首先在窗口中被引發,然后在Grid控件中被引發,接下來在StackPanel面板中唄引發,依此類推,直至到達實際源頭,即標簽中的圖像為止。
隧道路由事件易於識別,他們都以單詞Preview開頭。而且,WPF通常成對地定義冒泡路由事件和隧道路由事件。這意味着如果發現冒泡的MouseUp事件,就還可以找到PreviewMouseUp隧道事件。隧道路由事件總在冒泡路由事件之前被觸發。如下圖所示:
更有趣的是,如果將隧道路由事件標記為已處理過,那就不會發生冒泡路由事件。這是因為兩個事件共享RoutedEventArgs類的同一個實例。
如果需要執行一些預處理(根據鍵盤上特定的鍵執行動作或過濾掉特定的鼠標動作),隧道路由事件是非常有用的。
如下面實例所示,該例測試PreviewKeyDown事件的隧道過程。當在文本框按下一個鍵時,事件首先在窗口觸發,然后再整個層次結構中向下傳遞。如果在任何位置將PreviewKeyDown事件標記為已處理過,就不會發生冒泡的KeyDown事件。
下面是所需的XAML標記:
<Window x:Class="TunnelRouteEvent.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="359" Width="329" PreviewKeyDown="SomethingClicked"> <Grid Margin="3" PreviewKeyDown="SomethingClicked"> <Grid.RowDefinitions> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="*"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> </Grid.RowDefinitions> <Label Margin="5" Grid.Row="0" HorizontalAlignment="Left" Background="AliceBlue" BorderThickness="1" BorderBrush="Black" PreviewKeyDown="SomethingClicked"> <StackPanel PreviewKeyDown="SomethingClicked"> <TextBlock Margin="3" PreviewKeyDown="SomethingClicked">Image and text label</TextBlock> <Image Source="face.jpg" Stretch="Fill" Width="16" Height="16" PreviewKeyDown="SomethingClicked"></Image> <TextBox Margin="3" PreviewKeyDown="SomethingClicked"></TextBox> </StackPanel> </Label> <ListBox Grid.Row="1" Margin="5" Name="lstMessages"></ListBox> <CheckBox Grid.Row="2" Margin="5" Name="chkHandle">Handle first event</CheckBox> <Button Grid.Row="3" Margin="5" Padding="3" HorizontalAlignment="Right" Name="cmdClear" Click="cmdClear_Click">Clear list</Button> </Grid> </Window>
后台代碼如下所示:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; namespace TunnelRouteEvent { /// <summary> /// MainWindow.xaml 的交互邏輯 /// </summary> public partial class MainWindow : Window { protected int eventCounter = 0; public MainWindow() { InitializeComponent(); } private void SomethingClicked(object sender, RoutedEventArgs e) { eventCounter++; string message = "#" + eventCounter.ToString() + ":\r\n" + " Sender: " + sender.ToString() + "\r\n" + " Source: " + e.Source + "\r\n" + " Original Source: " + e.OriginalSource + "\r\n" + " Event: " + e.RoutedEvent; lstMessages.Items.Add(message); e.Handled = (bool)chkHandle.IsChecked; } private void cmdClear_Click(object sender, RoutedEventArgs e) { eventCounter = 0; lstMessages.Items.Clear(); } } }