AMIGA-Magazin · Ausgabe 1/00 · Programmierung: AmigaOS 3.5 (Teil 2)

Aktuelles Heft 1/00

Inspektor Gadget

Im letzten Kursteil wurde auf die neuen Reaction-Klassen eingegangen und wie diese mittels ReActor zu einer Benutzeroberfläche zusammengestellt werden können. In dieser Folge werden die notwendigen Sourcecode-Teile ergänzt, um das Programm zum Leben zu erwecken.

von Michael Christoph

Mit diesem Kursteil dringen wir diesmal in die Praxis ein und werden ein Beispiel programmieren. An Hand des kleinen Programms lernen Sie, wie Sie eine Oberfläche erstellen und die Funktionen der Oberfläche ansprechen.

Das Grundgerüst

Als Vorbereitung wird die in »Vorbild« abgebildete Oberfläche erstellt. Es handelt sich um vier große horizontale Gruppen, die teilweise noch vertikale Gruppen enthalten. Die fertige Beschreibungsdatei finden Sie aber auch auf der CD zum Heft, bzw. auf der Homepage des AMIGA-Magazins.

struct Library *ResourceBase;
if(!(ResourceBase =
  OpenLibrary("resource.library",44))) 
{
  Write(Output(),"Program required"
  " AmigaOS 3.5!\n",31); exit(10);
}
/* ... Programmcode ... */
CloseLibrary(ResourceBase);

Listing 1: So prüfen Sie, ob das System OS-3.5-tauglich ist

Zuerst muß im Sourcecode sichergestellt werden, daß tatsächlich AmigaOS 3.5 zum Einsatz kommt. Erst ab dieser Version sind die entsprechenden Libraries und notwendigen Klassen enthalten. Es wird empfohlen, die neu hinzugekommene resource. library zur Feststellung zu verwenden. Die Version 44 entspricht dabei AmigaOS 3.5. Auch die workbench.library, icon.library, asl.library und version.library müssen in der Version 44 vorliegen. Da in den meisten Programmen jedoch die resource.library sowieso zum Einsatz kommt, bietet sie sich zur Prüfung an ­ in Listing 1 sehen Sie ein Beispiel.

Wurde die Library erfolgreich geöffnet, können evtl. vorhandene Parameter per ReadArgs() geparst werden. Danach wird die Umgebung für die neuen Reaction-Klassen bereitgestellt. Diese Aufgabe übernimmt die Funktion RL_OpenResource(), welche als Parameter den Bildschirm und einen Catalog erwartet. Während der Catalogzeiger NULL sein darf (dann werden nur die internen Texte verwendet), ist der Bildschirmzeiger zwingend erforderlich und kann z.B. mit LockPubScreen(NULL) (entspricht normalerweise der Workbench) ermittelt werden.

Als weiterer Parameter wird RCTResource benötigt. Diese Variable wird von ReActor automatisch mit in das erstellte Objektfile geschrieben und muß durch den Programmierer nur »durchgereicht« werden. Diese Variable stellt quasi den Aufhänger für die komplette Oberflächenbeschreibung dar. Zur Nachrichtenkommunikation werden noch zwei Messageports benötigt, die sich mit CreateMsgPort() erzeugen lassen. Dieser Punkt soll später optional werden. Die Ports werden beim Erzeugen des Fenster per RL_NewObject() mitgegeben. Als zusätzlichen Parameter erwartet die Funktion die ID des zu erzeugenden Objekts (z.B. des Fensters). Dabei werden sämtliche Umgebungsdaten des angegebenen Objekts bereitgestellt, also auch alle Gadgets erzeugt. Möchten Sie im späteren Programmlauf auf diese Gadgets zugreifen, können Sie per RL_GetObjectArray() ein Array mit den Gadgetzeigern abfragen.

Erste Einblicke

Bis jetzt wurde das Fenster nur erzeugt, allerdings noch nicht geöffnet. Das Fensterobjekt kennt unterschiedliche Kommandos, die ihm mit DoMethod() mitgeteilt werden ­ z.B. DoMethod(gb _WindowObj,WM_OPEN) um das Fenster zu öffnen, oder WM_CLOSE um das Fenster zu schließen. Auf zwei weitere Kommandos (WM_ICONIFY und WM_ HANDLEINPUT) wird bei der Beschreibung des Messageloops eingegangen.

Vorbild: So soll die Oberfläche unseres Beispielprogramms aussehen.
Achtung: gb_WindowObj ist hierbei kein Zeiger auf die altbekannte Intuition-Windows-Struktur, sondern ein Object. Er wird von RL_NewObject() zurückgeliefert. Wird ein direkter Zugriff auf die Windows-Struktur benötigt, kann diese per GetAttr(WINDOW_Window,gb_WindowObj,(ULONG *)&gb_Window); erfragt werden. Nach dem Aufruf enthält gb_Window den Zeiger auf die gewünschte Struktur oder NULL, wenn das Fenster noch nicht geöffnet wurde. Zeigerwerte der Windows-Struktur sollten Sie nicht mehr zwischenspeichern (z.B. RastPort oder BitMap), da diese beim Iconifizieren des Fenster nicht mehr stimmen würden. Verwenden Sie statt dessen immer nur den Window-Zeiger, der bei Iconify bzw. Schließen des Fenster natürlich auf NULL zu setzen ist.

Das Öffnen des Fensters erfolgt (wie bereits beschrieben) mit DoMethod(gb_WindowObj,WM_OPEN). Alternativ stehen auch einige Defines zur Verfügung, für die Fensterkommunikation sind dies:
struct Window * RA_OpenWindow(Object *win)
void RA_CloseWindow(Object *win)
ULONG RA_HandleInput(Object *win, UWORD *code)
BOOL RA_Iconify(Object *win)
struct Window * RA_Uniconify(Object *win)
Nach dem Öffnen des Fensters erfolgt in einer Schleife die Verarbeitung der Nachrichten (darauf wird später genauer eingegangen). Am Programmende sind die reservierten Resourcen in umgekehrter Reihenfolge freizugeben. RL_CloseResource() gibt dabei alle im Programmlauf angelegten und noch nicht freigegebenen Objekte (Fensterdaten und Gadgetdaten) frei. Zur Verdeutlichung der Beschreibung sehen Sie in Listing 2 eine kurze Übersicht der Funktionen in ihrer praktischen Anwendung (ohne Fehlerrückmeldungen):

Der Message-Loop

struct Library       *ResourceBase;
struct Screen        *gb_Screen;
struct Window        *gb_Window;
struct Gadget       **gb_Gadgets;
struct Catalog       *gb_Catalog;
RESOURCEFILE          gb_Resource;
Object                *gb_WindowObj;

if((ResourceBase =
OpenLibrary("resource.library",44)))
{
  /* benötigte Libraries öffnen ... */
  gb_Catalog = OpenCatalogA(NULL,PROGNAME
                        ".catalog",NULL);

  if((gb_Screen = LockPubScreen(NULL))) 
  { if((gb_Resource = RL_OpenResource(
      RCTResource,gb_Screen,gb_Catalog)))
    { if((gb_WindowObj = RL_NewObject(
        gb_Resource,WIN_ID_Main, /* z.Z. sind
        noch die beiden MsgPort-Zeiger notwendig */
        TAG_DONE))) 
      { gb_Gadgets = (struct Gadget **)
        RL_GetObjectArray(gb_Resource,gb_WindowObj,
                          GROUP_ID_Main);
        gb_Window = RA_OpenWindow(gb_WindowObj);
        /* messageloop ... */
        RA_CloseWindow(gb_WindowObj);
      }
      RL_CloseResource(gb_Resource);
    }
    UnlockPubScreen(NULL,gb_Screen);
  }
  if(gb_Catalog) CloseCatalog(gb_Catalog);
  /* geöffnete Libraries schließen ... */
  CloseLibrary(ResourceBase);
}

Listing 2: Die Funktionen zum Grundaufbau unseres Beispiels

Im Messageloop ist in altbekannter Weise auf das Eintreffen von Nachrichten zu warten (per Wait()). Dabei kommen jedoch keine DCMP-Messages mehr zum Einsatz. Da viele Aufgaben bereits von der Window-Klasse »transparent« für die Anwendung erledigt werden, benötigt diese natürlich zuerst die Nachrichten zur Vorauswertung. Dabei darf das Programm einzelne Nachrichten komplett »verschlucken« (z.B. Gadget-Shortcuts). Zuerst ist das Signalbit des Fensters (bzw. auch mehrerer Fenster) zu ermitteln:

ULONG windowsignal, signals;
GetAttr(WINDOW_SigMask, gb_WindowObj, &windowsignal);

Dieses wird mit den anderen Signalbits mit einem ODER verbunden, auf dessen Ereignisse gewartet werden soll. In unserem Programm haben wir lediglich ein Fenster, erlauben aber noch CTRL-C zum Abbrechen des Programms von der Shell aus:

const ULONG signals = Wait(windowsignal | SIGBREAKF_CTRL_C);

In einer Endlosschleife werden so lange die Nachrichten verarbeitet, bis der Benutzer das Fenster schließt oder das Programm mit CTRL-C abbricht. Die einzelnen Nachrichten werden mit dem Kommando WM_HAN-DLEINPUT angefordert. Dieses liefert WMHI_LASTMSG, wenn keine Nachrichten mehr vorliegen, sonst einen ULONG-Wert. Dieser setzt sich aus dem oberen Word für die Klasse und dem unteren Word für die Event-Daten zusammen. Daher muß der Programmierer vor der Weiterverarbeitung den Wert bitweise mit WMHI_CLASSMASK maskieren. Für die meisten IDCMP-Ereignisse existieren entsprechende WMHI-Equivalente. Das Gerüst des Message-Loops sehen Sie in Listing 3. Die einzelnen Message-Ereignisse sollten bekannt sein. Alle vorhandenen Defines mit kurzer Beschreibung sind in der Include-Datei classes/window.h zu finden.

Bei den Tastenereignissen ist der betreffende ASCII- oder Rawkey-Code durch (result & WMHI_KEYMASK) zu ermitteln. Der Qualifier läst sich nicht direkt auslesen. Wird dieser benötigt, so ist der umständliche Weg über eine Hook-Funktion notwendig (über WINDOW_IDCMPHook im Fenster zu installieren). Bei RawKeys läßt sich allerdings der zugehörige In-putEvent abfragen (GetAttr(WINDOW_InputEvent, gb_WindowObj, &ie)), um so doch direkt auf den Qualifiercode zugreifen zu können.

Kommunikation mit den Gadgets

Interconnection Die einzelnen Gadgets können untereinander kommunizieren und Nachrichten austauschen.
Bei den Gadget-Ereignissen (WMHI_GADGETUP) enthält die Nachricht neben der Gadget-ID auch die Gruppen-ID, in der sich das Gadget befindet. Da die Gadget-ID bei mehreren Gruppen jeweils wieder bei 0 beginnt, muß man hierbei entsprechend auch die Gruppe berücksichtigten. Eine Gruppe ist dabei z.B. eine Karteikartenbeschreibung oder ein anderes Fenster, nicht jedoch eine horizontale/vertikale Gestaltungsgruppe oder eine Karteikartenseite! Die Nummern werden von ReActor automatisch in der Reihenfolge der Erstellung vergeben und können leider nicht beeinflußt werden. Die Verarbeitung der Gadget-Nachrichten könnte so wie in Listing 4 aussehen: Wird nur eine Gadgetgruppe verwendet, so kann natürlich auf die Auswertung der Gruppen-ID verzichtet werden. Daten der Gadgets können per GetAttr() erfragt werden:

ULONG result; UWORD code; 
while((result = RA_HandleInput(
gb_WindowObj,&code)) != WMHI_LASTMSG)  
{ switch(result & WMHI_CLASSMASK)
{ case WMHI_CLOSEWINDOW: break;
  case WMHI_GADGETUP:    break;
  case WMHI_ICONIFY:     break;
  case WMHI_UNICONIFY:   break;
  case WMHI_MENUPICK:    break;
  case WMHI_RAWKEY:      break;
  case WMHI_VANILLAKEY:  break; 
} }

Listing 3: Beispiel für ein Messageloop-Gerüst

GetAttr(,gb_Gadgets[],(ULONG *) &attr);

Umgekehrt lassen sich mit SetGadgetAttrs() neue Werte an das Gadget übermitteln. Dabei können auch mehrere Tag-Paare gleichzeitig übergeben werden, welche mit TAG_DONE abzuschießen sind.

SetGadgetAttrs(gb_Gadgets[],gb_Window,NULL, , , TAG_DONE);

Der Parameter gb_Window ist dabei das Intuition-Fenster in dem sich das Gadget befindet und darf auch NULL sein. Daten für Gadgets darf man auch setzen, wenn das Fenster noch nicht oder gerade nicht geöffnet ist. gb_Gadgets ist das Array mit allen Gadget-Adressen, wobei über die GadgetID auf den gewünschten Eintrag zugegriffen wird. Die einzelnen GadgetIDs werden von ReActor automatisch in die *.h-Datei geschrieben und sind im Eingabefeld »Object Name« in der ReActor-Umgebung frei einzugeben. Zu beachten ist, daß allein durch das Setzen der neuen Werte die Anzeige nicht automatisch erneuert (refresht) wird. Diese Aktion muß ebenfalls vom Programm angefordert werden:

const UWORD groupid = RL_GROUPID(result); 
const UWORD gadid   = RL_GADGETID(result);
if(groupid == GROUP_ID_Main) 
{ switch(gadid) 
  { case GAD_ID_xxx: break; }
}

Listing 4: Die Auswertung der Gadget-Nachrichten

Ein Refresh ist normalerweise nur notwendig, wenn SetGadgetAttrs() != 0 liefert. Das Root-Gadget (Basisklasse) liefert allerdings immer 0, weshalb der Return-Wert allein nicht sehr zuverlässig ist. Um hier überflüssiges Neuzeichnen zu vermeiden, sollten die einzelnen Gadgets (und Tagwerte) ausprobiert werden. Nur bei Bedarf sollte das Programm einen Refresh für das einzelne Gadget (per RefreshGList) auslösen. Alternativ könnte direkt DoGadgetMethod(..., OM_UPDATE,...) zum Einsatz kommen ­ dadurch wird das Gadget automatisch refresht.

Kursübersicht
Grafische Benutzeroberflächen unter AmigaOS 3.5 entwerfen
Teil 1: Vorstellung der neuen ReAction-Gadgetklassen, Vorstellung von ReActor zum Erstellen der selbstlayoutenden Oberflächen
Teil 2: Der Sourcecode zur Oberfläche wird erstellt und dabei die resource. library vorgestellt. Abfrage und Setzen der Gadgetwerte.
Teil 3: Eigene Reaction-Gadgets/Images erstellen und deren Einbindung in das ReActor-Programm.
Einen Sonderfall hierbei bilden die Karteikarten: Elemente, die sich innerhalb einer Karteikartenseite befinden, die gerade nicht dargestellt ist, würden durch RefreshGList() bzw. SetGadgetAttrs() gezeichnet! Dem einzelnen Gadget ist nicht bekannt, daß es innerhalb einer Pagegruppe liegt und ob es gerade dargestellt wird. Daher existieren zwei analoge Funktionen im layout.gadget: SetPageGadgetAttrs() und RefreshPageGadget(). Diese benötigen als zusätzlichen Parameter das Pagegadget. Auch wenn in den Prototypen nach einem Object* verlangt wird, ist die Gadgetadresse anzugeben (gb_GadgetsPage [GAD_ID_Page]). Die normalen Gadget-Objekte basieren immer noch auf der Gadget-Struktur. Die Einträge sollte die Software aber nicht interpretieren, da die Daten gadgetspezifisch sind und sich in zukünftigen Klassen ändern können.

Interconnection

Einfacher wäre es jedoch, wenn die Gadgets ihre Daten untereinander austauschen würden, ohne daß dazu das Hauptprogramm eingreifen müßte. Auch diese Möglichkeit bieten die BOOPSI-Gadgets an! Per ICA_Target wird das zu informierende Gadget festgelegt und per ICA_Map die Übersetzungskonventionen. So kann beispielsweise beim Bewegen des Scrollers automatisch der Zahlenwert im Integer-Gadget aktualisiert werden. Und auch umgekehrt kann sich bei Eingaben eines neuen Zahlenwertes der Scroller verändern ­ automatisch ohne eine Zeile Programmcode.

Diese Kommunikation funktioniert immer nur für zwei Objekte untereinander. Zur Kommunikation von mehreren Objekten müssen Sie als Verteiler ein Modelclass-Objekt erzeugen. Dieses erhält von jedem Sender den Wert und kann diesen an beliebig viele andere Objekte weitermelden. Dazu ist je Empfänger ein ICClass-Objekt zu erzeugen, das den Wert (aus Modelclass) in den Zielwert (Zieltag) konvertiert. Auch das Programm kann hierbei als Empfänger dienen. Als Empfänger dient dann die ICA_Target-Kennung ICTARGET_IDCMP. Das Programm würde dann eine Message vom Classtyp IDCMP_IDCMPUPDATE erhalten ­ in der ReAction-Umgebung ist ein IDCMPHook notwendig, um diese Nachrichten »abhören« zu können. Zur Zeit ist lediglich eine Tag-Konvertierung möglich. Der Datenwert bleibt aber identisch. Der interne Scrollerwert (0 bis 3) kann somit nicht direkt an das Integer-Gadget weitergegeben werden, da dieses mit den Werten 1 bis 4 arbeitet. Diese Konvertierung muß daher das Programm übernehmen.

Einbindung von Menüs

ReActor nimmt uns zwar die Erstellung der Oberfläche (auch mehrerer Fenster) ab, die Menüs hingegen sind weiterhin auf »altmodische« Weise zu erstellen. Allerdings ist die mit OS 2.x eingeführte Möglichkeit der Menüerzeugung über die gadtools.library so sehr einfach. Daher wird hier nicht weiter darauf eingegangen. Lesen Sie hierzu die entsprechenden Kommentare im Sourcecode. Zu erwähnen ist lediglich, daß die Menüs von Hand lokalisiert werden müssen (s. Kapitel »Localisieren«). Das Einbinden der Menüliste nimmt uns allerdings ReActor wieder ab: WINDOW_MenuStrip, gb_Menues in der Fenstertagliste reicht aus.

Localisieren

Bereits seit AmigaOS 2.1 (Library V38) ist die locale. library Bestandteil des Betriebssystems. Über diese können die landesspezifischen Texte für das Programm abgelegt werden. Auch ReActor benutzt diese Library zur automatischen Lokalisierung der Oberfläche. Alle in der Oberfläche verwendeten Texte werden automatisch (von ReActor) in eine CD-Datei gespeichert. Das Programm wird aber noch wesentlich mehr Texte enthalten, die lokalisiert werden müssen. Diese werden am besten in eine eigene Datei gespeichert! Ändern Sie später etwas an der Oberfläche mit Hilfe von ReActor, würde die Catalog-Datei automatisch mit gespeichert und alle Erweiterungen von Ihnen wären verloren. Die eigenen internen Programmtexte lassen sich einfach mit Hilfe der GetStr(TX_SureToQuit) ermittelt wobei GetStr() wie folgt als Define zu erstellen ist:

#define GetStr(id) GetCatalogStr(gb_Catalog,id,id ## _STR)

Dadurch ersparen Sie sich immer die ausführliche Schreibweise dieses Funktionsaufrufes. Wie folgend noch beschrieben wird, sind die von ReActor erstellten Catalogtexte speziell zu übersetzen. Die eigenen Texte werden normal compiliert und zum Programm hinzugelinkt. Die Aufrufe zum Erzeugen der Cataloge sind in den jeweiligen Catalogdateien angegeben.

Das Programm übersetzen

Der Übersetzungsvorgang ist mit den gängigen Compilern identisch, dabei bieten StormC und MaxonC (ab V4) eine komfortable Oberfläche an. Andere Compiler starten Sie meist per Shellaufruf. Generell ist der Sourcecode ganz normal zu übersetzen (er enthält alle bisher beschriebenen Teile). Hinzugelinkt werden müssen das von ReActor erzeugte Objektfile mit der Oberflächenbeschreibung und die Catalogtexte. Diese sind allerdings zuerst noch zu übersetzen. Dazu wird mit der neuen CatComp-Version die .cd-Datei in eine _cd.asm-Assembler-Datei übersetzt, welche noch assembliert werden muß. Erst die dabei entstandene Objekt-Datei läßt sich zum Programm hinzulinken und enthält dann alle internen Texte für die Oberflächenelemente. Ein Programm besteht somit immer aus (mindestens) den drei folgenden Objektfiles:

Die genauen Übersetzungsanweisungen für verschiedene Compiler finden Sie im Kopf des Sourcecodes. Der Einfachheit halber wurden die internen Programmtexte nicht in eine eigene Datei ausgelagert, sondern mit dem Hauptprogramm übersetzt. Die Textstrings (und String-IDs) sind danach noch von Hand in eine gemeinsame cd-Datei einzutragen, welche mit dem Programm weitergegeben werden kann.

Dieser Teil konnte nur einen groben Überblick über die neuen Möglichkeiten der Oberflächengestalltung geben. Vor allem die notwendigen Änderungen im Sourcecode sollten aber damit deutlich sein. Der ausführlich kommentierte Sourcecode zeigt die komplette Programmumgebung und die Nachrichtenbehandlung, dabei wird auch auf die Iconifyfunktion eingegangen und ARexx-Kommandos unterstützt. Das Programm verwendet viele unterschiedliche Gadget-Arten und kann somit auch zum »Nachschlagen« dienen. Sollten dennoch Fragen auftreten, können Sie sich gerne an den Autoren wenden:
     michael@meicky-soft.de.
Alle Programmteile (Sourceodes, Oberflächenbeschreibung) können unter
     http://www.meicky-soft.de/amiga-magazin/reaction.html
geladen werden bzw. befinden sich auch auf der CD zur nächsten Ausgabe.
Das manuelle Eingreifen in das automatische Layout von Reaction ist sehr schwierig bis unmöglich. Daher sollten eigene Anzeigeflächen etc. als neues Gadget implementiert werden. Dadurch passen sie sich wieder automatisch in den Layoutprozess ein und programmseitig haben Sie keine »Arbeit« mehr damit (Größe/Lage/Refresh). Wie Sie eigene, reactiontaugliche Gadgets erstellen beschreibt der Kursteil in der nächsten Ausgabe.

Beschreibung zum Programm
Die meisten Werte werden per Interconnection automatisch unter den Gadgets ausgetauscht (s. »Interconnection«). Da mehrere Empfänger existieren, wird die ModelClass als Zielobjekt eingebunden. Diese enthält je Empfänger ein ICClass-Objekt, das den Wert in die Zielbasis konvertiert. Nicht möglich ist diese Vorgehensweise beim String/Integer-Gadget. Das Programm hört daher die Gadgetveränderungen ab und paßt den Ausgabestring an. Umgekehrt wird die Eingabe auf Gültigkeit geprüft und bei Zutreffen allen Gadgets der neue (Zahlen-) Wert mitgeteilt. Ungültige Eingaben werden per DisplayBeep() gemeldet und der Cursor bleibt im Stringgadget. Das Integer-Gadget hat einen Eingabebereich zugeteilt, so daß nur gültige Werte zwischen 1 und 4 eingegeben werden können. Außerdem muß es ebenfalls manuell über die Werteänderungen informiert werden, da die Ausgabe nicht mit den internen Werten von 0 bis 3 übereinstimmt. Mit der Checkbox wird das Stringgadget zwischen Enable/Disable gewechselt, was wieder programmseitig realisiert wurde (Interconnection GA_Checked - GA_Disabled wäre bei umgekehrter Funktionsweise möglich). Über die vier Buttons der Toolbar, bzw. den vier Buttons daneben, kann direkt eine der Jahreszeiten aktiviert werden. Der neue Wert wird lediglich vom Programm an alle Gadgets weitergemeldet.

lb


 Hauptseite © 1999 All Rights Reserved. Alle Rechte vorbehalten Franzis' Verlag GmbH
Veröffentlichung und Vervielfältigung nur mit schriftlicher Genehmigung des Verlags

Kommentare, Fragen, Korrekturen und Kritik bitte an Webmaster AMIGA schicken.
Zuletzt aktualisiert am 29. Januar 2000, Michael Christoph.