Как я могу обновить текущую строку в C # Windows Console App?

458

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

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

  • 0
    Если вы ДЕЙСТВИТЕЛЬНО заинтересованы в классных интерфейсах командной строки, вы должны проверить curses / ncurses.
  • 0
    @CharlesAddis, но не работает ли curses / ncurses только в C ++?
Теги:
console

15 ответов

671
Лучший ответ

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

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

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

  • 6
    для (int i = 0; i <= 100; ++ i) перейдет на 100%
  • 11
    Как вы справляетесь, когда предыдущая запись была длиннее новой? Есть ли какой-нибудь способ получить ширину консоли и заполнить строку пробелами?
Показать ещё 9 комментариев
235

Вы можете использовать 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.
  • 0
    Слишком сложно, да. Большая гибкость, да. Ответить на вопрос, не совсем так. Отличный ресурс, хотя, как вы говорите, пользовательский индикатор выполнения и т. Д ...
Показать ещё 13 комментариев
73

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

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
    Эффективность рассматриваемой операции действительно не должна иметь значения. Все должно происходить слишком быстро, чтобы пользователь мог это заметить. Ненужная микроптимизация - это плохо.
  • 0
    @Malfist: В зависимости от длины цикла пользователь может заметить или не заметить. Как я добавил в редактировании выше (до того, как увидел ваш комментарий), SetCursorPosition представил мерцание и занимает почти вдвое больше времени, чем другие параметры.
Показать ещё 5 комментариев
23

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

  • 1
    +1, это оказывается самый быстрый из представленных методов (см. Мой тестовый комментарий ниже)
15

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

  • 2
    Мне всегда было интересно, почему \ n \ r. Спасибо за эту информацию.
  • 12
    За исключением того, что это на самом деле \ r \ n.
12

Мне просто пришлось играть с классом 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();
    }
}
  • 0
    Хорошая модификация, хотя пример кода не мой. Это взято из блога Брэда Абрамса (см. Ссылку в моем ответе). Я думаю, что он был написан как простой пример, демонстрирующий SetCursorPosition. Между прочим, я определенно удивлен (в положительном смысле) начавшейся дискуссии о том, что я считаю простым образцом. Вот почему я люблю этот сайт :-)
4

Явно использую 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();    
}
  • 0
    -1, Вопрос требует C #, я переписываю его в C #, а вы меняете его обратно на F #
  • 0
    Это похоже на конфликт редактирования, а не на то, что он меняет ваш C # на F #. Его изменение было через минуту после твоего, и сосредоточено на спринте.
Показать ещё 3 комментария
2
    public void Update(string data)
    {
        Console.Write(string.Format("\r{0}", "".PadLeft(Console.CursorLeft, ' ')));
        Console.Write(string.Format("\r{0}", data));
    }
2

Из документов консоли в 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:");

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

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

Я искал такое же решение в 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
  • 0
    Здесь вы можете использовать функцию VB локальной статической переменной: Static intLastLength As Integer .
0

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

0

Вот мои ответы на 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.");

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

    }
}
-1

Здесь еще один: 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]);
        }
    }
}

Ещё вопросы

Сообщество Overcoder
Наверх
Меню