21
May 08

Delphi: SetTimer

:: articles :: by Gilberto Saraiva

Camaradas,

O SetTimer é uma função disponibilizada pela plataforma Windows ( API ) que consiste em criar um Temporizador que notifica um Objeto Windows ( Handle ) sempre que atingir o términio do tempo estipulado. Um temporizador desse tipo é gerenciado pelo Windows, ou seja, não será relacionado com o processamento do seu aplicativo, ficando assim com uma precisão boa mesmo enquanto seu aplicativo processa alguma coisa.

Um detalhe muito importante sobre o processamento da notificação do temporizador ( Timer ), é que como a notificação padrão é feita via mensagem, a WM_TIMER, e mensagens são administradas pela VCL através do Objeto TApplication application, em 2 respectivos locais, o primeiro em ProcessMessages que processa a fila inteira de mensagens que a aplicação possuí, e o segundo HandleMessage que processa apenas a primeira mensagem da fila, se existir alguma, sinaliza o Evento OnIde e aguarda uma nova mensagem (em caso de fila a liberação é instantanea), você podera perder a precisão do temporizador por falta de manipulação da recepção de mensagens. Já vi muita gente reclamando a precisão do SetTimer na utilização dentro de um aplicativo Delphi, mas nunca vi essa reclamação de um programador C, pois o mesmo tem o trabalho de gerênciar as notificações para o objeto receptor.

Então o fato é, temporizadores não são ruins, são ótimas opções para criação de simuladores de interação, Inteligências artificiais em alguns pontos, e até mesmo para criação de cronometros e agendamentos.

 Delphi |  copy code |? 
1
{$EXTERNALSYM SetTimer}
2
function SetTimer(hWnd: HWND; nIDEvent, uElapse: UINT;
3
  lpTimerFunc: TFNTimerProc): UINT; stdcall;

O 1º parametro é referente ao Objeto Windows ( Handle ) receptor, qualquer objeto com Handle ( Objeto Windows ).
O 2º parametro é referente ao índice controlado pelo programador, servirá como identificador na hora do processamento da notificação WM_TIMER.
O 3º parametro indica o intervalo, em milisegundos, em que o temporizador notificará o objeto.
O 4º parametro é nosso cargo chefe, deve ser analizado como primeira opção sempre pois utilizando este parametro deixaremos a necessidade de utilizar a VCL ou qualquer ou controle de Objetos Windows para a recepção da notificação.

Chegando neste ponto, podemos perceber que possuímos 2 caminhos, o primerio através de um Objeto Windows e o outro através de um Callback, a escolha é você quem faz na hora da utilização, vou exemplificar as duas maneiras de utilização, então preste atenção, pois é bastante interessante :D .

Como utilizar um temporizador através de um Objeto Windows?

Para facilitar o entendimento e não aprofundar muito em matérias que não são a questão do artigo, vamos utilizar a VCL para administrar nosso Objeto Windows.

 Delphi |  copy code |? 
01
02
  TfrmTest = class(TForm)
03
    btnStart: TButton;
04
    procedure btnStartClick(Sender: TObject);
05
  private
06
    { Private declarations }
07
  public
08
    procedure WMTimer(var Msg: TWMTimer); message WM_TIMER;
09
  end;
10

Declaramos o a manipulação da mensagem WM_TIMER para o nosso “TForm” frmTest. No frmTest colocamos também um TButton btnStart para criamos o temporizador através de seu evento OnClick.

 Delphi |  copy code |? 
1
procedure TfrmTest.btnStartClick(Sender: TObject);
2
begin
3
  SetTimer(Handle, 0, 1000, nil);
4
end;

Este é o evento OnClick do TButton btnStart. Entendendo melhor o que acontece aqui:

  • Handle é o Handle do frmTest, como é Handle é Objeto Windows e pode receber mensagens.
  • 0 é o índice que eu informei para o temporizador, então sempre que eu receber um WM_TIMER vou verificar qual índice ele contém e poder estruturar tarefas em cima disso.
  • 1000 é o intervalo em milisegundos que o temporizador irá me notificar no Handle do frmTest. 1000 milisegundos = 1 segundo, logo 60000 milisegundos = 1 minuto.
  • nil é indicado como NIL pois queremos interagir com a mensagem através de mensagens.
  •  Delphi |  copy code |? 
    1
    2
    procedure TfrmTest.WMTimer(var Msg: TWMTimer);
    3
    begin
    4
      KillTimer(Handle, Msg.TimerID);
    5
      ShowMessage('Evento do temporizador: ' + IntToStr(Msg.TimerID));
    6
    end;

    Este procedimento é a parte fundamental no funcionamento de um temporizador via Objeto Windows. Perceba que já aparece uma nova função, a KillTimer, que nada mais é que a contra-API da SetTimer, ou seja, cria o temporizador com SetTimer, e destroí o mesmo com KillTimer.

     Delphi |  copy code |? 
    1
    {$EXTERNALSYM KillTimer}
    2
    function KillTimer(hWnd: HWND; uIDEvent: UINT): BOOL; stdcall;

    O 1º parametro é o Objeto Windows que recebe a notificação do temporizador, tem que ser igual ao que foi utilizado no SetTimer.
    O 2º parametro é o índice criado pelo programador, tem que ser igual ao que foi utilizado no SetTimer.

    Pois bem, já vimos que a utilização do temporizador através da VCL é fácil, e pode se extender a um universo infinito de possibilidades. Vou mostrar um exemplo prático:

     Delphi |  copy code |? 
    01
    02
    type
    03
      PTimerParam = ^TTimerParam;
    04
      TTimerParam = record
    05
        s: string;
    06
        i: integer;
    07
      end;
    08
     
    09
    procedure TfrmTest.btnStartClick(Sender: TObject);
    10
    var
    11
      Param: PTimerParam;
    12
    begin
    13
      New(Param);
    14
      Param^.s := 'Teste de título animado';
    15
      Param^.i := 1;
    16
      SetTimer(Handle, Integer(Param), 100, nil);
    17
    end;
    18
     
    19
    procedure TfrmTest.WMTimer(var Msg: TWMTimer);
    20
    begin
    21
      KillTimer(Handle, Msg.TimerID);
    22
      with PTimerParam(Msg.TimerID)^ do
    23
      begin
    24
        Caption := Copy(s, 1, i);
    25
        i := i + 1;
    26
        if i <= Length(s) then
    27
          SetTimer(Handle, Msg.TimerID, 100, nil)
    28
        else
    29
          Dispose(Pointer(Msg.TimerID));
    30
      end;
    31
    end;
    32

    Agora a segunda parte da história:

    Como utilizar um temporizador através de um Callback?

    Primeiramente, a utilização do SetTimer por Callback é mais eficiente em relação ao processo via Objeto Windows, pois não há dependência de objeto para que o reconhecimento da notificação do temporizador seja feita em relação a aplicação, pois o callback será chamado internamente pela DefWindowProc.

    Então vamos entender como declarar um procedimento no padrão do Callback TimerProc:

     C |  copy code |? 
    1
    2
    VOID CALLBACK TimerProc(      
    3
        HWND hwnd,
    4
        UINT uMsg,
    5
        UINT_PTR idEvent,
    6
        DWORD dwTime
    7
    );

    Esse modelo traduzido para Delphi:
     Delphi |  copy code |? 
    1
    procedure TimerProc(hwnd: HWND; uMsg, idEvent: UINT; dwTime: DWORD); stdcall;

    Então após entendermos a estrutura do último parametro do SetTimer podemos escrever um exemplo prático de resultado semelhante ao exemplo anterior:

     Delphi |  copy code |? 
    01
    02
    type
    03
      PTimerParam = ^TTimerParam;
    04
      TTimerParam = record
    05
        s: string;
    06
        i: integer;
    07
      end;
    08
     
    09
    procedure TimerProc(hwnd: HWND; uMsg, idEvent: UINT; dwTime: DWORD); stdcall;
    10
    begin
    11
      KillTimer(hwnd, idEvent);
    12
      with PTimerParam(idEvent)^ do
    13
      begin
    14
        frmMain.Caption := Copy(s, 1, i);
    15
        i := i + 1;
    16
        if i <= Length(s) then
    17
          SetTimer(hwnd, idEvent, 100, @TimerProc)
    18
        else
    19
        begin
    20
          Dispose(Pointer(idEvent));
    21
          DeallocateHWnd(hwnd);
    22
        end;
    23
      end;
    24
    end;
    25
     
    26
    procedure TfrmMain.btnRunClick(Sender: TObject);
    27
    var
    28
      Param: PTimerParam;
    29
    begin
    30
      New(Param);
    31
      Param^.s := 'Teste de título animado';
    32
      Param^.i := 1;
    33
      SetTimer(AllocateHWnd(nil), Integer(Param), 100, @TimerProc);
    34
    end;
    35

    Antes de analizar o código vou citar um detalhe importante:

    Veja que ao chamar o SetTimer eu utilizo o AllocateHWnd como primeiro parametro, o porquê disso é que o SetTimer opera de maneira diferente ao se indicar um Objeto Windows que não pertence à aplicação, a definição é a seguinte:
    Caso o Handle passado como parametro pertença a aplicação, mais especificamente a mesma Thread que chama a SetTimer. No caso do Handle ser igual a 0 ( zero ) ou não pertencer a aplicação, o parametro passado como índice do temporizador será ignorado e um novo índice será criado e o retorno da chamada ao SetTimer indicará o novo índice.

    Depois de entender esse detalhe importante podemos perceber porque fazemos a criação de um Objeto Windows utilizando o AllocateHWnd. A finalidade disso é não passar um Objeto Windows inválido e ter o índice, que no nosso caso indica um ponteiro que serve de parametro, resetado.

    Agora entendendo um pouco o código:

    Perceba que sempre que recebemos a notificação de términio do intervalo do temporizador, finalizamos o temporizador, e porque fazemos isso? Fazemos isso pois se dentro da notificação, seja na utilização do callback ou por mensagem ( WM_Timer ), o processo que escrevermos lá demorar mais do que o intervalo, criaremos uma fila de processamento, e que acaba por criar uma sequência de processamento e o efeito de execução por intervalo não será perceptível.

    Fluxograma para entendimento rápido:

    Qualquer dúvida fique avontade em perguntar!

    Abraço a todos

    Tags: , , , , , , , , , , , , , ,


    5 comments

    1. Thiago J. da Silva

      Oi Gilberto Saraiva,

      Eu tive dificuldade em entender seu exemplo usando um objeto windows. Eu não entendi porque você criou o tipo TTimerParam.
      Estou desenvolvendo um programa em delphi para ler dados através da porta serial e quero temporizar a aquisição. Já tentei fazer isso de diversas maneiras, mas não obtive sucesso. Se você pudesse me explicar como eu posso usar esse temporizador para o meu caso ou me indicar outro método eu ficaria MUITO grato.

      Atenciosamente,

      Thiago José da Silva

    2. Ola Thiago José da Silva,

      Veja que no começo do artigo eu informo que o 2 parametro da API SetTimer é indicada pelo programador, e o mesmo faz o uso como necessário entendendo que o parametro é númerico de tamanho 4 (ex: integer, cardinal, longint, pointer e outros).

      O que fiz no exemplo foi indicar como 2 parametro o ponteiro de uma variável que tenho na memória, que é do tipo TTimerParam. Então ao receber a mensagem no Objeto Windows eu apenas forço o entendimento do 2 parametro como ponteiro de uma variável desse tipo, assim eu acesso as informações que nela contém.

      Eu ainda não expliquei isso aqui no site, mas pra termos de ajuda:

       Delphi |  copy code |? 
      1
      type
      2
        PTimerParam = ^TTimerParam; // tipo que guarda um ponteiro de um objeto TTimerParam
      3
        TTimerParam = record // tipo que guarda uma string e um inteiro
      4
          s: string;
      5
          i: integer;
      6
        end;

      Assim é possível utilizar as funções de alocação/desalocação de memória por tipo, New e Dispose
      Exemplo:
       Delphi |  copy code |? 
      01
      var
      02
        OPonteiroDaMinhaVariavel: PTimerParam;
      03
       
      04
      procedure CriaEspacoNaMemoria;
      05
      begin
      06
        // crio um espaço na memória para guardar as informações que preciso
      07
        New(OPonteiroDaMinhaVariavel);
      08
        OPonteiroDaMinhaVariavel^.s := 'Meu texto';// um texto qualquer
      09
        OPonteiroDaMinhaVariavel^.i := 0001;// um número qualquer
      10
      end;
      11
       
      12
      procedure DestroiEspacoNaMemoria;
      13
      begin
      14
        Dispose(OPonteiroDaMinhaVariavel);
      15
        // aqui a memória foi desalocada, então não é possível mais ter acesso
      16
        // ao que estava lá.
      17
      end;

    3. Amigo…

      Atualmente estou desenvolvendo um objeto para comunicacao.
      Dentro desse objeto tenho outros que sao criados((parente).
      Voce sabe como faco para que o identifique como por exemplo num formulario (Objeto.Objeto.Objeto)

      Ex: form1.novoobjeto.novoObjeto

    4. Parabéns pelo Post.
      Eu preciso executar uma tarefa a cada milisegundo. Não consegui fazer com “while” nem com Timer pois ambos pulam alguns milisegundos.
      Tentei utilizar o SetTimer do seu post atual e mesmo assim ele pula alguns segundos.
      Eu dentro da funcao eu faço apenas

      FrmPrincipal.Memo1.Lines.Add(FormatDateTime(’hh:mm:ss:zzz’, now));

      Tem outra coisa para me sugerir?

    5. Cleriston,

      Exatidão dessa é muito difícil quando se tem uma interface gráfica pouco otimizada(interface de componentes), as que lhe permitiria ter uma exatidão dessas são apenas aquelas que tem aceleração gráfica e consomem menos tempo de processador para a parte visual.

      Uma maneira de aperfeiçoar a precisão é usando thread, assim você teria no mínimo um processamento exclusivo para a obtenção do intervalo desejado.

      Abraços.

    Leave a comment