Como posso atualizar a linha atual em um aplicativo de console do Windows em C #?


507

Ao criar um aplicativo de console do Windows em C #, é possível gravar no console sem precisar estender uma linha atual ou ir para uma nova linha? Por exemplo, se eu quiser mostrar uma porcentagem representando a proximidade de um processo, gostaria de atualizar o valor na mesma linha que o cursor, e não precisar colocar cada porcentagem em uma nova linha.

Isso pode ser feito com um aplicativo de console C # "padrão"?


Se você está realmente interessado em interfaces de linha de comando, você deve conferir curses / ncurses.
Charles Addis

@CharlesAddis, mas maldições / ncurses não funcionam apenas em C ++?
Xam

Respostas:


783

Se você imprimir apenas "\r"no console, o cursor retornará ao início da linha atual e poderá reescrevê-lo. Isso deve fazer o truque:

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

Observe os poucos espaços após o número para garantir que o que estava lá antes seja apagado.
Observe também o uso de em Write()vez de, WriteLine()pois você não deseja adicionar um "\ n" no final da linha.


7
para (int i = 0; i <= 100; ++ i) será 100% #
Nicolas Tyler

13
Como você lida com a gravação anterior mais longa que a nova gravação? Existe alguma maneira de obter a largura do console e preencher a linha com espaços, talvez?
Tirou Chapin

6
@druciferre Em cima da minha cabeça, posso pensar em duas respostas para sua pergunta. Ambos envolvem salvar a saída atual como uma string primeiro e preenchê-la com uma quantidade definida de caracteres como este: Console.Write ("\ r {0}", strOutput.PadRight (nPaddingCount, '')); O "nPaddingCount" pode ser um número definido por você ou você pode acompanhar a saída anterior e definir nPaddingCount como a diferença de comprimento entre a saída anterior e a atual, mais a duração da saída atual. Se nPaddingCount for negativo, você não precisará usar o PadRight, a menos que faça abdominais (prev.len - curr.len).
John Odom

1
Código bem organizado. Se qualquer uma das dezenas de threads puder gravar no console a qualquer momento, isso causará problemas, independentemente de você estar escrevendo novas linhas ou não.
Mark

2
@JohnOdom, você só precisa manter o comprimento da saída anterior (sem preenchimento) e alimentá-lo como o primeiro argumento para PadRight(salvar a sequência ou o comprimento sem preenchimento, é claro).
Jesper Matthiesen

254

Você pode usar Console.SetCursorPositionpara definir a posição do cursor e, em seguida, escrever na posição atual.

Aqui está um exemplo mostrando um simples "botão giratório":

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

Observe que você precisará substituir qualquer saída existente por uma nova saída ou espaços em branco.

Atualização: Como foi criticado que o exemplo move o cursor apenas de volta em um caractere, acrescentarei isso para esclarecimentos: SetCursorPositionVocê pode definir o cursor para qualquer posição na janela do console.

Console.SetCursorPosition(0, Console.CursorTop);

irá definir o cursor para o início da linha atual (ou você pode usar Console.CursorLeft = 0diretamente).


8
O problema pode ser resolvido usando \ r, mas usar SetCursorPosition(ou CursorLeft) permite mais flexibilidade, por exemplo, não escrever no início da linha, subir na janela, etc. barras de progresso personalizadas ou gráfico ASCII.
Dirk Vollmar

14
+1 por ser detalhado e ir além da chamada do dever. Coisas boas, obrigado.
2032 Copas

1
+1 por mostrar uma maneira diferente de fazer isso. Todos os outros mostraram \ r, e se o OP está simplesmente atualizando uma porcentagem, com isso, ele pode simplesmente atualizar o valor sem precisar reescrever a linha inteira. O OP nunca disse que queria ir para o início da linha, apenas que ele queria atualizar algo na mesma linha que o cursor.
209 Andy

1
A flexibilidade adicional de SetCursorPosition custa um pouco de velocidade e um cursor perceptível pisca se o loop for longo o suficiente para o usuário perceber. Veja meu comentário de teste abaixo.
217 Kevin

5
Confirme também que o comprimento da linha não faz com que o console se agrupe na próxima linha ou você pode ter problemas com o conteúdo que está sendo executado na janela do console de qualquer maneira.
Mandrake

84

Até agora, temos três alternativas concorrentes para fazer isso:

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
}

Eu sempre usei Console.CursorLeft = 0uma variação na terceira opção, então decidi fazer alguns testes. Aqui está o código que eu usei:

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

Na minha máquina, obtenho os seguintes resultados:

  • Backspaces: 25,0 segundos
  • Devoluções de carro: 28,7 segundos
  • SetCursorPosition: 49,7 segundos

Além disso, SetCursorPositioncausou cintilação notável que eu não observei com nenhuma das alternativas. Portanto, a moral é usar backspaces ou retornos de carro quando possível , e obrigado por me ensinar uma maneira mais rápida de fazer isso, ASSIM!


Atualização : nos comentários, Joel sugere que SetCursorPosition seja constante em relação à distância movida enquanto os outros métodos são lineares. Testes adicionais confirmam que esse é o caso, porém o tempo constante e lento ainda são lentos. Nos meus testes, gravar uma longa seqüência de backspaces no console é mais rápido que o SetCursorPosition até cerca de 60 caracteres. Portanto, o backspace é mais rápido para substituir partes da linha com menos de 60 caracteres (mais ou menos), e não pisca, por isso vou defender meu endosso inicial de \ b over \ re e SetCursorPosition.


4
A eficiência da operação em questão realmente não deveria importar. Tudo deve ocorrer muito rápido para o usuário perceber. Microptimização desnecessária é ruim.
Malfist 20/05/09

@ Malfist: Dependendo da duração do loop, o usuário pode ou não perceber. Como adicionei na edição acima (antes de ver seu comentário), o SetCursorPosition apresentou o flicker e leva quase o dobro do tempo das outras opções.
217 Kevin

1
Concordo que é uma micro-otimização (executá-lo um milhão de vezes e levar 50 segundos ainda é uma quantidade muito pequena de tempo), +1 para os resultados, e definitivamente poderia ser muito útil saber.
209 Andy

6
O benchmark é fundamentalmente falho. É possível que o tempo de SetCursorPosition () seja o mesmo, não importa a distância que o cursor se mova, enquanto as outras opções variam de acordo com o número de caracteres que o console precisa processar.
Joel Coehoorn

1
Este é um resumo muito bonito das diferentes opções disponíveis. No entanto, também vejo cintilação ao usar \ r. Com \ b obviamente não há oscilações porque o texto da correção ("Contando:") não é reescrito. Você também piscará se adicionar \ b adicional e reescrever o texto da correção, como está ocorrendo com \ be SetCursorPosition. Em relação à observação de Joel: Joel está basicamente certo, no entanto, ainda vai superar SetCursorPosition em linhas muito longas, mas a diferença fica menor.
Dirk Vollmar

27

Você pode usar a sequência de escape \ b (backspace) para fazer backup de um número específico de caracteres na linha atual. Isso apenas move a localização atual, não remove os caracteres.

Por exemplo:

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

Aqui, linha é a linha de porcentagem a ser gravada no console. O truque é gerar o número correto de caracteres \ b para a saída anterior.

A vantagem deste sobre o \ r abordagem é que, se funciona mesmo se a sua saída percentual não é no início da linha.


1
+1, este acaba por ser o método mais rápido apresentado (ver o meu comentário teste abaixo)
Kevin

19

\ré usado para esses cenários.
\r representa um retorno de carro, o que significa que o cursor retorna ao início da linha.
É por isso que o Windows usa \n\rcomo seu novo marcador de linha.
\nmove você para baixo de uma linha e \rretorna para o início da linha.


22
Exceto que é realmente \ r \ n.
Joel Mueller #

14

Eu só tive que brincar com a ConsoleSpinnerclasse do divo . O meu não é nem de longe tão conciso, mas simplesmente não me agradou que os usuários dessa classe tivessem que escrever seu próprio while(true)loop. Estou filmando para uma experiência mais como esta:

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

    // Do some work...

    spin.Stop(); 
}

E eu percebi isso com o código abaixo. Como não quero que meu Start()método bloqueie, não quero que o usuário tenha que se preocupar em escrever um while(spinFlag)loop semelhante e quero permitir vários giradores ao mesmo tempo em que tive que gerar um thread separado para lidar com o fiação. E isso significa que o código precisa ser muito mais complicado.

Além disso, eu não fiz muito multiencadeamento, então é possível (provavelmente até) que eu tenha deixado um bug sutil ou três lá. Mas parece funcionar muito bem até agora:

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

Boa modificação, embora o código de exemplo não seja meu. Foi retirado do blog de Brad Abrams (veja o link na minha resposta). Eu acho que acabou de ser escrito como um exemplo simples demonstrando SetCursorPosition. Aliás, estou definitivamente surpreso (de uma maneira positiva) com a discussão iniciada sobre o que eu pensava ser apenas uma amostra simples. É por isso que eu amo este site :-)
Dirk Vollmar

4

Usar explicitamente um retorno de carrage (\ r) no início da linha, em vez de (implicitamente ou explicitamente) usar uma nova linha (\ n) no final, deve ser o que você deseja. Por exemplo:

void demoPercentDone() {
    for(int i = 0; i < 100; i++) {
        System.Console.Write( "\rProcessing {0}%...", i );
        System.Threading.Thread.Sleep( 1000 );
    }
    System.Console.WriteLine();    
}

-1, Pergunta pede C #, eu reescrevê-lo em C # e você mudá-lo de volta para F #
Malfist

Parece mais um conflito de edição do que ele alterando seu C # de volta para F #. A mudança dele foi um minuto depois da sua e se concentrou no sprint.
209 Andy

Obrigado pela edição. Costumo usar o modo interativo F # para testar as coisas e percebi que as partes importantes eram as chamadas BCL, que são iguais em C #.
1126 James Hugard

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

1

Nos documentos do console no MSDN:

Você pode resolver esse problema, definindo a propriedade TextWriter.NewLine da propriedade Out ou Error como outra seqüência de terminação de linha. Por exemplo, a instrução C #, Console.Error.NewLine = "\ r \ n \ r \ n" ;, define a sequência de terminação de linha do fluxo de saída de erro padrão para duas seqüências de retorno de carro e avanço de linha. Em seguida, você pode chamar explicitamente o método WriteLine do objeto de fluxo de saída de erro, como na instrução C #, Console.Error.WriteLine ();

Então - eu fiz isso:

Console.Out.Newline = String.Empty;

Então eu sou capaz de controlar a saída sozinho;

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

Outra maneira de chegar lá.


Você poderia usar apenas Console.Write () para o mesmo fim, sem redefinindo a propriedade NewLine ...
Radosław Gers

1

Isso funciona se você quiser fazer a geração de arquivos parecer legal.

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

E este é o método que obtive de alguma resposta abaixo e a modifiquei

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

0

Aqui está outro: 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]);
        }
    }
}

0

Se você deseja atualizar uma linha, mas as informações são muito longas para serem exibidas em uma linha, podem ser necessárias novas linhas. Eu encontrei esse problema, e abaixo está uma maneira de resolver isso.

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

Eu estava procurando a mesma solução em vb.net e encontrei este e é ótimo.

no entanto, como @JohnOdom sugeriu uma maneira melhor de lidar com o espaço em branco se o anterior for maior que o atual.

Eu faço uma função no vb.net e pensei que alguém poderia ser ajudado ..

aqui está o meu 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

Aqui você pode usar o recurso de VB de uma variável estática local: Static intLastLength As Integer.
Mark Hurd

0

Eu estava fazendo uma pesquisa para ver se a solução que escrevi poderia ser otimizada para velocidade. O que eu queria era um cronômetro de contagem regressiva, não apenas atualizando a linha atual. Aqui está o que eu criei. Pode ser útil para alguém

            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

Inspirado pela @ E.Lahu Solution, a implementação de uma barra progride com porcentagem.

public class ConsoleSpinner
{
    private int _counter;

    public void Turn(Color color, int max, string prefix = "Completed", string symbol = "■",int position = 0)
    {
        Console.SetCursorPosition(0, position);
        Console.Write($"{prefix} {ComputeSpinner(_counter, max, symbol)}", color);
        _counter = _counter == max ? 0 : _counter + 1;
    }

    public string ComputeSpinner(int nmb, int max, string symbol)
    {
        var spinner = new StringBuilder();
        if (nmb == 0)
            return "\r ";

        spinner.Append($"[{nmb}%] [");
        for (var i = 0; i < max; i++)
        {
            spinner.Append(i < nmb ? symbol : ".");
        }

        spinner.Append("]");
        return spinner.ToString();
    }
}


public static void Main(string[] args)
    {
        var progressBar= new ConsoleSpinner();
        for (int i = 0; i < 1000; i++)
        {
            progressBar.Turn(Color.Aqua,100);
            Thread.Sleep(1000);
        }
    }

0

Aqui está a minha opinião sobre as respostas de s soosh e 0xA3. Ele pode atualizar o console com mensagens do usuário enquanto atualiza o girador e também possui um indicador de tempo decorrido.

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

uso é algo como isto:

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

    }
}

-1

O SetCursorPositionmétodo funciona no cenário de multiencadeamento, onde os outros dois métodos não

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.