鼠标双击事件不冒泡

17

我的情况比较简单:我有一个包含雇员行的ListView,每个雇员行中都有“增加”和“减少”按钮来调整他的薪水。

假设在我的程序中,双击一个雇员行意味着“解雇这个人”。

问题是,在我快速点击“增加”按钮时,这会触发ListViewItem的双击事件。当然,我只是想增加他们的工资,而不是解雇他们。

根据其他所有事件的工作方式,我希望能够通过在事件上设置Handled=true来解决此问题。然而,这并不起作用。在我看来,WPF生成了两个完全不相关的双击事件。

以下是重现我的问题所需的最小示例。可见的组件:

<ListView>
    <ListViewItem MouseDoubleClick="ListViewItem_MouseDoubleClick">
            <Button MouseDoubleClick="Button_MouseDoubleClick"/>
    </ListViewItem>
</ListView>

处理程序代码:

private void Button_MouseDoubleClick(object s, MouseButtonEventArgs e) {
    if (!e.Handled) MessageBox.Show("Button got unhandled doubleclick.");
    e.Handled = true;
}

private void ListViewItem_MouseDoubleClick(object s, MouseButtonEventArgs e) {
    if (!e.Handled) MessageBox.Show("ListViewItem got unhandled doubleclick.");
    e.Handled = true;
}

运行此程序并双击列出的按钮后,两个消息框按顺序显示。(此外,此按钮在此之后处于按下状态。)

作为一个“修复”,在ListViewItem处理程序中,我可以检查事件附加的可视树,并检查“是否有一个按钮存在其中”,从而丢弃该事件,但这是最后的手段。在编写这样的补丁之前,我希望至少了解问题。

有人知道WPF为什么会这样做,以及避免此问题的一种优雅的惯用方法吗?

7个回答

10

我认为你会发现MouseDoubleClick事件是在MouseDown事件之上的一个抽象层。也就是说,如果两个MouseDown事件在足够快的时间内发生,那么MouseDoubleClick事件也会被触发。如MSDN所述,无论这个路由事件似乎遵循元素树中的冒泡路线,它实际上是一个由每个UIElement沿着元素树直接引发的路由事件如果你在MouseDoubleClick事件处理程序中设置Handled属性为true,则随后的MouseDoubleClick事件将以Handled设置为false的状态发生。

你可以尝试在Button 上处理 MouseDown并将其设置为已处理,这样它就不会传播到ListViewItem

希望我能自己验证,但目前我没有 .NET 环境。


谢谢,但我也无法完全适配。在Button上(或Button周围的容器包装器上)的MouseDown / MouseLeftButtonDown处理程序从未被触发,因此似乎事件已经被Button“Handled”了。此外,我无法从ListViewItem上的MouseDown / MouseLeftButtonDown推断出双击,因为这些事件被ListViewItem内容中的其他可视组件(如文本框)处理和吞噬了。 - Deestan

8
MSDN文档中提供了如何防止MouseDoubleClick事件冒泡的建议:

希望处理鼠标双击的控件作者应在ClickCount等于2时使用MouseLeftButtonDown事件。这将导致Handled状态在元素树中的其他元素处理事件的情况下适当地传播。

因此,您可以处理MouseLeftButtonDown事件,并在ClickCount为2时将Handled设置为true。但是,对于按钮,此方法会失败,因为它们已经处理了MouseLeftButtonDown事件并且不会引发该事件。
但是仍然有PreviewMouseLeftButtonDown事件。在您的按钮上使用它,在ClickCount等于2时将handled设置为true,如下所示:
 private void Button_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)   {
     if (e.ClickCount == 2)
         e.Handled = true;
 }

但是仍然有PreviewMouseLeftButtonDown事件。在您的按钮上使用它将handled设置为true - 这不起作用,因为Preview事件首先通过ListViewItem。 - Deestan
@Deestan,是的,但 PreviewMouseLeftButtonDown 将在 PreviewMouseDoubleClick 之前触发。ListViewItem 只会寻找后者, 所以您可以处理前者以停止它。 - Robert Levy
@Robert Levy:啊,我明白了。是的,那样有点用。但是会出现视觉故障,如果你在双击后按住按钮,它不会下降。而且,这只是偶然有效 - 我们没有遵循任何记录的行为。 - Deestan
虽然这并不是一个完全可接受的答案,但这是最佳的建议解决方法。感谢您的贡献! - Deestan

4

由于这个问题还没有明确的答案,我采用了以下的解决方法:

protected override void ListViewItem_MouseDoubleClick(MouseButtonEventArgs e) {
    var originalSource = e.OriginalSource as System.Windows.Media.Visual;
    if (originalSource.IsDescendantOf(this)) {
        // Test for IsDescendantOf because other event handlers can have changed
        // the visual tree such that the actually clicked original source
        // component is no longer in the tree.
        // You may want to handle the "not" case differently, but for my
        // application's UI, this makes sense.
        for (System.Windows.DependencyObject depObj = originalSource;
             depObj != this;
             depObj = System.Windows.Media.VisualTreeHelper.GetParent(depObj))
        {
            if (depObj is System.Windows.Controls.Primitives.ButtonBase) return;
        }
    }

    MessageBox.Show("ListViewItem doubleclicked.");
}

为了文档说明的目的,这里不必要地使用了完整的命名空间来定义类名。


4

虽然这种方法可能不太优雅或习惯,但您可能会更喜欢它,而不是您当前的解决方法:

    int handledTimestamp = 0;

    private void ListViewItem_MouseDoubleClick(object sender, MouseButtonEventArgs e)
    {
        if (e.Timestamp != handledTimestamp)
        {
            System.Diagnostics.Debug.WriteLine("ListView at " + e.Timestamp);
            handledTimestamp = e.Timestamp;
        }
        e.Handled = true;
    }

    private void Button_MouseDoubleClick(object sender, MouseButtonEventArgs e)
    {
        if (e.Timestamp != handledTimestamp)
        {
            System.Diagnostics.Debug.WriteLine("Button at " + e.Timestamp);
            handledTimestamp = e.Timestamp;
        }
        e.Handled = true;
    }

奇怪的是,如果您不设置e.Handled = true,它将无法正常工作。如果您不设置e.Handled并在按钮处理程序中设置断点或Sleep,您将看到ListView处理程序中的延迟。(即使没有显式延迟,仍然会有一些小的延迟,足以破坏它。)但是一旦您设置了e.Handled,无论延迟多长时间,它们都将具有相同的时间戳。我不确定这是为什么,也不确定这是否是可以依赖的文档行为。


虽然不太美观,但很有趣。 :) 由于两个点击事件都源自相同的MouseDown操作系统窗口消息,因此时间戳可以绑定到该消息上。如果确实在某处记录了这种行为,那么这是一个好的解决方案。可惜,MSDN在这里没有提供太多信息。 - Deestan
测试过了。总的来说它能工作,但是每当某些代码以某种原因触及事件处理队列时,它就会出问题。我不建议使用这个。 - Deestan
谢谢您提供的解决方法 - 最终我使用了一种变体。正如Deestan所指出的,在某些情况下(除了未标记为已处理的情况),时间戳是不同的。对连续事件之间的时间间隔进行阈值处理,将其设置为几百毫秒,对我来说效果很好。 - alexei

2

Control.MouseDoubleClick不是冒泡事件而是直接事件。

通过使用Snoop(一款用于浏览可视树和路由事件的工具),我发现 'ListView' 和 'ListBoxItem' 的 Control.MouseDoubleClick 事件同时被触发。你可以使用Snoop工具进行检查。


首先,为了找到答案,需要检查MouseDoublClick事件的两个事件参数是否为同一个对象。你会期望它们是相同的对象。如果是真的,那就像你的问题一样奇怪,但它们并不是相同的实例。我们可以使用以下代码进行检查。

RoutedEventArgs _eventArg;
private void Button_MouseDoubleClick(object s, RoutedEventArgs e)
{
    if (!e.Handled) Debug.WriteLine("Button got unhandled doubleclick.");
    //e.Handled = true;
    _eventArg = e;
}

private void ListViewItem_MouseDoubleClick(object s, RoutedEventArgs e)
{
    if (!e.Handled) Debug.WriteLine("ListViewItem got unhandled doubleclick.");
    e.Handled = true;
    if (_eventArg != null)
    {
        var result = _eventArg.Equals(e);
        Debug.WriteLine(result);
    }
}

这意味着MouseDoubleClick的事件参数在某个地方被重新创建,但我不太理解为什么会这样。
更清楚地说,让我们来检查BottonBase.Click的事件参数。它将返回有关检查相同实例的真值。
    <ListView>
        <ListViewItem ButtonBase.Click="ListViewItem_MouseDoubleClick">
            <Button Click="Button_MouseDoubleClick" Content="click"/>
        </ListViewItem>
    </ListView>

如果您只关注执行,那么有很多解决方案。如上所述,我认为使用标志(_eventArg)也是一个不错的选择。


1

我刚遇到了同样的问题。有一个简单但不明显的解决方案。

以下是如何通过控件引发双击事件....

private static void HandleDoubleClick(object sender, MouseButtonEventArgs e)
{
    if (e.ClickCount == 2)
    {
        Control control = (Control)sender;
        MouseButtonEventArgs mouseButtonEventArgs = new MouseButtonEventArgs(e.MouseDevice, e.Timestamp, e.ChangedButton, e.StylusDevice);
        if (e.RoutedEvent == UIElement.PreviewMouseLeftButtonDownEvent || e.RoutedEvent == UIElement.PreviewMouseRightButtonDownEvent)
        {
            mouseButtonEventArgs.RoutedEvent = Control.PreviewMouseDoubleClickEvent;
            mouseButtonEventArgs.Source = e.OriginalSource;
            mouseButtonEventArgs.OverrideSource(e.Source);
            control.OnPreviewMouseDoubleClick(mouseButtonEventArgs);
        }
        else
        {
            mouseButtonEventArgs.RoutedEvent = Control.MouseDoubleClickEvent;
            mouseButtonEventArgs.Source = e.OriginalSource;
            mouseButtonEventArgs.OverrideSource(e.Source);
            control.OnMouseDoubleClick(mouseButtonEventArgs);
        }
        if (mouseButtonEventArgs.Handled)
        {
            e.Handled = true;
        }
    }
}

因此,如果您处理PreviewMouseDoubleClick并在子控件上设置e.Handled = true,则父控件上的MouseDoubleClick不会触发。


-1
  1. 双击事件的触发方式很难更改,因为它们依赖于用户设置,并且该延迟在控制面板中进行了自定义。
  2. 您应该查看RepeatButton,它允许您按下按钮并在按下时生成多个点击事件。
  3. 如果您想要自定义事件冒泡,则应搜索预览事件,这允许您阻止事件的传播。什么是WPF预览事件?

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接