WPF_05_路由事件


路由事件

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);
}

我的公眾號


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM