【问题标题】:Why is .NET Core handling ReadKey differently on Raspbian?为什么 .NET Core 在 Raspbian 上处理 ReadKey 的方式不同?
【发布时间】:2018-03-21 04:45:45
【问题描述】:

我正在编写一个 .NET Core 控制台应用程序。我想将控制台输入限制为每个输入的一定数量的最大字符。我有一些代码通过使用Console.ReadKey() 而不是Console.ReadLine() 构建一个字符串来做到这一点,一切都可以在Windows 上完美地测试它。然后,当我部署到运行 Raspbian 的 Raspberry Pi 3 时,我很快遇到了各种各样的问题。我记得 Linux 处理行尾的方式与 Windows 不同,而且退格的处理方式似乎也不同。我改变了处理这些的方式,关闭了 ConsoleKey 而不是字符,换行问题消失了,但退格键有时只会注册。此外,有时字符会在我的输入框之外输出到控制台,即使我将 ReadKey 设置为不自行输出到控制台。我是否遗漏了有关 Linux 如何处理控制台输入的内容?

//I replaced my calls to Console.ReadLine() with this. The limit is the
//max number of characters that can be entered in the console.
public static string ReadChars(int limit)
{
    string str = string.Empty; //all the input so far
    int left = Console.CursorLeft; //store cursor position for re-outputting
    int top = Console.CursorTop;
    while (true) //keep checking for key events
    {
        if (Console.KeyAvailable)
        {
            //true to intercept input and not output to console
            //normally. This sometimes fails and outputs anyway.
            ConsoleKeyInfo c = Console.ReadKey(true);
            if (c.Key == ConsoleKey.Enter) //stop input on Enter key
                break;
            if (c.Key == ConsoleKey.Backspace) //remove last char on Backspace
            {
                if (str != "")
                {
                     tr = str.Substring(0, str.Length - 1);
                }
            }
            else if (c.Key != ConsoleKey.Tab && str.Length < limit)
            {
                //don't allow tabs or exceeding the max size
                str += c.KeyChar;
            }
            else
            {
                //ignore tabs and when the limit is exceeded
                continue;
            }
            Console.SetCursorPosition(left, top);
            string padding = ""; //padding clears unused chars in field
            for (int i = 0; i < limit - str.Length; i++)
            {
                padding += " ";
            }
            //output this way instead
            Console.Write(str + padding);
        }
    }
    return str;
}

【问题讨论】:

  • 处理击键的是终端。您将不会收到任何未发送到您的应用程序的击键。换行与此无关。 .NET(和 Core)将使用操作系统的设置。此外,它已经将\n 识别为 Windows 中的换行符
  • Windows 可以识别回车符 \r,而 Linux 只使用换行符 \n。我最初是在检查\r,这当然会导致问题。我试图弄清楚是否还有其他类似我没有考虑的差异。我认为这暗示这与终端处理击键的方式不同。我的观点只是调用 ReadKey 在不同的机器上会给出不同的结果,即使我做出完全相同的击键,毫无疑问是因为不同的系统如何处理这些击键。
  • 不太确定这有什么意义。在 Linux 上,您仍然按 Enter 键,而不是按 Ctrl+J 来获取 \n。 ReadKey 告诉您按下的键,而不是它产生的字符。因此,只要您使用 Key 而不是 KeyChar 就不会有问题。也许你暴露了一个兼容性问题,这都是相当新的,所以它不是不可想象的。它们支持大约十种不同的 Linux 风格,Raspian 不是其中之一。最好告诉他们,使用New Issue button
  • @tyjkenn Linux 使用\n。 Windows 使用\r\n。只有经典 Mac OS使用 来使用 \r。但是,这些都与 keystrokes 无关,只有文件、字符串和流。您是否检查了Key 而不是 KeyChar? Key 表示 Shift、Alt、Enter、Down Arrow、Page Down、A、B、C 等。 KeyChar 是如何将其转换为字符的方式。许多键没有等效字符
  • 别介意换行的事情。我通过使用 Key 而不是 KeyChar 解决了这个问题,因此我的代码。也许我应该把那部分排除在外,但我认为它会更好地解释我的代码。更改没有修复退格,而才是真正的问题。那以及当我告诉它不要时它正在输出到控制台的事实。我担心这是兼容性问题,但是有解决方法吗? ReadLine 工作正常,除了我不能限制允许的字符数。

标签: c# raspberry-pi .net-core console-application


【解决方案1】:

我测试并发现Console.ReadKey(true) 确实存在一些错误,当快速键入或触发重复键时,键实际上会回显到控制台。这是你意想不到的事情,但我不知道为什么会发生。

如果你有兴趣调试它,你可以看看下面的源代码

https://referencesource.microsoft.com/#mscorlib/system/console.cs,1476

我选择了解决该问题的方法。因此,您的方法几乎没有问题。 Left ArrowRight Arrow 键应该被处理或者它们不应该被允许。我通过添加以下代码选择了后者

if (c.Key == ConsoleKey.LeftArrow || c.Key == ConsoleKey.RightArrow) {
   continue;
}

当您使用下面输入字符时

Console.Write(str + padding);

你基本上也扰乱了光标位置,这是不正确的。因此,您需要在此之后使用以下设置光标位置

Console.CursorLeft = str.Length;

现在是处理泄漏密钥的部分,这可能是一个 .NET 错误,我在下面添加了代码

else
{
    //ignore tabs and when the ilimit is exceeded
    if (Console.CursorLeft > str.Length){

        var delta = Console.CursorLeft - str.Length;
        Console.CursorLeft = str.Length;
        Console.Write(new String(' ',delta));
        Console.CursorLeft = str.Length;
    }
    continue;
}

因此,我们检查是否有任何不可见的原因回显了某些内容,然后将其删除。然后进行压力测试

$ docker run -it console
Please enter some text:
tarun6686e
You entered: tarun6686e

下面是我使用的最终代码

using System;

namespace ConsoleTest
{
    public class Program {
        public static string tr="";
        //I replaced my calls to Console.ReadLine() with this. The limit is the
        //max number of characters that can be entered in the console.
        public static string ReadChars(int limit)
        {
            string str = string.Empty; //all the input so far
            int left = Console.CursorLeft; //store cursor position for re-outputting
            int top = Console.CursorTop;

            while (true) //keep checking for key events
            {
                if (Console.KeyAvailable)
                {
                    //true to intercept input and not output to console
                    //normally. This sometimes fails and outputs anyway.
                    ConsoleKeyInfo c = Console.ReadKey(true);
                    string name = Enum.GetName(typeof(ConsoleKey), c.Key);
                    var key = c.KeyChar;
                    // Console.WriteLine(String.Format("Name={0}, Key={1}, KeyAscii={2}", name, key,(int)key));
                    if (c.Key == ConsoleKey.Enter) //stop input on Enter key
                        {
                            Console.WriteLine();
                            break;
                        }
                    if (c.Key == ConsoleKey.LeftArrow || c.Key == ConsoleKey.RightArrow) {
                        continue;
                    }

                    if (c.Key == ConsoleKey.Backspace) //remove last char on Backspace
                    {
                        if (str != "")
                        {
                            str = str.Substring(0, str.Length - 1);
                        }
                    }
                    else if (c.Key != ConsoleKey.Tab && str.Length < limit)
                    {
                        //don't allow tabs or exceeding the max size
                        str += c.KeyChar;
                    }
                    else
                    {
                        //ignore tabs and when the ilimit is exceeded
                        if (Console.CursorLeft > str.Length){

                            var delta = Console.CursorLeft - str.Length;
                            Console.CursorLeft = str.Length;
                            Console.Write(new String(' ',delta));
                            Console.CursorLeft = str.Length;
                        }
                        continue;
                    }
                    Console.SetCursorPosition(left, top);
                    string padding = ""; //padding clears unused chars in field
                    for (int i = 0; i < limit - str.Length; i++)
                    {
                        padding += " ";
                    }
                    //output this way instead
                    Console.Write(str + padding);
                    Console.CursorLeft = str.Length;
                }
            }
            return str;
        }

        public static void Main(string[] args) {
            Console.WriteLine("Please enter some text: ");
            var text = ReadChars(10);

            Console.WriteLine("You entered: " + text);
        }
    }
}

【讨论】:

  • 链接错误,您需要 .NETCore 中特定于 Unix 的控制台风格。我认为是this one。看起来很无辜。也许太天真了:)
  • 感谢@HansPassant 的更正,我没有意识到需要查看 .NET 核心代码库
  • 在弄乱了这段代码之后,我设法让这种方法发挥作用。但是,在我的程序的另一部分中,我以另一种方式使用键盘输入(翻阅页面),在重新绘制整个控制台时,我无法捕捉到某些键。最终,我不得不禁用回声。
【解决方案2】:

我认为 Stephen Toub 在this GitHub issue 中的评论暴露了根本问题:

您可能会想到我们现在只在 ReadKey(intercept: true) 调用期间禁用回显,因此在用户键入和您调用 ReadKey(intercept: true) 之间的竞争中,键可能是回显' d 即使您希望它不会,但您不会丢失按键。

这是寒冷的舒适,但准确。这是一场很难取胜的比赛。核心问题是 Linux 终端的工作方式与 Windows 控制台非常不同。它的运作方式更像是 1970 年代的电传打字机。你敲打着键盘,不管电脑是否注意到你输入的内容,电传打字机只是回应了你输入的内容,把它敲在纸上。直到您按下 Enter 键,计算机才会开始处理文本。

与 Windows 控制台非常不同,它要求程序有一个活动的读取调用来回显任何键入的文本。

所以这是与控制台 api 的根本不匹配。它需要一个Echo 属性来给你任何正确执行此操作的希望。因此,您可以在开始接受输入之前将其设置为 false 并自己处理回声。这仍然是一场比赛,但至少你有机会清除任何预先输入的文本。

您现在唯一的半体面解决方法是在您开始您的程序之前disable echo。要求您通过您的方法完成所有输入。

【讨论】:

  • 对我来说幸运的是,通过该方法运行我的所有输入都非常好,所以我认为这种解决方法不仅仅是“半体面”。谢谢!
猜你喜欢
  • 1970-01-01
  • 2016-11-03
  • 2016-04-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-03-11
  • 1970-01-01
相关资源
最近更新 更多