Navigation
« 

Anonymous




Register
Login
« 
« 

Amiga Future

« 

Community

« 

Knowledge

« 

Last Magazine

The Amiga Future 167 was released on the March 5th.

The Amiga Future 167 was released on the March 5th.
The Amiga Future 167 was released on the March 5th.

The Amiga Future 167 was released on the March 5th.
More informations

« 

Service

« 

Search




Advanced search

Unanswered topics
Active topics
« 

Social Media

Twitter Amigafuture Facebook Amigafuture RSS-Feed [german] Amigafuture RSS-Feed [english] Instagram YouTube Patreon WhatsApp
« 

Advertisement

Amazon

Patreon

« 

Partnerlinks

Reaction-Programmierung Teil ,4-6

Description: Amiga Aktuell Ausgabe 6,8,11/2002

Categories: [DE] Workshops

Link to this article: Select all

[url=https://www.amigafuture.de/app.php/kb/viewarticle?a=1876&sid=0b734534c8b19d11e9822c90bb3930c5]Artikeldatenbank - Reaction-Programmierung Teil ,4-6[/url]

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.

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

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

Einführung in die Reaction-Programmierung (von Martin R. Elsner)
Teil 5: Ressourcen


Heute kommen wir zu einem etwas anderen Teil der Programmentwicklung, der nicht direkt mit Reaction zusammenhängt, den man aber auch für unsere Objekte benötigt (ich hoffe, die Puristen unter euch werden mir das nicht übel nehmen ;). Zusätzlich zum eigentlichen Quellcode benötigt man meist weitere Daten; man spricht auch von Ressourcen (wörtlich "Vorräte" oder "Hilfsmittel"). Dabei handelt es sich weitgehend um Texte und Grafiken.

Dabei kommt den Texten eine besondere Bedeutung zu, da diese möglichst immer in der richtigen Sprache erscheinen sollen! Gerade im Amiga-Bereich sollte man darauf achten, dass eigene Programme mindestens die Voraussetzungen dafür bieten. Die eigentliche Übersetzung (d.h. die Erstellung sogenannter "Catalogs") kann mit geringen Kenntnissen von fast jedem Interessierten angefertigt werden, was den Programmierer wieder sehr entlastet. Die Lokalisierung eines Programms soll der Hauptteil des heutigen Kurses werden, zuvor aber ein paar Worte zu den
Grafiken

Grafiken müssen im Allgemeinen nicht sprachspezifisch sein, aber verwöhnte User möchten gerne auch diesen Teil des Programms selbst konfigurieren. Zum Teil ist es auch nötig, dem Anwender die Auswahl zwischen verschieden großen Grafiken zu bieten (siehe z.B. die Bilder in den Listern von ClassAction, die 16 oder 11 Pixels hoch sein können), da verschiedene Bildschirmauflösungen oder Schriften Aussehen und Benutzbarkeit des Programms beeinträchtigen.

Da das Einbinden von Grafiken in den Quelltext nicht ohne Hilfsmittel möglich ist, empfehle ich dafür den Einsatz von ReActor. Dort können Images angelegt und direkt mit Binärdaten gefüllt werden, die dann in der von ReActor erzeugten Objektdatei abgelegt werden. Vorteile sind hier die einfache Handhabung und die Kompaktheit des Programms, Nachteil ist allerdings die statische Festlegung, die ein Ändern der Grafiken nur durch Neukompilieren erlaubt. Außerdem ist es nicht möglich, selten benutzte Grafiken erst bei Bedarf zu laden.

Statt mit ReActor kann man Bilder aber auch im Quelltext anlegen, und zwar mit dem Bitmap-Objekt. Es erlaubt die Angabe einer Grafikdatei (BITMAP_SourceFile), aus der mittels Datatypes die benötigten Informationen ausgelesen werden. Wichtig ist hierbei, dass der Bildschirm mit BITMAP_Screen übergeben wird, damit die Farbinformationen richtig zusammengestellt werden. Daran sollte man auch denken, wenn das Programm verschiedene Bildschirme benutzt; ein Bitmap kann nicht für mehrere Screens benutzt werden, ohne die Bildinformationen neu zu berechnen. Übrigens erlauben nicht alle Datatypes die Erzeugung einer Transparenz-Maske (siehe BITMAP_Masking/BITMAP_MaskPlane), so dass manche Grafiken immer als Rechtecke erscheinen. Hier empfiehlt sich ein Test mit verschiedenen Grafikformaten, wobei das altgediente IFF-Format immer brauchbare Ergebnisse liefert und auch mit allen Grafikprogrammen am Amiga bearbeitet werden kann.

Während alle BOOPSI-Objekte, die Schalter und Eingabemöglichkeiten kapseln, im Kern aus einer "struct Gadget" bestehen, bauen Bitmaps, Labels etc. auf einer "struct Image" auf (dies wird auch in der Klassenhierarchie deutlich: die "rootclass" als Basis ist Mutterklasse für "gadgetclass", "imageclass" und "icclass", wobei letztere eher unbedeutend ist). Dies hat den Vorteil, dass an allen Stellen, an denen ein Image erwartet wird (u.a. bei den Knoten der Listbrowser-Labels), ein Zeiger z.B. auf das Bitmap-Objekt verwendet werden kann, denn die ersten Bytes dieses Objekts enthalten eine "struct Image".

Dadurch ist es ohne Probleme möglich, Grafikdateien aus einem Verzeichnis einzulesen und zu verwenden, die dann auch ausgetauscht werden können, z.B. um ganze "Skins", also einheitliche Oberflächen, zu realisieren (siehe auch WINDOW_BackFillName der Window-Klasse).

Soweit zu den Grafiken, nun zum schwierigeren Teil:
Catalog Descriptions

Die Vorbereitung eines Programms für die Übersetzung nennt man Lokalisierung. Grundlage ist am Amiga immer eine Catalog-Description-Datei (#?.cd).

Wer das Tool ReActor verwendet, hat schon Bekanntschaft mit dieser Datei gemacht, denn in ihr werden sämtliche anzuzeigenden Texte abgelegt (z.B. die Werte für GA_Text, WA_Title usw.). Deswegen fordert ReActor an diesen Stellen auch schon die Eingabe einer ID, mit der dieser Text später automatisch durch einen entsprechenden übersetzen String ersetzt werden kann.

Wenn weitere Texte im Programm benötigt werden (Fehlermeldungen, Warnhinweise, Menüpunkte,...), sollte man diese der Einfachheit halber in ReActor einbinden, da die Koordination sonst etwas aufwendiger ist. Dazu kann man z.B. unter "Images" Labels hinzufügen, die dann in LABEL_Text den entsprechenden Text (z.B. "Are you sure?"), unter "Locale ID" einen aussagekräftigen Namen ("MSG_AREYOUSURE") und als "Object Name" eine ähnliche Bezeichnung mit Wiedererkennungswert erhalten ("LABEL_AREYOUSURE"). Sortiert man die Einträge alphabetisch, so erhält man dadurch eine gute Übersicht über die vorhandenen Texte.

Doch die .cd-Datei lässt sich genauso gut auch per Hand anlegen, wenn man ReActor nicht nutzen möchte. Sie sieht beispielsweise so aus:

; *** catalog description file of "ClassAction.res"
;
MSG_HELP (272//)
Help
;
MSG_ICONIFY (273//)
Iconify
;
MSG_PROJECT (274//)
Project
;
...

Der Aufbau ist recht einfach: (//) ;

Die Werte in der Klammer können jeweils entfallen, was dann bedeutet: keine Nummer: Nummer des letzten Eintrags + 1 keine Mindest-/Höchstlänge: keine Beschränkung des Strings.

(Zu den Namenskonventionen siehe CatComp-Anleitung)

Allerdings sollte man an der von ReActor generierten .cd-Datei nichts per Hand ändern, da diese beim nächsten Speichern in ReActor überschrieben wird.

Von der Beschreibung zum Quelltext

Mit dem Programm CatComp, das u.a. auf der DevCD zu finden ist, oder ähnlichen Programmen (siehe unten) lassen sich nun aus der .cd-Datei Quelltexte erzeugen. Der Aufruf:

catcomp myproject.cd CFILE myproject.h

Dies erzeugt eine Header-Datei myproject.h, in der Nummern und Strings enthalten sind. Dies reicht aus, um im Quelltext einen lokalisierten Text anzusprechen. ReActor benötigt allerdings auch eine Objektdatei mit den nötigen Informationen, sodass eine .asm-Datei erzeugt werden muss:

catcomp myproject.cd CFILE myproject.h ASMFILE myproject.asm XDEF

Die zusätzliche Assembler-Datei muss nun noch übersetzt und zum Programm gelinkt werden.

In StormC kann die .cd-Datei einfach zum Projekt hinzugefügt werden, als Übersetzungsskript wählt man "catcomp.srx", das man per Hand ändern sollte ("Makefile editieren"):

PARSE ARG '"' filename '"' '"' projectname '"' .

/* .h und .asm-Datei erzeugen, wobei "CD" angehängt wird, um Verwechslungen zu vermeiden */
objectname_h = LEFT(filename,LASTPOS('.cd',filename)-1)||"CD.h"
objectname_asm = LEFT(filename,LASTPOS('.cd',filename)-1)||"CD.asm"

OBJECTS filename objectname_h objectname_asm

ADDRESS COMMAND "catcomp "||filename||" CFILE "||objectname_h||" ASMFILE "||objectname_asm||" XDEF"

ADDFILE objectname_h QUIET
ADDFILE objectname_asm QUIET

Für die zusätzliche .asm-Datei, die nach dem Kompilieren im Projekt auftaucht, kann das Übersetzungsskript "phxass.srx" ausgewählt werden; folgende Änderung bietet sich an:




ADDRESS COMMAND 'StormC:StormSYS/PhxAss '||filename||' TO '||objectname||' SET "CATCOMP_ARRAY" NOEXE I stormc:asm_include QUIET'



In stormc:asm_include habe ich die Assembler-Includes (auf der DevCD: "ADCD_2.1:NDK/NDK_3.5/Include/include_i/#?") hinterlegt, die PhxAss benötigt.
Verwendung der lokalisierten Texte

Das Projekt sollte jetzt schon übersetzt werden können und laufen.

In allen Dateien, in denen wir lokalisierte Texte verwenden wollen, müssen wir die nötigen Header-Dateien einfügen:

#include
#define CATCOMP_NUMBERS
#define CATCOMP_STRINGS
#include "MyProjectCD.h"

Im Quelltext müssen wir die Locale.library und den entsprechenden Catalog öffnen:

struct LocaleBase *LocaleBase;
struct Catalog *MyCatalog;
*LocaleBase = (struct LocaleBase*)OpenLibrary( "locale.library",39 );
*MyCatalog = OpenCatalog( NULL,"MyProject.catalog",OC_BuiltInLanguage,"english",
OC_Version,1,CATVERS,TAG_DONE );

und natürlich am Ende mit CloseCatalog(MyCatalog) und CloseLibrary(LocaleBase) wieder schließen.

OC_BuiltInLanguage gibt die Sprache der Texte an, die unser Projekt bereits enthält. Man sollte diese Texte immer in Englisch angeben, damit man im Falle fehlender Catalogs immer noch die Chance hat, alle Beschriftungen und Meldungen zu verstehen. OC_Version ist von Interesse, falls mehrere Programmversionen existieren und Veränderungen bei den Texten stattgefunden haben. Unveränderte Texte werden nur dann richtig dargestellt, wenn auch die Nummern identisch sind, was bei der Verwendung von ReActor nicht unbedingt der Fall ist. Auf die Version des Catalogs werde ich unten noch kurz eingehen.

Während das Ergebnis von OpenLibrary geprüft werden sollte, kann uns der Rückgabewert von OpenCatalog fast egal sein - solange wir nur die Funktion GetCatalogStr() benutzen, reicht auch ein NULL-Wert, sodass das Programm in jedem Fall mit den Standardtexten benutzt werden kann!

Um die von ReActor erzeugten Objekte brauchen wir uns nicht mehr zu kümmern, nur unsere eigenen Texte müssen noch eingebaut werden: z.B.

ShowMessage( GetCatalogStr(MyCatalog,MSG_AREYOUSURE,MSG_AREYOUSURE_STR) );

(vorausgesetzt, es existiert eine Funktion ShowMessage(), die einen Text anzeigt). Dies kann mit einem Makro auch abgekürzt werden: mit

#define GETSTR(id) GetCatalogStr(MyCatalog,id,id ## _STR)

reicht dann
GETSTR(MSG_AREYOUSURE)

Das bedeutet natürlich, dass sämtliche Quelltextdateien nach solchen Texten durchsucht, die Texte in ReActor oder direkt in die .cd-Datei eingetragen und dann im Quelltext mit GetCatalogStr() ausgelesen werden müssen ...
Catalog erzeugen

Damit auch die richtigen Texte im Programm angezeigt werden, müssen natürlich zunächst Catalogs erzeugt werden. Zu diesem Zweck empfehle ich das Programm ReCatIt, das die Arbeit sehr erleichtert. Entscheidet man sich für die manuelle Methode, so sind 3 Schritte nötig:

1. Übersetzungsdatei #?.ct erzeugen:

catcomp MyProject.cd CTFILE MyProject.ct

2. Übersetzungsdatei übersetzen und anpassen; Beispiel:

## version $VER: ClassAction.catalog 44.0 (05-05-02)
## codeset 0
## language deutsch
;
MSG_HELP
Hilfe
;
MSG_ICONIFY
Verbergen
;
MSG_PROJECT
Projekt
;

3. Catalog erzeugen:

catcomp MyProject.cd MyProject.ct CATALOG MyProject.catalog

... und fertig. ReCatIt vereint alle drei Schritte und bietet eine praktische Oberfläche zur Bearbeitung der Texte.

Diesen Catalog muss man allerdings im richtigen Verzeichnis hinterlegen, denn der Name "MyProject.catalog" sagt nichts über die Sprache aus. Sie wird im Verzeichnis Locale:Catalogs/ oder PROGDIR:Catalogs/ abgelegt, also z.B. in Locale:Catalogs/deutsch/MyProject.catalog.

Für weitergehende Fragen verweise ich auf die Docs von locale.library, CatComp und ReCatIt.

Ich hoffe, das hat wieder ein paar Probleme gelöst, Fragen beantwortet und den Anstoß zu vielen Übersetzungen geliefert! Übrigens ist die Lokalisierung am Amiga meiner Meinung nach besser gelöst als unter Windows, selbst wenn man dort eine sonst recht komfortable Entwicklungsoberfläche wie Delphi nutzt.

Ein kleiner Hinweis in eigener Sache: ClassAction erlaubt es recht einfach, .cd/.ct-Dateien mit mehreren Cli-Befehl oder ARexx-Skripten zu verbinden, so dass man den Aufruf von CatComp oder ReCatIt mit einem Mausklick (oder zweien ;) erledigen kann.

So, genug der Werbung, jetzt kann ausprobiert werden,

euer Martin R. Elsner

Bezugsquellen:

CatComp:
DevCD: ADCD_2.1:NDK/NDK_3.5/Tools/CatComp 
(leider nicht im Aminet - falls jemand eine Adresse kennt, unter der man CatComp herunterladen kann, wäre ich über einen Hinweis dankbar!) 

FlexCat (Alternative zu CatComp mit kleineren Unterschieden):
Aminet: dev/misc/FlexCat.lha 

KitCat (kompatible, aber etwas eingeschränkte - deutsche - Alternative zu CatComp):
Aminet: dev/misc/KitCat.lha 

ReCatIt:
Aminet: dev/misc/ReCatIt.lha oder dev/misc/ReCatItPro.lha 
CatEdit (Alternative zu ReCatIt):
Aminet: dev/misc/CatEdit.lha 


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

Sechster und letzter Teil: Eigene Klassen


Die größten Probleme sollten jetzt schon gelöst sein, vielleicht ein eigenes Programm fertiggestellt, das ausgiebig Reaction-Objekte benutzt. Aber vielleicht ist der Eine oder Andere irgendwo auf eine Idee gestoßen, die er mit den Standard-Objekten nicht lösen konnte. Aber auch dafür gibt es eine Lösung: Man programmiert einfach eine eigene Klasse!

Zunächst ein kleiner Hinweis: Man sollte sich zunächst überlegen, ob die Idee wirklich nicht mit den schon vorhandenen Objekten umgesetzt werden könnte. Manchmal findet man in den Autodocs Klassen oder Tags, die man noch nicht genau kennt und bisher unterschätzt hat. Z.B. kann ein Label nicht nur eine einfache Beschriftung, sondern mehrere Zeilen Text und Grafiken darstellen.

Außerdem ist es empfehlenswert, Standardklassen zu verwenden, da diese meist gut getestet sind, weiterentwickelt werden und für einheitliche Oberfläche und Funktionsweise der Programme sorgen.

Falls man trotzdem nicht fündig wird, kann auch im Aminet recherchiert werden. Hier muss man aber auf die eben erwähnten Vorteile verzichten und hat meist auch keinen Zugriff auf die Quelltexte. Dann bleibt keine Wahl: Man muss selber ran!

Eine eigene Klasse wird immer von einer schon bestehenden abgeleitet. Dazu bietet sich eine Klasse an, die möglichst viele der gewünschten Eigenschaften schon mitbringt, sodass nur die Differenzen neu programmiert werden müssen. Man entscheidet sich z.B. dafür, den Button als Basisklasse zu nutzen und eine neue - private - Klasse zu entwickeln.

Eine umfassende Einführung in die Erzeugung eigener Klassen findet man im Kurs von Michael Christoph (http://www.meicky-soft.de/amiga-magazin/reaction.html oder direkt bei http://www.amiga-magazin.de/magazin/a02 ... ieren.html). Hier möchte ich deswegen nur als Beispiel eine Klasse aufführen, die mir Stephan Rupprecht zur Verfügung gestellt hat, und die ich für den Einsatz in ClassAction etwas modifiziert habe: eine Icon-Klasse, basierend auf Gadgetclass (also der Basisklasse für alle Gadgets), die ein Icon als Grafik verwendet und auf Mausklicks wie ein Button reagiert.

Zum Vergleich: Ein Button kann natürlich mit GA_Image auch mit einer beliebigen Grafik gefüttert werden, die man z.B. über ein Penmap- oder Bitmap-Objekt aus einem Icon (bzw. der .info-Datei) erzeugt hat, vorausgesetzt, ein entsprechender DataType existiert im System. Allerdings ist der aktuelle Icon-Datatype nicht in der Lage, den Hintergrund der Grafik transparent zu zeichnen, und nicht auf allen Systemen ist ein Icon- Datatype installiert. Unser Icon-Button soll dagegen immer funktionieren und das Icon wie auf der Workbench anzeigen. Der Einfachheit halber soll die Klasse nicht extern, sondern im Quellcode des Programms selbst enthalten sein.

Was benötigt man für eine eigene Klasse?

Nun, außer der Festlegung auf eine Basisklasse wird hauptsächlich eine Ereignisbehandlungsfunktion benötigt, und zwar in Form eines Hooks. Diese kann dann mit DoSuperMethod auf die geerbten Methoden zurückgreifen (falls sie existieren), kann aber auch (und das ist ja der Sinn der Sache) neue Funktionen einführen. Bei Gadgets kann dies hauptsächlich die Darstellung oder die Reaktion auf Ereignisse betreffen.

Für unseren Icon-Button müssen zwei Dinge geregelt werden: das Zeichen des Icons, für das wir DrawIconState verwenden, und die Reaktion auf einen Mausklick. Dazu existieren die Methoden GM_RENDER und GM_HANDLEINPUT, die wir in unserer Behandlungsroutine butclass_DISPATCH abfangen und in butclass_RENDER und butclass_HANDLEINPUT bearbeiten. Wir müssen natürlich das Icon selbst (d.h. einen Zeiger auf ein DiskObject) irgendwo speichern; dazu verwenden wir einfach GA_UserData. Wenn wir aber auch einen BackFill- Hook unterstützen und auf einen Doppelklick prüfen wollen, brauchen wir zusätzliche Eigenschaften in unserem Objekt: Dazu definieren wir eine Struktur InstanceData, die von Intuition angelegt und freigegeben wird, aber von uns genutzt werden kann. Diese muss bei OM_NEW bzw.

OM_SET/OM_UPDATE gefüllt werden und kann dann in den sonstigen Routinen mit Hilfe des Makros INST_DATA verwendet werden. Außerdem muss bei GM_DOMAIN die Größe unseres Buttons ausgerechnet werden, damit die übergeordneten Objekte den nötigen Platz reservieren können.

Zunächst die nötigen Includes, Abkürzungen und Bibliotheken:

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include

#define REG(reg,arg) register __##reg arg
#define G(x) ((struct Gadget *)x)

extern struct Library
*IconBase,
*DOSBase,
*IntuitionBase,
*UtilityBase,
*LayersBase,
*GfxBase;

Nun unsere zusätzlichen Daten pro Objekt, und die benötigten Funktionen:

struct InstanceData{
ULONG secs; /* nur zur Prüfung ... */
ULONG micro; /* ... auf Doppelklick */
struct Hook *backfill; /* siehe butclass_RENDER */
Object *parent; /* siehe butclass_SET */
};

ULONG butclass_DISPATCH( REG( a0, Class *cl ), REG( a2, Object *o ), REG( a1, Msg msg ) );
ULONG butclass_DOMAIN( Class *cl, Object *o, struct gpDomain *gpd );
ULONG butclass_HANDLEINPUT( Class *cl, Object *o, struct gpInput *gpi );
ULONG butclass_RENDER( Class *cl, Object *o, struct gpRender *gpr );
ULONG butclass_SET( Class *cl, Object *o, struct opSet *ops );

Zwei Funktionen erlauben das Anlegen und Freigeben unserer Klasse:


IClass *MakeIconClass(){
IClass *cl;

if( cl = MakeClass( NULL,
"gadgetclass", /* die Basisklasse */
NULL,
sizeof( struct InstanceData ), /* Größe unserer Zusatzdaten */
0L ) )
cl->cl_Dispatcher.h_Entry = (HOOKFUNC) butclass_DISPATCH;
/* Einzige Initialisierung: Einsetzen unseres Dispatchers */
return cl;
}

BOOL RemoveIconClass( IClass *cl ){
if( cl ) return FreeClass(cl);
else return FALSE;
}

D.h. wir speichern den Zeiger (cl=MakeIconClass()). um ihn beim Erzeugen von Gadgets zu verwenden (gadget=(Object*)NewObject( cl,NULL,...)), und geben die Klasse zuletzt mit RemoveIconClass(cl) wieder frei.

Und jetzt der ganze Rest:

ULONG butclass_DISPATCH( REG( a0, Class *cl ), REG( a2, Object *o ), REG( a1, Msg msg ) ){
/* allgemeine Ereignisbehandlungroutine */
ULONG ret = 0L;

switch( msg->MethodID ){
case GM_HANDLEINPUT:
case GM_GOACTIVE: ret = butclass_HANDLEINPUT( cl, o, (struct gpInput *) msg ); break;

case OM_SET:
case OM_UPDATE: ret = butclass_SET( cl, o, (struct opSet *) msg ); break;

case GM_RENDER: ret = butclass_RENDER( cl, o, (struct gpRender *) msg ); break;

case GM_DOMAIN: ret = butclass_DOMAIN( cl, o, (struct gpDomain *) msg ); break;

case OM_NEW:{
if( ret = DoSuperMethodA( cl, o, msg ) ){
/* zusätzlich zum Erzeugen des Objekts müssen Eigenschaften gesetzt werden */
butclass_SET( cl, (Object *) ret, (struct opSet *) msg );
}
break;
}
default: ret = DoSuperMethodA( cl, o, msg );
/* in JEDEM Fall alle anderen Methoden an die Mutterklasse übergeben! */
}
return ret;
}

/****************************************************************************/

void butclass_redraw( Object *o, struct GadgetInfo *gi ){
/* siehe butclass_HANDLEINPUT */
struct RastPort *rp;

if( rp = ObtainGIRPort( gi ) ){
DoMethod( o, GM_RENDER, (ULONG) gi, (ULONG) rp, GREDRAW_REDRAW );
ReleaseGIRPort( rp );
}
}

/****************************************************************************/

ULONG butclass_HANDLEINPUT( Class *cl, Object *o, struct gpInput *gpi ){
/* Reaktion auf Eingaben (Mausklicks) des Benutzers */
InputEvent *ie = gpi->gpi_IEvent;
ULONG retval = GMR_MEACTIVE;

if( ie ){
switch( ie->ie_Class ){
case IECLASS_RAWMOUSE:{
switch( ie->ie_Code ){
case SELECTUP: retval = GMR_NOREUSE; break;

case SELECTDOWN:{
InstanceData *id = (InstanceData*)INST_DATA( cl, o );

#ifdef DOUBLECLICK
/* Falls das Icon eine GADGETUP-Botschaft nur nach einem */
/* Doppelklick liefern soll, müssen wir den Zeitpunkt des */
/* letzten Klicks prüfen: */
if( DoubleClick( id->secs, id->micro, ie->ie_TimeStamp.tv_secs,
ie->ie_TimeStamp.tv_micro ) ){
G(o)->Flags |= GFLG_SELECTED;
butclass_redraw( o, gpi->gpi_GInfo );
retval = GMR_NOREUSE | GMR_VERIFY;

id->secs = id->micro = 0UL;
}else{
id->secs = ie->ie_TimeStamp.tv_secs;
id->micro = ie->ie_TimeStamp.tv_micro;
G(o)->Flags ^= GFLG_SELECTED;
butclass_redraw( o, gpi->gpi_GInfo );
retval = GMR_NOREUSE;
}
#else
/* GADGETUP nach einfachem Klick */
G(o)->Flags |= GFLG_SELECTED;
butclass_redraw( o, gpi->gpi_GInfo );
retval = GMR_NOREUSE | GMR_VERIFY;
#endif
}
break;

case MENUDOWN: retval = GMR_REUSE; break;
}
break;
}
}
}
return retval;
}

/****************************************************************************/

BOOL IsIconFrameless( struct DiskObject *icon ){
/* Prüft, ob das Icon ohne Rahmen dargestellt werden muss */
/* siehe butclass_RENDER*/
ULONG frameless;

IconControl( icon,ICONCTRLA_GetFrameless, (ULONG) &frameless,TAG_DONE );

if( frameless == FALSE ){
ULONG globalfl;
if( IconControl( NULL,ICONCTRLA_GetGlobalFrameless, (ULONG) &globalfl,TAG_DONE ) )
frameless = globalfl;
}

return frameless;
}

/****************************************************************************/

ULONG butclass_RENDER( Class *cl, Object *o, struct gpRender *gpr ){
/* Zeichnen des Icons */
RastPort *rp = gpr->gpr_RPort;
DiskObject *icon = (struct DiskObject *) G(o)->UserData;
Hook *hook;
InstanceData *id = (InstanceData*)INST_DATA( cl, o );
Layer *layer = gpr->gpr_GInfo->gi_Layer;
WORD x = G(o)->LeftEdge, y = G(o)->TopEdge;

/* Unter unserem Icon muss der richtige Hintergrund dargestellt werden: */
hook = InstallLayerHook( layer, id->backfill );

DrawIconState(rp, icon, NULL, x,y,
(G(o)->Flags & GFLG_SELECTED) != 0L,
ICONDRAWA_DrawInfo, (ULONG) gpr->gpr_GInfo->gi_DrInfo,
ICONDRAWA_Frameless, IsIconFrameless( icon ),
TAG_DONE );

InstallLayerHook( layer, hook );

return 0L; }

/****************************************************************************/

ULONG butclass_SET( Class *cl, Object *o, struct opSet *ops ){
/* Setzen der Eigenschaften */
InstanceData *id = (InstanceData*)INST_DATA( cl, o ); TagItem *ti, *tlist = ops->ops_AttrList;
DiskObject *icon = NULL;

while( ti = NextTagItem( &tlist ) ){
switch( ti->ti_Tag ){
case GA_BackFill:
id->backfill = (Hook*) ti->ti_Data;
break;
case LAYOUT_Parent:
id->parent = (Object *) ti->ti_Data;
break;

case GA_UserData:
icon = (struct DiskObject *) ti->ti_Data;
break;
}
}

if( id->parent && icon ){
/* Berechnen der Icongröße und Mitteilung an das Parent-Objekt */
Rectangle rect;
UWORD w,h;

GetIconRectangle( NULL, icon, NULL, &rect,
ICONDRAWA_Borderless, FALSE, TAG_DONE );
/* Wir zählen also den Rand mit */

w = ( rect.MaxX - rect.MinX ) + 1;
h = ( rect.MaxY - rect.MinY ) + 1;

SetAttrs( id->parent,
LAYOUT_ModifyChild, (ULONG) o,
CHILD_MinWidth, w,
CHILD_MinHeight, h,
CHILD_MaxWidth, w,
CHILD_MaxHeight, h,
TAG_DONE );
/* Dies war die einzige Stelle, für die wir id->Parent brauchten. */
/* Da das Icon nachträglich geändert werden kann, müssen wir es */
/* im Objekt speichern. */
}

return (ops->MethodID != OM_NEW) ? DoSuperMethodA( cl, o, (Msg) ops ) : 1L;
/* bei OM_NEW wurde die SuperMethod schon in HANDLEINPUT aufgerufen! */
}

/****************************************************************************/

ULONG butclass_DOMAIN( Class *cl, Object *o, struct gpDomain *gpd ){
/* Berechnen der Gadget-Ausmaße */
InstanceData *id = (InstanceData*)INST_DATA( cl, o );
DiskObject *icon = (struct DiskObject *) G(o)->UserData;

if( icon ){
/* nur wenn das Icon schon geladen wurde, steht die Größe des Buttons fest */
struct Rectangle rect;
UWORD w,h;

GetIconRectangle( NULL, icon, NULL, &rect,
ICONDRAWA_Borderless, FALSE,
TAG_DONE );

w = rect.MaxX - rect.MinX + 1;
h = rect.MaxY - rect.MinY + 1;

switch( gpd->gpd_Which ){
case GDOMAIN_MINIMUM:
gpd->gpd_Domain.Width = w;
gpd->gpd_Domain.Height = h;
break;

case GDOMAIN_NOMINAL:
case GDOMAIN_MAXIMUM:
gpd->gpd_Domain.Width = w;
gpd->gpd_Domain.Height = h;
break;
}
return 1L;
}

return 0L;
}

Mit dieser Klasse können nun Objekte erzeugt und in Layouts eingefügt werden, wobei mit GA_UserData ein Zeiger auf das Icon übergeben werden muss, das man mit GetIconTags bei gegebenem Dateinamen laden kann.

Natürlich ist diese Klasse weder besonders trickreich noch völlig ausgereift - man könnte noch weitere Eigenschaften hinzufügen, die die Reaktion auf Doppelklick oder einfachen Klick steuern etc. Aber ich hoffe, sie hat die grundlegende Funktionsweise eigener Klassen verständlich gemacht.

So, das soll's erst mal gewesen sein - wer mehr über Reaction wissen will, findet in den schon erwähnten Dokumenten und im Kurs von Michael Christoph noch viel mehr Informationen.

Ansonsten stehe ich natürlich weiterhin mit meinem beschränkten Wissen zur Verfügung ;)

euer

Martin R. Elsner