Using Readline() and ReadKey() Simultaneously

2019-05-05 05:11发布

问题:

Is there any way to detect both Readline and ReadKey, so that in most cases it behaves as a readline, except for some special key inputs that should be detected?

I need some "parallel" implementation to introduce simultaneity. The code below is synchronous and does not meet my need

while ((line = Console.ReadLine()) != "x")
{    
    if (line == "BLABLA")
    {
        //Stuff
    }

    else
    {
        //Stuff
    }

    ConsoleKeyInfo ki = Console.ReadKey(true);
    if ((ki.Key == ConsoleKey.V) && (ki.Modifiers == ConsoleModifiers.Control))
    {
        //Stuff
    }
}

回答1:

Here's a function I just made to do this.

Right now it only handles backspace, enter, and Esc, but it could easily be modified to handle other keys if you deem them necessary.

    // returns null if user pressed Escape, or the contents of the line if they pressed Enter.
    private static string ReadLineOrEsc()
    {
        string retString = "";

        int curIndex = 0;
        do
        {
            ConsoleKeyInfo readKeyResult = Console.ReadKey(true);

            // handle Esc
            if (readKeyResult.Key == ConsoleKey.Escape)
            {
                Console.WriteLine();
                return null;
            }

            // handle Enter
            if (readKeyResult.Key == ConsoleKey.Enter)
            {
                Console.WriteLine();
                return retString;
            }

            // handle backspace
            if (readKeyResult.Key == ConsoleKey.Backspace)
            {
                if (curIndex > 0)
                {
                    retString = retString.Remove(retString.Length - 1);
                    Console.Write(readKeyResult.KeyChar);
                    Console.Write(' ');
                    Console.Write(readKeyResult.KeyChar);
                    curIndex--;
                }
            }
            else
            // handle all other keypresses
            {
                retString += readKeyResult.KeyChar;
                Console.Write(readKeyResult.KeyChar);
                curIndex++;
            }
        }
        while (true);
    }


回答2:

No, not as such. Both methods block until the user enters something on the console. So even if you would find a way to have both run in parallel, it will not be deterministic which one gets the first shot.

There is a (not obvious) similar problem: how to make Console.ReadLine() abort/break after a certain amount of time without user input.

There have been multiple attempts for this problem here:

  • Console.ReadLine Break
  • How to add a Timeout to Console.ReadLine()?

Most are modelled around either creating your own version of a ReadLine function that adds a timeout (or in your case special handling for certain character (codes)) or the use some sort of threading.

Both ways are either non-trivial or have their own issues (make sure you review the comments, even for the accepted answers).

In short, I think you will need to roll your own version of ReadLine, based on Console.ReadKey with your special handling included and that much of the genuine Console.ReadLine behavior that you require. Note that this even include such basic things as RETURN, ARROW KEYS, BACKSPACE handling, etc.

Update: There is the getline.cs Code from the Mono project, which implements a line editing capability like it was provided by some venerable UNIX shells (EMACS mode, if you care). For that, I believe it will need to implement some sort of ReadLine replacement, although I haven't checked. Maybe you can use this as a starting point.



回答3:

In response to @Overlord Zurd, I improved the code provided by the user.

public class ConsoleOutput
{
    private ConsoleOutputType OutputType { get; set; }
    private object MyObject { get; }

    private static bool IsInserting { get; set; }
    public string KeyName => IsKey() && (ConsoleKeyInfo)MyObject != null ? ((ConsoleKeyInfo)MyObject).Key.ToString() : "Null";
    public string OutputString => !IsKey() && MyObject != null ? (string)MyObject : string.Empty;

    public static event Action<string> ReadInput = delegate { };

    public static event Action<ConsoleKeyInfo> ReadKey = delegate { };

    private ConsoleOutput()
    {
    }

    public ConsoleOutput(object obj)
    {
        MyObject = obj;

        OutputType = obj is ConsoleKeyInfo ? ConsoleOutputType.Key : ConsoleOutputType.Value;
    }

    public bool IsKey()
    {
        return OutputType == ConsoleOutputType.Key;
    }

    public bool IsExitKey()
    {
        if (!IsKey())
            return false;

        var info = ((ConsoleKeyInfo)MyObject);
        return (info.Modifiers & ConsoleModifiers.Control) != 0 && info.Key == ConsoleKey.B;
    }

    public string GetValue()
    {
        return (string)MyObject;
    }

    // returns null if user pressed Escape, or the contents of the line if they pressed Enter.
    public static ConsoleOutput ReadLineOrKey()
    {
        string retString = "";

        int curIndex = 0;
        do
        {
            ConsoleKeyInfo readKeyResult = Console.ReadKey(true);

            // handle Enter
            if (readKeyResult.Key == ConsoleKey.Enter)
            {
                ReadInput?.Invoke(retString);

                Console.WriteLine();
                return new ConsoleOutput(retString);
            }

            // handle backspace
            if (readKeyResult.Key == ConsoleKey.Backspace)
            {
                if (curIndex > 0)
                {
                    retString = retString.Remove(retString.Length - 1);

                    Console.Write(readKeyResult.KeyChar);
                    Console.Write(' ');
                    Console.Write(readKeyResult.KeyChar);

                    --curIndex;
                }
            }
            else if (readKeyResult.Key == ConsoleKey.Delete)
            {
                if (retString.Length - curIndex > 0)
                {
                    // Store current position
                    int curLeftPos = Console.CursorLeft;

                    // Redraw string
                    for (int i = curIndex + 1; i < retString.Length; ++i)
                        Console.Write(retString[i]);

                    // Remove last repeated char
                    Console.Write(' ');

                    // Restore position
                    Console.SetCursorPosition(curLeftPos, Console.CursorTop);

                    // Remove string
                    retString = retString.Remove(curIndex, 1);
                }
            }
            else if (readKeyResult.Key == ConsoleKey.RightArrow)
            {
                if (curIndex < retString.Length)
                {
                    ++Console.CursorLeft;
                    ++curIndex;
                }
            }
            else if (readKeyResult.Key == ConsoleKey.LeftArrow)
            {
                if (curIndex > 0)
                {
                    --Console.CursorLeft;
                    --curIndex;
                }
            }
            else if (readKeyResult.Key == ConsoleKey.Insert)
            {
                IsInserting = !IsInserting;
            }
#if DEBUG
            else if (readKeyResult.Key == ConsoleKey.UpArrow)
            {
                if (Console.CursorTop > 0)
                    --Console.CursorTop;
            }
            else if (readKeyResult.Key == ConsoleKey.DownArrow)
            {
                if (Console.CursorTop < Console.BufferHeight - 1)
                    ++Console.CursorTop;
            }
#endif
            else
            // handle all other keypresses
            {
                if (IsInserting || curIndex == retString.Length)
                {
                    retString += readKeyResult.KeyChar;
                    Console.Write(readKeyResult.KeyChar);
                    ++curIndex;
                }
                else
                {
                    // Store char
                    char c = readKeyResult.KeyChar;

                    // Write char at position
                    Console.Write(c);

                    // Store cursor position
                    int curLeftPos = Console.CursorLeft;

                    // Clear console from curIndex to end
                    for (int i = curIndex; i < retString.Length; ++i)
                        Console.Write(' ');

                    // Go back
                    Console.SetCursorPosition(curLeftPos, Console.CursorTop);

                    // Write the chars from curIndex to end (with the new appended char)
                    for (int i = curIndex; i < retString.Length; ++i)
                        Console.Write(retString[i]);

                    // Restore again
                    Console.SetCursorPosition(curLeftPos, Console.CursorTop);

                    // Store in the string
                    retString = retString.Insert(curIndex, new string(c, 1));

                    // Sum one to the cur index (we appended one char)
                    ++curIndex;
                }
            }

            if (char.IsControl(readKeyResult.KeyChar) &&
                readKeyResult.Key != ConsoleKey.Enter &&
                readKeyResult.Key != ConsoleKey.Backspace &&
                readKeyResult.Key != ConsoleKey.Tab &&
                readKeyResult.Key != ConsoleKey.Delete &&
                readKeyResult.Key != ConsoleKey.RightArrow &&
                readKeyResult.Key != ConsoleKey.LeftArrow &&
                readKeyResult.Key != ConsoleKey.Insert)
            {
#if DEBUG
                if (readKeyResult.Key == ConsoleKey.UpArrow || readKeyResult.Key == ConsoleKey.DownArrow)
                    continue;
#endif

                ReadKey?.Invoke(readKeyResult);

                Console.WriteLine();
                return new ConsoleOutput(readKeyResult);
            }
        }
        while (true);
    }
}

As you can see, I implemented Insert, Arrow control, Delete, etc etc... (Insert was an important thing because if you write any text with this code you will see behavior as Insert key provides).

And example of use:

internal class Program
{
    private static void Main(string[] args)
    {
        Console.Write("Write test string: ");
        var test = ConsoleOutput.ReadLineOrKey();

        if (test.IsKey())
            Console.WriteLine(test.KeyName);
        else
            Console.WriteLine($"Output string: {test.OutputString}");
        Console.Read();
    }
}

You can keep updated on this link (is a link to my lib I'm currently working at).