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
until Keypressed;
c:=ReadKey;
case c of
#13:writeln;
#59:writeln(#13#10,'F1');
#60:writeln(#13#10,'F2');
#61:writeln(#13#10,'F3');
else write(c);
end;
until c=#27;
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;
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;
interface
uses go32;
uses dos;
const KL_S_BIOSEM = true;
KL_BEZ_BIOSU = false;
procedure ZapniObsluhuKlavesnice(rezim:boolean);
procedure VypniObsluhuKlavesnice;
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
oldint9_handler:tseginfo;
newint9_handler:tseginfo;
backupDS:Word; external name '___v2prt0_ds_alias';
oldint9_handler:pointer;
newint9_handler:pointer;
paltbuf:array[0..6] of byte;
obsluha:pointer;
bios:boolean;
procedure CtiKod;interrupt;
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
kl_kod:=InPortB($60);kl_kod:=Port[$60];
if kl_kod=$e1 then
if odpocet=0 then odpocet:=7 else else
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;
vsechny_klavesy[KEY_CTRLBREAK]:=false;
op:=odpocet;
odpocet:=0;
case op of
2:case klmod128 of
69:vsechny_klavesy[KEY_PAUSE]:=true;
70:vsechny_klavesy[KEY_CTRLBREAK]:=true;
55:vsechny_klavesy[KEY_PRINT]:=(kl_kod<128);
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);
56:begin
ZpracujJednotu(KEY_LALT,KEY_ALT,KEY_PALT);
PrectiAltovyBuffer;
end;
29:ZpracujJednotu(KEY_LCTRL,KEY_CTRL,KEY_PCTRL);
55:vsechny_klavesy[KEY_PRINT]:=(kl_kod<128);
42,70:odpocet:=4;
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);
56:begin
ZpracujJednotu(KEY_PALT,KEY_ALT,0);
PrectiAltovyBuffer;
end;
29:ZpracujJednotu(KEY_PCTRL,KEY_CTRL,0);
42:ZpracujJednotu(KEY_PSHIFT,KEY_SHIFT,0);
54:ZpracujJednotu(KEY_LSHIFT,KEY_SHIFT,0);
end;
if (kl_kod<128) and (vsechny_klavesy[KEY_ALT]) then
begin
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;
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;
if kl_kod>127 then kl_kod:=0 else
if vsechny_klavesy[KEY_SHIFT] then inc(kl_kod,1000);
end;
if odpocet>0 then dec(odpocet);
if bios=false then
OutPortB($20,$20);
if bios=false then
Port[$20]:=$20 else
begin
asm
call oldint9_handler
end;
end;
end;
procedure CtiKod_dummy; begin end;
procedure int9_handler; assembler;
asm
cli
push ds
push es
push fs
push gs
pusha
mov ax,cs:[backupDS]
mov ds,ax
mov es,ax
mov ax,dosmemselector
mov fs,ax
call obsluha
popa
pop gs
pop fs
pop es
pop ds
jmp cs:[oldint9_handler]
sti
end;
procedure int9_dummy; begin end;
procedure int9_nbhandler; assembler;interrupt;
asm
cli
push ds
push es
push fs
push gs
pusha
mov ax,cs:[backupDS]
mov ds,ax
mov es,ax
mov ax,dosmemselector
mov fs,ax
call obsluha
popa
pop gs
pop fs
pop es
pop ds
sti
end;
procedure int9_nbdummy; begin end;
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;
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);
newint9_handler:=@ctikod;
GetIntVec(kbdint, oldint9_handler);
SetIntVec(kbdint, newint9_handler);
end;
procedure VypniObsluhuKlavesnice;
begin
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));
SetIntVec(kbdint, oldint9_handler);
end;
end.