WPF键事件的排队和重放

11

我正在尝试改进一个WPF业务应用程序的响应性,以便当用户处于“屏幕之间”等待服务器响应后出现新屏幕时,他们仍可以输入数据。我能够排队事件(在后台面板上使用PreviewKeyDown事件处理程序),但我遇到了一些困难,即在加载新面板后,只是将我出列的事件抛回到新面板上。特别是,新面板上的TextBoxes没有获取文本。我尝试了许多方法,例如引发相同的事件(在捕获它们时设置Handled为true,在再次引发它们时设置Handled为false)、创建新的KeyDown事件、新的PreviewKeyDown事件、ProcessInput、在面板上引发RaiseEvent、设置焦点在正确的TextBox上并在TextBox上引发RaiseEvent等。

这似乎应该非常简单,但我想不出来。

以下是我尝试过的一些方法。假设有一个名为EventQ的KeyEventArgs队列:

以下是无效的方法之一:

        while (EventQ.Count > 0)
        {
            KeyEventArgs kea = EventQ.Dequeue();
            tbOne.Focus(); // tbOne is a text box
            kea.Handled = false;
            this.RaiseEvent(kea);
        }

这是另一个例子:

        while (EventQ.Count > 0)
        {
            KeyEventArgs kea = EventQ.Dequeue();
            tbOne.Focus(); // tbOne is a text box
            var key = kea.Key;                    // Key to send
            var routedEvent = Keyboard.PreviewKeyDownEvent; // Event to send
            KeyEventArgs keanew = new KeyEventArgs(
                Keyboard.PrimaryDevice,
                PresentationSource.FromVisual(this),
                0,
                key) { RoutedEvent = routedEvent, Handled = false };

            InputManager.Current.ProcessInput(keanew);
        }

还有另一个:

        while (EventQ.Count > 0)
        {
            KeyEventArgs kea = EventQ.Dequeue();
            tbOne.Focus(); // tbOne is a text box
            var key = kea.Key;                    // Key to send
            var routedEvent = Keyboard.PreviewKeyDownEvent; // Event to send
            this.RaiseEvent(
              new KeyEventArgs(
                Keyboard.PrimaryDevice,
                PresentationSource.FromVisual(this),
                0,
                key) { RoutedEvent = routedEvent, Handled = false }
            );
        }

我注意到的一个奇怪的事情是,在使用InputManager方法(#2)时,空格会出现。但普通文本键则不会。


http://msdn.microsoft.com/en-us/library/ms741870.aspx 或许您需要不同的线程模型?例如两个UI线程,这样您就不必引发事件了。 - Erti-Chris Eelmaa
@Erti-ChrisEelmaa,我在我的原始帖子中可能没有表达清楚,但我需要展示新的屏幕,而不是相同的屏幕。这不是一个后台进程正在运行,而是屏幕变空白直到服务器响应。当服务器响应时,将显示具有全新组件的新屏幕,并且用户在此期间输入的任何内容都应该被传递到新屏幕上。我还对您提到的“两个UI线程”感到困惑,我以为WPF全部都是STA? - Mishax
@LukeMarlin 是的,我有很多非标准光标导航和功能键链接到命令,现在它们都能正常工作,我希望有一个单一的处理程序来处理所有这些事件,因为它们已经经过测试并且可以正常工作。此外,文本比你想象的更加困难,因为我需要将光标从一个字段移动到下一个,并正确使用大写等。我想重用已经存在并且有效的内容。 - Mishax
你能展示一下你尝试发送按键事件的方式吗? - Luke Marlin
让我们在聊天中继续这个讨论:http://chat.stackoverflow.com/rooms/28428/discussion-between-luke-marlin-and-mishax - Luke Marlin
显示剩余3条评论
3个回答

10

我在做一些研究时发现了相同的资源,所以我认为你在回答中的做法是相当有效的。

我自己找到了另一种方法,使用Win32 API。我不得不引入一些线程和小延迟,因为由于某种原因,键盘事件没有按正确的顺序重放。总体而言,我认为这个解决方案更容易,而且我还弄清楚了如何包括修改键(通过使用Get/SetKeyboardState函数)。大写字母正在工作,键盘快捷键也应该可以。

启动演示应用程序,按下按键1空格2空格3制表符4空格5空格6,然后单击按钮将产生以下结果:

输入图像描述

Xaml:

<UserControl x:Class="WpfApplication1.KeyEventQueueDemo"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300" >

    <StackPanel>
        <TextBox x:Name="tbOne" Margin="5,2" />
        <TextBox x:Name="tbTwo" Margin="5,2" />
        <Button x:Name="btn" Content="Replay key events" Margin="5,2" />
    </StackPanel>
</UserControl>

代码后台:

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interop;

namespace WpfApplication1
{
    /// <summary>
    /// Structure that defines key input with modifier keys
    /// </summary>
    public struct KeyAndState
    {
        public int Key;
        public byte[] KeyboardState;

        public KeyAndState(int key, byte[] state)
        {
            Key = key;
            KeyboardState = state;
        }
    }

    /// <summary>
    /// Demo to illustrate storing keyboard input and playing it back at a later stage
    /// </summary>
    public partial class KeyEventQueueDemo : UserControl
    {
        private const int WM_KEYDOWN = 0x0100;

        [DllImport("user32.dll")]
        static extern bool PostMessage(IntPtr hWnd, UInt32 Msg, int wParam, int lParam);

        [DllImport("user32.dll")]
        static extern bool GetKeyboardState(byte[] lpKeyState);

        [DllImport("user32.dll")]
        static extern bool SetKeyboardState(byte[] lpKeyState);

        private IntPtr _handle;
        private bool _isMonitoring = true;

        private Queue<KeyAndState> _eventQ = new Queue<KeyAndState>();

        public KeyEventQueueDemo()
        {
            InitializeComponent();

            this.Focusable = true;
            this.Loaded += KeyEventQueueDemo_Loaded;
            this.PreviewKeyDown += KeyEventQueueDemo_PreviewKeyDown;
            this.btn.Click += (s, e) => ReplayKeyEvents();
        }

        void KeyEventQueueDemo_Loaded(object sender, RoutedEventArgs e)
        {
            this.Focus(); // necessary to detect previewkeydown event
            SetFocusable(false); // for demo purpose only, so controls do not get focus at tab key

            // getting window handle
            HwndSource source = (HwndSource)HwndSource.FromVisual(this);
            _handle = source.Handle;
        }

        /// <summary>
        /// Get key and keyboard state (modifier keys), store them in a queue
        /// </summary>
        void KeyEventQueueDemo_PreviewKeyDown(object sender, KeyEventArgs e)
        {
            if (_isMonitoring)
            {
                int key = KeyInterop.VirtualKeyFromKey(e.Key);
                byte[] state = new byte[256];
                GetKeyboardState(state); 
                _eventQ.Enqueue(new KeyAndState(key, state));
            }
        }

        /// <summary>
        /// Replay key events from queue
        /// </summary>
        private void ReplayKeyEvents()
        {
            _isMonitoring = false; // no longer add to queue
            SetFocusable(true); // allow controls to take focus now (demo purpose only)

            MoveFocus(new TraversalRequest(FocusNavigationDirection.Next)); // set focus to first control

            // thread the dequeueing, because the sequence of inputs is not preserved 
            // unless a small delay between them is introduced. Normally the effect this
            // produces should be very acceptable for an UI.
            Task.Run(() =>
            {
                while (_eventQ.Count > 0)
                {
                    KeyAndState keyAndState = _eventQ.Dequeue();

                    Application.Current.Dispatcher.BeginInvoke((Action)(() =>
                    {
                        SetKeyboardState(keyAndState.KeyboardState); // set stored keyboard state
                        PostMessage(_handle, WM_KEYDOWN, keyAndState.Key, 0);
                    }));

                    System.Threading.Thread.Sleep(5); // might need adjustment
                }
            });
        }

        /// <summary>
        /// Prevent controls from getting focus and taking the input until requested
        /// </summary>
        private void SetFocusable(bool isFocusable)
        {
            tbOne.Focusable = isFocusable;
            tbTwo.Focusable = isFocusable;
            btn.Focusable = isFocusable;
        }
    }
}

嗨,这看起来非常不错。我进行了轻微的调整,使用BackgroundWorker代替Task.Run,以便更好地兼容.NET 3和4。这会产生很好的结果! - Mishax
没错,Task API 仅适用于 .NET 4。对于早期的 .NET 版本,您可以将 Task.Run(() => 替换为 ThreadPool.QueueUserWorkItem((o) =>,我更喜欢这种简洁的语法而不是 BackgroundWorker。 - Mike Fuchs

3
enqueue系统是我一直想要自己实现的东西,作为我的项目的一部分,它可以使多线程UI无缝运行(一个线程将事件路由到另一个线程)。唯一的问题是,WPF没有公共API来注入输入事件。以下是我几周前与某位微软员工交流时复制/粘贴的内容:
“WPF没有公开的方法以适当的方式注入输入事件。这种情况在公共API中不受支持。您可能需要进行大量反射和其他黑客攻击。例如,WPF将一些输入视为“可信任的”,因为它知道它来自消息泵。如果您只是触发了输入事件,该事件将无法获得信任。”
我认为你需要重新思考你的策略。

...或者不要进行大量的反射和破解,而是可以直接使用消息泵 - Mike Fuchs

1
感谢大家的支持,但我并没有从SO社区中找到解决方案,所以我将自己回答这个问题,因为这似乎是我接近解决方案的最佳方式。正如Erti-Chris所说的“hack”,似乎是我们留下的唯一选择。我已经尝试了一些方法来分解问题,这样我就不会觉得自己在编写一个全新的键盘处理程序。我采用的方法是将事件分解为InputManager处理和TextComposition组合。抛出KeyEventArgs(原始的或我自己创建的)似乎无法在PreviewKeyDown处理程序中注册。
部分困难来自于Erti-Chris帖子中的信息,另一部分似乎与TextBoxes试图对箭头键等特定键作出不同于普通键(如字母“A”)的反应有关。
为了继续进行,我发现这篇文章中的信息很有用:

http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/b657618e-7fc6-4e6b-9b62-1ffca25d186b

这是我现在正在得到一些积极结果的解决方案:

    Keyboard.Focus(tbOne); // the first element on the Panel to get the focus
    while (EventQ.Count > 0) 
    {
        KeyEventArgs kea = EventQ.Dequeue();
        kea.Handled = false;
        var routedEvent = KeyDownEvent; 

        KeyEventArgs keanew = new KeyEventArgs(
                     Keyboard.PrimaryDevice,
                     PresentationSource.FromVisual(tbOne),
                     kea.Timestamp,
                     kea.Key) { RoutedEvent = routedEvent, Handled = false };
        keanew.Source = tbOne;

        bool itWorked = InputManager.Current.ProcessInput(keanew);
        if (itWorked)
        {
            continue;
            // at this point spaces, backspaces, tabs, arrow keys, deletes are handled
        }
        else
        {
            String keyChar = kea.Key.ToString();
            if (keyChar.Length > 1)
            {
                // handle special keys; letters are one length
                if (keyChar == "OemPeriod") keyChar = ".";
                if (keyChar == "OemComma") keyChar = ",";
            }
            TextCompositionManager.StartComposition(new TextComposition(InputManager.Current, Keyboard.FocusedElement, keyChar));
        }
    }

如果有人能向我展示更好的方法,我很乐意将您的贡献标记为答案,但目前这就是我正在使用的。


你需要为数字添加特殊处理,它们会显示为D1、D2等。此外,如果其中一个键是制表符,用于移动光标的tbOne需要被替换为获取焦点的控件。 - Mike Fuchs

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