Wpf DispatcherTimer.Tick прерван событием захвата мыши в том же потоке

У меня есть созданный поток, в котором запущен диспетчер для обработки событий с ловушками мыши низкого уровня, и создан DispatcherTimer на основе этого диспетчера.

Когда DispatcherTimer.Tick() был запущен, я смоделировал еще одно сообщение с нажатой мышью. Затем произошло нечто странное: Dispatcher.Tick() был прерван, и в том же потоке сработало событие перехвата мыши. После обработки события перехвата мыши DispatcherTimer.Tick() продолжает завершаться.

Насколько я понимаю, диспетчер будет обрабатывать задачи одну за другой, поэтому он должен полностью выполнить метод DispatcherTimer.Tick(), а затем другие события ловушки.

Это поведение нормально? Могу ли я в любом случае гарантировать, что DispatcherTimer.Tick() был полностью выполнен до запуска события ловушки?


Создание потока диспетчера:

public static class DispatcherBuilder 
    {
        public static Dispatcher Build()
        {
            Dispatcher dispatcher = null;
            var manualResetEvent = new ManualResetEvent(false);
            var thread = new Thread(() =>
            {
                dispatcher = Dispatcher.CurrentDispatcher;
                var synchronizationContext = new DispatcherSynchronizationContext(dispatcher);
                SynchronizationContext.SetSynchronizationContext(synchronizationContext);

                manualResetEvent.Set();

                try
                {
                    Dispatcher.Run();
                }
                catch 
                {
                    // ignore
                }
            }, maxStackSize:1);
            thread.Priority = ThreadPriority.Normal;
            thread.SetApartmentState(ApartmentState.STA);
            thread.IsBackground = true;
            thread.Start();
            manualResetEvent.WaitOne();
            manualResetEvent.Dispose();
            return dispatcher;
        }
    }

Код для создания таймера и настройки хука:

// creating disapatcher:

            _hookDispatcher = DispatcherBuilder.Build();




// creating timer on the dispatcher
            _mouseDownTimer = new DispatcherTimer(DispatcherPriority.Normal, _hookDispatcher);
            _mouseDownTimer.Tick += new EventHandler(mouseDownTimer_Tick);
            _mouseDownTimer.Interval = TimeSpan.FromMilliseconds(_dataService.Settings.RightBtnPopupDelayMs);



// create hook

            _hookDispatcher.Invoke(() =>
            {
                _mouseHook = new MouseHook();

                _mouseHook.MouseUp += MouseHookOnMouseUp;
                _mouseHook.MouseDown += MouseHookOnMouseDown;
                _mouseHook.MouseWheel += MouseHookOnMouseWheel;
                _mouseHook.MouseMove += MouseHookOnMouseMove;
                _mouseHook.MouseHWheel += MouseHookOnMouseHWheel;

                _mouseHook.Start();
            });

Ниже приведен некоторый вывод Debug.WriteLine():


177,250 MouseDown  Right Thread:14
177,250 MouseDown Right Captured Thread:14

// DeispatcherTimer.Tick() Started
177,360  Timer  Tick  Begin  Thread:14   -- in DeispatcherTimer.Tick()
177,360  Sending RightButtonDown  -- in DeispatcherTimer.Tick()

// MouseHookOnMouseUp() called in same thread
177,485 MouseUp  Right Thread:14  -- in MouseHookOnMouseUp()
177,500 MouseUp Right Captured Thread:14 -- in MouseHookOnMouseUp()

// MouseHookOnMouseDown() called in same thread
177,500 MouseDown  Right Thread:14  -- in MouseHookOnMouseDown()

// Returned to DeispatcherTimer.Tick() in same thread
177,500  Timer Tick End -- in DeispatcherTimer.Tick()


177,500 MouseDown  Right Thread:14


person cuiliang    schedule 23.05.2020    source источник
comment
Можете ли вы поделиться образцом кода?   -  person Pavel Anikhouski    schedule 23.05.2020
comment
@PavelAnikhouski Я добавил немного кода, спасибо.   -  person cuiliang    schedule 23.05.2020


Ответы (1)


Кажется, все в порядке.

«Когда DispatcherTimer.Tick() был запущен, я смоделировал еще одно сообщение о нажатой мыши. Затем произошло нечто странное: Dispatcher.Tick() был прерван, и в том же потоке было запущено событие перехвата мыши. [...]»

Обычно обработчики событий выполняются в том же потоке, в котором возникло событие, за исключением того, что выполнение маршалируется в другой поток источником события или самим обработчиком.
Поскольку вы явно настроили DispatcherTimer для запуска в _hookDispatcher, Обработчик Tick, вызывающий событие, выполняется в потоке _hookDispatcher. Поэтому обработчик Tick и все события, вызванные этим обработчиком и соответствующими им обработчиками событий, выполняются в одном и том же потоке — потоке, с которым связан DispatcherTimer.

Это отвечает на ваш первый вопрос, почему обработчики событий выполняются в том же потоке, в котором выполняется DispatcherTimer.

"[...] После обработки события перехвата мыши DispatcherTimer.Tick() продолжает заканчиваться.

Насколько я понимаю, диспетчер будет обрабатывать задачи одну за другой, поэтому он должен полностью выполнить метод DispatcherTimer.Tick(), а затем другие события ловушки».

События всегда вызываются синхронно (WPF не реализует асинхронные события).

1) Будет выполнен обработчик Tick.
2) Этот обработчик вызывает событие мыши.
3) Делегат события вызывается синхронно, что означает, что продолжение обработчика Tick "приостановлено". ".
4) Вызов делегата события мыши означает, что все зарегистрированные обратные вызовы также вызываются.
5) После вызова последнего обратного вызова обработчик Tick может продолжить выполнение.

Так что ваши наблюдения верны, но такое поведение абсолютно нормально в данном контексте.

Если вы хотите, чтобы обработчик Tick продолжал выполняться, пока будет обрабатываться событие мыши, вы должны инициировать событие мыши в фоновом потоке. Но поскольку события ввода обрабатываются для выполнения работы, связанной с пользовательским интерфейсом, очень вероятно, что эти обработчики будут вызывать Dispatcher для доступа к ресурсам пользовательского интерфейса (или любым DispatcherObject). Выполнение таких обработчиков в фоновом потоке, заставляющее обработчики снова синхронизироваться с потоком пользовательского интерфейса, слишком дорого и обычно не имеет никакого смысла.

Рекомендуемое решение — вызывать события с помощью Dispatcher.InvokeAsync:

private void OnTick(object sender, EventArgs e)
{
  Dispatcher.InvokeAsync(RaiseMouseEvent, DispatcherPriority.Background);
}

Dispatcher.InvokeAsync выполняется асинхронно. Таким образом, управление немедленно возвращается вызывающему объекту (обработчику Tick) после его вызова.

person BionicCode    schedule 23.05.2020