Как я могу обновить текущую строку в C # Windows Console App? - PullRequest
457 голосов
/ 20 мая 2009

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

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

Ответы [ 15 ]

714 голосов
/ 20 мая 2009

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

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

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

234 голосов
/ 20 мая 2009

Вы можете использовать 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 напрямую).

75 голосов
/ 20 мая 2009

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

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 символов. Так что backspace быстрее для замены частей строки короче 60 символов (или около того), и не мерцает, поэтому я буду придерживаться моего первоначального одобрения \ b над \ r и SetCursorPosition.

26 голосов
/ 20 мая 2009

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

Например:

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 заключается в том, что if работает, даже если ваш процентный вывод находится не в начале строки.

16 голосов
/ 20 мая 2009

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

14 голосов
/ 20 мая 2009

Мне просто нужно было поиграть с классом диво 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();
    }
}
4 голосов
/ 20 мая 2009

Явное использование возврата каретки (\ 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();    
}
2 голосов
/ 14 сентября 2012
    public void Update(string data)
    {
        Console.Write(string.Format("\r{0}", "".PadLeft(Console.CursorLeft, ' ')));
        Console.Write(string.Format("\r{0}", data));
    }
1 голос
/ 17 июня 2011

Из документации консоли в MSDN:

Вы можете решить эту проблему, установив свойство TextWriter.NewLine объекта Свойство Out или Error в другую строку строка завершения. Например, Оператор 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:");

Еще один способ добраться туда.

0 голосов
/ 08 ноября 2018

Я искал это, чтобы посмотреть, может ли написанное мной решение оптимизироваться по скорости. Я хотел таймер обратного отсчета, а не просто обновление текущей строки. Вот что я придумал. Может быть кому-то пригодится

            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("");
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...