ORINUT - Organizando o Input do Utilizador
-----------------------------------------
by CodeWiz

Ja quantas vezes nao estamos a escrever o interface de um programa e queremos por aquelas combinacoes especiais de teclas e acrescentar suporte para o rato ?

Os que ja o tentaram fazer devem-se lembrar que foi um pincel. O que era bom mesmo era arranjar maneira de juntar o input do utilizador num so sitio que depois seria onde nos iriamos buscar informacao sobre os inputs dados.

Nesta serie de artigos eh isso mesmo que vamos fazer. Quando chegarmos ao fim vamos ter uma unit em TP para lidar com isto (eu estou neste momento a fazer o port da unit para C... depois ei de a publicar em conjunto com a versao final desta em TP para que ninguem fique a chupar no dedo).

Comecemos pelo keyboard. O TP ja nos dah umas funcoes algo primitivas para ajudar ao controle do keyboard. Com um bocadinho de jeito ate conseguimos detectar as setas do cursos, as teclas dos F's mas pouco mais. Saber se foi o Right Shift ou o Left Shift que estava carregado eh nos impossivel. Por isso nao temos outra hipotese senao ir buscar as teclas directamente ao controlador do keyboard que eh uma peca de equipamento que esta neste momento por debaixo das teclas que voces devem tar a usar para ler isto (se estao a usar o mouse entao a explicacao do proximo numero sera mais realista em relacao ao vosso caso).

Primeiro vamos estudar o que acontece quando alguem/alguma coisa carrega numa tecla. Comecemos pelo principio...

In the beggining there was n0thing... (|x-y-z| -> It's a kind of magic...)

huh ? WHAT DO YOU MEAN I'M IN THE WRONG MOVIE! say what brother ? ... shit ... been droppin' too much acid...

Quando carregamos numa tecla a tecla vai pra baixo (DUH!) e faz contacto com uma malha de fios que estao ligados ao controlador do keyboard que muito gentilmente notifica o CPU via interrupt que alguem carregou numa tecla. A parte chata disto eh que o controlador ainda nao tem inteligencia artificial avancada o suficiente para fazer um scan da superficie da tecla e saber que caracter deve mandar. A unica coisa que o controlador do keyboard sabe eh que a tecla com o numero x foi carregada. A este numero os americanos (tipos espertos!) chamaram *scancode* ou se kiserem, para aqueles que programam ah demasiado tempo e o vosso corpo ja fabrique cobre (piadinha poh Speed 2), *keycode*.

Isto tudo eh muita baril mas pra que que serve. Bom entao eh assim. No DOS ah duas maneiras de tomarem conta do keyboard. Ou usam os servicos do BIOS (via o interrupt 16H) ou tomam conta do keyboard.

Eu como sou daqueles tipos a dar para o revolucionario acho que era fixe a gente tomar de assalto o interrupt 09h e tomar como refem o keyboard. Ker o keyboard queira quer nao... e eh isso mesmo que vamos fazer.

Uma das coisas que temos que decidir antes de tomarmos o int 09h de assalto eh decidir se vamos deixar que os servicos do BIOS continuem a funcionar. Se por um lado convem, visto que nos so vamos receber scancodes e o BIOS eh o unico que os sabe traduzir para caracteres ASCII por outro lado nao porque corriamos o risco de geral dois interrupts por tecla e nos podemos fazer a traducao com um minimo de trabalho e cuidado.

Nao nos vamos preocupar com a velocidade ou com ate que ponto e que isto eh a maneira ideal de fazer isto, de momento so queremos uma coisa que funcione e depois vamos entao tratar de optimizar e de tomar em conta certas coisas que acontecem.

Uma pequena nota. Este codigo eh bastante low-level e a teoria por tras nao eh nada de especial. De maneira que o que eu fiz foi documentar todos os passos de cada uma das funcoes. Ate porque ah coisas que se explicam melhor com codigo ah frente e este eh um desses casos. Espero que esteja acessivel a todos.

Em primeiro lugar, todas estas coisas que acontecem sao eventos. Por isso vamos ter de criar um array para guardar os eventos.

const
  MaxEventQueue = 511;

type
  TQueueEvent = array[0..15] of byte;
  TQueue = array[0..MaxEventQueue] of TQueueEvent;
Espero que os vossos programas nunca gerem mais de 512 eventos sem voces tratarem deles. Se isso acontecer vamos ter um pequeno problema de overrun o que vai fazer com que se percam dados. Mais! Se isto acontecer... deve estar algo de errado com o programa!!! (Ninguem vos manda fazer um jogo 3D com um engine the radiosity completo!)

Agora que ja temos um sitio para guardar os eventos bem ordenadinhos convem sabermos de que modo vamos guardar os eventos. Bom! Cada event vai ter um byte de identificacao sendo os restantes 15 bytes usados para guardar informacao sobre este event.

Resta-nos entao criar as constantes com os eventos para ajudar quando forem a ler codigo! :)

const
  EVN_None        = $00;
  EVN_KeyUp       = $03;
  EVN_KeyDown     = $05;
  EVN_CtrlBreak   = $07;
Aqueles de voces que estiverem atentos ah numeracao dos eventos vao achar esquesito porque que eu numerei os eventos assim quando era mais logico fazer 1..2..3..4..etc ate ao fim. Mas esta numeracao serve para ajudar a nossa primeira implementacao que vai ser toda em pascal! Os eventos do mouse quando passados para o event handler do mouse vem sobre a forma de uma mask que eh exactamente igual ao numero de ID de cada evento do mouse. Nunca pode haver dois eventos de mouse na mesma chamada do event handler do mouse. Assim poupa-se tempo na traducao do evento do mouse para o numero de ID. Os outros eventos sao numerados de forma a nao cairem em cima dos eventos do mouse e esses sim sao numerados por ordem crescente.

Esta primeira versao vai ser bastante burra. Mesmo assim, vai conseguir detectar quando eh que o buffer de eventos perde dados.

var
  EventsOverrun : boolean;
Bom e agora sao precisas funcoes para operar sobre os eventos.
function  GetNextEvent(var event : TQueueEvent) : boolean;
Esta funcao retorna o primeiro evento da lista.
procedure ClearEventQueue;
Esta limpa a lista de eventos.
procedure InitEventHandling;
Inicializa a libraria. Tem de ser chamada antes de se poder usar as outras funcoes todas da libraria.
procedure ShutDownEventHandling;
Antes de sair do programa convem chamar esta funcao.
function  IsEventAvailable : boolean;
A irma mais velha do keypressed! Quando kiserem saber se chegou algum evento chamem esta funcao.

Passemos entao ah implementacao. Em primeiro lugar vamos precisar de umas variaveis :

var
  Queue           : TQueue;
  NextFreeEvent,
  CurrEvent       : integer;
  OldKeybInt,
  OldMouseInt,
  OldCtrlBreakInt : pointer;
  OldMask         : word;
  Inited,
  EventsActive    : boolean;
Em seguida temos os interrupt handlers para o keyboard :
procedure DisableInts; inline($FA); { Esta funcao "desliga" os interrupts    }
procedure EnableInts;  inline($FB); { Esta funcao "liga" os interrupts       }

{+--------------------------------------------------------------------------+}
{|                                                                          |}
{+--------------------------------------------------------------------------+}

{$F+,S-,W-}

{ ////////////////////////////////////////////////////////////////////////// }
{ // KeybInt (interrupt handler)                                             }
{ ////////////////////////////////////////////////////////////////////////// }
{ // Este ISRH trata de ler a tecla do keyboard e gerar o evento             }
{ // correspondente.                                                         }
{ //                                                                         }
procedure KeybInt(Flags,CS,IP,AX,BX,CX,DX,SI,DI,DS,ES,BP : Word); interrupt;
var
  t : byte;

begin

  { //////////////////////////////////////////////////////////////////////// }
  { // Nao eh obrigatorio... mas pode evitar certas complicacoes             }
  { //                                                                       }
  DisableInts;

  { //////////////////////////////////////////////////////////////////////// }
  { // Leh a tecla do controlador do keyboard                                }
  { //                                                                       }
  t := port[$60];

  { //////////////////////////////////////////////////////////////////////// }
  { // So vamos processar esta tecla se estivermos a aceitar eventos         }
  { //                                                                       }
  if EventsActive then
  begin

    { ////////////////////////////////////////////////////////////////////// }
    { // Se o bit 7 do byte lido estiver a 0 entao esta tecla foi agora      }
    { // para baixo, caso contario veio para cima                            }
    { //                                                                     }
    Queue[NextFreeEvent][0] := EVN_KEYDOWN;

    if (t and $80) <> 0 then
    begin
      Queue[NextFreeEvent][0] := EVN_KEYUP;
    end;

    { ////////////////////////////////////////////////////////////////////// }
    { // Guarda a tecla que originou o evento. Convem remover o bit 7 da     }
    { // tecla visto que essa indicacao ja eh dado pelo tipo de evento       }
    { //                                                                     }
    Queue[NextFreeEvent][1] := (t and $7F);

    { ////////////////////////////////////////////////////////////////////// }
    { // Incrementa o index para o proximo evento                            }
    { //                                                                     }
    inc(NextFreeEvent);

    { ////////////////////////////////////////////////////////////////////// }
    { // Trata de fazer o wrap-around para evitar escrever fora do array!    }
    { //                                                                     }
    if nextfreeevent > MaxEventQueue then NextFreeEvent := 0;

    { ////////////////////////////////////////////////////////////////////// }
    { // Se depois de incrementar este index for igual ao index do evento    }
    { // que o programa vai ler a seguir entao temos uma situacao de overrun }
    { // que ainda nao aconteceu mas no caso de acontecer algum event antes  }
    { // do evento ser lido pelo programa entao estamos mal!!!               }
    { //                                                                     }
    if NextFreeEvent = CurrEvent then Overrun := TRUE;
  end;

  { //////////////////////////////////////////////////////////////////////// }
  { // Este pedaco de codigo faz um tipo de reset qq ao controlador do       }
  { // keyboard. Eu ja nao me lembro porque que tenho ke fazer isto mas sei  }
  { // que a respostas esta nas Norton User Guides acerca do keyboard.       }
  { //                                                                       }
  { // NOTA : Nao tenho a certeza mas se este codigo nao estiver presente    }
  { //        a libraria funcionan na mesma. DAMN! Where did I put those     }
  { //        Norton Guides!!!                                               }
  t := port[$61];
  port[$61] := t or $80;
  port[$61] := t;

  { //////////////////////////////////////////////////////////////////////// }
  { // Os interrupts ja podem voltar                                         }
  { //                                                                       }
  EnableInts;

  { //////////////////////////////////////////////////////////////////////// }
  { // Sinalizar o fim de interrupt handler ao hardware (PIC)                }
  { //                                                                       }
  port[$20] := $20;
end;

{+--------------------------------------------------------------------------+}
{|                                                                          |}
{+--------------------------------------------------------------------------+}

{ ////////////////////////////////////////////////////////////////////////// }
{ // CtrlBreakInt (interrupt handler)                                        }
{ ////////////////////////////////////////////////////////////////////////// }
{ // Este ISRH trata de sinalizar o Ctrl+Break ao programa.                  }
{ //                                                                         }
procedure CtrlBreakInt(Flags,CS,IP,AX,BX,CX,DX,SI,DI,DS,ES,BP :
Word);interrupt;
begin

  { //////////////////////////////////////////////////////////////////////// }
  { // Eu nao tenho nada contra os interrupts mas nao quero que algum destes }
  { // interrupt handler aconteca a meio de um outro e estrague as variaveis }
  { //                                                                       }
  DisableInts;

  { //////////////////////////////////////////////////////////////////////// }
  { // So vamos notificar o Ctrl+Break se estivermos a aceitar eventos       }
  { //                                                                       }
  if EventsActive then
  begin

    { ////////////////////////////////////////////////////////////////////// }
    { // Ctrl+Break                                                          }
    { //                                                                     }
    Queue[NextFreeEvent][0] := EVN_CTRLBREAK;

    { ////////////////////////////////////////////////////////////////////// }
    { // Incrementa o index para o proximo evento                            }
    { //                                                                     }
    inc(NextFreeEvent);

    { ////////////////////////////////////////////////////////////////////// }
    { // Trata de fazer o wrap-around para evitar escrever fora do array!    }
    { //                                                                     }
    if nextfreeevent > MaxEventQueue then NextFreeEvent := 0;

    { ////////////////////////////////////////////////////////////////////// }
    { // Se depois de incrementar este index for igual ao index do evento    }
    { // que o programa vai ler a seguir entao temos uma situacao de overrun }
    { // que ainda nao aconteceu mas no caso de acontecer algum event antes  }
    { // do evento ser lido pelo programa entao estamos mal!!!               }
    { //                                                                     }
    if NextFreeEvent = CurrEvent then EventsOverrun := TRUE;

  end;

  { //////////////////////////////////////////////////////////////////////// }
  { // Os interrupts ja podem voltar                                         }
  { //                                                                       }
  EnableInts;

  { //////////////////////////////////////////////////////////////////////// }
  { // Sinalizar o fim de interrupt handler ao hardware (PIC)                }
  { //                                                                       }
  port[$20] := $20;

end;

{$F-,S+}

{+--------------------------------------------------------------------------+}
{|                                                                          |}
{+--------------------------------------------------------------------------+}

{ ////////////////////////////////////////////////////////////////////////// }
{ // ClearEventQueue                                                         }
{ ////////////////////////////////////////////////////////////////////////// }
{ // Limpa a lista de eventos                                                }
{ //                                                                         }
procedure ClearEventQueue;
begin

  { //////////////////////////////////////////////////////////////////////// }
  { // Nao faz sentido limpar uma lista de eventos que ainda nem esta a ser  }
  { // usada!!!                                                              }
  { //                                                                       }
  if not Inited then exit;

  { //////////////////////////////////////////////////////////////////////// }
  { // Quando isto acontece quer dizer que a lista ou esta vazia ou esta     }
  { // prestes a sofrer um overrun. Neste caso eh porque a lista fica vazia  }
  { //                                                                       }
  CurrEvent := NextFreeEvent;

  { //////////////////////////////////////////////////////////////////////// }
  { // Se a lista esta vazia nao pode estar a acontecer um overrun           }
  { //                                                                       }
  EventsOverRun := FALSE;

end;

{+--------------------------------------------------------------------------+}
{|                                                                          |}
{+--------------------------------------------------------------------------+}

{ ////////////////////////////////////////////////////////////////////////// }
{ // InitEventHandling                                                       }
{ ////////////////////////////////////////////////////////////////////////// }
{ // Inicializa tudo e mais alguma coisa de maneira a que se possa receber   }
{ // os eventos                                                              }
{ //                                                                         }
procedure InitEventHandling;
begin

  { //////////////////////////////////////////////////////////////////////// }
  { // Inicializar duas vezes seguidas tem dois problemas                    }
  { //                                                                       }
  { //   1 - Eh estupido!                                                    }
  { //                                                                       }
  { //   2 - Nao dava jeito nenhum ter os handlers antigos iguais aos novos  }
  { //       (vejam o codigo para perceber)                                  }
  { //                                                                       }
  if Inited then exit;

  { //////////////////////////////////////////////////////////////////////// }
  { // Convem inicializar umas quantas variaveis para evitar confusoes       }
  { //                                                                       }

  { //////////////////////////////////////////////////////////////////////// }
  { // Nao convem que assim que se mudem os handlers este comecem a criar    }
  { // eventos                                                               }
  { //                                                                       }
  EventsActive  := FALSE;

  { //////////////////////////////////////////////////////////////////////// }
  { // Eh dificil fazer um overrun logo ao principio                         }
  { //                                                                       }
  EventsOverrun := FALSE;

  { //////////////////////////////////////////////////////////////////////// }
  { // A lista de eventos encontra-se vazia                                  }
  { //                                                                       }
  CurrEvent     := 0;
  NextFreeEvent := CurrEvent;

  { //////////////////////////////////////////////////////////////////////// }
  { // Guardar os interrupt handler antigos                                  }
  { //                                                                       }
  GetIntVec($1B,OldCtrlBreakInt);
  GetIntVec($09,OldKeybInt);

  { //////////////////////////////////////////////////////////////////////// }
  { // Instalar os novos interrupt handlers                                  }
  { //                                                                       }
  SetIntVec($1B,Addr(CtrlBreakInt));
  SetIntVec($09,Addr(KeybInt));

  { //////////////////////////////////////////////////////////////////////// }
  { // Se chegamos aqui entao a inicializao correu bem e nesse caso podemos  }
  { // ligar os eventos                                                      }
  { //                                                                       }
  Inited       := TRUE;
  EventsActive := TRUE;

end;

{+--------------------------------------------------------------------------+}
{|                                                                          |}
{+--------------------------------------------------------------------------+}

{ ////////////////////////////////////////////////////////////////////////// }
{ // ShutDownEventHandling                                                   }
{ ////////////////////////////////////////////////////////////////////////// }
{ // Esta na hora de fechar a loja. Toca a por tudo como estava quando       }
{ // comecamos                                                               }
{ //                                                                         }
procedure ShutDownEventHandling;
begin

  { //////////////////////////////////////////////////////////////////////// }
  { // Desinicializar duas vezes seguidas tem um problema                    }
  { //                                                                       }
  { //   1 - Eh estupido!                                                    }
  { //                                                                       }
  if not Inited then exit;

  { //////////////////////////////////////////////////////////////////////// }
  { // Desligar os eventos                                                   }
  { //                                                                       }
  EventsActive := FALSE;

  { //////////////////////////////////////////////////////////////////////// }
  { // Por os handler antigos no sitio                                       }
  { //                                                                       }
  SetIntVec($1B,OldCtrlBreakInt);
  SetIntVec($09,OldKeybInt);

  { //////////////////////////////////////////////////////////////////////// }
  { // Limpar a lista de eventos                                             }
  { //                                                                       }
  CurrEvent     := 0;
  NextFreeEvent := CurrEvent;

  { //////////////////////////////////////////////////////////////////////// }
  { // Missao bem sucedida. Esta tudo como estava quando entramos            }
  { //                                                                       }
  Inited        := FALSE;

end;

{+--------------------------------------------------------------------------+}
{|                                                                          |}
{+--------------------------------------------------------------------------+}

{ ////////////////////////////////////////////////////////////////////////// }
{ // GetNextEvent                                                            }
{ ////////////////////////////////////////////////////////////////////////// }
{ // Preenche com uma copia do primeiro evento da lista o buffer passado ah  }
{ // funcao                                                                  }
{ //                                                                         }
function GetNextEvent(var event : TQueueEvent) : boolean;
begin

  { //////////////////////////////////////////////////////////////////////// }
  { // Convem presumir que nao vamos conseguir                               }
  { //                                                                       }
  GetNextEvent := FALSE;

  { //////////////////////////////////////////////////////////////////////// }
  { // Se a lista estiver vazia ou a libraria ainda nao tiver sido           }
  { // inicializada entao sai                                                }
  { //                                                                       }
  if (not inited) or (CurrEvent=NextFreeEvent) then exit;

  { //////////////////////////////////////////////////////////////////////// }
  { // Copia o evento                                                        }
  { //                                                                       }
  Event := Queue[CurrEvent];

  { //////////////////////////////////////////////////////////////////////// }
  { // Incrementa o index para o proximo evento                              }
  { //                                                                       }
  inc(CurrEvent);

  { //////////////////////////////////////////////////////////////////////// }
  { // Trata de fazer o wrap-around para evitar ler fora do array!           }
  { //                                                                       }
  if CurrEvent > MaxEventQueue then CurrEvent := 0;

  { //////////////////////////////////////////////////////////////////////// }
  { // Sucesso!                                                              }
  { //                                                                       }
  GetNextEvent := TRUE;

end;

{+--------------------------------------------------------------------------+}
{|                                                                          |}
{+--------------------------------------------------------------------------+}

{ ////////////////////////////////////////////////////////////////////////// }
{ // IsEventAvailable                                                        }
{ ////////////////////////////////////////////////////////////////////////// }
{ // Retorna TRUE se estiver algum evento ah espera e se a libraria ja tiver }
{ // sido inicializada. Caso contrario retorna FALSE                         }
{ //                                                                         }
function  IsEventAvailable : boolean;
begin

  { //////////////////////////////////////////////////////////////////////// }
  { // Quem nao perceber esta linha veja este bocado de codigo :             }
  { //                                                                       }
  { // IsEventAvailable := FALSE;                                            }
  { // if not Inited then exit;                                              }
  { // if (CurrEvent = NextFreeEvent) then exit;                             }
  { // IsEventAvailable := TRUE;                                             }
  { //                                                                       }
  { // Esta linha em baixo faz isto tudo. Compressao 4:1. Eat your heart     }
  { // out DriveSpace!!!                                                     }
  { //                                                                       }
  IsEventAvailable := ((not (CurrEvent=NextFreeEvent)) and Inited);

end;

{+--------------------------------------------------------------------------+}
{|                                                                          |}
{+--------------------------------------------------------------------------+}

{ ////////////////////////////////////////////////////////////////////////// }
{ // Main                                                                    }
{ ////////////////////////////////////////////////////////////////////////// }
{ // Por uma questao de boas maneiras inicializa as variaveis para valores   }
{ // seguros                                                                 }
{ //                                                                         }
begin
  NextFreeEvent := 0;
  CurrEvent     := 0;
  Inited        := FALSE;
  EventsActive  := FALSE;
  EventsOverrun := FALSE;
end.



código - events.pas Desta vez eh tudo. Na proxima semana vamos acrescentar eventos para o mouse e comecar a estudar o comportamento de certas teclas mais esquisitas que tem dois, tres e mais scancodes.

"Beware of NULL for the portal of dark debugging times lies there!"

-CodeWiz