【问题标题】:How can I update the current line in a C# Windows Console App?如何更新 C# Windows 控制台应用程序中的当前行?
【发布时间】:2010-10-27 16:12:06
【问题描述】:

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

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

【问题讨论】:

  • 如果你真的对酷炫的命令行界面感兴趣,你应该看看 curses/ncurses。
  • @CharlesAddis 但curses/ncurses 不是仅在 C++ 中有效吗?
  • @Xam 在 .NET Core 中进行跨平台编程时,我碰巧选择了 curses 库作为示例来实现。包是dotnet-curses

标签: c# windows console


【解决方案1】:

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

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

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

【讨论】:

  • for(int i = 0; i
  • 当前一次写入比新写入长时,你如何处理?有没有办法获得控制台的宽度并用空格填充行?
  • @druciferre 在我的脑海中,我可以为你的问题想出两个答案。它们都涉及首先将当前输出保存为字符串,然后用一组字符填充它,如下所示: Console.Write("\r{0}", strOutput.PadRight(nPaddingCount, ' ')); “nPaddingCount”可以是您自己设置的数字,也可以跟踪先前的输出并将 nPaddingCount 设置为先前输出和当前输出之间的长度差加上当前输出长度。如果 nPaddingCount 为负数,那么您不必使用 PadRight,除非您执行 abs(prev.len - curr.len)。
  • @malgm 组织良好的代码。如果十几个线程中的任何一个可以随时写入控制台,那么无论您是否正在编写新行,都会给您带来麻烦。
  • @JohnOdom 您只需要保留之前的(未填充的)输出长度,然后将其作为第一个参数提供给PadRight(当然,首先保存未填充的字符串或长度) .
【解决方案2】:

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

这是一个example,显示了一个简单的“微调器”:

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);
    }
}

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

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

Console.SetCursorPosition(0, Console.CursorTop);

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

【讨论】:

  • 这个问题可以使用 \r 来解决,但是使用SetCursorPosition(或CursorLeft)可以提供更大的灵活性,例如不在行首书写,在窗口中向上移动等,因此这是一种更通用的方法,可用于例如输出自定义进度条或 ASCII 图形。
  • +1 表示冗长且超出职责范围。好东西谢谢。
  • +1 用于展示不同的操作方式。其他人都显示了 \r,如果 OP 只是更新一个百分比,那么他可以只更新值而无需重新编写整行。 OP 从未真正说过他想移动到行首,只是他想更新光标所在行的内容。
  • SetCursorPosition 增加的灵活性是以一点速度和明显的光标闪烁为代价的,如果循环足够长让用户注意到的话。请参阅下面的测试评论。
  • 还要确认行长不会导致控制台换行到下一行,否则您可能会在控制台窗口下运行的内容出现问题。
【解决方案3】:

到目前为止,对于如何做到这一点,我们有三个相互竞争的替代方案:

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
}

我一直使用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);
}

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

  • 退格键:25.0 秒
  • 回车:28.7 秒
  • SetCursorPosition:49.7 秒

此外,SetCursorPosition 引起了明显的闪烁,这是我在使用任何一种替代方案时都没有观察到的。所以,道德是尽可能使用退格或回车,并且感谢教我更快的方法,所以!


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

【讨论】:

  • 有问题的操作的效率真的不重要。这一切都应该发生得太快,以至于用户无法注意到。不必要的微优化是不好的。
  • @Malfist:根据循环的长度,用户可能会或可能不会注意到。正如我在上面的编辑中添加的那样(在我看到您的评论之前),SetCursorPosition 引入了闪烁,并且花费的时间几乎是其他选项的两倍。
  • 我同意这是一个微优化(运行一百万次并花费 50 秒仍然是非常少的时间),+1 的结果,它肯定会非常有用知道。
  • 基准测试存在根本缺陷。无论光标移动多远,SetCursorPosition() 时间都可能相同,而其他选项因控制台必须处理的字符数而异。
  • 这是对可用不同选项的一个很好的总结。但是,我在使用 \r 时也看到闪烁。使用 \b 显然没有闪烁,因为没有重写修复文本(“计数:”)。如果您添加额外的 \b 并重写修复文本,您也会得到闪烁,因为它正在使用 \b 和 SetCursorPosition 发生。关于 Joel 的评论:Joel 基本上是正确的,但是 \r 在很长的行上仍然会优于 SetCursorPosition,但差异会越来越小。
【解决方案4】:

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

例如:

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);
}

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

\r 方法相比,此方法的优势在于,即使您的百分比输出不在行首,if 也有效。

【讨论】:

  • +1,事实证明这是最快的方法(见下面我的测试评论)
【解决方案5】:

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

【讨论】:

  • 其实是\r\n.
【解决方案6】:

我只需要玩 Divo 的 ConsoleSpinner 类。我的远没有那么简洁,但是我觉得那个类的用户必须编写自己的while(true)循环。我正在拍摄更像这样的体验:

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

    // Do some work...

    spin.Stop(); 
}

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

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

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();
    }
}

【讨论】:

  • 很好的修改,虽然示例代码不是我的。它取自 Brad Abrams 的博客(请参阅我的答案中的链接)。我认为它只是作为演示 SetCursorPosition 的简单示例编写的。顺便说一句,对于我认为只是一个简单示例的讨论开始,我绝对感到惊讶(以积极的方式)。这就是我喜欢这个网站的原因:-)
【解决方案7】:

在行首显式使用 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();    
}

【讨论】:

  • -1,问题要求 C#,我用 C# 重写,你改回 F#
  • 这看起来像是编辑冲突,而不是他将您的 C# 更改回 F#。他的变化比你晚一分钟,并且专注于 sprintf。
  • 感谢您的编辑。我倾向于使用 F# 交互模式来测试事物,并认为重要的部分是 BCL 调用,这在 C# 中是相同的。
【解决方案8】:
    public void Update(string data)
    {
        Console.Write(string.Format("\r{0}", "".PadLeft(Console.CursorLeft, ' ')));
        Console.Write(string.Format("\r{0}", data));
    }

【讨论】:

    【解决方案9】:

    来自 MSDN 中的控制台文档:

    你可以通过设置来解决这个问题 的 TextWriter.NewLine 属性 输出或错误属性到另一行 终止字符串。例如, C# 语句,Console.Error.NewLine = "\r\n\r\n";,设置行终止 标准错误输出的字符串 流到两个回车和行 饲料序列。那么你就可以 显式调用 WriteLine 方法 错误输出流对象的,如 在 C# 语句中, Console.Error.WriteLine();

    所以 - 我这样做了:

    Console.Out.Newline = String.Empty;
    

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

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

    到达那里的另一种方式。

    【讨论】:

    • 你可以使用 Console.Write() 来达到同样的目的,而不需要重新定义 NewLine 属性...
    【解决方案10】:

    如果你想让生成的文件看起来很酷,这很有效。

                    int num = 1;
                    var spin = new ConsoleSpinner();
                    Console.ForegroundColor = ConsoleColor.Green;
                    Console.Write("");
                    while (true)
                    {
                        spin.Turn();
                        Console.Write("\r{0} Generating Files ", num);
                        num++;
                    }
    

    这是我从下面的一些答案中得到并修改它的方法

    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;
                    case 4: Console.Write("\r"); break;
                }
                Thread.Sleep(100);
                Console.SetCursorPosition(23, Console.CursorTop);
            }
        }
    

    【讨论】:

      【解决方案11】:

      这是另一个:D

      class Program
      {
          static void Main(string[] args)
          {
              Console.Write("Working... ");
              int spinIndex = 0;
              while (true)
              {
                  // obfuscate FTW! Let's hope overflow is disabled or testers are impatient
                  Console.Write("\b" + @"/-\|"[(spinIndex++) & 3]);
              }
          }
      }
      

      【讨论】:

        【解决方案12】:

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

        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");
        
            }
        }
        

        【讨论】:

          【解决方案13】:

          我在 vb.net 中寻找相同的解决方案,我找到了这个,它很棒。

          但是,如果前一个大于当前一个,@JohnOdom 建议使用更好的方法来处理空格。

          我在 vb.net 中做了一个函数,并认为有人可以得到帮助..

          这是我的代码:

          Private Sub sPrintStatus(strTextToPrint As String, Optional boolIsNewLine As Boolean = False)
              REM intLastLength is declared as public variable on global scope like below
              REM intLastLength As Integer
              If boolIsNewLine = True Then
                  intLastLength = 0
              End If
              If intLastLength > strTextToPrint.Length Then
                  Console.Write(Convert.ToChar(13) & strTextToPrint.PadRight(strTextToPrint.Length + (intLastLength - strTextToPrint.Length), Convert.ToChar(" ")))
              Else
                  Console.Write(Convert.ToChar(13) & strTextToPrint)
              End If
              intLastLength = strTextToPrint.Length
          End Sub
          

          【讨论】:

          • 这里可以使用局部静态变量的VB特性:Static intLastLength As Integer
          【解决方案14】:

          我正在对此进行搜索,以查看我编写的解决方案是否可以针对速度进行优化。我想要的是一个倒数计时器,而不仅仅是更新当前行。 这就是我想出的。可能对某人有用

                      int sleepTime = 5 * 60;    // 5 minutes
          
                      for (int secondsRemaining = sleepTime; secondsRemaining > 0; secondsRemaining --)
                      {
                          double minutesPrecise = secondsRemaining / 60;
                          double minutesRounded = Math.Round(minutesPrecise, 0);
                          int seconds = Convert.ToInt32((minutesRounded * 60) - secondsRemaining);
                          Console.Write($"\rProcess will resume in {minutesRounded}:{String.Format("{0:D2}", -seconds)} ");
                          Thread.Sleep(1000);
                      }
                      Console.WriteLine("");
          

          【讨论】:

            【解决方案15】:

            受@E.Lahu 解决方案的启发,实现了带有百分比的进度条。

            public class ConsoleSpinner
            {
                private int _counter;
            
                public void Turn(Color color, int max, string prefix = "Completed", string symbol = "■",int position = 0)
                {
                    Console.SetCursorPosition(0, position);
                    Console.Write($"{prefix} {ComputeSpinner(_counter, max, symbol)}", color);
                    _counter = _counter == max ? 0 : _counter + 1;
                }
            
                public string ComputeSpinner(int nmb, int max, string symbol)
                {
                    var spinner = new StringBuilder();
                    if (nmb == 0)
                        return "\r ";
            
                    spinner.Append($"[{nmb}%] [");
                    for (var i = 0; i < max; i++)
                    {
                        spinner.Append(i < nmb ? symbol : ".");
                    }
            
                    spinner.Append("]");
                    return spinner.ToString();
                }
            }
            
            
            public static void Main(string[] args)
                {
                    var progressBar= new ConsoleSpinner();
                    for (int i = 0; i < 1000; i++)
                    {
                        progressBar.Turn(Color.Aqua,100);
                        Thread.Sleep(1000);
                    }
                }
            

            【讨论】:

              【解决方案16】:

              这是我对 s soosh 和 0xA3 的回答。 它可以在更新微调器时使用用户消息更新控制台,并且还具有经过时间指示器。

              public class ConsoleSpiner : IDisposable
              {
                  private static readonly string INDICATOR = "/-\\|";
                  private static readonly string MASK = "\r{0} {1:c} {2}";
                  int counter;
                  Timer timer;
                  string message;
              
                  public ConsoleSpiner() {
                      counter = 0;
                      timer = new Timer(200);
                      timer.Elapsed += TimerTick;
                  }
              
                  public void Start() {
                      timer.Start();
                  }
              
                  public void Stop() {
                      timer.Stop();
                      counter = 0;
                  }
              
                  public string Message {
                      get { return message; }
                      set { message = value; }
                  }
              
                  private void TimerTick(object sender, ElapsedEventArgs e) {
                      Turn();
                  }
              
                  private void Turn() {
                      counter++;
                      var elapsed = TimeSpan.FromMilliseconds(counter * 200);
                      Console.Write(MASK, INDICATOR[counter % 4], elapsed, this.Message);
                  }
              
                  public void Dispose() {
                      Stop();
                      timer.Elapsed -= TimerTick;
                      this.timer.Dispose();
                  }
              }
              

              用法是这样的:

              class Program
              {
                  static void Main(string[] args)
                  {
                      using (var spinner = new ConsoleSpiner())
                      {
                          spinner.Start();
                          spinner.Message = "About to do some heavy staff :-)"
                          DoWork();
                          spinner.Message = "Now processing other staff".
                          OtherWork();
                          spinner.Stop();
                      }
                      Console.WriteLine("COMPLETED!!!!!\nPress any key to exit.");
              
                  }
              }
              

              【讨论】:

                【解决方案17】:

                SetCursorPosition 方法适用于多线程场景,而其他两种方法则不适用

                【讨论】:

                  猜你喜欢
                  • 2011-04-23
                  • 2011-09-07
                  • 1970-01-01
                  • 1970-01-01
                  • 2013-02-06
                  • 2011-12-07
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多