WPF DoDragDrop导致控件动画停止

3
这里是一个简化版的场景:我在窗口上放置了一个控件(比如一个矩形),并将MouseMove事件挂钩使其启动拖放。然后在MouseDown事件中,我让它执行动画,向右移动50个像素。然而,当我按住鼠标在矩形上时,控件只会移动大约一个像素就停下来了。只有当我移动鼠标时,动画才会继续进行。有谁知道为什么会这样以及如何解决?非常感谢!以下是重现此问题的源代码:
public partial class Window1 : Window
{
    public Window1()
    {
        InitializeComponent();
    }

    private void rectangle1_MouseMove(object sender, MouseEventArgs e)
    {
        if (e.LeftButton == MouseButtonState.Pressed)
        {
            DragDrop.DoDragDrop(this, new DataObject(), DragDropEffects.Copy);
        }
    }

    private void rectangle1_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        ThicknessAnimation animation = new ThicknessAnimation();
        Thickness t = rectangle1.Margin;
        t.Left += 50;
        animation.To = t;
        animation.Duration = new Duration(TimeSpan.FromSeconds(0.25));
        rectangle1.BeginAnimation(Rectangle.MarginProperty, animation);
    }
}

如果您需要Window1.xaml文件:
<Window x:Class="DragDropHaltingTest.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<Grid>
    <Rectangle Margin="12,12,0,0" Name="rectangle1" Stroke="Black" Fill="Blue" Height="29" HorizontalAlignment="Left" VerticalAlignment="Top" Width="31" MouseMove="rectangle1_MouseMove" MouseLeftButtonDown="rectangle1_MouseLeftButtonDown" />
</Grid>


你想要达到什么效果?你希望动画在 MouseLeftButtonDown 被触发时立即发生吗?还是你不希望它在 MouseMove 触发之前进行动画,只想消除 1 像素的移动? - kenwarner
这只是一个简化版本,只是为了重现问题,但我想要实现的效果与我的代码所暗示的完全一样:一旦鼠标左键按下,控件就会进行动画。拖放操作并不意味着与动画有关,但我的问题是拖放操作干扰了动画。 我不想消除1像素的移动;我希望移动继续进行而不中断,忽略DoDragDrop()调用。 - user258132
也许这个解决方法对你有帮助:http://stackoverflow.com/questions/3538224/dodragdrop-from-another-thread/7710461#7710461 - Vlad Rudenko
1个回答

4
我遇到了和这个问题相同的情况:我的拖放源沿着一个动画路径移动。如果我将鼠标放在拖放源的路径上并按住左键,当源接触到鼠标时,动画会停止。只有当我释放鼠标或移动鼠标时,动画才会继续。(有趣的是,如果我打开Windows任务管理器并进行周期性的进程列表刷新,动画也会继续!)
情况分析
据我所知,WPF动画在CompositionTarget.Rendering事件中更新。在正常情况下,它每秒会触发60次屏幕刷新。在我的情况下,当我的拖放源移动到鼠标下方时,它会触发MouseMove事件。在该事件处理程序中,我调用DragDrop.DoDragDrop。此方法会阻塞UI线程,直到拖放完成。当UI线程进入DragDrop.DoDragDrop时,CompositionTarget.Rendering不会被触发。从某种意义上来说,这是可以理解的,因为触发事件的线程正在执行拖放操作。但是!如果你移动鼠标(也许其他一些输入也能做到),那么就会触发MouseMove事件。它在仍被阻塞的UI线程中触发。在MouseMove被触发和处理后,CompositionTarget.Rendering继续在仍被“阻塞”的UI线程中定期触发。
我从下面的两个堆栈跟踪中收集到了这一点。我根据自己对它们意义的贫乏理解将堆栈帧分组。第一个堆栈跟踪是从我的MouseMove事件处理程序中取出的,当没有拖放操作时,这是“通常情况”。
  • C) 事件特定处理(鼠标事件)
    System.Windows.Input.MouseEventArgs.InvokeEventHandler
    :
    System.Windows.Input.InputManager.HitTestInvalidatedAsyncCallback
  • B) 消息分派
    System.Windows.Threading.ExceptionWrapper.InternalRealCall
    :
    MS.Win32.HwndSubclass.SubclassWndProc
    MS.Win32.UnsafeNativeMethods.DispatchMessage
  • A) 应用程序主循环
    System.Windows.Threading.Dispatcher.PushFrameImpl
    :
    System.Threading.ThreadHelper.ThreadStart
第二个堆栈跟踪是在拖放操作期间从我的CompositionTarget.Rendering事件处理程序中取出的。框架组A、B和C与上述相同。
  • F) 事件特定处理(渲染事件) System.Windows.Media.MediaContext.RenderMessageHandlerCore System.Windows.Media.MediaContext.AnimatedRenderMessageHandler
  • E) 消息分发(与B相同) System.Windows.Threading.ExceptionWrapper.InternalRealCall : MS.Win32.HwndSubclass.SubclassWndProc
  • D) 拖放(在我的事件处理程序内开始) MS.Win32.UnsafeNativeMethods.DoDragDrop : System.Windows.DragDrop.DoDragDrop
  • C) 事件特定处理(鼠标事件)
  • B) 消息分发
  • A) 应用程序主循环

所以,WPF正在运行消息分发(E)内部的消息分发(B)。这解释了为什么在UI线程上调用并阻塞线程的DragDrop.DoDragDrop时,我们仍然能够在同一线程上运行事件处理程序。我想不出为什么内部消息分发不会从拖放操作的开始一直运行,以执行常规的CompositionTarget.Rendering事件,以更新动画。

可能的解决方法

触发额外的MouseMove事件

一种解决方法是在执行DoDragDrop时触发鼠标移动事件,就像VlaR建议的那样。他的链接似乎假定我们使用Forms。对于WPF,细节有些不同。遵循MSDN论坛上的解决方案(由原帖作者?),这段代码可以解决问题。我修改了源代码,因此它应该适用于32位和64位,并且还可避免计时器在触发之前被GC的罕见机会。

[DllImport("user32.dll")]
private static extern void PostMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);

private static Timer g_mouseMoveTimer;

public static DragDropEffects MyDoDragDrop(DependencyObject source, object data, DragDropEffects effects, Window callerWindow)
{
    var callerHandle = new WindowInteropHelper(callerWindow).Handle;
    var position = Mouse.GetPosition(callerWindow);
    var WM_MOUSEMOVE = 0x0200u;
    var MK_LBUTTON = new IntPtr(0x0001);
    var lParam = (IntPtr)position.X + ((int)position.Y << (IntPtr.Size * 8 / 2));
    if (g_mouseMoveTimer == null) {
        g_mouseMoveTimer = new Timer { Interval = 1, AutoReset = false };
        g_mouseMoveTimer.Elapsed += (sender, args) => PostMessage(callerHandle, WM_MOUSEMOVE, MK_LBUTTON, lParam);
    }
    g_mouseMoveTimer.Start();
    return DragDrop.DoDragDrop(source, data, effects);
}

使用此方法而不是DragDrop.DoDragDrop将使动画继续进行而不停止。然而,似乎这并没有让DragDrop更早地理解拖动正在进行中。因此,您可能会冒多次开始拖动的风险。调用代码应像这样保护它:

private bool _dragging;

source.MouseMove += (sender, args) => {
    if (!_dragging && args.LeftButton == MouseButtonState.Pressed) {
        _dragging = true;
        MyDoDragDrop(source, data, effects, window);
        _dragging = false;
    }
}

还有另一个小故障。由于某种原因,在上述情况下,只要左鼠标键按下,鼠标光标就会在正常箭头光标和拖动光标之间闪烁,而且只要拖放源在光标上方移动,鼠标没有移动。

只有当鼠标真正移动时才允许拖拽

我尝试的另一个解决方案是,在开始的情况下禁止拖放。为此,我连接了几个窗口事件来更新一个状态变量,所有代码都可以压缩到此初始化程序中。

/// <summary>
/// Returns a function that tells if drag'n'drop is allowed to start.
/// </summary>
public static Func<bool> PrepareForDragDrop(Window window)
{
    var state = DragFilter.MustClick;
    var clickPos = new Point();
    window.MouseLeftButtonDown += (sender, args) => {
        if (state != DragFilter.MustClick) return;
        clickPos = Mouse.GetPosition(window);
        state = DragFilter.MustMove;
    };
    window.MouseLeftButtonUp += (sender, args) => state = DragFilter.MustClick;
    window.PreviewMouseMove += (sender, args) => {
        if (state == DragFilter.MustMove && Mouse.GetPosition(window) != clickPos)
            state = DragFilter.Ok;
    };
    window.MouseMove += (sender, args) => {
        if (state == DragFilter.Ok)
            state = DragFilter.MustClick;
    };
    return () => state == DragFilter.Ok;
}

调用代码将类似于以下内容:
public MyWindow() {
    var dragAllowed = PrepareForDragDrop(this);
    source.MouseMove += (sender, args) => {
        if (dragAllowed()) DragDrop.DoDragDrop(source, data, effects);
    };
}

这个解决方法可以避免拖动源在鼠标左键按下时因为动画将其移动到静止的鼠标光标上而导致拖动开始的易于再现的情况。然而,它并没有解决根本问题。如果您点击拖动源并移动得很少,只会触发一个鼠标事件,动画就会停止。幸运的是,在实际操作中几乎不太可能发生这种情况。这个解决方案的好处是不直接使用Win32 API,也不需要涉及后台线程。

从后台线程调用DoDragDrop

不可行。必须从UI线程调用DragDrop.DoDragDrop并阻塞UI线程。使用Dispatcher调用DoDragDrop的后台线程并没有帮助。


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