c# - 向 XNA 游戏添加类似输入框的控件

标签 c# xna

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

早些时候我发现这段代码让我可以使用 MessageBox在我的游戏周围,安全地暂停其执行并显示一条消息:

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

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

最佳答案

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

问题

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

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

  • 问题的第二部分是检测您的哪个文本框(或一般的 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 Client Profile。

    事件输入
    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 时,别忘了设置X , Y , 和 Width (!!!) 实例上的值( Height 由字体自动设置)。

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

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

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

    您可以使用 Click , TabEnter文本框上的事件以切换订阅者(我推荐这样做,因为当您可以通过选项卡浏览它并单击以选择时,它会给 UI 带来非常好的感觉)。

    尚未解决的问题

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

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

    我真的需要吗?

    关于c# - 向 XNA 游戏添加类似输入框的控件,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/10216757/

    相关文章:

    c# - 有没有办法使用 LINQ 将重复项目的集合与其子集合合并?

    c# - 退出我的 wp7 游戏后图形损坏

    c# - 哪个图形库适合技术应用(dx/ogl/xna)

    c# - 使用 c# 和 xna 在单独的类中加载 texture2D

    silverlight - 性能分析 Windows Phone 7 应用程序 (SL/XNA)

    c# - 如何检索上传到 Amazon S3 的图片的 url

    c# - TextTransform.exe 似乎只接受旧版本的 C#

    c# - 使用属性名称设置属性值

    c# - WPF 中的连续进度条?

    c# - 3D/2D 自上而下的 View 使用什么样的投影和相机?