Workshops / Hintergrundberichte / Buchvorstellungen
---------------------------------------------------

1. Einführung in die Reaction-Programmierung (von Martin R. Elsner)

Teil 4: Listbrowser, Clicktab, Speedbar und Hooks

Es dauert nicht lange, bis man in die Verlegenheit kommt, eines der
genannten Gadgets benutzen zu müssen - heute wollen wir verhindern,
dass man dabei den Spaß am Programmieren verliert!

Zunächst eine kleine Zusammenfassung der Exec-Listen, die man häufig
benötigt. Listen sind eine praktische Form, dynamisch Daten zu speichern. Im
Gegensatz zu Arrays benötigt man aber ein paar Kenntnisse, Strukturen
und Funktionen, um mit Ihnen umgehen zu können.

Am Amiga gibt es ein Standardformat für eine doppelt verkettete Liste
(d.h. man kann die Liste sowohl von vorne nach hinten als auch von hinten
nach vorne durchlaufen, es existieren zwei Zeiger pro Knoten), das auch in
eigenen Programmen einfach eingesetzt werden kann und für das schon die
wichtigsten Funktionen existieren. Die Liste ist dabei eine Struktur

 struct List {
   struct  Node *lh_Head;       /* Zeiger auf ersten Knoten     */
   struct  Node *lh_Tail;       /* Dummy, muss immer NULL sein  */
   struct  Node *lh_TailPred;   /* Zeiger auf letzten Knoten    */
   UBYTE   lh_Type;             /* Typ                          */
   UBYTE   l_pad;               /* Dummy                        */
 };

die dann als Aufhänger für beliebig viele Knoten dient:

 struct Node {
   struct  Node *ln_Succ;       /* Zeiger auf nächsten Knoten   */
   struct  Node *ln_Pred;       /* Zeiger auf vorhergeh. Knoten */
   UBYTE   ln_Type;             /* Typ, siehe exec/nodes.h      */
   BYTE    ln_Pri;              /* Priorität zur Sortierung     */
   char    *ln_Name;            /* Name zur Identifikation      */
 };

(zu finden in exec/lists.h bzw. exec/nodes.h)

Man benötigt pro Liste eine Variable vom Typ struct List. Diese muss
mit NewList() initialisiert werden:

 struct List MyList;
 NewList(&MyList);

Danach kann man dann Knoten für Knoten erzeugen. Da ein Node keinen
wirklichen Speicherplatz für unsere Daten bereitstellt bzw. die vorhandenen
Variablen für uns tabu sind, müssen wir eine eigene Struktur definieren; um
z.B. pro Knoten einen String zu speichern, kann man folgende Struktur
definieren:

 struct MyData {
   struct Node nd;
   char MyString[1000];
 }

(Man kann alternativ die Variablen aus struct Node direkt in MyData
übernehmen, um MyData.ln_Succ statt MyData.nd.ln_Succ zu schreiben.)

Die so definierte Struktur ist dann äquivalent zur struct Node und kann
in allen Listenfunktionen verwendet werden.
Um nun einen Knoten einzufügen, reserviert man zunächst Speicher mit

 struct MyData *md;
 md = (struct MyData*)AllocVec(sizeof(struct MyData),MEMF_CLEAR);

und hängt diesen Knoten an die Liste an:

 AddTail(&MyList,(struct Node*)md);

Jetzt können wir speichern, was zu speichern ist:

 strcpy(md->MyString,"Irgendein Text");

Statt AddTail() kann man auch AddHead(), Insert() oder Enqueue() verwenden,
je nach Platzierung des neuen Knotens.

Die Liste muss natürlich später wieder freigegeben werden, was mit RemHead()
folgendermaßen passieren kann:

 struct Node *md;
 while( !IsListEmpty(&MyList) ) FreeVec(RemHead(&MyList));

RemHead() entfernt nur die Adresse des ersten Knotens aus der Liste, FreeVec gibt
dann den Speicher wieder frei. Mit RemTail() und Remove() kann man schließlich
den letzten oder einen bestimmten Knoten entfernen.
IsListEmpty ist nur ein Makro, das es erleichtert, den Zustand der Liste zu prüfen;
man sollte nämlich wissen, dass MyList.lh_Head nie NULL wird, sondern bei einer
leeren Liste auf den unteren Teil unserer Struktur zeigt, der immer als letzter
Knoten angesehen wird (was manche Listenoperationen vereinfacht, aber den
Einsteiger meist verwirrt). Wer mehr darüber wissen will, dem empfehle ich die
ROM Kernel Manuals (z.B. auf der Developer-CD unter
ADCD_2.1:Reference/ROM_Kernel_Manuals/Libraries_Manual
in ExecLibraries->ExecListsAndQueues).


Jetzt zur Praxis: solche "Exec linked lists" werden auch gerne in BOOPSI-Objekten
benutzt, in denen eine variable Datenstruktur benötigt wird. Im ClickTab sind das
die Seiten (bzw. Überschriften), im SpeedBar die Buttons und im ListBrowser die Zeilen.
Intern arbeiten die Gadgets mit Listen, die unserer MyList sehr ähnlich sind;
sie können unter CLICKTAB_Labels, SPEEDBAR_Buttons und LISTBROWSER_Labels
entweder direkt in ReActor oder im eigenen Quelltext definiert werden.
Es werden erweiterte Nodes benutzt, in denen die spezifischen Daten untergebracht werden.
Wir brauchen uns um die genaue Struktur nicht zu kümmern, sondern können und müssen
statt mit AllocVec nun mit den Routinen

 AllocClickTabNode()
 AllocSpeedButtonNode()
 AllocListBrowserNode()

neue Knoten erzeugen. Übergeben werden Tags, die die Eigenschaften des Knotens festlegen,
und beim ListBrowser die Anzahl der Spalten. Mit

 FreeClickTabNode()
 FreeSpeedButtonNode()
 FreeListBrowserNode()

kann man die Knoten wieder freigeben, mit FreeListBrowserList() sogar die gesamt Liste.

Eine Änderung der Knoten ist mit

 Get/SetListBrowserNodeAttrs()
 Get/SetClickTabNodeAttrs()
 Get/SetSpeedButtonNodeAttrs()

möglich, wobei die Tags denen von Alloc...Node() entsprechen.

Beim Zugriff auf die Listen ist Vorsicht geboten, da die Objekte natürlich permanent auf
die Liste zugreifen können. Um Änderungen durchzuführen, muss man die Liste zuerst abhängen:

 SetGadgetAttrs((struct Gadget *)lb,NULL,NULL,LISTBROWSER_Labels,~0,TAG_DONE);

dann können die Änderungen an der Liste durchgeführt werden (Einfügen/Ändern/Löschen von
Knoten), woraufhin man die Liste wieder anhängt:

 SetGadgetAttrs((struct Gadget *)lb,intuiwin,NULL,LISTBROWSER_Labels,&MyList,TAG_DONE);

Das war's auch schon fast ;)



Das LISTBROWSER-Gadget

Schauen wir und direkt einen mehrspaltigen Listbrowser an:
Wir wollen zwei Spalten anzeigen, in der ersten Spalte soll ein Image, in der zweiten
ein Text erscheinen.

 struct ColumnInfo LBColumns[3];
 struct List LBList;
 Object *ListBrowser;

 /* Die Spalten werden nicht als Liste, sondern als Array definiert: */
 LBColumns[0].ci_Title = "Erste Zeile";
 LBColumns[0].ci_Width = 40;
 LBColumns[0].ci_Flags = 0;
 LBColumns[1].ci_Title = "Zweite Zeile";
 LBColumns[1].ci_Width = 60;
 LBColumns[1].ci_Flags = 0;
 LBColumns[2].ci_Title = "";
 LBColumns[2].ci_Width = -1; /* bedeutet letzter Eintrag, keine Spalte (wie TAG_END) */
 LBColumns[2].ci_Flags = 0;

 NewList( &LBList );

#define LB_ID 1

 ListBrowser = (Object*)NewObject(
   LISTBROWSER_GetClass(),NULL,
   GA_ID, LB_ID,
   GA_RelVerify, TRUE,
   LISTBROWSER_ColumnInfo, &LBColumns[0],
   LISTBROWSER_Labels, &LBList,
   TAG_END);

 Image = (Object*)NewObject(
   BITMAP_GetClass(),NULL,
   BITMAP_Screen,scr,BITMAP_SourceFile,filename,TAG_END);
 /* scr ist der am Anfang geholte Bildschirmzeiger, filename irgenein Bild-Dateiname */
 /* Natürlich kann man auch eine ganze Reihe von Bildern laden und verwenden. */


Dieser Listbrowser kann nun an ein Layout angehängt und angezeigt werden.
Wir werden aber noch keine Zeilen sehen. Angenommen, es gibt eine Funktion GetNewData(),
die uns String für String und schließlich NULL zurückliefert; dann müssen wir die Strings
speichern, da der ListBrowser die Texte nicht kopiert. Wir könnten eine eigene Liste oder
ein Array anlegen, aber der ListBrowser besitzt ja schon eine Struktur!
Wir haben das Glück, dass wir - anders als im ClickTab - eine eigene Node-Struktur
definieren können, die dann vom ListBrowser mittel LBNA_NodeSize angelegt und verwaltet
wird. Also benutzen wir unser MyData:

 SetGadgetAttrs((Gadget *)lb,NULL,NULL,LISTBROWSER_Labels,NULL,TAG_DONE);
 newdata=GetNewData();
 while( newdata ){
   struct MyData newnode;

   newnode=(struct MyData *)AllocListBrowserNode(2,LBNA_NodeSize,sizeof(struct MyData),TAG_END);
   /* Also zwei Spalten, jeder Knoten kann als struct MyData benutzt werden */

   strcpy(newnode->MyString,newdata);

   SetListBrowserNodeAttrs( (struct Node *)newnode,
     LBNA_Column,0, LBNCA_Image,Image,
     LBNA_Column,1,
       LBNCA_Text,newnode->MyString, LBNA_Flags,LBFLG_CUSTOMPENS,LBNCA_FGPen,2, LBNCA_Justification,LCJ_LEFT,
       /* ein paar Beispiele, was man noch so übergeben kann */
     TAG_END );

   AddTail(&MyList,(struct Node*)newnode);

   newdata=GetNewData();
 }
 SetGadgetAttrs((struct Gadget *)lb,intuiwin,NULL,LISTBROWSER_Labels,&MyList,TAG_DONE);

Da newnode erst nach AllocListBrowserNode() verfügbar ist und wir noch nicht die Adresse von MyString kennen,
können wir dort noch nicht den Text angeben; deswegen muss SetListBrowserNodeAttrs() aufgerufen werden.
Hier muss die Reihenfolge der Tags beachtet werden: zunächst wird die Spalte angegeben, dann die Eigenschaften,
die der Knoten in dieser Spalte besitzen soll.

Interessant ist die Möglichkeit, mit dem Listbrowser Baumstrukturen abzubilden. Dies wird durch Setzen von
LISTBROWSER_Hierarchical möglich und erfordert die Angabe des Tags LBNA_Generation für jeden Node, 1 steht
für die oberste Ebene, 2 für die nächste (also die Children der ersten Ebene) etc.
Mit ShowListBrowserNodeChildren(), HideListBrowserNodeChildren(),
ShowAllListBrowserChildren() und HideAllListBrowserChildren() lassen sich dann im Programm Knoten ein- und
ausblenden. Mit LISTBROWSER_ShowImage, LISTBROWSER_HideImage und LISTBROWSER_LeafImage lassen sich sogar
eigene Bilder für einen Knoten mit eingeblendeten Children, mit ausgeblendeten Children und ohne Children
angeben.


Screenshot:
Screenshot


Bei der Reaktion auf Mausklicks kann man an LISTBROWSER_RelEvent die Art des Ereignisses ablesen:

 switch( GadgetId ){
   case LB_ID:{
     GetAttr( LISTBROWSER_RelEvent,lb,&code );
     switch( code ){
       case LBRE_DOUBLECLICK:   ... break;
       case LBRE_NORMAL:        ... break;
       case LBRE_COLUMNADJUST:  ... break;
       ... /* siehe AutoDocs */
     }
     break;
   }
   ...
 }

Je nach Ereignis bekommt man dann weitere Informationen durch folgende Eigenschaften:
LISTBROWSER_RelColumn:          Angeklickte Spalte
LISTBROWSER_Selected            Gewählte Zeilen-/Nodenummer nach NORMAL oder DOUBLECLICK
LISTBROWSER_SelectedNode:       Gewählter ListbrowserNode nach NORMAL oder DOUBLECLICK
LISTBROWSER_NumSelected:        Anzahl ausgewählter Nodes, falls MultiSelect=TRUE

Für Ereignisse wie LBRE_CHECKED und alle Mehrfachselektionen bei MultiSelect=TRUE muss man
die ListbrowserLabels auswerten; mit GetListBrowserNodeAttrs() bekommt man heraus, ob
der Knoten ausgewählt (LBNA_Selected) oder der Haken gesetzt ist (LBNA_Checked, nur für Knoten,
bei denen LBNA_CheckBox=TRUE gesetzt wurde). Genauso wie LISTBROWSER_Selected und
LISTBROWSER_SelectedNode auch vom Programm gesetzt werden können, können einzelne Nodes durch
SetListBrowserAttrs() (de-)selektiert werden (vorher wieder die Liste abhängen!).
Übrigens rate ich von der Verwendung von LISTBROWSER_PersistSelect ab, wenn man LBRE_DOUBLECLICK
nutzen möchte, da ein Doppelklick in diesem Modus zur Zeit nicht immer gemeldet wird.

Leider bleibt hier kein Platz - bzw. für mich keine Zeit ;) - auf alle Features des Listbrowsers
genau einzugehen, aber ich rate jedem, jede Eigenschaft einmal auszuprobieren; der Listbrowser
ist eines der mächtigsten BOOPSI-Gadgets und kann für fast jedes Problem, das eine Liste von Daten
oder Objekten betrifft, angepasst und eingesetzt werden.


Das CLICKTAB-Gadget

Ein ClickTab ist sehr nützlich, wenn man viele Informationen anzeigen muss, diese aber nicht
gleichzeitig angezeigt werden können oder sollen. Dazu werden die Daten auf mehrere Seiten
verteilt und über eine Reihe von Titel-Buttons zugänglich gemacht. Die einzelnen Gadgets und Images
werden dabei von einem PAGE-Objekt verwaltet, die Buttons in einer Liste ähnlich den ListbrowserLabels.

In ReActor sieht das also so aus:
1. Am gewünschten Platz ein CLICKTAB einfügen.
2. Neue GadgetGroup erzeugen und dort ein PAGE einfügen.
3. Unter diesem Page nun für jede Seite ein LAYOUT erzeugen, und dort alle gewünschten Objekte einfügen.
4. Zurück zum ClickTab und dort unter CLICKTAB_PageGroup die neue Gruppe angeben.
5. Im ClickTab unter CLICKTAB_Labels für jede Seite einen Node anlegen und ihm sowohl einen bezeichnenden
Namen als auch eine eindeutige Nummer geben. Diese Nummer bezieht sich auf die Reihenfolge der Objekte
im Page-Objekt, d.h. 0 steht für erstes Layout im Page etc.

Die Erzeugung im Quelltext sieht ähnlich aus, wobei die CLICKTAB_Labels so anzulegen sind wie im Listbrowser
(Unterschiede: keine Spaltenanzahl in AllocClickTabNode, TNA_ statt LBNA_-Eigenschaften).
Wie bei ListbrowserNodes werden die Texte nicht kopiert. Da ClickTabs aber auch nicht für variable
Strukturen gedacht sind, sondern meist eine festgelegte Anzahl von Seiten aufweisen, wird man die
Titel der Seiten sowieso im Programm hinterlegen.


Das SPEEDBAR-Gadget

ist eine sehr gute Möglichkeit, eine Liste von Standardaktionen in einem Fenster unterzubringen, die
durch Images symbolisiert werden. Im Grunde handelt es sich nur um ein Layout mit einer Liste von
Gadgets, die Kapselung in einem Objekt erleichtert aber die Programmierung, und es werden Buttons
ausgeblendet, wenn dem Speedbar nicht genügend Platz zur Verfügung steht. Die Liste der Buttons ist
- wie zu erwarten - wieder eine unserer verketteten Listen, hier mit SpeedButtonNodes, die leider keinen
Text, sondern nur Images enthalten; glücklicherweise kann auch ein Hilfetext angegeben werden.
Übrigens sind die Eigenschaften SPEEDBAR_Visible und SPEEDBAR_Total, die die Zahl der sichtbaren und
aller Buttons im Speedbar angeben, nicht überschreibbar, auch wenn sie in ReActor aufgeführt werden.

Leider fehlen dem SpeedBar ein paar wünschenswerte Eigenschaften wie die Darstellung von Pfeilen, und die
fehlende Popup-Hilfe macht sich hier negativ bemerkbar. Man kann sich allerdings behelfen:
die Pfeile kann mal selber als Gadgets anlegen, und mit der Eigenschaft SPEEDBAR_Top lässt sich dann die
Anzeige unserer Buttons scrollen. Und für die Anzeige der Hilfe gibt es wenigstens die Möglichkeit,
mit SPEEDBAR_Window ein Fenster anzugeben, dessen Titelzeile zur Anzeige der Button-spezifischen
Hilfetexte missbraucht wird.


HOOKS

And now to something completely different: Hooks!

Worum geht es? Wenn ein eigenes Programm auf bestimmte Dinge reagieren soll, gibt es natürlich
die Möglichkeit, mittels eines MessagePorts auf Ereignisse zu warten und dann die entsprechende
Funktion auszuführen. Dies ist nicht immer möglich und bedeutet, dass die Funktion nicht direkt an
der benötigten Stelle aufgerufen wird. Einen anderen Weg gehen die sogenannten Hooks, wobei es
sich im Grunde nur um etwas erweiterte Funktionszeiger handelt, die an geeigneter Stelle übergeben
und dann automatisch aufgerufen werden. Da der Aufruf nicht unbedingt aus dem eigenen Prozess
heraus erfolgt, sollte die Hookfunktion möglichst kompakt sein und keine Anforderungen an die
Umgebung stellen (u.a. sollte kein printf() etc. benutzt werden).

Das komplizierteste ist die Parameterübergabe. StormC z.B. ermöglicht eine einfache
Implementation, die ich hier am Beispiel eines IDCMPHooks für ein Window-Objekt darstellen möchte;
Ziel soll sein, die Nachricht von Intuition zu speichern, wenn ein Ereignis auftaucht, das
WM_HANDLEINPUT sonst verschlucken würde.

 #include <utility/hooks.h>

 struct Hook IDCMPHook;
 struct IntuiMessage IDCMPMessage;

 /* Hilfsfunktion zum Initialisieren der Hook-Struktur:
 void InitHook( Hook *h,ULONG (*func)(), void *data ){
   if(h){
     h->h_Entry=func;
     h->h_SubEntry=NULL;
     h->h_Data=data;
   }
 }

 /* Die aufzurufende Funktion: */
 /* andere Compiler erfordern zusätzlich das Schlüsselwort __asm */
 ULONG __saveds IDCMPHandler( register __a0 Hook *h,
                              register __a2 void *o,
                              register __a1 void *msg ){
   memcpy( h->h_Data, msg,sizeof(struct IntuiMessage) );
   /* wir kopieren einfach die Message, um sie nachher zu verarbeiten */
   return(1);
 }

 ...
 /* Die Initialisierung im Programm: */

 InitHook( &IDCMPHook,(ULONG (*)())IDCMPHandler,&IDCMPMessage );

 /* ab hier steht unser Hook zur Verfügung und kann übergeben werden, z.B.: */

 Win = NewObject( WINDOW_GetClass(),NULL,
   ...
   WA_IDCMP,IDCMP_CLOSEWINDOW|IDCMP_GADGETUP|IDCMP_MOUSEBUTTONS|
            IDCMP_RAWKEY|IDCMP_DISKINSERTED|IDCMP_DISKREMOVED,
   WINDOW_IDCMPHook,&Main.IDCMPHook,
   WINDOW_IDCMPHookBits,IDCMP_DISKREMOVED|IDCMP_DISKINSERTED|IDCMP_MOUSEBUTTONS|IDCMP_RAWKEY,
   TAG_END );


Bei anderen Compilern kann dies theoretisch etwas anders aussehen, wenn Parameter nicht direkt
aus den Registern ausgelesen werden können; die Lösung dieses Problems und weitere Informationen
findet man auf der Developer-CD unter Reference/Amiga_Mail_Vol2/Archives/Plain/ma92/Hooks/Hooks.txt
Für alle, die noch Probleme mit den Hooks haben: in den meisten Fällen gibt es Alternativen
zu Hooks, sodass man auch ohne sie leben kann ...

Ansonsten wünsche ich viel Spaß beim Erzeugen und Freigeben aller Listen,

euer

Martin R. Elsner <email@martin-elsner.de>