在.NET控制台应用程序中监听按键操作

266

如何在控制台应用程序中一直运行直到按下一个键 (例如按下Esc)?

我假设它包裹在一个while循环中。我不喜欢ReadKey,因为它会阻塞操作并要求按下一个键,而不是继续监听键按下事件。

有什么方法可以实现这个功能吗?

10个回答

397

使用Console.KeyAvailable方法,确保只有在不阻塞程序的情况下才调用ReadKey方法:

Console.WriteLine("Press ESC to stop");
do {
    while (! Console.KeyAvailable) {
        // Do something
   }       
} while (Console.ReadKey(true).Key != ConsoleKey.Escape);

7
但如果你使用这个而不是readLine(),你会失去按“上”键调用历史记录的强大功能。 - v.oddou
3
在最后一行后面,只需添加 Console.ReadLine() 即可,对吧? - Gaffi
8
这个循环不会消耗大量的CPU/RAM吗?如果不会,那是为什么呢? - Sofia

89
你可以稍微改变你的方法 - 使用 Console.ReadKey() 来停止你的应用程序,但是在后台线程中进行你的工作:
static void Main(string[] args)
{
    var myWorker = new MyWorker();
    myWorker.DoStuff();
    Console.WriteLine("Press any key to stop...");
    Console.ReadKey();
}

myWorker.DoStuff()函数中,您可以在后台线程上调用另一个函数(使用Action<>()Func<>()是一种简单的方法),然后立即返回。


77

最短的方式:

Console.WriteLine("Press ESC to stop");

while (!(Console.KeyAvailable && Console.ReadKey(true).Key == ConsoleKey.Escape))
{
    // do something
}

Console.ReadKey() 是一个阻塞函数,它会停止程序的执行并等待键盘输入,但是由于先检查 Console.KeyAvailable,所以 while 循环不会被阻塞,而是一直运行直到按下 Esc


24

来自视频课程《在C#中构建.NET控制台应用程序》的Jason Roberts,在http://www.pluralsight.com

为了拥有多个正在运行的进程,我们可以执行以下操作。

  static void Main(string[] args)
    {
        Console.CancelKeyPress += (sender, e) =>
        {

            Console.WriteLine("Exiting...");
            Environment.Exit(0);
        };

        Console.WriteLine("Press ESC to Exit");

        var taskKeys = new Task(ReadKeys);
        var taskProcessFiles = new Task(ProcessFiles);

        taskKeys.Start();
        taskProcessFiles.Start();

        var tasks = new[] { taskKeys };
        Task.WaitAll(tasks);
    }

    private static void ProcessFiles()
    {
        var files = Enumerable.Range(1, 100).Select(n => "File" + n + ".txt");

        var taskBusy = new Task(BusyIndicator);
        taskBusy.Start();

        foreach (var file in files)
        {
            Thread.Sleep(1000);
            Console.WriteLine("Procesing file {0}", file);
        }
    }

    private static void BusyIndicator()
    {
        var busy = new ConsoleBusyIndicator();
        busy.UpdateProgress();
    }

    private static void ReadKeys()
    {
        ConsoleKeyInfo key = new ConsoleKeyInfo();

        while (!Console.KeyAvailable && key.Key != ConsoleKey.Escape)
        {

            key = Console.ReadKey(true);

            switch (key.Key)
            {
                case ConsoleKey.UpArrow:
                    Console.WriteLine("UpArrow was pressed");
                    break;
                case ConsoleKey.DownArrow:
                    Console.WriteLine("DownArrow was pressed");
                    break;

                case ConsoleKey.RightArrow:
                    Console.WriteLine("RightArrow was pressed");
                    break;

                case ConsoleKey.LeftArrow:
                    Console.WriteLine("LeftArrow was pressed");
                    break;

                case ConsoleKey.Escape:
                    break;

                default:
                    if (Console.CapsLock && Console.NumberLock)
                    {
                        Console.WriteLine(key.KeyChar);
                    }
                    break;
            }
        }
    }
}

internal class ConsoleBusyIndicator
{
    int _currentBusySymbol;

    public char[] BusySymbols { get; set; }

    public ConsoleBusyIndicator()
    {
        BusySymbols = new[] { '|', '/', '-', '\\' };
    }
    public void UpdateProgress()
    {
        while (true)
        {
            Thread.Sleep(100);
            var originalX = Console.CursorLeft;
            var originalY = Console.CursorTop;

            Console.Write(BusySymbols[_currentBusySymbol]);

            _currentBusySymbol++;

            if (_currentBusySymbol == BusySymbols.Length)
            {
                _currentBusySymbol = 0;
            }

            Console.SetCursorPosition(originalX, originalY);
        }
    }

2
你能解释一下你的方法吗?我正在尝试在我的控制台应用程序中实现相同的功能。如果能对代码进行解释就更好了。 - nayef harb
@nayefharb 设置了一堆任务,启动它们,然后 WaitAll 会等待它们全部返回。因此,单个任务不会返回,直到触发 CancelKeyPress,它将一直持续下去。 - wonea
static void Main(string[] args) 块的第一行添加 Console.CursorVisible = false; 可以避免光标闪烁的问题。 - Hitesh
var tasks = new[] { taskKeys }; should probably be var tasks = new[] { taskKeys, taskProcessFiles }; - Rev

14

这里提供一种方法,让你在另一个线程上执行某些操作并开始监听不同线程中按下的键。当你的实际进程结束或用户通过按下 Esc 键终止进程时,控制台将停止处理。

class SplitAnalyser
{
    public static bool stopProcessor = false;
    public static bool Terminate = false;

    static void Main(string[] args)
    {
        Console.ForegroundColor = ConsoleColor.Green;
        Console.WriteLine("Split Analyser starts");
        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine("Press Esc to quit.....");
        Thread MainThread = new Thread(new ThreadStart(startProcess));
        Thread ConsoleKeyListener = new Thread(new ThreadStart(ListerKeyBoardEvent));
        MainThread.Name = "Processor";
        ConsoleKeyListener.Name = "KeyListener";
        MainThread.Start();
        ConsoleKeyListener.Start();

        while (true) 
        {
            if (Terminate)
            {
                Console.WriteLine("Terminating Process...");
                MainThread.Abort();
                ConsoleKeyListener.Abort();
                Thread.Sleep(2000);
                Thread.CurrentThread.Abort();
                return;
            }

            if (stopProcessor)
            {
                Console.WriteLine("Ending Process...");
                MainThread.Abort();
                ConsoleKeyListener.Abort();
                Thread.Sleep(2000);
                Thread.CurrentThread.Abort();
                return;
            }
        } 
    }

    public static void ListerKeyBoardEvent()
    {
        do
        {
            if (Console.ReadKey(true).Key == ConsoleKey.Escape)
            {
                Terminate = true;
            }
        } while (true); 
    }

    public static void startProcess()
    {
        int i = 0;
        while (true)
        {
            if (!stopProcessor && !Terminate)
            {
                Console.ForegroundColor = ConsoleColor.White;
                Console.WriteLine("Processing...." + i++);
                Thread.Sleep(3000);
            }
            if(i==10)
                stopProcessor = true;

        }
    }

}

6

针对其他答案无法很好处理的情况:

  • 响应式:显式/直接执行按键处理代码;避免了轮询或阻塞延迟的不确定性
  • 可选性:全局按键是“选择加入”的;默认情况下,如果没有操作,应用程序会正常退出
  • 关注点分离:监听代码更少侵入性;独立于控制台应用程序的核心逻辑运行。

这个页面上的许多解决方案都涉及轮询Console.KeyAvailable或阻塞Console.ReadKey。虽然在.NET中,Console在这里并不太合作,但您可以使用Task.Run来向更现代的Async模式转移。

要注意的主要问题是,默认情况下,您的控制台线程未设置为Async操作,这意味着当您从main函数底部跌落时,您的AppDoman和进程将结束,而不是等待Async完成。解决此问题的正确方法是使用Stephen Cleary的AsyncContext在单线程控制台程序中建立完整的Async支持。但对于更简单的情况,例如等待按键,安装完整的跳板可能过度。

下面的示例是用于某种迭代批处理文件中的控制台程序。在这种情况下,当程序完成其工作时,通常应该退出而不需要要求按键,然后我们允许一个可选的按键防止应用程序退出。我们可以暂停循环以检查事情,可能恢复,或者使用暂停作为已知的“控制点”,以清晰地从批处理文件中退出。

static void Main(String[] args)
{
    Console.WriteLine("Press any key to prevent exit...");
    var tHold = Task.Run(() => Console.ReadKey(true));

    // ... do your console app activity ...

    if (tHold.IsCompleted)
    {
#if false   // For the 'hold' state, you can simply halt forever...
        Console.WriteLine("Holding.");
        Thread.Sleep(Timeout.Infinite);
#else                       // ...or allow continuing on (to exit)
        while (Console.KeyAvailable)
            Console.ReadKey(true);     // flush/consume any extras
        Console.WriteLine("Holding. Press 'Esc' to exit.");
        while (Console.ReadKey(true).Key != ConsoleKey.Escape)
            ;
#endif
    }
}

5
如果您在使用Visual Studio,则可以在“调试”菜单中使用“开始而不调试”。
它会在应用程序完成后自动向控制台写入“按任意键继续......”,并保持控制台打开状态,直到按下键为止。

3
使用以下代码,您可以在控制台执行过程中监听空格键,并暂停,直到按下其他键,另外还可以选择监听Esc键以打破主循环。
static ConsoleKeyInfo cki = new ConsoleKeyInfo();

while(true) {
      if (WaitOrBreak()) break;
      //your main code
}

private static bool WaitOrBreak(){
    if (Console.KeyAvailable) cki = Console.ReadKey(true);
    if (cki.Key == ConsoleKey.Spacebar)
    {
        Console.Write("waiting..");
        while (Console.KeyAvailable == false)
        {
            Thread.Sleep(250);Console.Write(".");
        }
        Console.WriteLine();
        Console.ReadKey(true);
        cki = new ConsoleKeyInfo();
    }
    if (cki.Key == ConsoleKey.Escape) return true;
    return false;
}

0

根据我的经验,在控制台应用程序中读取最后按下的键的最简单方法如下(以箭头键为例):

ConsoleKey readKey = Console.ReadKey ().Key;
if (readKey == ConsoleKey.LeftArrow) {
    <Method1> ();  //Do something
} else if (readKey == ConsoleKey.RightArrow) {
    <Method2> ();  //Do something
}

我通常避免使用循环,而是将上述代码编写在一个方法中,并在“Method1”和“Method2”的末尾调用它,这样,在执行完“Method1”或“Method2”后,Console.ReadKey().Key就可以再次读取按键。

-1
Console.WriteLine("Hello");
var key = Console.ReadKey(); 
DateTime start = DateTime.Now;
bool gotKey = Console.KeyAvailable;

while ((DateTime.Now - start).TotalSeconds < 2)                
{
    if (key.Key == ConsoleKey.Escape)
    {
       Environment.Exit(0);
    } 
    else if (key.Key == ConsoleKey.Enter)
    {
       break;
    } 

1
目前你的回答不够清晰。请编辑并添加更多细节,以帮助其他人理解它如何回答所提出的问题。你可以在帮助中心找到有关如何撰写好答案的更多信息。 - Community

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