开发者

WPF DoDragDrop causes control animation to halt

Here's the scenario (simplified): I have a control (let's say, a Rectangle) on the Window. I hooked the MouseMove event to make it initiate a drag&drop. Then in the MouseDown event I let it animate, moving 50 pixels to the right. However, when I 开发者_如何学JAVAhold my mouse down on the Rectangle, the control moves about one pixel, and then pauses. Only when I move my mouse will the animation continue. Does anyone know why and how to solve this? Thanks a lot!!

Here's the source code to reproduce this problem:

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);
    }
}

In case you want 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>


I stumbled upon the same problem like this; My drag'n'drop source moved along an animated path. If I positioned the mouse on the path of the drag source and kept the left mouse button pressed down, the animation would stop when the source touched the mouse. The animation would continue when I either released the mouse button or moved the mouse. (Interestingly, the animation would also continue if I had Windows Task Manager open and it did its periodical process list refresh!)

Analysis of the situation

To my understanding, WPF animations are updated in the CompositionTarget.Rendering event. In normal situations it is fired at every screen refresh, which might be 60 times a second. In my case, when my drag source moves under the mouse it fires the MouseMove event. In that event handler, I call DragDrop.DoDragDrop. This method blocks the UI thread until the drag'n'drop finishes. When the UI thread has entered DragDrop.DoDragDrop, then CompositionTarget.Rendering is not fired. It is understandable in the sense that the thread that fires events is blocked doing drag'n'drop. But! If you move the mouse (and maybe some other kind of input can also do the trick), then a MouseMove event is fired. It is fired in the same UI thread that is still being blocked in a drag'n'drop operation. After MouseMove is fired and handled, CompositionTarget.Rendering continues firing regularly in the UI thread that is still being "blocked".

I gathered this from the comparison of two stack traces below. I have grouped the stack frames based on my poor understanding of what they mean. The first stack trace is taken from my MouseMove event handler while no drag'n'drop operation was active. This is "the usual case".

  • C) Event-specific handling (mouse event)
    System.Windows.Input.MouseEventArgs.InvokeEventHandler
    :
    System.Windows.Input.InputManager.HitTestInvalidatedAsyncCallback
  • B) Message dispatch
    System.Windows.Threading.ExceptionWrapper.InternalRealCall
    :
    MS.Win32.HwndSubclass.SubclassWndProc
    MS.Win32.UnsafeNativeMethods.DispatchMessage
  • A) Application main loop
    System.Windows.Threading.Dispatcher.PushFrameImpl
    :
    System.Threading.ThreadHelper.ThreadStart

The second stack trace is taken from my CompositionTarget.Rendering event handler during a drag'n'drop operation. Frame groups A, B, and C are identical to above.

  • F) Event-specific handling (rendering event) System.Windows.Media.MediaContext.RenderMessageHandlerCore
    System.Windows.Media.MediaContext.AnimatedRenderMessageHandler
  • E) Message dispatch (identical to B)
    System.Windows.Threading.ExceptionWrapper.InternalRealCall
    :
    MS.Win32.HwndSubclass.SubclassWndProc
  • D) Drag'n'drop (started inside my event handler)
    MS.Win32.UnsafeNativeMethods.DoDragDrop
    :
    System.Windows.DragDrop.DoDragDrop
  • C) Event-specific handling (mouse event)
  • B) Message dispatch
  • A) Application main loop

So, WPF is running message dispatching (E) inside message dispatching (B). This explains how it is possible that while DragDrop.DoDragDrop is called on the UI thread and blocks the thread, we are still able to run event handlers on that same thread. I can't imagine why the internal message dispatching isn't run continuously from the beginning of the drag'n'drop operation to perform regular CompositionTarget.Rendering events that would update animations.

Possible workarounds

Fire an extra MouseMove event

One workaround is to trigger a mouse move event when DoDragDrop is executing, as VlaR suggests. His link seems to assume we're using Forms. For WPF, the details are a bit different. Following the solution on the MSDN forums (by the original poster?), this code does the trick. I modified the source so it should work both in 32 and 64 bits and also to avoid the rare chance that the timer is GC'd before it fires.

[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);
}

Using this method instead of DragDrop.DoDragDrop will make the animation proceed without stopping. However, it seems that it doesn't make DragDrop understand any sooner that a drag is in progress. Therefore you risk entering multiple drag operations at once. The calling code should guard against it like this:

private bool _dragging;

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

There is another glitch, too. For some reason, in the situation described at the beginning, the mouse cursor will flicker between the normal arrow cursor and a drag cursor as long as the left mouse button is down, the mouse hasn't moved, and the drop source is traveling over the cursor.

Allow dragging only when the mouse really moves

Another solution I tried is to disallow drag'n'drop from starting in the case described at the beginning. For this, I hook up a few Window events to update a state variable all the code can be condensed into this initialization routine.

/// <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;
}

The calling code would then look something like this:

public MyWindow() {
    var dragAllowed = PrepareForDragDrop(this);
    source.MouseMove += (sender, args) => {
        if (dragAllowed()) DragDrop.DoDragDrop(source, data, effects);
    };
}

This works around the easily reproducible situation where dragging starts because an animation moved a drag source over a stationary mouse cursor while the left mouse button was down. This doesn't avoid the root problem, however. If you click on the drag source and move so little that just one mouse event is fired, animation will stop. Luckily this is less likely to happen in actual practice. This solution has the benefit of not using the Win32 API directly and not having to involve background threads.

Call DoDragDrop from a background thread

Doesn't work. DragDrop.DoDragDrop must be called from the UI thread and will block it. It doesn't help to invoke a background thread that uses the dispatcher to invoke DoDragDrop.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜