WPF-事件

203 阅读5分钟

1.两棵树

学习WPF事件之前先必须了解两棵树:Logical Tree(逻辑元素树) 和 Visual tree(可视元素树)

XAML文档是由XAML 对象元素嵌套构成,除了根节点外每个节点都可以存在子元素父元素已经兄弟元素,从而形成元素树。Logical Tree 是完全由布局控件和控件构成的,换句话说,每个节点不是布局控件就是控件。那么什么是VisualTree呢?我们知道,如果把一个树叶放在显微镜下观察,你会发现这片叶子也像一棵树----有自己的基部并向上生长出多级分叉。在WPF的Logic Tree上,扮演叶子的一般都是控件。如果我们把WPF中的控件也放在显微镜下观察,你会发现WPF控件本身也是一颗由更细微级别的组件(他们不是控件,而是一些可视化组件,派生至Visual类)组成的树。如果把Logic Tree延伸至Template组件级别,我们得到的就是Visual Tree。实际工作中我们大多数情况下都在和Logic Tree打交道,有时候为了实现一些棘手的功能会向Visual Tree求助。

目前你可以把Template理解为控件的骨架,我们甚至在保证控件功能不丢失的情况下为控件换一副新骨架,让它更漂亮。

如果想在Logic Tree上导航或者查找元素,可以借助LogicTreeHelper类的static方法来实现:

BringIntoView:把选定元素带进用户可视区域,经常用于可滚动的视图。

FindLogicNode:按给定名称(Name属性)查找元素,包括子集树上的元素。

GetChildren:获取所有直接子集元素。

GetParent:获取直接父级元素。

2.路由事件

功能定义:路由事件是一种可以针对元素树中的多个侦听器(而不是仅针对引发该事件的对象)调用处理程序的事件。

从该定义中我们可以了解一个事件可以触发多个侦听器,那么这多个侦听器都和哪些XAML对象元素有关呢,这就要从上面的两个树说起了,被点击元素的所有祖先元素的侦听器都会被触发。

简单元素树:

<Border Height="50" Width="300" BorderBrush="Gray" BorderThickness="1" Button.Click="BorderClickHandler">
    <StackPanel Background="#ffffff" Orientation="Horizontal" Button.Click="StackPanelClickHandler">
        <Button Button.Click="ButtonClickHandler" Name="YesButton" Width="100" >Yes</Button>
        <Button Name="NoButton" Width="100" >No</Button>
        <Button Name="CancelButton" Width="100" >Cancel</Button>
    </StackPanel>
</Border>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    public void ButtonClickHandler(object sender, RoutedEventArgs e)
    {
        MessageBox.Show("Yes Button");
    }
    public void StackPanelClickHandler(object sender, RoutedEventArgs e)
    {
        MessageBox.Show("StackPanel");
    }

    public void BorderClickHandler(object sender, RoutedEventArgs e)
    {
        MessageBox.Show("Border");
    }
}

sss777.gif

在这个简单元素树中,如果我们点击Yes按钮,那么事件的源就是Yes按钮,第一个触发Yes按钮的侦听器,如果该侦听器未绑定处理事件,则该事件将向上冒泡到Yes按钮元素树中的父级StackPanel,事件可能会冒泡到Border,然后再冒泡到元素树的页面根(未在代码中显示),这个过程就是事件路由。

那么当一个路由事件被激发后是沿着LogicTree传递还是沿VisualTree传递呢?答案是VisualTree---只有这样,“藏”在Template里的控件才能把消息送出来。

WPF 中的各种控件都有丰富的内容模型。 例如,可以将图像置于内 Button,这会有效地扩展按钮的可视化树。 但是,添加的图像不得中断导致按钮响应 Click 其内容的命中测试行为,即使用户单击了在技术上包含图像的部分像素也是如此。

  1. 路由策略

路由策略共有三种;

  • 浮升:浮升就是我们上面简单元素树例子中所讲到的,首先调用事件源上的事件处理程序。 路由事件随后会路由到后续的父级元素,直到到达元素树的根。 大多数路由事件都使用浮升路由策略。

  • 直接:只有源元素本身才有机会调用处理程序以进行响应。 这类似于 Windows 窗体用于事件的 "路由"。

  • 隧道:这与浮升刚好相反,最初将调用元素树的根处的事件处理程序。 随后,路由事件将朝着路由事件的源节点元素(即引发路由事件的元素)方向,沿路由线路传播到后续的子元素。

4.“已处理”概念

所谓已处理就相当于JavaScript中的阻止冒泡,方法是将的 Handled值设置为 true

还是上面的代码,我们将Yes按钮事件的Handled设置为true:

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

        public void ButtonClickHandler(object sender, RoutedEventArgs e)
        {
            MessageBox.Show("Yes Button");
            e.Handled = true;
        }
        public void StackPanelClickHandler(object sender, RoutedEventArgs e)
        {
            MessageBox.Show("StackPanel");
        }

        public void BorderClickHandler(object sender, RoutedEventArgs e)
        {
            MessageBox.Show("Border");
        }
    }

sss888777.gif

5.输入事件

WPF 平台中的路由事件的一个常见应用是用于输入事件。 在 WPF 中,隧道路由事件名称按约定以 "预览" 为前缀。 输入事件通常成对出现,一个是浮升事件,另一个是隧道事件。 例如, KeyDown事件和 PreviewKeyDown 事件具有相同的签名,前者是冒泡输入事件,后者是隧道输入事件。

实现了成对的 WPF 输入事件,以便输入中的单个用户操作(如按下鼠标按钮)将按顺序同时引发这一对的路由事件。 首先引发隧道事件并沿路由传播。 然后引发浮升事件并沿路由传播。

<Border Height="50" Width="300" BorderBrush="Gray" BorderThickness="1" Button.KeyDown="BorderKeyDownHandler" Button.PreviewKeyDown="BorderPreviewKeyDownHandler">
    <StackPanel Background="#ffffff" Orientation="Horizontal" Button.KeyDown="StackPanelKeyDownHandler" Button.PreviewKeyDown="StackPanelPreviewKeyDownHandler">
        <Button Name="YesButton" Width="100"   Button.KeyDown="ButtonKeyDownHandler" Button.PreviewKeyDown="ButtonPreviewKeyDownHandler">Yes</Button>
        <Button Name="NoButton" Width="100" >No</Button>
        <Button Name="CancelButton" Width="100" >Cancel</Button>
    </StackPanel>
</Border>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    public void ButtonKeyDownHandler(object sender, RoutedEventArgs e)
    {
        MessageBox.Show("Button KeyDown");
    }
    public void StackPanelKeyDownHandler(object sender, RoutedEventArgs e)
    {
        MessageBox.Show("StackPanel KeyDown");
    }

    public void BorderKeyDownHandler(object sender, RoutedEventArgs e)
    {
        MessageBox.Show("Border KeyDown");
    }

    private void ButtonPreviewKeyDownHandler(object sender, KeyEventArgs e)
    {
        MessageBox.Show("Button PreviewKeyDown");
    }
    private void StackPanelPreviewKeyDownHandler(object sender, KeyEventArgs e)
    {
        MessageBox.Show("StackPanel PreviewKeyDown");
    }
    private void BorderPreviewKeyDownHandler(object sender, KeyEventArgs e)
    {
        MessageBox.Show("Border PreviewKeyDown");
    }
}

ssskeydown.gif

6.EventSetter 和 EventTrigger

在样式中,可以通过使用 EventSetter 在标记中包含一些预声明的 XAML 事件处理语法。 在应用样式时,所引用的处理程序会添加到带样式的实例中。 只能为路由事件声明EventSetter 。

<Window.Resources>
    <Style TargetType="{x:Type Button}">
        <EventSetter Event="Click" Handler="Button_Click"/>
    </Style>
</Window.Resources>

<Border Height="50" Width="300" BorderBrush="Gray" BorderThickness="1">
    <StackPanel Background="#ffffff" Orientation="Horizontal" HorizontalAlignment="Center">
        <Button>Click me</Button>
    </StackPanel>
</Border>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    public void Button_Click(object sender, RoutedEventArgs e)
    {
        MessageBox.Show("click");
    }
}

ssskeydclick.gif

EventTrigger 与 一 EventSetter样,只能将路由事件用于 EventTrigger。EventTrigger还可以在页面级元素上声明为集合的一部分,或在ControlTemplate中声明。