C# стрелочный ввод для консольного приложения
У меня есть простое консольное приложение, написанное на C#. Я хочу иметь возможность обнаруживать нажатия клавиш со стрелками, чтобы позволить пользователю управлять. Как я могу обнаружить события keydown/keyup с помощью консольного приложения?
Все мои поиски привели к появлению информации о Windows Forms. У меня нет графического интерфейса. Это консольное приложение (для управления роботом через последовательный порт).
У меня есть функции, написанные для обработки этих событий, но я понятия не имею, как зарегистрироваться для фактического получения событий:
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 Forms. Это раздражает, так как я не могу использовать его в безголовой системе. Требование графического интерфейса пользователя для получения ввода с клавиатуры... глупо.
Но в любом случае, для потомков, вот мое решение. Я создал новый проект Form в моем.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.
8 ответов
Немного поздно, но вот как получить доступ к состоянию клавиатуры в консольном приложении.
Обратите внимание, что это не весь управляемый код, поскольку он требует, чтобы GetKeyState был импортирован из 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;
или другой пример, только для вашего случая:
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
Вы можете поместить это в спин, что-то вроде:
while(Running)
{
DoStuff();
System.Console.ReadKey(true).Key == ConsoleKey.UpArrow
Thread.Sleep(1)
}
Давайте посмотрим на
.NET
System.Console
class и воспроизведите то, что он делает внутри, посмотрите его исходный код:
Вам нужно создать поток, который постоянно объединяет
Key events
из
ReadConsoleInput
Win32 API от
Kernel32.dll
. 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://docs.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
}
}
У меня та же проблема, что и вы, и я нашли здесь интересный пост с использованием заданий. Оригинальный пост можно найти здесь: Консольное приложение C# - Как мне всегда читать ввод с консоли?
Я должен эмулировать выход ШИМ через Raspberry GPIO (используя моно C#), чтобы проверить подсветку ЖК-дисплея. С двумя простыми клавишами я хотел изменить рабочий цикл (вверх / вниз) и дополнительную клавишу для остановки программы.
Я попробовал это (переменные):
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" приводит к тому, что основная задача меняет переменную коэффициента заполнения (долг); то же самое с ключом "S" для уменьшения. Программа завершается при нажатии клавиши "X".
Пример кода:
ConsoleKeyInfo kb = Console.ReadKey();
if (kb.Key == ConsoleKey.LeftArrow)
Console.WriteLine("Left Arrow pressed");
Я опоздал на 10 лет, но я хотел поделиться этим с другими, которые могли бы прочитать эту ветку в будущем.
Есть более простой способ добиться того, что сказал Эргвун.
Вместо создания 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;
Вам необходимо импортировать
GetKeyState
или в моем случае
GetAsyncKeyState
метод из User32.dll.
Затем вам просто нужно пройти цикл и проверить, была ли нажата какая-либо из клавиш. Эти странные числа в строке являются кодами виртуальных клавиш для клавиш со стрелками влево, вверх, вправо и вниз соответственно.
Логический результат сохраняется внутри массива, который можно просмотреть позже, чтобы увидеть, была ли нажата какая-либо клавиша. Вам просто нужно определить порядок ключей в массиве для вашей программы.
Вы можете сделать это
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
}