14

ゲームで通常のテキスト入力を行いたいのですが、純粋な XNA を使用して行うのは非常に不快です。

以前に、ゲーム全体で使用できる次のコードを見つけましMessageBoxた。実行を安全に一時停止し、メッセージを表示します。

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

InputBoxできればゲームを中断 (一時停止) せずに、ゲームに機能を追加できるこれに似たものはありますか?

4

3 に答える 3

36

ああ、テキスト入力 - 私はこれについてごく最近の経験があります。

問題

通常、Keyboard.GetKeyboardState()テキスト入力を取得するのが苦手です。これには多くの理由がありますが、そのいくつかは次のとおりです。

  • どのキーが押されたかを検出するには、巨大なスイッチをコーディングする必要があります
  • 文字を大文字にするかどうかを手動で検出する必要があります (Shift または CapsLock)
  • これらのようなキーを解読してOemPeriod(テストのように) 実際の場所を確認し、それらを特定の値にマップする必要があります。
  • キーボード レイアウトまたはキーボード言語を検出/使用する方法はありません
  • キーが押された場合にタイミングを繰り返すための独自のメカニズムを実装する必要があります

問題の 2 番目の部分は、現在入力を受け取っている TextBox (または一般的な UI コントロール) を検出することです。

3 番目に、指定された境界で TextBox を描画する必要があります。また、キャレット (点滅する垂直位置インジケーター)、現在の選択 (実装するまでに行きたい場合)、を表すテクスチャを描画することもできます。ボックス、および強調表示された (マウスで) または選択された (フォーカスがある) 状態のテクスチャ。

第 4 に、コピー アンド ペースト機能を手動で実装する必要があります。


クイックノート

私はそれらを必要としなかったので、おそらくこれらすべての機能は必要ありません。単純な入力と、Enter や Tab などのキー、およびマウス クリックの検出が必要なだけです。たぶん貼り付けも。

解決

問題は (少なくとも、X-Box や WP7 ではなく、Windows について話す場合)、オペレーティング システムには、キーボードから必要なものすべてを実装するために必要なメカニズムが既に備わっているということです。

  • 現在のキーボード レイアウトと言語に基づいて文字を表示します
  • 繰り返し入力を自動的に処理します(キーが押し下げられた場合)
  • 特殊文字を自動的に大文字にして提供する

キーボード入力を取得するために使用するソリューションは、この Gamedev.net フォーラムの投稿からコピーしました。それは以下のコードです。これを .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タイプのプロパティを持つことによって、キーボード入力のディスパッチを処理する class を作成することでした。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を設定することを忘れないでください(はフォントによって自動設定されます)。XYWidthHeight

私がボックスに使用したテクスチャはテキスト ボックス テクスチャ(ハイライトされていないものにはグラデーションがあり、黒い背景で見栄えがします:))

ボックスを表示するには、スプライトバッチが既に開始されている (呼び出された!!!).Draw()インスタンスで (メソッド内で) メソッドを呼び出します。表示しているボックスごとに、マウス入力を受け取りたい場合はメソッドを呼び出す必要があります。Game.Draw()SpriteBatch.Begin().Update()

特定のインスタンスにキーボード入力を受信させたい場合は、KeyboardDispatcherインスタンスを使用して次のようにサブスクライブします。

_keyboardDispatcher.Subscriber = _usernameTextBox;

Clickテキストボックスの、Tabおよびイベントを使用してEnterサブスクライバーを切り替えることができます (タブで移動し、クリックして選択できると、UI に非常に優れた感触が得られるため、これをお勧めします)。


未解決の問題

テキストがボックスよりも広い場合にボックスがテキストをパンできる機能、キャレットを移動する機能 (追加するだけでなくテキストを挿入する機能) など、私が実装していないいくつかの機能について話しました。テキストなどを選択してコピーします。

これらの問題は、軽度から中程度の努力で解決できると確信していますが、そうする前に、次のことを自問してください。

本当に必要ですか?

于 2012-04-19T06:42:32.473 に答える
1

このようなコードを何度か書いたことがあるので、XNA で基本的なテキスト ボックスをプログラミングすることはそれほど難しくないと思います。背景色、ユーザーが入力した内容を表す文字列で塗りつぶす四角形を定義し、四角形内に Spritebatch.DrawString() を使用して文字列を表示します。SpriteFont.MeasureString() を使用すると、テキストを自由に配置したり、立ち入り禁止の場合にテキストを次の行に折り返すことができます。

次に、更新ごとに Keyboard.GetState() を見て、どのキーが押されたかを確認します。これが最大の問題かもしれません。ユーザーがすばやくタイプすると、キーストロークを見逃す可能性があります。ゲームは 1 秒あたりの更新回数が非常に多いためです。この問題はインターネット上で広く文書化されており、解決策があります。たとえば、こちら.

もう 1 つのオプションは、 Nuclexフレームワークで得られるものなど、事前に作成された XNA GUI コンポーネントを使用することです。

于 2012-04-19T02:24:56.163 に答える
-4

まあ、最も簡単な方法は次のとおりです(私の観点から;])

using TextboxInputTest.Textbox.TextInput;
private TextboxInput _inputTextBox

次に、マウスを有効にすることをお勧めします(表示するように設定します)

IsMouseVisible = true;

ここで、textBox 自体を初期化する必要があります

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

これはゲームの略で、これはこれです(変更する必要があるとは思えません)

background_box は、表示するために使用したい画像の名前です(これにはデフォルトのオプションはありません)

Arial はあなたが使いたいフォントです (ゲームのコンテンツに追加する必要があることを忘れないでください)

ボックスの位置を設定する

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

そして最後のステップとして、ボックスをコンポーネント配列に追加する必要があります

Components.Add(this._inputTextBox);

編集したい機能がたくさんあります。そのために、IntelliSense を使用することをお勧めします。

編集:私のせいです、すみません、私はそれらを頻繁に使用しているので、それを完全に忘れていました;]事前に言っておきますが、以下に表示されているのは私の仕事ではありません

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

それが役に立ったことを願っています。

よろしく、

ルレ

于 2012-04-18T20:44:29.963 に答える