我遇到了和这个问题相同的情况:我的拖放源沿着一个动画路径移动。如果我将鼠标放在拖放源的路径上并按住左键,当源接触到鼠标时,动画会停止。只有当我释放鼠标或移动鼠标时,动画才会继续。(有趣的是,如果我打开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;
}
}
还有另一个小故障。由于某种原因,在上述情况下,只要左鼠标键按下,鼠标光标就会在正常箭头光标和拖动光标之间闪烁,而且只要拖放源在光标上方移动,鼠标没有移动。
只有当鼠标真正移动时才允许拖拽
我尝试的另一个解决方案是,在开始的情况下禁止拖放。为此,我连接了几个窗口事件来更新一个状态变量,所有代码都可以压缩到此初始化程序中。
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
的后台线程并没有帮助。