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



    20
    May 08

    Delphi: CreateThread

    :: articles :: by Gilberto Saraiva

    Camaradas,

    O CreateThread é uma função disponibilizada pela plataforma Windows ( API ) cedida pela Kernel32, ela é responsável pela criação de um objeto Thread no windows que efetua operações no processador sendo gerenciado pelo windows na parte da fila de processamento e outros eventos.

     Delphi |  copy code |? 
    1
    function CreateThread(lpThreadAttributes: Pointer;
    2
      dwStackSize: DWORD; lpStartAddress: TFNThreadStartRoutine;
    3
      lpParameter: Pointer; dwCreationFlags: DWORD; var lpThreadId: DWORD): THandle; stdcall;
    4
    {$EXTERNALSYM CreateThread}

  • O 1º parametro é responsável pela segurança e compartilhamento do Objeto Thread que vai ser criado.
  • O 2º parametro é responsável pela definição do tamanho do Stack, ou seja, você pode definir o tamanho que vai ser utilizado pela Thread nos operadores ASM, em todo caso, se lhe falta de entendimento específico sobre o assunto é bom deixar em 0 que ele assume o tamanho padrão 1MB.
  • O 3º parametro é responsável pela definição do endereço inicial da thread, pra entendimento rápido é o ponteiro de uma function do mesmo padrão abaixo:
     Delphi |  copy code |? 
    1
    type
    2
      TThreadStartProc = function (Parameter: Pointer): Integer; stdcall;
  • O 4º parametro é responsável pela definição da “chave mestra” no processo de criação de uma thread, o ponteiro do parametro, podendo esse parametro ser qualquer coisa desde que persista enquanto a thread existir ou até o inicio da mesma.
  • O 5º parametro é responsável pelo modo em que a thread vai ser criada, Suspensa (CREATE_SUSPENDED) ou Não suspensa (0), a API poderá receber outro parametro, o STACK_SIZE_PARAM_IS_A_RESERVATION, mas esse não é muito utilizado pois não tem funcionamento na plataforma Windows 2000.
  • O 6º parametro é responsável pela recepção do ID da Thread na lista de Objetos Thead do Windows, esse valor serve para processos de identificação de origem do processamento através da API GetCurrentThreadId.
  • O retorno da API CreateThread é o Handle da Thread, como sabemos Handles são números que identificam objetos Windows e através de APIs podemos controla-los com muita especialidade.

    Agora, se você chegou até aqui, é porque realmente está interessado em aprender a utilizar as tão famosas thread, e a primeira coisa que lhe digo:

  • É simples, não tem mito nem urucubaca sobre a criação de thread para processamentos específicos.
  • Lembre-se Processamentos Específicos, se você quiser uma interação com algum componente na interface gráfica você precisa de criar uma sincronia com a VCL.

    Então agora sem o susto, podemos iniciar a utilização:

    Primeiramente baixe o código-fonte abaixo, abra o delphi e compile. Execute o aplicativo, clique em Run e veja que alguns processos são disparados, mais especificamente 100 processos paralelos.

    Nesse teste eu criei um pequeno controle de sincronia boolean feita através da variável SimpleLock, perceba que ao iniciar o processo de adicionar linha no TMemo mmoLog eu coloco o valor dela em True, indicando que estou processando algo, e logo no final ao terminar o processo eu coloco o SimpleLock como false, indicando que já terminei o processo. Assim antes de iniciar o processo eu verifico se o SimpleLock está em True, e se estiver eu espero até que ele esteja em False para iniciar um novo processamento.

    Agora partindo para o entendimento direto do código

    Escrevi um record, que servira de parametro para a minha função da Thread, como preciso de passar o parametro como ponteiro ( Pointer ) escrevi o tipo PMyParallelParams que nada mais é que uma indicação que o podemos guardar um ponteiro do objeto TMyParallelParams.

     Delphi |  copy code |? 
    01
    02
    type
    03
      PMyParallelParams = ^TMyParallelParams;
    04
      TMyParallelParams = record
    05
        A, B: integer;
    06
        ThreadName: string;
    07
        Handle: THandle;
    08
        Callback: procedure(AParam: PMyParallelParams) of object;
    09
      end;
    10

    A variável A é responsável por guardar o tamanho do passo ( Step Size ) que deve ser feito para se atingir o valor da variável B.
    ThreadName o próprio nome já diz tudo, é aonde guardamos o nome que idenfica a thread das demais.
    Handle é o Handle do Objeto Thread para manipulação da própria Thread durante o processamento.
    Callback é o procedimento que a Thread deve chamar no final do seu processamento.

    Perceba que todos esses valores foram criados por mim, e não fazem parte de um Thread nativa, ou seja, o código foi estruturado para funcionar com essa estrutura e nenhum processo é automático, todas as variáveis tem sua razão de existir, coisa que você podera escrever também para seus processamento próprios sendo específico para cada tipo de finalidade desejada.

    A declaração da SimpleLock para sincronia e do procedimento de Callback que informaremos para a nossa Thread.

     Delphi |  copy code |? 
    1
    2
        ...
    3
      private
    4
        { Private declarations }
    5
      public
    6
        SimpleLock: boolean;
    7
        procedure ReceiveThreadEnd(AParam: PMyParallelParams);
    8
      end;
    9

    Como podemos notar a criação de uma Thread para uma finalidade específica que não dependa de outros processos se torna quase completamente paralela, então no meu caso, coloquei um nome na função bem em conchavo com a especialidade da mesma.

     Delphi |  copy code |? 
    01
    function MyParallelProc(AParam: PMyParallelParams): Integer; stdcall;
    02
    var
    03
      i: integer;
    04
    begin
    05
      i := 0;
    06
      while i < AParam^.B do
    07
      begin
    08
        i := i + AParam^.A;
    09
        Sleep(100);
    10
      end;
    11
      AParam^.Callback(AParam);
    12
      Dispose(AParam);
    13
      EndThread(0);
    14
      CloseHandle(AParam^.Handle);
    15
      Result := 0;
    16
    end;

    O código é bem simples, algumas notações da parte final devem ser feitas:

  • AParam^.Callback(AParam); é a chamada que fazemos ao Callback.
  • Dispose(AParam); Como não precisamos mais do Parametro, liberamos a memória dele para não ocorrer os comuns vazamentos de memória ( memory leaks ).
  • EndThread(0); Finalizamos o processamento da Thread perante o Windows, indicando que o processamento não obteve erros, indicado pelo parametro 0. Só é possível utilizar a API EndThread quando o GetCurrentThread for igual ao Thread que queremos finalizar, e como estamos na própria Thread não há possibilidades de valor diferente.
  • CloseHandle(AParam^.Handle); Fechamos o Objeto Thread no windows, finalizamos todos os espaços gastos pelo Windows para sua criação e persistência.
  •  Delphi |  copy code |? 
    01
    var
    02
      i: integer;
    03
      dwNull: DWORD;
    04
      Params: PMyParallelParams;
    05
    begin
    06
      SimpleLock := false;
    07
      lblStillRunning.Caption := '100';
    08
      for i := 1 to 100 do
    09
      begin
    10
        New(Params);
    11
        Randomize;
    12
        Params^.A := Random(10) + 1;
    13
        Params^.B := Random(100);
    14
        Params^.ThreadName := 'Thread' + IntToStr(i);
    15
        Params^.Callback := ReceiveThreadEnd;
    16
        Params^.Handle := CreateThread(nil, 0, @MyParallelProc, Params,
    17
          0, dwNull);
    18
      end;

    Esse é o código de quando clicamos no TButton btnRun. Este código é o coração de todo nosso processo de entendimento de como iniciar um processo paralelo com o CreateThread, então atenção as explicações (Em tópicos para facilitar).

  • Primeiro passamos o SimpleLock para false, para indicar que nenhum processo está sendo efetuado.
  • Depois colocamos o caption no Label que informa quantas thread estão ainda rodando. No caso iniciamos em 100 pois criaremos 100 processos paralelos.
  • Ai vem o Loop que cria as Threads, de 1 até 100, contruindo 100 Thread ao todo.
  • New(Params);. New inicia um ponteiro tipificado, no nosso caso teremos espaço na memória para guardar os dados que precisamos em Params: PMyParallelParams. Esta parte é estruturada juntamente com o Dispose na function MyParallelProc, que efetua a liberação da memória utilizada pelo ponteiro.
  • Executamos o Randomize para aumentar a possibilidade de não obtenção de resultados semelhantes na utilização do Random.
  • Iniciamos a configuração do Params, que será passado para a Thread na function MyParallelProc. A variável A é configurada com um número randomico de 1 até 11.
  • A variável B é configurada com um número randomico de 0 até 100.
  • Colocamos o nome da Thread que iremos criar de acordo com o contador do loop.
  • Configuramos o Callback, sendo esse o procedimento ReceiveThreadEnd
  • Fazemos a chamada da API CreateThread com os parametros necessários.
    nil para definir que não teremos configuração de segurança nem tampouco de compartilhamento.

    0 para indicar que o StackSize vai ser o padrão, 1Mb.

    @MyParallelProc para indicar aonde está a function MyParallelProc que será o inicío da Thread.

    Params para indicar que o parametro a ser passado é o ponteiro do nosso record. Lembre-se que pra cada passo no Loop criamos um novo Params, no uso do New(), então até que nenhuma Thread chegue ao seu términio teremos 100 objetos TMyParallelParams na memória.

    0 para indicar que a Thread deve ser iniciada imediatamente após retornar o Handle da mesma.

    dwNull como não utilizaremos o ThreadID em nada, não precisamos guardar este valor.

  • A estrutura básica então é essa, não vou comentar muito sobre a parte da sincronia com a VCL pois isso será assunto para um outro artigo.

    Qualquer dúvida, sugestão, crítica ou agradecimentos, fique avontade.

    Lembrando mais uma vez, se você possuí material interessante sobre Delphi e outros,
    você pode postar o site na CommunityLinks

    Abraço a todos