【笔记】【WPF编程宝典】 第05章 路由事件


  路由事件是具有更强传播能力的事件——它们可 在元素树中向上冒泡或向下隧道传播,并沿着传播路径被事件处理程序处理。路由事件允许事件在某个元素上被处理,即使该事件源自另外一个元素也是如此。

5.1 理解路由事件

  WPF通过事件路由的概念增强了.NET事件模型。事件路由允许源自某个元素的事件由另一个元素引发。例如,使用事件路由,来自工具来按钮的单击机事件可在被代码处理之前上传到工具栏,然后上传到包含工具栏的窗口。
  事件路由为在最合适的位置编写紧凑的、组织良好的用于处理事件的代码提供了灵活性。要使用WPF内容模型,事件路由也是必须的,内容模型允许使用许多不同的元素构建简单元素,并且这些元素都拥有 自己独立的事件集合。

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

  与依赖项属性一样,路由事件由只读的静态字段表示,在静态构造函数中注册,并通过标准的.NET事件定义进行封装。例如,WPF的 Button 类提供的Click事件,该事件继承自抽象基类 ButtonBase 基类。下面的代码说明了该事件是如何定义和注册的:

public abstract class ButtonBase : ContentControl, ...
{
	public static readonly RoutedEvent ClickEvent;

	static ButtonBase()
	{
		ButtonBase.ClickEvent = EventManager.RegisterRountedEvent("Click", 
		                                                          RountingStrategy.Bubble, 
		                                                          typeof(RoutedEventHandler),
		                                                          typeof(ButtonBase));
		...
	}
	public event RoutedEventHandle Click
	{
		add 
		{
			base.AddHandler(ButtonBase.ClickEvent, value);
		}
		remove 
		{
			base.RemoveHandler(ButtonBase.ClickEvent, value);
		}
	}
	...
}

  路由事件使用EventManager.RegisterRoutedEvent()方法注册的。当注册事件时,需要指定事件的名称、路由类型、定义事件处理程序语法的委托以及拥有事件的类。事件封装器可使用AddHandler()和RemoveHandler()方法添加和删除已注册的程序,这两个方法都在FrameworkElement基类中定义,并被每个WPF元素继承。

5.1.2 共享路由事件

  与依赖项属性一样,可共享路由事件的定义。例如,UIElement和ContentElement这两个基类都使用了MouseUp事件。MouseUp事件由System.Windows.Input.Mouse类定义的。UIElement和ContentElement类只通过RoutedEvent.AddOwner()方法重用MouseUp事件。

5.1.3 引发路由事件

  路由事件不是通过传统的.NET事件封装器引发的,而是使用RaiseEvent()方法引发事件,所有元素都从UIElement类继承了该方法。下面是来自ButtonBase类深层的代码:

var e = new RoutedEventArgs(ButtonBase.ClickEvent, this);
base.RaiseEvent(e);

  RaiseEvent()方法负责为每个已经通过AddHander()方法注册的调用程序引发事件。因为AddHander()方法是公有的,所有调用程序可访问该方法——它们能够通过直接调用AddHander()方法注册它们自己,也可以使用事件封装器。
  所有WPF事件都为事件签名使用熟悉的.NET约定。每个事件处理程序的第一个参数都是提供引发该时间的对象的引用。第二个参数是EventArgs对象,该对象与其他所有可能很重要的附加细节绑定在一起。例如,MouseUp事件提供了一个MouseEventArgs对象,用于指示当事件发生时按下了哪些鼠标键:

private void img_mouseup(object sender, MouseButtonEventArgs e)
{
}

  在WPF中,如果事件不需要传递任何额外细节,可使用RoutedEventArgs类,该类包含了有关如何传递事件的一些细节。如果事件确实需要传递额外的信息,那么需要使用更特殊的继承自RoutedEventArgs的对象。

5.1.4 处理路由事件

  可在XAML中通过特性关联事件处理程序,特性的值就是事件处理程序方法的名称。

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

5.2 事件路由

  WPF 中许多控件都是内容控件,而内容控件可包含任何类型以及大量的嵌套内容。这种嵌套也产生了问题。例如,假设有如下标签,标签中包含一个StackPanel面板,该面板又包含了两块文本和一幅图像:

<Label BorderBrush="Black" BorderThickness="1">
	<StackPanel>
		<TextBlock Margin="3"> Image and text label</TextBlock>
		<Image Source="happyface.jpg" Stretch="None"></Image>
		<TextBlock Margin="3"> Courtesy of the StackPanel></TextBlock>
	</StatckPanel>
</Label>

  WPF 窗口中的元素都在一定层次上继承自UIElement类。UIElement定义了一些核心事件。例如,每个继承自UIElement的类都提供MouseDown和MouseUp事件。
  当点击上面这个特殊标签中的图像部分时,将引发Image.MouseDown事件和Image.MouseUp事件时合情合理的。但如果希望采用相同的方式来处理表上的所有单击事件,该怎么办呢?此时,不管用户单击了图像、某块文本还是标签内的空白处,都应当使用相同的代码进行响应。
  显然,可为每个元素的MouseDown或MouseUp事件关联同一个事件处理程序,但这样会使标记变得杂乱无章且难以维护。WPF使用路由事件模型提供了一个更好的解决方法。路由事件实际上以下列三种方式出现:

  1. 与普通.NET事件类似的直接路由事件(Direct Event)。它们源于一个元素,不传递给其他元素。例如,MouseEnter事件(当鼠标指针移动到元素上时发生)是直接路由事件。
  2. 在包含层次中向上传递的冒泡路由事件(Bubbling Event)。例如,MouseDown事件就是冒泡路由事件。该事件首先由被单击的元素引发,接下来被该元素的父元素引发,然后被父元素的父元素引发,以此类推,直到WPF到达元素树的顶部为止。
  3. 在包含层次中向下传递的隧道路由事件(Tunnelling Event)。隧道路由事件在事件到达恰当的控件之前为预览事件(甚至终止事件)提供了机会。例如,通过PreviewKeyDown事件可截获是否按下了某个键。首先在窗口级别上,然后是更具体的容器,只至到达当按下键时具有焦点的元素。

  当使用EventManager.RegisterEvent()方法注册路由事件时,需要传递一个RoutedStrategy枚举值,该值用于指示希望应用于事件的事件行为。MouseUp与MouseDown事件都是冒泡路由事件,因此现在可以确定在上面特殊的标签示例中发生什么事情。当单机标签上的图像部分时,按以下顺序触发MouseDown事件:

  1. Image.MouseDown
  2. StackPanel.MouseDown
  3. Label.MouseDown

5.2.1 RoutedEventArgs类

  在处理冒泡路由事件时,sender参数提供了对整个链条上最后那个链接的引用。例如,在上面的实例中,如果事件在处理之前,从图像向上冒泡到标签,sender参数就会引用标签对象。
  有些情况下,可能希望确定事件最初发生的位置。可从RoutedEventArgs类的属性获得这一信息以及其他细节。由于所有WPF事件参数类继承自RoutedEventArgs,因此任何处理程序都可以使用这些属性。

名称说明
Source指示引发了事件的对象
对于键盘事件,是当事件发生时(如按下键盘上的键)具有交点的控件
对于鼠标事件,是事件发生时(如单机鼠标按钮)鼠标指定下面所有元素中最靠上的元素
Original指示最初时什么对象引发了事件
OriginalSource通常与Source相同。但在某些情况下,OriginalSource指向对象树中更深的层次,以获得作为更高一级元素一部分的后台元素。
例如,如果单击窗口边框上的关闭按钮,事件源为Window对象,但事件最原始的源时Border对象。这是因为Window对象是由多个单独的、更小的部分构成的
RoutedEvent通过事件处理程序为触发的事件提供RoutedEvent对象
如果用同一个事件处理程序处理不同的事件,这一信息是非常有用的
Handled该属性允许终止事件的冒泡或隧道过程
如果控件将Handled属性设置为true,那么该事件就不会继续传递,也不会再为卡任何元素引发该事件

5.2.2 冒泡路由事件

  下图显示了单击标签中的图像之后的冒泡过程,MouseUp事件传递了5级,在自定义的 BubbledLabelClick窗体 中停止向上传递。
MouseUp事件冒泡过程
  该窗口的构建文件与隐藏类文件:

<Window x:Class="RoutedEvents.BubbledLabelClick"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="BubbledLabelClick" Height="359" Width="329"
        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" Background="AliceBlue" BorderBrush="Black" BorderThickness="1" MouseUp="SomethingClicked" HorizontalAlignment="Left">
            <StackPanel MouseUp="SomethingClicked">
                <TextBlock Margin="3" MouseUp="SomethingClicked">
                    Image and picture label
                </TextBlock>
                <Image Source="happyface.jpg" Stretch="None" MouseUp="SomethingClicked" >
                    <Image.OpacityMask>
                        <ImageBrush ImageSource="happyface.jpg"/>
                    </Image.OpacityMask>
                </Image>
                <TextBlock Margin="3" MouseUp="SomethingClicked">
                    Courtesy of the StackPanel
                </TextBlock>
            </StackPanel>
        </Label>

        <ListBox Margin="5" Name="LstMessages" Grid.Row="1" />
        <CheckBox Margin="5" Grid.Row="2" Name="ChkHandle">Handle first event</CheckBox>
        <Button Click="cmdClear_Click" Grid.Row="3" HorizontalAlignment="Right" Margin="5" Padding="3">Clear List</Button>
    </Grid>
</Window>
using System.Windows;
namespace RoutedEvents
{
    public partial class BubbledLabelClick : Window
    {
        private int _eventCounter;
        public BubbledLabelClick()
        {
            InitializeComponent();
        }

        private void SomethingClicked(object sender, RoutedEventArgs e)
        {
            _eventCounter++;
            var message = "#" + _eventCounter + ":\r\n" +
                          " Sender: " + sender + "\r\n" +
                          " Source: " + e.Source + "\r\n" +
                          " Original Source: " + e.OriginalSource;
            LstMessages.Items.Add(message);
            e.Handled = (bool) ChkHandle.IsChecked;
        }

        private void cmdClear_Click(object sender, RoutedEventArgs e)
        {
            _eventCounter = 0;
            LstMessages.Items.Clear();
        }
    }
}

  如果选中界面上的Handle first event复选框,RoutedEventArgs.Handled属性设为true,从而在事件第一次发生时就终止事件的冒泡过程。

5.2.3 处理挂起的事件

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

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

5.2.4 附加事件

  假设在StackPanel面板中封装了一堆按钮,并希望在一个事件处理程序中处理所有这些按钮的单击事件。粗略的方法是将每个按钮的Click事件关联到同一个事件处理程序。但Click事件支持事件冒泡,从而提供了一种更好的选择。可通过处理更高层次的元素的Click事件来处理所有按钮的Click事件。但看似浅显的代码却不能工作:

<StackPanel Click="DoSomething">
	<Button Name"cmd1">cmd1</Button>
	<Button Name"cmd1">cmd1</Button>
	<Button Name"cmd1">cmd1</Button>
	...
</StackPanel>

  问题在于StackPanel面板没有Click事件,所以XAML解析器会将器解释成错误。通过以下方式可以更正。

<StackPanel Button.Click="DoSomething">
	<Button Name"cmd1">cmd1</Button>
	<Button Name"cmd1">cmd1</Button>
	<Button Name"cmd1">cmd1</Button>
	...
</StackPanel>

  在代码中关联附件事件,但需要使用UIElement.AddHandler()方法,而不能使用+=运算符语法。

pnlButtons.AddHandler(Button.Click, new RoutedEventHandler(DoSomething));

  区分是哪一个按钮引发的事件,可通过按钮的Name属性或Tag属性。

5.2.5 隧道路由事件

  隧道路由事件的工作方式与冒泡路由事件相同,但方向相反。例如,如果MouseUp事件是隧道路由事件(实际上不是),在特殊的标签示例中单击图形将导致MouseUp事件首先在窗口中被引发,然后是Grid面板,接着是StatckPanel面板,以此类推,只至到达实际的源头,即标签中的图像为止。
  隧道路由事件易于识别,它们都以单词Preview开头。而且,WPF通常成对地定义冒泡路由事件和隧道路由事件。这意味着如果发现冒泡路由事件,就还可以找到对应的隧道路由事件。隧道路由事件总在冒泡路由事件之前被触发。
  如果将隧道路由事件标记为已处理,那就不会发生冒泡事件。这是因为两个事件共享RoutedEventArgs类的同一个实例。
  如果 需要执行一些预处理(根据键盘上特定的键执行动作或过滤掉特定的鼠标动作),隧道路由事件是非常有用的。

5.3 WPF 事件

  常用的事件通常包括以下5类:

  • 声明周期事件 :在元素被初始化、加载或卸载时发生
  • 鼠标事件 :鼠标动作的结果
  • 键盘事件 :键盘动作的结果
  • 手写笔事件 :平板电脑上用手写笔代替鼠标
  • 多点触控事件:一根或多根手指在多点触控屏上触摸的结果

5.3.1 生命周期事件

  当首次创建以及释放所有元素时都会引发事件,可使用这些事件初始化窗口。下表列出这些事件,它们在FrameworkElement类中定义

名称说明
Initialized当元素被实例化,并已根据XAML标记设置了元素的属性之后发生
这时元素已经初始化,但窗口的其他部分可能上未初始化。此外,尚未应用样式和数据绑定。这时,IsInitialized属性为true。
Initialized 事件是普通的.NET事件——并非路由事件
Load当整个窗口已经初始化并应用了样式和数据绑定时,该事件发生。这时在元素被呈现之前的最后一站。
这时,IsLoaded属性为true
UnLoad当元素被释放时,该事件发生。原因是包含元素的窗口被关闭或特定的元素被从窗口删除

  为了弄清Initialized事件和Loaded事件之间的关系,分析以下呈现过程时有帮助的。FrameworkElement 类实现了 ISupportInitialize 接口,该接口提供了两个用于控制初始化过程的方法。第一个方法时BeginInit(),在实例化元素后会立即调用该方法。调用BeginInit()方法后,XAML解析器设置所有元素的属性(并添加内容)。第二个方法是EndInit(),完成初始化后,将调用该方法,此时引发Initialized事件。
  当创建窗口时,会自下而上地初始化每个元素分支。这意味着,位于深层的嵌套元素在它们的容器之前被初始化。可确保元素书中当前元素以下的元素已经全部完成初始化。但是,包含当前元素的元素可能还没初始化,并且不能假定该窗口的任何其他部分已经初始化。
  在每个元素都完成初始化之后,还需要再它们的容器中进行布局、应用样式。如果需要的话,还会绑定到数据源。当引发窗口的Initialized事件后,就可以进入下一阶段了。
一旦完成舒适化过程,就会引发Loaded事件。Loaded事件和Initialized事件的发生过程相反——换句话说,包含其他所有元素的窗口首先引发Loaded事件,然后才是更深层次的嵌套元素。为所有元素都引发了Loaded事件之后,窗口就变得可见了,并且元素都已被呈现。

名称说明
SourceInitialized当取得窗口的HwndSource属性时(但在窗口可见之间)发生。
HwndSource是窗口句柄,如果调用Win32 API中的遗留函数 ,就可能需要使用该句柄
ContentRended在窗口首次呈现之后立即发生。
对于执行任何可能会影响窗口可视外观的更改操作,这不是一个好位置,否则将会强制进行第二次呈现(该用Loaded事件),然而,ContentRendered事件表明窗口已经完全可见,并且已经准备好接收输入
Activated当用户切换到该窗口时发生(例如,从应用程序的其他窗口或从其他应用程序切换到该窗口)。
当窗口第一次加载时也会引发Activated事件。
从概念上将,窗口的Activated事件相当于控件的GetFocus事件
Deactivated当用户从该窗口切换到其他窗口时发生(例如,切换到应用程序的其他窗口或切换到其他应用程序)。
当用户关闭窗口时也会发生该事件,该事件在Closing事件之后,但在Closed事件发生之前。
从概念上讲,窗口的Deactivated事件相当于控件的LostFocus事件
Closing当窗口关闭时发生,不管时用户关闭窗口还是通过代码调用Window.Close()或Applicaiton.Shutdown()方法关闭窗口。
Closing事件提供取消擦偶哦并保持打开状态的机会,具体通过将CancelEventArgs.Cancel属性设置为true实现该目标。但是,如果时因为用户关闭或注销计算而导致应用程序被关闭,就不能接收到Closing事件。为应对这种情况,需要处理Application.SessionEnding事件
Closed当窗口已经关闭后发生。但是,此时仍可以访问元素对象,当然时在Unloaded事件尚未触发之前。在此,可以执行一些清理工作,向永久存储位置写入设置信息等

  如果只对执行控件的第一次初始化感兴趣,完成这项任务的最好时机是在触发Loaded事件时。通常可在同一位置进行所有初始化,这个位置一般是Window.Loaded事件的处理程序。

5.3.2 输入事件

  输入事件是当用户使用某些种类的外设硬件进行交互时发生的事件,例如鼠标、键盘、手写笔或多点触控屏。输入事件可通过继承自InputEventArgs的自定义事件参数类传递额外的信息。
  InputEventArgs类只增加了两个属性:Timestamp和Device。Timestamp属性提供了以整数,只是了事件合适发生的毫秒数。Device属性返回一个对象,该对象提供与触发事件的设备相关联的的更过信息,设备可能时鼠标、键盘或手写笔。这三种可能的设备由不同的类表示,所有这些类都继承自抽象基类System.Windows.INput.InputDevice。

5.4 键盘输入

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

名称路由类型说明
PreviewKeyDown隧道当按下一个键时发生
KeyDown冒泡当按下一个键时发生
PreviewTextInput隧道当按键完成并且元素正在接收文本输入时发生。对于那些不会产生文本"输入"的按键(如Ctrl、Shift、Backspace等),不会引发该事件
TextInput冒泡当按键完成并且元素正在接收文本输入时发生,对于那些不会产生文本的按键,不会引发该事件
PreviewKeyUp隧道当释放一个按键时发生
KeyUp冒泡当释放一个按键时发生

  键盘处理永远不会向上面看到的那么简单。一些控件可能会挂起这些按键中的某些事件,从而可执行自己更特殊的键盘处理。最明显的例子时TextBox控件,它挂起了TextInput事件。对于一些按键,TextBox控件还挂起了KeyDown事件,如方向键。对于此类情形,通常仍可使用隧道路由事件。
  TextBox控件还添加了名为TextChanged的新事件。在氨基钠导致文本框中的文本发生改变之后会立即引发该事件。这时,在文本框中已经可以看到新的文本,所以组织不需要的按键已为时已晚。

5.4.1 处理按键事件

  下图展示了,在文本框中输入大写S键时的结果
监视键盘事件
  该例演示了非常重要的一点。每次按下一个键时,都会触发PreviewKeyDown和PreviewKeyUp事件。但只有当字符可以输入“输入”到元素中时,才会触发TextInput事件。这一动作时加上可能设计多个按键操作。上例中,为得到大写字母,需要按下两个键。首先,按下Shift键,接着按下S键。因此,分别看到了两个KeyDown和KeyUp事件,但只有一个TextInput事件。
  PreviewKeyDown、KeyDown、PreviewKeyUp和KeyUp事件都通过KeyEventArgs对象提供了相同的信息。最重要的信息时Key属性,该属性返回一个System.Windows.Input.Key枚举:

private void KeyEvent(object sender, KeyEventArgs e)
{
	var message = "Event: " + e.RoutedEvent + " " + "Key: " + e.Key;
	lstMessage.Items.Add(message);
}

  Key值没有考虑任何其他键的状态。例如,当按下S键时不比关系当前是否按下了Shift键,不管是否按下Shift键都会得到相同的Key值(Key.S)
  这里存在一个问题。根据Windows键盘设置,持续按下一个键一段时间,会重复引发按键事件。例如,保持按下S键,会在文本框中输入一系列S字符。同样,按下Shift键一段事件也会得到多个按键和一些列KeyDown事件。按下Shift+S键进行测试的真实情况是,文本框实际上会为Shift键引发一些列KeyDown事件,然后为S键引发KeyDown事件,随后是TextInput事件(对于文本框是TexChanged事件),最后是Shift键和S键引发KeyUp事件。如果希望葫芦恩恩哲熙而重复的Shift键,可通过检查KeyEventArgs.IsRepeat属性,确定按键是不是因为按住按键导致的结果。
  KeyDown事件发生后,接着发生PreviewTextInput事件(因为,TextBox控件挂起了TextInput事件,所以不会发生TextInput事件)。此时,文本尚未出现在控件中。
  TextInput事件使用TextCompositionEventArgs对象提供代码。该对象包含Text属性,该属性提供了处理过的文本,它们是控件即将接收到的文本。

private void TextInput(object sender, TextCompositionEventArgs e)
{
	var string = "Event: " + e.RoutedEvent + " Text: " + e.Text;
	lstMessage.Items.Add(message);
}

  理想情况下,可在控件中使用PreviewTextInput事件进行验证工作。例如,如果构建只能输入数字的文本框,可确保当前按键不是字母,如果是就设置Handled标志。可惜,对于某些可能希望处理的键就不会触发PreviewTextInput事件。例如,如果在文本框中按下了空格键,将直接绕过PreviewTextInput事件,这意味着还需要处理PreviewKeyDown事件。
  但在PreviewKeyDown事件处理程序中编写出可靠的验证逻辑是比较困难的,因为在此只直到Key值,这是级别很低的信息。例如,Key枚举区分数字键盘和普通键盘字母以上的数字键。这意味着根据按下数字9的方式,可能得到值Key.D9或Key.NumPad9。验证所有这些允许使用的键值至少可以说是非常枯燥的。
  一种选择是使用KeyConverter类将Key转换为更有用的字符串。例如,使用KeyConverter.ConverterToString()方法,Key.D9和Key.NumPad9都返回字符串"9"。如果只使用Key.ToString()方法,将得到不那么有用的枚举名称。
然而,即使使用KeyConverter类也仍存在缺陷,因为对于不糊i产生文本输入的按键,会得到更长一点的文本。
  最好同事处理PreviewTextInput事件和PreviewKeyDown事件,PreviewKeyDown用于那些在文本框中不会引发PreviewTextInput事件的按键。

private void pnl_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
	short val
	if(!short.TryPrase(e.Text, out val))
	{
		e.Handled = true;
	}
}
private void pnl_PreviewKeyDown(object sender, KeyEventArgs e)
{
	if(e.Key == Key.Space)
	{
		e.Handled = true;
	}
}

5.4.2 交点

  在Windows世界中,用户每次只能使用一个控件。当前接收用户按键的控件时具有焦点的控件。有时,有交点的控件的外观有些不同。例如,WPF按钮使用蓝色阴影显示它具有交点。
  为了让控件能教授焦点,必须将Focusable属性设置为true,这是所有空间的默认值。
  有趣的是,Focusable属性在UIElement类中定义,这意味着其他非控件元素也可以获得交点。通常,对于非控件类,Focusable属性设置为false,但也可设置为true。例如,使用布局容器测试这一点——当它获得焦点时,会在面板边缘的周围显示一条点画线边框。
  为将焦点从一个元素移动到另一个元素,用户可单击鼠标或使用Tab键和方向键。以前的开发矿建强制编程人员确保Tab键以合理方式移动焦点,并且确保在创库第一次显示时正确的控件获得焦点。在WPF中,不必再完成浙西额外的工作,因为WPF使用层次结构的元素布局实现了Tab键切换焦点的顺序。本质上,按下Tab键会将焦点移动到当前元素的第一个子元素,如果当前元素没有子元素,会将焦点移动到同级的下一个子元素。
  如果希望获得控制使用Tab键转移焦点顺序的功能,可按数字顺序设置每个控件的TabIndex属性。TabIndex属性为0的控件首先获得焦点,然后时次高的TabIndex值。如果多个元素具有相同的TabIndex值,WPF就使用自动Tab顺序,这意味着会跳过随后最靠近的元素。

  默认情况下,所有控件的TabIndex属性都被设置为Int32.MaxValue。这意味着可通过将某个特定控件的TabIndex属性设置为0,让该控件作为 窗口的开始点。
TabIndex属性是在Control类中定义的,在该类中还定义了IsTabStop属性。可通过将IsTabStop属性设置为false来组织控件被包含进Tab键焦点顺序。IsStop属性和Focusable属性之间的区别是,如果控件的IsTab属性被设置为false,控件仍可通过其他方式获取到焦点——编程或点击鼠标。

5.4.3 获取键盘状态

  当发生按键事件时,经常需要直到更多信息,而不仅要直到按下的是哪个键。而且确定器他键是否同事按下也非常重要。这意味着可能需要检查其他键的状态,特别是Shift、Ctrl、Alt等修饰键。
  对于键盘事件,获取这些信息比较容易。首先,KeyEventArgs对象包含KeyStates属性,该属性反应触发事件的键的属性。更有用的是,KeyboardDevice属性为键盘上的所有键提供了相同的信息。自然,KeyboardDevice属性提供了KeyboardDevice类的一个实例。它的属性包括当前是哪个元素具有焦点(FocusedElement)以及当事件发生时按下了哪些修饰键,并且可通过位逻辑来检查其状态。

if((e.KeyboardDevice.Modifiers & ModifierKeys.Control)=ModifierKeys.Control)
{
	lblIndo.Text = "You held the Control Key";
}

  KeyboardDevice属性还提供了几个简便的方法,这些方法都需要传入一个Key枚举值。

名称说明
IsKeyDown()当事件发生时,通知是否按下了该键
IsKeyUp()当事件发生时,通知是否释放了此键
IsKeyToggled()当事件发生时,通知当键是否处于"打开"状态。该方法只能对那些能够打开、关闭的键有意义。如Caps Lock、Scroll Lock、 Num Lock
GetKeyStates()返回一个或多个KeyStates枚举值,指明该键当时是否被释放了、按下了或处于切换转台。该方法本质 上和同一个键同时调用IsKeyDowm()方法和IsKeyToggled()方法相同

  当使用KeyEventArgs.KeyboardDevice属性时,代码获取虚拟键状态。这意味着获取在事件发生时键盘的状态,这些状态和键盘的当前状态未必相同。例如,粉衣以下当用户输入速度超乎代码执行速度时会法神干什么情况?每次引发KeyPress事件时,都访问触发事件的按键,而不是刚输入的字符。这几乎总是想得到的行为。
  然而,没有限制在键盘事件中获取键的信息,也可以随时获取键盘状态信息。技巧时使用使用KeyBoard类,该了和KeyboardDevice类非常类似,指示Keyboard类由静态程序构成。

if (Keyboard.IsKeyDown(Key.LeftShift))
{
	lblbInfo.Text = "The left Shift is held dowm";
}

5.5 鼠标输入

  鼠标事件执行几个关联任务。当鼠标移动到某个元素上时,可通过最基本的鼠标事件进行相应。这些事件是MouseEnter和MouseLeave。这两个事件是直接事件,这意味着它们不使用冒泡和隧道过程,而源自一个元素并且只被该元素引发。还可以响应PreviewMouseMove事件(隧道路由事件)和MouseMove事件(冒泡路由事件),只要移动鼠标就会引发这两个事件。所有这些使劲按都位代码提供关了相同信息,MouseEventArgs对象。MouseEventArgs对象包含当使劲按发生时表示 鼠标键状态的属性,以及GetPosition()方法,该方法返回相对所选元素的坐标。

5.5.1 鼠标单击

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

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

  所有鼠标事件都提供MouseButtonEventArgs对象。MouseButtonEventArgs类继承自MouseEventArgs类,并添加了几个成员。这些成员中相对不重要的时MouseButton和ButtonState。ClickCount属性更有趣,该属性用于通知鼠标键被单击了多少次,从而可以区分是单击还是双击。

5.5.2 捕获鼠标

  通常,元素每次接收到鼠标键"按下"事件后,不久就会接收到对应的鼠标键"释放"事件。但情况不见得总是如此。例如,如果单击一个元素,保持按下鼠标键,然后移动鼠标指针离开元素,这时该元素就不会接收到鼠标释放事件。
  某些情况下,可能希望通知鼠标键释放事件,即鼠标键释放事件是在鼠标已经离开了原来的元素之后发生的。为此,需要调用Mouse.Capture()方法并传递当前的元素以捕获鼠标。此后,就会接收到鼠标键按下和释放事件,直到再次调用Mouse.Capure()方法并传递空引用为止。当鼠标被一个元素捕获后,其他元素就不会接收到鼠标事件这意味着用户不能单击窗口中其他位置的按钮 ,不能单击文本框的内部。鼠标捕获有时用于可以被拖放并可以改变尺寸的元素。

5.5.3 鼠标拖放

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

  1. 用户单击元素(或选择元素中的一块特定区域),并保持鼠标键位按下状态。这时,某些信息被搁置起来,并且拖放操作开。
  2. 用户将鼠标移动到其他元素上。如果该元素可接收正在拖动的内容的类型,鼠标指针会变成拖放图标,否则鼠标指针会变成内部有一条线的圆形。
  3. 当用户释放鼠标时,元素接收信息并决定如何处理接收到的信息。在没有释放鼠标键时,可按下Esc键取消该操作。

  例如,可在窗口中添加两个文本框来尝试拖放操作支持的工作方式,因为TextBox控件提供了支持拖放的内置逻辑。如果选中文本框中的一些文本,就可以将这些文本拖动到另一个文本框中。当释放鼠标键时,这些文本将移动位置。同意技术在两个应用程序也可以工作——例如,可以从Word文档中拖拽一些文本,并放入到WPF应用程序的TextBox对象中,也可将文本从WPF应用程序的TextBox对象拖动到Word文档中。
  拖放操作有两个方法:源和目标。为了创建拖拽源,需要在某个位置调用DragDrop.DoDragDrop()方法初始化拖放操作。此时确定拖动操作的源,搁置希望移动的内容,并指明允许什么样的拖放效果(复制、移动等)。通常,在响应MouseDown或PreviewMouseDown事件时调用DoDragGrop()方法
  接收数据的元素需要将它的AllowDrop属性设置为true。此外,它还需要通过处理Drop事件来处理数据。将AllowDrop属性设置为true时,就将元素配置为允许任何类型的信息。如果希望有选择地接收内容,可处理DragEnter事件。这时,可以检查正在拖动的内容的数据类型,然后确定所允许的操作类型。

  例子源码如下:

<Window x:Class="RoutedEvents.DragAndDrop"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="DragAndDrop" Height="150" Width="350">
    <Grid Margin="5">
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>

        <TextBox Grid.Row="0" Grid.Column="0" Padding="10">
            <TextBox.HorizontalContentAlignment>Center</TextBox.HorizontalContentAlignment>
            <TextBox.VerticalContentAlignment>Center</TextBox.VerticalContentAlignment>
            <TextBox.VerticalAlignment>Center</TextBox.VerticalAlignment>
            <TextBox.TextAlignment>Center</TextBox.TextAlignment>
            <TextBox.Text>Drag from this TextBox</TextBox.Text>
        </TextBox>

        <Label Grid.Row="0" Grid.Column="1" Padding="20"
               MouseDown="lblSource_MouseDown">
            <Label.Background>LightGreen</Label.Background>
            <Label.HorizontalAlignment>Center</Label.HorizontalAlignment>
            <Label.VerticalAlignment>Center</Label.VerticalAlignment>
            <Label.Content>Or this Label</Label.Content>
        </Label>
        <Label Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" Padding="20" AllowDrop="True"
               Drop="lblTarget_Drop"
               DragEnter="lblTarget_DragEnter">
            <Label.Background>LightGreen</Label.Background>
            <Label.HorizontalAlignment>Stretch</Label.HorizontalAlignment>
            <Label.VerticalAlignment>Center</Label.VerticalAlignment>
            <Label.HorizontalContentAlignment>Center</Label.HorizontalContentAlignment>
            <Label.Content>To this Label</Label.Content>
        </Label>
    </Grid>
</Window>
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace RoutedEvents
{
    public partial class DragAndDrop : Window
    {
        public DragAndDrop()
        {
            InitializeComponent();
        }

        private void lblSource_MouseDown(object sender, MouseButtonEventArgs e)
        {
            var lbl = (Label) sender;
            DragDrop.DoDragDrop(lbl, lbl.Content, DragDropEffects.Copy);
        }

        private void lblTarget_Drop(object sender, DragEventArgs e)
        {
            ((Label) sender).Content = e.Data.GetData(DataFormats.Text);
        }

        private void lblTarget_DragEnter(object sender, DragEventArgs e)
        {
            e.Effects = e.Data.GetDataPresent(DataFormats.Text) ? DragDropEffects.Copy : DragDropEffects.None;
        }
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zhy29563

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值