天天看點

【WPF學習】第十四章 事件路由

  由上一章可知,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類的屬性

【WPF學習】第十四章 事件路由

 二、冒泡路由事件

  如下圖顯示了一個簡單視窗,該視窗示範了事件的冒泡過程。當單擊标簽中的一部時,在清單框中顯示事件發生的順序。圖中顯示了單擊标簽中的圖像之後視窗的情況。MouseUp事件傳遞了5級,在窗體中停止向上傳遞。

【WPF學習】第十四章 事件路由

  圖 冒泡的圖像單擊事件

  要建立該測試視窗,将元素層次結構中的圖像以及它上面的每個元素都關聯到同一個事件處理程式——名為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,進而在事件第一次發生時就終止事件的冒泡過程。是以,這時在清單框中就隻能看到第一個事件,如下圖所示:

【WPF學習】第十四章 事件路由

   因為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隧道事件。隧道路由事件總在冒泡路由事件之前被觸發。如下圖所示:

【WPF學習】第十四章 事件路由

  更有趣的是,如果将隧道路由事件标記為已處理過,那就不會發生冒泡路由事件。這是因為兩個事件共享RoutedEventArgs類的同一個執行個體。

  如果需要執行一些預處理(根據鍵盤上特定的鍵執行動作或過濾掉特定的滑鼠動作),隧道路由事件是非常有用的。

  如下面執行個體所示,該例測試PreviewKeyDown事件的隧道過程。當在文本框按下一個鍵時,事件首先在視窗觸發,然後再整個層次結構中向下傳遞。如果在任何位置将PreviewKeyDown事件标記為已處理過,就不會發生冒泡的KeyDown事件。

【WPF學習】第十四章 事件路由

 下面是所需的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();
        }
    }
}      

作者:Peter Luo

出處:https://www.cnblogs.com/Peter-Luo/

本文版權歸作者和部落格園共有,歡迎轉載,但必須給出原文連結,并保留此段聲明,否則保留追究法律責任的權利。

繼續閱讀