¿Cómo puedo actualizar la línea actual en una aplicación de Consola de Windows de C#?


Al crear una aplicación de consola de Windows en C#, ¿es posible escribir en la consola sin tener que extender una línea actual o ir a una nueva línea? Por ejemplo, si quiero mostrar un porcentaje que representa lo cerca que está un proceso de finalización, me gustaría actualizar el valor en la misma línea que el cursor, y no tener que poner cada porcentaje en una nueva línea.

¿Se puede hacer esto con una aplicación de consola C#" estándar"?

Author: GEOCHET, 2009-05-20

14 answers

Si imprime solo "\r" en la consola, el cursor vuelve al principio de la línea actual y luego puede reescribirla. Esto debería hacer el truco:

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

Observe los pocos espacios después del número para asegurarse de que lo que estaba allí antes se borra.
También observe el uso de Write() en lugar de WriteLine() ya que no desea agregar un "\n" al final de la línea.

 646
Author: shoosh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-12-15 15:31:38

Puede usar Console.SetCursorPosition para establecer la posición del cursor y luego escribir en la posición actual.

Aquí hay un ejemplo mostrando un simple "spinner":

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

Tenga en cuenta que tendrá que asegurarse de sobrescribir cualquier salida existente con una nueva salida o espacios en blanco.

Actualización: Ya que se ha criticado que el ejemplo mueve el cursor solo hacia atrás por un carácter, agregaré esto para aclarar: Usando SetCursorPosition puede establecer el cursor en cualquier posición en la ventana de la consola.

Console.SetCursorPosition(0, Console.CursorTop);

Pondrá el cursor al principio de la línea actual (o puede usar Console.CursorLeft = 0 directamente).

 219
Author: Dirk Vollmar,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-10-06 08:08:43

Hasta ahora tenemos tres alternativas competitivas para cómo hacer esto:

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
}

Siempre he usado Console.CursorLeft = 0, una variación de la tercera opción, así que decidí hacer algunas pruebas. Aquí está el código que usé:

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

En mi máquina, obtengo los siguientes resultados:

  • Espacios atrás: 25.0 segundos
  • Retornos de carro: 28.7 segundos
  • setCursorPosition: 49,7 segundos

Además, SetCursorPosition causó un parpadeo notable que no observé con ninguna de las alternativas. Entonces, la moraleja es usar backspaces o retornos de carro cuando sea posible, y gracias por enseñarme una forma más rápida de hacer esto, ¡ASÍ que!


Update: En los comentarios, Joel sugiere que setCursorPosition es constante con respecto a la distancia movida mientras que los otros métodos son lineales. Pruebas adicionales confirman que este es el caso, sin embargo el tiempo constante y lento sigue siendo lento. En mi pruebas, escribir una larga cadena de backspaces en la consola es más rápido que setCursorPosition hasta alrededor de 60 caracteres. Así que el retroceso es más rápido para reemplazar porciones de la línea más cortas que 60 caracteres (o así), y no parpadea, así que voy a mantener mi respaldo inicial de \b sobre \r y SetCursorPosition.

 68
Author: Kevin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-01-30 14:20:37

Puede usar la secuencia de escape \b (retroceso) para respaldar un número particular de caracteres en la línea actual. Esto solo mueve la ubicación actual, no elimina los caracteres.

Por ejemplo:

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

Aquí, line es la línea porcentual a escribir en la consola. El truco es generar el número correcto de caracteres \b para la salida anterior.

La ventaja de esto sobre el enfoque \r es que si funciona incluso si su porcentaje de salida no está al principio de la línea.

 24
Author: Sean,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-05-20 15:36:51

\r se utiliza para estos escenarios.
\r representa un retorno de carro que significa que el cursor vuelve al inicio de la línea.
Es por eso que Windows usa \n\r como nuevo marcador de línea.
\n te mueve por una línea, y \r te devuelve al comienzo de la línea.

 15
Author: Malfist,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-01-02 16:57:10

Solo tuve que jugar con la clase del divo ConsoleSpinner. El mío no es ni de lejos tan conciso, pero simplemente no me sentó bien que los usuarios de esa clase tengan que escribir su propio bucle while(true). Estoy buscando una experiencia más como esta:

static void Main(string[] args)
{
    Console.Write("Working....");
    ConsoleSpinner spin = new ConsoleSpinner();
    spin.Start();

    // Do some work...

    spin.Stop(); 
}

Y me di cuenta con el siguiente código. Dado que no quiero que mi método Start() se bloquee, no quiero que el usuario tenga que preocuparse por escribir un bucle similar a while(spinFlag), y quiero permitir varios spinners al mismo tiempo que tuve que generar un hilo separado para manejar el giro. Y eso significa que el código tiene que ser mucho más complicado.

Además, no he hecho mucho multi-threading por lo que es posible (probablemente incluso) que he dejado un error sutil o tres allí. Pero parece funcionar bastante bien hasta ahora:

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();
    }
}
 12
Author: Joel Coehoorn,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-05-20 19:37:19

Usar explícitamente un Carrage Return (\r) al principio de la línea en lugar de (implícita o explícitamente) usar una Nueva Línea (\n) al final debería obtener lo que desea. Por ejemplo:

void demoPercentDone() {
    for(int i = 0; i < 100; i++) {
        System.Console.Write( "\rProcessing {0}%...", i );
        System.Threading.Thread.Sleep( 1000 );
    }
    System.Console.WriteLine();    
}
 4
Author: James Hugard,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-05-20 18:41:13

Desde los documentos de consola en MSDN:

Puede resolver este problema configurando el TextWriter.Nueva línea propiedad de la Propiedad Out o Error a otra línea cadena de terminación. Por ejemplo, el Instrucción C#, Consola.Error.Nueva línea = "\r \ n\r \ n";, establece la terminación de la línea cadena para la salida de error estándar retorno de flujo a dos vagones y línea secuencias de alimentación. Entonces puedes llamar explícitamente al método WriteLine del objeto de flujo de salida de error, como en la C# instrucción, Consola.Error.WriteLine();

Así que hice esto:

Console.Out.Newline = String.Empty;

Entonces soy capaz de controlar la salida yo mismo;

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

Otra forma de llegar allí.

 2
Author: I Wanna Bea Programmer.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-06-17 18:45:11
    public void Update(string data)
    {
        Console.Write(string.Format("\r{0}", "".PadLeft(Console.CursorLeft, ' ')));
        Console.Write(string.Format("\r{0}", data));
    }
 2
Author: Jose,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-09-14 15:09:26

Aquí está mi opinión sobre las respuestas de s soosh y 0xA3. Puede actualizar la consola con mensajes de usuario mientras actualiza el spinner y también tiene un indicador de tiempo transcurrido.

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

El uso es algo así. programa de Clases {

    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
Author: cleftheris,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-10-01 21:13:33

Si desea actualizar una línea, pero la información es demasiado larga para mostrarse en una línea, puede necesitar algunas líneas nuevas. Me he encontrado con este problema, y a continuación hay una manera de resolver esto.

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
Author: lisunde,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-02-03 11:29:03

Estaba buscando la misma solución en vb.net y encontré este y es genial.

Sin embargo, como @JohnOdom sugirió una mejor manera de manejar el espacio en blanco si el anterior es más grande que el actual..

Hago una función en vb.net y pensé que alguien podría ser ayudado ..

Aquí está mi código:

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
Author: Zakir_SZH,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-12-28 13:07:47

Aquí hay otro: 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]);
        }
    }
}
 -1
Author: Tom,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-10-10 14:10:37

El método SetCursorPosition funciona en un escenario de subprocesos múltiples, donde los otros dos métodos no

 -1
Author: imgen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-04-12 02:50:53