Ответ 1
А, ввод текста - у меня очень недавний опыт с этим.
Проблема
Обычно Keyboard.GetKeyboardState()
отсасывает при вводе текста, и это по многим причинам, некоторые из которых:
- Вам нужно закодировать HUGE-переключатель, чтобы определить, какой ключ был нажат.
- Вам нужно вручную определить, следует ли писать буквы (Shift или CapsLock)
- Вам нужно расшифровать эти
OemPeriod
-подобные ключи (как в тесте), чтобы увидеть, где они на самом деле, и сопоставить их с определенными значениями. - Невозможно обнаружить/использовать раскладку клавиатуры или язык клавиатуры.
- Вы должны реализовать собственный механизм повторения синхронизации в случае удержания ключа
Вторая часть проблемы заключается в обнаружении того, какой из ваших TextBoxes (или элементов управления пользовательского интерфейса в целом) в настоящее время принимает этот вход, так как вы не хотите, чтобы все ваши ящики получали текст по мере ввода.
В-третьих, вам нужно нарисовать TextBox в определенных границах, и вы также можете рисовать каретку (мигающий индикатор вертикальной позиции), текущий выбор (если вы хотите дойти до его реализации), текстура который представляет поле и текстуры для выделенного (с мышью) или выбранного (с фокусом) состояния.
В-четвертых, вам нужно вручную реализовать функции копирования-вставки.
Быстрая заметка
Вам, вероятно, не нужны все эти функции, поскольку они мне не нужны. Вам просто нужен простой ввод и обнаружение для таких клавиш, как ввод или вкладка, а также щелчок мышью. Возможно также вставить.
Решение
Дело в том, что (по крайней мере, когда мы говорим о Windows, а не X-Box или WP7), в операционной системе уже есть механизмы, необходимые для реализации всего, что вам нужно с клавиатуры:
- Дает символы, основанные на текущей раскладке клавиатуры и языке.
- Автоматически обрабатывает повторяющийся ввод (в случае удержания клавиши)
- Автоматически использует заглавные буквы и предоставляет специальные символы
Решение, которое я использую для ввода клавиатуры, я скопировал этот пост форума Gamedev.net. Это код ниже, и вам просто нужно скопировать его в файл .cs, который вам больше не придется открывать.
Он используется для получения локализованного ввода с вашей клавиатуры, и все, что вам нужно сделать, это инициализировать его в методе переопределения Game.Initialize()
(с помощью Game.Window) и подключиться к событиям, чтобы получать вход в любом месте, d нравится.
Вам нужно добавить PresentationCore
(PresentationCore.dll) к своим ссылкам, чтобы использовать этот код (необходимый для пространства имен System.Windows.Input
). Это работает для .NET 4.0 и для .NET Client Client.
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
{
/// <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
, которому он отправляет принятый ввод. Идея заключается в том, что вы устанавливаете это свойство для этого элемента управления пользовательского интерфейса, который вы хотите получить.
Определения следующие:
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
.
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
, Tab
и Enter
в текстовом поле, чтобы переключаться между подписчиками (что я рекомендую, так как это дает действительно приятное ощущение пользовательскому интерфейсу, когда вы можете его пропустить, и нажмите, чтобы выбрать).
Неразрешенные проблемы
Ofc, я говорил о некоторых функциях, которые я не реализовал, таких как ящик, способный панорамировать текст, если текст был шире, чем поле, возможность перемещать каретку (вставлять текст, а не просто добавлять), для выбора и копирования текста и т.д.
Эти проблемы, которые вы могли бы решить с легким и средним усилием, я в этом уверен, но прежде чем вы это сделаете, спросите себя: