WPF DoDragDrop вызывает остановку управляющей анимации - PullRequest
3 голосов
/ 25 января 2010

Вот сценарий (упрощенный): у меня есть элемент управления (скажем, прямоугольник) в окне. Я подключил событие 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>

1 Ответ

3 голосов
/ 09 апреля 2013

Я наткнулся на ту же проблему, как эта; Мой источник drag'n'drop перемещался по анимированной дорожке. Если бы я поместил мышь на путь источника перетаскивания и удерживал левую кнопку мыши нажатой, анимация остановилась бы, когда источник коснулся мыши. Анимация продолжалась, когда я либо отпустил кнопку мыши, либо переместил мышь. (Интересно, что анимация также продолжалась бы, если бы у меня был открыт диспетчер задач Windows, и он периодически обновлял свой список процессов!)

Анализ ситуации

Насколько я понимаю, анимации WPF обновляются в событии CompositionTarget.Rendering. В обычных ситуациях он запускается при каждом обновлении экрана, что может быть 60 раз в секунду. В моем случае, когда мой источник перетаскивания перемещается под мышью, он запускает событие MouseMove. В этом обработчике событий я вызываю DragDrop.DoDragDrop. Этот метод блокирует поток пользовательского интерфейса, пока не завершится перетаскивание. Когда поток пользовательского интерфейса ввел DragDrop.DoDragDrop, то CompositionTarget.Rendering не запускается. Это понятно в том смысле, что поток, который генерирует события, блокируется при выполнении drag'n'drop. Но! Если вы двигаете мышь (и, возможно, какой-то другой вид ввода также может помочь), то происходит событие MouseMove. Он запускается в том же потоке пользовательского интерфейса, который все еще блокируется в операции перетаскивания. После того, как MouseMove запущен и обработан, CompositionTarget.Rendering продолжает регулярно запускаться в потоке пользовательского интерфейса, который все еще "заблокирован".

Я понял это из сравнения двух следов стека ниже. Я сгруппировал кадры стека на основе моего плохого понимания того, что они означают. Первая трассировка стека берется из моего обработчика событий 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) Drag'n'drop (запущен внутри моего обработчика событий)
    MS.Win32.UnsafeNativeMethods.DoDragDrop
    :
    System.Windows.DragDrop.DoDragDrop
  • C) Обработка событий, связанных с событием (событие мыши)
  • B) Отправка сообщения
  • A) Применение основного цикла

Итак, WPF выполняет диспетчеризацию сообщений (E) внутри диспетчеризации сообщений (B). Это объясняет, как возможно, что хотя DragDrop.DoDragDrop вызывается в потоке пользовательского интерфейса и блокирует поток, мы все еще можем запускать обработчики событий в этом же потоке. Я не могу себе представить, почему внутренняя диспетчеризация сообщений не запускается непрерывно с начала операции drag'n'drop для выполнения регулярных CompositionTarget.Rendering событий, которые бы обновляли анимацию.

Возможные обходные пути

Запустить дополнительное событие MouseMove

Один из обходных путей - вызвать событие перемещения мыши, когда DoDragDrop выполняется, как предполагает VlaR. Его ссылка, кажется, предполагает, что мы используем формы. Для 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;
    }
}

Есть еще один глюк. По какой-то причине в ситуации, описанной в начале, курсор мыши будет мерцать между обычным курсором стрелки и курсором перетаскивания, пока левая кнопка мыши нажата, мышь не перемещается, а источник отбрасывания перемещается курсор.

Разрешить перетаскивание, только когда мышь действительно двигается

Другое решение, которое я попытался, - запретить запуск drag'n'drop в случае, описанном в начале. Для этого я подключаю несколько событий Window, чтобы обновить переменную состояния, весь код может быть сжат в эту процедуру инициализации.

/// <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 из фоновой темы

Не работает. DragDrop.DoDragDrop должен быть вызван из потока пользовательского интерфейса и заблокирует его. Не помогает вызвать фоновый поток, который использует диспетчер для вызова DoDragDrop.

...