WPF_05_路由事件

wang2009發表於2021-11-03

路由事件

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 都是冒泡事件,當單擊標籤上的影像部分時:

  1. Image.MouseDown
  2. StackPanel.MouseDown
  3. 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);
}

我的公眾號

相關文章