|
|
|
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:
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 |
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>