Animações em tempo real com a VCL - Parte I

O Delphi facilita muito a construção de interfaces gráficas. Arrastando componentes da barra de ferramentas e alterando suas propriedades pelo “object inspector”, é muito fácil construir uma interface consistente para a maioria das aplicações.

Esta abordagem, porém, não pode ser utilizada para construir uma superfície de desenho adequada para a exibição de animações que exijam uma taxa de atualização aceitável (pelo menos 20 quadros por segundo). Para isso, a solução mais óbvia seria utilizar uma API ou framework projetado especificamente para este fim (como OpenGL ou DirectX).

 Existem, porém, dois problemas a ser vencidos para adotar um framework destes. O primeiro problema é que, caso não haja um hardware gráfico moderno disponível na máquina onde a aplicação irá ser executada, nenhuma aceleração de será provida (leia-se: todo o processamento gráfico será feito por software). O segundo problema é a curva de aprendizado, consideravelmente longa, que o programador terá de vencer para conseguir aproveitar os recursos que elas possam oferecer.

Então, qual biblioteca utilizar? Não dá pra fazer isso com a própria VCL?

Sim. Dá sim. Mas algumas otimizações precisam ser feitas. E é sobre estas otimizações que vamos falar.

Double Buffer ou Backbuffer

O TCanvas representa uma superfície de desenho que pode ser entendida e tratada como um array de pixels ou um buffer de pixels. O Windows mapeia este buffer para a memória de vídeo e então a placa de vídeo trata de interpretar esta informação e convertê-la em uma imagem visível no monitor.

Se desenharmos direto num TCanvas de um objeto visível, o desenho será imediatamente atualizado na tela. Até aí, tudo bem. Desde que sejam desenhados um ou dois objetos por vez, tudo irá ser atualizado rápido o suficiente para que a transição não seja facilmente percebida.

Ponha um TTimer num form, configure a propriedade interval para 20, carregue uma imagem qualquer num TImage e ponha o seguinte código no evento OnTimer:

procedure TForm1.Timer1Timer(Sender: TObject);
begin
  Canvas.Brush.Color := clWhite;
  Canvas.FillRect(Canvas.ClipRect);
  Canvas.Draw(X, Y, Image1.Picture.Graphic);
  Inc(X);
  Inc(Y);
  if X >= ClientWidth - Image1.Width then
     X := 0;
  if Y >= ClientHeight - Image1.Height then
     Y := 0;
end;

Ao executar este programa, você verá sua imagem sendo redesenha aproximadamente 50 vezes por segundo (1000/20=50), o suficiente para causar a impressão de movimento, mas a imagem fica piscando constantemente e o resultado é realmente muito feio.

A idéia geral do buffer duplo (Double buffer) é ter uma segunda área de desenho que fique sempre invisível. Daí, em vez de desenhar direto na tela, todos os desenhos seriam gerados neste buffer e, somente depois de pronto, copiaríamos o desenho para a área visível da tela, numa única operação.

A primeira impressão que essa idéia passa é de que, como estaremos percorrendo um caminho maior, as coisas ficarão mais lentas. Mas o fato é que, quando desenhamos num TCanvas visível, a operação demora mais porque, além do tempo gasto com o desenho em si, o programa tem que esperar a memória de vídeo ser atualizada para que o as mudanças apareçam na tela e o programa possa seguir. Quando desenhamos na memória (num TCanvas não visível) este atraso não existe, logo, a operação é concluída muito mais rápido. Além disso, quando vamos exibir a imagem na tela, ela já está pronta e somente uma operação de desenho será realizada nesta “área lenta”, o que contribui para que a animação tenha uma aparência mais agradável.
Vejamos como implementar esta idéia em Delphi.

Primeiro, declare uma variável privada do tipo TBitmap para ser o nosso “back buffer” e trate a inicialização e destruição deste objeto:

private
  { Private declarations }
  fOffScreen: TBitmap;

(...)

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  FreeAndNil(fOffScreen);
end;

(...)

procedure TForm1.FormCreate(Sender: TObject);
begin
  X := 0;
  Y := 0;
  fOffScreen := TBitmap.Create;
  fOffScreen.PixelFormat := pf24bit;
  fOffScreen.Width := ClientWidth;
  fOffScreen.Height := ClientHeight;
end;

Agora vamos desenhar na memória e, quando concluído o desenho, vamos transferi-lo para a “área visível” da tela.

procedure TForm1.Timer1Timer(Sender: TObject);
begin
  fOffScreen.Canvas.Brush.Color := clWhite;
  fOffScreen.Canvas.FillRect(fOffScreen.Canvas.ClipRect);
  fOffScreen.Canvas.Draw(X, Y, Image1.Picture.Graphic);

  BitBlt(Canvas.Handle, 0, 0,
         ClientWidth,
         ClientHeight,
         fOffScreen.Canvas.Handle,
         0, 0, SRCCOPY);
  Inc(X);
  Inc(Y);
  if X >= ClientWidth - Image1.Width then
     X := 0;
  if Y >= ClientHeight - Image1.Height then
     Y := 0;
end;

Execute este programa e veja que o problema da imagem piscando (conhecido como flickering) foi resolvido.

Eliminando interferências do Windows

Outra otimização mais sutil, mas não menos importante, é o tratamento adequado da mensagem WM_ERASEBKGND que o Windows envia ao programa quando ele acha que alguma área da janela deve ser atualizada.

O Delphi, por padrão, em resposta a esta mensagem irá invalidar toda a área cliente da janela, forçando um redesenho completo. Este comportamento faz sentido quando deixamos que o Delphi cuide sozinho do desenho e da atualização da janela, mas no nosso caso, queremos ter controle completo sobre o modo que a janela será exibida.

Vamos criar uma procedure que irá realizar o tratamento desta mensagem. Declare o seguinte procedimento :

procedure WMEraseBg(var Msg: TWMEraseBkgnd); message WM_ERASEBKGND;

E em sua implementação diga ao Windows, que você mesmo irá tratar do redesenho da janela, setando a mensagem MSG para 0:

procedure TfrmCGScreen.WMEraseBg(var Msg: TWMEraseBkgnd);
begin
  Msg.Result := 0;
end;

Um template para animações

Com estas duas otimizações implementadas, já dá pra fazer pequenas animações executarem de maneira eficiente. Mas ainda há espaço para outras otimizações (que trataremos no próximo post), como executar o programa em full screen, substituir o TTimer por funções mais precisas como QueryPerformaceCounter e utilizar o evento OnIdle do TApplication para realizar os desenhos da animação.

Neste link, você pode baixar os fontes de um projeto que implementa todas estas otimizações e utilizá-lo como template para seus próprios projetos. Faça o download do arquivo, abra-o no Delphi, clique com um botão direito em cima do form e escolha a opção “Add to Repository”. Pronto. Este template estará disponível no “Object Repository”!

Para usar o template, clique no menu File > New > Other, escolha o formulário adicionado anteriormente, marque a opção “inherit” e sobrescreva o método DrawScene para criar suas animações.

Até o próximo post.

Downloads


Links

Links para as postagens que fazem parte do mini curso "Animações em Tempo real com a VCL"