Não tem nada mais frustrante para o usuário quando este precisa realizar alguma operação e o programa simplesmente não responde: não se sabe se o sistema simplesmente travou ou se ainda está realizando a operação. Aqueles mais afoitos não pensam duas vezes e já vão teclando o Ctrl+Alt+Del para finalizar a tarefa. O grande problema disso é que podem ocorrer danos de integridade aos dados da aplicação, daí já vem o usuário solicitar o suporte e reparação dos problemas. Tudo por que o desenvolvedor deixou de aplicar um recurso simples e eficaz, capaz de diminuir jconsideravelmente os esforços de suporte e reparo: a barra de progresso.

Parece algo simples, mas certamente em algum momento de suas carreiras, muitos programadores já perderam algum tempo ao tentar implementar uma barra de progresso de forma eficiente.

O uso da barra é basicamente informativo e tem a função de mostrar para o usuário da aplicação que uma determinada tarefa está sendo executada corretamente. Seja para exibir o progresso de upload/download de dados, leitura de um extenso arquivo XML ou qualquer outra tarefa que demande tempo em sua execução. No caso de transmissão de dados ou a leitura de algum arquivo qualquer, é possível o programador mensurar qual a real progressão da tarefa e, nesse contexto, poderá incrementar a barra de progresso conforme o status da transmissão ou o status da leitura do arquivo.

Em outros casos é quase impossível prever qual a real progressão da tarefa, podendo o programador implementar uma barra de progresso no formato de progressão indeterminada.

De forma resumida, o uso da ProgressBar é uma forma respeitosa de informar ao usuário que o procedimento é demorado, que o programa não travou e que a tarefa está sendo executada corretamente.

O Visual Studio possui uma ferramenta especialmente desenvolvida para casos em que se exige a execução de duas tarefas concomitantes: o backGroundWorker. Podemos exibir as operações da thread da interface do usuário como a barra de progresso em movimento, por exemplo, enquanto tiramos proveito do backGroundWorker para realizar uma tarefa importante em segundo plano, como mostra a Figura 1.

Execução de threads simultâneos
Figura 1. Execução de threads simultâneos

Dicas de quando usar uma ProgressBar

  • Não utilizamos barras de progresso em processos que demore pouquíssimos segundos para finalizar, pois não há produtividade nenhuma nisso;
  • Em uma tarefa que existe muitos processos conhecidos, evite criar uma barra de progresso para cada processo. Prefira sempre criar apenas uma barra de progresso para toda a tarefa. Várias causam a mesma sensação de que o programa nunca vai parar de ser executado, pois o usuário nunca vai saber quantas barras de progresso serão executadas até a operação ser realmente terminada. Se preferir, crie duas barras de progresso, sendo uma para a tarefa principal e outra para cada processo contidos nela;
  • É importante que coloquemos os valores do progresso de acordo com a evolução real da execução do processo. Nunca valores utilizar valores aleatórios, o que causa um efeito nada profissional da aplicação;
  • Caso não seja conhecido o final do processo, utilizamos uma barra de status na modalidade indeterminada e, caso a tarefa seja muito demorada, o programador pode se utilizar de um truque simples para informar que os procedimentos estão sendo realizados usando uma simples label com o status da realização de um dos processos contidos na tarefa.

Mãos à obra

Nesse exemplo vamos utilizar um formulário com três buttons, uma progressbar, duas labels e dois componentes backgroundWorker, conforme ilustra a Figura 2.

Formulário exemplo
Figura 2. Formulário exemplo

Vamos criar o método que irá simular uma tarefa real muito longa, que em tese demandaria muito tempo para ser terminada, como mostra a Listagem 1.


/// <summary>
/// Simula uma tarefa que levará muito tempo para ser executada.
/// </summary>
/// <param name="tempo"> Um inteiro que representa o tempo a ser esperado.</param>
private void TarefaLonga(int p)
{
    for (int i = 0; i <= 10; i++)
    {
        // faz a thread dormir por "p" milissegundos a cada passagem do loop
        Thread.Sleep(p);
        label2.Text = "Tarefa: " + i.ToString() + " comcluída";
    }
}
Listagem 1. Método que simula uma tarefa qualquer

Utilizando a ProgressBar para um processo com fim definido

Pressupõe-se, neste caso, que é sabido quando o processo irá terminar. Exemplo: a leitura das linhas de um arquivo de texto onde se saiba o número total de linhas. Nesse caso, basta configurar a propriedade Maximum da progressbar para corresponder o número de total de linhas do arquivo e incrementar o progresso conforme as linhas são lidas.

Para isso configure o backgroundWorker1 conforme se segue:

Propriedades do componente backgroundWorker1\

Iremos habilitar as propriedades WorkerReportProgress e WorkerSupportsCancellation, conforme a Figura 3. A primeira propriedade basicamente relata o status da execução da tarefa, enquanto que a segunda propriedade oferece suporte ao cancelamento da operação.

Propriedades do backgroundWorker1
Figura 3. Propriedades do backgroundWorker1

Eventos do componente backgroundWorker1

Vamos dar um duplo clique nos seguintes eventos do componente backgroundWorker1, como mostra a Figura 4.

Eventos do backgroundWorker1
Figura 4. Eventos do backgroundWorker1

Isso vai gerar os respectivos métodos para a manipulação de uma tarefa em segundo plano. (Não vamos aqui entrar no mérito dos “Threading” em tarefas Assíncronas).

É no evento DoWork que chamamos a tarefa ou as tarefas demoradas a serem executadas em segundo plano. É aqui que vamos chamar o método TarefaLonga().

No evento ProgressChanged é que a mudança do progresso é incrementada ou decrementada e faremos nossa barra de progresso se mover aqui.

Por último, no evento RunWorkerCompleted é que verificamos que fim levou a operação. Se foi cancelada, se ocorreu algum erro ou se tudo ocorreu conforme o esperado. É aqui que programamos o que deve acontecer em cada um dos casos, ou seja, como o sistema vai se comportar após a operação.

Abaixo, na Listagem 2, temos os métodos gerados pelos eventos.


/// <summary>
/// //Aqui chamamos os nossos metodos com as tarefas demoradas.
/// </summary>
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) 
{
    for (int i = 0; i <100; i++)//representa uma tarefa com 100 processos.
    {
        //Executa o método longo 100 vezes.
        TarefaLonga(20);
        //incrementa o progresso do backgroundWorker 
        //a cada passagem do loop.
        this.backgroundWorker1.ReportProgress(i);
 
        //Verifica se houve uma requisição para cancelar a operação.
        if (backgroundWorker1.CancellationPending)
        {
            //se sim, define a propriedade Cancel para true
            //para que o evento WorkerCompleted saiba que a tarefa foi cancelada.
            e.Cancel = true;
 
            //zera o percentual de progresso do backgroundWorker1.
            backgroundWorker1.ReportProgress(0);
            return;
        }
    }
    //Finalmente, caso tudo esteja ok, finaliza
    //o progresso em 100%.
    backgroundWorker1.ReportProgress(100);
}
 
/// <summary>
/// Aqui implementamos o que desejamos fazer enquanto o progresso
/// da tarefa é modificado,[incrementado].
/// </summary>
private void backgroundWorker1_ProgressChanged(object sender, 
ProgressChangedEventArgs e)
{
    //Incrementa o valor da progressbar com o valor
    //atual do progresso da tarefa.
    progressBar1.Value = e.ProgressPercentage;
 
    //informa o percentual na forma de texto.
    label1.Text = e.ProgressPercentage.ToString() + "%";
}
 
/// <summary>
/// Após a tarefa ser concluida, esse metodo e chamado para
/// implementar o que deve ser feito imediatamente após a conclusão da tarefa.
/// </summary>
private void backgroundWorker1_RunWorkerCompleted(object sender, 
RunWorkerCompletedEventArgs e)
{
    if (e.Cancelled)
    {   
        //caso a operação seja cancelada, informa ao usuario.
        label2.Text = "Operação Cancelada pelo Usuário!";
 
        //habilita o Botao cancelar
        btnCancelar.Enabled = true;
        //limpa a label
        label1.Text = string.Empty;
    }else if (e.Error !=null)
            {
                        //informa ao usuario do acontecimento de algum erro.
        label2.Text = "Aconteceu um erro durante a execução do processo!";
            }else{
        //informa que a tarefa foi concluida com sucesso.
        label2.Text = "Tarefa Concluida com sucesso!";
    }
    //habilita os botões.
    btnTarefaDeterminada.Enabled = true;
    btnTarefaIndeterminada.Enabled = true;
}
Listagem 2. Implementação dos Eventos do backgroundWorker1

Chamando a tarefa de forma Assíncrona

Dê um duplo clique no botão “Rodar Processo” e implemente o código da Listagem 3.


private void btnTarefaDeterminada_Click(object sender, EventArgs e)
{
    //desabilita os botões enquanto a tarefa é executada
    btnTarefaIndeterminada.Enabled = false;
    btnTarefaIndeterminada.Enabled = false;
            
    //define o stilo padrao do progressbar
    progressBar1.Style = ProgressBarStyle.Blocks;
    progressBar1.Value = 0;
 
    //executa o processo de forma assincrona.
    backgroundWorker1.RunWorkerAsync();
}
Listagem 3. Código do btnTarefaDeterminada

Observe que na Listagem 3 chamamos toda a execução da tarefa de forma assíncrona através do método RunWorkerAsync(), que é o gatilho que dispara toda a operação.

Agora vamos dar um duplo clique no botão “Cancelar” e implementar o código da Listagem 4.


private void btnCancelar_Click(object sender, EventArgs e)
{
  //Cancelamento da tarefa com fim determinado [backgroundWorker1]
  if (backgroundWorker1.IsBusy)//se o backgroundWorker1 estiver ocupado
   {
       // notifica a thread que o cancelamento foi solicitado.
       // Cancela a tarefa DoWork 
       backgroundWorker1.CancelAsync();
    }
     //desabilita o botão cancelar.
     btnCancelar.Enabled = false;
     label1.Text = "Cancelando...";
}
Listagem 4. Código do botão btnCancelar

Nesta listagem, o método CancelAsync() é chamado solicitando o cancelamento da operação.

Executando o Programa

Ao executar o programa e clicar no botão “Rodar Processo” veremos um horrível erro de execução, como segue a Figura 5 abaixo.

Erro de Execução
Figura 5. Erro de Execução

Isso ocorre por que estamos tentando acessar um processo contido em uma thread diferente da thread que está sendo executada. Para corrigir isso invocamos o processo utilizando o método BeginInvoke daquele processo. Simplificando, o BeginInvoke() executa simultaneamente procedimentos entre threads diferentes.

Vamos alterar o código do método TarefaLonga() conforme a Listagem 5.


private void TarefaLonga(int p)
{
    for (int i = 0; i <= 10; i++)
    {
        // faz a thread dormir por "p" milissegundos a cada passagem do loop
        Thread.Sleep(p);
        label2.BeginInvoke(
           new Action(() =>
           {
               label2.Text = "Tarefa: " + i.ToString() + " comcluída";
           }
        ));                
    }
}
Listagem 5. Invocando um processo de outro thread

Executamos novamente o programa e ao clicar em “Rodar Processo”, temos a Figura 6.

Executando a tarefa
Figura 6. Executando a tarefa

Ao Cancelar Temos a Figura 7.

Operação cancelada
Figura 7. Operação cancelada

E ao concluir tem-se a Figura 8.

Progresso concluído
Figura 8. Progresso concluído

Utilizando progressBar para executar uma tarefa de conclusão não definida

Quando não se sabe ao certo qual o ritmo de progressão de uma tarefa, ou não se sabe quanto tempo ela vai demorar para ser finalizada utilizamos a progressBar em modo indeterminado. A barra de progresso não irá progredir, apenas exibirá uma animação que mostra que o programa não travou.

Para isso, definimos a propriedade ProgressBarStyle do componente progressBar para marquee. Além disso, precisamos configurar as seguintes propriedades:

Propriedades do componente bgWorkerIndeterminada

Habilite as seguintes propriedades, conforme a Figura 9.

Propriedade da bgWorkerIndeterminada
Figura 9. Propriedade da bgWorkerIndeterminada

Neste caso, apenas o suporte ao cancelamento deve ser ativado, já que a barra de progresso não será incrementada.

Eventos do componente bgWorkerIndeterminada

Dê um duplo clique para gerar os métodos dos seguintes eventos, presentes na Figura 10.

Eventos da bgWorkerIndeterminada
Figura 10. Eventos da bgWorkerIndeterminada

Observe que o evento ProgressChanged não foi implementado. Neste caso, esse evento não é necessário, pois não carregará a barra de progresso.

Vamos implementar os métodos gerados, como mostra a Listagem 6.


private void bgWorkerIndeterminada_DoWork(object sender, DoWorkEventArgs e)
{
  //executa a tarefa a primeira vez
  TarefaLonga(100);
  //Verifica se houve uma requisição para cancelar a operação.
  if (bgWorkerIndeterminada.CancellationPending)
  {
      //se sim, define a propriedade Cancel para true
      //para que o evento WorkerCompleted saiba que a tarefa foi cancelada.
      e.Cancel = true;
      return;
  }

  //executa a tarefa pela segunda vez
  TarefaLonga(500);
  if (bgWorkerIndeterminada.CancellationPending)
  {
      //se sim, define a propriedade Cancel para true
      //para que o evento WorkerCompleted saiba que a tarefa foi cancelada.
      e.Cancel = true;
      return;
  }
}

private void bgWorkerIndeterminada_RunWorkerCompleted(object sender, 
RunWorkerCompletedEventArgs e)
{  
  //Caso cancelado...
  if (e.Cancelled)
  {
      // reconfigura a progressbar para o padrao.
      progressBar1.MarqueeAnimationSpeed = 0;
      progressBar1.Style = ProgressBarStyle.Blocks;
      progressBar1.Value = 0;

      //caso a operação seja cancelada, informa ao usuario.
      label2.Text = "Operação Cancelada pelo Usuário!";

      //habilita o botao cancelar
      btnCancelar.Enabled = true;
      //limpa a label
      label1.Text = string.Empty;
  }
  else if (e.Error != null)
  {
      //informa ao usuario do acontecimento de algum erro.
      label2.Text = "Aconteceu um erro durante a execução do processo!";

      // reconfigura a progressbar para o padrao.
      progressBar1.MarqueeAnimationSpeed = 0;
      progressBar1.Style = ProgressBarStyle.Blocks;
      progressBar1.Value = 0;
  }
  else
  {
      //informa que a tarefa foi concluida com sucesso.
      label2.Text = "Tarefa Concluida com sucesso!";
              
      //Carrega todo progressbar.
      progressBar1.MarqueeAnimationSpeed = 0;
      progressBar1.Style = ProgressBarStyle.Blocks;
      progressBar1.Value = 100;
      label1.Text = progressBar1.Value.ToString() + "%";
  }
  //habilita os botões.
  btnTarefaDeterminada.Enabled = true;
  btnTarefaIndeterminada.Enabled = true;
}
Listagem 6. Métodos dos eventos bgWorkerIndeterminada

Podemos observar que na codificação apresentada na implementação do evento DoWork, chamamos o método TarefaLonga() por duas vezes: isso foi realizado propositalmente com fins didáticos para que o leitor compreenda que uma tarefa, após ser iniciada, não pode ser cancelada. O DoWork inicia e termina um procedimento e, logo após, inicia e termina o outro até terminar toda a thread. Nesse intervalo o DoWork verifica se existe um pedido de cancelamento pendente, em caso positivo, o sistema deixa de executar as demais tarefas.

Assim, caso o programador julgue que o usuário possa, em algum momento, cancelar a operação, o programador pode chamar o cancelamento entre as tarefas, o que dá certo controle ao programador, o que evita, dentre outros problemas, a inconsistência e fragmentação de dados do aplicativo. E ainda, se necessário, o desenvolvedor pode reverter as alterações até então realizadas por meio de codificação no evento RunWorkerCompleted, caso a operação seja efetivamente cancelada pelo usuário.

Dê um duplo clique no botão “Rodar Processo com Finalização Indeterminada” e implemente o seguinte código da Listagem 7.


private void btnTarefaIndeterminada_Click(object sender, EventArgs e)
{
    //desabilita os botões enquanto a tarefa é executada.
    btnTarefaDeterminada.Enabled = false;
    btnTarefaIndeterminada.Enabled = false;
    bgWorkerIndeterminada.RunWorkerAsync();
 
    //define a progressBar para Marquee
    progressBar1.Style = ProgressBarStyle.Marquee;
    progressBar1.MarqueeAnimationSpeed = 5;
 
    //informa que a tarefa esta sendo executada.
    label1.Text = "Processando...";
}
Listagem 7. Chamando tarefa com processo indeterminado

Como podemos observar, modificamos o style da progressBar para Marquee e também configuramos a velocidade da animação para 5, que é uma animação bem lenta.

Edite o código do botão “Cancelar” conforme a Listagem 8.


private void btnCancelar_Click(object sender, EventArgs e)
{
    //Cancelamento da tarefa com fim determinado [backgroundWorker1]
    if (backgroundWorker1.IsBusy)//se o backgroundWorker1 estiver ocupado
    {
        // notifica a thread que o cancelamento foi solicitado.
        // Cancela a tarefa DoWork 
        backgroundWorker1.CancelAsync();
    }
 
    //Cancelamento da tarefa com fim indeterminado [bgWorkerIndeterminada]
    if (bgWorkerIndeterminada.IsBusy)
    {
        // notifica a thread que o cancelamento foi solicitado.
        // Cancela a tarefa DoWork 
        bgWorkerIndeterminada.CancelAsync();
    }
 
    //desabilita o botão cancelar.
    btnCancelar.Enabled = false;
    label1.Text = "Cancelando...";
}
Listagem 8. Editando o Botão Cancelar

Executando o programa

Execute o programa e clique no botão “Rodar Processo com Finalização Indeterminada”. Veremos o mesmo resultado da Figura 11.

Tarefa com execução indeterminada
Figura 11. Tarefa com execução indeterminada

Ao concluir a tarefa tem-se a Figura 12.

Tarefa concluída
Figura 12. Tarefa concluída

Ao clicar em Cancelar tem-se a Figura 13.

Tarefa cancelada
Figura 13. Tarefa cancelada

Atenção! A atomicidade da operação depende exclusivamente da programação do desenvolvedor. Lembrando que uma vez iniciada uma tarefa dentro do DoWork ela será concluída. O cancelamento consiste em não executar as demais tarefas do processo como um todo, no momento que método CancelAsync() é invocado. Mais uma vez, depende do programador reverter ou não as modificações até então realizadas.

Para quê complicar? De fato o que parece ser algo simples é realmente muito simples de ser feito, o que não deixa de ser trivial. Da próxima vez que precisarmos implementar uma barra de progresso, utilizaremos a solução nativa do Visual Studio, o backGroundWorker. Uma solução bem simples, rápida, com satisfação garantida do usuário final e, o melhor de tudo, com todos os recursos e vantagens do .NET Framework.