Entwickeln mit ACS, Teil 2

Auf dem Weg vom Anfänger zum ACS-Profi haben wir letzten Monat gesehen, wie allereinfachste GEM-Programme zum Laufen gebracht werden. Doch was hinter den Kulissen geschieht, haben wir nicht sehen können. Dieses Mal wollen wir die Arbeitsweise und Ideologie von ACS beleuchten und etwas mehr Struktur in unsere Untersuchung bringen. Zum Schluß soll noch ein Programmbeispiel den Artikel auflockern.

Die objektorientierte Programmierung (OOP) hat die in den strukturierten Programmiersprachen verpönte Vereinigung von Daten und Programmcode wieder in den Programmieralltag gerückt. Es ist nun einmal eine Tatsache, daß Datenstrukturen spezielle, auf sich angepaßte Funktionen benötigen, die alleingestellt keinen Nutzen verrichten. Warum also eine Resource-Datei vom eigentlichen Code galvanisch trennen?

ACS überwindet die Schwächen eines RCS, indem es sich der Objektorientierung bedient. Die Verbindung zwischen Daten und den dazugehörigen Funktionen erreicht es durch Referenzen aus dem ACS-Editor. Die Objektorientierung wird weiterverfolgt, indem das Prinzip der Vererbung angewandt wird. Jede ACS-Applikation ist ein Kind der ‘Default-Applikation’ und erbt daher ihre Eigenschaften, die dann überladen, erweitert oder gelöscht werden können [1]. Die ‘Default-Applikation’, das ist das bekannte, unverständlicherweise von so wenigen gemochte ACS-Desktop mit seiner Menüleiste. Diese Applikation ist ohne jegliche Funktionalität, aber auch ohne Benutzercode lauffähig (probieren Sie es aus, die einzige Funktion, die absolut immer definiert werden muß, ist ACSinit).

Die Datei mit der Endung ‘*.AH’, die vom ACS-Editor erzeugt wird, enthält alle Daten, die das Aussehen der Oberfläche definieren. Das Prinzip ist folgendes: Da die Daten nicht statisch sind, sich also während der Ausführung ändern können, werden von allen dort definierten Strukturen Kopien angelegt, sobald diese benötigt werden. Die Kopien können nach Belieben verändert werden, das Original bleibt gleich und kann weiter kopiert werden.

Programmablauf

Der gesamte Programmablauf wird von den ACS-Funktionen gesteuert. Der Programmierer hat nur an bestimmten, fest definierten Stellen die Möglichkeit, den Programmfluß zu steuern. Kurz nach dem Programmstart ist es meistens nötig, irgendwelche Daten zu initialisieren. Der Programmierer kann alle Initialisierungen in der Funktion ACSinit erledigen. ACS ist an dieser Stelle schon betriebsbereit, d.h. daß auch das Default-Desktop installiert ist. Da das NEU-Icon (oder der Menüpunkt Öffnen) dazu benötigt wird, zum Beispiel eine neue Datei anzulegen, muß dem Desktop von hier aus mitgeteilt werden, was denn getan werden muß (siehe letzte Folge). Falls der Programmierer will, kann er auch beim Initialisieren Fenster öffnen.

Nachdem das Programm seine Initialisierungsphase abgeschlossen hat, springt ACS in die Ereignisschleife. Alle Ereignisse, die GEM meldet werden erst intern ausgewertet und dann, falls nötig, an das Programm weitergegeben. Diese haben dann die Form von Nachrichten, die an die Service-Routine des betreffenden Fensters geschickt werden, oder aber sie führen direkt Click- und Drag-Funktionen durch, falls ein Objekt angeklickt oder gezogen wurde und dessen Einträge im ACS-Editor mit den entsprechenden Funktionen ausgefüllt wurden. Falls ACS ein Ereignis selbst verwertet, heißt das, daß zum Beispiel ein Fenster getoppt oder ein Slider gezogen wurde und der Programmierer diese Aktionen nicht besonders behandeln möchte. Die Fensterroutinen, die im ACS-Editor nicht angegeben wurden, werden von ACS also automatisch verwaltet.

Auch vor dem Terminieren des Programms müssen oft Aktionen durchgeführt werden. Der Default-Desktop stellt zum Verlassen des Programms den Menüpunkt Beenden zur Verfügung. Dieser ruft die Funktion ACSclose auf, die das Programm beendet. Indem wir diese Routine selbst neu definieren, können wir zum Beispiel erst eine Abfrage durchführen, ob das denn wirklich erwünscht ist. Der Linker ersetzt dann automatisch die Default-Routine durch unsere. Wenn das Programm sich wirklich schon in der Terminierung befindet und Fenster gelöscht werden müssen, bekommen diese eine Nachricht (AS_TERM), auf die entsprechend reagiert werden kann. Allerdings kann, falls der Benutzer das Programm wirklich verlassen will (die Variable appexit steht auf TRUE), hier die Terminierung nicht mehr angehalten werden. Kurz vor dem tatsächlichen Programmende wird noch die Routine ACSterm aufgerufen, die noch eine abschließende Deinitialisierung, zum Beispiel eine Restaurierung der Farben, erledigt.

Wie Sie sehen, haben haben wir vielfältige Möglichkeiten, den Programmfluß umzulenken. Die Initialisierung und Terminierung sind zwei Dinge, die eigentlich immer erledigt werden müssen. Erstere haben wir bereits in der letzten Folge kennengelernt, letztere wird weiter unten noch erklärt, wenn es um Nachrichten geht.

typedef struct {

    /* Fensterroutinen */

    int     open(Awindow *window);
    Awindow *create(void *parameter);
    int     service(Awindow *window,
    int     task, void *in_out); 
    int     init(Awindow *window);
    void    keys(Awindow *window, int kstate, int key); 
    void    obchange(Awindow *window, int obnr, int new_state); 
    void    redraw(Awindow *window, Axywh *area); 
    void    topped(Awindow *window); 
    void    closed(Awindow *window); 
    void    fulled(Awindow *window); 
    void    arrowed(Awindow *window, int which); 
    void    hslid(Awindow *window, int pos);
    void    vslid(Awindow *window, int pos);
    void    sized(Awindow *window, Axywh *area); 
    void    moved(Awindow *window, Axywh *area);

    /* Fensterobjekte */

    OBJECT  *menu;
    OBJECT  *work;
    ICONBLK *iconblk;
    void    *user;

    /* Fenstertexte */

    char    *name; 
    char    *info;

    void    *reserved; 
    int     ob_edit, ob_col;
    int     wi_id;
    int     wi_kind;
    Axywh   wi_act;
    Axywh   wi_norma1;
    Axywh   wi_work;
    Axywh   wi_slider;
    int     wi_nx, wi_ny;
    int     snap_mask;
    int     type;
    int     kind;
    int     state;
    int     icon;
} Awindow;
Abb. 1

Die Fensterstruktur

Wir haben schon öfter von ihr gehört; doch was versteckt sich dahinter? In der Abbildung 1 sehen Sie eine an C++ angelehnte Darstellungsweise dieser Struktur. Funktionsdeklarationen darin sind nichts anderes als Zeiger auf die Funktionen selbst. In der Datei ACS.H, die sich auch auf der Demodiskette befindet, können Sie die syntaktisch korrekte Version betrachten. Wie Sie sehen können, enthält Awindow alle Attribute, die man im Fenstereditor angeben kann, und noch ein paar weitere, die das Fenster während des Programmablaufs beschreiben (zum Beispiel Position oder Slider-Stellung).

Wir werden uns nun anschauen, was für Aufgaben die Fensterfunktionen erledigen müssen und was die entsprechenden Default-Routinen tun. Diese haben selbstverständlich identische Ein- und Ausgabeparameter. Create ist die Routine, die ein Fenster erzeugt. Als Rückgabeparameter muß ein Zeiger auf die Fensterstruktur zurückgegeben werden oder NULL, falls ein Fehler geschah. Hier können Sie Ihre Daten, die zum Fensterinhalt gehören, initialisieren, Speicher allozieren oder gar gleich das entsprechende Fenster öffnen. Die Default-Routine heißt Awi_create und legt eine Kopie der ihr übergebenen Fensterstruktur an. Auf dem Desktop erscheint das Fenster-Icon, falls das entsprechende Flag aktiviert ist. Sie sollten (müssen) immer eine eigene Create-Routine definieren, obwohl es manchmal auch ohne geht.

typedef struct {
    void click(void); 
    void drag(void); 
    int     ob_flags;
    int     key;
    void *userp1; 
    void *userp2; 
    int     mo_index;
    int     type;
} AOBJECT;
Abb. 2
int     vdi_handle;
int     appexit;
Awindow *ev_window;
OBJECT  *ev_object;
int     ev_obnr;
int     ev_mmox, ev_mmoy;
int     ev_mmokstate;
Abb. 3

Awi_open ist die Default-Routine zu dem Eintrag Open und öffnet ein Fenster. Diese wird meistens gleich angesprungen. Sie könnten aber vor dem Öffnen auch noch einen Dateinamen erfragen. Als Parameter bekommt die Routine einen Zeiger auf die Fensterstruktur, deren Fenster geöffnet werden soll. Zurückgegeben werden OK oder FAIL, je nachdem, ob das Öffnen erfolgreich war oder nicht.

Die Service-Routine ist eine der wichtigsten überhaupt. Sie wird immer dann aufgerufen, wenn das Fenster eine Nachricht bekommt. Neben dem obligatorischen Zeiger auf die Struktur des betreffenden Fensters bekommt die Routine eine Nummer, die zur Identifizierung der Nachricht dient, und einen weiteren Zeiger, der unterschiedliche Bedeutungen haben kann. Einige Nachrichten sind bereits von ACS aus definiert, andere können selbst erstellt werden. Eine Nachricht muß jedes Programm aus werten: AS_TERM zeigt an, daß das Fenster nicht nur geschlossen, sondern gelöscht werden soll. Dies wird von der Default-Routine automatisch erledigt. Zur Auswertung der anderen Nachrichten muß diese aber durch eine eigene ersetzt werden. AS_UNTOPPED meldet dem Fenster, daß es nicht mehr das oberste ist, AS_MOUSE berichtet dem Programmierer, daß die Maus sich über seinem Fenster bewegt hat, und AS_INFO soll dem Fenster einige Informationen über sich selbst entlocken. Weitere Nachrichten sind vor allem für die Verwaltung von Auswahllisten vorhanden (mehr dazu in der nächsten Folge). Falls die Nachricht erledigt werden konnte, liefert die Funktion OK, FAIL sonst.

Eine weitere Funktion hat eine wichtige Bedeutung: Init wird immer dann aufgerufen, wenn das Flag AWS_MODIFIED im Status (Variable state in der Fensterstruktur) eines Fensters gesetzt ist. Jetzt muß der Fensterinhalt neu gezeichnet und unter Umständen auch die Slider neu gesetzt werden. Eine weitere Möglichkeit bietet Init im Zusammenhang mit dem Flag AWS_LATEUPDATE. An einem Beispiel soll die Bedeutung dieses Flags erläutert werden: Sollen viele Objekte innerhalb eines Fensters im Status verändert (beispielsweise selektiert) werden, kann jedes Objekt neu gezeichnet werden, oder aber man setzt das AWS_LATEUPDATE-Flag und zeichnet nichts.

Nach dem Rücksprung zur Ereignisschleife wird dieses Flag ausgewertet, und falls es gesetzt ist, die Init-Routine aufgerufen, die dann den Fensterinhalt nur einmal zeichnet. Der Parameter ist wieder ein Zeiger auf das Fenster und das Ergebnis sollte OK sein.

Von den übrigen Fensterfunktionen werden die meisten niemals benötigt. Sie sollten am besten unverändert bleiben, denn solange das Innere eines Fensters ein Work-Objekt ist, erledigen sie alles automatisch. Wer jetzt denkt, man könnte mit einem GEM-Objekt nicht Applikationen wie zum Beispiel Textverarbeitungen oder Grafikprogramme realisieren, wird weiter unten eines Besseren belehrt, wo wir sehen werden, daß sich mit USERDEF-Objekten solche Dinge um ein Vielfaches besser erledigen lassen als bisher üblich.

Fensterroutinen

Awindow *Awi_create(const Awindow *window);
int     Awi_open(Awindow *window);
void    Awi_closed(Awindow *window); 
void    Awi_delete(Awindow *window); 
void    Awi_topped(Awindow *window); 
void    Awi_redraw(Awindow *window, Axywh *limit);

Mausroutinen

void    Amo_busy(void); 
void    Amo_unbusy(void); 
void    Amo_hide(void); 
void    Amo_show(void);

Speicherverwaltung

void    *Ax_malloc(long size);
void    Ax_free(void *memory);

Modale Dialoge

int     A_dialog(OBJECT *dia);
int     alert_str(const char *alert, const char *para);
Abb. 4
typedef struct {
    OBJECT  *pb_tree;
    int     pb_obj;
    int     pb_prevstate;
    int     pb_currstate;
    int     pb_x, pb_y, pb_w, pb_h;
    int     pb_xc, pb_yc, pb_wc, pb_hc;
    long    pb_parm;
} PARMBLK;
Abb. 5

Neben den Funktionen sollten auch ein paar wichtige Fenstervariablen nicht unerklärt bleiben. Unter Fensterobjekte (Abbildung 1) sehen Sie Zeiger auf die verschiedenen Objekte, die Sie im ACS-Editor definiert haben. Da ein Fenster meistens aber nicht nur durch seinen visuellen Inhalt bestehen kann, also noch weitere Daten wie zum Beispiel die Adresse eines Bildes benötigt, können all diese in einer eigenen Struktur gespeichert und dann dessen Adresse unter user eingetragen werden, damit jedes Fenster weiß, wo seine Daten zu finden sind.

name und info sind Zeiger auf den Namen und die Infozeile. Diese können natürlich auch vom Programm aus verändert werden, doch es ist wichtig, die Kopie des alten Strings zu löschen (mit Ast_delete), und der neue Name muß mit Ast_create erzeugt (kopiert) worden sein, denn nach dem Löschen eines Fensters wird ACS versuchen diesen freizugeben.

Weiterhin wichtig sind wi_work, wi_act und wi_slider, denn sie enthalten die Koordinaten des Fensterinhaltes, Fensterrahmens und der Fenster-Slider in einer Struktur Axywh. Diese bietet Platz für die x- und y-Koordinate sowie die Breite und Höhe.

Die Objektstruktur

ACS verwendet dieselbe Objektstruktur wie GEM selbst. Damit ist eine vollständige Kompatibilität gewährleistet, und es können objektmanipulierende Funktionen wie objc_find oder auch form_do direkt auf ACS-Objekte ausgeübt werden. Die Daten für die zusätzliche Funktionalität müssen aber irgendwo gespeichert werden. Hat ein Objekt einige dieser neuen Eigenschaften, so hat es einen Nachfolger (in der Objektstruktur, nicht aber im Objektbaum). Der ist vom Typ AOBJECT (Abbildung 2), einer Struktur, die in zwei Punkten OBJECT gleicht: sie ist von gleicher Größe, und der Eintrag ob_flags befindet sich an derselben Stelle, damit beide Objektarten unterschieden werden können. Wenn Sie also diese erweiterten Werte eines Objekts erreichen wollen, schauen Sie, ob der Nachfolger (Index + 1) das Flag AEO gesetzt hat. Falls dies der Fall ist, ist das ein Objekt vom Typ AOBJECT.

Abb. 6

Abbildung 2 zeigt diesen Typ in der gewohnten Syntax von C++. Wie Sie erkennen können, stehen hier alle Werte, außer dem Index, die Sie unter Refs in dem ACS-Editor setzen können. Die zwei Routinen sind für die Behandlung der Mausklicks und der Ziehoperationen zuständig. Beide haben leere Argumentlisten und liefern auch keine Werte zurück. In key steht der Code des Tastaturkürzels, der dem Klick auf das Objekt gleicht, also die selbe Routine aufruft. In mo_index steht die Nummer der Mausform, die angezeigt wird, sobald die Maus sich über diesem Objekt befindet. Für den Programmierer sind userp1, userp2 und type wichtig, bieten sie doch die Möglichkeit, Daten mit den Objekten zu verbinden und einzelne Objekte voneinander zu unterscheiden, denn ein Objekt, das ein anderes akzeptieren kann, muß erst überprüfen, ob dieses vom geeigneten Typus ist.

Globale Variablen

In der Abbildung 3 sehen Sie die wichtigsten Variablen, die ACS bereitstellt. In vdi_handle befindet sich das Handle der ACS-Workstation. Sie können dieses für die VDI-Funktionen benutzen, es ist aber besser, wenn Sie sich eine eigene Workstation (v_opnvwk) aufmachen, damit Ihnen ACS nicht die Attribute verändert. appexit zeigt an, daß das Programm verlassen wird. Wenn dieses Flag gesetzt (TRUE) ist, dann besteht keine Möglichkeit, die Terminierung abzubrechen. Die restlichen Variablen können aus Click- & Drag-Funktionen benutzt werden, um an aktuelle Daten zu gelangen. Sie enthalten das Fenster (ev_window), den Objektbaum (ev_object) und die Objektnummer (ev_obnr) des Objekts, das angeklickt oder gezogen wurde. Die Mauskoordinaten können dann in ev_mmox und ev_mmoy gefunden werden, der Status der alternierenden Tasten in ev_mmokstate.

Wichtige Funktionen

Als eine kleine Zusammenfassung finden Sie in der Abbildung 4 noch die wichtigsten Funktionen der ACS-Library. Die meisten erklären sich bereits durch ihren Namen. Gesagt sei hier noch, das Awi_closed und Awi_topped besser Awi_close und Awi_top hießen, damit deutlicher herauskäme, daß sie für das Schließen beziehungsweise Toppen eines Fensters verantwortlich sind. Awi_redraw zeichnet den Teil eines Fensters neu, der in limit übergeben wurde.

Die Mausroutinen verstecken (Amo_hide) und zeigen (Amo_show) den Maus-Cursor oder machen ihn zur Biene (Amo_busy), wenn zeitintensivere Operationen durchgeführt werden, und wandeln ihn wieder in den Ursprungszustand (Amo_unbusy) zurück.

Die Speicherverwaltung funktioniert unter ACS genauso wie unter C. Die zur Verfügung stehenden Funktionen lauten Ax_malloc und Ax_free und verhalten sich wie ihre C-Pendants. Sie sollten (müssen) sie aber verwenden, denn sie sind an das Fenster-Handling angepaßt.

Bleiben also noch zwei Routinen zum Darstellen von modalen Dialogen: A_dialog zeichnet eine modale Dialogbox, die sich im Objektbaum dia befindet. alert_str bringt eine Alertbox auf den Bildschirm, die noch einen Zusatzparameter in Form eines Strings erwartet.

Dynamische Objekte

Bis jetzt ist es uns zwar gelungen, alle möglichen Elemente der Benutzerführung einzusetzen; eine Applikation braucht aber auch Fenster mit solchen Inhalten wie zum Beispiel Grafik oder Text. Diese können sich, noch während das Fenster dargestellt wird, verändern. Eine Möglichkeit, dies durchzuführen wäre, indem man gegen das Prinzip von ACS verstieße, das Ersetzen der gesamten Work-Objekt-Behandlung. Der damit verbundene Aufwand verbietet diesen Weg von vorne herein. Wie Sie aber vermutl ich wissen, stellt uns GEM einen Objekttyp zur Verfügung, dessen Zeichenroutinen vom Programmierer bereitgestellt werden müssen. Dieser Typ (USERDEF) befindet sich auch im Teilefenster vom ACS-Editor und kann dort wie eine Box behandelt werden. Unter Specs (Abb. 6) können Sie dann den Namen der Routine eintragen, die für das Zeichnen dieses Objekts zuständig ist. Daneben kann auch ein Parameter angegeben werden, der im Grunde gleiche Objekte, die dieselbe Zeichenfunktion besitzen, im Detail unterscheidet.

Was muß der Programmierer beim Zeichnen eines solchen Objekts nun beachten? Erstens muß die Zeichenroutine das Argument, das sie bekommt, über den Stack holen. Da Pure- und Turbo-C per default die Parameterübergabe über Register tätigen, muß der Funktion der Modifizierer cdecl vorangestellt werden. Das Argument ist vom Typ PARMBLK * (Abbildung 5), die Rückgabe int. Zweitens muß man sich stets vor Augen halten, daß diese Routine von AES aus aufgerufen wurde. Die Routinen von AES sind hier also Tabu, da GEM nicht reentrant ist. Wohl aber können VDI-Routinen benutzt werden, und diese werden meistens auch gebraucht. Drittens befindet sich ACS nicht im Normalzustand, d.h. daß keine der globalen Variablen (ev_window, ev_object ...) gültig sein muß.

Zum Zeichnen selbst brauchen wir zum Beispiel die Koordinaten oder die Flags des Objekts. Diese stecken im übergebenen Parameter innerhalb einer Struktur, die in der Abbildung 5 gezeigt wird. pb_tree zeigt auf den Objektbaum, in dem sich das Objekt befindet. In unserem Fall ist dies also das Work-Objekt des betreffenden Fensters. pb_obj ist der Index des Userdefs innerhalb des Baumes. Die nächsten beiden Einträge pb_prevstate und pb_currstate dienen der Optimierung der Ausgabe: Betrachten Sie einen Button. Beim ersten Zeichnen sind beide Werte gleich. Wenn der Benutzer ihn selektiert, ist pb_prevstate der Zustand des Buttons vor dem Selektieren, pb_currstate der danach. Die Zeichenroutine muß nicht das Objekt zeichnen, sondern zum Beispiel nur invertieren. Dies bedeutet eine hohe Zeitersparnis. Die Variablen, die noch übrig bleiben, sind die wichtigsten. Es sind die Koordinaten des ganzen Objekts und des Bereichs, der gezeichnet werden soll. Beide Werte sind aktuelle Bildschirmkoordinaten. Bleibt noch pb_parm, der Wert, den Sie im ACS-Editor angegeben haben.

Dadurch daß ev_window in der Userdef-Zeichenfunktion nicht gültig ist, können wir aber nicht an die fensterspezifischen Daten (window->user) gelangen. Glücklicherweise paßt in pb_parm aber auch ein Zeiger, der dann eben genau auf diese Daten zeigt. Der Zeiger kann an der selben Stelle initialisiert werden wie window->user. Wie das alles in der Praxis geschieht, sehen Sie in unserem Programmbeispiel.

Abb. 7
Abb. 8
Abb. 9

Ein Zeichenprogramm

Die Überschrift klingt etwas pompös, wenn man sich das kurze Listing anschaut. Es ist auch so, daß dieses Programm nur der Rahmen, also die Benutzeroberfläche eines Zeichenprogramms ist, obwohl dieses bereits Bilder (Doodle monochrom 640x400) laden und anzeigen kann. Den Rest kann sich jeder selbst zusammenhacken; das Gerüst ist stabil und schränkt niemanden ein.

Wir verlangen von dem Programm, daß es ein Grafikfenster mit einer Menüleiste führt, mit der solche Operationen wie Laden oder Löschen möglich sind, und ein Tool-Fenster, das eine Auswahl an Zeichenoperationen wie Linien oder Füllen ermöglicht. Diese Toolbox soll ein Slave (völlig abhängig) vom Grafikfenster sein, d.h. daß sie nicht getrennt geöffnet werden kann. Da es mehrere Grafik-, aber nur ein Tool-Fenster geben soll, wird es geöffnet, wenn ein Grafikfenster geöffnet wird. Wenn weitere Grafiken aufgemacht werden, wird die Toolbox nur getoppt, aber sie bleibt so lange offen, bis das letzte Grafikfenster geschlossen wird.

Öffnen Sie im ACS-Editor eine Datei namens PAINT.ACS. Klicken Sie das Icon Menüs an, und schieben Sie das NEU-Icon in die entstandene Menüliste. Ein Doppelklick öffnet die Menüleiste, die nur einen einen Titel enthält. Dieser Titel ist nur für Desktop-Menüleisten wichtig; in einem Fenster wird er nicht dargestellt. Im Abb. 7 sehen Sie, wie das Menü Bild aufgebaut ist. Für den Titel des Menüs müssen Sie Title aus der Teilebox verwenden. Schieben Sie einen Baustein dieses Typs auf die neue Menüleiste. Nachdem Sie diesen umbenannt haben, klicken Sie ihn einmal an. Die nun erscheinende Box vergrößern Sie passend und tragen die Einträge ein. Diese sind vom Typ String. Unter Refs können Sie zu jedem Eintrag eine Funktion angeben, die ausgeführt wird, wenn der Eintrag angewählt wird. Unsere Funktionen heißen paint_... mit dem Verb des Kommandos in Englisch (Beispiel: paint_clear, paint_load, paint_close oder paint_quit in Listing 1). Wenn die letzten Zeichen in den Einträgen Standard-Tastaturkürzel sind und Sie im Menüeditor Optionen-Tasten anwählen, dann erkennt ACS diese Kürzel automatisch. Die Menüleiste schieben Sie in ein neues Fenster namens PAINTFENSTER. Tragen Sie dort noch die Fensterfunktionen ein, die Sie in Abb. 8 sehen. Die Infoleiste kann entfallen, da sie nur unnötigen Platz im Fenster einnimmt. Alle anderen Einträge können Sie in der Default-Stellung lassen. Öffnen Sie ein neues Objekt mit dem Namen PAINTWORK und wählen Sie eine unsichtbare Umrahmung mit weißem Hintergrund, wie im ersten Teil unserer Folge, an. Machen Sie die Box 640x400 Punkte groß. Auf diese kommt nun ein Userdef. Dieses soll genauso groß sein wie die Umrahmung. Am einfachsten erreichen Sie dies, indem Sie in dem Popup, das nach einem Doppelklick auf das Objekt erscheint, einmal horizontal und dann vertikal füllen. Die Specs stellen Sie so ein wie in Abb. 6 gezeigt. Unter Refs tragen Sie noch bei Click, user_click und bei Index USERPAR ein. Auch dieses Objekt schieben Sie auf das PAINTFENSTER. Dieses ist damit fertig.

Das TOOLFENSTER hat nur den Namen, den Mover und die Funktionen wie in Abb. 9 zu sehen, eingetragen. Unter Attribute ist nur Bleibt angeklickt. Den Fensterinhalt (Work-Objekt) dieses Fensters können Sie definieren wie Sie wünschen; es sollte ein paar Icons enthalten, die verschiedene Zeichenoperationen repräsentieren. Unter Flags klicken Sie für jedes Zeichentool das alternativ auftreten kann Radiobutton an. Damit ist gewährleistet, daß nur ein Werkzeug gleichzeitig aktiviert werden kann. Es ist auch erforderlich, jedem Tool einen Index zu geben, denn nur so können diese in der Applikation unterschieden werden. Geben Sie nun die Datei als ANSI-C-File aus.

Listing 1 enthält das Programm selbst, Listing 2 die zugehörige Projektdatei. Wenn Sie das Programm übersetzt haben, können Sie es starten und unter Laden ein Bild laden. Das fertige Programm zeigt Abb. 11. Probieren Sie alle möglichen Einstellungen und Funktionen. Wie Sie sehen können, ist ACS auch im Scrolling ziemlich schnell, so daß auch Geschwindigkeitsfanatiker zufriedengestellt werden. Dadurch daß wir uns an das Prinzip von ACS gehalten haben, können Sie beliebig viele Fenster öffnen, vorausgesetzt, Sie besitzen entsprechend viel Speicher.

ACSinit erledigt neben dem Üblichen (siehe letzte Folge), das erzeugen des Teilefensters. Daß dies hier und nur hier getan wird, ist der einzige Grund, warum wir die Create-Routine im Fenstereditor nicht angeben mußten. Die Adresse der Fensterstruktur der Toolbox wird in einer globalen Variable (toolbox) gespeichert, damit wir später dieses Fenster direkt öffnen können. Nun wird noch die Variable screen initialisiert, die die Ausgabe auf den Bildschirm ermöglicht.

Das Programm enthält für jedes Fenster eine Struktur, die alle Daten (auch das Bild) enthält. Diese heißt PICTURE. In der Routine paint_create wird diese frisch alloziert und gelöscht. Jetzt wird ein neues Fenster erzeugt und der Eintrag user mit der Adresse der Bildstruktur gefüllt. Die Zeile

window>work[USERPAR].ob_spec.userblk->ub_parm = (long)picture;

trägt diese Adresse auch in das Userdef ein, damit wir dieses später auswerten können. Das Fenster wird gleich danach geöffnet.

Abb. 10

Das Öffnen übernimmt aber unsere eigene Funktion, denn es muß auch noch unser Sklave, die Toolbox geöffnet werden:

(toolbox->open)(toolbox);

Sie werden sich vielleicht fragen, warum wir nicht

Awi_open(toolbox);

für das Öffnen angewandt haben. Nun, dieser Befehl würde die Toolbox zwar öffnen, aber nicht die Funktion tool_open aufrufen, die eigentlich erwünscht wäre. Deshalb gilt es, so weit wie möglich indirekte Aufrufe vom ersten Typ zu tätigen. Nur darf sich eine Funktion nicht selbst aufrufen, denn das würde ohne geeignete Vorkehrungen in einer endlosen Schleife enden. In paint_open dürfen wir deswegen nicht

(window->open)(window); 

schreiben, sondern

Awi_open(window);

Entsprechend zu paint_open funktioniert auch paint_close, nur daß hier die Routinen zum Schließen angewandt werden.

Da unser Fenster unter user eine eigene Struktur alloziert hat, müssen wir diese auch löschen, wenn das Fenster gelöscht wird. Dazu müssen wir eine eigene Service- Routine paint_service screiben. Diese fängt bei uns nur die Nachricht AS_TERM ab, die zeigt, daß das Fenster entfernt wird. Wenn diese Nachricht eintrifft, wird nur term angesprungen, die ihrerseits den Speicher freigibt und das Fenster entfernt.

Die Routinen tool_open muß darauf achten, daß das Tool-Fenster nur einmal aufgemacht wird, andernfalls wird nur getoppt. Wenn das Fenster geöffnet ist, hat die Fenstervariable wi_id einen sinnvollen Wert, sonst -1. Alle Aufrufe dieser Funktion werden in der globalen Variable tool_count mitgezählt.

Bevor die Toolbox geschlossen wird, muß die Variable tool_count den Wert Null besitzen. Die beiden ähnlichen Zeilen:

window->state |= AWS_TERM; 
window->state &= ~AWS_TERM;

verhindern nur das Zeichnen eines Icons und sind nicht weiter wichtig.

Abb. 11

Nun folgen noch die Routinen der Menüeinträge. Ich werde sie nicht weiter erklären, denn der Code spricht für sich selbst und hat nicht viel mit der ACS-Programmierung zu tun. Wichtiger ist schon draw_rect, die Routine, die für das Zeichnen des Userdefs zuständig ist. Sie wertet die übergebenen Daten aus und kopiert den entsprechenden Ausschnitt des Bildes auf den Bildschirm. Die Funktion user_click ist überhaupt nicht definiert. Je nachdem, was für ein Programm Sie entwickeln möchten, müssen Sie hier die entsprechende Funktionalität hinzufügen.

Grischa Ekart

Literatur:

[1] Grischa Ekart: Objektorientierte Programmierung, ST-Computer 7/8 1991

/*            PAINT.C V1.0, 22.2.1992         */
/* by Grieche Ekart / (c) 1992 MAXON Computer */

#include "g:\acs\acs.h"
#include <tos.h>
#include <stdio.h>
#include <string.h>
#include <etdlib.h>

static Awindow *paint_create(void *not_used); 
static int paint_open(Awindow *window); 
static void paint_closed(Awindow *window); 
static int paint_service (Awindow *window, int task, void *in_out); 
static int tool_open(Awindow *window); 
static void tool_closed(Awindow *window); 
static int cdecl draw_rect(PARMBLK *pb); 
static void user_click(void); 
static void term(Awindow *window); 
static void paint_clear(void); 
static void paint_close(void); 
static void paint_load(void); 
static void paint_quit(void);

typedef struct {
    MFDB    bitblk;
    char    name[80];
    char    picture[32000L];
} PICTURE;

MFDB screen;

#include "paint.h"
#include "paint.ah"

Awindow *toolbox; 
int     tool_count = 0;

int
ACSinit(void)
{
    Awindow *rootwindow;

    rootwindow = Awi_root ();/* Adresse vom Rootfenster */
    if(rootwindow == NULL) 
        return(FAIL);

    toolbox = Awi_create(&TOOLFENSTER); /* erzeuge Toolbox */

    if(toolbox == NULL) 
        return(FAIL);

    (rootwindow->service)(rootwindow, AS_NEWCALL, &PAINTPENSTER.create); /* NEU-Icon öffnet Paintfenster */

    screen.fd_addr = 0L; /* aktueller Bildschirm */

    return(OK);
}

static Awindow
*paint_create(void *not_used)
{
    Awindow *window;
    PICTURE *picture;

    picture = Ax_malloc(sizeof(PICTURE)); 
    if(picture == NULL) 
        return(NULL);

    memset(picture, 0, sizeof(PICTURE)); 
    window = Awi_create(&PAINTFENSTER); 
    if(window == NULL)
    {
        Ax_free(picture); 
        return(NULL);
    }
    picture->bitblk.fd_addr = &picture->picture; 
    window->user = picture; 
    window->work[USERPAR].ob_spec.userblk->ub_parm = (long)picture;

    (window->open)(window); 
    return(window);
}

static int
paint_open(Awindow *window)
{
    (toolbox->open)(toolbox);
    Awi_open(window); 
    return(OK);
}

static void
paint_closed (Awindow *window)
{
    (toolbox->closed)(toolbox);
    Awi_closed(window);
}

static int
paint_service (Awindow *window, int task, void *in. out)
{
    switch(task)
    {
        case AS_TERM:
            term(window); 
            break;

        default:
            return(FALSE);
    }
    return(TRUE);
}

static void
terra(Awindow *window)
{
    Ax_free(window->user);  /* Bildspeicher freigeben */
    Awi_delete(window);     /* Fenster löschen */
}

static int
tool_open(Awindow *window)
{
    tool count++; 
    if(window->wi_id == -1)
        Awi_open(window);
    else
        Awi_topped(window); 
    
    return(OK);
}

static void
tool_closed(Awindow *window)
{
    tool_count--;
    if(!tool_count)
    {
        window->state |= AWS_TERM;
        Awi_closed (window); 
        window->state &= ~AWS_TERM;
    }
}

static void 
paint_clear(void)
{
    PICTURE *picture; 

    picture = ev_window->user;
    if(alert_str("[1][| Wollen Sie das Bild |%s| wirklich löschen?]”
                 "[ Nein | Ja ]", ev_window->name) == 2)
    {
        memset(picture->picture, 0, 32000L); 
        Awi_redraw(ev_window, &ev_window->wi_work);
    }
)

static void 
paint_quit(void)
{
    term(ev_window);
}

static void 
paint_close(void)
{
    (ev_window->closed)(ev_window);
}

static char *fileselect (void)
    /*
     * Hole Filenamen 
     */
{
    char *p; 
    int button; 
    char file [16];
    static char path [80]; /* letzter Pfad */

    path [0] = 'A' + Dgetdrv(); /* aktuelles Laufwerk ermitteln */
    path [1] = ';';
    Dgetpath (path + 2, 0); /* aktueller Pfad */ 
    if (path [2] == 0) { 
        path [2] = '\\';
        path [3] = '\0'; 
    }; 
    p = strrchr (path, '\\');   /* select entfernen */
    *(p + 1) = '\0';
    strcat (path, "*.PIC");     /* PIC suchen */
    file [0] = '\0';

    Aev_unhidepointer();    /* Zeiger wieder sichtbar */
    if (fsel_input (path, file, &button) == 0 || button ==0 || file [0] == '\0') { 
        return NULL;
    };

    if (strlen (file) == 9) { /* 8 + . doch Extension anhängen */ 
        if (*(file + 8) == '.') strcat (file, "PIC");
    };
    if (strchr (file, '.') == NULL) { /* hänge Extension an */
        strcat (file, ".PIC");
    };

    p = strrchr (path, '\\'); /* select entfernen */ 
    strcpy (p + 1, file);

    return path;
}

static void 
paint_load(void)
{
    PICTURE *picture;
    FILE *file;
    char *name;

    picture = ev_window->user; 
    name = fileselect(); 
    if(name == NULL) 
        return;

    strcpy (picture->name, name);
    if((file = fopen(picture->name, "rb"))== NULL)
    {
        form_alert(1, "[1][File not found.][ Again ]");
        return;
    }
    if(fread(picture->picture, 1L, 32000L, file) != 32000L)
    {
        form_alert(1, "[1][Falsches Dateiformat!][ Abbruch ]"); 
        fclose(file); 
        return;
    }
    Ast_delete(ev_window->name); 
    ev_window->name = Ast_create(name); 
    wind_set(ev_window->wi_id, WF_NAME, ev_window->name, 0, 0); 
    picture->bitblk.fd_w = 640; 
    picture->bitblk.fd_h = 400; 
    picture->bitblk.fd_stand = 1; 
    picture->bitblk.fd_wdwidth = 40; 
    picture->bitblk.fd_nplanes = 1; 
    Awi_redraw(ev_window, &ev_window->wi.work);
}

static int cdecl 
draw_rect(PARMBLK *pb)
{
    PICTURE *picture;
    int     pxyarray[8];
    int     index[2] = {BLACK, WHITE};

    picture = (PICTURE *)pb->pb_parm; /* eigene Daten */
    pxyarray[0] = pb->pb_xc - pb->pb_x;
    pxyarray(1] = pb->pb_yc - pb->pb_y;
    pxyarray[2] = pxyarray[0] + pb->pb_wc - 1;
    pxyarray[3] = pxyarray[1] + pb->pb_hc - 1;
    pxyarray[4] = pb->pb_xc; 
    pxyarray[5] = pb->pb_yc; 
    pxyarray[6] = pb->pb_xc + pb->pb_wc - 1;
    pxyarray[7] = pb->pb_yc + pb->pb_hc - 1;
    vrt_cpyfm(vdi_handle, MD_REPLACE, pxyarray, &picture->bitblk, &screen, index); 
    return(pb->pb_prevstate);
}

static void 
user_click(void)
{
    /* überprüfen, welches Tool angewählt ist und entsprechend zeichnen. */
}

;*          PAINT.PRJ V1.0, 22.2.1992         */
;* by Grischa Ekart / (c) 1992 MAXON Computer */

PAINT.PRG           ; name of executable program
=                   ; list of modules follows...
PCSTART.O           ; startup code
PAINT.C (PAINT.H)   ; depends also from surface
                    ; definition

G:\ACS\ACS.LIB      ; ACS Library
                    ;
PCTOSLIB.LIB
PCSTDLIB.LIB        ; standard library
PCGEMLIB.LIB        ; AES and VDI library

Grischa Ekart
Aus: ST-Computer 05 / 1992, Seite 134

Links

Copyright-Bestimmungen: siehe Über diese Seite