如何模拟鼠标按下和松开事件来实现最佳点击效果,或其他方法?

32
在WPF中,大多数控件都有MouseUpMouseDown事件(以及特定于鼠标按钮的变体),但没有一个简单的Click事件可以直接使用。如果要使用这些事件实现类似点击的行为,则需要同时处理两者,我认为这有点麻烦。

显而易见的问题是,您不能简单地省略MouseDown事件,因为如果您的点击是从另一个控件开始的,并在仅处理MouseUp的控件上释放,那么您所期望的点击将会触发,而实际上不应该: MouseUpMouseDown都应该发生在同一控件上。

因此,如果有任何更优雅的解决方案来解决这个通用问题,我将对此感兴趣。


注意:下面有几个好的解决方案,我选择接受Rachel的答案,因为它似乎得到了很好的接受,但此外,我想添加以下注释:

Rachel的按钮答案非常干净和简单,但你需要将你的实际控件包装在一个按钮中,而且在某些情况下,你可能不会认为你的控件是一个按钮,只是因为它可以被点击(例如如果它更像一个超链接)。此外,你需要每次引用一个模板。

Rick Sladkey的行为答案更直接地回答了最初的问题,即如何模拟单击/使控件可点击,缺点是你需要引用System.Windows.Interactivity,就像Rachel的解决方案一样,它会大量膨胀Xaml代码。

我的附加事件答案具有与Xaml-Markup相当接近的正常单击事件的优点,可以使用一个属性完成,我唯一看到的问题是代码中的事件附加没有被清晰地封装(如果有人知道如何修复,请在答案中添加评论)。


1
WPF控件与WinForms不同,因为它们旨在是无外观的。控件不再由UI和行为组成,而只是行为。按钮的默认UI基于所使用的主题,但对于任何旨在像按钮一样运作但不必看起来像按钮的内容,都可以进行覆盖。 - Rachel
我知道这已经很老了,但我注意到Rachel的答案存在一个问题,即当控件位于“ControlTemplate”中时,无法很容易地引用它们。解决方法是在每个控件上创建一个“Loaded”事件,并在代码中实例化它们。为了简单起见,我将坚持A.R.的解决方案。 - windowsgm
@killianmcc:你不应该在控件模板中引用控件,所以你可能做错了什么。你的实际内容不应该在ControlTemplate中,它只是你的内容的框架,然后会被放置在ContentPresenter所在的位置。此外,你可以在模板内绑定属性到按钮上,通常会为各种画刷和布局属性这样做。 - H.B.
放在 ContentPresenter 所在的位置,就像在 ControlTemplate 中一样?但我并不能绑定所有属性(例如,这是一个子窗口,我需要将文本传递给一个在 ControlTemplate 内部的 TextBlock 来显示)。 - windowsgm
@killianmcc:我修改了Rachel的答案,在示例中,Label将放置在模板中的ContentPresenter处。由于Label不是在模板内部的,所以您应该可以轻松引用它。除非您只想设置“文本”,否则不要将TextBlock放入模板中,然后您可以使用Text = "{TemplateBinding Content}",然后您将所有设置为按钮内容的内容最终都出现在TextBlock中(这可能会导致非文本异常;只需使用 ContentPresenter,它会自动绑定到Content并为文本创建TextBlock)。 - H.B.
哦,我现在明白了。对不起,谢谢! - windowsgm
5个回答

19

我会使用一个 Button 控件并覆盖 Button.Template 以直接显示内容。

<ControlTemplate x:Key="ContentOnlyTemplate" TargetType="{x:Type Button}">
    <ContentPresenter />
</ControlTemplate>

<Button Template="{StaticResource ContentOnlyTemplate}">
    <Label Content="Test"/>
</Button>

我正在寻找对非按钮控件(例如TextBlocks)的单击行为。 - H.B.
我会将可点击的项制作成一个按钮,并覆盖ControlTemplate以显示标签...我会更改我的帖子,以展示一个例子。 - Rachel
2
@H.B.:这是使用WPF的预期解决方案。控件是“无外观”的,因为它们可以以任何你选择的方式进行模板化 - 因此,任何需要“可点击”的控件都可以是具有自定义模板的按钮。 - Dan Puzey
@Rachel:我知道这只是一个例子,但我会删除Label,并在那里只放置一个ContentPresenter,这样就是创建可点击控件的一般方式。模板名称当然应该是像ClickableControl或者ContentOnlyTemplate这样的东西。@Dan Puzey:听起来很合理。 - H.B.
我过去使用过这种方法,从未遇到任何延迟。可能是模板的问题,需要查看代码才能确定是否是这种情况。 - Rachel
显示剩余3条评论

14

您可以向任何元素添加以下行为,以便它使用正常的按钮逻辑触发ButtonBase.Click事件:

public class ClickBehavior : Behavior<FrameworkElement>
{
    protected override void OnAttached()
    {
        AssociatedObject.MouseLeftButtonDown += (s, e) =>
        {
            e.Handled = true;
            AssociatedObject.CaptureMouse();
        };
        AssociatedObject.MouseLeftButtonUp += (s, e) =>
        {
            if (!AssociatedObject.IsMouseCaptured) return;
            e.Handled = true;
            AssociatedObject.ReleaseMouseCapture();
            if (AssociatedObject.InputHitTest(e.GetPosition(AssociatedObject)) != null)
                AssociatedObject.RaiseEvent(new RoutedEventArgs(ButtonBase.ClickEvent));
        };
    }
}

注意鼠标捕获/释放和输入命中测试检查的使用。有了这个行为,我们可以编写如下的点击处理程序:

<Grid>
    <Rectangle Width="100" Height="100" Fill="LightGreen" ButtonBase.Click="Rectangle_Click">
        <i:Interaction.Behaviors>
            <utils:ClickBehavior/>
        </i:Interaction.Behaviors>
    </Rectangle>
</Grid>

代码如下:

    private void Rectangle_Click(object sender, RoutedEventArgs e)
    {
        Debug.WriteLine("Code-behind: Click");
    }

将这段代码全部转换为代码后很容易实现;重要的部分是捕获和点击逻辑。

如果您不熟悉行为(behaviors),请安装Expression Blend 4 SDK并添加此命名空间:

xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"

并将System.Windows.Interactivity添加到您的项目中。

编辑:

以下是如何在代码后台将点击行为附加到元素并添加单击事件处理程序:

void AttachClickBehaviors()
{
    AttachClickBehavior(rectangle1, new RoutedEventHandler(OnAttachedClick));
}

void OnAttachedClick(object sender, RoutedEventArgs e)
{
    Debug.WriteLine("Attached: Click");
}

// Reusable: doesn't need to be in the code-behind.
static void AttachClickBehavior(FrameworkElement element, RoutedEventHandler clickHandler)
{
    Interaction.GetBehaviors(element).Add(new ClickBehavior());
    element.AddHandler(ButtonBase.ClickEvent, clickHandler);
}

现在已经测试过了,它和我的解决方案有相同的问题:似乎无法处理“弹出窗口”,这很烦人... - H.B.
事件处理程序通常对弹出窗口不起作用,因为它们不是可视树的一部分。解决方法是将处理程序(或行为)安装在弹出窗口的子元素上。您可以使用VisualTreeHelper.GetChild(popup, 0) 来获取该元素,或者您可能有一种通过名称访问它的方式。 - Rick Sladkey
我找不到 WPF 的文档,但是这篇 Silverlight 文档描述了问题:Visual Tree 之外的路由事件:http://msdn.microsoft.com/zh-cn/library/cc189018(v=vs.95).aspx - Rick Sladkey

5

好的,我想尝试一下附加事件并看看能做到什么程度,以下内容在大多数情况下都有效,但需要进行一些改进/调试:

public static class Extensions
{
    public static readonly RoutedEvent ClickEvent = EventManager.RegisterRoutedEvent(
        "Click",
        RoutingStrategy.Bubble,
        typeof(RoutedEventHandler),
        typeof(UIElement)
        );

    public static void AddClickHandler(DependencyObject d, RoutedEventHandler handler)
    {
        UIElement element = d as UIElement;
        if (element != null)
        {
            element.MouseLeftButtonDown += new MouseButtonEventHandler(element_MouseLeftButtonDown);
            element.MouseLeftButtonUp += new MouseButtonEventHandler(element_MouseLeftButtonUp);
            element.AddHandler(Extensions.ClickEvent, handler);
        }
    }

    public static void RemoveClickHandler(DependencyObject d, RoutedEventHandler handler)
    {
        UIElement element = d as UIElement;
        if (element != null)
        {
            element.MouseLeftButtonDown -= new MouseButtonEventHandler(element_MouseLeftButtonDown);
            element.MouseLeftButtonUp -= new MouseButtonEventHandler(element_MouseLeftButtonUp);
            element.RemoveHandler(Extensions.ClickEvent, handler);
        }
    }

    static void element_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        UIElement uie = sender as UIElement;
        if (uie != null)
        {
            uie.CaptureMouse();
        }
    }
    static void element_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        UIElement uie = sender as UIElement;
        if (uie != null && uie.IsMouseCaptured)
        {
            uie.ReleaseMouseCapture();
            UIElement element = e.OriginalSource as UIElement;
            if (element != null && element.InputHitTest(e.GetPosition(element)) != null) element.RaiseEvent(new RoutedEventArgs(Extensions.ClickEvent));
        }
    }
}

使用方法:

<TextBlock local:Extensions.Click="TextBlock_Click" Text="Test"/>

编辑:更改为使用鼠标捕获而不是存储发送器/源,因为它没有被充分清除。剩下的问题是您不能在代码中使用UIElement.AddHandler(Extensions.ClickEvent, handler)添加事件,因为这省略了其他鼠标事件的附件,这些事件需要用于触发单击事件(您需要使用Extensions.AddClickHandler(object, handler)代替)。


4

这里有一个我经常使用的简单解决方案。它易于操作,在各种情况下均可使用。

在您的代码中,放置以下内容。

private object DownOn = null;

private void LeftButtonUp(object sender, MouseButtonEventArgs e)
{
  if (DownOn == sender)
  {
    MessageBox.Show((sender as FrameworkElement).Name);
  }
  DownOn = null;
}

private void LeftButtonDown(object sender, MouseButtonEventArgs e)
{
  DownOn = sender;
}

现在你所需要做的就是设置对于你关心的所有控件的处理程序,即:
<Border Background="Red"  MouseLeftButtonUp="LeftButtonUp" MouseLeftButtonDown="LeftButtonDown".../>
<Border Background="Blue" MouseLeftButtonUp="LeftButtonUp" MouseLeftButtonDown="LeftButtonDown".../>

如果您有大量控件,并且不想在XAML中完成所有操作,可以在控件/窗口构造函数中以编程方式完成。

这将涵盖大多数情况,并且可以轻松调整以处理特殊情况。


这是我自己的做法,正如在ChrisF的回答评论中所提到的,不算太糟糕但也不是非常优雅。 - H.B.
也许不是这样,但我的经验告诉我,为简单的行为制作一堆模板很快就会进入过度设计的领域。看看其他人还能想出什么有趣的东西会很有趣。 - A.R.
如果你感兴趣的话,我已经想出了一个有趣的解决方案,它扩展了这种方法。 - H.B.

-1

首先添加一个鼠标点击事件函数:

/// <summary>
/// Returns mouse click.
/// </summary>
/// <returns>mouseeEvent</returns>
public static MouseButtonEventArgs MouseClickEvent()
{
    MouseDevice md = InputManager.Current.PrimaryMouseDevice;
    MouseButtonEventArgs mouseEvent = new MouseButtonEventArgs(md, 0, MouseButton.Left);
    return mouseEvent;
}

在你的WPF控件中添加一个点击事件:
private void btnDoSomeThing_Click(object sender, RoutedEventArgs e)
{
    // Do something
}

最后,从任何函数中调用点击事件:

btnDoSomeThing_Click(new object(), MouseClickEvent());

为了模拟双击事件,需要添加一个类似于PreviewMouseDoubleClick的双击事件,并确保任何代码都在单独的函数中开始:
private void lvFiles_PreviewMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
    DoMouseDoubleClick(e);
}

private void DoMouseDoubleClick(RoutedEventArgs e)
{
    // Add your logic here
}

要调用双击事件,只需从另一个函数(如KeyDown)中调用它:

private void someControl_KeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
    if (e.Key == Key.Enter)
        DoMouseDoubleClick(e);
}

抱歉,但问题的重点并不是在代码中完全模拟单击,而是为不公开“Click”事件的控件创建所需的行为。 - H.B.

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