Как обновить текущую строку в консольном приложении C# Windows?

при создании консольного приложения Windows на C# можно ли писать на консоль без необходимости расширения текущей строки или перехода на новую строку? Например, если я хочу показать процент, представляющий, насколько близок процесс к завершению, я просто хотел бы обновить значение в той же строке, что и курсор, и не нужно помещать каждый процент в новую строку.

можно ли это сделать с помощью" стандартного " консольного приложения C#?

14 ответов


если вы печатаете только "\r" в консоли курсор возвращается к началу текущей строки, а затем вы можете переписать его. Это должно сделать трюк:

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

обратите внимание на несколько пробелов после номера, чтобы убедиться, что все, что было раньше стирается.
Также обратите внимание на использование Write() вместо WriteLine() так как вы не хотите добавлять "\n" в конце строки.


можно использовать 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);
    }
}

обратите внимание, что вам нужно будет обязательно перезаписать любой существующий вывод новыми выводами или пробелами.

Update: поскольку критикуется, что пример перемещает курсор только на один символ, я добавлю это для уточнения: использование SetCursorPosition вы можете установить курсор на любое положение в окне консоли.

Console.SetCursorPosition(0, Console.CursorTop);

устанавливает курсор в начало текущей строки (или вы можете использовать Console.CursorLeft = 0 напрямую).


до сих пор у нас есть три конкурирующих Альтернативы для того, как это сделать:

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

на моей машине, я получаю следующие результаты:

  • Backspaces: 25.0 секунд
  • Возвраты Каретки: 28.7 секунд
  • SetCursorPosition: 49.7 секунды!--14-->

кроме того, SetCursorPosition вызвало заметное мерцание, которое я не наблюдал ни с одной из альтернатив. Итак, мораль в том, чтобы используйте backspaces или возврат каретки, когда это возможно и спасибо, что научил меня более быстрый способ сделать это, так!


обновление: в комментариях Джоэл предполагает, что SetCursorPosition постоянна относительно расстояния, перемещаемого, в то время как другие методы линейный. Дальнейшее тестирование подтверждает, что это так, постоянное время и медленно по-прежнему медленно. В моих тестах запись длинной строки обратных пространств на консоль происходит быстрее, чем SetCursorPosition, пока где-то около 60 символов. Таким образом, backspace быстрее для замены частей строки короче 60 символов (или около того), и он не мерцает, поэтому я буду стоять на своем первоначальном одобрении \b над \r и SetCursorPosition.


можно использовать \b (backspace) escape-последовательность для резервного копирования определенного количества символов в текущей строке. Это просто перемещает текущее местоположение, оно не удаляет символы.

например:

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

здесь строка - это процентная строка для записи на консоль. Фокус в том, чтобы сгенерировать правильное число \b символы для предыдущего вывода.

преимущество над \r подход заключается в том, что если работает даже если ваш процент вывода не в начале строки.


\r используется для этих сценариев.
\r представляет возврат каретки, что означает, что курсор возвращается в начало строки.
Вот почему windows использует \n\r как новый маркер линии.
\n перемещает вас вниз по линии, и \r возвращает вас к началу строки.


мне просто пришлось играть с диво 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) -like loop, и я хочу разрешить несколько прядильщиков одновременно, я должен был spawn отдельная нить для обработки спиннинга. А это значит, что код должен быть намного сложнее.

кроме того, я не сделал так много многопоточности, поэтому возможно (вероятно, даже), что я оставил там тонкую ошибку или три. Но, похоже, до сих пор это работает довольно хорошо:

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

явное использование возврата Carrage (\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();    
}

из консольных документов в MSDN:

Вы можете решить эту проблему путем установки для textwriter.Свойство NewLine Свойство Out или Error для другой строки строка завершения. Например, С заявлением о#, консоли.Ошибка.новая строка= "\r\n\r\n";, устанавливает окончание строки строка для вывода стандартной ошибки поток к возвращению и линии экипажа 2 последовательности подачи. Тогда вы можете явно вызывать метод WriteLine объекта потока вывода ошибок, как В С# , Приставка.Ошибка.WriteLine();

Так я сделал это:

Console.Out.Newline = String.Empty;

тогда я могу контролировать выход сам;

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

другой способ добраться туда.


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

вот мой взгляд на ответы 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();
    }
}

использование-это что-то вроде этого. программа класса {

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

    }

Если вы хотите обновить одну строку, но информация слишком длинная, чтобы показать на одной строке, может потребоваться несколько новых строк. Я столкнулся с этой проблемой, и ниже приведен один из способов ее решения.

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

    }
}

Я искал то же решение в 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

вот еще один :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]);
        }
    }
}

на SetCursorPosition метод работает в многопоточном сценарии, где два других метода не