Console Application - WriteLine above current work

2020-04-04 16:45发布

I have seen a few other posts very similar to this one, but the answers they give are not correctly answering the question. Sorry if there is something hidden away that I couldnt find...

I want to use Console.WriteLine() to print something above my current Console.ReadLine(), for example, my app prints "Hello world" and starts a thread that (in 5 seconds) will print "I just waited 5 seconds" above the line where I need to input something, like this:

Hello world
Please input something: _

Then 5 seconds will pass and it will look like this:

Hello world
I just waited 5 seconds
Please input something: _

So far I've tried using Console.SetCursorPosition(0,Console.CursorTop - 1) but this just prints over the line "Please input something: _" and if I use Console.CursorTop - 2 instead it crashes saying "[2] Out of range" (no idea why this is) and if I use Console.CursorTop - 2 it prints under "Please input something: _"... so my question is how do I print something ABOVE the line "Please input something: _"

2条回答
你好瞎i
2楼-- · 2020-04-04 17:30

Just moving the cursor is not good enough, the problem is that you are inserting text. That is possible, the Console.MoveBufferArea() method gives you access to the underlying screen buffer of the console and lets you move text and attributes to another line.

There are a couple of tricky corner-cases. One you already found, you have to force the console to scroll if the cursor is located at the end of the buffer. And the timer is a very difficult problem to solve, you can only really do this correctly if you can prevent Console.ReadLine() from moving the cursor at the exact same time that the timer's Elapsed event inserts the text. That requires a lock, you cannot insert a lock in Console.ReadLine().

Some sample code you can play with to get you there:

static string TimedReadline(string prompt, int seconds) {
    int y = Console.CursorTop;
    // Force a scroll if we're at the end of the buffer
    if (y == Console.BufferHeight - 1) {
        Console.WriteLine();
        Console.SetCursorPosition(0, --y);
    }
    // Setup the timer
    using (var tmr = new System.Timers.Timer(1000 * seconds)) {
        tmr.AutoReset = false;
        tmr.Elapsed += (s, e) => {
            if (Console.CursorTop != y) return;
            int x = Cursor.Left;
            Console.MoveBufferArea(0, y, Console.WindowWidth, 1, 0, y + 1);
            Console.SetCursorPosition(0, y);
            Console.Write("I just waited {0} seconds", seconds);
            Console.SetCursorPosition(x, y + 1);
        };
        tmr.Enabled = true;
        // Write the prompt and obtain the user's input
        Console.Write(prompt);
        return Console.ReadLine();
    }
}

Sample usage:

static void Main(string[] args) {
    for (int ix = 0; ix < Console.BufferHeight; ++ix) Console.WriteLine("Hello world");
    var input = TimedReadline("Please input something: ", 2);
}

Note the test on the Console.Top property, it ensures that nothing goes drastically wrong when the user typed too much text and forced a scroll or if Console.ReadLine() completed at the exact same time that the timer ticked. Proving that it is thread-safe in all possible cases is hard to do, there will surely be trouble when Console.ReadLine() moves the cursor horizontally at the exact same time that the Elapsed event handler runs. I recommend you write your own Console.ReadLine() method so you can insert the lock and feel confident it is always safe.

查看更多
家丑人穷心不美
3楼-- · 2020-04-04 17:44

You can use carriage-return (\r, or U+000D) to return the cursor to the start of the current line, and then overwrite what's there. Something like

// A bunch of spaces to clear the previous output
Console.Write("\r                               ");
Console.WriteLine("\rI just waited 5 seconds");
Console.Write("Please input something: ");

However, if the user has started typing already this won't work anymore (as you may not overwrite all they have typed, and they will lose what they've typed on the screen, although it's still there in memory.

To properly solve this you actually need to modify the console's character buffer. You have to move everything above the current line one line up, and then insert your message. You can use Console.MoveBufferArea to move an area up. Then you need to save the current cursor position, move the cursor to the start of the line above, write your message, and reset the cursor position again to the saved one.

And then you have to hope that the user doesn't type while you're writing your message, because that would mess things up. I'm not sure you can solve that while using ReadLine, though, as you cannot temporarily lock something while ReadLine is active. To properly solve that you may have to write your own ReadLine alternative that reads individual keypresses and will lock on a common object when writing the resulting character to avoid having two threads writing to the console at the same time.

查看更多
登录 后发表回答