路由事件能沿着元素树隧道向下或冒泡向上旅游,并且被沿路的事件处理器处理。路由事件能在一个元素上被处理(诸如一个标签)即使它由另一个元素发起(诸如该标签内部的一个图像)。正如依赖属性,路由事件能用传统的方法消费—用正确签名连接一个事件处理器—但是你需要理解他们如何工作,以了解它们的所有特征。

理解路由事件

.NET开发者都熟悉事件的思想—对象(诸如WPF元素)发送消息通知你的代码有事发生。WPF用事件路由增强.NET事件模型的概念。事件路由允许事件由一个元素发起,但是由另一个元素处理。例如,点击工具栏按钮,开始事件,上升到工具栏,然后到包含窗口,直到事件被你的代码处理。

事件路由让你能灵活地写紧凑、组织良好的代码,在最便利的地方处理事件。对于WPF内容模型,事件路由也是必要的,它允许建立简单元素(如按钮)包含一打不同的成分,每个都有自己独立的事件集合。

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

路由事件是只读静态字段,在静态构造函数内注册,包装为标准的.net事件定义。

例如,Button类提供了Click事件,它从ButtonBase抽象类继承。这是事件的定义与注册代码:

public abstract class ButtonBase : ContentControl, ...
{
    // 定义事件
    public static readonly RoutedEvent ClickEvent;

    // 注册事件
    static ButtonBase()
    {
        ButtonBase.ClickEvent = EventManager.RegisterRoutedEvent(
          "Click", RoutingStrategy.Bubble,
          typeof(RoutedEventHandler), typeof(ButtonBase));
        ...
    }

    // 包装事件
    public event RoutedEventHandler Click
    {
        add
        {
            base.AddHandler(ButtonBase.ClickEvent, value);
        }
        remove
        {
            base.RemoveHandler(ButtonBase.ClickEvent, value);
        }
    }

    ...
}

路由事件用EventManager.RegisterRoutedEvent()方法注册。你要指定事件名称,路由类型,定义事件处理器语法的委托(本例中,RoutedEventHandler),和拥有事件的类(本例中,ButtonBase)。

通常,路由事件包装为普通的.NET事件,所有的.NET语言都可以访问路由事件。事件包装器使用AddHandler()RemoveHandler()方法添加和移除注册调用者,二者都被定义在FrameworkElement基类中,所有WPF元素都继承了这两个方法。

共享路由事件

路由事件能在类之间共享。例如,二个基类UIElementContentElement都使用了MouseUp事件,MouseUp事件定义在System.Windows.Input.Mouse类中。UIElementContentElement类简单地使用RoutedEvent.AddOwner()方法重用事件:

UIElement.MouseUpEvent = Mouse.MouseUpEvent.AddOwner(typeof(UIElement));

引发路由事件

当然,和任何事件一样,定义类需要在某些点引起路由事件,确切地发生在哪里是一个实现细节。但是,重要的因素是你的事件不是通过传统.NET事件包装器引起的。而是使用RaiseEvent()方法,所有元素从UIElement类继承了此方法。这是ButtonBase类中的相应代码:

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

RaiseEvent()方法负责引发事件到每个调用者。调用者可以直接调用AddHandler()注册自己,或他们能使用事件包装器。

所有的WPF事件签名遵守.NET约定。第一个参数是引发事件的对象(sender)。第二个参数是EventArgs对象,附加可能用到的所有细节。例如,MouseUp事件提供MouseEventArgs对象,指示当事件发生时鼠标按压了哪个按钮:

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

如果一个事件不需要传送额外的细节,则使用RoutedEventArgs类,它包含一些关于事件如何被路由的细节。如果事件需要传送额外的信息,它使用一个更特殊的RoutedEventArgs派生对象(诸如MouseButtonEventArgs)。因为每个WPF事件参数类从RoutedEventArgs派生,每个WPF事件处理器都能够访问关于事件路由的信息。

处理路由事件

可以在XAML标记中附加事件:

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

事件处理方法的命名约定是:ElementName_EventName

用代码连接事件处理器:

img.MouseUp += new MouseButtonEventHandler(img_MouseUp);

这个代码按照要求的签名为事件构造了委托对象(MouseButtonEventHandler),将委托指向img_MouseUp()方法,然后将委托添加到为img.MouseUp事件注册的事件处理器列表。

可以省略委托对象MouseButtonEventHandler的构造,以上代码也可以简写为:

img.MouseUp += img_MouseUp;

也可以直接使用UIElement.AddHandler() 连接事件处理器:

img.AddHandler(Image.MouseUpEvent,new MouseButtonEventHandler(img_MouseUp));

这个方法需要显示构造委托对象MouseButtonEventHandler,因为UIElement.AddHandler()方法支持所有的WPF事件,不知道你要添加的委托类型。

一些开发者更喜欢使用事件被定义类的名字,而不是事件被引发类的名字,这是等价的代码,明确MouseUp事件是定义在UIElement中:

img.AddHandler(UIElement.MouseUpEvent,new MouseButtonEventHandler(img_MouseUp));

解除事件处理器只能通过代码,可以使用自减操作符:

img.MouseUp -= img_MouseUp;

也可以使用UIElement.RemoveHandler()方法:

img.RemoveHandler(Image.MouseUpEvent,new MouseButtonEventHandler(img_MouseUp));

不要多次重复添加事件处理器。

事件路由

路由事件有三个流派:

  • 直接事件,某个元素引发事件后,不传递到其他元素。
  • 冒泡事件,沿元素树自底到顶依次引发事件,从引发事件的元素开始,直到元素树根结点。
  • 隧道事件,

RoutedEventArgs类

对于冒泡事件,sender参数指向链条最后一环。例如,如果一个事件从图像冒泡到标签,则sender参数指向标签。

RoutedEventArgs类的属性:

| Source |
指示什么对象引发事件。当事件发生时,在键盘事件情况下,指拥有焦点的控件。在鼠标事件情况下,指鼠标指针下最顶的元素。 |

| OriginalSource |
指最初引发事件的对象。通常,OriginalSource等同于Source。但是,在某些情况下,OrignialSource指对象树更深的元素。例如,如果点击了窗口的边界,Source是Window对象,但是,OriginalSource是Border对象。 |

| RoutedEvent |
提供RoutedEvent对象,用于事件处理器的触发,如静态的UIElement.MouseUpEvent对象。这些信息常用在使用相同的事件处理器处理不同的事件时。 |

| Handled |
用于中止冒泡或者隧道过程。当Handled为true时,事件不再旅游,不会再为任何其他的元素引发事件。 |

冒泡事件

下面是一个简单的窗口用来演示事件冒泡。当你点击标签的一个部分,事件次序被显示在一个列表框。MouseUp事件旅行通过5层,向上结束在BubbledLabelClick窗口。

bubblelabelclick

为创造这测试窗格,图像和它的每个长辈元素被连线到相同的事件处理器—SomethingClicked()方法。

<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>
      <RowDefinition Height="*"></RowDefinition>
      <RowDefinition Height="Auto"></RowDefinition>
      <RowDefinition Height="Auto"></RowDefinition>
    </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">
         Image and text label</TextBlock>
        <Image Source="happyface.jpg" Stretch="None" 
         MouseUp="SomethingClicked" />
        <TextBlock Margin="3" 
         MouseUp="SomethingClicked">
         Courtesy of the StackPanel</TextBlock>
      </StackPanel>
    </Label>
     
    <ListBox Grid.Row="1" Margin="5" Name="lstMessages"></ListBox>
    <CheckBox Grid.Row="2"  Margin="5" Name="chkHandle">
     Handle first event</CheckBox>
    <Button Grid.Row="3" Margin="5" Padding="3" HorizontalAlignment="Right"
     Name="cmdClear" Click="cmdClear_Click">Clear List</Button>
  </Grid>
</Window>

SomethingClicked()方法简单地检查RoutedEventArgs对象的属性和添加一个消息到列表框:

protected int eventCounter = 0;

private void SomethingClicked(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;
    lstMessages.Items.Add(message);
    e.Handled = (bool)chkHandle.IsChecked;
}

如果你已经选择chkHandle复选框,SomethingClicked()方法设置RoutedEventArgs.Handled属性为真,在事件第一次发生时,停止事件冒泡序列。结果,你将会看到只有第一个事件出现在列表。

因为SomethingClicked()方法处理由Window引发的MouseUp事件,你将能拦截在列表框和空白的窗口表面上的点击。但是,当你点击Clear按钮(它移除所有的列表框内容)时,MouseUp事件不会被引发。那是因为按钮压缩掉了MouseUp事件,和引起一个更高水平的Click事件。与此同时,Handled标记被设置为真,阻止MouseUp事件继续。

处理压缩事件

使用AddHandler()方法,设置第三个参数为true,即使Handled标志为真,也会收到事件:

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

从设计角度讲,这不是一个好决定。

附加事件

一个面板,里面有一组按钮。众所周知,面板不支持按钮的Click事件,那么如何在面板上定义这组按钮的Click事件?

答案是使用附加事件。附加事件的语法形如:ClassName.EventName,这是代码示例:

<StackPanel Button.Click="DoSomething" Margin="5">
  <Button Name="cmd1">Command 1</Button>
  <Button Name="cmd2">Command 2</Button>
  <Button Name="cmd3">Command 3</Button>
  ...
</StackPanel> 

在使用代码绑定附加事件时,只能使用UIElement.AddHandler()方法,不能使用自加操作符。

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

DoSomething()方法中,有几个方法可以获知是哪一个按钮引发的事件:

  • 根据按钮的文本、或者按钮的名字区分按钮。这个方法因为无法进行编译时检查,最好不使用。
  • 为每个按钮设置名字,然后通过名字访问相应的字段对象与sender或者e.Source比较,这是最好的方法。
  • 也可以通过测试Tag属性区分按钮。

对象比较法测试按钮:

private void DoSomething(object sender, RoutedEventArgs e)
{            
    if (e.Source == cmd1)
    { ... }
    else if (e.Source == cmd2)
    { ... }
    else if (e.Source == cmd3)
    { ... }
}

使用Tag标签测试按钮:

private void DoSomething(object sender, RoutedEventArgs e)
{
    object tag = ((FrameworkElement)sender).Tag;
    MessageBox.Show((string)tag);
}

隧道事件

隧道事件都以单词Preview开头,隧道事件与冒泡事件通常成对出现,总是先引发隧道事件,再引发冒泡事件。

隧道事件与冒泡事件工作相同,方向相反。例如,上例中,如果定义的是隧道事件PreviewMouseUp,点击标签中的图像后,首先引发窗口的PreviewMouseUp事件,然后是Grid,然后是StackPanel等等,直到到达实际源,就是标签中的图像。

image

如果你标记隧道事件为Handled,冒泡事件就不会发生。因为两事件共享相同的RoutedEventArgs类。

隧道事件主要用于执行一些预处理,比如,对特定击键采取行动或者过滤掉特定鼠标行为。

WPF事件

事件分五类:

  • Lifetime事件
  • 鼠标事件
  • 键盘事件
  • Stylus事件
  • Multitouch事件

鼠标、键盘、Stylus、Multitouch事件放到一起统称为输入事件。

Lifetime事件

FrameworkElement类定义的Lifetime事件

Initialized

发生在元素被实例化,并且它的属性按照XAML标记设置完成以后。在这一点,元素被初始化,但是,还没有应用样式和数据绑定。在这一点,IsInitialized属性为true。Initialized是直接事件,不是路由事件。

Loaded

发生在整个窗口被初始化,并且样式和数据绑定已经应用。这是元素呈现之前的最后一站。在这一点,IsLoaded属性为true。

Unloaded

发生在下列情况出现时,元素释放,容器窗口关闭,指定元素从窗口被移除。

FrameworkElement实现了ISupportInitialize接口,有两个方法:BeginInit()、EndInit()。呈现过程:

  1. 元素实例化
  2. BeginInit()
  3. 设置元素的所有属性
  4. 触发Initialized事件
  5. EndInit()

在创建一个窗口时,每个分支的元素按自底向上的顺序被初始化。这意味着,被嵌套的元素先于他们的容器初始化。当初始化事件发生时,当前元素的子元素肯定已完全初始化了。但是,包含该元素的元素可能没有初始化,并且也不能肯定窗口的其他部分被初始化。

在每个元素被初始化后,每个元素被放到各自的容器中,被样式化,绑定到数据源,如果必要的话。在窗口的Initialized事件发生后,是时候进到下一步了。

Loaded事件按照与Initialized事件相反的路径装载,换句话说,容器窗口首先被装载,接着是它包含的元素。当所有元素的Loaded事件发生完成,窗口被显示,元素被呈现。

相对于FrameworkElement类,窗口类添加了Lifetime事件:

SourceInitialized

发生在HwndSource属性被获得,在窗口呈现之前。HwndSource属性是遗留代码中的功能。

ContentRendered

发生在在窗口呈现之后。在这个事件中,不要有可能影响window视觉外观的代码,也不要强制执行第二次呈现操作。但是,ContentRendered事件确实指示窗口完全可见,并且准备好输入。

Activated

发生在用户切换到这个窗口时,可能从应用程序的另一个窗口,也可能从另外一个应用程序。窗口第一次加载时,也会引发Activated事件。相当于控件的GotFocus事件。

Deactivated

发生在用户从窗口离开时,可能到应用程序的另一个窗口,也可能到另外一个应用程序。当窗口被关闭时,在Closing事件之后,Closed事件之前,引发Deactivated事件。相当于控件的LostFocus事件。

Closing

发生在窗口被关闭时。或者用户操作,或者在代码中使用Window.Close()方法,或者Application.Shutdown()方法。通过设置CancelEventArgs.Cancel属性为true,Closing事件可以取消关闭操作,保持窗口打开。但是,如果计算机关机或者日志关闭不会发生Closing事件。为了处理这个可能性,你需要处理Application.SessionEnding事件。

Closed

发生在窗口关闭以后。但是,元素对象仍是可访问的,并且Unloaded事件还没有发生。在此处,你能执行清除,写设置到配置文件,到注册表等等。

如果你只想在首次初始化控件,最好在Loaded事件引发时。通常,凡是初始化都能在Window.Loaded事件中执行。

也可以在Window构造函数的InitializeComponent()语句之后添加初始化代码。但是,如果这些初始化代码会引发异常,最好将初始化代码移到Loaded事件中。

输入事件

输入事件的自定义事件参数由InputEventArgs类派生。

InputEventArgs类添加二属性:Timestamp和Device。

Timestamp提供一个整数,使用毫秒指示事件何时发生。时间戳不表示实际的时间。大的时间戳值表示后发生的事件。

Device返回触发事件设备的信息对象。可以是鼠标、键盘、或手写笔。每个都是一个类,都派生自抽象的System.Windows.Input.InputDevice类。

下几节,将详细介绍如何处理鼠标、键盘、和多点触摸行为。

键盘输入

所有元素按发生顺序的键盘事件:

PreviewKeyDown

隧道事件,发生在键按下时

KeyDown

冒泡事件,发生在键按下时

PreviewTextInput

隧道事件,发生在击键完成,且元素收到文本输入时。当按键却没有文本输入时,此事件不会发生。如:按Ctrl、Shift、Backspace、箭头键、功能键等等。

TextInput

冒泡事件,发生在击键完成,且元素收到文本输入时。当按键却没有文本输入时,此事件不会发生。

PreviewKeyUp

隧道事件,发生在键被释放时。

KeyUp

冒泡事件,发生在键被释放时。

一些控件压缩一些事件,他们能执行自己更特殊的键盘处理。例如,文本框控件压缩了TextInput事件,也压缩了功能键的KeyDown事件。但是,没有压缩隧道事件PreviewTextInput和PreviewKeyDown,它们仍能使用。

文本框控件也添加新的事件,TextChanged事件。发生在紧接着一个击键引发文本框改变文本以后。这时,新的文本已显示在文本框中。所以太迟不能阻止不希望的击键。

处理按键

范例:监视一个文本框所有的可能的键事件,并且报告事件何时发生。图形5-6显示键入一个大写字母S的结果。

每次按键都会引发PreviewKeyDown和KeyDown事件。但是,TextInput事件只有当一个字符被键入到元素中才被引发。实际上,键入行为可能涉及组合按键。例如,当键入大写字母S时,你使用Shift+S键,结果,你将会看到两个KeyDown和KeyUp事件,但是只有一个TextInput事件。

PreviewKeyDown、KeyDown、PreviewKeyUp、和KeyUp事件都使用KeyEventArgs对象提供信息。最重要的是Key属性,返回System.Windows.Input.Key枚举值。识别被按下或释放的键。这是处理键事件的事件处理器:

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

Key值不考虑其它键的状态。例如,当按下S键时,不管是否按下Shift键,都将获得相同的键值(Key.S)。

默认情况下,按住一个键一段时间后,会重复键值。所以,当输入Shift+S时,可能会产生多个Shift,如果你希望忽视重复Shift键,依靠检查KeyEventArgs.IsRepeat属性,可以确定是否一个击键是键一直被按着的结果。如下所示:

if ((bool)chkIgnoreRepeat.IsChecked && e.IsRepeat) return;

TextInput事件提供TextCompositionEventArgs对象。此对象包含一个Text属性,是控件即将收到的处理文本。见代码:

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

KeyConverter用于将Key枚举值转换为更有用的字符串:

KeyConverter converter = new KeyConverter();
string key = converter.ConvertToString(e.Key);

考虑一个只接受数字的文本框,需要在PreviewTextInput、和PreviewKeyDown事件中都进行验证,因为有些输入会旁路掉PreviewTextInput事件,而且,PreviewKeyDown过于低层次,对于相同的输入提供了过多的信息。最佳做法是,在PreviewTextInput中验证大多数输入,在PreviewKeyDown中验证PreviewTextInput漏掉的输入。

private void pnl_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
    short val;
    if (!Int16.TryParse(e.Text, out val))
    {
        // 拒绝非数字键输入
        e.Handled = true;
    } 
}

private void pnl_PreviewKeyDown(object sender, KeyEventArgs e)
{
    if (e.Key == Key.Space)
    {
        // 拒绝空格键,它不会引发PreviewTextInput事件。
        e.Handled = true;
    }
}

你能连接这两个事件处理器到单个的文本框,也可连接他们到包括几个仅输入数字的文本框的容器中,以提高编码效率。

焦点

为了控件能接受焦点,它的Focusable属性必须是true,这也是所有控件的默认值。Focusable属性定义在UIElement类中,这意味着非控件元素也能获得焦点。通常,非控件类的Focusable默认为false。但是,你能设置它为true。

如果没有显式设置Tab序列,当你按下Tab键切换焦点时,焦点将移动到当前元素的第一个子元素,如果当前的元素没有子元素,到同层的下一个子元素。

如果为每个控件设置TabIndex属性,TabIndex为0的控件获得第一焦点,紧接着跳跃到下一个最高TabIndex值(例如1,然后2,然后3,等等)。如果一个以上元素具有相同的TabIndex值,则使用自动的tab序列,跳到随后最近的元素。

所有控件的TabIndex属性的默认值为Int32.MaxValue。

除了TabIndex属性,Control类还定义了IsTabStop属性,设为false时,按压Tab键,控件将不能获得焦点。但是,与Focusable为false不同,控件仍然能通过其他方式(如鼠标点击、代码中调用Fucus()方法)获得焦点。

当控件是不可见或不可用时,不管如何设置TabIndex、IsTabStop、和Focusable属性,这些控件都不包括在Tab序列中。控制可见的属性是Visibility,控制可用的属性是IsEnabled。

键状态

键事件的KeyEventArgs对象包含一个KeyStates属性,反映了触发事件键的属性。KeyboardDevice属性提供对于键盘上的任何键相同的信息。

KeyboardDevice属性提供KeyboardDevice类的一个实例。它有两个属性:FocusedElement、Modifiers。焦点元素是指事件发生时拥有焦点的元素。修饰符是指当事件发生时什么修饰符键被按下,包括Shift,Ctrl,和Alt。并且你能使用按位逻辑核对它们的状况,像这样:

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

KeyboardDevice属性也提供几个方便的方法,列在表5-5。对于每个方法,你传递入一个Key枚举值。

IsKeyDown(),IsKeyUp():测试键是按下或是未按下。

IsKeyToggled():键是否是在一个开启状态。这只能应用于具有开关状态的键,诸如Caps Lock,Scroll Lock,和Num Lock。

GetKeyStates():返回一个或多个KeyStates枚举值,告诉你目前这个键是否是未按下、按下、或开启状态。这方法本质上等同于同时调用IsKeyDown()和IsKeyToggled()。

当你使用KeyEventArgs.KeyboardDevice属性,你的代码获得虚拟的键状态。这意味着它获得在事件发生时键盘的状态。这不是必然地等同于当前键盘状态。例如,考虑如果用户打字比代码执行更快发生什么。每次你的KeyPress事件引发,你将访问那个引发事件的击键,而不是刚键入的字符。这几乎总是你希望的行为。

但是,你不限于在键事件中获得键信息。你能在任何时间获得键盘状态。诀窍是使用键盘类Keyboard,它非常类似于KeyboardDevice,除了它的成员是静态的。这是一个例子,使用Keyboard类检出左Shift键当前的状态:

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

鼠标输入

最根本的鼠标事件允许你对鼠标在一个元素上移动作出反应。这些事件是MouseEnter(当鼠标指针在元素上移动)和MouseLeave(当鼠标指针离开元素时)。这两个事件都是直接事件。

例如,如果你有一个StackPanel,其中包含一个按钮。你移动鼠标指针到按钮上,MouseEnter事件将首先对StackPanel引发(因为你进入它的边界),然后对于按钮(因为你移动直接在它上)。当你移开鼠标,MouseLeave事件将首先对按钮引发,然后对StackPanel。

鼠标移动时会引发两个事件:PreviewMouseMove(隧道事件)和MouseMove(冒泡事件)。所有的这些事件提供一个MouseEventArgs对象。MouseEventArgs对象包含一些鼠标按钮状态的属性。它包含一个GetPosition()方法,获得相对于一个元素的坐标。这是一个例子,用与设备无关的像素显示鼠标指针相对于窗口的位置:

private void MouseMoved(object sender, MouseEventArgs e)
{
    Point pt = e.GetPosition(this);
    lblInfo.Text = 
      String.Format("You are at ({0},{1}) in window coordinates",
      pt.X, pt.Y);
}

在本例中,坐标从窗口客户区域的左上角测量(就在标题栏下面)。

UIElement类有两个属性:

IsMouseOver,确定是否鼠标在一个元素上,或者在它的一个子元素上。

IsMouseDirectlyOver,确定是否鼠标在一个元素上,但不在它的子元素上。

鼠标点击

所有元素的鼠标点击事件

| PreviewMouseLeftButtonDown PreviewMouseRightButtonDown
| 隧道 | 发生在按下鼠标按钮时 |

| MouseLeftButtonDown MouseRightButtonDown
| 冒泡 | 发生在按下鼠标按钮时 |

| PreviewMouseLeftButtonUp PreviewMouseRightButtonUp
| 隧道 | 发生在鼠标按钮释放时 |

| MouseLeftButtonUp MouseRightButtonUp
| 冒泡 | 发生在鼠标按钮释放时 |

所有的鼠标按钮事件提供一个MouseButtonEventArgs对象。MouseButtonEventArgs类从MouseEventArgs类派生(这意味着它包含相同的坐标和按钮状态信息),并且它添加几个成员。MouseButton(告诉你哪一个按钮触发了事件)和ButtonState(告诉你当事件发生时按钮被按压或未被按压)。ClickCount,这告诉你按钮被点击多少次,可用于区分单击(ClickCount是1)、双击(ClickCount是2)。

通常,Windows应用在被鼠标点击后反应(up事件而不是down事件)。

一些元素添加更高水平的鼠标事件。例如,Control类添加PreviewMouseDoubleClick和MouseDoubleClick事件代替MouseLeftButtonUp事件。类似地,Button类有一个Click事件,能被鼠标或键盘触发。

为获得当前的鼠标位置和鼠标按钮状态,你能使用Mouse类的静态成员,类似于MouseButtonEventArgs的相应成员。