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");
}
}
在这个简单元素树中,如果我们点击Yes按钮,那么事件的源就是Yes按钮,第一个触发Yes按钮的侦听器,如果该侦听器未绑定处理事件,则该事件将向上冒泡到Yes按钮元素树中的父级StackPanel,事件可能会冒泡到Border,然后再冒泡到元素树的页面根(未在代码中显示),这个过程就是事件路由。
那么当一个路由事件被激发后是沿着LogicTree传递还是沿VisualTree传递呢?答案是VisualTree---只有这样,“藏”在Template里的控件才能把消息送出来。
WPF 中的各种控件都有丰富的内容模型。 例如,可以将图像置于内 Button,这会有效地扩展按钮的可视化树。 但是,添加的图像不得中断导致按钮响应 Click 其内容的命中测试行为,即使用户单击了在技术上包含图像的部分像素也是如此。
- 路由策略
路由策略共有三种;
-
浮升:浮升就是我们上面简单元素树例子中所讲到的,首先调用事件源上的事件处理程序。 路由事件随后会路由到后续的父级元素,直到到达元素树的根。 大多数路由事件都使用浮升路由策略。
-
直接:只有源元素本身才有机会调用处理程序以进行响应。 这类似于 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");
}
}
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");
}
}
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");
}
}
EventTrigger 与 一 EventSetter样,只能将路由事件用于 EventTrigger。EventTrigger还可以在页面级元素上声明为集合的一部分,或在ControlTemplate中声明。