This article will teach you how to capture keyboard input in your
XNA-based game the right way. Keyboard input in XNA is non-trivial
because the classes provided in the
Microsoft.Xna.Framework.Input namespace are not
equipped for text input.
Polling XNA's Keyboard class for pressed keys might get
you somewhat further, but has the following shortcomings:
- You might miss keys that were pressed between your poll intervals. Since polling typically occurs once per frame, the lower the frame rate, the more trouble the player will have typing.
-
Supporting different keyboard layouts with special characters will
prove difficult. Windows'
MapVirtualKeyEx()andToAscii()can handle some of the intricacies, but there are things that won't work - accents for example are entered by first pressing the accent key and then the letter to be accented. - Text entry for far-east languages like Chinese or Japanese works entirely different from western text entry. A japanese user will type an entire word phonetically and then be presented with a series of possible characters he could be meaning, with the most likely ones being listed first. This is done using the IME (Input Method Editor), which you have to render yourself in a full-screen application.
WM_CHAR
The only possible way to get text input right is to process
WM_CHAR messages yourself. To do so, you need to hook
the window procedure of XNA's rendering window and intercept
WM_GETDLGCODE and aWM_CHAR messages.
Of course, this approach is not portable to the XBox 360. While you can attach USB keyboards to the XBox 360, you cannot hook XNA's window on the XBox 360 and even if that would be possible, your XBox 360 does not have the keyboard layouts, character sets and IME support your PC has.
I think the best compromise one can achieve is to provide traditional console-style text entry (letting the user select the characters from a matrix using the gamepad) while additionally allowing the use of the keyboard on the PC side. This is doable with little effort by just disabling the keyboard processing for PC compiles.
Code
Here's a small class that will hook the XNA rendering window and capture keyboard and mouse input from it.
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Microsoft.Xna.Framework.Input;
using NativeWindow = System.Windows.Forms.NativeWindow;
using Message = System.Windows.Forms.Message;
namespace Nuclex {
// TODO: Add IME support (complicated, since we need to draw the IME windows in XNA)
// http://msdn.microsoft.com/en-us/library/bb206300(VS.85).aspx
// Will do this later once the GUI has reached a state where this is possible
/// <summary>
/// Intercepts the input sent to a window and feeds it to an input receiver
/// </summary>
public sealed class WindowInputCapturer : NativeWindow, IInputCapturer, IDisposable {
/// <summary>
/// Flag returned by a window in response to WM_GETDLGCODE to indicate that it
/// is interested in receiving WM_CHAR messages
/// </summary>
private const int DLGC_WANTCHARS = 0x0080;
/// <summary>
/// Flag returned by a window in response to WM_GETDLGCODE to indicate that it
/// wants to process all keyboard input from the user
/// </summary>
private const int DLGC_WANTALLKEYS = 0x0004;
#region enum WindowMessages
/// <summary>List of window message relevant to the input capturer</summary>
private enum WindowMessages : int {
/// <summary>Sent to a window to ask which types of input it processes</summary>
WM_GETDLGCODE = 0x0087,
/// <summary>Indicates that the user has pressed a key on the keyboard</summary>
WM_KEYDOWN = 0x0100,
/// <summary>Indicates that the user has released a key on the keyboard</summary>
WM_KEYUP = 0x0101,
/// <summary>Indicates that the user has entered text</summary>
WM_CHAR = 0x0102,
/// <summary>Indicates that the user has entered text (UTF-32 variant)</summary>
/// <remarks>
/// This is only required if the window is an ANSI window (created by
/// CreateWindowA() and not reset to unicode). In this case, windows will
/// send WM_CHAR with ANSI characters and WM_UNICHAR with UTF-32 characters.
/// </remarks>
WM_UNICHAR = 0x0109,
/// <summary>Indicates that the mouse cursor has been moved</summary>
WM_MOUSEMOVE = 0x0200,
/// <summary>Indicates that the left mouse button was pressed down</summary>
WM_LBUTTONDOWN = 0x0201,
/// <summary>Indicates that the left mouse button was released again</summary>
WM_LBUTTONUP = 0x0202,
/// <summary>Indicates that the right mouse button was pressed down</summary>
WM_RBUTTONDOWN = 0x0204,
/// <summary>Indicates that the right mouse button was released again</summary>
WM_RBUTTONUP = 0x0205,
/// <summary>Indicates that the middle mouse button was pressed down</summary>
WM_MBUTTONDOWN = 0x0207,
/// <summary>Indicates that the middle mouse button was released again</summary>
WM_MBUTTONUP = 0x0208,
/// <summary>Indicates that the mouse wheel has been rotated</summary>
WM_MOUSEHWHEEL = 0x020A,
/// <summary>Indicates that an extended mouse button was pressed down</summary>
WM_XBUTTONDOWN = 0x020B,
/// <summary>Indicates that an extended mouse button was released again</summary>
WM_XBUTTONUP = 0x020C,
/// <summary>Indicates that the mouse wheel has been rotated or tilted</summary>
/// <remarks>
/// This window message is only supported by Windows Vista. Mouse drivers may,
/// however, emulate it on Windows XP by directly communicating with
/// the low-level driver and injecting this message into the active window.
/// </remarks>
WM_MOUSEHWHEEL_TILT = 0x020E
}
#endregion // enum WindowMessages
/// <summary>Initializes a new window input capturer</summary>
/// <param name="windowHandle">Handle of the window to capture input from</param>
public WindowInputCapturer(IntPtr windowHandle) : this(windowHandle, null) { }
/// <summary>Initializes a new window input capturer</summary>
/// <param name="windowHandle">Handle of the window to capture input from</param>
/// <param name="inputReceiver">Input receive all captured input will be sent to</param>
public WindowInputCapturer(IntPtr windowHandle, IInputReceiver inputReceiver) {
AssignHandle(windowHandle);
this.InputReceiver = inputReceiver;
}
/// <summary>Immediately releases the resources owned by the input capturer</summary>
public void Dispose() {
this.inputReceiver = null;
// If this isn't a repetitive call to Dispose(), disconnect the NativeWindow instance
// from the XNA GameWindow handle
if(!this.disposed) {
ReleaseHandle();
this.disposed = true;
}
// The NativeWindow class does some processing in its finalizer which I think
// might not anticipate the finalizer being completely avoided. So to be on the
// safe side, we let its finalizer run even when we have been disposed!
// GC.SuppressFinalize(this);
}
/// <summary>Input receiver any captured input will be sent to</summary>
public IInputReceiver InputReceiver {
get { return this.inputReceiver; }
set { this.inputReceiver = value; }
}
/// <summary>
/// Overridden window message callback used to capture input for the window
/// </summary>
/// <param name="message">Window message sent to the window</param>
protected override void WndProc(ref Message message) {
base.WndProc(ref message);
// If no input receiver has been assigned, we can stop right here
if(this.inputReceiver == null)
return;
// Process the message differently based on its message id
switch(message.Msg) {
// Window is being asked which types of input it can process
case (int)WindowMessages.WM_GETDLGCODE: {
if(Is32Bit) { // If we know we're dealing with 32 bits
int returnCode = message.Result.ToInt32();
returnCode |= (DLGC_WANTALLKEYS | DLGC_WANTCHARS);
message.Result = new IntPtr(returnCode);
} else { // anywhere else, be on the safe side
long returnCode = message.Result.ToInt64();
returnCode |= (DLGC_WANTALLKEYS | DLGC_WANTCHARS);
message.Result = new IntPtr(returnCode);
}
break;
}
// Key on the keyboard was pressed / released
case (int)WindowMessages.WM_KEYDOWN: {
int virtualKeyCode = message.WParam.ToInt32();
this.inputReceiver.InjectKeyPress((Keys)virtualKeyCode);
break;
}
case (int)WindowMessages.WM_KEYUP: {
int virtualKeyCode = message.WParam.ToInt32();
this.inputReceiver.InjectKeyRelease((Keys)virtualKeyCode);
break;
}
// Character has been entered on the keyboard
case (int)WindowMessages.WM_CHAR: {
char character = (char)message.WParam.ToInt32();
this.inputReceiver.InjectCharacter(character);
break;
}
// Mouse has been moved
case (int)WindowMessages.WM_MOUSEMOVE: {
short x = (short)(message.LParam.ToInt32() & 0xFFFF);
short y = (short)(message.LParam.ToInt32() >> 16);
this.inputReceiver.InjectMouseMove(x, y);
break;
}
// Left mouse button pressed / released
case (int)WindowMessages.WM_LBUTTONDOWN: {
this.inputReceiver.InjectMousePress(MouseButtons.Left);
break;
}
case (int)WindowMessages.WM_LBUTTONUP: {
this.inputReceiver.InjectMouseRelease(MouseButtons.Left);
break;
}
// Right mouse button pressed / released
case (int)WindowMessages.WM_RBUTTONDOWN: {
this.inputReceiver.InjectMousePress(MouseButtons.Right);
break;
}
case (int)WindowMessages.WM_RBUTTONUP: {
this.inputReceiver.InjectMouseRelease(MouseButtons.Right);
break;
}
// Middle mouse button pressed / released
case (int)WindowMessages.WM_MBUTTONDOWN: {
this.inputReceiver.InjectMousePress(MouseButtons.Middle);
break;
}
case (int)WindowMessages.WM_MBUTTONUP: {
this.inputReceiver.InjectMouseRelease(MouseButtons.Middle);
break;
}
// Extended mouse button pressed / released
case (int)WindowMessages.WM_XBUTTONDOWN: {
short button = (short)(message.WParam.ToInt32() >> 16);
//int button = (int)(m.WParam.ToInt64() >> 32);
if(button == 1)
this.inputReceiver.InjectMousePress(MouseButtons.X1);
if(button == 2)
this.inputReceiver.InjectMousePress(MouseButtons.X2);
break;
}
case (int)WindowMessages.WM_XBUTTONUP: {
short button = (short)(message.WParam.ToInt32() >> 16);
//int button = (int)(m.WParam.ToInt64() >> 32);
if(button == 1)
this.inputReceiver.InjectMouseRelease(MouseButtons.X1);
if(button == 2)
this.inputReceiver.InjectMouseRelease(MouseButtons.X2);
break;
}
// Mouse wheel rotated
case (int)WindowMessages.WM_MOUSEHWHEEL: {
short ticks = (short)(message.WParam.ToInt32() >> 16);
//int ticks = (int)(m.WParam.ToInt64() >> 32);
this.inputReceiver.InjectMouseWheel((float)ticks / 120.0f);
break;
}
}
}
/// <summary>Determines whether the assembly is executing as 32 bit code</summary>
private static bool Is32Bit {
get { return (IntPtr.Size == 4); }
}
/// <summary>Input receiver any captured input will be sent to</summary>
private IInputReceiver inputReceiver;
/// <summary>True when the object has been disposed</summary>
private bool disposed;
}
} // namespace Nuclex
Post new comment