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) 时,确实会出现空格。但是普通的文本键没有。

4

3 回答 3

10

当我做一些研究时,同样的资源出现在我身上,所以我认为你在回答中所做的事情是非常有效的。

我看了看,发现了另一种方法,使用 Win32 API。我不得不引入一些线程和小的延迟,因为由于某种原因,关键事件没有按正确的顺序重播。总的来说,我认为这个解决方案更简单,而且我还想出了如何包含修饰键(通过使用 Get/SetKeyboardState 函数)。大写字母正在工作,键盘快捷键也应如此。

启动演示应用程序,按 键1 space 2 space 3 tab 4 space 5 space 6,然后单击按钮会产生以下结果:

在此处输入图像描述

xml:

<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;
        }
    }
}
于 2013-04-27T17:41:31.910 回答
3

入队系统是我自己想做的事情,作为我项目的一部分,它允许多线程 UI 正常运行(一个线程将事件路由到另一个线程)。只有一个小问题,即 WPF 没有公共 API 来注入 INPUT 事件。这是几周前与我交谈过的一位微软员工的副本/粘贴:

“WPF 没有公开用于以正确方式注入输入事件的公共方法。公共 API 不支持这种情况。您可能需要进行大量反射和其他黑客攻击。例如,WPF 将某些输入视为“受信任”,因为它知道它来自消息泵。如果您只是引发输入事件,则该事件将不受信任。”

我认为你需要重新考虑你的策略。

于 2013-04-23T19:29:50.633 回答
1

感谢大家的支持,但我还没有真正从 SO 社区找到解决方案,所以我要自己回答这个问题,因为这是我似乎最接近解决方案的地方。Erti-Chris 所说的“黑客”似乎是我们所剩下的。我有一些运气分解了这个问题,所以我没有感觉我正在编写一个全新的键盘处理程序。我遵循的方法是将事件分解为 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));
        }
    }

如果有人可以向我展示更好的方法,我很高兴将您的贡献标记为答案,但现在这就是我正在使用的。

于 2013-04-27T08:44:55.553 回答