A reader of an earlier post in the MonoGame + WinForms series pointed out a problem I never thought to test for: using MonoGame’s keyboard input with your embedded MonoGame control doesn’t work. Today’s post will solve this.
First, why doesn’t the keyboard just work? Not too surprisingly, MonoGame’s keyboard input is handled by the underlying OpenTK game window, which is bound up behind the Game object. We don’t have a practical way to use the Game object, even when we want to embed our actual game. So just as I suggested replacing the Game object with another implementation, we can replace the keyboard services with a simple implementation that’s compatible with WinForms.
To use MonoGame’s keyboard input, we would call a static method within the Keyboard class, giving us back a KeyboardState to work with. We won’t be able to use the Keyboard class directly, but we can create a parallel version of it that still gives us back KeyboardStates. That’s the easy part. Most of the pain points come from WinForms itself.
Implementation
We’ll start with the WinForms sample from the first MonoGame + WinForms post. We need to take care of the following bits:
- Create a new implementation of Keyboard, which we’ll call ControlKeyboard.
- Write the necessary utility code to convert WinForms keys to XNA keys.
- Add input handling in GraphicsDeviceControl.
- Wire it all up in some WinForms-frienly way.
ControlKeyboard
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
namespace WinFormsGraphicsDevice
{
// Matches the same signatures as Microsoft.Xna.Framework.Input.Keyboard
public static class ControlKeyboard
{
// Since we only have access to KeyboardState's Array constructor, cache arrays to
// prevent generating garbage on each set or lookup.
static Keys[] _currentKeys = new Keys[0];
static Dictionary<int, Keys[]> _arrayCache = new Dictionary<int, Keys[]>();
public static KeyboardState GetState ()
{
return new KeyboardState(_currentKeys);
}
public static KeyboardState GetState (PlayerIndex playerIndex)
{
return new KeyboardState(_currentKeys);
}
internal static void SetKeys (List<Keys> keys)
{
if (!_arrayCache.TryGetValue(keys.Count, out _currentKeys)) {
_currentKeys = new Keys[keys.Count];
_arrayCache.Add(keys.Count, _currentKeys);
}
keys.CopyTo(_currentKeys);
}
}
}
There’s actually more code here than in the actual implementation of Keyboard. I’m taking a little extra care to avoid racking up needless garbage, since we only have access to the KeyboardState constructor that takes a raw array rather than the list we’d rather use.
In your game code, everywhere that you would call Keyboard.GetState()
, you would instead call ControlKeyboard.GetState()
.
KeyboardUtil
using System.Collections.Generic;
using System.Windows.Forms;
using XKeys = Microsoft.Xna.Framework.Input.Keys;
namespace WinFormsGraphicsDevice
{
internal static class KeyboardUtil
{
private static Dictionary<Keys, XKeys> _map;
static KeyboardUtil ()
{
_map = new Dictionary<Keys, XKeys>() {
{ Keys.A, XKeys.A },
{ Keys.Add, XKeys.Add },
{ Keys.Alt, XKeys.LeftAlt },
// ...
{ Keys.Y, XKeys.Y },
{ Keys.Z, XKeys.Z },
{ Keys.Zoom, XKeys.Zoom },
};
}
public static XKeys ToXna (Keys key)
{
XKeys xkey;
if (_map.TryGetValue(key, out xkey))
return xkey;
else
return XKeys.None;
}
}
}
This is the utility code for converting one key enum to another, and it parallels a similar KeyboardUtil class that exists in MonoGame’s internals. Only a snippet of the code is presented, since it’s just a long tedious list of mappings. You can download the full code in the sample project at the end of this post.
GraphicsDeviceControl Changes
#region Input
private const int WM_KEYDOWN = 0x100;
private const int WM_KEYUP = 0x101;
private List<Microsoft.Xna.Framework.Input.Keys> _keys;
// We would like to just override ProcessKeyMessage, but our control would only intercept it
// if it had explicit focus. Focus is a messy issue, so instead we're going to let the parent
// form override ProcessKeyMessage instead, and pass it along to this method.
internal new void ProcessKeyMessage (ref Message m)
{
if (m.Msg == WM_KEYDOWN) {
XKeys xkey = KeyboardUtil.ToXna((Keys)m.WParam);
if (!_keys.Contains(xkey))
_keys.Add(xkey);
}
else if (m.Msg == WM_KEYUP) {
Microsoft.Xna.Framework.Input.Keys xnaKey = KeyboardUtil.ToXna((Keys)m.WParam);
if (_keys.Contains(xnaKey))
_keys.Remove(xnaKey);
}
}
#endregion
This code can be added anywhere in the GraphicsDeviceControl class. It maintains per-control keyboard state information.
ControlKeyboard.SetKeys(_keys);
Add this to the start of your OnPaint method. Although OnPaint is not the most conceptually-ideal place to put this, it will ensure that your Draw overrides see consistent key state. If you’re working with a GameControl instead, you can try putting it in the Update section instead.
Wiring Everything Up
Because WinForms focus creates a big mess, there’s no simple way to just override keyboard events in your control and make everything work. There are lots of ways you could deal with this:
- Override ProcessKeyMessage (properly) in the control anyway, and take responsibility for keeping your control in focus.
- P/Invoke GetKeyState()
- Process keyboard events at the form, and delegate to your child control(s).
There’s probably others. I’ve opted for the third choice. Since the form is top dog, you can capture keyboard messages and forward them to GraphicsDeviceControl instances before any other control gets a chance to grab them. In this case, I’m letting all keyboard events pass on as if nothing happened, but you could also choose to suppress the events here if you don’t want the rest of your form interfering with your game input. You’d want to set a flag or something to toggle that behavior.
// For each GraphicsDeviceControl instance that you want to respond to input, call its
// public-facing ProcessKeyMessage method.
protected override bool ProcessKeyPreview (ref Message m)
{
spinningTriangleControl.ProcessKeyMessage(ref m);
return base.ProcessKeyPreview(ref m);
}
Put that code in the form containing your GraphicsDeviceControl(s). Since each control sets the global keyboard state from its own internal state immediately before using it, you can control which controls respond to input based on whether you pass them key messages or not.
A full sample application is available below. The spinning triangle control has been modified to dim the triangle if you hold down the space bar. Only the left control is hooked up to respond to user input.
33.92 kB - Downloaded 2634 times
ac8lon
u133sc
4di27q
4986ll
kr6b30
sl4dq9