Lovely Helper: Ein Desk-Accessory, Teil 1

Grundsätzliches

Eigentlich gedachte ich - nach Abschluß der ‘Algorithmen & Datenstrukturen’ - für einige Zeit in der Versenkung zu verschwinden. Doch - wie so oft - es kommt immer anders, besonders als man denkt. So möchte ich Ihnen heute den ersten Teil eines neuen und völlig anders gearteten Themas vorstellen.

Zur Einstimmung

Es geht um Accessories. Genauer gesagt, um die Programmierung eines speziellen Accessorys mit dem Titel Lovely Helper. Wie bei seinen kommerziellen Brüdern (Sidekick, ...) handelt es sich dabei um ein Programm, das eine ganze Reihe unterschiedlicher Fähigkeiten besitzt, die den Umgang mit dem Computer erleichtern sollen.

Bevor ich Ihnen nun die Details dieser Fähigkeiten vorstelle, möchte ich einige Worte auf die Entwicklungsumgebung verwenden. Wie Sie es vielleicht aus den Algorithmen & Datenstrukturen von mir gewohnt sind, erfolgt die Programmierung unter Pascal+ (Version > 2.00). Mit der Angabe der Programmiersprache ist es aber heute leider nicht ganz getan. Die Natur des Problems - Programmierung eines Accessorys und damit zwangsläufig die Nähe zu GEM - macht es sinnvoll, ein weiteres Tool zur Programmerstellung hinzuzuziehen. Ich spreche von einem Resource-Editor, den ich verwenden werde, um die Dialoge unseres Helpers zu entwerfen. Damit entfällt viel stupide Arbeit, die sich auf die Positionierung von irgendwelchen Dialogkomponenten mit Hilfe der entsprechenden Pascal-Befehle bezieht. Zusätzlich wird es für Sie leicht möglich, auf das äußere Erscheinungsbild des endgültigen Produktes - auch ohne großartige Programmiererfahrung - Einfluß zu nehmen.

Anmerkung: Ich werde zur Erstellung der Dialoge den Kuma-Resource-Editor benutzen. Für Sie ist es jedoch unproblematisch, einen Resource-Editor Ihrer Wahl zu benutzen. Insbesondere möchte ich dabei auf den DRI-Editor hinweisen, welchen Sie als Sonderdisk für relativ wenig Geld über diese Zeitschrift beziehen können.

Kommen wir nun zu den einzelnen Bestandteilen des Lovely-Helpers.

Aus der Abbildung 1 können Sie zunächst eine grobe Übersicht gewinnen, die ich Ihnen nun kommentieren möchte:

  1. Den ersten - und schlichtesten - Bestandteil unseres Programms werden wir gleich heute behandeln. Es handelt sich dabei um ein Tool, das Ihnen, in allen GEM-Applikationen, die Möglichkeit gibt, den Füllgrad der angeschlossenen Massenspeicher und des Hauptspeichers zu überwachen. Wie auch bei allen folgenden Helper-Bestandteilen werde ich Ihnen hier eine Möglichkeit - in Form eines Programmtextes - aufzeigen, mit der die entsprechende Funktion auch abgelöst vom Helper realisiert werden kann. So wird es Ihnen bereits heute möglich sein, mit den angegebenen Sources ein funktionierendes Accessory zu erstellen.

  2. Weiterer Bestandteil des Helpers ist ein Druckerspooler, der das Ausdrucken von bis zu 100 unterschiedlichen Dateien erlaubt und im Hintergrund arbeitet, also die Bearbeitung anderer Probleme nicht stört. Die Dateien dürfen dabei entweder vom Typ Text sein oder ein gängiges, monochromes Bildformat besitzen. Die unterschiedlichen Dateiarten werden dabei automatisch erkannt.

  3. Mit dem dritten Bestandteil, betitelt mit Kalender, wird es Ihnen möglich sein, den Kalender eines beliebigen Jahres einzusehen und zu drucken. Berücksichtigt werden dabei alle festen und beweglichen Feiertage. Gewissermaßen nebenbei erhalten wir dabei auch noch ein Programm zu Einsehen und Setzen der Systemzeit.

  4. Der vierte Helper-Bestandteil befaßt sich mit einem Manko des Betriebssystems: Ohne unvertretbaren Aufwand (Hardcopy) können keine Directories gedruckt werden! Abhilfe schafft die Helper-Routine zum Drucken eines Directorys. Mit ihr ist es möglich, entweder ein normales Inhaltsverzeichnis zu drucken, oder sämtliche Ordner einer Partition/Diskette, mit allen enthaltenen Dateien, auszugeben.

  5. Als Abschluß werden wir noch einen naturwissenschaftlichen Taschenrechner behandeln, der Punkt-vor-Strich-Rechnung, Klammerung und alle gängigen Grundrechenarten und Funktionen realisieren wird.

Doch genug geschwafelt! Fangen wir an.

Einige Aspekte der GEM-Programmierung

Der Einfachheit halber, und um die folgenden, ohnehin zahlreichen Erklärungen in Grenzen zu halten, werde ich mich bei den GEM-Fähigkeiten unseres Helpers auf Dialoge und Alarme beschränken. Ferner setze ich voraus, daß Sie im Umgang mit Ihrem Resource-Editor soweit geübt sind, daß Ihnen die Konstruktion von Dialogen mit den drei gängigsten GEM-Objekten - edierbaren und nicht edierbaren Texten sowie Feldtasten - keine Probleme bereitet.

Unter diesen Voraussetzungen ist es leicht möglich, die komplette Benutzerführung unseres Helpers in Form von Abbildungen der beteiligten Dialoge wiederzugeben. In diese Bilder werden dabei jeweils die Namen der interessierenden Objekte eingetragen. Von Interesse sind natürlich nur solche Objekte, auf die von Seiten unseres Programmes zugegriffen wird. Diverse Verzierungen - vielleicht möchten Sie das eine oder andere Image in die an sich recht tristen Dialoge einschmuggeln - werden durch diese Art der Programmierung Ihnen überlassen.

Ihr Helper bekommt dadurch ein individuelles Äußeres, und unsere Probleme mit GEM lassen sich auf drei einfache Fragen reduzieren, die ich nun nacheinander behandeln möchte.

Wie machen wir diese Dialoge für Pascal-Programme verfügbar?

Dazu ist zu sagen, daß jedem GEM-Objekt - sei es nun ein Dialog, ein Menü oder auch nur ein Bestandteil irgendeines anderen GEM-Objektes - bei seiner Konstruktion eine Nummer zugewiesen wird, durch die es in eindeutiger Weise charakterisiert ist. Üblicherweise werden diese Nummern als Angelpunkte bezeichnet und in Form von Konstanten für die unterschiedlichen Programmiersprachen verfügbar gemacht. Durch das Setzen einer entsprechenden Option innerhalb Ihres Resource-Editors wird so beim Speichern des Resources - zusätzlich zu der eigentlichen Resource-Datei und einer editorspezifischen Informationsdatei - eine Datei mit Konstanten in der ausgewählten Sprache generiert. Bei uns sollte das natürlich Pascal sein.

Abb. 1: Die Bestandteile des Lovely Helpers

Mit der Compileroption $i und nachstehendem Namen der Konstantendatei - üblicherweise mit Appendix ‘*.I’ - ist es dann ein leichtes die entsprechende Datei in unsere Sources einzubinden. Dieses Verfahren ist auf jedem Fall dem direkten Einsetzen der Konstanten in die Pascal-Programme vorzuziehen, da die Konstanten auf diese Weise dynamisch - bei eventueller Änderung des Resources - mit angepaßt werden.

Listing 1 zeigt nun, in einem groben Schema eines Accessorys, dieses Vorgehen.

Zum Laden der eigentlichen Resource-Datei wird dort die Funktion initialisieren verwendet. Mit load_resource wird hier zunächst versucht, die Resource-Datei aufzufinden. Der Dateiname ist als Parameter an load_resource zu übergeben. load_resource ermittelt dabei einen Wahrheitswert, der anzeigt ob das Laden der Datei erfolgreich war.

Mögliche Fehlerquellen sind:

Im Falle des fehlerlosen Ablaufes können nun die Dialoge mit der Prozedur find_dialog den entsprechenden Variablen zugewiesen werden. Dabei ist als erster Parameter der im Resource-Editor festgelegte Angelpunkt des Dialoges anzugeben; als zweiter Parameter ist eine Variable vom Typ dialog_ptr zu übergeben. Unter ihr kann später der Dialog ausgeführt und bearbeitet werden.

Nachdem nun alle Dialoge auf diese Weise geladen und initialisiert worden sind, ist es - aus optischen Gründen - noch ganz sinnvoll sie mit center_dialog zu zentrieren.

Wenden wir uns nun der nächsten Frage zu:

Was ist der grundsätzliche Rahmen für die Programmierung eines Accessorys?

Wie Sie sicherlich bereits bemerkt haben, habe ich beim Scannen von initialisieren eine Funktion - namentlich menu_register - unterschlagen. Das liegt daran, daß sie nichts mit dem Laden des Resources zu tun hat, sondern zur generellen Ausstattung eines Accessorys gehört, auf die ich jetzt zu Sprechen kommen möchte. Beginnen wir mit der Compileroption $s zu Anfang des Programmes. Durch die $s nachfolgende positive, ganze Zahl wird dem Compiler die Stackgröße des Programmes mitgeteilt. Da der Default-Wert des Stacks der komplette verfügbare Speicher ist, wäre es in einem Accessory fatal, diese Compileroption nicht anzugeben. Lediglich unschön ist es, einen zu hohen Wert vorzugeben, weil dieser Speicher nach dem Programmstart unwiderruflich und unnötigerweise verloren ist.

Weiter unten im Listing folgen nun die Bindingdateien der Pascal+ GEM-Library - gemconst.pas, gemtype.pas und gemsubs.pas - und die bereits erwähnten Resource-Angelpunkte sowie einige obligatorische Variablen, auf deren Bedeutung ich später zu sprechen komme.

Zurück zu menu_register. Übergeben wird menu_register die Applikationsnummer des Accessorys - der Wert, den man zuvor durch init_gem erhalten hat -, sowie eine Variable vom Typ str255, die die Bezeichnung enthalten muß, unter der man das Accessory in der Menüleiste wiederfinden will. Man erhält die Nummer des zugehörigen Menüeintrags zurück. Diese wird benötigt, um Eindeutigkeit herzustellen, falls man aus einem Accessory mehrere Menüeinträge zu verwalten hat. Auch dazu später mehr.

Anmerkung: GEM merkt sich keinesfalls die komplette Bezeichnung Ihres Accessorys, sondern nur eine Referenz auf die Variable apl_name. Der positive Effekt dieser Begebenheit ist, daß Sie, aus welchen Gründen auch immer, einfach den Namen eines Accessorys ändern können, indem Sie nachträglich den Wert von apl_name modifizieren.

Das eigentliche Kernstück eines jeden Accessorys finden Sie nun im Hauptprogramm des Listings 1 - eine Endlosschleife.

Ja, Sie haben richtig gelesen: ENDLOSSCHLEIFE.

Wird ein Accessory nämlich einmal korrekt initialisiert und darauffolgend gestartet, sollte es tunlichst nie verlassen werden, weil dies unweigerlich zum Absturz des Betriebssystems führen würde. Innerhalb dieser Endlosschleife befinden sich nun zwei wesentliche Teile:

Zunächst ist da ein Aufruf der Funktion get_event. Um get_event ausreichend erklären zu können, werde ich etwas weiter ausholen.

Das Stichwort lautet Multitasking. Für unser Kistchen sicherlich etwas zu hoch gegriffen, aber doch irgendwo zutreffend. Die “Multi-Tasks” sind nämlich die installierten Accessories plus gerade laufendes Programm. Diese Programme teilen sich in trauter Gemeinsamkeit die Rechenkapazität des ST. Dazu ist es not-wendig - da der ST in der Regel ja nur eine CPU besitzt - ein System zu entwickeln, das den Programmen in einer gewissen Reihenfolge die Kontrolle über die CPU zur Verfügung stellt.

Das heißt: Maximal ein Programm kann arbeiten; die anderen liegen in dieser Zeit auf der faulen Haut.

{**************************************************}
{* Listing 01  : Generelle Vorgehensweise zur     *}
{*               Installation von Accessoirs      *}
{*                                                *}
{*                                                *}
{* Datei       : MUSTER1.PAS                      *}
{* last update : 24.5.1988                        *}
{**************************************************}

{$s<Stacksize>}

PROGRAM resource_muster (input,output);

CONST {$i gemconst.pas}
      {Für <Filename> ist später der Name der 
       Resource-Angelpunkte *.I anzugeben}
      {$i <Filename>}

TYPE  {$i gemtype.pas)

VAR   msg      : message_buffer;
      apl_name : str255;
      apl_nr   ,
      menu_nr  ,
      event    ,
      dummy    : integer;

      dialog_1 ,
          .
          .
          .
      dialog_n : dialog_ptr;

{$i gemsubs.pas}

    .
    . {Hier irgendwo stehen später die Hauptroutinen)
    .

FUNCTION initialisieren : boolean;

    VAR ok : boolean;

    BEGIN
        {Für <Filename> ist unten später der Dateiname des RSC-Files 
         (*.RSC) anzugeben.} 
        ok:=load_resource('<Filename>');
        IF ok THEN 
            BEGIN
                apl_name:='<Desk-Top Eintrag>'; 
                menu_nr:=menu_register(apl_nr, apl_name); 
                find_dialog(diag_1,dialog_1);
                             .
                             .
                             .
                find_dialog(diag_n,dialog_2); 
                center_dialog(dialog_1);
                            .
                            .
                            .
                center_dialog(dialog_n);
            END;
        initialisieren:=ok;
    END;

BEGIN
    apl_nr:=init_gem;
    IF apl_nr>«0 THEN
        IF initialisieren THEN 
            WHILE true DO 
                BEGIN
                    event:=get_event( ... );
                    {Auf get_event erfolgt eine genaue Auswertung der 
                    Rückgabeparameter, die von Fall zu Fall unterschiedlich 
                    sein kann. Dazu später mehr.}
                END;
END.

Listing 1: MUSTER1.PAS

Der Algorithmus, der dieses steuert, sieht - in grober Vereinfachung - folgendermaßen aus:

init_system;
WHILE NOT beschäftigt DO 
    IF will_einer_was_von_mir THEN 
        BEGIN
            apl_nr:=wer_ist_es_denn;
            weitermachen(apl_nr);
        END;

Wenn das Ganze funktionieren soll, hat jedes Programm oder Accessory darauf zu achten, daß es zunächst einmal verkündet, ob es Arbeit hat. Ist ihm eine Arbeitsberechtigung erteilt worden und die Arbeit abgeschlossen, muß es sich wieder zurückmelden, damit andere Programme in ihrer Arbeit weiter fortfahren können.

Dieses alles regelt die nicht ganz triviale Funktion get_event. Sie besitzt immerhin 22 Parameter, von denen man in einem speziellen Fall aber meistens nur sehr wenige wirklich benötigt.

Sehen wir sie uns einmal an:

    FUNCTION get_event(     event_mask  : integer;
                            btn_nask    : integer;
                            btn_state   : integer;
                            n_clicks    : integer;
                            ticks       : long_integer;
                            r1_flag     : boolean;
                            r1_x        , 
                            r1_y        , 
                            r1_w        ,
                            r1_h        : integer;
                            r2_flag     : boolean;
                            r2_x        ,
                            r2_y        ,
                            r2_w        ,
                            r2_h        : integer;
                        VAR message     : message_buffer;
                        VAR key         : integer;
                        VAR bcnt        ,
                            bstate      : integer;
                        VAR mx          ,
                            my          : integer;
                        VAR kbd_state   : integer): integer;

Der wichtigste Parameter von allen ist dabei event_mask. Er legt fest, auf welches Ereignis unser Accessory warten soll, bevor es wieder zum Leben erweckt wird. Dabei gibt es eine ganze Reihe von möglichen Ereignissen, die wir jetzt nicht alle durchgehen werden, sondern uns, von Fall zu Fall, ansehen.

Trotzdem aber ein kleines Beispiel: Nehmen wir an, wir wollen, daß uns get_event alle zehn Sekunden die Kontrolle über den ST gibt. Die zugehörige Ereigniskonstante heißt e_timer. Ferner ist noch der genaue Zeitwert über die Variable ticks zu übergeben. Der komplette get_event-Aufruf könnte damit wie folgt lauten:

event:=get_event(e_timer,0,0,0,10000,false, 
                 0,0,0,0,false,0,0,0,0,msg,
                 dummy,dummy,dummy,
                 dummy,dummy.dummy);

Sie sehen, es sind keinesfalls 22 (echte) Parameter notwendig, um get_event sinnvoll zu benutzen. Im Beispiel sind es nur die Parameter Nummer 1 und 5, die entscheiden.

Abb. 2: Dialog DISKWAHL

Tritt nun eines der angeforderten Ereignisse ein, wird, für den Fall, daß der Rechner nicht gerade anderweitig beschäftigt ist, nach get_event im Programmtext fortgefahren. Der Funktionswert von get_event entspricht dabei gerade dem angeforderten Ereignis bzw. dem Bitmuster der angeforderten Ereignisse, falls mehrere anliegen sollten. Das habe ich oben übrigens unterschlagen: Soll auf mehrere unterschiedliche Ereignisse gleichzeitig gewartet werden, so sind diese ebenfalls als Bitmuster zu übergeben. Am sinnvollsten macht man dies wohl mit der bitweisen Veroderung der Ereignisse:

event:=get_event(e_timer | e_message, ... );

Dabei würde der obenstehende Aufruf von get_event auf die beiden Ereignisse e_timer und/oder e_message warten.

Auf einen get_event-Aufruf erfolgt nun - sinnvollerweise, als zweiter Bestandteil der zentralen Endlosschleife - eine Analysephase, in der herausgefunden werden muß, welches event denn nun wirklich eingetreten ist und welche Routinen demzufolge zu Starten sind. Auch das später am konkreten Beispiel.

Unsere vorerst letzte Frage, GEM betreffend, lautet:

Wie führe ich einen Dialog aus?

Zuständig für die Antwort ist Listing 2. Es zeigt das generelle Schema der Dialogausführung. Zu erkennen sind insgesamt drei Phasen, während denen zunächst im Dialog mit den drei Funktionen set_dtext, set_dedit und obj_setstate die veränderbaren Objekte initialisiert werden. Es folgt die Dialogausführung mit do_dialog bzw. bei wiederholter Ausführung mit redo_dialog. Letztendlich können die geänderten Objekte noch ausgelesen werden. Und zwar mit get_dedit, falls es sich um edierbare Texte, bzw. mit obj_state, falls es sich um bestimmte Flags eines beliebigen Objektes handelt.

Ferner ist hier noch das Prozedurenpaar begin_update und end_update anzutreffen. Diese beiden Prozeduren blocken alle Bildschirm Veränderungen seitens anderer Programme oder des Betriebssystems ab, welche unsere Dialoggraphik stören könnten. Ihrer Aufgabe zufolge sollten sie immer das komplette Dialoghandling, zumindestens aber den graphischen Teil desselben, einschließen.

Beispiele für die Parametrisierung der obigen Operationen finden Sie genügend innerhalb der konkreten Programme.

Sehe ich das richtig? Sie sind die Vertröstungen auf später leid und werden schon langsam unruhig. Naja, dann wollen wir mal zu den praktischen Dingen übergehen.

{*************************************************} 
{* Listing 02   : Generelle Vorgehensweise der   *}
{*                Bearbeitung eines Dialogs.     *}
{*                                               *}
{* Datei        : MUSTER2.PAS                    *}
{* last update  : 24.5.1988                      *}
{*************************************************} 

PROCEDURE do_dialog_n;

        ...

    VAR button : integer 
        str    : str255;

        ...

    BEGIN
        begin_update;

        {Initialisierungsphase:

         Besorgen d.Parameter u.Eintrag in den Dialog:

         Für Texte:

         Set_DText( dial : Dialog_Ptr ; item : Tree_Index ; s : Str255 ; 
                    font : short_integer ; just : E_Just ) ;

         Für editierbare Texte:

         Set_DEdit( dial : Dialog_Ptr ; item : Tree_Index ; 
                    template, valid, initial : Str255 ; 
                    font : short_integer ; just : TE_Just ) ;

         Für Feldtasten:

         Obj_SetState ( dial : Dialog_Ptr ; index : Tree_Index ; 
                        state : short_integer ; redraw : boolean ) ;

        }

        {Ausführungsphase:

         Ausführen des Dialoges mit:

         FUNCTION Do_Dialog( dial : Dialog_Ptr ;
                             start_obj : short_integer ) : short_integer ;

         oder, bei wiederholter Ausführung, mit:

         FUNCTION Redo_Dialog ( dial : Dialog_Ptr ;
                                start_obj : short_integer ) : short_integer;

         Die Dialoggraphik kann dabei mit:

         Obj_Redraw(Box:Dialog_Ptr; item:Tree_index);

         und

         Show_Dialog ( Box:Dialog_Ptr ); 

         immer wieder aufgefrischt werden.
        }

        {Die Auswertung der Ergebnisse erfolgt mit 
         folgenden beiden Operationen, sowie über den 
         Funktionswert von do- bzw. redo_dialog :

         FUNCTION Obj_State ( dial  : Dialog_Ptr ;
                              index : Tree_Index ) : short_integer ;

         Get_DEdit( dial : Dialog_Ptr ; item : Tree_Index ;
                    VAR s : Str255 ) ;
        }

        {Entfernen des Dialoges mit :
    
         End_Dialog( dial : Dialog_Ptr ) ;
        }

        end_update;
    END;

Listing 2: MUSTER2.PAS

INFO.ACC Vom Entwurf zum Programm

Als allererstes möchte ich Ihnen von den praktischen Dingen das Listing 3 - hilf.pas - ans Herz legen. Hier sind alle Betriebssystemaufrufe deklariert, die wir während der gesamten Helper-Entwicklung benötigen werden. Ferner finden sich hier einige sehr einfache, allerdings häufiger benötigte Hilfsroutinen, die allesamt selbsterklärend sein dürften. Auf die Funktionsweise der Betriebssystemaufrufe werde ich dabei jeweils dann eingehen, wenn sie benötigt werden. Eingebunden wird hilf.pas (wie nicht anders zu erwarten) mit $i. Zurück zur eigentlichen Aufgabe!

Wir können sie nun endgültig fixieren:

Nach Anwählen unseres Accessorys mit dem Namen ‘Information’ soll zunächst ein Dialog erscheinen, der eine Stationskennung (A, B,..., F) einliest. Darauffolgend sollen der Füllungsgrad der angewählten Station, sowie der freie Platz im Hauptspeicher in einem zweiten Dialog ausgegeben werden.

In unserer “graphischen Beschreibungsweise” kann man diese Dialoge folgendermaßen definieren:

Dialog DISKWAHL (Abb. 2) besteht nur aus Feldtasten, deren Namen Sie bitte der Abbildung entnehmen. Als Flags sind für alle Tasten Selectable und Exit zu wählen. Für die Taste BDISKAKT ist zusätzlich Default vorzugeben.

Dialog INFO (Abb. 3) enthält fast nur normale Texte, sowie eine Feldtaste zum Exit aus dem Dialog. Die Texte besitzen bis auf TAKTFLOP die Länge 8; TAKTFLOP hat die Länge 1. Die einzige Feldtaste besitzt die Flags Selectable, Exit und Default.Das Ganze bekommt den Namen INFO.RSC und wird mit den Pascal-Konstanten abgespeichert.

So. Von Seiten des Resource-Editors war’s das schon. Ist doch wirklich angenehmer als ein Paar dutzend Zeilen Source, oder etwa nicht?

Jetzt geht’s zur Programmierung. Die Verwaltung der beiden Dialoge finden Sie im Listing 5. Zunächst Dialog DISKWAHL. Als allererstes müssen wir hier die angeschlossenen Stationen ermitteln, damit der Programmanwender keinen Unfug machen kann, indem er ein nicht angeschlossenes Gerät auswählt. Dies erledigt der Betriebssystemaufruf drvmap (Deklaration siehe hilf.pas) für uns. Er liefert einen Bitvektor - Marke Langwort, also long_integer - zurück, der in aufsteigender Reihenfolge je eine 1 für eine angeschlossene Station und eine Null für eine nicht angeschlossene Station enthält.

Bit 0 auf 1 ==> Station A ist angeschlossen
Bit 1 auf 1 ==> Station B ist angeschlossen
Bit 2 auf 0 ==> Wo ist die Hard-Disk? Arbeiten Sie noch immer mit der Floppy? ...
Bit 31 auf 1 ==> Wohl kaum!

Nachdem die Bits mit ein wenig Aufwand auseinandergepflückt sind, werden die entsprechenden Feldtasten im Dialog DISKWAHL suspendiert (Zeilen 16-32). Es können dann nur noch die zulässigen Stationen ausgewählt werden. Nun wird der Dialog geführt.

Abb. 3: Dialog INFO

Anschließend - mit einem weiteren Betriebssystemaufruf (dsetdrive) - wird die neue, aktuelle Floppy gesetzt. Der Dialog verabschiedet sich dann mit end_dialog, wobei do_disk_dialog noch einen Wahrheitswert erhält, welcher angibt, ob abgebrochen wurde oder nicht.

Kommen wir nun zu dem Informationsdialog INFO. Hier benötigen wir drei unterschiedliche Betriebssystemaufrufe, um seine geforderten Inhalte zu ermitteln. Und zwar sind dies:

dfree (Listing 3, Zeilen 49-51).

dfree ermittelt unter Angabe einer Stationskennung (gleiche Codierung wie bei dsetdrive) einen Array von vier long_integer-Werten, die Daten über die angegebene Station enthalten. Dazu ist zuvor ein entsprechender Typ - buffer_type - zu deklarieren, der diese Werte aufnimmt. Das Array enthält nach dem Aufruf folgende Informationen:

buffer[1] die Größe der Sektoren der angewählten Station
buffer[2] die Anzahl der Sektoren pro Cluster
buffer[3] die Gesamtzahl der Cluster
buffer[4] die Anzahl der unbelegten Cluster

Auf einfachste Weise lassen sich aus diesen vier Daten die erforderlichen Informationen über eine Station errechnen. Betrachten Sie dazu die Zeilen 66-80 des Listings 5: Hier sind unter anderem die entsprechenden Formeln in Pascal-Notation aufgeführt.

dgetdrive (Listing 3, Zeilen 43+44)

Ein weiterer Betriebssystemaufruf do_infos ist dgetdrive. Mit dgetdrive kann, analog zu dsetdrive, die aktuelle Station ermittelt werden. Das Ergebnis ist ebenfalls in den Dialog einzutragen (Listing 5, Zeile 62-64).

malloc (Listing 3, Zeilen 60+61)

Mit malloc letztendlich können wir den freien Platz des Hauptspeichers erfragen. Dazu übergeben wir an die Funktion malloc den Wert -1 und erhalten als Funktionsergebnis den freien Speicher in Byte.

Anmerkung: Die obige Anwendung ist an sich nur eine Nebenfunktion von malloc. Eigentlich dient malloc zur Vergabe von Speicher, wobei der Wert (vom Typ long_integer), den man an malloc übergibt, gerade die Größe des erwünschten Speichers bezeichnet. Der Funktionswert von malloc kennzeichnet dabei den Beginn des von malloc angeforderten Speichers.

Das Einträgen der in 1-3 erhaltenen Werte in den INFO-Dialog und das einmalige Ausführen des Dialoges ist dann auch schon alles, was noch im Listing 5 passiert.

Deshalb wollen wir uns jetzt der Programmierung des Accessorys zuwenden (Listing 4). Hier ist der interessante Teil die Hauptroutine, weil sich dort eine konkrete Anwendung von get_event findet. Alle anderen Bestandteile entsprechen genau dem, was nach dem Schema für Accessory-Programmierung (Listing 1) zu erwarten war.

Unser get_event muß nun gerade so aussehen, daß unser Accessory die Kontrolle für sich verlangt, wenn zuvor sein Eintrag im Desktop angewählt wurde. In diesen Fall übermittelt GEM ein Message-Event (e_message) an unser Accessory. Da e_message aber wohl das komplexeste Ereignis von allen ist - es steuert alles, was irgendwie mit GEM zusammenhängt - benutzt get_event zur Übergabe der genauen Ereignisbeschreibung ein ganzes Array, den sogenannten Message_buffer.

In unserem Spezialfall sieht es nun so aus, daß, wenn unser Accessory selektiert wird, GEM ein Message-Event auslöst und den Message_buffer in der nullten Komponente mit der Konstanten ac_open belegt. Insofern brauchen wir nur mit get_event ein Message-Event anzufordern und nach Übergabe der Kontrolle an unser Accessory msg[0] auf ac_open zu überprüfen. Sollte diese Bedingung erfüllt sein, starten wir unsere Dialoge (do_info). Andernfalls wird wiederum ein Message-Event angefordert und so fort.

{************************************************}
{* Listing 03  : Einige des öfteren benötigte   *}
{*               Hilfsroutinen, sowohl eigene,  *}
{*               wie auch Betriebssystemaufrufe *}
{*                                              *}
{* Datei       : HILF.PAS                       *}
{* last update : 19.5.1988                      *}
{************************************************}

CONST {Suchprädikate für fsfirst/fsnext} 
      normal_file = $00; 
      read_only   = $01; 
      hidden      = $02;
      hidden_sys  = $04; 
      disk_label  = $08; 
      folder      = $10;

      {Sortierarten für die Directoryausgabe}
      sort_name  = 0;
      sort_date  = 1;
      sort_size  = 2;
      sort_type  = 3;

TYPE  buffer_type = ARRAY [1..4] OF long_integer;

      dta_name = PACKED ARRAY [1..14] OF char;

      dta_type = RECORD
                    reserviert  : PACKED ARRAY [0..21] OF byte; 
                    zeit        : integer;
                    datum       : integer;
                    groesse     : long_integer;
                    name        : dta_name;
                END;

      dta_Ptr_type = ^dta_type ;

PROCEDURE dsetdrive(drv : integer);
    GEMDOS($0e);

FUNCTION drvmap : long_integer;
    BIOS(10);

FUNCTION dgetdrive : integer;
    GEMDOS($19);

FUNCTION fgetdta : dta_ptr_type;
    GEMDOS($2f);

PROCEDURE dfree (VAR buffer : buffer_type;
                     drive  : integer);
    GEMDOS($36);

FUNCTION dsetpath(VAR name : cstring) : integer; 
    GEMDOS($3b);

FUNCTION dgetpath(VAR name : cstring;
                      drv : integer) : integer;
    GEMDOS($47);

FUNCTION malloc(needed : long_integer) : long_integer;
    GEMDOS($48);

FUNCTION fsfirst (VAR name : cstring;
                      attr : integer) : integer;
    GEMDOS($4e);

FUNCTION fsnext : integer;
    GEMDOS($4f);

PROCEDURE io_check(checked : boolean);
    EXTERNAL;

FUNCTION io_result : integer;
    EXTERNAL;

FUNCTION digit(ch : char) : integer;

    BEGIN
        digit:=ord(ch)-ord('0');
    END;

FUNCTION Charakter(int : integer) : char;

    BEGIN
        charakter:=chr(int+ord('0'));
    END;

PROCEDURE n_expand(     n_l : integer;
                    VAR n_s : str255);

    VAR i ,
        l : integer;

    BEGIN
        l:=length(n_s)+1;
        FOR i:=1 TO n_l DO
            n_s:=concat ('O’,n_s);
    END;

PROCEDURE s_expand(     s_l : integer;
                    VAR s_s : str255);

    VAR i ,
        l : integer;

    BEGIN
        l:=length(s_s)+1;
        FOR i:=1 TO s_l DO
            s_s:=concat(' ',s_s);
    END;

FUNCTION log(x : real) : real;

    BEGIN
        log:=ln(x)/ln(10);
    END;

FUNCTION exp10(x : real) : real;

    BEGIN
        exp10:=exp(ln(10)*x);
    END;

Listing 3: HILF.PAS

Tips zum Compilieren & Testen

Nachdem Sie alle Tipparbeiten (hoffentlich) glücklich beendet haben, geht es nun ans Compilieren. Grundsätzlich ist bei den Compileroptionen ACC anzuwählen. Ferner sind einfach alle Häkchen aus dem Compileroptionendialog zu entfernen, da Debugging bei Accessorys in keiner Weise erlaubt ist. Beim Linken gilt ähnliches: Als Linkoption ist ACC anzuwählen, außerdem sollten Sie die Datei PASTRIX noch als zusätzliche Linkdatei eintragen, da einige ihrer Routinen im Laufe der weiteren Helper-Teile benötigt werden. Wie Sie dem Dateinamen beim Laden des Resources vielleicht entnommen haben (Listing 4, Zeile 39), ist unser INFO.ACC für den Betrieb auf einer Diskette (Stationskennung A) vorgesehen. Durch Ändern der Stationskennung nach C kann man natürlich sehr leicht das Accessory auf eine Harddisk anpassen, welche lediglich über eine Bootvorrichtung verfügen muß. Ich würde Ihnen allerdings davon abraten, während der Tests mit der Harddisk zu arbeiten. Sollte nämlich dann ein Laufzeitfehler auftreten, sind Sie allemal der Dumme, denn Sie haben das Problem, Daten von einer Harddisk zu entfernen, die Sie nicht starten können, weil dabei laufend das Betriebssystem abstürzt. Der Fehler muß nicht einmal tiefschürfend sein. Es würde völlig ausreichen, wenn Sie beim Abtippen das Einstellen des Programmstacks vergessen haben.

Sollte dann bei Ihren Tests - die nach obigen Worten hoffentlich auf einer Diskette stattfinden - trotzdem ein Laufzeitfehler auftreten, so ist dieser natürlich ebenfalls ziemlich dumm, weil es nicht möglich ist, den Debugger zur Fehlersuche hinzuzulinken. Als Abhilfe empfiehlt sich hier, Ihr Accessory in ein normales GEM-Programm (mit Debugging) umzuwandeln, indem Sie alle accessory-typischen Konstruktionen streichen. Im Listing 5 sind dies z.B. die Zeilen:

42-43 Anmeldung der Menüleiste
58-61 der get_event-Aufruf und die Messageprüfung

Vielleicht ersetzen Sie auch noch die Endlosschleife durch eine terminierende Schleife. Der Resetknopf wird es Ihnen danken.

Sollte sich ein Fehler jedoch weiterhin als hartnäckig erweisen und im Source absolut nichts Verdächtiges zu finden sein, dann liegt das wahrscheinlich nicht an Ihnen, sondern an mir! Absolut sichere Methoden zum Entwickeln von Programmen, die länger als 5 Zeilen sind, hat man bisher leider noch nicht gefunden.

Meine Empfehlung in diesem Falle: Schreiben Sie mir! Für den Fall, daß mich Ihr Schreiben über die Redaktion in endlicher Zeit erreicht, werde ich mich bemühen, den Fehler zu finden und so bald wie möglich zu korrigieren. Am Ende der Serie wird es dann irgendeinen Vertrieb des fertigen Helpers geben. Wahrscheinlich über den PD-Service dieser Zeitschrift.

{**************************************************}
{* Listing 04   : Resource-Handling für das       *}
{*                Informations-Accessory          *}
{*                                                *}
{* Datei       : INFO.PAS                         *}
{* last update : 19.5.1988                        *}
{**************************************************}

{$s10}

PROGRAM info (input,output);

CONST {$i gemconst.pas}
      {$i trixcons.pas}
      {$i info.i}

TYPE  {$i gemtype.pas}
      {$i trixtype.pas}

VAR   msg         : message_buffer;
      apl_name    : str255;
      apl_nr      ,
      menu_nr     ,
      event       ,
      dummy       : integer;
      disk_dialog ,
      info_dialog : dialog_ptr;

{$i gemsubs.pas}
{$i trixsubs.pas}
{$i hilf.pas}
{$i info1.pas}

FUNCTION initialisieren : boolean;

   VAR ok : boolean;

   BEGIN
      ok:=load_resource('A:\INFO.RSC');
      IF ok THEN 
         BEGIN
            apl_name:='  Information'; 
            menu_nr:=menu_register(apl_nr,apl_name); 
            find_dialog(info,info_dialog); 
            find_dialog(diskwahl,disk_dialog); 
            center_dialog(info_dialog); 
            center_dialog(disk_dialog);
         END;
      initialisieren:=ok;
   END;

BEGIN
   apl_nr:=init_gem;
   IF apl_nr>=0 THEN
      IF initialisieren THEN 
         WHILE true DO 
            BEGIN
               event:=get_event(e_message,0,0,0,0, 
                                true,0,0,0,0, 
                                true,0,0,0,0,msg,dummy, 
                                dummy,dummy, 
                                dummy,dummy,dummy);

               IF msg[0]=ac_open THEN 
                  do_info;
            END;
END.

Listing 4: INFO.PAS

Vorausschau

Abschließen möchte ich mit einer Vorausschau auf das, was Sie in der nächsten Ausgabe erwartet. Es wird der erste Teil des Druckerspoolers sein. Insgesamt denke ich, daß wir den Spooler in zwei Ausgaben komplett montieren können. Beim nächsten Mal beginnen wir mit:

Bis dann, Ihr
Dirk Brockhaus

{**************************************************}
{* Listing 05  : Verwaltung der beiden Dialoge    *}
{*               des Diskinfo-Accessorys          *}
{*                                                *}
{*                                                *}
{* Datei       : INFO1.PAS                        *}
{* last update : 19.5.1988                        *}
{**************************************************}

FUNCTION do_disk_selekt : boolean;

   VAR button  : integer;
       map     : long_integer;

   BEGIN
      map:=drvmap;
      IF NOT(odd(map)) THEN
         obj_setstate(disk_dialog,bdiska,disabled,false); 
      map:=shr(map,1);
      IF NOT(odd(map)) THEN
         obj_setstate(disk_dialog,bdiskb,disabled,false); 
      map:=shr(map,1);
      IF NOT(odd(map)) THEN
         obj_setstate(disk_dialog,bdiskc,disabled,false); 
      map:=shr(map,1);
      IF NOT(odd(map)) THEN
         obj_setstate(disk_dialog,bdiskd,disabled,false); 
      map:=shr(map,1);
      IF NOT(odd(map)) THEN
         obj_setstate(disk_dialog,bdiske,disabled,false); 
      map:=shr(map,1);
      IF NOT(odd(map)) THEN
         obj_setstate(disk_dialog,bdiskf,disabled,false); 
      begin_update;
      button:=do_dialog(disk_dialog,0); 
      obj_setstate(disk_dialog,button,normal,false);
      CASE button OF
         bdiska : dsetdrive(0); 
         bdiskb : dsetdrive(1); 
         bdiskc : dsetdrive(2); 
         bdiskd : dsetdrive(3); 
         bdiske : dsetdrive(4); 
         bdiskf : dsetdrive(5);
      END;
      end_dialog(disk_dialog); 
      end_update;
      do_disk_selekt:=NOT(button-bdiskabb);
   END;

PROCEDURE do_info;

   VAR buffer  : buffer_type; 
       str     : str255; 
       button  : integer;

   BEGIN
      begin_update;
      IF do_disk_selekt THEN 
         BEGIN
            writev(str,malloc(-1));
            s_expand(8,str);
            set_dtext(info_dialog,tspeifre,str,system_font,te_left);
            str:=' ';
            str[1]:=chr(dgetdrive+ord('A'));
            set_dtext(info_dialog,taktflop,str,system_font,te_Left);
            dfree(buffer,0);
            writev(str,buffer[2]*buffer[3]*buffer[4]);
            s_expand(8,str);
            set_dtext(info_dialog,tbyteges,str,system_font,te_left);
            writev(str,(buffer[2]-buffer[1])*buffer[3]*buffer[4]);
            s_expand(8,str);
            set_dtext(info_dialog,tbytebel,str,system_font,te_left);
            writev(str,buffer[1]*buffer[3]*buffer[4]); 
            s_expand(8,str);
            set_dtext(info_dialog,tbytefre,str,system_font,te_left); 
            writev(str,buffer[3]*buffer[4]); 
            s_expand(8,str);
            set_dtext(info_dialog,tclugroe,str,system_font,te_left); 
            writev(str,buffer[2]); 
            s_expand(8,str);
            set_dtext(info_dialog,tcluanza,str,system_font,te_left); 
            button:=do_dialog(info_dialog,0); 
            obj_setstate(info_dialog,button,normal,false); 
            end_dialog(info_dialog);
         END;
      end_update;
   END;

Listing 5: INFO1.PAS

(* resource set indicies for INFO *)

CONST
   diskwahl = 0;  (* form/dialog *)
   bdiska   = 3;  (* BUTTON in tree DISKWAHL *)
   bdiskb   = 4;  (* BUTTON in tree DISKWAHL *)
   bdiskc   = 5;  (* BUTTON in tree DISKWAHL *)
   bdiskd   = 6;  (* BUTTON in tree DISKWAHL *)
   bdiske   = 1;  (* BUTTON in tree DISKWAHL *)
   bdiskf   = 8;  (* BUTTON in tree DISKWAHL *)
   bdiskakt = 9;  (* BUTTON in tree DISKWAHL *)
   bdiskabb = 10; (* BUTTON in tree DISKWAHL *)
   info     = 1;  (* form/dialog *)
   tspeifre = 5;  (* TEXT in tree INFO *)
   taktflop = 7;  (* TEXT in tree INFO *)
   tbyteges = 9;  (* TEXT in tree INFO *)
   tbytebel = 11; (* TEXT in tree INFO *)
   tbytefre = 13; (* TEXT in tree INFO *)
   tclugroe = 15; (* TEXT in tree INFO *)
   tcluanza = 17; (* TEXT in tree INFO *)
   bokinfo  = 18; (* BUTTON in tree INFO *)

Listing 6: INFO.I


Dirk Brockhaus
Aus: ST-Computer 04 / 1989, Seite 116

Links

Copyright-Bestimmungen: siehe Über diese Seite