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


507

使用C#构建Windows控制台应用程序时,是否可以在不必扩展当前行或转到新行的情况下写入控制台?例如,如果我想显示一个百分比,该百分比表示流程离完成的程度,那么我只想在与游标相同的行上更新值,而不必将每个百分比都放在新行上。

可以使用“标准” C#控制台应用程序完成此操作吗?


如果您真的对酷炫的命令行界面感兴趣,则应查看curses / ncurses。
Charles Addis

@CharlesAddis,但是curses / ncurses是否仅在C ++中起作用?
Xam

Answers:


783

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

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

请注意数字后的几个空格,以确保删除之前的内容。
还要注意使用of Write()WriteLine()因为您不想在行末添加“ \ n”。


7
for(int i = 0; i <= 100; ++ i)将达到100%
Nicolas Tyler

13
当上一次写入的时间长于新写入的时间时,如何处理?有没有办法获得控制台的宽度并在空格处填充行?
德鲁·查平

6
@druciferre我脑海中浮现出两个答案。它们都涉及首先将当前输出另存为字符串,并用诸如此类的一定数量的字符填充:Console.Write(“ \ r {0}”,strOutput.PadRight(nPaddingCount,'')); “ nPaddingCount”可以是您自己设置的数字,也可以跟踪先前的输出,并将nPaddingCount设置为先前输出与当前输出之间的长度差加上当前输出长度。如果nPaddingCount为负,则除非您进行abs(prev.len-curr.len),否则不必使用PadRight。
约翰·奥多姆

1
@malgm井井有条的代码。如果十几个线程中的任何一个可以在需要时随时写入控制台,那么无论您是否编写新行,都会给您带来麻烦。
2014年

2
@JohnOdom您只需要保留先前的(未填充的)输出长度,然后将其作为第一个参数输入PadRight(当然要保存未填充的字符串或长度)。
杰斯珀·马蒂森

254

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

这是显示一个简单的“旋转器” 的示例

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直接使用)。


8
可以使用\ r解决该问题,但是使用SetCursorPosition(或CursorLeft)可以提供更大的灵活性,例如,不在行的开头书写,不在窗口中向上移动等,因此它是一种更通用的方法,可用于例如输出自定义进度条或ASCII图形。
Dirk Vollmar,2009年

14
+1表示冗长,超出了职责范围。好东西,谢谢。
巴拿马杯2009年

1
+1用于显示不同的操作方式。其他所有人都显示\ r,如果OP仅更新百分比,则他可以更新值而不必重新编写整行。OP从来没有说过要移动到行的开头,而只是想更新与光标在同一行的内容。
安迪2009年

1
SetCursorPosition的灵活性增加了,但代价是速度有点慢,并且如果循环的时间足够使用户注意到,则光标闪烁也会很明显。请参阅下面的测试评论。
凯文,

5
还要确认行长不会导致控制台换行到下一行,否则无论如何您可能会遇到内容在控制台窗口上运行的问题。
Mandrake 2012年

84

到目前为止,我们有三种竞争方法可以做到这一点:

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引起了明显的闪烁,而这两种选择我都没有观察到。所以,道德是在可能的情况下使用退格键或回车符感谢您教给我一种更快的方式,所以!


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


4
有关操作的效率确实不重要。对于用户而言,这一切发生得太快了。不必要的微量纯化是有害的。
Malfist

@Malfist:取决于循环的长度,用户可能会注意到也可能不会注意到。正如我在上面的编辑中所添加的(在看到您的评论之前),SetCursorPosition引入了闪烁,所需时间几乎是其他选项的两倍。
凯文”

1
我同意这是一次微优化(将其运行一百万次并花费50秒仍然是非常少的时间),对于结果+1,这肯定非常有用。
安迪

6
基准从根本上是有缺陷的。无论光标移动多远,SetCursorPosition()的时间可能都是相同的,而其他选项则取决于控制台必须处理多少个字符。
乔尔·科洪

1
这是对各种可用选项的很好总结。但是,使用\ r时也会出现闪烁。使用\ b显然不会闪烁,因为修订文本(“ Counting:”)不会被重写。如果添加其他\ b并重写\ b和SetCursorPosition发生的修复文本,则也会闪烁。关于Joel的话:Joel基本上是正确的,但是\ r在很长的行上仍将胜过SetCursorPosition,但差异变小。
Dirk Vollmar,2009年

27

您可以使用\ 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方法,此方法的优点是即使您的输出百分比不在行首也可以运行。


1
+1,事实证明这是最快的方法(请参阅下面的测试评论)
凯文,

19

\r用于这些方案。
\r 表示回车,这意味着光标返回到行的开头。
这就是Windows将\n\r其用作新行标记的原因。
\n将您下移,然后\r返回到行首。


22
除了实际上是\ r \ n。
乔尔·穆勒

14

我只需要玩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的简单示例编写的。顺便说一句,对于以我认为仅仅是一个简单示例的讨论开始,我肯定(以积极的方式)感到惊讶。这就是为什么我喜欢这个网站的原因:-)
Dirk Vollmar 09年

4

在行的开头显式使用回车符(\ 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#
Malfist

看起来像是编辑冲突,而不是他将C#改回F#。他的换班仅比您晚一分钟,并且专注于sprintf。
安迪2009年

感谢您的修改。我倾向于使用F#交互模式来测试事物,并且发现重要的部分是BCL调用,这在C#中是相同的。
詹姆斯·休加德

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

1

从MSDN中的控制台文档中:

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

所以-我这样做了:

Console.Out.Newline = String.Empty;

这样我就可以自己控制输出了;

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

到达那里的另一种方式。


你可以只使用Console.Write()为了同样的目的,不需要重新定义NEWLINE属性...
拉多斯拉夫·热尔

1

如果您要使生成的文件看起来很酷,则可以使用此方法。

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

0

这是另一个: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]);
        }
    }
}

0

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

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

    }
}

0

我在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
马克·赫德

0

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

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

0

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

0

这是我对s sosh和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.");

    }
}

-1

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.