Respond to multiple KeyDown events

2020-04-11 07:40发布

问题:

I'm making a simple WinForm car race game. I've got two objects - cars, and they move on the form when key is pressed (Form1KeyDown_Event).

The only thing is, that when one player press a key, the other player cannot press his key (nothing happens). But when the first player releases the key, second player can press one his keys and normally control his car.

How can I listen for two player keys simultaneously? Should I use threads and have each car on its own thread?

回答1:

Here's a simple example of what you can do in order to listen to several keys at the same time, using the keyup and keydown events instead.

using System;
using System.Collections.Generic;
using System.Windows.Forms;

namespace WinFormTest {
    public partial class Form1 : Form {
        private readonly IDictionary<Keys, bool> downState;

        public Form1() {
            InitializeComponent();
            downState = new Dictionary<Keys, bool>();
            downState.Add(Keys.W, false);
            downState.Add(Keys.D, false);

            KeyDown += remember;
            KeyUp += forget;
        }

        protected override void OnLoad(EventArgs e) {
            base.OnLoad(e);
            Timer timer = new Timer() { Interval = 100 };
            timer.Tick += updateGUI;
            timer.Start();
        }

        private void remember(object sender, KeyEventArgs e) {
            downState[e.KeyCode] = true;
        }

        private void forget(object sender, KeyEventArgs e) {
            downState[e.KeyCode] = false;
        }

        private void updateGUI(object sender, EventArgs e) {
            label1.Text = downState[Keys.W] ? "Forward" : "-";
            label2.Text = downState[Keys.D] ? "Right" : "-";
        }
    }
}


回答2:

You might want to investigate going lower-level and using windows hooks to detect keyboard events. This requires P/Invoking into native methods, but is pretty straight-forward. The hook you'd want is WH_LL_KEYBOARD. Details can be found at pinvoke.net.

You'd need a bit of boilerplate, but it's as close to the keyboard events as you can reasonably expect to get:

[StructLayout(LayoutKind.Sequential)]
public struct KBDLLHOOKSTRUCT
{
    public uint vkCode;
    public uint scanCode;
    public uint flags;
    public uint time;
    public IntPtr dwExtraInfo;
}

public delegate IntPtr LowLevelKeyboardProc(int, IntPtr, KBDLLHOOKSTRUCT);

[DllImport("kernel32.dll")]
public static extern uint GetCurrentThreadId();

[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr GetModuleHandle(string lpModuleName);

[DllImport("user32.dll", SetLastError = true)]
public static extern bool UnhookWindowsHookEx(IntPtr hhk);

[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr SetWindowsHookEx(int idhook, LowLevelKeyboardProc proc, IntPtr hMod, uint threadId);

[DllImport("user32.dll")]
static extern int CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, ref KBDLLHOOKSTRUCT lParam);

public static IntPtr SetHook(LowLevelKeyboardProc proc)
{
    using (var curProc = Process.GetCurrentProcess())
    using (var curMod = curProc.MainModule)
    {
        return SetWindowsHookEx(WH_KEYBOARD_LL, proc, GetModuleHandle(curMod.ModuleName), 0u);
    }
}

public IntPtr MyKeyboardHook(int code, IntPtr wParam, ref KBDLLHOOKSTRUCT keyboardInfo)
{
    if (code < 0)
    {
        return CallNextHookEx(IntPtr.Zero, wParam, ref keyboardInfo);
    }

    // Do your thing with the keyboard info.

    return CallNextHookEx(IntPtr.Zero, code, wParam, ref keyboardInfo);
}

Make sure to unhook your handler when your app stops needing it. The KBDLLHOOKSTRUCT encapsulates all the info Windows will give you about a keyboard event; details of its members can be found at MSDN.

One detail of this kind of hook is that it gets executed on the thread that registered it, so make sure you take note of that, and don't set it on the UI thread if it's going to do anything long-running.