XNA - 键盘文本输入

15
好的,基本上我想要能够检索键盘文本。就像输入文本到文本框或其他东西一样。我只为Windows编写我的游戏。 我忽略了使用Guide.BeginShowKeyboardInput,因为它破坏了自包含游戏的感觉,并且指南始终显示XBOX按钮,这对我来说似乎不正确。是最简单的方法,但我不喜欢它。
接下来,我尝试使用System.Windows.Forms.NativeWindow。我创建了一个从中继承的类,传递了Games窗口句柄,实现了WndProc函数以捕获WM_CHAR(或WM_KEYDOWN),尽管WndProc为其他消息调用,但WM_CHAR和WM_KEYDOWN从未调用过。所以我不得不放弃那个想法,而且,我还引用了整个Windows表单,这意味着不必要的内存占用膨胀。
所以我最后的想法是创建一个线程级别的低级键盘钩子。到目前为止,这是最成功的。我得到WM_KEYDOWN消息(还没有尝试WM_CHAR),使用Win32函数MapVirtualKey将虚拟键码转换为字符。然后我得到了我的文本!(我现在只是使用Debug.Write打印)
有几个问题。好像我开启了大写锁定,并且Shift键没有响应。 (当然不是,因为每个键只有一个虚拟键码,所以翻译它只有一个输出),并且它会添加开销,因为它附加到Windows挂钩列表并且不如我希望的那么快,但缓慢可能更多是由于Debug.Write。
还有其他人解决了这个问题而不必使用屏幕键盘吗?或者有没有其他想法让我尝试?
提前感谢。
Jimmy提出的问题
也许我没有理解问题,但为什么您不能使用XNA Keyboard和KeyboardState类?
我的评论:
这是因为尽管您可以读取按键状态,但无法按照用户输入文本的方式获取对其进行访问。
因此,让我进一步澄清。我想要实现能够从用户那里读取文本输入,就好像他们正在Windows中键入文本框一样。键盘和KeyboardState类获取所有键的状态,但我必须将每个键和组合映射到其字符表示形式。当用户不使用与我相同的键盘语言时,特别是在符号方面时,这种情况就会发生(我的双引号是Shift + 2,而美国键盘则在返回键附近)。
似乎我的窗口挂钩是正确的方法,只是我没有收到WM_CHAR的原因是XNA消息泵不会翻译消息。
每当我收到WM_KEYDOWN消息时,添加TranslateMessage意味着我得到了我的WM_CHAR消息,然后我在我的MessageHook类中使用它来触发一个字符键入事件,我的KeyboardBuffer类已经订阅了该事件,然后将其缓冲到文本缓冲区:D(或StringBuilder,但结果是相同的)。
所以我已经按照自己的想法使其全部工作。
非常感谢Jimmy提供的非常有信息量的线程链接。
5个回答

9

在XNA中添加Windows钩子的方法

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using System.Diagnostics;
using System.Reflection;

/* Author: Sekhat
 * 
 * License: Public Domain.
 * 
 * Usage:
 *
 * Inherit from this class, and override the WndProc function in your derived class, 
 * in which you handle your windows messages.
 * 
 * To start recieving the message, create an instance of your derived class, passing in the
 * window handle of the window you want to listen for messages for.
 * 
 * in XNA: this would be the Game.Window.Handle property
 * in Winforms Form.Handle property
 */

namespace WindowsHookExample
{
    public abstract class WindowsHook : IDisposable
    {
        IntPtr hHook;
        IntPtr hWnd;
        // Stored here to stop it from getting garbage collected
        Win32.WndProcDelegate wndProcDelegate;

        public WindowsHook(IntPtr hWnd)
        {
            this.hWnd = hWnd;

            wndProcDelegate = WndProcHook;

            CreateHook();
        }

        ~WindowsHook()
        {
            Dispose(false);
        }

        private void CreateHook()
        {

            uint threadId = Win32.GetWindowThreadProcessId(hWnd, IntPtr.Zero);

            hHook = Win32.SetWindowsHookEx(Win32.HookType.WH_CALLWNDPROC, wndProcDelegate, IntPtr.Zero, threadId);

        }

        private int WndProcHook(int nCode, IntPtr wParam, ref Win32.Message lParam)
        {
            if (nCode >= 0)
            {
                Win32.TranslateMessage(ref lParam); // You may want to remove this line, if you find your not quite getting the right messages through. This is here so that WM_CHAR is correctly called when a key is pressed.
                WndProc(ref lParam);
            }

            return Win32.CallNextHookEx(hHook, nCode, wParam, ref lParam);
        }

        protected abstract void WndProc(ref Win32.Message message);

        #region Interop Stuff
        // I say thankya to P/Invoke.net.
        // Contains all the Win32 functions I need to deal with
        protected static class Win32
        {
            public enum HookType : int
            {
                WH_JOURNALRECORD = 0,
                WH_JOURNALPLAYBACK = 1,
                WH_KEYBOARD = 2,
                WH_GETMESSAGE = 3,
                WH_CALLWNDPROC = 4,
                WH_CBT = 5,
                WH_SYSMSGFILTER = 6,
                WH_MOUSE = 7,
                WH_HARDWARE = 8,
                WH_DEBUG = 9,
                WH_SHELL = 10,
                WH_FOREGROUNDIDLE = 11,
                WH_CALLWNDPROCRET = 12,
                WH_KEYBOARD_LL = 13,
                WH_MOUSE_LL = 14
            }

            public struct Message
            {
                public IntPtr lparam;
                public IntPtr wparam;
                public uint msg;
                public IntPtr hWnd;
            }

            /// <summary>
            ///  Defines the windows proc delegate to pass into the windows hook
            /// </summary>                  
            public delegate int WndProcDelegate(int nCode, IntPtr wParam, ref Message m);

            [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
            public static extern IntPtr SetWindowsHookEx(HookType hook, WndProcDelegate callback,
                IntPtr hMod, uint dwThreadId);

            [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
            public static extern bool UnhookWindowsHookEx(IntPtr hhk);

            [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
            public static extern int CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, ref Message m);

            [DllImport("coredll.dll", SetLastError = true)]
            public static extern IntPtr GetModuleHandle(string module);

            [DllImport("user32.dll", EntryPoint = "TranslateMessage")]
            public extern static bool TranslateMessage(ref Message m);

            [DllImport("user32.dll")]
            public extern static uint GetWindowThreadProcessId(IntPtr window, IntPtr module);
        }
        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            Dispose(true);
        }

        private void Dispose(bool disposing)
        {
            if (disposing)
            {
                // Free managed resources here
            }
            // Free unmanaged resources here
            if (hHook != IntPtr.Zero)
            {
                Win32.UnhookWindowsHookEx(hHook);
            }
        }

        #endregion
    }
}

那个消息结构体的成员顺序不正确,会产生乱码结果。它们应该按照这个顺序排列:https://msdn.microsoft.com/zh-cn/library/windows/desktop/ms644964(v=vs.85).aspx - Trevor Sundberg

1

1
这是一个简单的方法,我的意见是,先放置spacebackA-Z,然后是特殊字符!@#$%^&*()。(注意,需要导入System.Linq)以下是字段:
Keys[] keys;
bool[] IskeyUp;
string[] SC = { ")" , "!", "@", "#", "$", "%", "^", "&", "*", "("};//special characters

构造函数:

keys = new Keys[38];
Keys[] tempkeys;
tempkeys = Enum.GetValues(typeof(Keys)).Cast<Keys>().ToArray<Keys>();
int j = 0;
for (int i = 0; i < tempkeys.Length; i++)
{
    if (i == 1 || i == 11 || (i > 26 && i < 63))//get the keys listed above as well as A-Z
    {
        keys[j] = tempkeys[i];//fill our key array
        j++;
    }
}
IskeyUp = new bool[keys.Length]; //boolean for each key to make the user have to release the key before adding to the string
for (int i = 0; i < keys.Length; i++)
    IskeyUp[i] = true;

最后,更新方法:
string result = "";

public override void Update(GameTime gameTime)
{
    KeyboardState state = Keyboard.GetState();
    int i = 0;
    foreach (Keys key in keys)
    {
        if (state.IsKeyDown(key))
        {
            if (IskeyUp[i])
            {
                if (key == Keys.Back && result != "") result = result.Remove(result.Length - 1);
                if (key == Keys.Space) result += " ";
                if (i > 1 && i < 12)
                {
                    if (state.IsKeyDown(Keys.RightShift) || state.IsKeyDown(Keys.LeftShift))
                        result += SC[i - 2];//if shift is down, and a number is pressed, using the special key
                    else result += key.ToString()[1];
                }
                if (i > 11 && i < 38)
                {
                    if (state.IsKeyDown(Keys.RightShift) || state.IsKeyDown(Keys.LeftShift))
                       result += key.ToString();
                    else result += key.ToString().ToLower(); //return the lowercase char is shift is up.
                }
            }
            IskeyUp[i] = false; //make sure we know the key is pressed
        }
        else if (state.IsKeyUp(key)) IskeyUp[i] = true;
        i++;
    }
    base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen);
}

希望这可以帮助。我个人认为这比使用钩子更容易,而且这也可以轻松地修改以获得特殊结果(例如对键进行洗牌)。

唯一的问题是它不是基于事件的。因此,如果用户打字速度比您的更新速率快,键按下将被忽略。 - Sekhat
@Sekhat,如果Update方法每16毫秒运行一次,那么就是每秒60次更新。我非常怀疑用户能够以超过60个字符的速度输入每秒钟(即每分钟3600个字符)。:)甚至很少有人可以输入700 CPM(请参见typing-speed-test.aoeu.eu/?lang=en),而我处于80分位数,只有330 CPM。 - davidsbro
如果我在16毫秒内按下并释放一个键,你的代码将永远不会注册它。这比听起来容易,相信我。 - Sekhat
@Sekhat,你只是在猜测(你真的尝试过吗?)。我试图按下一个键,以便不让它注册,但每次按键都被注册了。此外,为什么用户会尝试如此快地按下一个键呢?他们在使用GamePad时也这样做吗?是否还需要钩子来使gamepad正常工作?另外,请注意您接受了使用keyboardstate的答案。 - davidsbro
完全没有猜测,当我最初使用内置的XNA工具开发某些东西时,我就遇到了这个问题。编辑:至于用户按键那么快,对我来说,这只是在测试使用KeyboardState的初始打字代码时输入句子时发生的。 - Sekhat

0
也许我没有理解问题,但是为什么你不能使用XNA键盘和KeyboardState类呢?

这是因为虽然您可以读取按键状态,但无法按照用户输入的方式访问已输入的文本。 - Sekhat
3
啊,我明白了。过去,我为此编写了一个实用类,类似于http://www.gamedev.net/community/forums/topic.asp?topic_id=457783上的那个。 - Jimmy
谢谢,看来我的窗口钩子是可行的,我没有收到WM_CHAR的原因是因为XNA消息泵不进行消息转换。我会试一试其他能得到的东西 :) - Sekhat

0

这个页面是关于在XNA中拦截WM_CHAR的谷歌搜索结果的顶部,所以我在这里留下一些笔记。也许对其他人有用(如果他们能理解我的英语=))。

我尝试使用Sekhat的windowshook代码,但似乎应该将WH_GETMESSAGE传递给SetWindowsHookEx,而不是Win32.HookType.WH_CALLWNDPROC(只有使用WH_GETMESSAGE代码时,lparaw才会指向Win32.Message)。

此外,有时会出现重复的消息(wparam为0)。 (看这里 - http://msdn.microsoft.com/en-us/library/ms644981%28v=VS.85%29.aspx 关于WPARAM中的PM_NOREMOVE / PM_REMOVE的一些内容)

当我添加类似于这样的东西时

    if (nCode >= 0 && wParam == 1)
    {
        Win32.TranslateMessage(ref lParam); 
        WndProc(ref lParam);
    }

wm_keypress wm_char重复停止(我猜1是PM_NOREMOVE或PM_REMOVE)。

附言: Nuclex变体现在显示404页面,但可通过WebArchive查看。 Nuclex变体有效,但会导致本机XNA MouseState(在XNA 3.1上)的鼠标滚轮处理出现故障=(


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