【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事件。spa

  但當單擊上面這個特殊標籤中的圖像部分時,想想會發生什麼事情。很明顯,引起Image.MouseDown事件和Image.MouseUp事件是合情合理的。但若是但願採用相同的方式來處理標籤上的全部單擊事件,該怎麼辦呢?此時,無論單擊了圖像、某塊文本仍是標籤內的空白處,都應當使用相同的代碼進行相應。設計

  顯然,可爲每一個元素的MouseDown或MouseUp事件關聯同一個事件處理程序,但這樣會是標記變得雜亂無章且難以維護。WPF使用路由事件模型提供了一個更好的解決方案。指針

  路由事件實際上如下列三種方式出現:code

  •   與普通.NET事件相似的直接路由事件(direct event)。它們源於一個元素,不傳遞給其餘元素。例如,MouseEnter事件(當鼠標指針移到元素上時發生)是直接路由事件。
  •   在包含層次中向上傳遞的冒泡路由事件(bubbling event)。例如,MouseDown事件就是冒泡路由事件。該事件首先由被單擊的元素引起,接下來被該元素的父元素引起,而後被父元素的父元素引起,依此類推,直到WPF到達元素樹的頂部爲止。
  •   在包含層次中向下傳遞的隧道路由事件(tunneling event)。隧道路由事件在事件到達恰當的控件以前爲預覽事件(甚至終止事件)提供了機會。例如,經過PreviewKeyDown事件可截獲是否按下了某個鍵。首先在窗口級別上,而後是更具體的容器,直至到達當按下鍵時具備焦點的元素。

  當使用EventManager.RegisterEvent()方法註冊路由事件時,須要傳遞一個RoutingStrategy枚舉值,該值用於指示但願應用於事件的事件行爲。xml

  MouseUp事件和MouseDown事件都是冒泡路由事件,所以如今能夠肯定在上面特殊的標籤示例中會發生什麼事情。當單擊標籤上的圖像部分時,按一下順序觸發MouseDown事件:對象

  (1)Image.MouseDown事件blog

  (2)StackPanel.MouseDown事件繼承

  (3)Label.MouseDown事件事件

  爲標籤引起了MouseDown事件後,該事件會傳遞到下一個控件(在本例中是位於窗口中的Grid控件),而後傳遞到Grid控件的父元素(窗口)。窗口時整個層次中的頂級元素,而且是事件冒泡順序的最後一站,它是處理冒泡路由事件(如MouseDown事件)的最後機會。若是用戶釋放了鼠標按鍵,就會按相同的順序觸發MouseUp事件。

  沒有限制要在某個位置處理冒泡路由事件。實際上,徹底可在任意層次上處理MouseDown事件或MouseUp事件。但一般選擇最合適的事件路由層次完成這一任務。

1、RoutedEventArgs類

  在處理冒泡路由事件時,sender參數提供了對整個鏈條上最後那個連接的引用。例如,在上面的示例中,若是事件在處理以前,從圖像向上冒泡到標籤,sender參數就會引用標籤對象。

  有些狀況下,可能但願肯定事件最初發生的位置。可從RoutedEventArgs類的屬性(以下表所示)得到這一信息以及其餘細節。因爲全部WPF事件參數類繼承自RoutedEventArgs,所以任何事件處理程序均可以使用這些屬性。

表 RoutedEventArgs類的屬性

 

 2、冒泡路由事件

  以下圖顯示了一個簡單窗口,該窗口演示了事件的冒泡過程。當單擊標籤中的一部時,在列表框中顯示事件發生的順序。圖中顯示了單擊標籤中的圖像以後窗口的狀況。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事件繼續傳遞。

3、處理掛起的事件

  有一種方法可接受被標記處理過的事件。不是經過XAML關聯事件處理程序,而是必須使用前面介紹的AddHandler()方法。AddHandler()方法提供了一個重載版本,該版本能夠接收一個Boolean值做爲它的第三個參數。若是將該參數設置爲true,那麼即便設置了Handled標記,也將接收到事件:

cmdClear.AddHandler(UIElement.MouseUpEvent,new MouseButtonEventHandler(cmdClear_MouseUp),true);

  這一般並非正確的設計決策。爲防止可能形成的困惑,按鈕被設計爲會掛起MouseUp事件。畢竟,可採用多種方式使用鍵盤「單擊」按鈕,這是Windows中很是廣泛的約定。若是爲按鈕錯誤地處理了MouseUp事件,而沒有處理Click事件,那麼事件處理代碼就只能對鼠標單擊作出相應,而不能對相應的鍵盤操做作出相應。

4、附加事件

  上面這個有趣的標籤示例是一個很是簡單的事件冒泡示例,由於全部的元素都支持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屬性。在此不列舉出具體實例。

5、隧道路由事件

  隨着路由事件的工做方式和冒泡路由事件相同,當方向相反。例如,若是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();
        }
    }
}
相關文章
相關標籤/搜索