ORINUT - Organizando o Input do Utilizador
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.
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! :)
Esta primeira versao vai ser bastante burra. Mesmo assim, vai conseguir
detectar quando eh que o buffer de eventos perde dados.
Passemos entao ah implementacao. Em primeiro lugar vamos precisar de umas
variaveis :
"Beware of NULL for the portal of dark debugging times lies there!"
-CodeWiz
-----------------------------------------
by CodeWiz
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!)
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.
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.
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.