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

Der Einstieg

Vor 10 Jahren war es für einen Programmierer schon eine besondere Leistung,
wenn er ein Workbench-Programm mit vielen Buttons, Texteingabefeldern,
Menüs usw. entwickelt hatte - jedes Element in seinem Fenster war
Handarbeit und benötigte einige Zeit, bis es an der richtigen Stelle war
und die richtige Größe hatte. Wenn dann so etwas wie eine Liste mit Scroll-
Möglichkeit gefragt war, musste erst mal einiges an Vorarbeit geleistet
werden. Die große Überraschung kam dann aber, wenn jemand auf die Idee kam,
die Zeichensätze der Workbench zu ändern, andere Auflösungen oder Farben zu
benutzen: da sah das mühsam zusammengestellte Programm eher wie ein
schlechter Scherz aus, was man heute noch mit einigen alten Schätzchen aus
dem Aminet ausprobieren kann.

Eine Alternative kam mit MUI, einem Programm, das standardisierte Objekte
mit sich brachte, die dann von jedem Programm benutzt werden konnten.
Leider war es nicht Bestandteil des OS und musste dazugekauft werden, und
es scheint überdies nicht mehr weiterentwickelt zu werden.
In diese Lücke trat dann spätestens mit OS3.5 Reaction - wobei ich mit
diesem Wort hier auch alles meine, was man als BOOPSI bezeichnet - "Basic
Object Oriented Programming System for Intuition". Genau wie bei MUI
handelt es sich also im Grunde um eine Menge von Oberflächenobjekten, die
einmal zur Verfügung gestellt werden und dann in allen Programmen
auftauchen können. Reaction bietet dann den Kitt, der alles zusammenhält
und auch globale Einstellungen für alle Objekte (Hintergrund, Zeichensätze
usw.) ermöglicht.

Wer also heute ein Programm für den Amiga (ab OS3.5) entwickeln will, kommt
kaum an Reaction vorbei; deswegen will ich hier versuchen, die Reaction-
Programmierung zu erläutern und Beispiele für eigene Programme zu geben.
Wer mich kennt, wird wohl auch ClassAction und damit ein gutes Beispiel für
Reaction kennen. Ich werde öfter mal an diesem Programm die Entwicklung
einer eigenen Oberfläche erklären und auf gute und schlechte Erfahrungen
hinweisen. Dabei halte ich mich größtenteils an die Programmiersprache C
(persönlich programmiere ich nur C++), die Programmierung weicht in anderen
Sprachen aber kaum ab. Grundkenntnisse in einer Programmiersprache setze
ich allerdings voraus. Für die Programmierung benötigen wir in jedem Fall
die entsprechenden Bibliotheksdateien, für C sind dies also sämtliche
Header-Files (z.B. classes/window.h, clib/window_protos.h). In diesen
Dateien sind die benötigten Konstanten, Typen und Funktionsprototypen
enthalten.

Die größten Probleme bei der Programmierung am Amiga ist einerseits das
Fehlen einer grafischen Entwicklungsumgebung wie z.B. Delphi, andererseits
das Fehlen von Büchern zur aktuellen Programmierung. Deswegen kann ich hier
auch keine Bücher empfehlen, sondern nur ein paar elektronische Wege zum
Glück: Die einfachste Möglichkeit, an Informationen heranzukommen, liegt
auf der Developer CD V2.1. Auf dieser CD befinden sich zum einen alle
AutoDocs und Header-Dateien der Bibliotheksfunktionen, die wir benötigen,
außerdem noch einige gute allgemeine Artikel, leider in Englisch (z.B. in
den Ordnern Reference/Amiga_Mail...). Zum zweiten findet man dort den
Storm-C(++)-Compiler in der (bzgl. der kommerziellen Nutzung)
eingeschränkten Version 3, den ich sehr empfehlen kann. Das für unser Thema
fast wichtigste ist aber ein kleines Programm im Storm-Verzeichnis mit dem
Titel ReActor. Dabei handelt es sich um ein Tool, mit dem man sich ohne
große Mühe eine Oberfläche zusammenbauen und direkt ansehen kann. Es
erzeugt beim Speichern alle benötigten C-Dateien sowie eine Katalogdatei,
mit der das Programm lokalisiert werden kann. Natürlich kommt man auch ohne
dieses Programm aus, aber gerade zum Ausprobieren und für kleinere
Programme ist es wunderbar. Ich werde im Folgenden sowohl die direkte
Programmierung als auch die Bedienung von ReActor beschreiben.

Weitere Informationen gibt es im Aminet. Dort findet man auch neue BOOPSI-
Klassen und Hinweise, wie man eigene Klassen programmiert. Den Anfänger
werden diese Beispiele jedoch meist mehr verwirren als aufklären. Wenn es
um konkrete Probleme geht, gibt es auch eine Mailingliste (Amiga C) unter
http://yahoogroups.com, in die man sich eintragen und Fragen stellen kann.
Leider hat mein AWeb die Anmeldung nicht hinbekommen, sodass ich hier nicht
mehr darüber sagen kann. Es gibt aber auch viele Programmierer, die gerne
weiterhelfen; ich beantworte gerne Fragen zur Reaction-Programmierung
(obwohl diese Einführung eigentlich alle Probleme beseitigen sollte).

So, jetzt wird's Zeit mit Reaction anzufangen. Wie man vielleicht schon
ahnt, hat BOOPSI sehr viel mit objektorientierter Programmierung (OOP) zu
tun. Deswegen muss man jetzt nicht C++ benutzen, aber man sollte sich mit
den Grundzügen der OOP vertraut machen:

Grob gesagt heißt objektorientierte Programmierung, dass die Programme nur
noch aus einzelnen Objekten bestehen, die Eigenschaften und Methoden
besitzen. Für BOOPSI heißt das z.B., dass es ein Objekt "Fenster" gibt, das
z.B. die Eigenschaft "Titel" besitzt, und die Methoden "Öffnen" und
"Schließen". Objekte gehören immer zu einer bestimmten Klasse, hier ist
dies die "Fensterklasse". Da die Klasse schon programmiert ist und als
Datei vorliegt (im Verzeichnis sys:classes), brauchen wir als Programmierer
nur noch ein Objekt (oder auch "Instanz") anzulegen. Klassen können auch
hierarchisch aufgebaut werden, z.B. gibt es die Klasse "Buttons", darunter
- sozusagen als Unterklasse - die Klasse "Radiobutton", die Eigenschaften
und Methoden von der übergeordneten Klasse übernimmt und zusätzliche zur
Verfügung stellt (Gadgetklassen sind im Verzeichnis sys:classes/gadgets
untergebracht).

Eins vorweg: Man sollte jetzt nicht direkt seinen Quelltexteditor starten
und wild draufloshacken; so eine Reaction-Oberfläche sollte erst mal
sorgfältig geplant werden. Wir werden uns im nächsten Teil mit Fenstern und
dem grundsätzlichen Aufbau beschäftigen, jetzt schauen wir uns erst mal an,
was in unserem Programm nötig ist, damit wir die OOP nutzen können.

Zunächst benötigt man wie gewohnt bestimmte Bibliotheken, die mit
OpenLibrary und CloseLibrary geöffnet und geschlossen werden. Außer den
Standardbibliotheken, die man fast immer benötigt (Intuition,Dos,...),
kommen nun die Klassen hinzu, die man nutzen möchte. Benötigt man z.B. ein
Fenster mit nur einem Button, so sieht das in C so aus:

 struct Library
*IntuitionBase,*DosBase,*WindowBase,*LayoutBase,*ButtonBase;
 ...
 IntuitionBase = OpenLibrary( "intuition.library", 39 );
 DosBase       = OpenLibrary( "dos.library", 39 );
 WindowBase    = OpenLibrary( "window.class", 44 );
 LayoutBase    = OpenLibrary( "gadgets/layout.gadget", 44 );
 ButtonBase    = OpenLibrary( "gadgets/button.gadget", 44 );

Die Klassen werden also genauso wie Bibliotheken geöffnet, wobei
OpenLibrary auch in sys:classes sucht, Unterverzeichnisse müssen aber
angegeben werden. Die Nummer der benötigten Version ist hier nicht wichtig,
es sollte aber in jedem Fall sichergestellt sein, dass alle Bibliotheken
geöffnet wurden:

 if( (IntuitionBase == NULL) || (WindowBase == NULL) || (LayoutBase ==
     NULL)) || (ButtonBase == NULL) ){
  CloseLibrary( ButtonBase );
  CloseLibrary( WindowBase );
  CloseLibrary( LayoutBase );
  CloseLibrary( DosBase );
  CloseLibrary( IntuitionBase );
  return;  /* am besten noch Fehlermeldung und raus aus dem Programm */
 }

Und am Ende müssen die Bibliotheken genauso freigegeben werden. Wer Reactor
benutzt, muss auch noch die resource.library öffnen. Übrigens ist
LayoutBase kein Fehler, wir brauchen das Layoutgadget für jedes Fenster.

Haben wir alles geöffnet, kann's losgehen. Statt mit den BOOPSI-Objekten
selbst arbeiten wir im Grunde immer mit Zeigern. D.h. unsere Objekte sind
4-Byte-Variablen, ich arbeite immer mit dem Typ Object*, was im Grunde ein
Zeiger auf ein Langwort ist. Da die BOOPSI-Objekte ja nicht sprachabhängig
sind, muss es eine Möglichkeit geben, Objekte anzulegen, Eigenschaften zu
setzen und zu lesen sowie Methoden auszuführen. Dazu gibt es die Funktionen

 Objekt = NewObject( Klasse,KlassenName,Eigenschaft1,Wert1,
                     Eigenschaft2,Wert2,...,TAG_END )

NewObject legt ein neues Objekt an. Die Klasse wird entweder durch einen
Zeiger auf die Klasse oder durch den Namen der Klasse angegeben. Die
meisten Klassen besitzen eine eigene Bibliotheksfunktion, die den Zeiger
liefert. Zusätzlich zum Erzeugen kann man auch direkt Eigenschaften
festlegen. Dies kann entweder als Liste geschehen, wie oben angegeben, oder
mit der Funktion

 Objekt = NewObjectA( Klasse,KlassenName,TagList );

wobei TagList der Zeiger auf eine Struktur ist, die die Eigenschaften und
Werte hintereinander als Langworte (4 Byte) enthält und wieder durch das
TAG_END (als Eigenschaft) abzuschließen ist (diese Struktur heißt TagItem,
die Definition befindet sich in utilities/tagitem.h).

Dieser alternative Aufruf existiert auch für die anderen angegebenen
Funktionen mit sogenannten Tags.

Natürlich kann das Ganze auch schiefgehen - z.B. wenn man mal den
Klassennamen falsch geschrieben hat. Also: immer den Rückgabewert auf 0
prüfen! Außerdem sollte man immer achtgeben, dass man nicht falsche
Eigenschaften setzt, also z.B. für einen Button eine Eigenschaft des
Stringgadgets setzt - dieser Fehler kann nicht vom Compiler erkannt werden,
sorgt aber mindestens für seltsame Effekt, wenn nicht sogar zum Absturz.

 Ergebnis = SetAttrs( Objekt,Eigenschaft1,Wert1,...,TAG_END )

SetAttrs setzt die angegebenen Eigenschaften des Objekts auf die
angegebenen Werte. Bei unseren Gadgets muss meist auch das Fenster
informiert werden, wozu man dann die Funktion

 Ergebnis = SetGadgetAttrs( Gadget,Fenster,Requester,Eigenschaft1,
                            Wert1,...,TAG_END )

benutzen sollte. "Fenster" ist ein Zeiger auf das Fenster, in dem sich das
Gadget befindet, "Requester" ein Zeiger auf den Requester, falls das Gadget
sich in einem solchen befindet.

Die Eigenschaften und zulässigen Werte sind in den AutoDocs aufgeführt. Wir
werden uns die wichtigsten noch anschauen.

 Ergebnis = GetAttr( Eigenschaft,Objekt,Pufferadresse )

GetAttr speichert den Wert der Eigenschaft des Objekts im Puffer (der
unbedingt 4 Bytes groß sein muss, auch wenn nur eine Boolesche Variable
zurückgegeben wird!). Ist das Ergebnis 0, so besitzt das Objekt diese
Eigenschaft gar nicht. Wer möchte, kann auch die reaction.library öffnen
und die Funktion GetAttrs benutzen, die auch mehrere Eigenschaften auf
einmal auslesen kann.

 Ergebnis = DoMethod( Objekt,Methode,.. )

DoMethod führt die angegebene Methode des angegebenen Objekts aus und
übernimmt methodenspezifische Parameter. Auch die Bedeutung des
Rückgabewerts hängt von der Methode ab. Wir werden diese Funktion
hauptsächlich für Fenster benutzen, ansonsten benötigen wir keine Methoden.

Es gibt zwar in der intuition.library noch DoGadgetMethod, aber diese
Funktion wird nur äußerst selten nötig sein.

Übrigens gibt es statt eigener Methoden oft Bibliotheksfunktionen, die mit
den Objekten arbeiten, z.B. "HideListBrowserNodeChildren" für Listbrowser-
Objekte.

 DisposeObject( Objekt )

Goldene Regel für Programmierer: was du ausleihst, musst du auch wieder
zurückgeben! Sobald also das Objekt nicht mehr benötigt wird, sollte es
freigegeben werden. Aber Vorsicht: erstens sollte man sicher sein, dass es
auch angelegt wurde, zweitens muss man nicht alles freigeben. Manche
Objekte "gehören" anderen Objekten. Werden dann diese Besitzer freigegeben,
so auch die untergeordneten Objekte. Für uns heißt das z.B., dass unsere
Gadgets zum Fenster gehören und nicht eigens gelöscht werden müssen. Man
sollte sich in jedem Fall angewöhnen, Zeiger nach der Freigabe auf NULL
(bzw. NIL) zu setzen, da die meisten Routinen mit einem NULL-Zeiger, aber
nicht mit einem Zeiger auf ein schon freigegebenes Objekt umgehen können.

Bis auf DoMethod (aus der amiga.lib) befinden sich übrigens alle Funktionen
in der intuition.library.

Das sieht zwar noch sehr theoretisch aus, aber wir schauen es uns mal im
Beispiel an.

/* Prototypen einbinden : */
#include <clib/exec_protos.h>
#include <clib/alib_protos.h>
#include <clib/intuition_protos.h>
#include <clib/dos_protos.h>
#include <clib/window_protos.h>
#include <clib/layout_protos.h>
#include <clib/button_protos.h>

/* Typen und Konstanten einbinden : */
#include <intuition/classusr.h>
#include <classes/window.h>
#include <gadgets/layout.h>
#include <gadgets/button.h>

 /* Die Dateien können natürlich bei anderen Compilern anders     */
 /* heißen und an anderen Stellen stehen. Zur Not muss man die    */
 /* Verzeichnisse nach den benötigten Konstanten etc. durchsuchen.*/

struct Library *IntuitionBase,*DosBase,*WindowBase,*LayoutBase,*ButtonBase;

int main(){

 Object *window,*layout,*button;

 /* wie gesagt, man kann auch einen anderen Typ benutzen,         */
 /* z.B. ULONG window oder APTR window, es müssen nur 4 Byte sein */

 struct Window *intuiwin; /* das brauchen wir noch */

 /* So, den nächsten Teil kennen wir schon: */

 IntuitionBase = OpenLibrary( "intuition.library", 39 );
 DosBase       = OpenLibrary( "dos.library", 39 );
 WindowBase    = OpenLibrary( "window.class", 44 );
 LayoutBase    = OpenLibrary( "gadgets/layout.gadget", 44 );
 ButtonBase    = OpenLibrary( "gadgets/button.gadget", 44 );
 if( (!IntuitionBase) || (!WindowBase) || (!LayoutBase) || (!ButtonBase) ){
  CloseLibrary( ButtonBase );
  CloseLibrary( WindowBase );
  CloseLibrary( LayoutBase );
  CloseLibrary( DosBase );
  CloseLibrary( IntuitionBase );
  return( 20 );
 }

 /* Jetzt wird's interessant : */

 layout = (Object*)NewObject( LAYOUT_GetClass(),NULL,
                              LAYOUT_Orientation,LAYOUT_VERTICAL,
                              LAYOUT_DeferLayout,TRUE,
                              LAYOUT_SpaceInner,TRUE,
                              LAYOUT_SpaceOuter,TRUE,
                              TAG_END );

 /* da haben wir's: LAYOUT_GetClass() kommt aus layout.gadget    */
 /* und liefert uns den benötigten Zeiger, wir brauchen also     */
 /* keinen Klassennamen. Dann folgen einige Eigenschaften, mit   */
 /* denen wir uns noch genauer beschäftigen werden. Sie          */
 /* betreffen jedenfalls das Aussehen und die Funktionsweise des */
 /* Layout. Übrigens gibt es auch Reaction-Makros, die diese     */
 /* Erzeugung etwas angenehmer machen (in                        */
 /* reaction/reaction_macros.h). */

 button = (Object*)NewObject( BUTTON_GetClass(),NULL,
                              GA_Text,"Mein erster Reaction-Button ...",
                              TAG_END );

 /* Man gewöhnt sich dran, oder? */
 /* Schön ist, dass man wirklich nur das angeben muss, was man   */
 /* benötigt; man muss nicht wie früher erst viele Parameter     */
 /* festlegen, sondern ist mit ein paar Zeilen fertig. Zudem     */
 /* sieht es auch noch übersichtlicher aus.*/

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

 /* Hier ordnen wir den Button unserem Layout unter. Dazu müssen  */
 /* wir nur die Eigenschaft LAYOUT_AddChild setzen. Übrigens      */
 /* hätten wir auch SetAttrs benutzen können, wenn wir sowieso    */
 /* weder Fenster noch Requester angeben. Aber wir sollten uns    */
 /* daran gewöhnen, mit SetGadgetAttrs zu arbeiten, denn wenn das */
 /* Fenster offen ist, geht's nicht anders. */

 window = (Object*)NewObject( WINDOW_GetClass(),NULL,
                              WINDOW_Layout,layout,
                              WINDOW_Position,WPOS_CENTERMOUSE,
                              WA_Activate,TRUE,
                              WA_Title,"Ein Fenster !!",
                              WA_DragBar,TRUE,
                              TAG_END );

 /* Hier übergeben wir dem Fenster direkt sein "Kind", nämlich   */
 /* unser Layout. Dann folgen einige Eigenschaften, mit denen    */
 /* wir uns nächstes Mal genauer beschäftigen. Sie betreffen     */
 /* jedenfalls das Aussehen und die Funktionsweise des Fensters. */
 /* Offen ist es aber noch nicht, also: */

 DoMethod( window,WM_OPEN );

 /* So einfach ist das. Wer sich mal die AutoDocs anschaut, wird  */
 /* feststellen dass man aber bei der Angabe eines Fensters immer */
 /* einen Zeiger auf eine Window-Struktur benötigt und kein       */
 /* Objekt. Kein Problem, holen wir ihn uns: */

 GetAttr( WINDOW_Window, window, &intuiwin );

 /* Immer aufpassen, dass man einen ZEIGER auf die Variable    */
 /* übergibt, sonst kommt der Guru ... */

 Delay( 500 ); /* wir warten erst mal ca. 10 Sekunden ... */

 SetGadgetAttrs( (Gadget*)button,intuiwin,NULL,
                 GA_Text,"... ist immer noch da!",TAG_END );

 /* Hier ist endlich unser intuiwin im Einsatz! Hat auch einen  */
 /* Sinn, denn das Fenster muss direkt aktualisiert werden.     */
 /* Übrigens ist es mir schon des öfteren passiert, dass ich    */
 /* TAG_END vergessen habe - gerade wenn ich zuvor GetAttr      */
 /* benutzt habe. Das macht sich dann meist durch seltsame      */
 /* Ergebnisse oder sogar einen Absturz bemerkbar. Sobald man   */
 /* mal das Programm debuggt und Enforcer oder ein ähnliches    */
 /* Tool startet, findet man meist den Fehler. */

 Delay( 500 ); /* weil's so schön ist ... */

 DoMethod( window,WM_CLOSE ); /* klar, oder ? */

 DisposeObject( window );

 /* So, das war's. Wie gesagt, das Layout und der Button werden vom */
 /* Window freigegeben. Natürlich müssen jetzt noch die Libraries   */
 /* geschlossen werden. Und wenn das Programm noch weiter geht,     */
 /* sollte man nicht vergessen, alle Zeiger auf NULL zu setzen:     */

 window   = NULL;
 layout   = NULL;
 button   = NULL;
 intuiwin = NULL;

 CloseLibrary( ButtonBase );
 CloseLibrary( WindowBase );
 CloseLibrary( LayoutBase );
 CloseLibrary( DosBase );
 CloseLibrary( IntuitionBase );
 return( 0 );
}


Screenshot:
Screenshot


An dieser Stelle möchte ich schon mal ein paar Tips loswerden, auch wenn
ich erst in den nächsten Teilen auf die angesprochenen Themen eingehe:

- Wer schon mit Reactor arbeitet, wird früher oder später beim
  Kompilieren die Fehlermeldung "Symbol _STR nicht definiert"
  erhalten. Der Grund ist einfach, dass man irgendwo vergessen hat, zu
  einem Text einen Konstantenbezeichner als "Locale ID" anzugeben.
  Diese Id's dienen zur Lokalisierung des Programms und sind unbedingt
  notwendig; am Anfang reicht es, irgendeinen Text einzugeben, z.B.
  MSG_TEXT1 usw.

- Reactor stürzt manchmal ab, wenn man falsche Einstellungen vorgenommen
  hat oder Objekte entfernt hat, die an anderer Stelle noch eingetragen
  waren. (Z.B. sollte man erst den Verweis auf ein Label entfernen,
  dann das Label selbst.) Man sollte hier also ein bisschen mitdenken
  und nicht alles dem Programm überlassen ;)

- Manches funktioniert nicht so, wie man erwarten könnte: z.B. wird
  normalerweise ein Gadget direkt nach SetGadgetAttrs() aktualisiert,
  bei einem Checkbox-Gadget allerdings muss RefreshGList() explizit
  aufgerufen werden. Man sollte nicht direkt verzweifeln, sondern die
  AutoDocs durchstöbern, meist findet man irgendwo einen Hinweis oder
  eine Funktion, die das Problem löst!


Jetzt sind wir schon am Ende des ersten Teils angekommen. Unser erstes
Programm ist schon fertig und sieht doch hübsch aus! Jedenfalls kann sich
niemand über endlose Quelltexte beschweren. Vor allem gab es nirgends
irgendeine absolute Größe oder Position! Jetzt ist der Punkt gekommen,
dieses Programm abzutippen und erste Erfahrungen zu sammeln. Wer möchte,
kann sich ja schon mal die AutoDocs zu den Gadgets anschauen, oder im
Internet stöbern. Eine schöne Beschreibung von BOOPSI und ein schon etwas
komplexeres Beispielprogramm findet sich auf der Developer-CD unter
Reference/Amiga_Mail_Vol2/IV-23/. Im nächsten Teil geht es dann
hauptsächlich um Fenster (Erzeugung, Eigenschaften, Ereignisbehandlung) und
ReActor.


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

euer

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