【问题标题】:Adding inputbox-like control to XNA game为 XNA 游戏添加类似输入框的控件
【发布时间】:2012-04-18 19:40:34
【问题描述】:

我希望我的游戏有正常的文本输入,但使用纯 XNA 似乎很不愉快。

之前我发现这段代码可以让我在游戏中使用MessageBox,安全地暂停它的执行并显示一条消息:

[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern uint MessageBox(IntPtr hWnd, String text, String caption, uint type);

是否有类似的东西可以为我的游戏添加InputBox 功能,最好不要中断(暂停)游戏?

【问题讨论】:

    标签: c# xna


    【解决方案1】:

    啊,文本输入 - 我最近有这方面的经验。

    问题

    通常,Keyboard.GetKeyboardState() 在获取文本输入方面很糟糕,这有很多原因,其中一些是:

    • 你必须编写一个巨大的开关来检测按下了什么键
    • 您必须手动检测是否将字母大写(Shift 或 CapsLock)
    • 您必须破译那些类似于OemPeriod 的密钥(如在测试中)以查看它们的实际位置,并将它们映射到特定值。
    • 无法检测/使用键盘布局或键盘语言
    • 您必须实现自己的计时重复机制,以防按键被按住

    问题的第二部分是检测您的哪个文本框(或一般的 UI 控件)当前正在接收此输入,因为您不希望所有框都在您键入时接收文本。

    第三,您需要在指定的范围内绘制 TextBox,您还可以绘制插入符号(闪烁的垂直位置指示器)、当前选择(如果您想实现它)、纹理表示框,以及突出显示(使用鼠标)或选中(有焦点)状态的纹理。

    第四,您必须手动实现复制粘贴功能。


    快速笔记

    您可能不需要所有这些功能,因为我不需要它们。您只需要简单的输入,并检测诸如 enter 或 tab 之类的键,以及鼠标单击。也许也可以粘贴。

    解决方案

    问题是(至少当我们谈论 Windows,而不是 X-Box 或 WP7 时),操作系统已经拥有必要的机制来实现您通过键盘所需的一切:

    • 根据当前键盘布局和语言提供字符
    • 自动处理重复输入(在按键被按住的情况下)
    • 自动大写并提供特殊字符

    我用于获取键盘输入的解决方案,我复制了 this Gamedev.net forum post。它是下面的代码,您只需将其复制粘贴到一个 .cs 文件中,您将永远不必再次打开该文件。

    它用于从您的键盘接收本地化输入,您需要做的就是在您的Game.Initialize() 覆盖方法中初始化它(通过使用 Game.Window),并连接到事件以在您的任何地方接收输入'我喜欢。

    您需要将PresentationCore (PresentationCore.dll) 添加到您的引用中才能使用此代码(System.Windows.Input 命名空间需要)。这适用于 .NET 4.0 和 .NET 4.0 客户端配置文件。

    事件输入

    using System;
    using System.Runtime.InteropServices;   
    using Microsoft.Xna.Framework;
    using Microsoft.Xna.Framework.Input;
    using System.Text;
    using System.Windows.Input;
    
    namespace EventInput
    {
    
        public class KeyboardLayout
        {
            const uint KLF_ACTIVATE = 1; //activate the layout
            const int KL_NAMELENGTH = 9; // length of the keyboard buffer
            const string LANG_EN_US = "00000409";
            const string LANG_HE_IL = "0001101A";
    
            [DllImport("user32.dll")]
            private static extern long LoadKeyboardLayout(
                  string pwszKLID,  // input locale identifier
                  uint Flags       // input locale identifier options
                  );
    
            [DllImport("user32.dll")]
            private static extern long GetKeyboardLayoutName(
                  System.Text.StringBuilder pwszKLID  //[out] string that receives the name of the locale identifier
                  );
    
            public static string getName()
            {
                System.Text.StringBuilder name = new System.Text.StringBuilder(KL_NAMELENGTH);
                GetKeyboardLayoutName(name);
                return name.ToString();
            }
        }
    
        public class CharacterEventArgs : EventArgs
        {
            private readonly char character;
            private readonly int lParam;
    
            public CharacterEventArgs(char character, int lParam)
            {
                this.character = character;
                this.lParam = lParam;
            }
    
            public char Character
            {
                get { return character; }
            }
    
            public int Param
            {
                get { return lParam; }
            }
    
            public int RepeatCount
            {
                get { return lParam & 0xffff; }
            }
    
            public bool ExtendedKey
            {
                get { return (lParam & (1 << 24)) > 0; }
            }
    
            public bool AltPressed
            {
                get { return (lParam & (1 << 29)) > 0; }
            }
    
            public bool PreviousState
            {
                get { return (lParam & (1 << 30)) > 0; }
            }
    
            public bool TransitionState
            {
                get { return (lParam & (1 << 31)) > 0; }
            }
        }
    
        public class KeyEventArgs : EventArgs
        {
            private Keys keyCode;
    
            public KeyEventArgs(Keys keyCode)
            {
                this.keyCode = keyCode;
            }
    
            public Keys KeyCode
            {
                get { return keyCode; }
            }
        }
    
        public delegate void CharEnteredHandler(object sender, CharacterEventArgs e);
        public delegate void KeyEventHandler(object sender, KeyEventArgs e);
    
        public static class EventInput
        {
            /// <summary>
            /// Event raised when a character has been entered.
            /// </summary>
            public static event CharEnteredHandler CharEntered;
    
            /// <summary>
            /// Event raised when a key has been pressed down. May fire multiple times due to keyboard repeat.
            /// </summary>
            public static event KeyEventHandler KeyDown;
    
            /// <summary>
            /// Event raised when a key has been released.
            /// </summary>
            public static event KeyEventHandler KeyUp;
    
            delegate IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
    
            static bool initialized;
            static IntPtr prevWndProc;
            static WndProc hookProcDelegate;
            static IntPtr hIMC;
    
            //various Win32 constants that we need
            const int GWL_WNDPROC = -4;
            const int WM_KEYDOWN = 0x100;
            const int WM_KEYUP = 0x101;
            const int WM_CHAR = 0x102;
            const int WM_IME_SETCONTEXT = 0x0281;
            const int WM_INPUTLANGCHANGE = 0x51;
            const int WM_GETDLGCODE = 0x87;
            const int WM_IME_COMPOSITION = 0x10f;
            const int DLGC_WANTALLKEYS = 4;
    
            //Win32 functions that we're using
            [DllImport("Imm32.dll", CharSet = CharSet.Unicode)]
            static extern IntPtr ImmGetContext(IntPtr hWnd);
    
            [DllImport("Imm32.dll", CharSet = CharSet.Unicode)]
            static extern IntPtr ImmAssociateContext(IntPtr hWnd, IntPtr hIMC);
    
            [DllImport("user32.dll", CharSet = CharSet.Unicode)]
            static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
    
            [DllImport("user32.dll", CharSet = CharSet.Unicode)]
            static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
    
    
            /// <summary>
            /// Initialize the TextInput with the given GameWindow.
            /// </summary>
            /// <param name="window">The XNA window to which text input should be linked.</param>
            public static void Initialize(GameWindow window)
            {
                if (initialized)
                    throw new InvalidOperationException("TextInput.Initialize can only be called once!");
    
                hookProcDelegate = new WndProc(HookProc);
                prevWndProc = (IntPtr)SetWindowLong(window.Handle, GWL_WNDPROC,
                    (int)Marshal.GetFunctionPointerForDelegate(hookProcDelegate));
    
                hIMC = ImmGetContext(window.Handle);
                initialized = true;
            }
    
            static IntPtr HookProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
            {
                IntPtr returnCode = CallWindowProc(prevWndProc, hWnd, msg, wParam, lParam);
    
                switch (msg)
                {
                    case WM_GETDLGCODE:
                        returnCode = (IntPtr)(returnCode.ToInt32() | DLGC_WANTALLKEYS);
                        break;
    
                    case WM_KEYDOWN:
                        if (KeyDown != null)
                            KeyDown(null, new KeyEventArgs((Keys)wParam));
                        break;
    
                    case WM_KEYUP:
                        if (KeyUp != null)
                            KeyUp(null, new KeyEventArgs((Keys)wParam));
                        break;
    
                    case WM_CHAR:
                        if (CharEntered != null)
                            CharEntered(null, new CharacterEventArgs((char)wParam, lParam.ToInt32()));
                        break;
    
                    case WM_IME_SETCONTEXT:
                        if (wParam.ToInt32() == 1)
                            ImmAssociateContext(hWnd, hIMC);
                        break;
    
                    case WM_INPUTLANGCHANGE:
                        ImmAssociateContext(hWnd, hIMC);
                        returnCode = (IntPtr)1;
                        break;
                }
    
                return returnCode;
            }
        }
    }
    

    现在您已经可以按原样使用它了(通过订阅EventInput.CharEntered 事件),并使用逻辑来检测将您的输入发送到哪里。


    KeyboardDispatcher、IKeyboardSubscriber

    我所做的是创建一个类KeyboardDispatcher,它通过拥有一个IKeyboardSubscriber 类型的属性来处理键盘输入的分派,它将接收到的输入发送到该属性。想法是将此属性设置为要接收输入的 UI 控件。

    定义如下:

    public interface IKeyboardSubscriber
    {
        void RecieveTextInput(char inputChar);
        void RecieveTextInput(string text);
        void RecieveCommandInput(char command);
        void RecieveSpecialInput(Keys key);
    
        bool Selected { get; set; } //or Focused
    }
    
    public class KeyboardDispatcher
    {
        public KeyboardDispatcher(GameWindow window)
        {
            EventInput.EventInput.Initialize(window);
            EventInput.EventInput.CharEntered += new EventInput.CharEnteredHandler(EventInput_CharEntered);
            EventInput.EventInput.KeyDown += new EventInput.KeyEventHandler(EventInput_KeyDown);
        }
    
        void EventInput_KeyDown(object sender, EventInput.KeyEventArgs e)
        {
            if (_subscriber == null)
                return;
    
            _subscriber.RecieveSpecialInput(e.KeyCode);
        }
    
        void EventInput_CharEntered(object sender, EventInput.CharacterEventArgs e)
        {
            if (_subscriber == null)
                return;
            if (char.IsControl(e.Character))
            {
                //ctrl-v
                if (e.Character == 0x16)
                {
                    //XNA runs in Multiple Thread Apartment state, which cannot recieve clipboard
                    Thread thread = new Thread(PasteThread);
                    thread.SetApartmentState(ApartmentState.STA);
                    thread.Start();
                    thread.Join();
                    _subscriber.RecieveTextInput(_pasteResult);
                }
                else
                {
                    _subscriber.RecieveCommandInput(e.Character);
                }
            }
            else
            {
                _subscriber.RecieveTextInput(e.Character);
            }
        }
    
        IKeyboardSubscriber _subscriber;
        public IKeyboardSubscriber Subscriber
        {
            get { return _subscriber; }
            set
            {
                if (_subscriber != null)
                    _subscriber.Selected = false;
                _subscriber = value;
                if(value!=null)
                    value.Selected = true;
            }
        }
    
        //Thread has to be in Single Thread Apartment state in order to receive clipboard
        string _pasteResult = "";
        [STAThread]
        void PasteThread()
        {
            if (Clipboard.ContainsText())
            {
                _pasteResult = Clipboard.GetText();
            }
            else
            {
                _pasteResult = "";
            }
        }
    }
    

    使用相当简单,实例化KeyboardDispatcher,即在Game.Initialize()中并保持对它的引用(这样你就可以在选定的[focused]控件之间切换),并传递一个使用IKeyboardSubscriber接口的类,比如你的TextBox


    文本框

    接下来是您的实际控制。现在我最初编写了一个相当复杂的盒子,它使用渲染目标将文本渲染到纹理,这样我就可以移动它(如果文本比盒子大),但是在经历了很多痛苦之后,我放弃了它并做了一个非常简单的版本。随时改进它!

    public delegate void TextBoxEvent(TextBox sender);
    
    public class TextBox : IKeyboardSubscriber
    {
        Texture2D _textBoxTexture;
        Texture2D _caretTexture;
    
        SpriteFont _font;
    
        public int X { get; set; }
        public int Y { get; set; }
        public int Width { get; set; }
        public int Height { get; private set; }
    
        public bool Highlighted { get; set; }
    
        public bool PasswordBox { get; set; }
    
        public event TextBoxEvent Clicked;
    
        string _text = "";
        public String Text
        {
            get
            {
                return _text;
            }
            set
            {
                _text = value;
                if (_text == null)
                    _text = "";
    
                if (_text != "")
                {
                    //if you attempt to display a character that is not in your font
                    //you will get an exception, so we filter the characters
                    //remove the filtering if you're using a default character in your spritefont
                    String filtered = "";
                    foreach (char c in value)
                    {
                        if (_font.Characters.Contains(c))
                            filtered += c;
                    }
    
                    _text = filtered;
    
                    while (_font.MeasureString(_text).X > Width)
                    {
                        //to ensure that text cannot be larger than the box
                        _text = _text.Substring(0, _text.Length - 1);
                    }
                }
            }
        }
    
        public TextBox(Texture2D textBoxTexture, Texture2D caretTexture, SpriteFont font)
        {
            _textBoxTexture = textBoxTexture;
            _caretTexture = caretTexture;
            _font = font;           
    
            _previousMouse = Mouse.GetState();
        }
    
        MouseState _previousMouse;
        public void Update(GameTime gameTime)
        {
            MouseState mouse = Mouse.GetState();
            Point mousePoint = new Point(mouse.X, mouse.Y);
    
            Rectangle position = new Rectangle(X, Y, Width, Height);
            if (position.Contains(mousePoint))
            {
                Highlighted = true;
                if (_previousMouse.LeftButton == ButtonState.Released && mouse.LeftButton == ButtonState.Pressed)
                {
                    if (Clicked != null)
                        Clicked(this);
                }
            }
            else
            {
                Highlighted = false;
            }
        }
    
        public void Draw(SpriteBatch spriteBatch, GameTime gameTime)
        {
            bool caretVisible = true;
    
            if ((gameTime.TotalGameTime.TotalMilliseconds % 1000) < 500)
                caretVisible = false;
            else
                caretVisible = true;
    
            String toDraw = Text;
    
            if (PasswordBox)
            {
                toDraw = "";
                for (int i = 0; i < Text.Length; i++)
                    toDraw += (char) 0x2022; //bullet character (make sure you include it in the font!!!!)
            } 
    
            //my texture was split vertically in 2 parts, upper was unhighlighted, lower was highlighted version of the box
            spriteBatch.Draw(_textBoxTexture, new Rectangle(X, Y, Width, Height), new Rectangle(0, Highlighted ? (_textBoxTexture.Height / 2) : 0, _textBoxTexture.Width, _textBoxTexture.Height / 2), Color.White);
    
    
    
            Vector2 size = _font.MeasureString(toDraw);
    
            if (caretVisible && Selected)
                spriteBatch.Draw(_caretTexture, new Vector2(X + (int)size.X + 2, Y + 2), Color.White); //my caret texture was a simple vertical line, 4 pixels smaller than font size.Y
    
            //shadow first, then the actual text
            spriteBatch.DrawString(_font, toDraw, new Vector2(X, Y) + Vector2.One, Color.Black);
            spriteBatch.DrawString(_font, toDraw, new Vector2(X, Y), Color.White);
        }
    
    
        public void RecieveTextInput(char inputChar)
        {
            Text = Text + inputChar;
        }
        public void RecieveTextInput(string text)
        {
            Text = Text + text;
        }
        public void RecieveCommandInput(char command)
        {
            switch (command)
            {
                case '\b': //backspace
                    if (Text.Length > 0)
                        Text = Text.Substring(0, Text.Length - 1);
                    break;
                case '\r': //return
                    if (OnEnterPressed != null)
                        OnEnterPressed(this);
                    break;
                case '\t': //tab
                    if (OnTabPressed != null)
                        OnTabPressed(this);
                    break;
                default:
                    break;
            }
        }
        public void RecieveSpecialInput(Keys key)
        {
    
        }
    
        public event TextBoxEvent OnEnterPressed;
        public event TextBoxEvent OnTabPressed;
    
        public bool Selected
        {
            get;
            set;
        }
    }
    

    当您实例化TextBox 时,不要忘记在实例上设置XYWidth (!!!) 值(Height 是由字体自动设置的)。

    我用于盒子的纹理是(未突出显示有渐变,在黑色背景上看起来不错:))

    要显示该框,请在实例上调用 .Draw() 方法(在您的 Game.Draw() 方法中),并已启动 spritebatch(已调用 SpriteBatch.Begin() !!!)。对于您显示的每个框,如果您希望它接收鼠标输入,您应该调用.Update() 方法。

    当您希望特定实例接收键盘输入时,请使用您的 KeyboardDispatcher 实例订阅它,例如:

    _keyboardDispatcher.Subscriber = _usernameTextBox;
    

    您可以使用文本框上的 ClickTabEnter 事件来切换订阅者(我建议您这样做,因为当您可以通过标签浏览它时,它会给 UI 带来非常好的感觉,然后单击以选择)。


    未解决的问题

    Ofc,我谈到了一些我没有实现的功能,例如如果文本比框宽,框能够平移文本,移动插入符号的能力(插入文本,而不仅仅是追加)、选择和复制文本等。

    我敢肯定,这些问题你可以通过轻微到中等的努力来解决,但在你这样做之前,问问自己:

    我真的需要它吗?

    【讨论】:

    • >我真的需要它吗? —不,我将限制最大字符串长度:)
    • 这就是精神 :) 此外,正如我在上面粘贴的代码中一样,长度限制为框的宽度,因此您无需添加任何内容!祝你好运!
    • 写得真好,代码也不错。当我看到 XNA 键盘输入有多么糟糕时,我知道人们必须做一些不同的事情,因为框架提供的解决方案远远不够。感谢您的帮助!
    • 这真是太棒了。在找到这个之前,我花了很长时间尝试根据 shift 和 caps lock 手动映射键。
    • @NikoDrašković 您可以将您的线程创建/使用代码复制到停止挂起的 KeyboardDispatcher 的构造函数中。显然,这仅在您在程序启动期间创建实例时才有效,额外的十分之几秒将被忽视。将[STAThread] 添加到Program.Main 也可以,但正如您所说,我不确定风险。我知道这不适用于 Xbox 360。我将使用黑客的方式在启动过程中抢先获取剪贴板的内容,即使它没有做任何事情,因为这不会影响以后的表现。
    【解决方案2】:

    已经编写了几次这样的代码,我想说在 XNA 中编写一个基本的文本框并不难。您定义一个用背景颜色填充的矩形,一个代表用户输入内容的字符串,并在矩形内使用 Spritebatch.DrawString() 显示该字符串!使用 SpriteFont.MeasureString(),您可以随心所欲地对齐文本,将文本换行到下一行,等等。

    然后您查看 Keyboard.GetState() 每次更新并检查已按下哪些键。这可能是最大的问题,因为如果用户输入速度很快,你会错过一些击键 - 游戏每秒只更新这么多次。这个问题在互联网上被广泛记录,并且有解决方案,例如here

    另一种选择是使用预制的 XNA GUI 组件,例如使用 Nuclex 框架获得的组件。

    【讨论】:

      【解决方案3】:

      嗯,最简单的方法如下(从我的角度来看;])

      using TextboxInputTest.Textbox.TextInput;
      private TextboxInput _inputTextBox
      

      那么我建议启用鼠标(设置为可见)

      IsMouseVisible = true;
      

      现在你需要初始化文本框本身

      this._inputTextBox = new TextboxInput(this, "background_box", "Arial");
      

      这代表游戏,就是这个(怀疑你需要改变它)

      background_box 是你想用来显示的图片的名称(afaik,没有默认选项)

      Arial 是你想要使用的字体(别忘了你必须将它添加到游戏内容中

      设置盒子的位置

      this._inputTextBox.Position = new Vector2(100,100);
      

      最后一步,你必须将盒子添加到组件数组中

      Components.Add(this._inputTextBox);
      

      您可能想要编辑许多功能,为此,我建议使用 IntelliSense

      编辑:我的错,对不起,我使用它们太常见了,我完全忘记了;] 提前说,你在下面看到的不是我的工作

      http://www.4shared.com/file/RVqzHWk0/TextboxInput.html

      希望对您有所帮助。

      问候,

      雷莱斯

      【讨论】:

      • 嗯,我在哪里可以找到 TextboxInputTest.Textbox.TextInput 我需要这样做?
      • -1:这个答案几乎完全没用。你告诉他使用不属于标准 XNA 库的东西,而不是告诉他从哪里得到它或如何制作它。
      猜你喜欢
      • 2010-12-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-11-12
      • 2012-04-01
      • 1970-01-01
      相关资源
      最近更新 更多