C#控制台应用程序中的箭头键输入

17

我有一个用C#编写的简单控制台应用程序。我希望能够检测箭头键按下事件,以便用户可以进行控制。如何在控制台应用程序中检测keydown/keyup事件?

我通过搜索得到的所有信息都与Windows Forms有关。但是我没有图形用户界面(GUI)。这是一个控制机器人通过串口的控制台应用程序。

我已经编写了处理这些事件的函数,但不知道如何注册以实际接收这些事件:

  private void myKeyDown(object sender, KeyEventArgs e)
  {
      switch (e.KeyCode)
      {
          case Keys.Left:
                 ...
          case Keys.Right:
                 ...
          case Keys.Up:
                 ...
      }
  }

  private void myKeyUp(object sender, KeyEventArgs e)
  {
      ... pretty much the same as myKeyDown
  }

这可能是一个非常基础的问题,但我对C#相当新,并且以前从未需要获取这种类型的输入。

更新: 许多人建议我使用System.Console.ReadKey(true).Key。这不会有所帮助。我需要知道按键按下的时刻,释放的时刻,支持同时按下多个键。而且,ReadKey是一个阻塞调用--这意味着程序将停止并等待按键被按下。

更新: 看起来唯一可行的方法是使用Windows窗体。这很烦人,因为我无法在无头系统上使用它。要求使用表单GUI接收键盘输入是...愚蠢的。

但是,为了记录下来,这是我的解决方案。我在我的.sln中创建了一个新的窗体项目:

    private void Form1_Load(object sender, EventArgs e)
    {
        try
        {
            this.KeyDown += new KeyEventHandler(Form1_KeyDown);
            this.KeyUp += new KeyEventHandler(Form1_KeyUp);
        }
        catch (Exception exc)
        {
            ...
        }
    }

    void Form1_KeyDown(object sender, KeyEventArgs e)
    {
        switch (e.KeyCode)
        {
            // handle up/down/left/right
            case Keys.Up:
            case Keys.Left:
            case Keys.Right:
            case Keys.Down:
            default: return;  // ignore other keys
        }
    }

    private void Form1_KeyUp(object sender, KeyEventArgs e)
    {
        // undo what was done by KeyDown
    }

请注意,如果您按住一个键,KeyDown将被多次调用,而KeyUp只会在释放键时调用一次。因此,您需要优雅地处理重复的KeyDown调用。


你可以在不同的线程中调用ReadKey,可以参考我的旋转线程建议。但这并不能解决keyup/keydown的问题。 - Rushyo
7个回答

17

现在可能有点晚了,但是这是如何在控制台应用程序中访问键盘状态的方法。

请注意,它并不完全是托管代码,因为它要求从User32.dll导入GetKeyState

/// <summary>
/// Codes representing keyboard keys.
/// </summary>
/// <remarks>
/// Key code documentation:
/// http://msdn.microsoft.com/en-us/library/dd375731%28v=VS.85%29.aspx
/// </remarks>
internal enum KeyCode : int
{
    /// <summary>
    /// The left arrow key.
    /// </summary>
    Left = 0x25,

    /// <summary>
    /// The up arrow key.
    /// </summary>
    Up,

    /// <summary>
    /// The right arrow key.
    /// </summary>
    Right,

    /// <summary>
    /// The down arrow key.
    /// </summary>
    Down
}

/// <summary>
/// Provides keyboard access.
/// </summary>
internal static class NativeKeyboard
{
    /// <summary>
    /// A positional bit flag indicating the part of a key state denoting
    /// key pressed.
    /// </summary>
    private const int KeyPressed = 0x8000;

    /// <summary>
    /// Returns a value indicating if a given key is pressed.
    /// </summary>
    /// <param name="key">The key to check.</param>
    /// <returns>
    /// <c>true</c> if the key is pressed, otherwise <c>false</c>.
    /// </returns>
    public static bool IsKeyDown(KeyCode key)
    {
        return (GetKeyState((int)key) & KeyPressed) != 0;
    }

    /// <summary>
    /// Gets the key state of a key.
    /// </summary>
    /// <param name="key">Virtuak-key code for key.</param>
    /// <returns>The state of the key.</returns>
    [System.Runtime.InteropServices.DllImport("user32.dll")]
    private static extern short GetKeyState(int key);
}

有趣。这将需要自己轮询键盘,这意味着我不能只拥有基于事件的应用程序。但这很容易做到。 - Tim
2
是的,控制台应用程序通常没有消息循环,但是没有什么可以阻止您自己编写消息循环,然后使应用程序的其余部分成为事件驱动的。 - Ergwun

9
var isUp = Console.ReadKey().Key == ConsoleKey.UpArrow;

或者举个例子,针对您的情况:

或者另一个例子,针对您的情况:

while (true)
{
   var ch = Console.ReadKey(false).Key;
   switch(ch)
   {
       case ConsoleKey.Escape:
          ShutdownRobot();
          return;
       case ConsoleKey.UpArrow:
          MoveRobotUp();
          break;
       case ConsoleKey.DownArrow:
          MoveRobotDown();
          break;
   }
}

1
System.Console.ReadKey(true).Key == ConsoleKey.UpArrow

你可以将它放入旋转中,例如:

while(Running)
{
  DoStuff();
  System.Console.ReadKey(true).Key == ConsoleKey.UpArrow
  Thread.Sleep(1)
}

1
按住一个键或按住多个键怎么办?我不仅想获取最新按下的键 - 我想处理按住多个键(或根本没有按键)。 - Tim
然后,您需要直接处理键盘消息。在Windows上执行此操作的最简单方法是使用DirectInput。 - Rushyo
如果必须手动完成,相关的Win32函数是GetConsoleWindow和SetWindowsHookEx。挂钩控制台的“WH_CALLWNDPROC”事件以捕获原始的Windows消息。控制台没有像Windows窗体那样的抽象层,但它可能具有您正在寻找的功能,因为没有人建议它,这意味着这是您下一个逻辑步骤。 - Rushyo
1
如果您打算创建一个高度交互式的应用程序,最好在Windows Form中创建一个仿控制台。这是创建基本视频游戏UI的流行技术。 - Rushyo
请记住,在键盘上的每个按键和 shift、ctrl、alt 等修饰键之间,您可以使用数百个键组合。这就是为什么想要更多功能不是标准的功能。 - Rushyo
处理多个箭头键。忽略那个 ;) - Rushyo

0

让我们来看一下 .NETSystem.Console 类并复制它内部的操作,查看它的 源代码

您需要创建一个线程,不断从 Kernel32.dll 中的 ReadConsoleInput Win32 Api 池化 Key events。该 api 还返回鼠标/焦点事件,因此并非所有事件都与您的用例相关。该 api 阻塞当前线程,直到出现新事件。您必须阅读 ReadConsoleInput 参考文档 以改进以下代码。

此应用程序无法在新的 Windows Terminal 中运行,因为 WT 仅发送 KeyUp 事件(不准确的过度简化,抱歉!它发送 VT 序列,因为据我所知,它们不会分别处理按键上/下)。因此,请在常规控制台主机窗口中使用它。

以下只是一个工作示例。还需改进。

using System.Runtime.InteropServices;
using static Win32Native;

class Program
{
    public static void Main()
    {
        Console.WriteLine("Hello");
        while (true)
        {
            var key = KeyHandler.ReadKey();
            Console.WriteLine($"Event:{key.KeyPressType} Key:{key.Key.Key}");
        }
    }
}

enum KeyPressType { KeyUp, KeyDown, Unknown }
class SimpleKeyRecord { public KeyPressType KeyPressType; public ConsoleKeyInfo Key; }
class KeyHandler
{
    static IntPtr _consoleInputHandle = Win32Native.GetStdHandle(Win32Native.STD_INPUT_HANDLE);
    
    public static SimpleKeyRecord ReadKey()
    {
        var result = new SimpleKeyRecord();
        Win32Native.InputRecord ir;
        int numEventsRead;

        if (!Win32Native.ReadConsoleInput(_consoleInputHandle, out ir, 1, out numEventsRead) || numEventsRead == 0)
        {
            throw new InvalidOperationException();
        }

        if (ir.eventType != 1)  // see https://learn.microsoft.com/en-us/windows/console/input-record-str
        {
            result.KeyPressType = KeyPressType.Unknown; // Focus/Mouse/Menu event.
            return result;
        }

        result.KeyPressType = ir.keyEvent.keyDown ? KeyPressType.KeyDown : KeyPressType.KeyUp;

        ControlKeyState state = (ControlKeyState)ir.keyEvent.controlKeyState;
        bool shift = (state & ControlKeyState.ShiftPressed) != 0;
        bool alt = (state & (ControlKeyState.LeftAltPressed | ControlKeyState.RightAltPressed)) != 0;
        bool control = (state & (ControlKeyState.LeftCtrlPressed | ControlKeyState.RightCtrlPressed)) != 0;

        result.Key = new ConsoleKeyInfo((char)ir.keyEvent.uChar, (ConsoleKey)ir.keyEvent.virtualKeyCode, shift, alt, control);

        short virtualKeyCode = ir.keyEvent.virtualKeyCode;

        return result;
    }
}

class Win32Native
{
    public const int STD_INPUT_HANDLE = -10;

    [DllImport("kernel32.dll", CharSet = System.Runtime.InteropServices.CharSet.Ansi, SetLastError = true)]
    public static extern IntPtr GetStdHandle(int whichHandle);
    
    [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    internal static extern bool ReadConsoleInput(IntPtr hConsoleInput, out InputRecord buffer, int numInputRecords_UseOne, out int numEventsRead);

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
    internal struct InputRecord
    {
        internal short eventType;
        internal KeyEventRecord keyEvent;
    }

    // Win32's KEY_EVENT_RECORD
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
    internal struct KeyEventRecord
    {
        internal bool keyDown;
        internal short repeatCount;
        internal short virtualKeyCode;
        internal short virtualScanCode;
        internal char uChar; // Union between WCHAR and ASCII char
        internal int controlKeyState;
    }

    [Flags]
    internal enum ControlKeyState
    {
        RightAltPressed = 0x0001,
        LeftAltPressed = 0x0002,
        RightCtrlPressed = 0x0004,
        LeftCtrlPressed = 0x0008,
        ShiftPressed = 0x0010,
        NumLockOn = 0x0020,
        ScrollLockOn = 0x0040,
        CapsLockOn = 0x0080,
        EnhancedKey = 0x0100
    }
}

它可以同时检测多个按键的按下。 工作示例


WT实际上支持此功能,但您需要启用win32-input-mode - Gerardo Grignoli

0

我晚了10年,但我想与其他可能在未来阅读此线程的人分享这个。

有一种更简单的方法可以实现Ergwun所说的。

不必创建Enum和Class,您可以在一行代码中实现结果。

[System.Runtime.InteropServices.DllImport("user32.dll")]
static extern short GetAsyncKeyState(int key); 
   
. . .

bool[] keys = new bool[4];

for (int i = 0; i < 4; i++)                            
    keys[i] = (0x8000 & GetAsyncKeyState((char)"\x25\x26\x27\x28"[i])) != 0;

你需要从User32.dll中导入GetKeyState或者像我一样的GetAsyncKeyState方法。
然后,你只需要通过一个循环来检查是否有任何键被按下。 这个字符串中的奇怪数字是左、上、右和下箭头键的虚拟键代码
布尔结果存储在一个数组中,稍后可以循环遍历以查看是否按下了某个键。你只需要为程序定义键的顺序即可。

0

我遇到了和你一样的问题,我在这里找到了一个使用任务的有趣帖子。 原始帖子可以在这里找到: C#控制台应用程序-如何始终从控制台读取输入?

我需要通过Raspberry GPIO(使用mono C#)模拟PWM输出以测试LCD背光。 我想用两个简单的键来改变占空比(上/下),并使用额外的一个键停止程序。

我尝试了这个(变量):

static ConsoleKeyInfo key = new ConsoleKeyInfo();
static int counter = 0;
static int duty = 5; // Starts in 50%

主程序:
static void Main(string[] args)
{
// cancellation by keyboard string
    CancellationTokenSource cts = new CancellationTokenSource();
    // thread that listens for keyboard input
    var kbTask = Task.Run(() =>
    {
        while (true)
        {
            key = Console.ReadKey(true);
            if (key.KeyChar == 'x' || key.KeyChar == 'X')
            {
                cts.Cancel();
                break;
            }
            else if (key.KeyChar == 'W' || key.KeyChar == 'w')
            {
                if (duty < 10)
                    duty++;
                //Console.WriteLine("\tIncrementa Ciclo");
                //mainAppState = StateMachineMainApp.State.TIMER;
                //break;
            }
            else if (key.KeyChar == 'S' || key.KeyChar == 's')
            {
                if (duty > 0)
                    duty--;
                //Console.WriteLine("\tDecrementa Ciclo");
                //mainAppState = StateMachineMainApp.State.TIMER;
                // break;
            }
        }
    });

    // thread that performs main work
    Task.Run(() => DoWork(), cts.Token);

    string OsVersion = Environment.OSVersion.ToString();
    Console.WriteLine("Sistema operativo: {0}", OsVersion);
    Console.WriteLine("Menú de Progama:");
    Console.WriteLine(" W. Aumentar ciclo útil");
    Console.WriteLine(" S. Disminuir ciclo útil");
    Console.WriteLine(" X. Salir del programa");

    Console.WriteLine();
    // keep Console running until cancellation token is invoked
    kbTask.Wait();
}

static void DoWork()
{
    while (true)
    {
        Thread.Sleep(50);
        if (counter < 10)
        {
            if (counter < duty)
                Console.Write("─");
                //Console.WriteLine(counter + " - ON");
            else
                Console.Write("_");
                //Console.WriteLine(counter + " - OFF");
            counter++;
        }
        else
        {
            counter = 0;
        }
    }
}

当需要增加占空比时,按下“W”键会使主任务更改占空比变量(duty);按下“S”键则会减少。当按下“X”键时,程序结束。


-2

你可以做到这一点

bool keyWasPressed = false;
if (consolekey.avalable)
{
    keyvar = console.readkey(true);
    keyWasPressed = true;
}
if(keyWasPressed)
{
    // enter you code here using keyvar
}
else
{
    // the commands that happen if you don't press anything
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接