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

Windows, Ereignisbehandlung, Layouts und ReActor

Bevor wir zum eigentlichen Thema kommen, hier noch ein paar Ergänzungen zum
ersten Teil:

Ein kleiner "Fehler" hatte sich in das Beispielprogramm eingeschlichen:

 ...
 GetAttr( WINDOW_Window, window, &intuiwin );
 ...

Das ist fast richtig, wird allerdings unter C++ zurückgewiesen, weil GetAttr
als dritten Parameter einen ULONG* erwartet, und keinen Window*.
Also einfach ändern in

 GetAttr( WINDOW_Window, window, (ULONG*)&intuiwin );

Michael Christoph hat mich darauf hingewiesen, dass er im Netz auch einen
(schönen und umfangreichen) Reaction-Kurs unter [1] veröffentlicht hat. Außerdem
kam von ihm die begründete Beschwerde, dass man beim Öffnen der Libraries testen
sollte, ob wirklich die benötigten Versionen vorhanden sind bzw. ob OS3.5
installiert ist; dazu reicht es zu prüfen, ob die resource.library geöffnet
werden kann. Natürlich sollten immer alle Funktionsergebnisse kontrolliert
und gegebenenfalls eine Fehlermeldung ausgegeben oder sogar das Programm
beendet werden. Ich werde aber im Rahmen meiner Beispiele auf die Angabe dieser
Überprüfungen verzichten, da ich sowieso nur einzelne Quelltextteile angebe.

Wer trotzdem weiterlesen will ;) wird diesmal mehr über den Aufbau eines
Fensters und die Abfrage der Ereignisse erfahren und ich gehe in einem Beispiel
auf die Benutzung von ReActor ein.


Im letzten Teil haben wir schon ein einfaches Window erzeugt, geöffnet und
geschlossen. Als Tags können beim Erzeugen alle von Intuition bekannten
Eigenschaften angegeben werden, also Höhe, Breite, Position, Fenstertitel,
die anzuzeigenden Windowgadgets und - besonders wichtig! - die IDCMP-Flags,
also die Ereignisse, auf die das Fenster reagieren soll. Zusätzlich gibt es
WINDOW_...-Tags, die spezielle Fähigkeiten der Window-Klasse bzw. von Reaction
ausnützen, u.a. vereinfachte Ikonifizierung und Hintergrundgrafik.

Ich möchte hier nicht die Bedeutung aller einzelnen Tags aufführen, sondern
einmal einen Auszug aus der Fensterdefinition von ClassAction angeben
(sämtliche Fensterwerte sind in der globalen Struktur Main abgelegt, was die
Übersicht deutlich erhöht):

 ...
 WA_PubScreen, Main.Scr,
 /* Bildschirm, auf dem geöffnet werden soll (Screen*) */
 WA_Top,   Main.WinY,     WA_Left,   Main.WinX,
 WA_Width, Main.WinWidth, WA_Height, Main.WinHeight,
 /* Position und Größe des Bildschirms */
 WA_Zoom, Main.ZoomCoords,
 /* Hier muss die Adresse eines Arrays aus 4 Worten stehen, */
 /* d.h. short ZoomCoords[4];                               */
 /* in dem die Werte Left/Top/Width/Height der alternativen */
 /* Größe gespeichert sind (->ZoomGadget)                   */
 WA_RMBTrap, TRUE,
 /* Rechte Maustaste öffnet kein Menü, sondern wird als normales */
 /* Ereignis behandelt                                           */
 WINDOW_IconifyGadget, TRUE,
 WINDOW_Icon,          Main.Dobj,
 WINDOW_AppPort,       Main.AppPort,
 WINDOW_AppWindow,     (ULONG)TRUE,
 /* Diese Tags machen das Fenster zum AppWindow, d.h. wir werden      */
 /* benachrichtigt, wenn der Benutzer auf das Iconify-Gadget          */
 /* drückt oder das AppIcon öffnet oder wenn Icons auf unser Fenster  */
 /* gezogen werden. Mehr dazu unten.                                  */
 WINDOW_SharedPort, Main.MsgPort,
 /* Wenn man unbedingt einen eigenen MessagePort angeben möchte,  */
 /* kann man dies hier tun.                                       */
 /* Sonst wird ein neuer Port automatisch erzeugt.                */
 WA_IDCMP, IDCMP_CLOSEWINDOW|IDCMP_GADGETUP|IDCMP_MOUSEBUTTONS|
                   IDCMP_RAWKEY|IDCMP_DISKINSERTED|IDCMP_DISKREMOVED,
 /* Das sind alle Ereignisse, die mich interessieren.             */
 /* Die DISK-Ereignisse werden allerdings nicht von               */
 /* WM_HANDLEINPUT gemeldet, dafür gibt es einen anderen Weg:     */
 WINDOW_IDCMPHook,     &Main.IDCMPHook,
 WINDOW_IDCMPHookBits, IDCMP_DISKREMOVED|IDCMP_DISKINSERTED|
                                           IDCMP_MOUSEBUTTONS|IDCMP_RAWKEY,
 /* Eine Alternative zu einer eigenen Ereignisauswertung sind     */
 /* Hooks: man kann eine eigene Funktion definieren, die in allen */
 /* angegebenen Fällen automatisch aufgerufen wird.               */
 WINDOW_BackFillName, Main.BackgroundPattern,
 /* Eine feine Sache sind die Hintergründe: einfach einen Dateinamen */
 /* angeben, und das Fenster wird bunt!                              */
 ...

Dies sind nicht alle Tags, weil manche schon in ReActor definiert werden - wie
das geht, steht weiter unten. Jetzt schauen wir uns mal an, wie die Ereignisse
in einem Programm verarbeitet werden können. Dazu verändern wir unser
Beispielprogramm etwas:

 ...
 BOOL end;
 ULONG windowsignal,receivedsignal,result,code;
 MsgPort *applport;
 #define BUTTON_ID 1
 ...

 button = (Object*)NewObject( BUTTON_GetClass(),NULL,
                                                          GA_Text,"Mein zweiter Reaction-Button ...",
                                                          GA_ID,BUTTON_ID,    /* <- */
                                                          GA_RelVerify,TRUE,  /* <- */
                                                          TAG_END );

 /* Unser Button erhält eine eindeutige Nummer und soll sich melden, sobald  */
 /* er (nach dem Drücken) losgelassen worden ist.                            */

 SetGadgetAttrs( (Gadget*)layout,NULL,NULL,LAYOUT_AddChild,button,TAG_END );

 applport = CreateMsgPort();
 /* Dieser MessagePort wird nur für das Icon benötigt; ist unser Window      */
 /* ikonifiziert, kann ja der Windowport keine Nachrichten mehr liefern.     */
 /* Noch mal der Hinweis: Auch dieses Ergebnis sollte man prüfen!            */
 /* Andererseits können viele Funktionen auch mit NULL umgehen, also muss    */
 /* das Programm nicht unbedingt beendet werden. Setzt man z.B. hier einfach */
 /* applport = NULL, dann funktioniert nur das Iconify nicht.                */

 window = (Object*)NewObject( WINDOW_GetClass(),NULL,
                                                          WINDOW_Layout,layout,
                                                          WINDOW_Position,WPOS_CENTERMOUSE,
                                                          WA_Activate,TRUE,
                                                          WA_Title,"Fenster Teil 2 !",
                                                          WA_DragBar,TRUE,
                                                          /* jetzt kommen die neuen Attribute: */
                                                          WA_CloseGadget,TRUE,
                                                          WINDOW_AppWindow,TRUE,
                                                          WINDOW_AppPort,applport,
                                                          WINDOW_IconifyGadget,TRUE,
                                                          WA_IDCMP,IDCMP_CLOSEWINDOW|IDCMP_GADGETUP,
                                                          TAG_END );

 /* Diesmal mit Schließgadget, Benachrichtigung bei Schließen oder Gadget */
 /* Zusätzlich zu AppWindow, AppPort und IconifyGadget kann man noch das  */
 /* Icon selbst angeben, das mit GetIconTags geholt werden kann.          */
 /* Ansonsten wird ein Standardicon verwendet.                            */

 DoMethod( window,WM_OPEN );

 GetAttr( WINDOW_Window, window, (ULONG*)&intuiwin );

Jetzt müssen wir auf die Ereignisse des Windows warten und entsprechend
reagieren. Wenn wir nur Standardereignisse benötigen, reicht es dazu aus, die
Funktion WM_HANDLEINPUT des Window-Objekts zu benutzen, bei manchen
Gelegenheiten wird man aber um die Hooks nicht herumkommen (dazu später mehr).
Wir erhalten von WM_HANDLEINPUT eine Kombination aus dem auslösenden Objekt
und der Art des Ereignisses, die wir mit den WMHI-Konstanten zerlegen können:

 GetAttr( WINDOW_SigMask,window,&windowsignal );
 end = FALSE;
 while( !end ){

   receivedsignal = Wait( windowsignal );

   while( (result = DoMethod(window,WM_HANDLEINPUT, &code)) != WMHI_LASTMSG){
         /* code enthält weitere Daten, z.B. die gedrückte Taste bei RAWKEY     */

         switch( result & WMHI_CLASSMASK ){
           case WMHI_CLOSEWINDOW:{
                 end = TRUE;
                 break;
           }
           /* beim Iconify sollte man UNBEDINGT verhindern, dass in dem         */
           /* Zeitraum, in dem das Fenster geschlossen ist, irgend etwas mit    */
           /* dem nun ungültigen Zeiger intuiwin angestellt wird !              */
           /* Dieser muss beim erneuten Öffnen neu geholt werden.               */
           case WMHI_ICONIFY:{
                 DoMethod( window,WM_ICONIFY );
                 intuiwin = NULL;
                 break;
           }
           case WMHI_UNICONIFY:{
                 DoMethod( window,WM_OPEN );
                 GetAttr( WINDOW_Window, window, (ULONG*)&intuiwin );
                 break;
           }
           case WMHI_GADGETUP:{
                 /* in result steht auch die mit GA_ID angegebene ID, um sie zu      */
                 /* erhalten muss man aber nicht WMHI_GADGETMASK, sondern            */
                 /* RL_GADGETMASK verwenden, da man sonst GadgetID und GruppenID     */
                 /* (bitweises or) erhält:                                           */
                 switch( result & RL_GADGETMASK ){
                   case BUTTON_ID:{
                         SetGadgetAttrs( (Gadget*)button,intuiwin,NULL,
                           GA_Text,"Ok!",TAG_END
                         );
                         break;
                   }
                 }
                 /* Die GruppenID wird nur benötigt, falls man mit mehr als einer    */
                 /* Gadget-Gruppe arbeitet (siehe ReActor!), da die GadgetID's nur   */
                 /* innerhalb einer Gruppe eindeutig sind.                           */
           }
         }
   }
 }

 DoMethod( window,WM_CLOSE );

 DeleteMsgPort( applport );
 /* kann auch mit NULL umgehen, man muss also keine Unterscheidung machen.   */
 ...

Übrigens muss man zum genaueren Auswerten der AppWindow-Botschaften (z.B.
welche Icons auf das Fenster gezogen wurden) direkt auf den applport
zugreifen, d.h.

 while( appmsg=(AppMessage*)GetMsg( applport ) ){
   switch( appmsg->am_Type ){
         case AMTYPE_APPICON:   ...
         case AMTYPE_APPWINDOW: ...
   }
   ReplyMsg( (Message*)appmsg );
 }

Dies sollte nach Wait und vor WM_HANDLEINPUT geschehen.


Nun zum generellen Fensteraufbau: Das ominöse Layout-Gadget stand bisher immer
still in der Ecke - dabei übernimmt es eine der wichtigsten Aufgaben: die
Aufteilung des Platzes im Fenster!

In der aktuellen ClassAction-Version sind genau 11 Layout-Gadgets damit
beschäftigt, Buttons, Listbrowser, Stringgadgets und den Fuelgauge im Fenster
anzuordnen. Da dies bei größeren Projekten schon etwas unübersichtlich wird,
sollte man sich schon am Anfang überlegen, welche Funktionalitäten man benötigt
und welche Gadgets (oder auch anderen Elemente wie Menüs) dies am effizientesten
leisten. Dann kann man diese Elemente in horizontale oder vertikale Gruppen
zusammenfassen und diese Gruppen-Layouts einem übergeordnetes, nämlich dem
Root-Layout, unterordnen. Eine gut überlegte Aufteilung nutzt sowohl dem
Programmierer (z.B. bei einer späteren Erweiterung) als auch dem Benutzer
(Übersichtlichkeit, Ähnlichkeit zu anderen Programmen).

So erhält man eine hierarchische Struktur, die in ReActor als Baum dargestellt
wird. Die Layouts sind dabei im Grunde nur Hilfsobjekte für die Positionierung,
können aber auch mit einem sichtbaren Rahmen versehen werden und eine
Überschrift erhalten, was die Oberfläche meist übersichtlicher macht.

Mit Tags können der Rahmen, die Überschrift und die Farben, viel wichtiger aber
die Positionierung der untergeordneten Elemente, angegeben werden:
LAYOUT_Orientation legt fest, ob es sich um eine horizontale oder vertikale
Gruppe handelt. Weiter kann festgelegt werden, ob die Elemente die gleiche Größe
haben sollen, ob sie z.B. in einem horizontalen Layout oben, unten oder in der
Mitte auftauchen sollen, ob Platz zwischen den Elementen bzw. zwischen Elementen
und Rahmen reserviert werden soll usw.

Eingefügt wird mit LAYOUT_AddChild: Mit diesem Tag kann dem Layout ein anderes
Objekt (z.B. Button oder weiteres Layout) untergeordnet werden. Man kann hier
die Adresse eines zuvor angelegten Objektes angeben oder direkt das Objekt
erzeugen:

 ...
 LAYOUT_AddChild,NewObject(BUTTON_GetClass(),NULL,...,TAG_END),
 ...

Somit ist es sogar möglich, das gesamte Fenster in einem Befehl zu erzeugen:

 window = (Object*)NewObject(
   WINDOW_GetClass(), NULL,
   WINDOW_Layout, layout=NewObject(
         LAYOUT_GetClass(), NULL,...,
         LAYOUT_AddChild, button=NewObject(
           BUTTON_GetClass(), NULL,...
           TAG_END
         ),
         TAG_END
   ),
   ...
   TAG_END
 );

...was mit Reaction-Makros (siehe reaction/reaction_macros.h) noch schöner aussieht
- wobei allerdings nicht verschwiegen werden soll, dass erstens manch andere
Sprache nicht die direkte Zuweisung (...=NewObject) zulässt, und zweitens hier
auch keine Überprüfung auf Fehler stattfinden kann (welche bei geöffneten
Libraries im Grunde nur bei Speichermangel auftreten).

(Mit LAYOUT_AddImage werden Images hinzugefügt, die zur Darstellung von Texten
und Grafiken gedacht sind und daher zum Teil anders behandelt werden als
Gadgets. Auf Images geh ich später noch ein.)

Falls nicht LAYOUT_Inverted,TRUE, angegeben wird, werden die Objekte in der
Reihenfolge des Einfügens von links nach rechts bzw. oben nach unten eingefügt.
Die Größe des Elements soll ja im Allgemeinen automatisch berechnet werden,
aber wir können mit CHILD_WeightedWidth bzw. CHILD_WeightedHeight den
relativen Platzverbrauch angeben; der Wert 0 heißt dabei minimale Größe. Alle
CHILD_...-Tags wirken sich dabei auf das zuvor mit AddChild / ModifyChild
angegebene Element aus. Mit der Tagfolge

 SetGadgetAttrs( layout,intuiwin,NULL,
   LAYOUT_ModifyChild,object1,
   CHILD_WeightedWidth,30,
   LAYOUT_ModifyChild,object2,
   CHILD_WeightedWidth,70,
   TAG_END
 );
 RethinkLayout( layout,intuiwin,NULL,TRUE );

kann man zum Beispiel auch nachträglich die Aufteilung zwischen den zwei
zuvor hinzugefügten Objekten verändern. Da SetGadgetAttrs das Layout nicht
neu zeichnet, muss RethinkLayout aufgerufen werden. Leider gibt es keine
Möglichkeit, mit einem Ziehbalken direkt die Objekte anzupassen.


Und jetzt zum interaktiven Teil: ReActor - ein paar Fußstapfen ...

Dann wollen wir mal: Wir starten einfach das Programm ReActor, gewöhnlich im
Verzeichnis StormC:Tools/ReActor. Das Programm lädt alle Klassenbeschreibungen,
und ein Fenster mit 6 Seiten zeigt sich:

Windows - GadgetGroups - Images - Connections - Requesters - Misc

Uns werden vor allem die ersten beiden interessieren, Images lassen sich auch
unter den Gadgets anordnen, auf Connections gehe ich nur kurz ein, unter Misc
ist dann nur noch ARexx interessant.

Wir sind auf der Windows-Seite und wählen "Add >> ApplicationWindow."
Ein Doppelklick auf das erschienene Fenster öffnet ein weiteres Fenster, in dem
man nun alle Eigenschaften (Tags) festlegen kann, die wir oben schon erwähnt
haben. Aus der linken Liste werden Elemente mit Doppelklick ausgewählt, diese
erscheinen in der Mitte, rechts kann man dann die Eigenschaft festlegen. Wir
klicken WA_Title (in der Mitte) an und geben rechts als Title "Fenster Teil
2 !" ein. Wie schon erwähnt sollte man zunächst als Local ID drei Minuszeichen
--- eingeben, solange man sein Programm nicht lokalisiert. Später sollte man
dann hier einen aussagekräftigen Bezeichner wie MSG_WINDOWPART2 angeben, unter
dem der Text sowohl im Programm als auch in der Übersetzungsdatei erscheint.
Die anderen Eigenschaften kann man so stehen lassen, wer möchte kann ein paar
Gadgets ausblenden oder die Größe und Position des Fensters angeben. Allerdings
stellen wir fest, dass WINDOW_Layout noch keinen Wert hat!

Also Fenster mit "Use" schließen, nach "GadgetGroups" wechseln. Ein Klick auf
"Add" öffnet direkt das Fenster für unsere Gadgets. Oft reicht eine solche
Gruppe aus, da wir hier den gesamten Layout-Baum einrichten könnten.
Erste Aktion ist hier das Klicken auf "Layout" und das Doppelklicken auf
"Root layout". Das öffnet wieder ein Fenster mit allen möglichen Tags.
Mit "Use" wieder raus, wir wollen endlich unseren Button!

Einfach Doppelklick auf "Button", der wird direkt an unser Layout angehängt.
Wenn man hier einen Fehler gemacht hat, kann man mit "Rem" auch das Element
wieder löschen oder mit "Up/Down" an die richtige Stelle schieben.
Der Button bekommt die gleichen Eigenschaften, wie wir sie oben direkt im
Programm stehen hatten. Dabei fällt auf, dass die GA_ID zwar ausgewählt ist,
aber keine Zahl eingegeben werden kann - dieser Wert wird automatisch von
ReActor vergeben, wir sollten daher dem Button einen schönen Namen geben:
Haben wir mit "Use" das Button-Fenster verlassen, geben wir einfach unter
"Object Name" BUTTON_EXAMPLE ein. Dieser Name dient später zum Identifizieren
des Gadgets im Programm. Das gleiche sollte man auch mit dem Layout, der
Gruppe und dem Fenster machen, damit der Quelltext übersichtlicher wird -
z.B. LAYOUT_ROOT, GROUP_MAIN und WINDOW_MAIN.

Jetzt müssen wir wieder auf die "Windows"-Seite, rein ins WINDOW_MAIN, und
WINDOW_Layout anklicken. GROUP_MAIN steht nun zur Verfügung und kann ausgewählt
werden.

Bisher haben wir noch gar nichts vom Fenster gesehen, jetzt können wir es aber
ausprobieren: im Hauptfenster gibt es den Button "Open", der das Fenster
probeweise öffnet. Hier kann sich natürlich noch das eine oder andere vom
späteren Aussehen unterscheiden, aber zum Prüfen der bisherigen Zusammenstellung
sollte diese Vorschau alle Wünsche erfüllen. Eine Reaktion auf Mausklicks (außer
dem Schließ-Button) sollte man natürlich nicht erwarten 8). Falls etwas nicht so
aussieht, wie es sollte, einfach zurück auf die entsprechende Seite, Tag ändern
und wieder auf "Open" klicken.

Und hier die erwähnten 11 Layouts in ClassAction:

Screenshot:
Screenshot

Hinter den Layouts verbergen sich noch weitere Buttons, StringGadgets und
Penmaps.


Wie bindet man nun das erzeugte Fenster in das Programm ein?

Beim Speichern erzeugt ReActor mehrere Dateien: die Resourcedatei *.res, die die
eingegebenen Informationen enthält und nur für ReActor selbst nötig ist; eine
Objektdatei *.o mit den fertigen Images, Gadgets, Fenstern etc.; eine
Headerdatei *.h, die die nötigen Konstanten enthält, um die Objekte aus unserem
Programm heraus anzusprechen; und schließlich eine Catalogdatei *.cd, die die
für die Lokalisierung nötigen Informationen enthält.

Auf die Lokalisierung gehe ich später ein; daher reichen folgende Schritte:
o Einbindung der Headerdatei in den Quelltext
o Objekte im Quelltext anlegen (dazu muss resource.library geöffnet werden)
  und wie gewohnt verwenden
o Objektdatei zum Programm linken (zu Projekt hinzufügen)

Also nehmen wir an, wir haben ein Fenster WINDOW_MAIN und eine Gruppe GROUP_MAIN
mit einem Layout und einem Button BUTTON_EXAMPLE in ReActor angelegt:

  #include <libraries/resource.h>
  #include "testprogramm.h"           /* von ReActor */
  RESOURCEFILE resource;
  Library      *ResourceBase;
  Screen       *screen;
  Object       **group1;
  ...
  ResourceBase = OpenLibrary( "resource.library",39 );
  ...
  screen = LockPubScreen( NULL );
  /* wir müssen einen Screen angeben; gibt man statt einem Bildschirmnamen    */
  /* in LockPubScreen NULL an, wird unser Fenster auf der Workbench geöffnet. */

  resource = RL_OpenResource( RCTResource,screen,NULL );
  /* Über diesen Zeiger wird der Inhalt unserer Resourcedatei angesprochen    */

  window = RL_NewObject( resource,WINDOW_MAIN,...,TAG_END );
  /* man kann hier weitere Tags angeben, die erst jetzt bekannt sind - z.B.   */
  /* Hooks, Icon oder MessagePorts.

  group1 = RL_GetObjectArray( resource, window, GROUP_MAIN );
  /* jede Gruppe wird als Array angesprochen */

  DoMethod( win,WM_OPEN );
  GetAttr( WINDOW_Window,window,(ULONG*)&intuiwin );
  ...
  /* ab jetzt unterscheidet sich das Vorgehen nicht von den "handgemachten"   */
  /* Objekten, allerdings werden die Gadgets jetzt über das Array             */
  /* angesprochen:                                                            */
  SetGadgetAttrs( (Gadget*)group1[BUTTON_EXAMPLE],intuiwin,NULL,
        GA_Text,"...",TAG_END
  );
  /* BUTTON_EXAMPLE ist ebenfalls die GA_ID, die in der Ereignisbehandlung    */
  /* auftreten wird (was oben unsere BUTTON_ID war).                          */
  ...
  DoMethod( window,WM_CLOSE );

  RL_CloseResource( resource );
  /* gibt auch alle Objekte (Window und Gadgets) frei */

  UnlockPubScreen( NULL,screen );

  CloseLibrary( ResourceBase );
  ...

So, das sollte für diesmal reichen - mit ReActor kann man ja jetzt ausprobieren,
was es so für Objekte gibt, und was die einzelnen Tags bedeuten (zur Not hilft
ein Blick in die AutoDocs...). Wie erwähnt, sollte man aber am Anfang schon mal
mit einem Absturz rechnen, wenn man irgendwo doch etwas verkehrt gemacht hat!

Im nächsten Teil geht es dann um einzelne Gadgets und ihre Besonderheiten.

Übrigens habe ich gehört, dass noch eine Reaction-Oberfläche für StrICQ gesucht
wird - wer also Lust hat, kann sich daran versuchen!

Ansonsten wünsche ich schon mal viel Spaß mit Reaction,

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

[1] http://www.meicky-soft.de/amiga-magazin/reaction.html