开发者

C# arrow key input for a console app

I have a simple console app written in C#. I want to be able to detect arrow key presses, so I can allow the user to steer. How do I detect keydown/keyup events with a console app?

All my googling has led to info about windows Forms. I don't have a GUI. This is a console app (to control a robot over a serial port).

I have functions written to handle these events, but I have no idea how to register to actually receive the events:

  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
  }

This is probably a really basic question, but I'm fairly new to C#, and I've never needed to get this kind of input before.

Update: Many are suggesting I use System.Console.ReadKey(true).Key. This will not help. I need to know the moment a key is held down, when it is released, with support for multiple keys to be held down simultaneously. Also, ReadKey is a blocking call -- which means that the program will stop and wait for a key to be pressed.

Update: It seems that the only viable way to do this is to use Windows Forms. This is annoying, as I cannot use it on a headless system. Requiring a Form GUI to receive keyboard input is ... stupid.

But anyway, for posterity, here's my solution. I created a new Form project in my .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)
        {
            ...
        }
    }

    v开发者_JAVA百科oid 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
    }

Note that if you hold down a key, KeyDown will be called numerous times, and KeyUp will only be called once (when you release it). So you need to handle repeated KeyDown calls gracefully.


A bit late now, but here's how to access keyboard state in a console application.

Note that it's not all managed code as it requires GetKeyState to be imported from User32.dll.

/// <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);
}


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

or another example, just for your case:

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;
   }
}


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

You could put that into a spin, something like:

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


I have the same issue that you and I found, here, an interesting post using tasks. The original post can be found here: C# Console Application - How do I always read input from the console?

I have to emulate a PWM output through a Raspberry GPIO (using mono C#) to test a LCD backlight. With two simple keys I wanted to change the duty cycle (up/down) and an extra key to stop the program.

I tried this (variables):

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

Main program:

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;
        }
    }
}

When it's needed to increment the duty cycle, pressing 'W' key makes that the main task changes the duty cycle variable (duty); the same thing with 'S' key to decrement. The program finishes when 'X' key is pressed.


I am 10 years too late, but I wanted to share this with others that might read this thread in the future.

There is a simpler way to achieve what Ergwun said.

Instead of creating the the Enum and Class you can achieve the result in one line of code.

[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;

You need to import the GetKeyState or in my case GetAsyncKeyState method from User32.dll.

Then you just need to go through a loop and check if any of the keys were pressed. These strange numbers in the string are the Virtual-Key Codes for the left, up, right and down arrow keys respectively.

The boolean result is stored inside an array that can be looped through later to see if some key was pressed. You just need to define the order of the keys in the array for your program


Let's take a look at .NET System.Console class and replicate what it does internally, see it's source code:

You need to create a thread that constantly pools Key events from ReadConsoleInput Win32 Api from Kernel32.dll. The api also returns mouse/focus events, so not all events will be of interest for your use case. The api blocks the current thread until a new event arrives. You must read ReadConsoleInput reference docs to improve the following code.

This apps doesn't work within the new Windows Terminal, because WT only sends KeyUp events (inaccurate oversimplification, sorry! It sends VT sequences as AFAIK they dont handle key down/up separtedly). So use it in a regular Console Host window.

The following is just a working sample. Needs work.

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
    }
}

It detects multiple keydowns simultaneously.

C# arrow key input for a console app


you can do this

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
}
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜