jjzjj

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

coder 2024-05-22 原文

我希望我的游戏有正常的文本输入,但使用纯 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 由字体自动设置)。

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

    要显示该框,请拨打 .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# - 向 XNA 游戏添加类似输入框的控件的更多相关文章

    1. ruby - 我需要将 Bundler 本身添加到 Gemfile 中吗? - 2

      当我使用Bundler时,是否需要在我的Gemfile中将其列为依赖项?毕竟,我的代码中有些地方需要它。例如,当我进行Bundler设置时:require"bundler/setup" 最佳答案 没有。您可以尝试,但首先您必须用鞋带将自己抬离地面。 关于ruby-我需要将Bundler本身添加到Gemfile中吗?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/4758609/

    2. ruby - 将 Bootstrap Less 添加到 Sinatra - 2

      我有一个ModularSinatra应用程序,我正在尝试将Bootstrap添加到应用程序中。get'/bootstrap/application.css'doless:"bootstrap/bootstrap"end我在views/bootstrap中有所有less文件,包括bootstrap.less。我收到这个错误:Less::ParseErrorat/bootstrap/application.css'reset.less'wasn'tfound.Bootstrap.less的第一行是://CSSReset@import"reset.less";我尝试了所有不同的路径格式,但它

    3. ruby - 续集在添加关联时访问many_to_many连接表 - 2

      我正在使用Sequel构建一个愿望list系统。我有一个wishlists和itemstable和一个items_wishlists连接表(该名称是续集选择的名称)。items_wishlists表还有一个用于facebookid的额外列(因此我可以存储opengraph操作),这是一个NOTNULL列。我还有Wishlist和Item具有续集many_to_many关联的模型已建立。Wishlist类也有:selectmany_to_many关联的选项设置为select:[:items.*,:items_wishlists__facebook_action_id].有没有一种方法可以

    4. c# - 如何在 ruby​​ 中调用 C# dll? - 2

      如何在ruby​​中调用C#dll? 最佳答案 我能想到几种可能性:为您的DLL编写(或找人编写)一个COM包装器,如果它还没有,则使用Ruby的WIN32OLE库来调用它;看看RubyCLR,其中一位作者是JohnLam,他继续在Microsoft从事IronRuby方面的工作。(估计不会再维护了,可能不支持.Net2.0以上的版本);正如其他地方已经提到的,看看使用IronRuby,如果这是您的技术选择。有一个主题是here.请注意,最后一篇文章实际上来自JohnLam(看起来像是2009年3月),他似乎很自在地断言RubyCL

    5. C# 到 Ruby sha1 base64 编码 - 2

      我正在尝试在Ruby中复制Convert.ToBase64String()行为。这是我的C#代码:varsha1=newSHA1CryptoServiceProvider();varpasswordBytes=Encoding.UTF8.GetBytes("password");varpasswordHash=sha1.ComputeHash(passwordBytes);returnConvert.ToBase64String(passwordHash);//returns"W6ph5Mm5Pz8GgiULbPgzG37mj9g="当我在Ruby中尝试同样的事情时,我得到了相同sha

    6. ruby - 可以通过多少种方法将方法添加到 ruby​​ 对象? - 2

      当谈到运行时自省(introspection)和动态代码生成时,我认为ruby​​没有任何竞争对手,可能除了一些lisp方言。前几天,我正在做一些代码练习来探索ruby​​的动态功能,我开始想知道如何向现有对象添加方法。以下是我能想到的3种方法:obj=Object.new#addamethoddirectlydefobj.new_method...end#addamethodindirectlywiththesingletonclassclass这只是冰山一角,因为我还没有探索instance_eval、module_eval和define_method的各种组合。是否有在线/离线资

    7. ruby - 如何在 Ruby 中向现有方法定义添加语句 - 2

      我注意到类定义,如果我打开classMyClass,并在不覆盖的情况下添加一些东西我仍然得到了之前定义的原始方法。添加的新语句扩充了现有语句。但是对于方法定义,我仍然想要与类定义相同的行为,但是当我打开defmy_method时似乎,def中的现有语句和end被覆盖了,我需要重写一遍。那么有什么方法可以使方法定义的行为与定义相同,类似于super,但不一定是子类? 最佳答案 我想您正在寻找alias_method:classAalias_method:old_func,:funcdeffuncold_func#similartoca

    8. ruby-on-rails - 添加回形针新样式不影响旧上传的图像 - 2

      我有带有Logo图像的公司模型has_attached_file:logo我用他们的Logo创建了许多公司。现在,我需要添加新样式has_attached_file:logo,:styles=>{:small=>"30x15>",:medium=>"155x85>"}我是否应该重新上传所有旧数据以重新生成新样式?我不这么认为……或者有什么rake任务可以重新生成样式吗? 最佳答案 参见Thumbnail-Generation.如果rake任务不适合你,你应该能够在控制台中使用一个片段来调用重新处理!关于相关公司

    9. ruby - 我如何添加二进制数据来遏制 POST - 2

      我正在尝试使用Curbgem执行以下POST以解析云curl-XPOST\-H"X-Parse-Application-Id:PARSE_APP_ID"\-H"X-Parse-REST-API-Key:PARSE_API_KEY"\-H"Content-Type:image/jpeg"\--data-binary'@myPicture.jpg'\https://api.parse.com/1/files/pic.jpg用这个:curl=Curl::Easy.new("https://api.parse.com/1/files/lion.jpg")curl.multipart_form_

    10. 基于C#实现简易绘图工具【100010177】 - 2

      C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

    随机推荐