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

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

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

Ответ 1

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

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

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

Ответ 2

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

Ответ 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);
}

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

  • Backspace: 25,0 секунд
  • Возврат каретки: 28,7 секунд
  • SetCursorPosition: 49,7 секунд

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


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

Ответ 4

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

Ответ 5

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

Ответ 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();
    }
}

Ответ 7

Явно использую Return (Carr 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();    
}

Ответ 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 Out или Error в другую строку строка завершения. Например, С#, Console.Error.NewLine = "\ r\n\r\n"; устанавливает прекращение строки строка для стандартного вывода ошибки поток на два возврата каретки и линию последовательности подачи. Тогда ты можешь явно вызвать метод WriteLine объекта потока выходных данных ошибки, так как в заявлении С# Console.Error.WriteLine();

Итак - я сделал это:

Console.Out.Newline = String.Empty;

Затем я могу сам управлять выходом;

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

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

Ответ 10

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

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

    }
}

Ответ 11

Вот мои ответы на 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();
    }
}
Использование

- вот что.   классная программа   {

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

    }

Ответ 12

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

Ответ 13

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

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

Ответ 14

Это работает, если вы хотите, чтобы генерирующие файлы выглядели круто.

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

Ответ 15

Здесь еще один: D

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

Ответ 16

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