Capturing Keyboard Input in XNA

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() and ToAscii() 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;
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

The content of this field is kept private and will not be shown publicly.
  • Lines and paragraphs break automatically.
  • Allowed HTML tags: <br> <a> <em> <strong> <u> <i> <b> <cite> <blockcode> <code> <ul> <ol> <li> <dl> <dt> <dd> <p> <pre> <span>
  • You can highlight code with any of the following tags: <blockcode>

More information about formatting options