Como usar corretamente uma barra de progresso (TProgressBar)?

Categoria: Artigos
Categoria Pai: Addicted 2 Delphi!
Acessos: 15909
Imagem meramente ilustrativa

Quando comecei a programar, uma das coisas mais legais e que me chamavam sempre a atenção eram as barras de progresso. Eu não sei porque, mas eu sempre olhava a barrinha enchendo e achava muito engenhoso. No início, eu queria porque queria, que meus projetos utilizassem aquilo e cheguei até mesmo a utilizar uma barra de progresso na inicialização de meus programas, na tela de splash, para indicar o progresso do carregamento de cada um dos TForm que eram criados automaticamente. Aboli este uso quando descobri que não era correto carregar todos os TForm na inicialização e foi aí que eu entendi que uma barra de progresso só faz sentido quando ela é realmente necessária, ou seja, não adianta forçar seu uso apenas para tornar seu programa mais legal de alguma forma.

Use uma barra de progresso APENAS quando seu programa precisar indicar ao usuário o andamento de uma tarefa realmente demorada da qual você conheça o evento no qual o processamento termina. Este "evento" pode ser qualquer coisa que seja representada por um número. O processamento dos bytes de um arquivo de tamanho conhecido, o processamento de arquivos num diretório quando você previamente sabe a quantidade de arquivos, a validação de um arquivo XML com um arquivo XSD, desde que você obtenha previamente a quantidade total de nós dentro do XML, etc., são exemplos de processamento onde uma barra de progresso pode ser usada. Sua utilização é válida também quando você não conhece o evento final, mas tem meios de conseguí-lo de forma muito rápida, por exemplo, é possível contar de forma relativamente rápida a quantidade de arquivos dentro de uma estrutura de diretórios, dada uma raiz inicial, recursivamente ou mesmo a quantidade total de nós dentro de um arquivo XML, usando XPATH. O exemplo anexado a este artigo, o qual vou explicar passo-a-passo, faz uso da contagem prévia de arquivos dentro de uma estrutura de diretórios recusivamente, e em um teste ele contou 300.000 arquivos em pouco menos de 10 segundos.

A regra básica é: se você não tiver condições de saber o evento no qual o processamento termina antes do processamento começar ou se a obtenção deste evento for muito complexa ou demorada, então uma barra de progresso não se aplica e você deve informar ao usuário que algo está acontecendo, mas que não tem meios de dizer quando vai acabar. Normalmente nesse caso, se usa uma animação qualquer para indicar que algo está sendo feito em segundo plano.

A seguir vou começar a explicar como converter uma rotina que realiza um processamento lento para que ela possa ser usada com uma barra de progresso da forma correta. Quero salientar que todos os procedimentos que eu vou explicar foram criados por mim para diminuir a complexidade dessa conversão e que existem outros meios de se fazer a mesma coisa. Por exemplo, aqui eu recomendo que as rotinas demoradas sejam agrupadas em um método simples que faz todo o processamento, mas isso não é requerido, apenas escolhi esse meio porque EU achei mais fácil. O importante é entender o que está sendo feito e seguir minha recomendação ou criar seu próprio modo de fazer a mesma coisa. Fica a seu critério, caro leitor.


Uma thread, claro!

Não existe mágica alguma, quando eu falei que era mais complexo usar uma barra de progresso da forma correta, era disso que eu estava falando: processos demorados devem ser postos em uma thread. Não são todos que tem paciência ou mesmo conhecimento para criar threads da forma correta, quanto mais uma apenas para fazer funcionar uma simples barra de progresso. Muitos de nós, eu me incluía neste meio, preferimos usar Application.ProcessMessages e obter o mesmo efeito, contudo, esta sensação de que está tudo bem, é falsa! O uso indiscriminado de ProcessMessages pode causar problemas difíceis de debugar, access violations inesperados e outros tipos de comportamentos bizarros em programas mais complexos. Já tive problemas em um programa que fazia uso de FTP assíncrono e só depois de muito penar, descobri que ele era causado pelos ProcessMessages. Resolvi todos os problemas passando a usar threads. No artigo O lado negro do Application.ProcessMessages, são apresentados alguns motivos para evitar seu uso indiscriminado.

Eu falei no parágrafo anterior que eu me incuía no rol de pessoas que usam ProcessMessages, mas ao escrever este artigo eu bolei uma forma mais simples de implementar de forma correta o uso de barras de progresso e por isso, vou passar a usar sempre. Eu só tenho a ganhar e você também, caro leitor, caso entenda como tudo foi feito e tenha um pouco menos de preguiça ;) Abaixo eu lhes apresento uma unit com uma classe que configura uma thread básica e introduz alguns eventos, propriedades e métodos que vão ser de grande ajuda para fazer tudo funcionar como um relógio:

unit UProgressThread;

interface

uses
  Classes;

type
  TOnProgress = procedure (const PText: String; const PNumber: Cardinal) of object;
  TOnMax = procedure (const PMax: Int64) of object;

  TProgressThread = class (TThread)
  private
    FText: String;
    FNumber: Cardinal;
    FOnProgress: TOnProgress;
    FMax: Int64;
    FOnMax: TOnMax;
    procedure CallOnProgress;
    procedure CallOnMax;
  protected
    procedure DoProgress;
    procedure DoMax;
    property Text: String read FText write FText;
    property Number: Cardinal read FNumber write FNumber;
    property Max: Int64 read FMax write FMax;
  public
    constructor Create; reintroduce;
    property OnProgress: TOnProgress read FOnProgress write FOnProgress;
    property OnMax: TOnMax read FOnMax write FOnMax;
  end;

implementation

{ TProgressBarThread }

procedure TProgressThread.CallOnMax;
begin
  if Assigned(FOnMax) then
    FOnMax(FMax);
end;

procedure TProgressThread.CallOnProgress;
begin
  if Assigned(FOnProgress) then
    FOnProgress(FText,FNumber);
end;

procedure TProgressThread.DoMax;
begin
  if Assigned(FOnMax) then
    Synchronize(CallOnMax);
end;

procedure TProgressThread.DoProgress;
begin
  if Assigned(FOnProgress) then
    Synchronize(CallOnProgress);
end;

constructor TProgressThread.Create;
begin
  inherited Create(True);
  FreeOnTerminate := True;
end;

end.

A classe TProgressThread é simplesmente uma descendente de TThread (linha 12) que cria uma thread em estado suspenso (linha 63) por padrão e instrui a mesma a ser liberada da memória automaticamente (linha 64). Dessa forma, basta fazer TProgressThread.Create e automaticamente  a thread é criada em modo suspenso e será liberada da memória tão logo ela termine. Esta classe expõe dois eventos essenciais nesta implementação: OnProgress e OnMax (linhas 29 e 30). O primeiro é executado a cada iteração do processo demorado e o segundo é executado quando há a necessidade de contagem prévia da quantidade total de iterações, permitindo a configuração da barra de progresso antes do início do processamento. As propriedades Text, Number e Max (linhas 24, 25 e 26) são retornadas pelos eventos OnProgress e OnMax. Os métodos DoProgress e DoMax (linhas 22 e 23) são métodos que, ao serem executados, executam os eventos correspondentes.

Esta classe não foi feita para ser usada diretamente, ela precisa ser estendida para cada rotina demorada que precisa habilitar barras de progresso. Eu sei que ainda está nebuloso até aqui, mas continue lendo, no final tudo ficará claro.


Agrupar é preciso

Se você já tem um projeto que, em algum ponto, realiza algum processamento demorado o primeiro passo é pegar este processamento e colocá-lo dentro de um método (function ou procedure) adaptado para que o mesmo, sozinho, realize todo o processamento. Foi exatamente isso que foi feito com uma rotina que obtém o tamanho de todos os arquivos dentro de uma estrutura de diretórios recursivamente. É uma função simples, com um parâmetro para passar o diretório inicial e que, ao final, retorna o valor dos tamanhos de todos os arquivos encontrados somados. Veja como ficou:

function DirectoryTreeSize(PInitialDir: String): Int64;
var
  SearchRecord: TSearchRec;
begin
  Result := 0;

  if FindFirst(PInitialDir + '*.*', faAnyFile, SearchRecord) = 0 then
    try
      repeat
        if ((SearchRecord.Attr and faDirectory) = faDirectory) then
        begin
          if (SearchRecord.Name <> '.') and (SearchRecord.Name <> '..') then
            Inc(Result,DirectoryTreeSize(PInitialDir + SearchRecord.Name + '\' + ExtractFileName(PInitialDir)));
        end
        else
          Inc(Result,FileSize(PInitialDir + SearchRecord.Name));
      until FindNext(SearchRecord) <> 0;
    finally
      FindClose(SearchRecord)
    end;
end;

Esta função é bem demorada, porque ela usa um método redundante para obter o tamanho de um arquivo. Este método é redundante porque o tamanho dos arquivos já poderia ser obtido instantaneamente lendo SearchRecord.FileSize. Utilizei uma função para leitura genérica do tamanho de um arquivo apenas para desacelerar o processo de forma a fazer sentido o uso de uma barra de progresso.  A função FileSize, para leitura genérica do tamanho de um arquivo, é definida como segue:

function FileSize(PFileName: TFileName): Cardinal;
var
  FileHandle: THandle;
begin
  FileHandle := CreateFile(PChar(PFileName), GENERIC_READ, FILE_SHARE_READ, nil, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
  try
    Result := GetFileSize(FileHandle, nil);
  finally
    CloseHandle(FileHandle);
  end;
end;

Como foi dito anteriormente, a função DirectoryTreeSize percorre recursivamente uma estrutura de diretórios e lê o tamanho de cada arquivo encontrado. Não há como saber quantos arquivos existem dentro de um diretório recursivamente, logo, a única solução é varrer a mesma estrutura e simplesmente contar cada arquivo. Isso parece estúpido, pois vamos acabar percorrendo a mesma estrutura de diretórios duas vezes, uma para obter a quantidade de arquivos e a outra exatamente igual, para processá-los, mas a varredura para contar os arquivos é extremamente rápida, pelo menos é MUITO MAIS RÁPIDA que a obtenção do tamanho de cada arquivo e sendo assim esse procedimento é válido para este exemplo. Abaixo está a função que retorna o total de arquivos a processar dentro da estrutura de diretórios:

function TDirectoryTreeSize.DirectoryTreeFileCount(PInitialDir: String): Cardinal;
var
  SearchRecord: TSearchRec;
begin
  Result := 0;

  if FindFirst(PInitialDir + '*.*', faAnyFile, SearchRecord) = 0 then
    try
      repeat
        if ((SearchRecord.Attr and faDirectory) = faDirectory) then
        begin
          if (SearchRecord.Name <> '.') and (SearchRecord.Name <> '..') then
            Inc(Result,DirectoryTreeFileCount(PInitialDir + SearchRecord.Name + '\' + ExtractFileName(PInitialDir)));
        end
        else
          Inc(Result);
      until FindNext(SearchRecord) <> 0;
    finally
      FindClose(SearchRecord)
    end;
end;

Note que a função DirectoryTreeFileCount é praticamente igual à função DirectoryTreeSize. Isso era esperado neste caso, já que precisamos passar pelos arquivos que serão processados, mas apenas precisamos contá-los.


Estendendo a classe TProgressThread

Agora que temos o procedimento demorado isolado em uma função, e temos uma segunda função que nos dará de forma rápida a quantidade de elementos que serão processados, podemos adaptá-las dentro de uma classe filha da classe TProgressThread. O nome da classe e o nome da unit que a contém eu convenciono como sendo o mesmo nome da função original, logo, a classe se chama TDirectoryTreeSize e sua unit UDirectoryTreeSize. A implementação completa você vê abaixo:

unit UDirectoryTreeSize;

interface

uses
  UProgressThread;

type
  TDirectoryTreeSize = class (TProgressThread)
  private
    FInitialDir: String;
    FResult: Int64;
    function DirectoryTreeSize(PInitialDir: String): Int64;
    function DirectoryTreeFileCount(PInitialDir: String): Cardinal;
  public
    procedure Execute; override;
    property InitialDir: String write FInitialDir;
    property Result: Int64 read FResult;
  end;

implementation

uses
  Windows, SysUtils;

function FileSize(PFileName: TFileName): Cardinal;
var
  FileHandle: THandle;
begin
  FileHandle := CreateFile(PChar(PFileName), GENERIC_READ, FILE_SHARE_READ, nil, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
  try
    Result := GetFileSize(FileHandle, nil);
  finally
    CloseHandle(FileHandle);
  end;
end;

{ TDirectoryTreeSize }

function TDirectoryTreeSize.DirectoryTreeFileCount(PInitialDir: String): Cardinal;
var
  SearchRecord: TSearchRec;
begin
  Result := 0;

  if FindFirst(PInitialDir + '*.*', faAnyFile, SearchRecord) = 0 then
    try
      repeat
        if ((SearchRecord.Attr and faDirectory) = faDirectory) then
        begin
          if (SearchRecord.Name <> '.') and (SearchRecord.Name <> '..') then
            Inc(Result,DirectoryTreeFileCount(PInitialDir + SearchRecord.Name + '\' + ExtractFileName(PInitialDir)));
        end
        else
          Inc(Result);
      until FindNext(SearchRecord) <> 0;
    finally
      FindClose(SearchRecord)
    end;
end;

function TDirectoryTreeSize.DirectoryTreeSize(PInitialDir: String): Int64;
var
  SearchRecord: TSearchRec;
begin
  Result := 0;

  if FindFirst(PInitialDir + '*.*', faAnyFile, SearchRecord) = 0 then
    try
      repeat
        if ((SearchRecord.Attr and faDirectory) = faDirectory) then
        begin
          if (SearchRecord.Name <> '.') and (SearchRecord.Name <> '..') then
            Inc(Result,DirectoryTreeSize(PInitialDir + SearchRecord.Name + '\' + ExtractFileName(PInitialDir)));
        end
        else
        begin
          Text := SearchRecord.Name;
          Number := FileSize(PInitialDir + Text);
          DoProgress;
          Inc(Result,Number);
        end;
      until FindNext(SearchRecord) <> 0;
    finally
      FindClose(SearchRecord)
    end;
end;

procedure TDirectoryTreeSize.Execute;
begin
  inherited;
  Max := DirectoryTreeFileCount(FInitialDir);
  DoMax;
  FResult := DirectoryTreeSize(FInitialDir);
end;

end.

Calma, calma, não entre em pânico! Eu vou explicar tudo o que foi feito nesta unit da maneira mais simples e direta quanto for possível.


Como TDirectoryTreeSize foi implementada? (E como criar a sua própria classe) 

Anteriormente eu falei que seria necessário agrupar em um método (function ou procedure) todo o processamento demorado, portanto estou considerando que você já fez isso. Siga os passos a seguir e olhe com cuidado a classe TDirectoryTreeSize. Use os links nos números das linhas para pular para o ponto exato no código da classe acima.

  1. Declare na seção private o método que é o responsável por gerar o processamento demorado, doravante chamado simplesmente de MD (Método Demorado). No nosso exemplo, este método é o DirectoryTreeSize, o qual pode ser visto declarado na linha 13. A linha 14 mostra a declaração de nosso método adicional (doravante chamado simplesmente de MA) que tem o objetivo de contar a quantidade de elementos que serão processados (DirectoryTreeFileCount). Ainda na seção private declare quaisquer outros métodos adicionais (MAs) que serão necessários para que o MD possa funcionar. Use a classe como você faria com qualquer classe comum, declarando todos os métodos como privados. Não use variáveis globais ou locais na unit UDirectoryTreeSize. Caso precise de variáveis, use campos da classe e todos eles também privados e com propriedades públicas correspondentes. Se você já criou uma classe em sua vida, vai notar que nada disso é novo ou especial;

  2. Altere a definição do seu MD, de forma a incluir nele o método DoProgress, o qual gera o evento OnProgress no qual o TProgressBar pode ser atualizado na aplicação. O local exato da colocação do DoProgress depende da sua implementação. Normalmente ele deve ser posto dentro de um loop que vai "rodar" na mesma quantidade de vezes que o número de iterações informado, conhecido ou, no caso do nosso exemplo, obtido pelo MA de contagem (linha 92). As linha 78 a 80, mostram como deve ser feita a implementação do método DoProgress. O evento OnProgress tem dois parâmetros, um parâmetro PText do tipo String e outro parâmetro PNumber do tipo Cardinal (DWord). Toda vez que o evento OnProgress é ativado sua aplicação pode ler os valores de PText e PNumber a fim de atualizar um TLabel com informações relevantes, por exemplo. Por este motivo, antes de executar o DoProgress as propriedades Text e Number (declaradas em TProgressThread) foram preenchidas, com o nome do arquivo e seu tamanho, respectivamente. Assim, quando o evento OnProgress for ativado pelo DoProgress, os parâmetros PText e PNumber estarão preenchidos com informações que podem ser exibidas na aplicação. Usar estes dois parâmetros é absolutamente opcional. Eles existem apenas como um bônus. O mais importante é chamar DoProgress, porque ele gerará o evento que informa que o TProgressBar precisa ser atualizado, usando seu método StepIt, ou incrementando sua propriedade Position;

  3. Para cada parâmetro do MD, crie um campo privado na classe, que seja do mesmo tipo do parâmetro. No nosso exemplo o MD possui o parâmetro PInitialDir, logo, criamos um campo FInitialDir (linha 11). Caso houvesse mais parâmetros, cada um deles se transformaria em um campo na classe;

  4. Para cada parâmetro dos MAs, crie campos privados que sejam dos mesmos tipos de cada parâmetro. No nosso exemplo o MA DirectoryTreeFileCount possui o mesmo parâmetro que o MD (PInitialDir), logo, a criação de um campo privado para ele (FInitialDir) já foi coberta no passo 3;

  5. Para cada campo criado nos passos 3 e 4 crie uma propriedade correspondente. Na linha 17 foi criada a propriedade InitialDir. A classe precisa conhecer todas as variáveis que serão necessárias para que seus métodos internos possam funcionar, portanto, se seus métodos internos precisarem de mais informações, cada uma destas precisa ser informada na classe e isso deve ser feito por meio de propriedades. Note que a propriedade InitialDir possui uma restrição de acesso, ou seja, esta propriedade é somente para escrita (write-only). Isso não é necessário, mas como o parâmetro PInitialDir é somente de entrada, eu resolvi criar a propriedade mantendo o mesmo padrão. Se houvesse um parâmetro out, a propriedade correspondente deveria ser read-write. Em suma, por motivos óbvios, as propriedades que representam parâmetros devem ter sempre acesso para que possam ser "escritas", elas nunca devem ser read-only, mas podem ser write-only;

  6. Caso seu MD seja uma função (retorna um valor) crie um campo privado de nome FResult, do mesmo tipo do retorno do MD; Na linha 12 podemos ver a declaração do campo privado. Note que ele tem o mesmo tipo do retorno do MD;

  7. Caos você tenha precisado executar o passo 6, crie agora uma propriedade Result correspondente (linha 18). Esta propriedade precisa ser somente leitura para evitar utilização incorreta da classe, além disso, faz todo sentido que esta propriedade seja somente leitura, já que ela representa o retorno do MD;

  8. Declare o método Execute na seção public da classe (linha 16). Este é o método que deve conter a lógica da thread, em outras palavras é nele onde o MD precisa ser executado. Vá para a implementação do método Execute e realize os dois passos a seguir;

    1. Caso você tenha precisado implementar um MA para calcular a quantidade de iterações do seu MD, chame primeiramente o MA e retorne na propriedade Max a quantidade de iterações calculadas, em seguida, execute o método DoMax. Ao executar o método DoMax, um evento OnMax será ativado e sua propriedade PMax conterá aquilo que foi retornado na propriedade Max. Na sua aplicação, no manipulador do evento OnMax, você configura a propriedade Max do seu TProgressBar. As linhas 92 e 93 mostram como deve ser feito; primeiro chamamos o método DirectoryTreeFileCount, o qual retorna na propriedade Max e logo em seguida há a chamada ao método DoMax, o qual vai gerar o evento OnMax, que pode ser manipulado na aplicação principal para configurar o TProgressBar. Note que DirectoryTreeFileCount foi chamada com o parâmetro FInitialDir, o qual deve ter sido configurado após a criação da instância de TDirectoryTreeSize. Não se preocupe, isso será mostrado posteriormente;

    2. Imediatamente após a execução do MA, caso haja um, é hora de chamar o MD. Caos seu MD seja uma função, execute-o exatamente como no exemplo (linha 94). Note que o retorno da função é colocado em FResult, o que é esperado, já que a propriedade associada (Result) serve exatamente para isso. Caso seu MD não precise retornar nenhum valor, apenas processar algo, então haveria apenas a chamada ao MD e nem mesmo FResult (ou sua propriedade) teria sido criado (passos 6 e 7)

  9. Caso alguns de seus campos privados precisem de inicialização prévia (criação), você deve, na seção public da classe, declarar o método construtor da seguinte forma "constructor Create; override;" e não se esqueça de implementar o método destrutor "destructor Destroy; override;" também na seção public, destruindo (Free), cada um dos campos criados no construtor. 
 

Como utilizar a classe TDirectoryTreeSize? (ou sua própria classe, como preferir)

Esta é a parte mais fácil e mais compensadora deste artigo. Abaixo está uma parte da unit UFORMPrincipal, que contém a classe TFORMPrincipal, da qual removi algumas partes para facilitar o entendimento:

unit UFORMPrincipal;

interface

uses
  Forms, StdCtrls, Classes, Controls, UDirectoryTreeSize, ExtCtrls, ComCtrls;

type
  TFORMPrincipal = class(TForm)
    BUTNProcessamentoPesado: TButton;
    EDITDiretorioInicial: TEdit;
    PRBAProgresso: TProgressBar;
    LABEArquivo: TLabel;
    LABEPercentual: TLabel;
    procedure BUTNProcessamentoPesadoClick(Sender: TObject);
  private
    { Private declarations }
    FStartTime: TTime;
    FDirectoryTreeSize: TDirectoryTreeSize;
    procedure DoProgress (const PText: String; const PNumber: Cardinal);
    procedure DoMax(const PMax: Int64);
    procedure DoTerminate(PSender: TObject);
  public
    { Public declarations }
  end;

var
  FORMPrincipal: TFORMPrincipal;

implementation

{$R *.dfm}

uses
  Windows, SysUtils, Dialogs;

{ TFORMPrincipal }

procedure TFORMPrincipal.BUTNProcessamentoPesadoClick(Sender: TObject);
begin
  FDirectoryTreeSize := TDirectoryTreeSize.Create;

  with FDirectoryTreeSize do
  begin
    InitialDir := EDITDiretorioInicial.Text;
    OnMax := DoMax;
    OnProgress := DoProgress;
    OnTerminate := DoTerminate;

    LABEPercentual.Caption := '0.00%';
    FStartTime := Now;
    BUTNProcessamentoPesado.Enabled := False;
    EDITDiretorioInicial.Enabled := False;

    Resume;
  end;
end;

procedure TFORMPrincipal.DoMax(const PMax: Int64);
begin
  PRBAProgresso.Step := 1;
  PRBAProgresso.Position := 0;
  PRBAProgresso.Max := PMax;
  PRBAProgresso.DoubleBuffered := True;
end;

procedure TFORMPrincipal.DoProgress(const PText: String; const PNumber: Cardinal);
begin
  PRBAProgresso.StepIt;
  LABEArquivo.Caption := 'Arquivo ' + IntToStr(PRBAProgresso.Position) + ' / ' + IntToStr(PRBAProgresso.Max) + ': ' + FormatFloat('(###,###,###,###,##0 bytes) ',PNumber) + PText;
  LABEPercentual.Caption := FormatFloat('##0.00%',PRBAProgresso.Position / PRBAProgresso.Max * 100);
end;

procedure TFORMPrincipal.DoTerminate(PSender: TObject);
begin
  Application.MessageBox(PChar('O tamanho total dos arquivos contidos na estrutura de diretórios "' + EDITDiretorioInicial.Text + '" é ' + FormatFloat('###,###,###,###,##0 bytes',FDirectoryTreeSize.Result)),PChar(Format('Processamento concluído em %s',[FormatDateTime('hh:nn:ss',Now - FStartTime)])),MB_ICONINFORMATION);
  BUTNProcessamentoPesado.Enabled := True;
  EDITDiretorioInicial.Enabled := True;
end;

end.

Para um entendimento ainda melhor, abra o exemplo anexado neste artigo. Abaixo segue a explicação detalhada:

  1. Declare um campo privado na classe TFORMPrincipal do tipo TDirectoryTreeSize. O campo FDirectoryTreeSize pode ser visto na linha 19;

  2. Declare o manipulador do evento OnProgress (DoProgress) na seção private de TFORMPrincipal (linha 20);

  3. Implemente o manipulador DoProgress (linhas 69 a 71). No nosso exemplo, este manipulador é ativado sempre que um arquivo tem seu tamanho lido. Na linha 69 o valor de TProgressBar é incrementado. TProgressBar.StepIt é um método que adiciona o valor da propriedade TProgressBar.Step à propriedade TProgressBar.Position. Existem pessoas que preferem atribuir diretamente à propriedade TProgressBar.Position seu valor +1, mas eu prefiro ser mais prático e usar TProgressBar.StepIt, que já faz isso. Na linha 70 o nome do arquivo, contido em PText, e seu tamanho, contido em PNumber, são atribuídos de forma formatada a um TLabel. Na linha 71, como uma característica adicional, eu faço um calculo simples que retorna em um TLabel o percentual atual, porque todos nós amamos percentuais que incrementam :);

  4. Declare o manipulador do evento OnMax (DoMax) na seção private de TFORMPrincipal (linha 21);

  5. Implemente o manipulador DoMax (linhas 61 a 64). Em nosso exemplo, evento OnMax é ativado quando o MA de contagem de arquivos retorna a quantidade total de arquivos que serão processados. Este evento tem apenas um parâmetro (PMax), o qual retorna esta quantidade. É neste evento onde devemos configurar o TProgressBar. Na linha 61 é configurado o Step (passo) do TProgressBar como 1, de forma que ao se usar TProgressBar.StepIt, TProgressbar.Position seja automaticamente incrementada em 1 unidade. Na linha 62 TProgressBar.Position é configurada como zero para que o TProgressBar "esvazie". Na linha 63 é configurada a propriedade TProgressBar.Max com o valor do parâmetro PMax. É exatamente para isso que o evento OnMax serve! Finalmente, na linha 64, a fim de evitar flickering, a propriedade TProgressBar.DoubleBuffered é configurada como True;

  6. Declare o manipulador do evento OnTerminate (DoTerminate) na seção private de TFORMPrincipal (linha 22);

  7. Implemente o manipulador DoTerminate (linhas 76 a 78). O evento OnTerminate é ativado quando o método Execute de uma thread termina. O fim do método Execute sinaliza o fim do MD e por isso, caso o MD seja uma função, é neste evento onde devemos obter o valor retornado pela mesma. É também neste evento onde devemos reabilitar controles que foram desabilitados antes do início da thread. Na linha 76 exibimos uma mensagem para o usuário informando o tamanho total de todos os arquivos somados na estrutura de diretórios. Esse valor está em FDirectoryTreeSize.Result. Exibimos também o tempo que o procedimento levou para ser concluído (Now - FStartTime). As linhas 77 e 78 reabilitam os controles que foram desabilitados antes do início da thread (veja mais adiante neste passo-a-passo);

  8. Crie um método para iniciar e configurar a instância de TDirectoryTreeSize. Isso também pode ser feito diretamente num manipulador de eventos, como eu fiz, mas admito que criar um método para isso é mais elegante. Como o intuito aqui é ser o mais direto possível nas explicações, codificar tudo no manipulador do evento OnClick de um botão é perfeitamente válido e vai funcionar à contento. As linhas 41 a 56 mostram exatamente tudo que precisa ser feito. Abaixo segue o detalhamento;

    1. Instancie a classe TDirectoryTreeSize (linha 41) no campo criado para este propósito (FDirectoryTreeSize);

    2. Configure as propriedades requeridas pela classe TDirectoryTreeSize (linha 45). No caso do exemplo, existe apenas uma propriedade (InitialDir), a qual deve ser preenchida com o diretório inicial, a partir do qual será feita a varredura recursiva. EDITDiretorioInicial é um componente TEdit que existe em TFORMPrincipal para este fim. Se houvessem mais propriedades, elas deveriam ser preenchidas aqui, uma após a outra, a fim de manter o código organizado;

    3. Atribua os manipuladores de evento aos eventos da classe TDirectoryTreeSize (linhas 46 a 48). O manipulador do evento OnMax (DoMax) só precisa ser criado e atribuído caso haja um método interno que calcula a quantidade de elementos (itens) iteráveis ANTES da realização do MD. Caso a quantidade de elementos iteráveis seja conhecida previamente, você não deve ter criado um MA para calcular este valor e consequentemente o evento OnMax nunca será ativado, porque não é necessário. Como no nosso exemplo nós precisamos calcular a quantidade total de arquivos dos quais o tamanho deve ser retornado, então o evento OnMax será ativado assim que o total de arquivos for obtido e o parâmetro PMax conterá este total. O evento OnProgress será ativado a cada vez que um arquivo tiver seu tamanho obtido. No manipulador desse evento (DoProgress) é possível obter o nome do arquivo e o seu tamanho individual nos parâmetros PText e PNumber. O evento OnTerminate, manipulado pelo método DoTeminate, é um evento de TThread e é ativado sempre que uma thread termina, e imediatamente ANTES dela ser liberada da memória (caso ela tenha sido criada com FreeOnTerminate = True). É neste evento onde obtemos o valor de retorno do MD, pois ele sinaliza que o MD terminou de realizar sua tarefa, logo, caso o MD seja uma função, seu retorno já é conhecido;

    4. Execute procedimentos "pré-MD". As linhas 50 a 53 contém código que deve ser executado ANTES do MD ser executado na thread e o que vai nestas linhas, depende de sua implementação. Não há nada de muito especial. No caso do exemplo, um TLabel está sendo "zerado" na linha 50, um campo de contagem de tempo está sendo preenchido com o valor atual (Now) na linha 51, o TButton onde todo este código está, é desabilitado (para evitar cliques múltiplos) na linha 52 e, finalmente, o TEdit onde se digita o parâmetro InitialDir é desabilitado (linha 53);

    5. Inicialize TDirectoryTreeSize (TThread) usando o método Resume (linha 55). Como por padrão nossa classe TDirectoryTreeSize é uma neta de TThread com CreateSuspended = True, então, para iniciar a thread, simplesmente executamos seu método Resume, o qual tem por finalidade continuar a execução de uma thread suspensa. Se você tem um Delphi mais recente, no lugar de Resume coloque Start, pois o método Resume foi depreciado;

Após realizar todos estes passos, execute e teste! Use o exemplo anexado a este artigo para ver exatamente como tudo foi implementado e comece agora mesmo a converter seus métodos demorados para usarem corretamente o TProgressBar e TThreads :)

Esse trabalho todo, compensa?

Você agora deve estar pensando que isso tudo foi feito apenas para implementar o uso correto de uma simples TProgressBar, mas não é bem assim. Primeiramente, não é tanto trabalho como parece. O artigo ficou grande porque ele é didático, mas ao olhar o código fonte do exemplo anexado, se nota que não tem muita coisa escrita e se você levar em conta que precisa implementar apenas uma classe (TDirectoryTreeSize no exemplo) e depois usá-la, menos código ainda vai sobrar para ser escrito.

Em segundo lugar, mas não menos importante, apesar do título do artigo falar do TProgressBar, este é apenas a ponta do iceberg dentro do contexto, em outras palavras, o "pretexto", para se usar um TProgressBar de forma correta é que deve ser levado em conta: o uso de uma thread para executar um processamento demorado.

A dica de ouro é: Sempre que você tiver algo que vai demorar um tempo considerável, é preciso colocar esse "algo" em uma thread, isso garante que sua aplicação permaneça responsiva e processando outras mensagens (Windows Messages) importantes sem imprevistos. Além disso, para não deixar seu usuário a ver navios, você precisa mantê-lo informado acerca do andamento da tarefa e é aí onde o TProgressBar entra.


  Arquivos anexados  
Arquivo Descrição Tamanho Modificado em
Download this file (PRBR.zip) PRBR Este projeto demostra o uso correto de uma barra de progresso para um processo demorado, usando uma thread 246 KB 19/09/2016 às 18:24