2.12 Verwendung von Libraries

^

2.12.1 Konzeptionelles

Libraries (deutsch: Bibliotheken) sind Sammlungen von Funktionen, SUBs, Konstanten und Struktur-Typen, die von anderen Programmen und Libraries verwendet werden können. Libraries können keine statischen UI-Objekte enthalten, aber UI-Objekte können zur Laufzeit erzeugt werden (siehe unten).

Libraries bieten folgende Vorteile:

  • Verwendung von geprüftem Code. Die Routinen einer Library sind meist sehr gründlich getestet. Bugfixes in einer Library wirken sich sofort auf alle Programme aus, die diese Library verwenden, ohne dass das Programm neu compiliert werden muss.

  • Schnellere Programmentwicklung. Routinen aus einer Library muss man nicht selbst oder noch einmal schreiben.

  • Mehrere BASIC-Programme können die gleiche Library gleichzeitig verwenden. Sie können Library-Routinen also so verwenden, als gehörten sie ausschließlich zu Ihrem Programm.
Sie können eine Library für R-BASIC sowohl mit R-BASIC als auch mit dem PC/GEOS SDK schreiben. Eine mit dem SDK geschriebene Library kann R-BASIC um Funktionen erweitern, die im ursprünglichen Konzept nicht vorgesehen sind. Um eine R-BASIC-Library mit dem SDK zu schreiben müssen Sie das "SDK Library Kit" von der R-BASIC Webseite herunterladen und der dort enthaltenen Anleitung folgen.

Um eine Library mit R-BASIC zu schreiben, gehen Sie folgendermaßen vor:

  1. Öffnen Sie ein neues R-BASIC Programm

  2. Wählen Sie aus dem Menü "Extras" den Punkt "In BASIC Library umwandeln". Es wird ein neues Codefenster "Exports" aktiv. Das Fenster "UI-Objekte" wird deaktiviert, weil R-BASIC Libraries keine statischen UI-Objekte enthalten können.

  3. Speichern Sie die Library unter einem aussagekräftigen Namen. Dieser Name wird später zum Einbinden der Library verwendet. Der Ort, an dem Sie die Library speichern, spielt keine Rolle.

  4. Schreiben Sie den BASIC Code genau so als ob Sie ein Programm erstellen. Vereinbarungen (Anweisungen: DIM, DECL, CONST, STRUCT), die von der Library für andere Programme bereitgestellt werden sollen, müssen in das "Exports" Fenster. Library-interne Vereinbarungen, die nicht exportiert (für andere Programme bereitgestellt werden) werden sollen, sollten Sie im DIM & DATA-Fenster unterbringen.

  5. Compilieren Sie Ihre Library. R-BASIC speichert den compilierten Code im Ordner Userdata\R-BASIC\Library\Bin. Dort können ihn alle BASIC-Programme finden.
Um den Librarycode zu testen, müssen Sie
  • die Library compilieren,

  • ein Programm schreiben, das die Library einbindet (Anweisung: Include "Name der Library") und dort die Library-Routinen aufrufen.

Include

Die Anweisung Include bindet eine Library ein. Jedes Programm kann bis zu 32 Libraries einbinden, wobei auch die Libraries zählen, die von eingebundenen Libraries verwendet werden.

Include erwartet den GEOS Namen der Library, das heißt, die Groß- und Kleinschreibung als auch eventuelle Leerzeichen im Namen müssen beachtet werden.


Syntax: INCLUDE "LibraryName"
Beispiel:
INCLUDE "Demo Library"

customError

Die numerische Systemvariable customError (Datentyp INTEGER) kann benutzt werden um eine selbstdefinierte Fehlernummer zu speichern. Sie kann geschrieben und gelesen werden. Das ist insbesondere für Libraries, auch für SDK-Libraries, interessant. R-BASIC selber verwendet diesen Wert nicht.

Beispiel:
customError = 12
IF customError = 4 THEN  Print "Fehler 4 aufgetreten."
Beispiel:

Wir wollen eine Library schreiben, die zwei Routinen und zwei Konstanten exportiert. Die Funktion Schummel soll eine Zahl von 1 bis 6 liefern, wobei die 6 aber häufiger vorkommt. Die Sub Meldung soll in Abhängigkeit von einer Zahl, die der Routine übergeben wird, zwei unterschiedliche Meldungen ausgeben. Dazu brauchen wir die beiden Konstanten.

Den kompletten Code für dieses Beispiel finden Sie im Ordner "Beispiel\Library", Dateien "Library einfach" und "Library einfach Test Programm".

Im Exports-Fenster vereinbaren wir:

CONST STATE_WIN = 1
CONST STATE_LOSE = 2
DECL FUNCTION Schummel() AS Real
DECL SUB Meldung(state as Real)
Den Code für die beiden Routinen schreiben wir im BASIC-Code Fenster:
FUNCTION Schummel() AS Real
DIM x
  x = 10*Rnd()        ' 0 ... 9.9999999
  x = Int(x)+1        ' 1 ... 10
  IF x > 6 THEN x = 6
  Return x
END Function

SUB Meldung(state as Real)
  IF state = STATE_WIN THEN
    MsgBox "Wow, du hast gewonnen. " \
    + "Das hätte ich dir gar nicht zugetraut."
  ELSE
    WarningBox "Du hast ja so mies gespielt! " \
    + "Kein Wunder, dass du verloren hast."
  END IF
End SUB
Wir speichern die Library unter einem aussagekräftigen Namen, z.B. "Test Lib" und compilieren sie dann. Der Name wird für die Include-Anweisung benötigt.

Unser Testprogramm könnte so aussehen:
Include "Test Lib"
ClassicCode
DIM x
  x = schummel()
  Print x
  IF x > 4 THEN
    Meldung(state_win)
  ELSE
    Meldung(state_lose)
End IF
Die Anweisung ClassicCode bewirkt, dass R-BASIC automatisch einige UI-Objekte anlegt (ein Primary mit einem View und einem BitmapContent), so dass die Print-Anweisung verwendbar ist und außerdem der Code beim Programmstart automatisch abgearbeitet wird (sogenannter klassischer BASIC Modus).

Wenn wir jetzt eine Änderung an der Library in der Library vornehmen, müssen wir sie neu compilieren. Beim nächsten Start unseres Testprogramms wird die geänderte Library verwendet. Es ist dazu nicht nötig (aber trotzdem sinnvoll), die Library zu speichern.

Beim Schreiben einer Library können Sie alles tun, was sie auch beim Schreiben eines normalen Programms tun können. Es ist aber ist folgendes zu beachten:

  • Eine Library kann nicht "gestartet" werden. Es gibt kein "Hauptprogramm", nur eine Sammlung von Unterprogrammen.

  • Bezeichner, die von der Library anderen Programmen zur Verfügung gestellt werden sollen (man sagt: sie sollen "exportiert" werden), müssen im EXPORTS-Fenster vereinbart werden.

    Exportiert werden können:

      -SUB's

      -FUNCTION's

      -ACTION-Handler

      -STRUCT's

      -Konstanten (CONST-Anweisung)

      -Variablen aller Typen (DIM-Anweisung). Diese Variablen werden vom Programm, dass die Library benutzt, als globale Variablen verwendet. Sie können zum Beispiel benutzt werden um Daten zwischen dem Programm und der Library auszutauschen.

  • Nicht exportiert werden können LABEL's.

  • Libraries können keine UI-Objekte enthalten. Das UI-Objekte Fenster ist gesperrt. Weiter unten ist beschrieben, wie Sie trotzdem Objekte in Libraries verwenden können.

  • Eine Library kann eine eigene PictureList haben (siehe Kapitel 2.8.6.2: Verwendung der "Picture-List"). Auf diese PictureList kann nur innerhalb der Library zugegriffen werden. Wenn Sie Bilder aus einer Library-PictureList verwenden wollen, müssen Sie eine Library-Routine schreiben, die auf die PictureList der Library zugreift.

  • DATA Zeilen innerhalb einer Library sind zulässig. Auch hier gilt: Auf diese DATA-Zeilen kann nur von innerhalb der Library zugegriffen werden. DATA Zeilen sollten deshalb im DIM & DATA Fenster stehen.
Wichtig!

Wenn ein Programm eine Routine (Sub, Function, Actionhandler) aus einer Library verwendet, so wird die Routine über eine Nummer identifiziert. Diese Nummer entspricht der Position in der Liste der exportierten Routinen. Dabei zählen nur Routinen. Vereinfacht gesagt zählt R-BASIC die DECL-Anweisungen im EXPORTS Fenster. Das bedeutet konkret:

  • Wenn Sie die Reihenfolge der exportierten Routinen ändern wird die Library inkompatibel. Alle Programme, die diese Library benutzen müssen dann neu compiliert werden.

  • Wenn Sie neue Routinen vor den bereits vorhandenen Routinen einfügen, wird die Library ebenfalls inkompatibel. Sie dürfen neue Routinen im EXPORT-Fenster nur nach allen bereits vorhandenen Routinen einfügen.

  • Für den Export von Structs, Konstanten und Variablen gelten diese Einschränkungen nicht. Hier können Sie beliebig schieben oder hinzufügen.

  • Die genannten Einschränkungen gelten nur für exportierte Routinen. Routinen, die im DIM & DATA Fenster deklariert sind, unterliegen diesen Einschränkungen nicht.
Natürlich wird eine Library auch inkompatibel, wenn Sie die Parameter einer Routine ändern, Konstanten einen anderen Wert geben oder dergleichen.

Beispiel. Das ursprüngliche EXPORT-Fenster sieht so aus:
CONST ZAHL_1 = 12
CONST TEXT_1 = "Hallo Welt"
DECL SUB Routine1 ( )
DECL FUNCTION Routine2(x as REAL) as REAL
Inkompatible Änderung: Reihenfolge vertauscht
CONST ZAHL_1 = 12
CONST TEXT_1 = "Hallo Welt"
DECL FUNCTION Routine2(x as REAL) as REAL
DECL SUB Routine1 ( )
Inkompatible Änderung: Neue Routine eingeschoben
CONST ZAHL_1 = 12
CONST TEXT_1 = "Hallo Welt"
DECL SUB Routine3 (z as INTEGER )
DECL SUB Routine1 ( )
DECL FUNCTION Routine2(x as REAL) as REAL
Kompatible Änderung: Neue Routine angefügt
CONST ZAHL_1 = 12
CONST TEXT_1 = "Hallo Welt"
DECL SUB Routine1 ( )
DECL FUNCTION Routine2(x as REAL) as REAL
DECL SUB Routine3 (z as INTEGER )
Kompatible Änderungen: Konstanten hinzugefügt und verschoben
CONST ZAHL_1 = 12
CONST ZAHL_2 = 559.8

DECL SUB Routine1 ( )
DECL FUNCTION Routine2(x as REAL) as REAL
CONST X_1 = 77
CONST X_2 = 85.7

DECL SUB Routine3 (z as INTEGER )
CONST TEXT_1 = "Hallo Welt"       ' verschieben ist OK
CONST TEXT_2 = "Hallo Jungs"

Die Library-Versionsnummer

Jedes Mal wenn eine neue Version einer Library veröffentlicht wird sollten Sie die Versionsnummer ändern.

Die Werte für Major und Minor können im Menüpunkt "Programm" geändert werden. Der Major-Wert sollte geändert (vergrößert) werden, wenn wesentliche neue Funktionen dazugekommen sind. Ein neuer Wert für Minor sollte Bugfixes oder kleinere Änderungen anzeigen. Insbesondere wenn Sie neue Routinen hinzugefügt haben sollen Sie die Versionsnummer (Major oder Minor) vergrößern.

Programme werden mit neueren Versionen einer Library problemlos zusammenarbeiten, wenn Sie die oben angegebenen Hinweise zur Kompatibilität beachten. Eine ausführliche Beschreibung der Versionsnummern finden Sie im Kapitel 2.11.1.


ConvertObjForSDK

Dies ist eine Hilfsfunktion für Programmierer von SDK Libraries. "Normale" Objektvariablen speichern R-BASIC Objekte in einer Art und Weise, mit der man im SDK nichts anfangen kann. Die Funktion ConvertObjForSDK liefert eine "Objektvariable", die so angepasst wurde, dass man aus einer SDK-Routine heraus mit dem BASIC-Objekt kommunizieren kann.
Syntax: <objVar> = ConvertObjForSDK ( <objExpression> )
Wichtige Hinweise:
  • Verwenden Sie ConvertObjForSDK nur, wenn Sie eine Routine aus einer SDK-Library aufrufen wollen und der Programmierer der SDK-Library hat explizit festgelegt, dass ConvertObjForSDK zu verwenden ist!

  • Nutzen Sie den von ConvertObjForSDK zurückgegebene Wert niemals für eine "normale" Objekt-Routine von R-BASIC, oder GEOS wird crashen!
Technische Details:

R-BASIC Objektvariablen enthalten eine Referenz auf ein Objekt, die nicht identisch mit dem im SDK verwendeten Objekt-Pointer (optr) des Objekts ist. ConvertObjForSDK ersetzt diese BASIC interne Referenz durch den realen optr des Objekts. Weitere Details zu Objektvariablen und wie man ConvertObjForSDK einsetzt finden Sie in der Beschreibung, wie man SDK Libraries erstellt. Diese kann auf der R-BASIC Homepage heruntergeladen werden.

^

2.12.2 UI-Objekte in Libraries

Dieses Kapitel richtet sich an fortgeschrittene Programmierer.

Wie bereits oben erwähnt können Sie in einer Library keinen Objekt-Tree im UI Codefenster anlegen. Es ist jedoch zulässig Objekte zur Laufzeit des Programms anzulegen und später wieder zu vernichten. Die dazu benötigten Befehle CreateObject und DestroyObject sind im Kapitel 2.1.5 (Anlegen und Vernichten von Objekten zur Laufzeit) des Objekthandbuchs beschrieben. Außerdem sollten Sie die Kapitel 2.1.3 (Verwaltung von Objektblöcken, Befehle CreateObjBlock und DestroyObjBlock) sowie 3.3.8 (Hintertürchen für Programmierer, Befehle ObjAddHint und ObjRemoveHint) aus dem Objekthandbuch lesen.

Um eine Library zu schreiben, die Objekte, z.B. eine Dialogbox, bereitstellt, sollten Sie folgendermaßen vorgehen:

  • Schreiben Sie in der Library eine Initialisierungs-Routine (Function mit dem Rückgabetyp Object), die den Objekt-Tree anlegt.

    Die Initialisierungs-Routine sollte das Top-Objekt des gerade erzeugten Objekt-Trees zurückgeben.

  • Schreiben Sie in der Library eine Cleanup-Routine, die von der Library erzeugten Tree vernichtet und den Objektblock wieder freigibt.

  • Sollte es notwendig sein, auf andere Objekte als das Top-Objekt des Objekt-Trees zuzugreifen - was üblicherweise der Fall ist - so sollten Sie Routinen in der Library schreiben, die

      - entweder das gewünschte Objekt zurückgeben, so dass das Programm selbst auf das Objekt zugriefen kann

      - oder die auf das UI-Objekt zugreifen und die gewünschten Daten zurückgeben bzw. setzen.

  • Falls Sie mehrere Objekt-Trees in der Library haben wollen (z.B. mehrere Dialogboxen) sollten Sie auch mehrere Initialisierungs- und Cleanup-Routinen schreiben.
Im Programm, dass die Library nutzt, müssen Sie folgendes tun:
  • Rufen Sie die Initialisierungs-Routine der Library im OnStartup-Handler ihres Programms auf. Nur falls Sie die Library-UI sofort benötigen sollten Sie den OnInit-Handler verwenden. Binden Sie das von der Initialisierungs-Routine zurückgegebene Objekt in den Objekt-Tree Ihres Programms ein, indem Sie die Instancevariable "parent" dieses Objekts belegen.

    Sie sollten die Initialisierungs-Routine der Library nicht rufen, wenn GEOS nach einem Systemneustart mit geöffnetem Programm wieder hochfährt. Beachten Sie dazu das Beispiel unten.

  • Rufen Sie die CleanUp-Routine im OnExit-Handler ihres Programms.

    Sie sollten die Cleanup-Routine der Library nicht rufen, wenn GEOS mit geöffnetem Programm herunterfährt. Beachten Sie dazu das Beispiel unten.

Beispiel

Die folgenden Codefragmente sind an die Beispiele "UI Lib" und "Dialog Library" sowie den dazugehörigen Testprogrammen angelehnt. Die vollständigen Dateien finden Sie im Ordner "Beispiel\Library". Die beiden Beispiele beleuchten unterschiedliche Aspekte des Themas.

Erzeugen eines UI-Trees

- Verwenden Sie CreateObjBlock um einen neuen Objektblock anzulegen und CreateObject um die gewünschten Objekte darin zu erzeugen.

- Setzen Sie die Instance-Variablen der Objekte auf die Werte, die Sie sonst im UI-Code setzen würden. Verwenden Sie den Befehl ObjAddHint, um Instancevariablen zu setzen, die sonst nur im UI-Code verfügbar sind. Vergessen Sie nicht, die Objekte in einem Tree zu verlinken (Instancevariable parent setzen, Children ist read-only).

Die Initialisierungs-Routine. Sie soll folgenden UI-Tree nachbilden:

Dialog LibraryDialog
  Caption$ = "Notizen eingeben"
  Children = DialogClearButton
  End Object

Button DialogClearButton
  Caption$ = "Text löschen"
  ActionHandler = DoClearText
End Object
Dabei muss der Actionhandler "DoClearText" irgendwo in der Library implementiert sein.
FUNCTION BuildLibUI () AS object
dim objBlock as handle
dim dlg, obj as object

    ! Anlegen des Objektblocks, der die Objekte aufnehmen soll
  objBlock = CreateObjBlock()

    ! In diesem Objektblock erzeugen wir die UI-Objekte
    ! und setzen die entsprechenden Instancevariablen
  dlg = CreateObject (objBlock, Dialog)
  dlg.caption$ = "Notizen eingeben"

  ob = CreateObject (objBlock, Button)
  ob.caption$ = "Text löschen"
  ob.actionHandler = DoClearText
  ob.parent = dlg, 0             ' Button in Tree einbinden

Zugriff auf Objekte in der Library

Nur die Library selbst kennt die genaue Struktur des UI-Trees. Deshalb sollten Sie das "Finden" von Objekten als Routine in der Library programmieren. Falls Sie später die Struktur des UI-Trees ändern, müssen Sie nur Code in der Library ändern. Die Programme, welche die Library nutzen, brauchen dann nicht neu compiliert werden.

Die folgende Routine liefert den Button, der oben, in der Routine BuildLibUI, angelegt wurde. Ihr wird das Top-Objekt des Library-Trees übergeben. Dieses ist das einzige, dass das aufrufende Programm wirklich kennt.

FUNCTION LibFindButton (topObj as OBJECT) AS OBJECT
' Wir wissen: Der Button ist das erste Child des Dialog-Objekts
return topObj.Children(0)
END FUNCTION
Im Beispielcode der Library "UI Lib" wird erklärt, wie man von einem ActionHandler aus ein Objekt im UI-Tree der Library finden kann.

Vernichten des UI-Trees

Um das System beim Beenden des Programms sauber zu halten sollten Sie dafür sorgen, dass alle Objekte und Objektblöcke, die Sie angelegt haben, auch wieder vernichtet werden. Die folgende Routine erledigt das für einen kompletten Tree. Ihr wird das Top-Objekt des zu vernichtenden Trees übergeben. Anschließend müssen Sie noch den leeren Objektblock freigeben. Den Code dafür finden Sie in den Beispielen.

Die Routine DestroyTree arbeitet rekursiv. Das heißt, sie ruft sich selbst auf. Rekursive Programmierung ist schwer zu verstehen und schwer zu erklären. Aber sie ist sehr leistungsfähig und der übliche Weg, Baumstrukturen durchzugehen. Sie können diese Routine einfach benutzen. Eine kurze Erklärung finden Sie im Code der Beispiele.

SUB DestroyTree (obj as object)
dim ch as object

WHILE obj.numChildren     ' noch children da?
  ch = obj.children(0)    ' erstes Child holen
  DestroyTree(ch)         ' ... und samt seiner Unter-Children vernichten
                          ' das ehemals zweite Child ist jetzt das erste Child
WEND

' Das Objekt hat jetzt keine Children mehr
obj.parent = NullObj(),0  ' Objekt aus dem Tree nehmen
DestroyObject(obj)        ' Objekt vernichten

END SUB
Beachten Sie, dass diese Beispielroutine nur den regulären Tree vernichtet. Sie prüft nicht ab, ob das zu vernichtende Objekt ein View ist, das ein Content hat. Bei Bedarf können Sie vor dem eigentlichen Vernichten des Objekts die Instancevariable Class$ abfragen. Sie enthält die Objektklasse im Klartext (z.B. "VIEW") und Sie können dann das Content oder den damit verbundenen VisTree vernichten. Die Routine DestroyTree arbeitet auch für den VisTree.

Verwendung im Programm

Im Programm müssen Sie den Library UI-Tree anlegen (am Programmstart) und wieder vernichten (am Programmende). Dazu verwenden Sie die OnStartup und OnExit Handler des Application-Objekts. Die einfachste Variante sieht so aus:
SYSTEMACTION AppStartup
DIM ob as OBJECT
  ob = BuildLibUI()
  ob.parent = DemoPrimary, 0  ' Einbinden in den GenTree
END ACTION
SYSTEMACTION AppExit
DIM libTopObj as Object
  libTopObj = DemoPrimary.Children(0)
  DestroyLibUI(libTopObj)
END ACTION
Jedes Mal, wenn das Programm startet, wird die Library UI erzeugt und bei jedem Programmende wird sie vernichtet. Der wesentliche Nachteil bei diesem einfachen Vorgehen ist, dass das Vernichten und Erzeugen auch passiert, wenn GEOS bei offenem Programm herunterfährt. Das kostet nicht nur Zeit, sondern es gehen auch alle Eingaben (z.B. Texte oder Auswahlfelder), die in der Library UI gemacht wurden, verloren.

Deswegen ist es eine bessere Idee, abzufragen ob GEOS herunterfährt oder nach einem System-Shutdown neu startet. Der Parameter "flags" enthält spezielle Bits, die genau das anzeigen. Das Flag AF_RESTORE ist gesetzt wenn das Programm nach einem System Shutdown wieder öffnet. Das Flag AF_SHUTDOWN ist gesetzt wenn das System herunterfährt, unser Programm aber noch offen ist. Um zu testen, ob das Flag gesetzt ist, verwenden wir die logische Operation AND. Ergibt diese Operation den Wert Null, so ist das Bit nicht gesetzt und wir müssen handeln.

SYSTEMACTION AppStartup
dim ob as object
IF (flags AND AF_RESTORE) = 0 THEN
  ' Das Programm startet neu.
  ' Wir müssen die Library-UI erzeugen.
  ob = BuildLibUI()
  ob.parent = DemoPrimary, 0  ' Einbinden in den GenTree
END IF
END ACTION
SYSTEMACTION AppExit
DIM libTopObj as Object
IF (flags and AF_SHUTDOWN) = 0 THEN
  ' Das Programm wird geschlossen.
  ' Wir müssen die Library-UI vernichten
  libTopObj = DemoPrimary.Children(0)
  DestroyLibUI(libTopObj)   ' Cleanup-Routine
END IF
END ACTION
Es ist möglich, die UI-Objekte der Library in globalen Variablen zwischenzuspeichern. Das erleichtert den Zugriff auf sie. Sie müssen aber beachten, dass globale Variablen einen Systemneustart nicht überleben. Im Beispielcode des Programms "Dialog Library Test Programm" ist beschrieben, wie Sie in diesem Fall vorgehen können.

^

Weiter...