第5章:路由事件

本章目标

  • 理解路由事件

  • 掌握键盘输入事件

  • 掌握鼠标输入事件

  • 掌握多点触控输入事件

理解路由事件

​ 每个.NET 开发人员都熟悉“事件”的思想——-当有意义的事情发生时,由对象(如WPF元素)发送的用于通知代码的消息。WPF 通过事件路由(event routing)的概念增强了.NET 事件模型。事件路由允许源自某个元素的事件由另一个元素引发。例如,使用事件路由,来自工具栏技钮的单击事件可在被代码处理之前上传到工具栏,然后上传到包含工具栏的窗口。

​ 事件路由为在最合适的位置编写紧凑的、组织良好的用于处理事件的代码提供了灵活性。
要使用 WPF 内容模型,事件路由也是必需的,内容模型允许使用许多不同的元素构建简单元素(如按钮),并且这些元素都拥有自己独立的事件集合。

定义、注册和封装路由事件

​ WPF 事件模型和WPF属性模型非常类似。与依赖项属性一样,路由事件由只读的静态字段表示,在静态构造函数中注册,并通过标准的.NET 事件定义进行封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
using System.ComponentModel;
using System.Security;
using System.Windows.Input;
using System.Windows.Media;
using MS.Internal.Commands;
using MS.Internal.KnownBoxes;
using MS.Internal.PresentationFramework;

namespace System.Windows.Controls.Primitives;

[DefaultEvent("Click")]
[Localizability(LocalizationCategory.Button)]
public abstract class ButtonBase : ContentControl, ICommandSource
{
//单击事件
public static readonly RoutedEvent ClickEvent;

//静态构造函数
static ButtonBase()
{
Click = EventManager.RegisterRoutedEvent("Click", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ButtonBase));
//其他内容略

}

[Category("Behavior")]
public event RoutedEventHandler Click
{
add
{
AddHandler(ClickEvent, value);
}
remove
{
RemoveHandler(ClickEvent, value);
}
}
}

​ 依赖项属性是使用 DependencyProperty Register()方法注册的,而路由事件是使用EventManager.RegisteiRoutedEvent()方法注册的。当注册事件时,需要指定事件的名称、路由类型(稍后介绍与路由类型相关的更多细节)、定义事件处理程序语法的委托(在该例中是RoutedEventHandler)以及拥有事件的类(在该例中是 ButtonBase 类)。

​ 通常,路由事件通过普通的.NET 事件进行封装,从而使所有.NET 语言都能访问它们。事件封装器可使用 AddHandler()和 RemoveHandler()方法添加和删除已注册的调用程序,这两个方法都在 FrameworkElement 基类中定义,并被每个 WPF 元素继承。

共享路由事件

​ 与依赖项属性一样,可在类之间共享路由事件的定义。例如,UIElement(该类是所有普通WPF元素的起点)和ContentElemeat(该类是所有内容元素的起点,内容元素是可以被放入流文档中的单独内容片段)这两个基类都使用了 Mouselp 事件。MouseUp 事件是由 SystemWindows Lnput.Mouse 类定义的。UIElement 类和 ContentElement类只通过 Routed- Event. AddOwner()方法重用MouseUp 事件:

引发路由事件

​ 与所有事件类似,定义类需要在一些情况下引发事件。到底在哪里发生是实现细节。然而,重要的细节是事件不是通过传统的.NET 事件封装器引发的,而是使用 RaiseEvent() 方法引发事件,所有元素都从 UIElement 类继承了该方法。下面是来自 ButtonBase 类深层的代码:

​ 所有WPF事件都为事件签名使用熟悉的.NET 约定。每个事件处理程序的第一个参数(sender)都提供引发该事件的对象的引用。第二参数是EventArgs 对象,该对象与其他所有可能很重要的附加细节绑定在一起。例如,MouseUP 事件提供了一个 MouseEventArgs 对象,用于指示当事件发生时按下了哪些鼠标键:

1
2
3
4
private void img_MouseUp(object sender, MouseButtonEventArgs e)
{

}

​ 在WPF中,如果事件不需要传递任何额外细节,可使用 RoutedEventArgs 类,该类包含了有关如何传递事件的一些细节。如果事件确实需要传递额外的信息,那么需要使用更特殊的继承自 RoutedEventArgs 的对象(如上面示例中的 MouseButtonEventArgs)。因为每个 WPF 事件参数类都继承在 RoutedEventArgs 类,所以每个 WPF 事件处理程序都可访问与事件路由相关的信息。

处理路由事件

​ 正如第2章所述,可使用多种方法关联事件处理程序。最常用的方法是为 XAML 标记添加事件特性。事件特性技照想要处理的事件命名,它的值就是事件处理程序方法的名称。下面的示例使用这一语法将Image 对象的Mouselp 事件连接到名为img_MouseUp 的事件处理程序:

1
<Image Source="happyface.jpg" Stretch="None" Name="img" MouseUp="img_MouseUp" />

​ 通常约定以 “元素名_事件名”的形式命名事件处理程序方法,但这不是必的。如果没有为元素定义名称(可能是因为不需要在代码的任何地方与元素进行交互),可考虑使用以下形式的事件名称:

1
<Button Click="btn_Click">Ok</Button>

​ 也可以使用代码连接事件。下面的代码和上面给出的XAML 标记具有相同的效果:

1
img.MouseUp +=new MouseButtonEventHandler(img_MouseUp);

​ 上面的代码创建了一个针对该事件具有正确签名的委托对象(在该例中,是 MouseButtonEventHandler 委托的实例),并将该委托指向img_MouseUp() 方法。然后将该委托添加到img.MouseUp 事件的已注册的事件处理程序列表中。C#还允许使用更精简的语法,隐式的创建合适的委托对象:

1
img.MouseUp +=img_MouseUp;

事件路由

​ 在WPF中,许多控件都是内容控件,而内容控件可包含任何类型以及大量的嵌套内容。例如,可以构建包含图形的按钮,创建混合了文本和图片内容的标签,或者为了实现滚动或折叠的显示效果而在特定容器中放置内容。甚至可以多次重复嵌套,直至达到所希望的层次深度。

​ 这种可以任意嵌套的能力也带来了一个有趣问题。例如,假设有如下标签,其中包含一个StackPanel 面板,该面板又包含了两块文本和一副图像:

1
2
3
4
5
6
7
8
9
10
11
<Label BorderBrush="Black" BorderThickness="1">
<StackPanel>
<TextBlock Margin="3">
Image and text label
</TextBlock>
<Image Source="happyface.jpg" Stretch="None" />
<TextBlock Margin="3">
Courtesy of the StackPanel
</TextBlock>
</StackPanel>
</Label>

​ 放在 WPF 窗口中的所有要素都在一定层次上继承自 UIElement 类,包括 Label、 StackPanel、TextBlock 和Image。UIElement 定义了一些核心事件。例如,每个维承自 UIElenent 的类部提供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 事件

RoutedEventArgs 类

​ 在处理冒泡路由事件时,sender 参数提供了对整个链条上最后那个链接的引用。例如,在上面的示例中,如果事件在处理之前,从图像向上冒泡到标签,sender 参数就会引用标签对象。

​ 有些情况下,可能希望确定事件最初发生的位置。可从 RouteeEventArgs 类的属性(如下表所示)获得这一信息以及其他细节。由于所有 WPF 事件参数类继承自 RoutedEventArgs ,因此任何事件处理程序都可以使用这些属性。

名称 说明
Source 指示引发了事件的对象。
OriginalSource 指示最初是什么对象引发了事件。通常与Source 属性值相同,但在某些特定情况下,该属性指向对象树中更深的层次。
RoutedEvent 通过事件处理程序为触发的事件提供 RoutedEvent 对象(如静态的 UIElement.MouseUpEvent 对象)。
Handled 该属性允许终止事件的冒泡或隧道过程。如果该控件将handled 属性设为true,那么事件就不会继续传递,也不会再为其他任何元素引发该事件。

冒泡路由事件

​ 以下案例演示了事件冒泡的过程,当单击标签中的一部分时,在列表中显示事件发生的顺序。

下面是所需的XAML标记:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<Window x:Class="Wpf教学案例.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Wpf教学案例"
mc:Ignorable="d"
xmlns:sys="clr-namespa"
Title="Window1" Height="450" Width="800"
MouseUp="SomethingClicked">
<Grid Margin="3" MouseUp="SomethingClicked">

<!--行设置-->
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>

<Label Margin="5" Grid.Row="0" HorizontalAlignment="Left" Background="AliceBlue"
BorderBrush="Black" BorderThickness="1"
MouseUp="SomethingClicked">
<StackPanel MouseUp="SomethingClicked">
<TextBlock Margin="3" MouseUp="SomethingClicked">
Text Block -1
</TextBlock>
<Image Source="happyface.jpg" Stretch="None" MouseUp="SomethingClicked" Width="600" Height="350"/>
<TextBlock Margin="3" MouseUp="SomethingClicked">
Text Block -2
</TextBlock>
</StackPanel>
</Label>

<ListBox Grid.Row="1" Margin="3" Name="lstMessages"></ListBox>

<CheckBox Grid.Row="2" Margin="3" Name="chkHandle">是否终止事件冒泡</CheckBox>

<Button Grid.Row="3" Margin="3" Padding="3" HorizontalAlignment="Right" Name="btnClear" Click="btnClear_Click" >Clear List</Button>
</Grid>
</Window>

下面是业务逻辑的C#代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;

namespace Wpf教学案例
{
/// <summary>
/// Window1.xaml 的交互逻辑
/// </summary>
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}

/// <summary>
/// 事件数量
/// </summary>
int eventCounter = 0;

/// <summary>
/// 事件处理
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void SomethingClicked(object sender, RoutedEventArgs e)
{
eventCounter++;
string message = "#" + eventCounter.ToString() + ":\r\n" +
"sender:" + sender.ToString() + ":\r\n" +
"source:" + e.Source + ":\r\n" +
"orignal source:" + e.OriginalSource;

this.lstMessages.Items.Add(message);

e.Handled = (bool)chkHandle.IsChecked;
}

/// <summary>
/// 单击事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnClear_Click(object sender, RoutedEventArgs e)
{
this.eventCounter = 0;
this.lstMessages.Items.Clear();
}
}
}

提示

大多数WPF 元素没有提供Click 事件,而是提供了更直接的 MouseDown 和 MouseUp 事件. Click 事件专用于基于按钮的控件。

处理挂起的事件

​ 有一种方法可接收被标记为处理过的事件,不是通过XAML 关联事件处理程序,而是必须使用前面介绍的AddHandler() 方法。AddHandler() 方法提供了一个重载版本,该版本可以接收一个 Boolean 值作为它的第三个参数。如果将该参数设置为true, 那么即使设置了Handled 标志,也将接收到事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 public Window1()
{
InitializeComponent();

//添加事件处理程序
this.btnClear.AddHandler(UIElement.MouseUpEvent,new MouseButtonEventHandler(btnClear_MouseUp),true);
}

/// <summary>
/// 事件处理程序
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnClear_MouseUp(object sender, MouseButtonEventArgs e)
{
this.lstMessages.Items.Add("执行了MouseUp!");
}

附加事件

​ 假设在 StackPanel 面板中封装了一堆按钮,并希望在一个事件处理程序。但Click 事件支持事件冒泡,从而提供了一种更好的选择。可通过处理更高层次元素的 Click 事件(如包含按钮的StackPanel 面板)来处理所有按钮的Click 事件。看似前线的代码却不能工作

1
2
3
4
5
<StackPanel Click="btn_Click"  Margin="5">
<Button x:Name="btn1">btn1</Button>
<Button x:Name="btn2">btn1</Button>
<Button x:Name="btn3">btn1</Button>
</StackPanel>

​ 问题在于 StackPanel 面板没有 Click 事件,所以 XAML 解析器会将其解释成错误。解决方案是以 “类名.事件名” 的形式使用不同的关联事件语法,下面是更正后的示例。

1
2
3
4
5
6
7
<StackPanel Button.Click="btn_Click"  Margin="5">
<Button x:Name="btn1">btn1</Button>
<Button x:Name="btn2">btn1</Button>
<Button x:Name="btn3">btn1</Button>

<Label Name="lblMsg" />
</StackPanel>

​ 以下是对应C#代码:

1
2
3
4
5
6
7
8
9
/// <summary>
/// 按钮单击事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btn_Click(object sender, RoutedEventArgs e)
{
this.lblMsg.Content =e.OriginalSource.ToString();
}

隧道路由事件

​ 隧道路由事件的工作方式和冒泡路由事件相同,但方向相反。根据.NET约定,隧道路由事件总是以单词Priview开头(如PreviewKeyDown)。而且,WPF通常成对地定义冒泡路由事件和隧道路由事件。这意味着如果发现冒泡的MouseUp事件,就还可以找到PreviewMouseUp隧道事件。隧道路由事件总在冒泡路由事件之前被触发。

注意:

如果将隧道路由事件标记为已处理过,那就不会发生冒泡路由事件了。这是因为两个事件共享RuotedEventArgs类的同一个实例。

应用场景:

​ 如果需要执行一些预处理(例如,根据键盘上特定的键执行动作或过滤掉特定的鼠标动作),隧道路由事件是非常有用的。

​ 下面通过PreviewKeyDown事件,实例演示事件的隧道路由过程。当在文本框中按下一个键时,事件首先在窗口触发,然后在整个层次结构中向下传递。如果在传递的任意位置将PreviewKeyDown事件标记为已处理过,就不会发生冒泡的KeyDown事件。

XAML标记代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<Window x:Class="Wpf教学案例.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Wpf教学案例"
mc:Ignorable="d"
xmlns:sys="clr-namespa"
Title="Window1" Height="450" Width="800"
PreviewKeyDown="SomeKeyPressed">

<Grid Margin="3" >
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>

<Label PreviewKeyDown="SomeKeyPressed" Margin="5" Background="AliceBlue" BorderBrush="Black" BorderThickness="1" HorizontalContentAlignment="Stretch">
<StackPanel PreviewKeyDown="SomeKeyPressed">
<TextBlock PreviewKeyDown="SomeKeyPressed" Margin="3" HorizontalAlignment="Center"> Image and text label</TextBlock>
<Image PreviewKeyDown="SomeKeyPressed" Source="happyface.png" Stretch="None"/>
<DockPanel Margin="0,5,0,0" PreviewKeyDown="SomeKeyPressed">
<TextBlock Margin="3"> Type here:</TextBlock>
<TextBox PreviewKeyDown="SomeKeyPressed" KeyDown="SomeKeyPressed"></TextBox>
</DockPanel>
</StackPanel>
</Label>

<ListBox x:Name="lstMessages" Margin="5" Grid.Row="1"></ListBox>
<CheckBox x:Name="chkHandle" Margin="5" Grid.Row="2">Handle first event</CheckBox>
<Button Click="cmdClear_Click" Grid.Row="3" HorizontalAlignment="Right" Margin="5" Padding="3">Clear List</Button>
</Grid>

</Window>

cs后台代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;

namespace Wpf教学案例
{
/// <summary>
/// Window1.xaml 的交互逻辑
/// </summary>
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}

protected int eventCounter = 0;

private void SomeKeyPressed(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();
}
}
}

演示效果如下:

注意:

上图中的第6项对于TextBox而言在处理完PreviewKeyDown隧道路由事件后又引发KeyDown冒泡路由事件。

​ 当勾选Handle firste vent复选框后,再在文本框内按下键时显示效果如下:

WPF事件

​ WPF的事件包括生命周期事件、输入事件、路由事件和行为等方面。

生命周期事件

​ 在WPF中,窗体和控件具有生命周期事件,这些事件允许在不同的阶段执行代码。以下是一些常见的生命周期事件:

所有元素的生命周期事件:

事件 描述
Initialized 在初始化期间引发。
Loaded 在控件加载到窗体上时引发。
Unloaded 在控件从窗体中卸载时引发。

Windows 类的生命周期事件:

事件 描述
SourceInitialized 这个事件发生在WPF窗体的资源初始化完毕,并且可以通过WindowInteropHelper获得该窗体的句柄用来与Win32交互。
ContentRendered 当Window的内容(例如页面)完成渲染时触发。
Activated 当Window获得焦点成为活动窗口时触发。
Deactivated 当Window失去焦点不再是活动窗口时触发。
Closing 在Window尝试关闭时触发,允许取消关闭或执行清理操作。
Closed 在Window完全关闭并且不再可见时触发。

xaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<Window x:Class="Wpf教学案例.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Wpf教学案例"
mc:Ignorable="d"
xmlns:sys="clr-namespa"
Title="Window1" Height="450" Width="800"
Loaded="Window_Loaded" Activated="Window_Activated" Deactivated="Window_Deactivated"
Closing="Window_Closing" Unloaded="Window_Unloaded">

<Grid>
</Grid>

</Window>

cs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;

namespace Wpf教学案例
{
/// <summary>
/// Window1.xaml 的交互逻辑
/// </summary>
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}

private void Window_Loaded(object sender, RoutedEventArgs e)
{
// 在窗体加载时执行的代码

}

private void Window_Activated(object sender, EventArgs e)
{
// 在窗体激活时执行的代码
}

private void Window_Deactivated(object sender, EventArgs e)
{
// 在窗体非激活状态时执行的代码
}

private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
// 在窗体关闭时执行的代码
}

private void Window_Unloaded(object sender, RoutedEventArgs e)
{
// 在窗体卸载时执行的代码
}
}
}

输入事件

​ 输入事件是当用户使用某些种类的外设硬件进行交互时发生的事件,例如鼠标、键盘、手写笔或多点触控屏。输入事件可通过继承在 InputEventArgs 的自定义事件参数类型传递额外的信息。

键盘输入

​ 当用户按下键盘上的一个键时,就会发生一些列事件。

名称 路由类型 说明
Preview kyDown 隧道 当按下一个键时发生
KeyDown 冒泡 当按下一个键时发生
PreviewTextInput 隧道 当按下一个键且产生文本时发生
TextInput 冒泡 当按下一个键且产生文本时发生
PreviewKeyUp 隧道 当释放一个按键时发生
KeyUp 冒泡 当释放一个按键时发生

处理按键事件

​ 以下案例在一个文本框中监视所有可能得键盘事件,并在发生时给出报告。

xaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<Window x:Class="Wpf教学案例.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Wpf教学案例"
mc:Ignorable="d"
xmlns:sys="clr-namespa"
Title="Window1" Height="450" Width="800">

<Grid Margin="3" >
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>

<Label Grid.Row="0" Margin="5" Background="AliceBlue" BorderBrush="Black" BorderThickness="1" HorizontalContentAlignment="Stretch">
<StackPanel>

<DockPanel Margin="0,5,0,0">
<TextBlock Margin="3"> Type here:</TextBlock>
<TextBox Name="txtContent"
PreviewKeyDown="KeyEvent"
KeyDown="KeyEvent"
PreviewTextInput="TextInput"
TextInput="TextInput"
PreviewKeyUp="KeyEvent"
KeyUp="KeyEvent"></TextBox>
</DockPanel>
</StackPanel>
</Label>

<ListBox Grid.Row="1" x:Name="lstMessages" Margin="5"></ListBox>
<CheckBox Grid.Row="2" x:Name="chkIgnoreRepeat" Margin="5" >Ignoe Repeated keys</CheckBox>
<Button Grid.Row="3" Click="Button_Click" HorizontalAlignment="Right" Margin="5" Padding="3">Clear List</Button>
</Grid>

</Window>

cs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;

namespace Wpf教学案例
{
/// <summary>
/// Window1.xaml 的交互逻辑
/// </summary>
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}



private void KeyEvent(object sender, KeyEventArgs e)
{
//是否或略重复键
if ((bool)chkIgnoreRepeat.IsChecked && e.IsRepeat) return;

string message = "Event:" + e.RoutedEvent + " " + " key:" + e.Key;

this.lstMessages.Items.Add(message);
}

private new void TextInput(object sender, TextCompositionEventArgs e)
{

string message = "Event:" + e.RoutedEvent + " " + " text:" + e.Text;

this.lstMessages.Items.Add(message);
}

private void Button_Click(object sender, RoutedEventArgs e)
{
this.txtContent.Text = null;
this.lstMessages.Items.Clear();
}
}
}

焦点

​ 在Windows世界中,用户每次只能使用一个控件。当前接收用户按键的控件是具有焦点的控件。有时,有焦点的控件外观有些不同。例如,WPF按钮使用蓝色阴影显示它具有的焦点。为让控件能接受焦点,必须将Focusable 属性设置为 true,这是所有控件的默认值。

​ 为将焦点从一个元素移到另一个元素,用户可单击鼠标或使用Tab 键和方向键。如果希望获得控制使用 Tab 键转移焦点顺序的功能,可按数字顺序设置每个控件的 Tabindex 属性。 Tabindex 属性为0的控件首先获得焦点,然后是次高的 TabIndex 值。如果多个元素具有相同的 TabIndex 值,WPF 就使用自动 Tab 顺序。

​ TabIndex 属性是在 Control 类中定义的,在该类中还定义了 IsTabStop 属性。可通过 IsTabStop 属性设置为false 来阻止控件被包含进 Tab 键焦点顺序。

获取键盘状态

​ 当发生按键事件时,经常需要知道更多信息,而不仅要知道按下的是哪个键。而且确定其它键是否同时被按下了也非常重要。这意味着可能需要检查其他键的状态,特别是 Shift、Ctrl 和Alt 等修饰键。

​ 对于键盘事件(PreviewKeyDown、KeyDown、PreviewKeyUp 和 KeyUp), 获取这些信息比较容易。首先, KeyEventArgs 对象包含 KeyStates 属性,该属性反映触发事件的键的属性。更有用的是, KeyboardDevice 属性为键盘上的所有键提供了相同的信息。

页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>

<Grid.RowDefinitions >
<RowDefinition Height="30"></RowDefinition>
<RowDefinition Height="auto"></RowDefinition>
</Grid.RowDefinitions>

<TextBox Grid.Row="0" KeyDown="TextBox_KeyDown"/>

<Label Grid.Row="1" Name="lblInfo" Content="?" />
</Grid>
</Window>

后台:

1
2
3
4
5
6
7
private void TextBox_KeyDown(object sender, KeyEventArgs e)
{
if ((e.KeyboardDevice.Modifiers & ModifierKeys.Control) == ModifierKeys.Control)
{
this.lblInfo.Content = "你按下了Ctrl键";
}
}

​ KeyboardDevice 属性还提供了几个简便方法,这些方法在下表中列出。

名称 说明
IsKeyDown() 当事件发生时,通知是否按下了该键。
IsKeyUp() 当事件发生时,通知是否释放了该键。
IsKeyToggled() 当事件发生时,通知该键是否处于“打开”状态。该方法只对那些能够打开、关闭的键有意义,如 CapsLock 键、ScrollLock键以及NumLock键。
GetKeyStates() 返回一个或多个KeyStates 枚举值,指明该键当前是否被释放了、按下了或处于切换状态。

鼠标输入

​ 鼠标事件执行几个关联的任务,当鼠标移到某个元素上时,可通过最基本的鼠标事件进行响应,这些事件是 MouseEnter(当鼠标指针移到元素上时引发该事件)和MoseLeave( 当鼠标指针离开元素时引发该事件)。这两个事件都是直接事件,这意味着它们不使用冒泡和隧遗过程,而是源自一个元素并且只被该元素引发。考虑到控件嵌入到 WPF窗口的方式,这是合理的。

​ 例如,如果有一个包含按钮的 StackPanel 面板,并将鼠标指针移到按钮上。那么首先会为这个 StckPanel 面板引发 MouseEnter 事件(当鼠标指针进入StackPanel 面板的边界时),然后为被钮引发 MouseEnter 事件(当鼠标指针移到按钮上时)。将鼠标指针移开时,首先为按钮。然后为 StackPanel 面板引发 MouseLeave 事件。

​ 还可响应 PreviewMouseMove 事件(隧道路由事件)和 MouseMove 事件(冒泡路由事件),只要移动鼠标机会引发这两个事件。所有这些事件都为代码提供了相同的信息:MouseEventArgs 对象。MouseEventArgs 对象包含当事件发生时标识鼠标键状态的属性,以及 GetPosition() 方法,该方法返回相对于所选元素的鼠标坐标。

页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>

<Grid.RowDefinitions >
<RowDefinition Height="350"></RowDefinition>
<RowDefinition Height="auto"></RowDefinition>
</Grid.RowDefinitions>

<TextBox Background="AliceBlue" Grid.Row="0" MouseMove="TextBox_MouseMove"/>

<Label Grid.Row="1" Name="lblInfo" Content="坐标:" />
</Grid>
</Window>

后台:

1
2
3
4
5
6
7
8
9
private void TextBox_MouseMove(object sender, MouseEventArgs e)
{
//坐标
Point p = e.GetPosition(this);

//显示
lblInfo.Content = string.Format("坐标:X:{0},Y:{1}",p.X,p.Y);

}

鼠标单击

​ 鼠标单击事件的引发方式和按键事件的引发方式有类似之处。区别是对于鼠标左键和鼠标右键引发不同的事件。下表根据它们的发生顺序列出了这些事件。除这些事件外,还有两个响应鼠标滚轮动作的事件:PreviewMouseWheel 和 MouseWheel。

名称 路由类型 说明
PreviewMouseLeftButtonDown
PreviewMouseRightButtonDown
隧道 当按下鼠标按键时发生
MouseLeftButtonDown
MouseRightButtonDown
冒泡 当按下鼠标按键时发生
PreviewMousetLeftButtonUp
PreviewMousetRightButtonUp
隧道 当释放鼠标时发生
MouseLeftButtonUp
MouseRightButtonUp
冒泡 当释放鼠标时发生

​ 某些元素添加了更高级的鼠标事件。例如,Control 类添加了 PreviewMouseDoubleClick 事件和 MouseDoubleClick 事件,这两个事件代替了 MouseLeftButtonUp 事件。与此类似,对于Button 类,通过鼠标或键盘可触发Click 事件。

捕获鼠标

​ 通常,元素每次接收到鼠标键“按下”事件后,不久后就会接收到对应的鼠标键“释放”事件。但情况不见得总是如此。例如,如果单击一个元素,保持按下鼠标键,然后移动鼠标指针离开该元素,这时该元素就不会接收到鼠标键释放事件。

​ 某些情况下,可能希望通知鼠标键释放事件,即使鼠标键释放事件是在鼠标已经离开了原来的元素之后发生的。为此,需要调用Mouse.Caphure()方法并传递恰当的元素以捕获鼠标。此后,就会接收到鼠标键按下事件和释放事件,直到再次调用Mouse.Caphure()方法并传递空引用为止。当鼠标被一个元素捕获后,其他元素就不会接收到鼠标事件。这意味着用户不能单击窗口中其他位置的按钮,不能单击文本框的内部。鼠标捕获有时用于可以被拖放并可以改变尺寸的元素。

​ 有些情况下,可能由于其他原因(不是您的错)丢失鼠标捕获。例如,如果需要显示系统对话框,Windows 可能会释放鼠标捕获。如果当鼠标键释放事件发生后没有释放鼠标,并且用单击了另一个应用程序中的窗口,也可能丢失鼠标捕获。无论哪种情况,都可以通过处理元素的 LostMouseCapture 事件来响应鼠标捕获的丢失。

​ 当鼠标被一个元素捕获时,就不能与其他元素进行交互(例如,不能单击窗口中的其他元素)。鼠标捕获通常用于短时间的操作,如拖放。

鼠标拖放

​ 本质上,拖放操作通过以下三个步骤进行:

(1)用户单击元素(或选择元素中的一块特定区域),并保持鼠标按键为按下状态。

(2)用户将鼠标移到其他元素上。

(3)当用户释放鼠标键时。

​ 如果希望两个未提供内置拖放功能的元素之间进行拖放,例如:可能希望从Label对象或TextBox对象拖动文本、并放到一个标签中。对于这种情况,需要处理拖放事件。

​ 拖放操作有两个方面:源和目标。为了创建拖放源,需要在某个位置调用 DragDrop.DoDragDrop() 方法来初始化拖放操作。此时确定拖放操作的源,搁置希望移动的内容,并指明允许什么样的拖放效果(复制、移动等)。

​ 接收数据的元素需要将它的 AllowDrop 属性设置为true.此外,它还需要通过处理 Drop事件来处理数据。

页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>

<Grid.RowDefinitions >
<RowDefinition ></RowDefinition>
<RowDefinition ></RowDefinition>
<RowDefinition ></RowDefinition>
</Grid.RowDefinitions>

<Label Grid.Row="0" Name="lblSource" Content="标签1" MouseDown="lblSource_MouseDown" />
<Label Grid.Row="1" Name="lblTarget" Content="标签2" AllowDrop="True" DragEnter="lblTarget_DragEnter" Drop="lblTarget_Drop"/>
<TextBox Grid.Row="2" Name="lblInfo" Text="文本框" />
</Grid>
</Window>

后台:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/// <summary>
/// 鼠标按下事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void lblSource_MouseDown(object sender, MouseButtonEventArgs e)
{
//触发源
Label lbl = sender as Label;

//设置拖放操作
DragDrop.DoDragDrop(lbl, lbl.Content, DragDropEffects.Copy);
}

/// <summary>
/// 拖放进入事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void lblTarget_DragEnter(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(DataFormats.Text))
e.Effects= DragDropEffects.Copy;
else
e.Effects= DragDropEffects.None;
}

/// <summary>
/// 拖放的释放事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void lblTarget_Drop(object sender, DragEventArgs e)
{
//设置目标按钮文本内容
((Label)sender).Content=e.Data.GetData(DataFormats.Text);
}

注意:

如果希望在两个应用程序之间传递数据,那么务必检查 System.Windows.Clipboard 类,该类提供了静态方法,用于在 Windows 剪贴板中放置数据,并以各种不同的格式检索剪贴板中的数据。

多点触控输入

​ 多点触控(multi-touch)是通过触摸屏幕与应用程序进行交互的一种方式。多点触控输入和更传统的基于笔(pen-based)的输入的区别是多点触控识别手势(gesture)用户可移动多根手指以执行常见操作的特方式。例如,在触摸屏上放置两根手指并同时移动它们,这通常意味着“放大”,而以一根手指为支点转动另一根手指意味着“旋转”。并且因为用户直接在应用程序窗口中进行这些手势,所以每个手势自然会被连接到某个特定的对象。例如,简单的具有多点触控功能的应用程序,可能会在虚拟桌面上显示多幅图片,并且允许用户拖动、缩放以及旋转每幅图片,进而创建新的排列方式。

多点触控的输入层次

​ 正如您在上面了解到的,WPF允许使用键盘和鼠标的高层次输入(例如单击和文本改变)和低层次输入(鼠标事件以及按键事件)。这很重要,因为有些应用程序需要加以更精细的控制。多点触控输入同样应用了这种多层次的输入方式,并且对于多点触控支持,WPF 提供了三个独立的层次:

(1)原始触控(raw touch):这是最低级的支持,可访问用户执行的每个触控。缺点是由您的应用程序负责将单独的触控消息组合到一起,并对它们进行解释。如果不准备识别标准触摸手势,反而希望创建以独特方式响应多点触控输入的应用程序,使用原始触控是合理的。一个例子是绘图程序,例如 Windows7画图程序,该程序允许用户同时使用多根手指在触摸屏上绘图。

(2) 操作(manipulation):这是一个简便的抽象层,该层将原始的多点触控输入转换成更有意义的手势,与 WPF 控件将一系列 MouseDown 和 MouseUp 事件解释为更高级的MouseDoubleClick 事件很相似。WPF 支持的通用手势包括移动(pan)、缩放(zoom)、旋转(rotate)以及轻按(tap)。

(3)内置的元素支持(built-in element support):有些元素已对多点触控事件提供了内置支持,从而不需要再编写代码。例如,可滚动的控件支持触控移动,如ListBox、ListView、DataGrid、TextBox 以及 ScrolIViewer。

原始触控

​ 与基本的鼠标和键盘事件一样,触控事件被内置到低级的 UIElement 以及 ContentElement 类。

名称 路由类型 说明
PreviewTouchDown 隧道 当用户触摸元素时发生
TouchDown 冒泡 当用户触摸元素时发生
PreviewTouchMove 隧道 当用户移动放到触摸屏上的手指时发生
TouchMove 冒泡 当用户移动放到触摸屏上的手指时发生
PreviewTouchUp 隧道 当用户移开手指,结束触摸时发生
TouchUp 冒泡 当用户移开手指,结束触摸时发生
TouchEnter 当触点从元素外进入元素内时发生
TouchLeave 当触点离开元素时发生

​ 所有这些事件都提供了一个 TouchEventArgs 对象,该对象提供了两个重要成员。第一个是 GetTouchPoint()方法,该方法返回触控事件发生位置的坐标。第二个是 TouchDevice 属性,该属性返回一个 TouchDevice 对象。当用户在不同的位置按下两根手指,WPF 将它们作为两个触控设备,并为每个触控设备指定唯一的ID,当用户移动这些手指,并且触控事件发生时,代码可以通过 TouchDevice.Id 属性区分两个触点。

​ 下面的示例演示了一个简单的原始触控程序。当用户在Canvas 控件上触摸时,应用程序添加一个小的椭圆元素以显示触点。然后,当用户移动手指时,代码移动椭圆从而使其跟随手指移动。为了创建这个示例,需要处理 TouchDown、TouchUp() 以及 TouchMove 事件:

页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Canvas x:Name="canvas" Background="LightSkyBlue"
TouchDown="canvas_TouchDown"
TouchUp="canvas_TouchUp"
TouchMove="canvas_TouchMove">

</Canvas>
</Window>

后台:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Markup;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfApp1
{
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
public partial class MainWindow : Window
{


public MainWindow()
{
InitializeComponent();
}

/// <summary>
/// 元素集合
/// </summary>
private Dictionary<int, UIElement> movingEllipses= new Dictionary<int, UIElement>();

private void canvas_TouchDown(object sender, TouchEventArgs e)
{
//创建圆
Ellipse ellipse = new Ellipse();
ellipse.Width = 30; //宽度
ellipse.Height=30; //高度
ellipse.Stroke=Brushes.White; //边框
ellipse.Fill=Brushes.Green; //填充

//设置圆的位置
TouchPoint tp = e.GetTouchPoint(canvas);
Canvas.SetTop(ellipse, tp.Bounds.Top);
Canvas.SetLeft(ellipse,tp.Bounds.Left);

//将圆加入元素集合
movingEllipses[e.TouchDevice.Id] = ellipse;

//将圆加入到画布
canvas.Children.Add(ellipse);
}

private void canvas_TouchUp(object sender, TouchEventArgs e)
{
//元素
UIElement element = movingEllipses[e.TouchDevice.Id];

//将元素移动到新的坐标点
TouchPoint tp=e.GetTouchPoint(canvas);
Canvas.SetTop(element, tp.Bounds.Top);
Canvas.SetLeft(element, tp.Bounds.Left);
}

private void canvas_TouchMove(object sender, TouchEventArgs e)
{
//元素
UIElement element = movingEllipses[e.TouchDevice.Id];

//从画布中移除元素
canvas.Children.Remove(element);

//从集合中移除元素
movingEllipses.Remove(e.TouchDevice.Id);
}
}
}

操作

​ 该例使用基本的安排在Canvas 面板上显示三幅图像。此后用户可使用移动、旋转以及缩放手势来移动、转动、缩小或放大图像。

页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Canvas x:Name="canvas"
ManipulationStarting="canvas_ManipulationStarting"
ManipulationDelta="canvas_ManipulationDelta"
>

<Image Canvas.Top="10" Canvas.Left="10" Width="200"
IsManipulationEnabled="True" Source="a.jpg">
<Image.RenderTransform>
<MatrixTransform></MatrixTransform>
</Image.RenderTransform>
</Image>

<Image Canvas.Top="30" Canvas.Left="350" Width="200"
IsManipulationEnabled="True" Source="a.jpg">
<Image.RenderTransform>
<MatrixTransform></MatrixTransform>
</Image.RenderTransform>
</Image>

<Image Canvas.Top="100" Canvas.Left="200" Width="200"
IsManipulationEnabled="True" Source="a.jpg">
<Image.RenderTransform>
<MatrixTransform></MatrixTransform>
</Image.RenderTransform>
</Image>

</Canvas>
</Window>

后台:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Markup;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfApp1
{
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
public partial class MainWindow : Window
{


public MainWindow()
{
InitializeComponent();


}

private void canvas_ManipulationStarting(object sender, ManipulationStartingEventArgs e)
{
//设置容器
e.ManipulationContainer = canvas;

//设置模式
e.Mode = ManipulationModes.All;
}

private void canvas_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
{
//元素
Image img = e.Source as Image;

//矩阵
Matrix matrix = ((MatrixTransform)img.RenderTransform).Matrix;

//操作增量
ManipulationDelta md = e.DeltaManipulation;

//中心位置
Point center = new Point(img.ActualWidth / 2, img.ActualHeight / 2);
center = matrix.Transform(center);

matrix.ScaleAt(md.Scale.X, md.Scale.Y, center.X, center.Y);

//指定矩阵的旋转
matrix.RotateAt(e.DeltaManipulation.Rotation, center.X, center.Y);

matrix.Translate(e.DeltaManipulation.Translation.X, e.DeltaManipulation.Translation.Y);

((MatrixTransform)img.RenderTransform).Matrix= matrix;

}
}
}

惯性

​ WPF还有一层构建在基本操作支持之上的特性,称为慣性(intertia)。本质上,通过惯性可以更選真、更流畅地操作元素。

​ 现在,如果用户用移动手势拖动图中的一幅图像,当手指从触摸屏上拾起时图像会立即停止移动。但如果启用了惯性特征,那么图像会维续移动非常短的一段时间,正常地减速。该特性为操作提供了势头的效果和感觉。当将元素拖动进它们不能穿过的边界时,惯性还会使元素被弹回,从而使它们的行为像是真实的物理对象。

​ 为给上一个示例添加惯性特性,只需处理 ManipulationlaertiaStarting 事件。与其他操作事件一样,该事件从一幅图像开始并冒泡至 Canvas 面板。当用户结束手势并拾起手指释放元素时,触发ManipulationinertiaStarting 事件。这时,可使用 ManipulationinertiaStartingEventsAgs 对象确定当前速度—当操作结束时元素的移动速度—并设置希望的减速度。下面的示例为移动、缩放以及旋转手势添加了惯性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void canvas_ManipulationInertiaStarting(object sender, ManipulationInertiaStartingEventArgs e)
{
//设置线性惯性运动减慢的速率
e.TranslationBehavior=new InertiaTranslationBehavior();
e.TranslationBehavior.InitialVelocity = e.InitialVelocities.LinearVelocity;
e.TranslationBehavior.DesiredDeceleration = 10.0 * 96.0 / (1000.0 * 1000.0);

//设置扩展惯性运动减慢的速率
e.ExpansionBehavior = new InertiaExpansionBehavior();
e.ExpansionBehavior.InitialVelocity = e.InitialVelocities.ExpansionVelocity;
e.ExpansionBehavior.DesiredDeceleration = 0.1 * 96 / 1000.0 * 1000.0;

//设置旋转惯性运动减慢的速率
e.RotationBehavior = new InertiaRotationBehavior();
e.RotationBehavior.InitialVelocity = e.InitialVelocities.AngularVelocity;
e.RotationBehavior.DesiredDeceleration = 720 / (1000.0 * 1000.0);
}

本章小结

​ 本章深入分析了路由事件。首先研究了路由事件,并看到了它们是如何使开发人员能够在不同层次上处理事件的—直接在源中处理事件或在包含元素中处理事件。接下来,本章介绍了为能够处理键盘、鼠标以及多点触控输入,这些路由策略在 WPF元素中的实现方式。

课后作业