int21h

Klávesnice od A do Z

Vstup z klávesnice je zcela základní část funkce programu. Podobně jako s mnoha jinými aspekty programování je to tak, že jestli se spokojíme s málem, tak je to triviální. Pokud ale chceme trochu více, začnou se záležitost neuvěřitelně komplikovat.
Podívejme se tedy nejprv na ony jednoduché začátky:

Readln

Začátečník se nepochybně setká nejprve s procedurou readln. Sice vím, že mí čtenáři žádní začátečníci nejsou, ale na chvíli se u readln zastavme. Nepodceňujme readln! Je to úžasně silný prostředek jazyka pascal. Umožňuje totiž načíst proměnné různých typů: čísla, řetězce a znaky. Dokonce umožňuje načíst jich proměnlivý počet. V neposlední řadě můžeme pomocí readln číst ze souborů a z libovolného zařízení DOSu. Je důležité si také uvědomit, že vstup se ukončuje klávesou enter. Existuje ještě varianta read, která ohraničuje vstup trochu jinak. Skoro nikdo ji neumí suverénně používat a většina programátorů se jí vyhýbá. V každé, případě se ale hodí pouze pro načítání ze souborů - ne pro vstup z klávesnice.
Ještě si u read/readln všimněme, co se stane, načítáme-li proměnnou číselného typu a uživatel zadá (omylem) něco jiného než číslo. Vyvolá se běhová chyba č.106
Dosti nepraktické že?
Praktické ale je, že můžeme používat Backspace.

Shrňme tedy dojmy z read/readln.
Jsou to vysokoúrovňové funkce. Hodí se pro "vážné programy", ale ne třeba pro hry, protože:
* program během vstupu z klávesnice stojí
* vstup z klávesnice musí být ukončen klávesou Enter
* je vidět vypisovaný text

Readkey

Pro hry je praktičtější funkce readkey. Ta se naopak pro hry celkem hodí. V bodech si zase vypíchneme charakteristické znaky této funkce:
* vstup je ukončen po jednom znaku
* vypisovaný text není vidět
* při použití společně s funkcí Keypressed program pořád běží a nestojí
Použití ale není tak přímočaré jako u readln. Jak už jsem zmínil, funkce m;že být kombinována s Keypressed.
Dodejme ještě, že obě funkce jsou obsaženy v jednotce Crt. Na začátku programu proto musí být
uses Crt;
Příklad použití:
uses Crt;
var c:char;

begin
repeat
repeat
{Cykl neustale bezi. V tomto bloku byvaji akce, ktere se maji provadet i
kdyz uzivatel vuvec nesaha na klavesnici}
until Keypressed; {Pri stisku klavesy vypadne z cyklu}
c:=ReadKey;       {zjistime, co to vlastne bylo za klavesu}


case c of         {takze co to bylo?}
   #13:writeln;
   #59:writeln(#13#10,'F1');
   #60:writeln(#13#10,'F2');
   #61:writeln(#13#10,'F3');
   else write(c);

end;
until c=#27;  {pri stisku ESC konec}
end.  
Zdroják je myslím dostatečně jednoduchý. Všimněte si, že oproti readln reaguje Readkey na více kláves. Například F-ka, šipky, atd.
Apropo šipky. Zkuste zmáčknout nějakou šipku. Proč se na obrazovce napsalo písmeno? Nemačkali jsme přece písmeno, ale šipku! Inu, klávesnice prostě fungují trochu divně :-)
Jde o to, že některé klávesy vracejí tzv. rozšířený kód. Při stisku klávesy vracejí nevracejí jeden bajt, ale dva bajty. Napřed vyšlou nulu a až potom vlastní kód. Je proto rozumné napsat si vlastní funkci, která se s tímto jevem dokáže nějak poprat.
uses crt;


Function MyKey:word;
var c:word;
begin
c:=word(Readkey);

if c=0 then MyKey:=256+word(Readkey) else MyKey:=c;

end;


var c:word;
begin
repeat
repeat until Keypressed;
c:=MyKey;

writeln(c);
until c=27;       {pri stisku ESC konec}
end.  

Takovouto jednoduchou funkcí si ušetříme spoustu práce!
Teď ale něco zkusme. Udělejme pokus!
Za řádku writeln(c); zkuste vložit delay(800);
Spus?te program, zmáčkněte nějakou klávesu a nepouštějte.
Za chvíli začne počítač pípat a i po uvolnění klávesnice to pořád vypadá, jako byste klávesu pořád drželi. Došlo totiž k přeplnění bufferu klávesnice. To je klasický problém. Data přicházejí do klávesnice rychleji než je odebíráme.
Je proto důležité umět mazat buffer klávesnice:
Dělá se to třeba takto:
while keypressed do readkey;  
Často budete ve svých programech čekat na stisk libovolné klávesy. (Press any key to continue). Jak to nejlépe napsat?
Procedure AnyKey;
begin
repeat until Keypressed;
while Keypressed do readkey;

end;  
Během experimentování jste asi zjistili, že i když ReadKey umí detekovat více kláves než Readln, tak přesto neodhalí všechny. Neodhalí stisk altů, controlů a dalších.
To jsou takzvané funkční klávesy, které fungují poněkud jinak než běžné klávesy. Více o tomto tématu najdete třeba v AThelpu.
Také jste patrně zjistili, že ReadKey nedokáže detekovat více kláves zmáčknutých současně. No jo, ale co by to bylo za hru, kdyby nedokázala zpracovat více stisknutých kláves současně?

Obsazení přerušení

Pročetli jste se do vysoké školy programování :-)
Abychom dokázali detekovat více stisknutých kláves současně, musíme tak blízko hardwaru, jak to jen jde. Musíme obsadit hardwarové přerušení. Konkrétně se jedná o IRQ 1 neboli INT 9
Pověsíme se tedy na přerušení INT9h a budeme sledovat port 60h.
Port 60h totiž patří klávesnici a z něho budeme načítat hodnoty kláves. Pozor! Nejde o ASCII kódy, ale o polohové kódy - scan kódy. ASCII kódy vytváří ze scan kódů BIOS a DOS (resp. ovladač klávesnice). My se ale dostaneme ke scan kódům ještě dřív než oni, takže ASCII zatím nejsou k dispozici.
Přerušení funguje tak:
Při každém stisku klávesy se vygeneruje INT 9 a na portu 60h se objeví scan kód klávesy. Při jejím uvolnění se rovněž vyvolá INT 9 a na portu 60h se objeví scan kód klávesy +128
Obvyklá programátorská realizace řešení spočívá ve vytvoření pole
var klavesy:array[0..127] of boolean
Každý prvek pole představuje možný scan kód a tedy jednu klávesu. Pokud je daná klávesa stisknutá, má prvek hodnotu TRUE, pokud není, je FALSE.
Kritický kód vypadá třeba takto:
var k:byte;
begin
k:=Port[$60];
klavesy[k mod 128]:=(k<128);  
O programování přerušení klávesnice jsem už psal v jiném článku, a to DOS a chráněný režim - 1. díl
Tam, mimo jiné, uvidíte kompletní zdrojáky pro Freepascal. Všimněte si, že buďto můžeme přerušení obsloužit sami a BIOS s DOSem k němu vůbec nepustit nebo si uděláme, co potřebujeme a pak předáme řízení na původní handler.
Nicméně ani tohle řešení není optimální a definitivní.
Když to vyzkoušíte, tak zjistíte, že uvedené zdrojáky nedokáží odlišit např. šedé šipky od šipek na numerické klávesnici. Také nevidí klávesu pause, printscreen a další.
Čím to je?
V uvedených prográmcích předpokládám, že z portu příjde vždy jedna jednoznačná hodnota. Jenže ona to bohužel není pravda :-(
Takhle to bylo u těch nejstarších klávesnic. S počítači třídy AT přišly i nové klávesnice a aby zůstaly zpětně kompatibilní, tak se jejich ovládání paradoxně zesložitilo. Na XT klávesnicích byly šipky jen na numerické klávesnici, jenže na AT jsou i šedé šipky. A šedé šipky napřed vyšlou prefixový kód E0h a potom scan kód šipky, který je totožný s polohovým kódem numerické šipky.
Jestli byla stisknuta šedá nebo numerická šipka tedy odlišíme jen podle toho, zda před kódem šipky přišel prefix. Analogický problém je u Altů, Controlů, Enterů, Insert, Home, atd...
Doporučuji vám podívat se do ATHelpu na stránku Scan kódy klávesnice
Smutné zjištění je, že některé klávesy posílají podstatně divočejší sekvence než jen E0h, scan kód. Největší svinsto posílá pause. Pause má dokonce tu zvláštnost, že nevysílá kód při uvolnění klávesy! Pozoruhodné je, že i některé kombinace kláves mají samostatný kód. Aby byla situace ještě horší, tak některé klávesy periodicky opakují kód stisknutí klávesy. Např. když stisknete klávesu A, tak vyšle kód 1Eh. Během doby, kdy ji držíte, nevysílá nic. Až při uvolnění vyšle 158. 158=1Eh(30d)+128
Jenže když zmáčknete Alt (levý nebo pravý), tak během doby, co ho držíte, klávesnice periodicky opakuje jeho kód tak dlouho, dokud ho nepustíte. Stejně se chová i Printscreen. Ten to ještě komplikuje tím, že opakovací kódy nejsou totožné s prvotním kódem. Printscreen je mimochodem privilegovaná klávesa Windows. Když jste ve windows, tak se na Printscreen nedostanete, protože ho zpracovávají přímo Windows v režimu RING 0. Stejně se nedostanete na CTRL-ALT-DEL nebo ALT-Tab a některé jiné. V čistém DOSu samoyřejmě ano, ale ve windows ne.
Raději ani nepomyslet, jak se chovají nadstardandní klávesy všelijakých moderních, klávesnic, jak mají zvláštní tlačítka pro rychlé spouštění vybraných aplikací.
Vra?me se ještě k těm šipkám. Víme tedy jak rozlišit, jestli byla zmáčknuta šedá nebo numerická šipka. V některých případech nás to bude skutečně zajímat, ale ve většině případů nikoliv. ?ipka jako šipka. Jak to ale skloubit? Doporučuji definovat si v poli stisknutých kláves prvky pro pomyslnou klávesu "obecná šipka". V handleru napřed skutečně rozlišíme o jakou šipku šlo, nastavíme příslušné prvky pole, ale kromě toho nastavíme ještě "obecnou šipku". Při tom nesmíme zapomenout na možnost, že uživatel může držet současně např. šedou š. dolů + numerickou š. dolů, pustí numerickou (šedou pořád drží) a tudíž je obecná šipka dolů pořád zmáčklá, ačkoliv došlo k puštění klávesy.
Jsou to ale radosti, co?
Zkrátka napsat pořádný ovladač klávesnice není žádná sranda. Já jsem si tu práci dal za vás a v tomto archívu si můžete stáhnout kompletní unit na správu klávesnice přes INT 9.
Na konci článku ještě uvidíte zdroják souboru rezklav.pas
Ten vám ale samotný k přeložení stačit nebude, protože je nutný i REZKLAV.INC
Jednotka je "obojživelná" - funguje i ve FP i v reálném režimu TP. Má jednu vychytávku navíc. Užitečná vlastnost BIOSového ovladače klávesnice je možnost zadávání kódu znaku přes ALT + číselný kód na numerické klávesnici. Moje jednotka to umí taky a dokonce vám dovolí zadat až pětimístný kód (to kvůli podpoře unicode znaků)

REZKLAV.PAS
unit rezklav;
{Rezidentni obsluha klavesnice. Cte polohove kody, ktere pak pomoci tabulek}
{prevedu do ASCII znaku. Vse si udelam sam a nebudu zavisly na DOSovych}
{ovladacich klavesnice}
{Tato unita umi rozlisovat mezi normalnimi sipkami a sipkami na numericke
klavesnici. Pro pohodli programatora ale rovnez definuje kody, ktere mezi
nimi neodlisuji.
napr. Sipka vpravo na numericke klavesnici dava:
KEY_NUM_6 = true a KEY_CURSOR_RIGHT = true
Seda sipka vpravo dava:
KEY_G_CURSOR_RIGHT = true a KEY_CURSOR_RIGHT = true.
Vidite, ze KEY_CURSOR_RIGHT zustava spolecne.


Jestlize zavolate ZapniObsluhuKlavesnice s parametrem KL_S_BIOSEM, tak dal
budou normalne fungovat standardni funkce klavesnice jako
ReadKey, KeyPressed a vubec cely BIOS okolo klavesnice. Na druhou stranu se
musite starat, aby klavesnice nepipala.
Kdyz zavolate ZapniObsluhuKlavesnice(KL_BEZ_BIOSU) tak pipat zarucene nebude,
ale prestanou fungovat standardni pascali funkce okolo klavesnice.}



{Pri pohledu do zdrojaku zarazi velice bizarni chovani klavesy Pause.
Posila velice exotickou sekvenci a hlavne nesignalizuje uvolneni klavesy.
Zvlastne se taky taky chova Printscreen. Pri zmacknuti totiz vysle 4-bajtovou
sekvenci. Pokud ho ale nepustime, ale drzime dal, tak periodicky vysila
jinou, a to 2-bajtovou sekvenci. Periodicke vysilani maji rovnez oba Alty.}




{$IFDEF FPC}{$MODE FPC}{$ENDIF}
{$Q-}     {debugovaci informace musi byt v kazdem pripade povypinana}

{$R-}     {jinak se to cele zhrouti}
{$S-}
{$D-}
{$F+}
{$DEFINE OPATRNOST}   {Zda se, ze je mozne ji vypnout...}


interface

{$IFDEF FPC}
uses go32;
{$ELSE}
uses dos;
{$ENDIF}


const KL_S_BIOSEM  = true;
      KL_BEZ_BIOSU = false;

procedure ZapniObsluhuKlavesnice(rezim:boolean);
procedure VypniObsluhuKlavesnice;


{$I rezklav.inc}  {kody klaves}
var kl_kod:word;
    kl_zmena:boolean;
    vsechny_klavesy : Array[0..160] of boolean;
    AltBuf:word;



implementation
const kbdint = $9;
      levy_shift = 42;
      pravy_shift = 54;
      odpocet:byte=0;


var

    {$IFDEF FPC}
    oldint9_handler:tseginfo;
    newint9_handler:tseginfo;
    backupDS:Word; external name '___v2prt0_ds_alias';
    {$ELSE}
    oldint9_handler:pointer;
    newint9_handler:pointer;
    {$ENDIF}


    paltbuf:array[0..6] of byte; {je to zahada, ale se stringem mi to nefungovalo}
    obsluha:pointer;
    bios:boolean;



procedure CtiKod;{$IFNDEF FPC}interrupt;{$ENDIF}
  procedure ZpracujJednotu(alternativa,spolecna,ja:byte);
  begin
  if ja<>0 then vsechny_klavesy[ja]:=(kl_kod<128);
  if kl_kod<128 then vsechny_klavesy[spolecna]:=true else

     if vsechny_klavesy[alternativa]=false then
        vsechny_klavesy[spolecna]:=false;
  end;


  procedure PrectiAltovyBuffer;
  var i,j:longint;
  begin

  if vsechny_klavesy[KEY_ALT]=false then
     begin
     altbuf:=0;
     j:=1;
     for i:=paltbuf[0] downto 1 do
         begin

         altbuf:=altbuf+paltbuf[i]*j;
         j:=j*10;
         end;
     paltbuf[0]:=0;
     end;
  end;


var klmod128,op:byte;
    ab:byte;



begin
{$IFDEF FPC}kl_kod:=InPortB($60);{$ELSE}kl_kod:=Port[$60];{$ENDIF}
if kl_kod=$e1 then

   if odpocet=0 then odpocet:=7 else else {krkolome osetreni Pause}

if kl_kod=$e0 then
   if odpocet=0 then odpocet:=2 else else


if odpocet<3 then
   begin
   kl_zmena:=true;
   klmod128:=kl_kod mod 128;


   vsechny_klavesy[KEY_PAUSE]:=false;     {vopruz z Pause}

   vsechny_klavesy[KEY_CTRLBREAK]:=false; {stejne se chova CTRL-break}


   op:=odpocet;
   odpocet:=0;
   case op of

      2:case klmod128 of
           69:vsechny_klavesy[KEY_PAUSE]:=true;         {Pause}
           70:vsechny_klavesy[KEY_CTRLBREAK]:=true;     {CTRL-break}

           55:vsechny_klavesy[KEY_PRINT]:=(kl_kod<128); {Printscreen poprve}
        end;


      1:case klmod128 of

           71..83:ZpracujJednotu(klmod128,klmod128+Priznak_jednoty,klmod128+Priznak_sedych_sipek);
           28:ZpracujJednotu(KEY_G_ENTER,KEY_ENTER,KEY_NUM_ENTER);{sedy Enter}
           56:begin
              ZpracujJednotu(KEY_LALT,KEY_ALT,KEY_PALT);          {pravy Alt}

              PrectiAltovyBuffer;
              end;
           29:ZpracujJednotu(KEY_LCTRL,KEY_CTRL,KEY_PCTRL);       {pravy Ctrl}
           55:vsechny_klavesy[KEY_PRINT]:=(kl_kod<128); {Printscreen opakovane}
           42,70:odpocet:=4;  {4-bajtova sekvence E0,neco,E0,neco}

           else vsechny_klavesy[klmod128]:=(kl_kod<128);
        end;


      0:begin
        vsechny_klavesy[klmod128]:=(kl_kod<128);
        case klmod128 of

           71..83:ZpracujJednotu(klmod128+Priznak_sedych_sipek,klmod128+Priznak_jednoty,klmod128);
           28:ZpracujJednotu(KEY_NUM_ENTER,KEY_ENTER,0); {num Enter}
           56:begin
              ZpracujJednotu(KEY_PALT,KEY_ALT,0);        {levy Alt}

              PrectiAltovyBuffer;
              end;
           29:ZpracujJednotu(KEY_PCTRL,KEY_CTRL,0);      {l. Ctrl}
           42:ZpracujJednotu(KEY_PSHIFT,KEY_SHIFT,0);    {l. Shift}

           54:ZpracujJednotu(KEY_LSHIFT,KEY_SHIFT,0);    {p. Shift}
        end;


        if (kl_kod<128) and (vsechny_klavesy[KEY_ALT]) then

           begin {drzime Alt a pritom byla}{zmacknuta klavesa na numericke klavesnici?}
           case kl_kod of
              71:ab:=7;
              72:ab:=8;
              73:ab:=9;
              75:ab:=4;
              76:ab:=5;
              77:ab:=6;
              79:ab:=1;
              80:ab:=2;
              81:ab:=3;
              82:ab:=0;
              56:ab:=11;    {komplikace - periodicke vysilani Altu}

              else ab:=10;
           end;
           if ab<>11 then
              if ab=10 then paltbuf[0]:=0 else

                 if paltbuf[0]<5 then
                    begin
                    inc(paltbuf[0]);
                    paltbuf[paltbuf[0]]:=ab;
                    end

                    else begin
                    paltbuf[0]:=1;
                    paltbuf[1]:=ab;
                    end;
           end;
        end;
   end; {case odpocet}

   if kl_kod>127 then kl_kod:=0 else
      if vsechny_klavesy[KEY_SHIFT] then inc(kl_kod,1000); {musim mit na pameti shifty}

   end;


if odpocet>0 then dec(odpocet);


{$IFDEF FPC}
if bios=false then

   OutPortB($20,$20);


{$ELSE}
if bios=false then
   Port[$20]:=$20 else

   begin
   asm
   call oldint9_handler
   end;
   end;
{$ENDIF}
end;
procedure CtiKod_dummy; begin end;



{$IFDEF FPC}
procedure int9_handler; assembler;
asm
cli
{$IFDEF OPATRNOST}
push ds
push es
push fs
push gs
pusha
{$ENDIF}
   mov ax,cs:[backupDS]
   mov ds,ax
   mov es,ax
   mov ax,dosmemselector
   mov fs,ax
   call obsluha

{$IFDEF OPATRNOST}
popa
pop gs
pop fs
pop es
pop ds
{$ENDIF}
jmp cs:[oldint9_handler]
sti
end;
procedure int9_dummy; begin end;


procedure int9_nbhandler; assembler;interrupt;

asm
cli
{$IFDEF OPATRNOST}
push ds
push es
push fs
push gs
pusha
{$ENDIF}
   mov ax,cs:[backupDS]
   mov ds,ax
   mov es,ax
   mov ax,dosmemselector
   mov fs,ax
   call obsluha
{$IFDEF OPATRNOST}
popa
pop gs
pop fs
pop es
pop ds
{$ENDIF}
sti
end;

procedure int9_nbdummy; begin end;
{$ENDIF FPC}


procedure ZapniObsluhuKlavesnice(rezim:boolean);
begin

kl_zmena:=false;
bios:=rezim;
altbuf:=0;
FillChar(paltbuf,sizeof(paltbuf),0);
FillChar(vsechny_klavesy,sizeof(vsechny_klavesy),0);
obsluha:=@CtiKod;



{$IFDEF FPC}
lock_data(obsluha, sizeof(obsluha));
lock_data(kl_kod, sizeof(kl_kod));
lock_data(bios, sizeof(bios));
lock_data(kl_zmena, sizeof(kl_zmena));
lock_data(odpocet, sizeof(odpocet));
lock_data(paltbuf, sizeof(paltbuf));
lock_data(altbuf, sizeof(altbuf));
lock_data(vsechny_klavesy,sizeof(vsechny_klavesy));
lock_data(dosmemselector, sizeof(dosmemselector));


lock_code(@CtiKod,longint(@CtiKod_dummy) - longint(@CtiKod));

if bios then
   begin
   lock_code(@int9_handler,longint(@int9_dummy)-longint(@int9_handler));
   newint9_handler.offset:=@int9_handler;
   end
   else begin

   lock_code(@int9_nbhandler,longint(@int9_nbdummy)-longint(@int9_nbhandler));
   newint9_handler.offset:=@int9_nbhandler;
   end;
newint9_handler.segment:=get_cs;
get_pm_interrupt(kbdint, oldint9_handler);
set_pm_interrupt(kbdint, newint9_handler);
{$ELSE}
newint9_handler:=@ctikod;
GetIntVec(kbdint, oldint9_handler);
SetIntVec(kbdint, newint9_handler);
{$ENDIF}

end;


procedure VypniObsluhuKlavesnice;
begin
{$IFDEF FPC}
set_pm_interrupt(kbdint, oldint9_handler);
unlock_data(dosmemselector, sizeof(dosmemselector));
unlock_data(kl_kod, sizeof(kl_kod));
unlock_data(bios, sizeof(bios));
unlock_data(kl_zmena, sizeof(kl_zmena));
unlock_data(odpocet, sizeof(odpocet));
unlock_data(paltbuf, sizeof(paltbuf));
unlock_data(altbuf, sizeof(altbuf));
unlock_data(vsechny_klavesy,sizeof(vsechny_klavesy));
unlock_data(obsluha, sizeof(obsluha));


unlock_code(@CtiKod,longint(@CtiKod_dummy) - longint(@CtiKod));

if bios then
   unlock_code(@int9_handler,longint(@int9_dummy)-longint(@int9_handler)) else
   unlock_code(@int9_nbhandler,longint(@int9_nbdummy)-longint(@int9_nbhandler));

{$ELSE}
SetIntVec(kbdint, oldint9_handler);
{$ENDIF}
end;
end.  
2007-03-06 | Laaca
Reklamy: