Da Tastatursteuerung von Dialogen inzwischen zum guten Ton des Programmierer gehört, wollte ich dieses Feature auch in meine Programme ein bauen. Ich beschloss, die für diesen Zweck unentbehrlichen Routinen Form_Keybd und mit dem Inline-Assembler dem Betriebssystem selbst zu entlocken.
Die Funktionen form_keybd und form_button sind Routinen, die zum Befehlsumfang des AES gehören und normalerweiser intern für die wohl jedem GEM-Programmierer bekannte Funktion form_do zur Steuerung von Dialogen benutzt werden. Doch nun mal schön langsam, was ist das AES überhaupt? Das AES (Application Environment Service) ist der Teil unseres Betriebssystems, der sich um all die schönen Dinge wie Menüs, Dialoge, Fenster und Accessories kümmert. Im Prinzip ist es also eine Sammlung von Routinen, die sich um die grafische Oberfläche kümmern und auch von Programmierern benutzt werden können. Meistens sind diese Befehle schon in Bibliotheken wie der GEMAes-Unit vordefiniert. Man kann aber die Funktionen des AES mittels des Assembler-Befehls Trap #2 (teilt dem System mit, daß es für uns eine Funktion des AES ausführen soll) auf unterster Ebene aufrufen. Dabei müssen wir natürlich noch einige Parameter übergeben, wie z.B. welche Funktion mit welchen Variablen durchgeführt werden soll. Hierzu wird die Adresse des sogenannten Parameterblocks, dem Record GEMParBlk (siehe Listing, fl], [2]) übergeben, das als Einträge Zeiger auf sechs verschiedene Felder besitzt:
globalary: Dieses Feld enthält allgemeine Informationen wie zum Beispiel die Kennung der Applikation und spielt für uns keine wesentliche Rolle.
controlary: enthält Informationen über die Funktion, denn in controlary[0] befindet sich die Nummer des Funktionsaufrufes und in den Feldelementen 1-4 die Anzahl der übergebenen und erhaltenen Parameter (siehe Listing).
intin: dient zur Übergabe von Integerwerten an das AES;
intout: dient zur Rückgabe von Integerwerten vom AES;
adrin: dient zur Übergabe von Adressen, meist Zeiger auf Objekte, an das AES;
adrout: dient zur Rückgabe von Adressen vom AES;
Möchte man also eine AES-Funktion nutzen, ohne die Unit GEMaes zu bemühen, müssen wir nur die Parameter, die Funktionsnummer und die Anzahl der Parameter in die verschiedenen Felder der Struktur eintragen und die Adresse dieses Records dem AES bekanntgeben. (Dazu schreibt man die Adresse in das Datenregister DL) Der Befehl Trap #2 wird auch vom VDI benutzt, deshalb muß man noch in D0 den Wert 200 eintragen (das magische Wort für einen AES-Aufruf). Nun muß man nur noch in die Falle sprich Trap \ treten. Dann kann man sich die Ergebnisse wieder aus den Feldern ziehen, und schon ist man fertig-
Auf diese Weise werden auch die Funktionen der GEMAes-Unit erzeugt. Also legt die Unit auch einen Parameterblock im Speicher an. Ich beschloß nun, beim Aufruf meiner Funktionen diesen Block mitzubenutzen. Dies bringt zwei Vorteile: 1) Ich spare den Speicherplatz für die Felder, und 2) ist das Feld globalary schon gefüllt. (Dies erledigt das AES beim Aufruf von appl_init.) Doch wie komme ich an dieses Feld?
Ganz einfach: das Betriebssystem legt im Speicher eine Tabelle mit sogenannten Systemvektoren an. Diese Systemvektoren sind Speicheradressen, die bei allen TOS-Versionen gleich sind, und deren Inhalt Adressen von Funktionen bilden, die vom Betriebssystem benutzt werden. Einer dieser Vektoren (nämlich die Adresse $88) gibt die Adresse der Routine an, die beim Aufruf von Trap #2 durchgeführt wird. Und was nützt uns das? Ich verbiege jetzt diesen Vektor, das heißt, ich trage in die Speicherstelle $88 die Adresse einer eigenen Routine (in diesem Fall die Adresse des Labels @GetAesPb) ein. Das bedeutet: wird das AES aufgerufen, springt das Betriebssystem an mein Label und führt die Befehle, die dort stehen, aus. Da mich aber nur die Adresse des Parameterblocks interessiert (die beim Aufruf des Traps #2 in D1 steht), kopiere ich sie in den Zeiger aespb, biege den Vektor wieder zurecht (das heißt, ich trage die alte Adresse wieder in die Speicherstelle $88 ein) und springe dann in die alte Routine, damit der AES-Aufruf auch ordnungsgemäß durchgeführt wird. Dieser Aufruf wurde nicht von meiner Unit vollzogen, sondern sollte von der GEM Aes-Unit stammen(z.B. von appl_init). Es sollte also sichergestellt sein, daß direkt nach dem Aufruf der Procedure Install, die den Vektor verbiegt, die Funktion appl_init vom Hauptprogramm aufgerufen wird.
Nun besitzen wir genügend Wissen, um die fehlenden Funktionen form_keybd und form_button selbst zu schreiben. Doch welche Parameter brauchen diese Routinen? Hier hilft ein kleiner Blick in die Literatur ([1],[2]) und das kommentierte Listing. Damit man den Sinn des ganzen Aufwands auch versteht, habe ich noch die Routine My_Form_Do hinzugefügt, die den Aufruf von form_do imitiert, aber noch eine weitere Eigenschaft besitzt: Alle auswählbaren (ob_flag SELECTABLE gesetzt) Buttons können mit der Alternate-Taste gesteuert werden. Hierzu sucht die Routine nach dem ersten Großbuchstaben (muß nicht der erste Buchstabe sein) im Button-Text und vergleicht ihn mit der gedrückten Taste. Also reagiert 'Abbruch’ auf [Alternate][A] und ‘aLles ok’ auf [Alternate][L]. Wir müssen hierzu den Zeiger auf die Tastaturbelegungstabelle ermitteln (siehe Listing), da beim Drücken von [Alternate] die Funktion evnt_keybd keinen ASCII-Code des gedrückten Zeichens mehr liefert. Man kann jetzt also in fast allen alten Programmen, die man in GEM geschrieben hat, die Dialoge mit der Alternate-Taste steuern, eventuell muß man die RCS-Files so anpassen, daß keine Großbuchstaben mehr doppelt belegt sind. Um es noch schöner zu machen, braucht man Userdefined Objects ([3]), dann kann man den auslösenden Buchstaben z.B. unterstreichen oder als kleinen Buchstaben in die linke obere Ecke schreiben. Ich wünsche nun viel Spaß beim Ausprobieren.
Literatur:
[1] Jankowsoki, Reschke, Rabisch, Atari ST Profibuch, Sybex-Verlag
[2] Geiß, D. und J. , Vom Anfänger zum GEM-Profi, Hüthig-Verlag
[3] ST Computer 10/91, S.1S1, ‘Userdefined Objects in MAXON-Pascal'
{(c) 1992 MAXON Computer}
UNIT MyForm;
INTERFACE
USES DOS,BIOS,GEMAes;
TYPE
Tabellen_Typ = PACKED ARRAY [0..128] OF CHAR;
Tabellen_Zeiger = ^ Tabellen_Typ;
Tabellen_Puffer = RECORD
Normal :Tabellen_Zeiger;
Shift :Tabellen_Zeiger;
Capslock :Tabellen_Zeiger;
END;
KeyPtr = ^Tabellen_Puffer;
MSG_feld = ARRAY[0..7] OF INTEGER;
C_String = PACKED ARRAY [0..255] OF CHAR;
String_Ptr = ^C_string;
{ Die Struktur Objekt enthält alle wichtigen }
{ Informationen über Objekte. (z.B. Boxen, }
{ Menutitel,-eintrage) siehe hierzu in [1] }
Object = RECORD
ob_next, ob head, ob_tail,
ob_type, ob_flags, ob_state :
INTEGER;
ob spec : String Ptr;
ob_x, ob_y, ob_w, ob_h : INTEGER;
END;
Tree = ARRAY [0..50] of Object;
ObPtr = ^Tree;
controlary = ARRAY[0..5] OF INTEGER ;
globalary = ARRAY[0..15] OF INTEGER;
intinary = ARRAY[0..16] OF INTEGER;
intoutary = ARRAY[0..7] OF INTEGER ;
adrinary = ARRAY[0..2] OF ObPtr ;
adroutary = ARRAY[0..5] OF Longlnt ;
GEMParBlk = RECORD
control : ^controlary;
global : ^globalary;
intin : ^intinary ;
intout : ^intoutary ;
adrin : ^adrinary ;
adrout : ^adroutary ;
END;
FUNCTION My_Form_Do(dia:ObPtr;child:INTEGER)
:INTEGER;
IMPLEMENTATION
VAR stptr : POINTER;
OldAdr: LongInt;
{ Zeiger auf den Parameterblock }
aespb : ^GEMParBlk;
keytab: KeyPtr;
PROCEDURE AesCall;ASSEMBLER;
ASM
move.l aespb,d1
{ AES-Parameterblock }
move.w #200,d0
{ Magic-Number für AES }
trap #2
{ GEM-Aufruf }
END;
FUNCTION form_keybd(VAR fo_ktree:ObPtr;
fo_kobject,fo_kobnext,fo_kchar :INTEGER;
VAR fo_knextobject,fo_knextchar :INTEGER)
:INTEGER;
BEGIN
{ Ich trage nun die Variablen in die Felder ein}
aespb^.adrin^[0]:=fo_ktree;
{ Zeiger auf Dialogbox )
aespb^.intin^[0]:=fo_kobject;
{ Objektindex des aktuellen Edit-Objektes }
aespb^.intin^[1]:=fo_kchar;
{ gedrückte Taste )
aespb^.intin^[2];=fo_kobnext;
{ unbenutzt, deshalb 0 wählen }
{ Funktionsnummer des AES ist hier 55 }
aespb^.control^[0]:=55;
{ Anzahl der Elemente in intinary }
aespb^.control^[1]:=3;
{ Anzahl der Elemente in intoutary }
aespb^.control^[2]:=3;
{ Anzahl der Elemente in adrinary }
aespb^.control^[3]:=1;
{ Anzahl der Elemente in adroutary }
aespb^.control^[4]:=0;
AesCall;
fo_knextobject:=aespb^.intout^[1];
{ Objektindex des neuen Edit-Objektes }
{ z.B. bei [TAB] }
fo_knextchar:=aespb^.intout^[2];
{ in den Text einzutragendes Zeichen; }
{ hierzu wird objc_edit benutzt }
form_keybd:=aespb^.intout^[0];
{ Flag zum Verlassen des Dialoges; }
{ 0: Dialog verlassen }
END;
FUNCTION form_button(VAR fo_btree:ObPtr;
fo_bobject,fo_bclicks:INTEGER; VAR
fo_bnxtobj:INTEGER);INTEGER;
BEGIN
aespb^.adrin^[0]:=fo_btree;
{ Zeiger auf Dialogbox }
aespb^.intin^[0];=fo_bobject;
{ Objektindex des angeklickten Objektes }
aespb^.intin^[1]:=fo_bclicks;
{ Anzahl der Mausklicks }
{ Funktionsnummer des AES ist hier 56)
aespb^.control^[0]:=56;
aespb^.control^[1]:=2;
aespb^.control^[2]:-2;
aespb^.control^[3]:=1;
aespb^.control^[4]:=0;
AesCall;
fo_bnxtobj:=aespb^.intout^[1];
{ Objektindex des neuen aktuellen Objektes }
form_button:=aespb^.intout^[0];
{ Flag zum Verlassen des Dialoges; }
{ 0: Dialog verlassen }
END;
Procedure GetKeyPtr;
VAR longi :LongInt;
BEGIN
{ bei Übergabe eines Zeigers auf -1 }
longi:=-1;
{ an Keytbl erhält man den Zeiger }
keytab:=KeyTbl(@longi,@longi,@longi);
{ auf die aktuellen Tabellen }
Bioskeys;
END;
FUNCTION My_Form_Do(dia:ObPtr;child:INTEGER)
:INTEGER;
CONST ROOT= 0;
MAX_DEPTH=255;
VAR
i,j,obj,x,y,pos,button,cli,leave,event: INTEGER;
msg:Msg_Feld;
sp_key,key,new_pos:INTEGER;
ch:CHAR;
specstr :String_Ptr;
BEGIN
leave:=1;
IF child>0 THEN
objc_edit(dia,child,chr(0),pos,EDINIT,new_pos);
{ Cursor einschalten }
WHILE (leave>0) DO
BEGIN
event:=evnt_multi(MU_KEYBD|MU_BUTTON,$02,
$01,$01,0,0,0,0,0,0,0,0,0,0,
msg,0,0,
x,y,button,
sp_key,key,
cli) ;
pos:=newpos;
IF (event & MU_BUTTON >0) THEN
BEGIN
obj := objc_find(dia,ROOT,MAX_DEPTH,x,y);
IF obj >=0 THEN
BEGIN
leave:=form_button(dia,obj,cli,obj);
{ bei editierbaren Feldern Cursor wechseln }
IF ((dia^[obj].ob_flags &EDITABLE>0)
AND(obj<>child)) THEN
BEGIN
objc_edit(dia,child,chr(sp_key),
pos,EDEND,new_pos);
child:=obj;
objc_edit(dia,child,chr(sp_key),
pos,EDINIT,new_pos);
END;
END
ELSE
{ der beliebte Glockenton }
Bconout(10,0);
END;
IF (event & MU_KEYBD>0) THEN
{ Jetzt wird es interessant :}
BEGIN IF (sp_key=$0008) THEN
{ Alternate wurde gedrückt }
BEGIN
ch:=CHAR(keytab^.shift^[Hi(key)]);
{ASCII Zeichen ist ermittelt}
i: =0;
REPEAT
i:=i+1;
IF (dia^[i].ob_type=G_BUTTON) AND
(dia^[i].ob_flags&SELECTABLE>0)
THEN
BEGIN
specstr:=dia^[i].ob_spec;
{ Zeiger auf Buttontext }
j:=1;
REPEAT
j:=j+1;
UNTIL (specstr^[j]=chr(0) )
OR (specstr^[j]=ch);
IF specstr^[j]<>chr(0) THEN
{ Der Button ist gefunden }
leave:=form_button(dia,i,1,obj);
END;
UNTIL dia^[i].ob_flags&LASTOB<>0;
END
ELSE
BEGIN
leave:=form_keybd(dia,child,0,key,obj,sp_key);
IF (sp_key>0) THEN
objc_edit(dia,child,chr(sp_key),pos,EDCHAR,new_pos)
ELSE
IF ((dia^[obj].ob_flags & EDITABLE>0)AND
(obj<>child)AND (obj<=dia^[ROOT].ob_tail))
THEN
BEGIN
objc_edit(dia,child,
chr(sp_key),pos,EDEND,new_pos);
child:=obj;
objc_edit(dia,child,
chr(sp_key),pos,EDINIT,new_pos);
END;
END;
END;
END;
IF (child > 0) THEN
objc_edit(dia,child,chr(sp_key),poe,EDEND,new_pos);
My_Form_Do:=obj;
END;
PROCEDURE Install; ASSEMBLER;
ASM
lea @GetAespb,a0
{ GetAespb in GEM Trap einhängen }
move.l $00000088,OldAdr
{ alte Adressse in OldAdr speichern}
move.l a0,$00000088
lea @ToEnd,A0
jmp (A0)
@GetAespb:
cmp.w #200,d0
{ war es wirklich ein AES-Aufruf ?}
bne @jmpold
move.l d1,aespb
{ Adresse des Parameterblocks kopieren}
move.l OldAdr,$88;
@jmpold:
move.l OldAdr,a0
{ alte Adresse anspringen)
jmp (a0)
@ToEnd:
END;
BEGIN
GetKeyPtr;
{ Vektoren kann man nur im Supervisor }
{ Mode verbiegen }
stptr:=SUPER(NIL);
Install;
stptr:=SUPER(stptr);
END.