路由事件
WPF用更高級的路由事件替換普通的.NET事件。路由事件具有更強傳播能力,可在元素樹中向上冒泡和向下隧道傳播,並沿着傳播路徑被事件處理程序處理。與依賴屬性一樣,路由事件由只讀的靜態字段表示,在靜態構造函數中注冊,並通過標准的.NET事件定義進行封裝。
public abstract class ButtonBase : ContentControl
{
// 定義
public static readonly RoutedEvent ClickEvent;
// 注冊
static ButtonBase()
{
// 事件名稱 路由類型 定義事件處理程序語法的委托 擁有事件的類
ButtonBase.ClickEvent = EventManager.RegisterRoutedEvent("Click",RoutingStategy.Bubble,typeof(RoutedEventHandler),typeof(ButtonBase));
}
// 傳統包裝
public event RoutedEventHandler Click
{
add
{
base.AddHandler(ButtonBase.ClickEvent,value);
}
remove
{
base.RemoveHandler(ButtonBase.ClickEvent,value);
}
}
}
共享路由事件
與依賴屬性一樣,可以在類之間共享路由事件的定義。
UIElement.MouseUpEvent = Mouse.MouseUpEvent.AddOwner(typeof(UIElement));
引發路由事件
路由事件不是通過傳統的.NET事件封裝器引發的,而是使用 RaiseEvent() 方法引發的,所有元素都從 UIElement 類繼承了該方法。
每個事件處理程序的第一個參數(sender)都提供了引發該事件的對象的引用。第二個參數是 EventArgs 對象,該對象與其他所有可能很重要的附加細節綁定在一起。如果不需要傳遞額外的細節可使用 RoutedEventArgs.
處理路由事件
可以使用多種方法關聯事件處理程序。
<Image Source="hello.jpg" Name = "img" MouseUp="img_MouseUp"/>
// 定義委托對象,並將該委托指向 img_MouseUp() 方法
// 然后將該委托添加到 img.MouseUp 事件的已注冊的事件處理程序列表中
img.MouseUp += new MouseButtonEventHandler(img_MouseUp);
// C# 還允許使用更精簡的語法,隱式地創建合適的委托對象
img.MouseUp += img_MouseUp;
上面的代碼方法依賴事件封裝器,事件封裝器調用 UIElement.AddHandler() 方法。也可以自行調用 UIElement.AddHanler() 方法直接連接事件。
// 這種方法要創建合適的委托類型(MouseButtonEventHandler),不能隱式地創建委托對象
img.AddHandler(Image.MouseUpEvent, new MouseButtonEventHandler(img_MouseUp));
// 也可以使用定義事件的類的名稱,而不是引發事件的類的名稱
img.AddHandler(UIElement.MouseUpEvent,new MouseButtonEventHandler(img_MouseUp));
如果想斷開事件處理程序,只能使用代碼,不能使用 XAML。
img.MouseUp -= img_MouseUp;
img.RemoveHandler(Image.MouseUpEvent,new MouseButtonEventHandler(img_MouseUp));
為同一個事件多次連接相同的事件處理程序,通常是錯誤的結果,這種情況下事件處理程序會被觸發多次。如果試圖刪除已經連接了兩次的事件處理程序,事件仍會觸發事件處理程序,但只觸發一次。
事件路由
<Label BorderThickness="1">
<StackPanel>
<TextBlock Margin="3">
Image and text label
</TextBlock>
<Image Source="hello.jpg"/>
<TextBlock Margin="3">
Courtesy of the StackPanel
</TextBlock>
</StackPanel>
</Label>
上面的標簽包含了一個面板,面板里又包含了兩塊文本和一副圖像。單擊圖像部分會引發 Image.MouseDown 事件,但如果想采用相同方式處理標簽上的所有單擊事件呢?顯然為每個元素的 MouseDown 事件關聯同一個處理程序會使得代碼雜亂無章切難以維護。
路由事件以下面三種方式出現:
- 與普通.NET事件類似的直接路由事件(direct event).它們源於同一個元素,不傳遞給其他元素。比如,MouseEnter事件是直接路由事件。
- 在包含層次中向上傳遞的冒泡路由事件(bubbling event).比如,MouseDown就是冒泡路由事件。該事件首先由被單擊的元素引發,接下來被改元素的父元素引發,然后被父元素的父元素引發,以此類推,直到WPF到達元素樹的頂部為止。
- 在包含層次中向下傳遞的隧道事件(tunneling event).隧道路由事件在事件到達恰到的控件之前未預覽事件提供了機會。比如,通過PreviewKeyDown可截獲是否按下了某個鍵。首先在窗口級別上,然后是更具體的容器,直至到達當按下鍵時具有焦點的元素。
當使用 EventManager.RegisterEvent() 方法注冊路由事件時,需要傳遞一個 RoutingStrategy 枚舉值,該值用於指示希望應用於事件的事件行為。
MouseUp 和 MouseDown 都是冒泡事件,當單擊標簽上的圖像部分時:
- Image.MouseDown
- StackPanel.MouseDown
- Label.MouseDown
按照嵌套的順序,一直向上傳遞到窗口。
RoutedEventArgs 類
在處理冒泡路由事件時,sender參數是對最后哪個鏈接的引用。如果事件在處理之前,從圖像向上冒泡到標簽,sender參數就會引用標簽對象。
| 名稱 | 說明 |
|---|---|
| Source | 指示引發了事件的對象。鍵盤事件-具有焦點的控件;鼠標事件-鼠標下面所有元素中最靠上的元素 |
| OriginalSource | 最初引發事件的對象的引用。通常與Source相同 |
| RoutedEvent | 通過事件處理程序為觸發的事件提供 RoutedEvent 對象。如果同一個處理程序處理不同的事件,這個信息非常有用 |
| Handled | 該屬性允許終止事件的冒泡或隧道過程。如果設置為 true,事件就不會繼續傳遞,也不會再為其他元素引發該事件 |
處理掛起的事件
按鈕(button)會掛起MouseUp事件,並引發更高級的Click事件。同時,Handled標志被設置為 true ,從而阻止MouseUp事件繼續傳遞。
有趣的是,有一種方法可接收被標記為處理過的事件:
// 最后一個參數如果為 true,即使設置了 Handled 標志,也將接收到事件
cmdClear.AddHander(UIElement.MouseUpEvent, new MouseButtonEventHandler(cmdClear_MouseUp),true);
附加事件
<!--StackPanel並沒有 Click 事件-->
<StackPanel Button.Click="DoSomething" Margin="5">
<Button>Command 1</Button>
<Button>Command 2</Button>
</StackPanel>
Click事件實際是在 ButtonBase 類中定義的,而Button類繼承了該事件。如果為ButtonBase.Click事件關聯事件處理程序,那么當單擊任何繼承自ButtonBase控件(包括Button類、RadioButton類以及CheckBox類)時,都會調用該事件處理程序。如果為 Button.Click事件關聯處理程序,只能被Button對象使用。
也可以在代碼中關聯附加事件,但需要使用 UIElement.AddHandler()方法,而不能使用 += 運算符語法。
stackPanel.AddHandler(Button.Click, new RoutedEventHandler(DoSomething));
這種情況下,怎么區分是哪個按鈕觸發的事件?可以通過 button的文本,或者Name,也可以設置Tag屬性。
<StackPanel Button.Click="DoSomething" Margin="5">
<Button Tag="first button">Command 1</Button>
<Button Tag="second button">Command 2</Button>
</StackPanel>
private void DoSomething(object sender, RoutedEventArgs e)
{
object tag = ((FrameworkElement)sender).Tag;
MessageBox.Show(tag.toString());
}
隧道路由事件
隧道路由事件以單詞 Preview 開頭,WPF通常成對地定義冒泡路由事件和隧道路由事件。隧道路由事件總在冒泡路由事件之前被觸發。如果將隧道路由事件標記為已處理,那就不會再觸發冒泡路由事件,因為兩個事件共享 RoutedEventArgs類的同一個實例。
如果需要執行一些預處理(根據鍵盤上特定的鍵執行動作或過濾掉特定的鼠標動作),隧道路由事件是非常有用的。隧道路由事件的工作方式和冒泡路由事件相同,但方向相反。先在窗口觸發,然后再整個層次結構中向下傳遞,如果在任意為止標記為已處理,就不會發生對應的冒泡事件。
WPF事件
WPF事件通常包括以下5類:
- 生命周期事件:在元素被初始化、加載或卸載時發生這些事件
- 鼠標事件
- 鍵盤事件
- 手寫筆事件:在平板電腦上用手寫筆代替鼠標
- 多點觸控事件:一根或多跟手指在多點觸控屏幕上觸摸的結果
聲明周期事件
首次創建以及釋放所有元素時都會引發事件,它們是在 FrameworkElement 類中定義的。
| 名稱 | 說明 |
|---|---|
| Initialized | 當元素被實例化,並根據XAML標記設置了元素的屬性之后發生。這時元素已經初始化,但窗口的其他部分可能尚未初始化。此外,尚未應用樣式和數據綁定。是普通的.NET事件 |
| Loaded | 當整個窗口已經初始化並應用了樣式和數據綁定時,該事件發生。這是元素呈現之前的最后一站。這時 IsLoaded 為true |
| Unloaded | 當元素被釋放時,該事件發生,原因時包含元素的窗口被關閉或特定的元素被從窗口中刪除 |
| FrameworkElement類實現了 ISupportInitialize接口用來控制初始化過程的方法。 |
- 第一個方法是BeginInit(),在實例化元素后會立即調用該方法。
- 之后XAML解析器設置所有元素的屬性並添加內容。
- 第二個方法是 EndInit(),完成初始化后將調用。此時引發Initialized事件
當創建窗口時,會自下而上地初始化每個元素分支。在每個元素都完成初始化后還需要在容器中進行布局、應用樣式、綁定到數據源。完成初始化過程就會引發Loaded事件,該過程是自上而下的的方式。當所有元素都引發Loaded事件后窗口就可見了。
可以在窗口構造函數里添加自己的代碼,但Loaded事件是更好的選擇。因為如果構造函數中發生異常就會在XAML解析器解析頁面時拋出該異常。該異常將與InnerException屬性中的原始異常一起封裝到一個沒有用處的 XamlParseException對象中。
鍵盤事件
| 名稱 | 路由類型 | 說明 |
|---|---|---|
| PreviewKeyDown | 隧道 | 按下一個鍵時發生 |
| KeyDown | 冒泡 | 按下一個鍵時發生 |
| PreviewTextInput | 隧道 | 當按鍵完成並且元素正在接收文本輸入時發生 |
| TextInput | 冒泡 | 當鍵盤完成並且元素正在接收文本輸入時發生 |
| PreviewKeyUp | 隧道 | 釋放按鍵發生 |
| KeyUp | 冒泡 | 釋放按鍵發生 |
比如對TextBox的輸入提供驗證操作:
private void textBox_PreviewTextInput(object sender,TextCompositionEventArgs e)
{
short val;
// KeyConverter.ConverterToString()方法,Key.D9 和 Key.NumPad9 都返回字符串 "9"
if(!Int16.TryParse(e.Text,out val))
{
// 只允許輸入數字
e.Handled = true;
}
}
private void textBox_PreviewKeyDown(object sender, KeyEventArgs e)
{
if(e.Key == Key.Space)
{
// 有一些按鍵,比如空格,會繞過 PreviewTextInput
e.Handled = true;
}
}
鼠標拖放
拖放操作有兩個方面:源和目標。需要在某個為止調用 DragDrop.DoDragDrop()方法來初始化拖放操作,此時確定拖動操作的源,擱置希望移動的內容,並指明允許什么樣的拖放效果(復制、移動等)。
private void lb_MouseDown(object sender, MouseButtonEventArgs e)
{
Label lb1 = (Label)sender;
DragDrop.DoDragDrop(lb1, lb1.Content, DragDropEffects.Copy);
}
接收數據的元素需要將它的 AllowDrop 屬性設置為 true。
<Label Grid.Row="1" AllowDrop="True" Drop="lbTarget_Drop">To Here</Label>
如果希望有選擇的接收內容,可以處理 DragEnter事件。
private void lb2_DragEnter(object sender, DragEventArgs e)
{
if(e.Data.GetDataPresent(DataFromats.Text))
e.Effects = DragDropEffects.Copy;
else
e.Effects = DragDropEffects.None;
}
最后就可以檢索並處理數據了。
private void lb2_Drop(object sender, DragEventArgs e)
{
((Label)sender).Content = e.Data.GetData(DataFromats.Text);
}
我的公眾號

