预览事件

预览事件(也称为“隧道”事件)是从应用程序的根元素开始,向下沿着元素树传递到触发事件的元素的路由事件。 事件数据中的Source 是引发事件的元素。 并非所有事件方案都支持或需要预览事件。 本文介绍预览事件的位置以及应用程序或组件如何与其交互。 有关如何创建预览事件的信息,请参阅 如何创建自定义路由事件

先决条件

本文假设您具有有关路由事件的基本知识,并且已经阅读了 路由事件概述。 若要遵循本文中的示例,如果你熟悉可扩展应用程序标记语言(XAML),并且知道如何编写 Windows Presentation Foundation (WPF) 应用程序,则它很有帮助。

标记为已处理的预览事件

在将预览事件标记为已在事件数据中处理时需谨慎。 将预览事件标记为在引发它的元素以外的元素上进行处理,可以防止引发它的元素处理该事件。 有时将预览事件标记为已处理是有意的。 例如,复合控件可能会禁止单个组件引发的事件,并将其替换为由完整控件引发的事件。 控件的自定义事件可以根据组件状态关系提供自定义事件数据和触发器。

对于 输入事件,事件数据由每个事件的预览和非预览(冒泡)等效项共享。 如果使用预览事件类处理程序将输入事件标记为已处理,则通常不会调用浮泡输入事件的类处理程序。 或者,如果使用预览事件实例处理程序将事件标记为已处理,则通常不会调用冒泡输入事件的实例处理程序。 尽管可以将类和实例的处理程序配置为在事件被标记为已处理时仍然调用,但这样的处理程序配置并不常见。 有关类处理及其与预览事件的关系的详细信息,请参阅 将路由事件标记为已处理和类处理

注释

并非所有预览事件都是 隧道 事件。 例如,PreviewMouseLeftButtonDown输入事件遵循通过元素树的向下路由,但是由路由中的每个元素引发和重新引发UIElement直接路由事件。

通过控件绕过事件抑制

某些复合控件禁止组件级别的输入事件,以便将其替换为自定义的高级事件。 例如,在其 OnMouseLeftButtonDown 方法中,WPF ButtonBase 将冒泡输入事件 MouseLeftButtonDown 标记为已处理,并引发 Click 事件。 事件 MouseLeftButtonDown 及其事件数据仍沿元素树路由继续,但由于事件在事件数据中标记为 Handled 事件,因此仅调用配置为响应已处理事件的处理程序。

如果希望应用程序根节点的其他元素处理被标记为已处理的路由事件,则可以:

  • 通过调用UIElement.AddHandler(RoutedEvent, Delegate, Boolean)方法附加处理程序,并将参数handledEventsToo设置为true。 此方法要求在获取元素的对象引用后,在后台代码中附加事件处理程序。

  • 如果标记为已处理的事件是冒泡事件,则附加等效预览事件的处理程序(如果可用)。 例如,如果控件抑制了 MouseLeftButtonDown 事件,则可以为PreviewMouseLeftButtonDown 事件附加一个处理程序。 此方法仅适用于实现 隧道冒泡 路由策略和共享事件数据的基元素输入事件。

以下示例实现一个名为 componentWrapper 的自定义控件,其中包含 TextBox。 控件被添加到名为 outerStackPanelStackPanel 中。

<StackPanel Name="outerStackPanel"
    VerticalAlignment="Center"
    custom:ComponentWrapper.CustomKey="Handler_PrintEventInfo"
    TextBox.KeyDown="Handler_PrintEventInfo"
    TextBox.PreviewKeyDown="Handler_PrintEventInfo" >
    <custom:ComponentWrapper
        x:Name="componentWrapper"
        TextBox.KeyDown="ComponentWrapper_KeyDown"
        custom:ComponentWrapper.CustomKey="Handler_PrintEventInfo"
        HorizontalAlignment="Center">
        <TextBox Name="componentTextBox" Width="200" KeyDown="Handler_PrintEventInfo" />
    </custom:ComponentWrapper>
</StackPanel>

每当发生击键时,KeyDown控件componentWrapper将侦听其TextBox组件引发的冒泡事件。 在该事件上,componentWrapper 控件:

  1. KeyDown 冒泡路由事件标记为已处理以抑制它。 因此,只有在后台代码中配置以响应已处理KeyDown事件的outerStackPanel处理程序会被触发。 outerStackPanel XAML 中为 KeyDown 事件附加的处理程序没有被调用。

  2. 引发名为 CustomKey 的自定义冒泡路由事件,该事件触发 outerStackPanel 事件的 CustomKey 处理程序。

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        // Attach a handler on outerStackPanel that will be invoked by handled KeyDown events.
        outerStackPanel.AddHandler(KeyDownEvent, new RoutedEventHandler(Handler_PrintEventInfo), 
            handledEventsToo: true);
    }

    private void ComponentWrapper_KeyDown(object sender, System.Windows.Input.KeyEventArgs e)
    {
        Handler_PrintEventInfo(sender, e);

        Debug.WriteLine("KeyDown event marked as handled on componentWrapper.\r\n" +
            "CustomKey event raised on componentWrapper.");

        // Mark the event as handled.
        e.Handled = true;

        // Raise the custom click event.
        componentWrapper.RaiseCustomRoutedEvent();
    }

    private void Handler_PrintEventInfo(object sender, System.Windows.Input.KeyEventArgs e)
    {
        string senderName = ((FrameworkElement)sender).Name;
        string sourceName = ((FrameworkElement)e.Source).Name;
        string eventName = e.RoutedEvent.Name;
        string handledEventsToo = e.Handled ? " Parameter handledEventsToo set to true." : "";

        Debug.WriteLine($"Handler attached to {senderName} " +
            $"triggered by {eventName} event raised on {sourceName}.{handledEventsToo}");
    }

    private void Handler_PrintEventInfo(object sender, RoutedEventArgs e)
    {
        string senderName = ((FrameworkElement)sender).Name;
        string sourceName = ((FrameworkElement)e.Source).Name;
        string eventName = e.RoutedEvent.Name;
        string handledEventsToo = e.Handled ? " Parameter handledEventsToo set to true." : "";

        Debug.WriteLine($"Handler attached to {senderName} " +
            $"triggered by {eventName} event raised on {sourceName}.{handledEventsToo}");
    }

    // Debug output:
    //
    // Handler attached to outerStackPanel triggered by PreviewKeyDown event raised on componentTextBox.
    // Handler attached to componentTextBox triggered by KeyDown event raised on componentTextBox.
    // Handler attached to componentWrapper triggered by KeyDown event raised on componentTextBox.
    // KeyDown event marked as handled on componentWrapper.
    // CustomKey event raised on componentWrapper.
    // Handler attached to componentWrapper triggered by CustomKey event raised on componentWrapper.
    // Handler attached to outerStackPanel triggered by CustomKey event raised on componentWrapper.
    // Handler attached to outerStackPanel triggered by KeyDown event raised on componentTextBox. Parameter handledEventsToo set to true.
}

public class ComponentWrapper : StackPanel
{
    // Register a custom routed event using the Bubble routing strategy.
    public static readonly RoutedEvent CustomKeyEvent = 
        EventManager.RegisterRoutedEvent(
            name: "CustomKey",
            routingStrategy: RoutingStrategy.Bubble,
            handlerType: typeof(RoutedEventHandler),
            ownerType: typeof(ComponentWrapper));

    // Provide CLR accessors for assigning an event handler.
    public event RoutedEventHandler CustomKey
    {
        add { AddHandler(CustomKeyEvent, value); }
        remove { RemoveHandler(CustomKeyEvent, value); }
    }

    public void RaiseCustomRoutedEvent()
    {
        // Create a RoutedEventArgs instance.
        RoutedEventArgs routedEventArgs = new(routedEvent: CustomKeyEvent);

        // Raise the event, which will bubble up through the element tree.
        RaiseEvent(routedEventArgs);
    }
}
Partial Public Class MainWindow
        Inherits Window

        Public Sub New()
        InitializeComponent()

        ' Attach a handler on outerStackPanel that will be invoked by handled KeyDown events.
        outerStackPanel.[AddHandler](KeyDownEvent, New RoutedEventHandler(AddressOf Handler_PrintEventInfo),
                                     handledEventsToo:=True)
    End Sub

    Private Sub ComponentWrapper_KeyDown(sender As Object, e As KeyEventArgs)
        Handler_PrintEventInfo(sender, e)
        Debug.WriteLine("KeyDown event marked as handled on componentWrapper." &
                        vbCrLf & "CustomKey event raised on componentWrapper.")

        ' Mark the event as handled.
        e.Handled = True

        ' Raise the custom click event.
        componentWrapper.RaiseCustomRoutedEvent()
    End Sub

    Private Sub Handler_PrintEventInfo(sender As Object, e As KeyEventArgs)
        Dim senderName As String = CType(sender, FrameworkElement).Name
        Dim sourceName As String = CType(e.Source, FrameworkElement).Name
        Dim eventName As String = e.RoutedEvent.Name
        Dim handledEventsToo As String = If(e.Handled, " Parameter handledEventsToo set to true.", "")
        Debug.WriteLine($"Handler attached to {senderName} " &
                        $"triggered by {eventName} event raised on {sourceName}.{handledEventsToo}")
    End Sub

    Private Sub Handler_PrintEventInfo(sender As Object, e As RoutedEventArgs)
        Dim senderName As String = CType(sender, FrameworkElement).Name
        Dim sourceName As String = CType(e.Source, FrameworkElement).Name
        Dim eventName As String = e.RoutedEvent.Name
        Dim handledEventsToo As String = If(e.Handled, " Parameter handledEventsToo set to true.", "")
        Debug.WriteLine($"Handler attached to {senderName} " &
                        $"triggered by {eventName} event raised on {sourceName}.{handledEventsToo}")
    End Sub

    ' Debug output
    '
    ' Handler attached to outerStackPanel triggered by PreviewKeyDown event raised on componentTextBox.
    ' Handler attached to componentTextBox triggered by KeyDown event raised on componentTextBox.
    ' Handler attached to componentWrapper triggered by KeyDown event raised on componentTextBox.
    ' KeyDown event marked as handled on componentWrapper.
    ' CustomKey event raised on componentWrapper.
    ' Handler attached to componentWrapper triggered by CustomKey event raised on componentWrapper.
    ' Handler attached to outerStackPanel triggered by CustomKey event raised on componentWrapper.
    ' Handler attached to outerStackPanel triggered by KeyDown event raised on componentTextBox. Parameter handledEventsToo set to true.
End Class

    Public Class ComponentWrapper
        Inherits StackPanel

        ' Register a custom routed event with the Bubble routing strategy.
        Public Shared ReadOnly CustomKeyEvent As RoutedEvent =
            EventManager.RegisterRoutedEvent(
                name:="CustomKey",
                routingStrategy:=RoutingStrategy.Bubble,
                handlerType:=GetType(RoutedEventHandler),
                ownerType:=GetType(ComponentWrapper))

        ' Provide CLR accessors to support event handler assignment.
        Public Custom Event CustomKey As RoutedEventHandler

            AddHandler(value As RoutedEventHandler)
                [AddHandler](CustomKeyEvent, value)
            End AddHandler

            RemoveHandler(value As RoutedEventHandler)
                [RemoveHandler](CustomKeyEvent, value)
            End RemoveHandler

            RaiseEvent(sender As Object, e As RoutedEventArgs)
                [RaiseEvent](e)
            End RaiseEvent

        End Event

    Public Sub RaiseCustomRoutedEvent()
        ' Create a RoutedEventArgs instance & raise the event,
        ' which will bubble up through the element tree.
        Dim routedEventArgs As New RoutedEventArgs(routedEvent:=CustomKeyEvent)
            [RaiseEvent](routedEventArgs)
        End Sub
    End Class

该示例演示了两种解决方法,用于获取隐藏的 KeyDown 路由事件以调用附加到以下项的 outerStackPanel事件处理程序:

  • PreviewKeyDown 事件处理程序附加到 outerStackPanel. 由于预览输入路由事件发生在等效的冒泡路由事件之前,示例中的 PreviewKeyDown 处理程序会优先于 KeyDown 处理程序运行。这个处理程序通过共享的事件数据同时抑制预览和冒泡事件。

  • 使用UIElement.AddHandler(RoutedEvent, Delegate, Boolean)方法在代码隐藏中将事件处理程序附加到outerStackPanel,并将handledEventsToo参数设置为true

注释

将输入事件的预览版本或非预览版本标记为已处理,都是用于抑制控件组件引发事件的策略。 使用的方法取决于应用程序要求。

另请参阅