为XNA游戏添加类似输入框的控件

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

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

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

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

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

问题

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

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

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

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

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


快速说明

您可能不需要所有这些function,因为我不需要它们。 您只需要简单的输入,并检测输入或制表符等键,以及鼠标单击。 也许还贴。

问题是(至少当我们谈论Windows而不是X-Box或WP7时),操作系统已经具备了从键盘实现所需的一切所需的机制:

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

我用来获取键盘输入的解决方案,我已经复制了这个Gamedev.net论坛post 。 它是下面的代码,您只需要将其复制粘贴到.cs文件中,您将永远不必再打开它。

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

您需要将PresentationCore (PresentationCore.dll)添加到您的引用,以便使用此代码( System.Windows.Input命名空间所需)。 这适用于.NET 4.0和.NET 4.0 Client Profile。

EventInput

 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 { ///  /// Event raised when a character has been entered. ///  public static event CharEnteredHandler CharEntered; ///  /// Event raised when a key has been pressed down. May fire multiple times due to keyboard repeat. ///  public static event KeyEventHandler KeyDown; ///  /// Event raised when a key has been released. ///  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); ///  /// Initialize the TextInput with the given GameWindow. ///  /// The XNA window to which text input should be linked. 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()保留对它的引用(这样你就可以在选定的[聚焦]控件之间切换),并传递一个使用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由字体自动设置)。

我用于盒子的纹理是 TextBoxTexture (unhighlighted有一个渐变,在黑色背景上看起来不错:))

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

如果希望特定实例接收键盘输入,请使用KeyboardDispatcher实例进行订阅,例如:

 _keyboardDispatcher.Subscriber = _usernameTextBox; 

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


尚未解决的问题

Ofc,我曾谈到过一些我没有实现过的function,例如如果文本宽于框,则能够平移文本的框,能够移动插入符号(插入文本,而不仅仅是追加),以选择和复制文字等

这些问题你可以轻松到中等的努力解决,我很确定,但在你做之前,问问自己:

我真的需要它吗?

编写这样的代码几次之后,我会说在XNA中编写基本文本框并不困难。 您可以定义一个用背景颜色填充的矩形,一个表示用户输入内容的字符串,并使用矩形内的Spritebatch.DrawString()显示字符串! 使用SpriteFont.MeasureString(),您可以根据需要对齐文本,在文本禁止时将文本换行到下一行等。

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

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

嗯,最简单的方法如下(从我的观点来看;))

 using TextboxInputTest.Textbox.TextInput; private TextboxInput _inputTextBox 

然后我建议启用鼠标(将其设置为可见)

 IsMouseVisible = true; 

现在你需要初始化textBox本身

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

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

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

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

设置框的位置

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

作为最后一步,您必须将该框添加到组件数组中

 Components.Add(this._inputTextBox); 

您可能希望编辑许多function,为此,我建议使用IntelliSense

编辑:我的错,对不起,我使用它们通常我完全忘记了这一点;]提前说,你看到的吼叫不是我的工作

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

希望它有所帮助。

问候,

Releis