如何更新 C# Windows 控制台应用程序中的当前行?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/888533/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-05 08:33:51  来源:igfitidea点击:

How can I update the current line in a C# Windows Console App?

c#windowsconsole

提问by IVR Avenger

When building a Windows Console App in C#, is it possible to write to the console without having to extend a current line or go to a new line? For example, if I want to show a percentage representing how close a process is to completion, I'd just like to update the value on the same line as the cursor, and not have to put each percentage on a new line.

在 C# 中构建 Windows 控制台应用程序时,是否可以写入控制台而无需扩展当前行或转到新行?例如,如果我想显示一个百分比来表示一个进程完成的接近程度,我只想更新光标所在行上的值,而不必将每个百分比放在一个新行上。

Can this be done with a "standard" C# console app?

这可以通过“标准”C# 控制台应用程序完成吗?

采纳答案by shoosh

If you print only "\r"to the console the cursor goes back to the beginning of the current line and then you can rewrite it. This should do the trick:

如果你只打印"\r"到控制台,光标会回到当前行的开头,然后你可以重写它。这应该可以解决问题:

for(int i = 0; i < 100; ++i)
{
    Console.Write("\r{0}%   ", i);
}

Notice the few spaces after the number to make sure that whatever was there before is erased.
Also notice the use of Write()instead of WriteLine()since you don't want to add an "\n" at the end of the line.

注意数字后面的几个空格,以确保之前的任何内容都被删除了。
还要注意使用Write()代替,WriteLine()因为您不想在行尾添加“\n”。

回答by Dirk Vollmar

You can use Console.SetCursorPositionto set the position of the cursor and then write at the current position.

您可以使用Console.SetCursorPosition设置光标的位置,然后在当前位置写入。

Here is an exampleshowing a simple "spinner":

这是一个显示简单“微调器”的示例

static void Main(string[] args)
{
    var spin = new ConsoleSpinner();
    Console.Write("Working....");
    while (true) 
    {
        spin.Turn();
    }
}

public class ConsoleSpinner
{
    int counter;

    public void Turn()
    {
        counter++;        
        switch (counter % 4)
        {
            case 0: Console.Write("/"); counter = 0; break;
            case 1: Console.Write("-"); break;
            case 2: Console.Write("\"); break;
            case 3: Console.Write("|"); break;
        }
        Thread.Sleep(100);
        Console.SetCursorPosition(Console.CursorLeft - 1, Console.CursorTop);
    }
}

Note that you will have to make sure to overwrite any existing output with new output or blanks.

请注意,您必须确保用新输出或空白覆盖任何现有输出。

Update: As it has been criticized that the example moves the cursor only back by one character, I will add this for clarification: Using SetCursorPositionyou may set the cursor to any position in the console window.

更新:由于有人批评该示例仅将光标向后移动一个字符,我将添加此说明:使用SetCursorPosition您可以将光标设置到控制台窗口中的任何位置。

Console.SetCursorPosition(0, Console.CursorTop);

will set the cursor to the beginning of the current line (or you can use Console.CursorLeft = 0directly).

将光标设置到当前行的开头(也可以Console.CursorLeft = 0直接使用)。

回答by Malfist

\ris used for these scenarios.
\rrepresents a carriage return which means the cursor returns to the start of the line.
That's why Windows uses \n\ras its new line marker.
\nmoves you down a line, and \rreturns you to the start of the line.

\r用于这些场景。
\r表示回车,表示光标返回到行首。
这就是 Windows\n\r用作其新行标记的原因。
\n将您向下移动一行,然后\r将您返回到该行的开头。

回答by James Hugard

Explicitly using a Carrage Return (\r) at the beginning of the line rather than (implicitly or explicitly) using a New Line (\n) at the end should get what you want. For example:

在行首显式使用 Carrage Return (\r) 而不是(隐式或显式)在末尾使用换行符 (\n) 应该得到您想要的结果。例如:

void demoPercentDone() {
    for(int i = 0; i < 100; i++) {
        System.Console.Write( "\rProcessing {0}%...", i );
        System.Threading.Thread.Sleep( 1000 );
    }
    System.Console.WriteLine();    
}

回答by Sean

You can use the \b(backspace) escape sequence to backup a particular number of characters on the current line. This just moves the current location, it does not remove the characters.

您可以使用\b(退格)转义序列来备份当前行上特定数量的字符。这只是移动当前位置,它不会删除字符。

For example:

例如:

string line="";

for(int i=0; i<100; i++)
{
    string backup=new string('\b',line.Length);
    Console.Write(backup);
    line=string.Format("{0}%",i);
    Console.Write(line);
}

Here, lineis the percentage line to write to the console. The trick is to generate the correct number of \bcharacters for the previous output.

这里,line是写入控制台的百分比行。诀窍是为之前的输出生成正确数量的\b字符。

The advantage of this over the \rapproach is that if works even if your percentage output is not at the beginning of the line.

\r方法相比,这种方法的优点是即使您的百分比输出不在行的开头,也可以工作。

回答by Kevin

So far we have three competing alternatives for how to do this:

到目前为止,我们有三个相互竞争的替代方案来实现这一点:

Console.Write("\r{0}   ", value);                      // Option 1: carriage return
Console.Write("\b\b\b\b\b{0}", value);                 // Option 2: backspace
{                                                      // Option 3 in two parts:
    Console.SetCursorPosition(0, Console.CursorTop);   // - Move cursor
    Console.Write(value);                              // - Rewrite
}

I've always used Console.CursorLeft = 0, a variation on the third option, so I decided to do some tests. Here's the code I used:

我一直使用Console.CursorLeft = 0,第三个选项的变体,所以我决定做一些测试。这是我使用的代码:

public static void CursorTest()
{
    int testsize = 1000000;

    Console.WriteLine("Testing cursor position");
    Stopwatch sw = new Stopwatch();
    sw.Start();
    for (int i = 0; i < testsize; i++)
    {
        Console.Write("\rCounting: {0}     ", i);
    }
    sw.Stop();
    Console.WriteLine("\nTime using \r: {0}", sw.ElapsedMilliseconds);

    sw.Reset();
    sw.Start();
    int top = Console.CursorTop;
    for (int i = 0; i < testsize; i++)
    {
        Console.SetCursorPosition(0, top);        
        Console.Write("Counting: {0}     ", i);
    }
    sw.Stop();
    Console.WriteLine("\nTime using CursorLeft: {0}", sw.ElapsedMilliseconds);

    sw.Reset();
    sw.Start();
    Console.Write("Counting:          ");
    for (int i = 0; i < testsize; i++)
    {        
        Console.Write("\b\b\b\b\b\b\b\b{0,8}", i);
    }

    sw.Stop();
    Console.WriteLine("\nTime using \b: {0}", sw.ElapsedMilliseconds);
}

On my machine, I get the following results:

在我的机器上,我得到以下结果:

  • Backspaces: 25.0 seconds
  • Carriage Returns: 28.7 seconds
  • SetCursorPosition: 49.7 seconds
  • 退格:25.0 秒
  • 回车时间:28.7 秒
  • 设置光标位置:49.7 秒

Additionally, SetCursorPositioncaused noticeable flicker that I didn't observe with either of the alternatives. So, the moral is to use backspaces or carriage returns when possible, and thanks for teaching mea faster way to do this, SO!

此外,SetCursorPosition导致明显的闪烁,我没有观察到任何一种替代方法。所以,道德是在可能的情况下使用退格或回车感谢教我更快的方法来做到这一点,所以!



Update: In the comments, Joel suggests that SetCursorPosition is constant with respect to the distance moved while the other methods are linear. Further testing confirms that this is the case, howeverconstant time and slow is still slow. In my tests, writing a long string of backspaces to the console is faster than SetCursorPosition until somewhere around 60 characters. So backspace is faster for replacing portions of the line shorter than 60 characters (or so), andit doesn't flicker, so I'm going to stand by my initial endorsement of \b over \r and SetCursorPosition.

更新:在评论中,Joel 建议 SetCursorPosition 相对于移动的距离是恒定的,而其他方法是线性的。进一步的测试证实情况确实如此,但是恒定时间和慢速仍然很慢。在我的测试中,向控制台写入一长串退格比 SetCursorPosition 快,直到大约 60 个字符。因此,退格对于替换短于 60 个字符(左右)的行的部分会更快,并且它不会闪烁,所以我将支持 \b 而不是 \r 和SetCursorPosition

回答by Joel Coehoorn

I just had to play with the divo's ConsoleSpinnerclass. Mine is nowhere near as concise, but it just didn't sit well with me that users of that class have to write their own while(true)loop. I'm shooting for an experience more like this:

我只需要和 divo 的ConsoleSpinner班级一起玩。我的远没有那么简洁,但我觉得那个类的用户必须编写自己的while(true)循环并不合适。我正在拍摄更像这样的体验:

static void Main(string[] args)
{
    Console.Write("Working....");
    ConsoleSpinner spin = new ConsoleSpinner();
    spin.Start();

    // Do some work...

    spin.Stop(); 
}

And I realized it with the code below. Since I don't want my Start()method to block, I don't want the user to have to worry about writing a while(spinFlag)-like loop, and I want to allow multiple spinners at the same time I had to spawn a separate thread to handle the spinning. And that means the code has to be a lot more complicated.

我用下面的代码实现了它。由于我不希望我的Start()方法被阻塞,我不希望用户不必担心编写一个类似while(spinFlag)循环,并且我希望同时允许多个微调器我不得不产生一个单独的线程来处理纺纱。这意味着代码必须复杂得多。

Also, I haven't done that much multi-threading so it's possible (likely even) that I've left a subtle bug or three in there. But it seems to work pretty well so far:

另外,我没有做过那么多的多线程处理,所以有可能(甚至可能)我在那里留下了一个或三个微妙的错误。但到目前为止它似乎工作得很好:

public class ConsoleSpinner : IDisposable
{       
    public ConsoleSpinner()
    {
        CursorLeft = Console.CursorLeft;
        CursorTop = Console.CursorTop;  
    }

    public ConsoleSpinner(bool start)
        : this()
    {
        if (start) Start();
    }

    public void Start()
    {
        // prevent two conflicting Start() calls ot the same instance
        lock (instanceLocker) 
        {
            if (!running )
            {
                running = true;
                turner = new Thread(Turn);
                turner.Start();
            }
        }
    }

    public void StartHere()
    {
        SetPosition();
        Start();
    }

    public void Stop()
    {
        lock (instanceLocker)
        {
            if (!running) return;

            running = false;
            if (! turner.Join(250))
                turner.Abort();
        }
    }

    public void SetPosition()
    {
        SetPosition(Console.CursorLeft, Console.CursorTop);
    }

    public void SetPosition(int left, int top)
    {
        bool wasRunning;
        //prevent other start/stops during move
        lock (instanceLocker)
        {
            wasRunning = running;
            Stop();

            CursorLeft = left;
            CursorTop = top;

            if (wasRunning) Start();
        } 
    }

    public bool IsSpinning { get { return running;} }

    /* ---  PRIVATE --- */

    private int counter=-1;
    private Thread turner; 
    private bool running = false;
    private int rate = 100;
    private int CursorLeft;
    private int CursorTop;
    private Object instanceLocker = new Object();
    private static Object console = new Object();

    private void Turn()
    {
        while (running)
        {
            counter++;

            // prevent two instances from overlapping cursor position updates
            // weird things can still happen if the main ui thread moves the cursor during an update and context switch
            lock (console)
            {                  
                int OldLeft = Console.CursorLeft;
                int OldTop = Console.CursorTop;
                Console.SetCursorPosition(CursorLeft, CursorTop);

                switch (counter)
                {
                    case 0: Console.Write("/"); break;
                    case 1: Console.Write("-"); break;
                    case 2: Console.Write("\"); break;
                    case 3: Console.Write("|"); counter = -1; break;
                }
                Console.SetCursorPosition(OldLeft, OldTop);
            }

            Thread.Sleep(rate);
        }
        lock (console)
        {   // clean up
            int OldLeft = Console.CursorLeft;
            int OldTop = Console.CursorTop;
            Console.SetCursorPosition(CursorLeft, CursorTop);
            Console.Write(' ');
            Console.SetCursorPosition(OldLeft, OldTop);
        }
    }

    public void Dispose()
    {
        Stop();
    }
}

回答by I Wanna Bea Programmer.

From the Console docs in MSDN:

从 MSDN 中的控制台文档:

You can solve this problem by setting the TextWriter.NewLine property of the Out or Error property to another line termination string. For example, the C# statement, Console.Error.NewLine = "\r\n\r\n";, sets the line termination string for the standard error output stream to two carriage return and line feed sequences. Then you can explicitly call the WriteLine method of the error output stream object, as in the C# statement, Console.Error.WriteLine();

您可以通过将 Out 或 Error 属性的 TextWriter.NewLine 属性设置为另一个行终止字符串来解决此问题。例如,C# 语句 Console.Error.NewLine = "\r\n\r\n"; 将标准错误输出流的行终止字符串设置为两个回车和换行序列。然后可以显式调用错误输出流对象的WriteLine方法,如C#语句Console.Error.WriteLine();

So - I did this:

所以 - 我这样做了:

Console.Out.Newline = String.Empty;

Then I am able to control the output myself;

然后我就可以自己控制输出了;

Console.WriteLine("Starting item 1:");
    Item1();
Console.WriteLine("OK.\nStarting Item2:");

Another way of getting there.

到达那里的另一种方式。

回答by lisunde

If you want update one line, but the information is too long to show on one line, it may need some new lines. I've encountered this problem, and below is one way to solve this.

如果您想更新一行,但信息太长无法在一行显示,则可能需要一些新行。我遇到过这个问题,下面是解决这个问题的一种方法。

public class DumpOutPutInforInSameLine
{

    //content show in how many lines
    int TotalLine = 0;

    //start cursor line
    int cursorTop = 0;

    // use to set  character number show in one line
    int OneLineCharNum = 75;

    public void DumpInformation(string content)
    {
        OutPutInSameLine(content);
        SetBackSpace();

    }
    static void backspace(int n)
    {
        for (var i = 0; i < n; ++i)
            Console.Write("\b \b");
    }

    public  void SetBackSpace()
    {

        if (TotalLine == 0)
        {
            backspace(OneLineCharNum);
        }
        else
        {
            TotalLine--;
            while (TotalLine >= 0)
            {
                backspace(OneLineCharNum);
                TotalLine--;
                if (TotalLine >= 0)
                {
                    Console.SetCursorPosition(OneLineCharNum, cursorTop + TotalLine);
                }
            }
        }

    }

    private void OutPutInSameLine(string content)
    {
        //Console.WriteLine(TotalNum);

        cursorTop = Console.CursorTop;

        TotalLine = content.Length / OneLineCharNum;

        if (content.Length % OneLineCharNum > 0)
        {
            TotalLine++;

        }

        if (TotalLine == 0)
        {
            Console.Write("{0}", content);

            return;

        }

        int i = 0;
        while (i < TotalLine)
        {
            int cNum = i * OneLineCharNum;
            if (i < TotalLine - 1)
            {
                Console.WriteLine("{0}", content.Substring(cNum, OneLineCharNum));
            }
            else
            {
                Console.Write("{0}", content.Substring(cNum, content.Length - cNum));
            }
            i++;

        }
    }

}
class Program
{
    static void Main(string[] args)
    {

        DumpOutPutInforInSameLine outPutInSameLine = new DumpOutPutInforInSameLine();

        outPutInSameLine.DumpInformation("");
        outPutInSameLine.DumpInformation("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");


        outPutInSameLine.DumpInformation("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
        outPutInSameLine.DumpInformation("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");

        //need several lines
        outPutInSameLine.DumpInformation("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
        outPutInSameLine.DumpInformation("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");

        outPutInSameLine.DumpInformation("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
        outPutInSameLine.DumpInformation("bbbbbbbbbbbbbbbbbbbbbbbbbbb");

    }
}

回答by Jose

    public void Update(string data)
    {
        Console.Write(string.Format("\r{0}", "".PadLeft(Console.CursorLeft, ' ')));
        Console.Write(string.Format("\r{0}", data));
    }