^

15.9 Schnittstelle zum GEOS Dateisystem

Ein Programm, das mit Dokumenten arbeitet, muss mit folgenden Situationen umgehen können:
  • Der Nutzer öffnet im GeoManager ein zu dem Programm gehörendes Dokument. In diesem Fall unterscheidet das System zwei Fälle. Wenn das Programm noch nicht läuft wird es gestartet und der OnStartup-Handler (in unserem Fall die Routine DocStartupHandler) des Programms wird ausgeführt. Läuft das Programm bereits wird stattdessen der OnConnection-Handler des Programms ausgeführt (in unserem Fall die Routine DocConnectionHandler). In beiden Fällen wird den Handlern der komplette Pfad zur Datei übergeben und das Programm muss in der Lage sein diese Datei zu öffnen. Das erledigen wir mit der Routine OpenExternalFile.

  • Der Nutzer selektiert im GeoManager ein Dokument und wählt den Menüpunkt "Drucken". Diese Situation wird nicht hier, sondern im Objekthandbuch, beim PrintControl-Objekt, Kapitel 4.14.8, besprochen.

  • Der Nutzer schließt das Programm während noch ein Dokument offen ist. Dann muss der OnExit-Handler (in unserem Fall die Routine DocExitHandler) dafür sorgen, dass die Dokumentendatei geschlossen wird. Falls erforderlich muss der Nutzer vorher gefragt werden, ob er Änderungen in der Datei speichern will.

  • GEOS fährt bei offenem Programm herunter. Auch hier ist der OnExit-Handler gefragt. Er muss dafür sorgen, dass die Datei so geschlossen wird, dass sie beim Wiederhochfahren automatisch geöffnet werden kann. Diese Aufgabe delegieren wir an das DocumentGuardian-Objekt. Vorher müssen wir eventuell in globalen Variablen gecachte Daten sichern.

  • Wenn GEOS wieder hochfährt, muss die beim Herunterfahren geschlossene Datei automatisch wieder geöffnet werden. Auch das delegieren wir komplett an das DocumentGuardian-Objekt. Danach müssen wir gegebenenfalls globale Variablen wieder herstellen.
Alle drei Handler müssen wie folgt im UI-Code als Handler des Application-Objekts vereinbart werden. Wenn Sie bereits einen entsprechenden Handler haben reicht es, wenn Sie den Code in den bereits definierten Handler verschieben.
Application MyAppObject
  OnStartup = DocStartupHandler
  OnExit = DocExitHandler
  OnConnection = DocConnectionHandler
  ...
End Object
Außerdem benutzen wir die Tatsache, dass alle Instancevariablen von GenericClass Objekten eine Systemrestart automatisch überleben. Das DocumentGuardian-Objekt merkt sich also z.B. den Namen und den Pfad der aktuell offenen Datei automatisch, ohne unser Zutun. Wenn wir weiterhin darauf achten, dass alle geänderten Daten des Dokument in irgendwelchen Instancevariablen gespeichert sind (das ist z.B. automatisch der Fall beim Text eines Memo-Objekts, dem ausgewählten Eintrag einer Liste oder der aktuellen Farbe bei einem ColorSelector), stehen uns diese Daten nach einem Systemneustart automatisch wieder zur Verfügung. Kümmern müssen Sie sich nur, wenn Sie Daten in globalen Variablen haben. Diese könnten Sie z.B. in der Instancevariablen "documentUserData" des DocumentGuardian-Objekts "retten".

Um eine DOS-Datei mit einem Programm zu verknüpfen muss in der GEOS.INI in der Kategorie [filemanager] unter "fileNameTokens" ein Eintrag der Form

*.EXT="DTOK", 0, "AppT", 16600
existieren, wobei *.EXT die DOS-Datei beschreibt, "DTOK", 0 das Token ist, das der Geomanager zur Anzeige der Datei verwenden soll und "AppT", 16600 das Token unseres Programms ist.

R-BASIC unterstützt das Setzen eines solchen Eintrags nicht. Der Nutzer muss das selbst tun, z.B. mit Hilfe des Voreinstellungs-Moduls.

Für GEOS- und VM-Dateien müssen wir nicht in die GEOS.INI eingreifen. Wir müssen der Datei nur ein CreatorToken zuweisen. Damit weiß das System zu welchem Programm die Datei gehört. Sinnvoller Weise geben wir der Datei auch noch ein eigenes Token. Das alles erledigt das DocumentGuardian-Objekt für uns. Wir müssen nur Token und CreatorToken bei der Konfiguration des Objekts angeben (siehe Kapitel 15.3, Routine DoInitDocumentGuardian).

Der OnStartup-Handler

Wie oben beschrieben, muss der OnStartup-Handler (er heißt DocStartupHandler) unterscheiden, ob das Programm neu startet oder ob GEOS gerade wieder hochfährt. Außerdem muss er wissen, ob eine Datendatei (Dokument) übergeben wurde oder nicht. Die entsprechende Information ist im Parameter 'flags' zu finden. Ist das Bit AF_RESTORE gesetzt, fährt GEOS nach einem Shutdown wieder hoch. Das Bit AF_DATA_FILE ist gesetzt, wenn eine Datei an den Handler übergeben wurde. Das ermöglicht uns folgendes Vorgehen:
  • Wir prüfen zunächst das Bit AF_RESTORE. Ist es gesetzt, rufen wir die Methode HandleRestart des DocumentGuardian-Objekts. Diese erledigt die notwendigen Schritte. DoReadCachedData stellt bei Bedarf die globalen Variablen wieder her.

  • Ist das Bit AF_RESTORE nicht gesetzt, startet das Programm gerade neu. In diesem Fall müssen wir unbedingt das DocumentGuardian-Objekt initialisieren. Das erledigt die weiter oben beschriebene Routine DoInitDocumentGuardian.

  • Falls eine Datendatei übergeben wurde (das Bit AF_DATA_FILE ist gesetzt), öffnen wir diese mit OpenExternalFile, andernfalls blenden wir mit DTShowNewOpenDialog den 'Neu/Öffnen' Dialog ein. DTShowNewOpenDialog wartet nicht bis der Nutzer etwas eingibt, sondern öffnet nur den Dialog und kehrt dann zurück, so dass die Startup-Sequenz des Programms fortgesetzt werden kann. Der Nutzer kann irgendwann später einen Button im 'Neu/Öffnen' Dialog anklicken und die DocumentTools Library ruft dann, wie vorn beschrieben, den DocumentAndToolButtonHandler.
SYSTEMACTION DocStartupHandler

  IF flags AND AF_RESTORE THEN
    DocumentObj.HandleRestart
    RETURN
  End IF

  DoInitDocumentGuardian(DocumentObj)

  IF flags AND AF_DATA_FILE THEN
    OpenExternalFile(dataFile$)
  ELSE
    DTShowNewOpenDialog(ConvertObjForSDK(DocumentObj),
      NOF_STARTUP + NOF_NEW_OPEN_TEMPLATE + NOF_CONFIG + NOF_IMPORT, "")
  End IF

  END ACTION

Der OnExit-Handler

Der OnExit-Handler namens DocExitHandler hat nichts zu tun, wenn gar kein Dokument offen ist. Das wird deswegen zuerst abgefragt.

Ist ein Dokument offen, muss er zwischen zwei Fällen unterscheiden:

  • GEOS fährt herunter. In diesem Fall ist das Bit AF_SHUTDOWN im Parameter 'flags' gesetzt. Dann sichern wir die wichtigen globalen Variablen (mit DoSaveCachedData) und rufen die Methode HandleShutdown des Document- Guardian-Objekts. Diese erledigt die notwendigen Schritte. Dazu gehört vor allem, die Datei zu schließen, sich aber den Namen und den Pfad zu merken, so dass die Methode HandleRestart genau diese Datei wieder öffnen kann.

  • Das Programm schließt. In diesem Fall muss das Dokument geschlossen werden. Vorher müssen wir den Nutzer fragen, ob er eventuelle Änderungen speichern will. Dazu verwenden wir die im Kapitel 15.4.5 (Schließen des Dokuments) beschriebene Funktion DTConfirmClose. Der Parameter 'FALSE' bewirkt, dass der Nutzer die Option 'Abbrechen' nicht hat. Je nach zurückgegebenem Wert rufen wir BasicSaveDoc (CLOSE_SAVE), BasicSaveAsDoc (CLOSE_SAVE_AS) oder nichts davon (CLOSE_DISCARD, Änderungen verwerfen).
    Abschließend können wir mit der Methode CloseDocument die Datei schließen.
SYSTEMACTION DocExitHandler
DIM cmd

  IF DocumentObj.documentHandle = NullFile() THEN RETURN
  IF flags AND AF_SHUTDOWN THEN
    DocSaveCachedData (DocumentObj)
    DocumentObj.HandleShutdown
    RETURN
  End IF

  cmd = DTConfirmClose(ConvertObjForSDK(DocumentObj), FALSE)
  ON cmd SWITCH
  CASE CLOSE_SAVE:        ' Änderungen speichern
    BasicSaveDoc' Handelt alle denkbaren Fälle
  END CASE
  CASE CLOSE_SAVE_AS:
    BasicSaveAsDoc()
  END SWITCH

  DocumentObj.CloseDocument

END ACTION

Der OnConnection-Handler

Der OnConnection-Handler (er heißt DocConnectionHandler) wird gerufen, wenn der Nutzer ein Dokument im Geomanager doppelklickt, das zugehörige Programm aber schon läuft. Der vollständige Pfad zu diesem Dokument wird dem Handler im Parameter dataFile$ übergeben. Da es nicht auszuschließen ist, dass GEOS den Handler auch in anderen Zusammenhängen ruft, fragen wir das Bit AF_DATA_FILE ab, bevor wir OpenExternalFile zum Öffnen der Datei rufen.
SYSTEMACTION DocConnectionHandler

  IF flags AND AF_DATA_FILE THEN
    OpenExternalFile(dataFile$)
  End IF

END ACTION

OpenExternalFile

Die Routine OpenExternalFile erledigt alles was nötig ist um eine Datei zu öffnen, die einem der Handler DocStartupHandler oder DocConnectionHandler übergeben wurde.
SUB OpenExternalFile (file$ as string(235))
DIM err, n, state
DIM fileName$ as String(32)
DIM path$ as String(235)

  state = DocumentObj.documentState
   IF state THEN
    err = BasicCloseDoc(True)
    IF err THEN RETURN
  END IF

  path$ = file$
  n = InStr("\\", path$)
  WHILE n <> 0
    path$ = Right$(path$, len(path$) - n)
    n = InStr("\\", path$)
  WEND

  fileName$ = path$
  path$ = left$(file$, len(file$) - len(fileName$) - 1)

  SetCurrentPath path$
  DocumentObj.OpenDocument fileName$

  DoReadDataFromDoc
  DoUpdateDocButtons

END SUB
Zunächst prüfen wir ob noch eine Datei offen ist (die Variable state ist dann ungleich Null). In diesem Fall erledigt BasicCloseDoc das Schließen der Datei mit vorheriger Nachfrage beim Nutzer. Entscheidet sich der Nutzer die Datei doch nicht zu schließen liefert BasicCloseDoc TRUE und wir verlassen die Routine OpenExternalFile.

Der nächste Schritt ist das Separieren von Pfad und Dateinamen. Dafür verwenden wir die lokale Variable path$ denn der Parameter file$ wird später noch gebraucht. Die WHILE Schleife sucht jeweils den nächsten Backslash. Aus "C:\GEOS\DOCUMENT\NAME.EXT" wird so schrittweise "GEOS\DOCUMENT\ NAME.EXT", "DOCUMENT\NAME.EXT" und schließlich "NAME.EXT". Das ist der Dateiname, also speichern wir ihn in fileName$. Der Pfad ist dann alles links davon, mit Ausnahme des letzten Backslash-Zeichens.

Jetzt können wir mit SetCurrentPath in den richtigen Ordner wechseln und mit der Methode OpenDocument das Dokument öffnen. Abschließend rufen wir DoReadDataFromDoc und DoUpdateDocButtons um die UI zu updaten.

^

15.10 Ein einfaches Beispiel

Am Beispiel der Programms "Yellow Notes", das im Ordner "Beispiele\Objekte \Dateiarbeit" gefunden werden kann, soll gezeigt werden, wie man die zum Dokumentinterface gehörende Routinen an das eigene Programm anpassen kann.

Kernobjekte des Programms sind ein Textobjekt (YNotesText) und ein Menu (YNotesColorMenu) mit zwei ColorSelektoren (YNotesTextColor und YNotesBackColor) für die Vordergrund- und die Hintergrundfarbe.

Sehr häufig ist es sinnvoll, die Dokumentdaten in einer Struktur zu speichern. Das vereinfacht den Zugriff auf die Daten und deren Verwaltung, insbesondere das Speichern in einer Dokumentdatei, enorm. Für das "Yellow-Notes"-Beispiel benötigen wir die Farben von Text und Hintergrund sowie den Notiztext selbst. Außerdem haben wir 8 Word Reserve vorgesehen, die wir später zur kompatiblen Erweiterung des Programms verwenden können.

STRUCT NotesData
  backcolor, textColor as word
  reserve[8] as word
  text as String(1024)
End STRUCT
Als Dokumentdatei wählen wir eine GEOS Datendatei. Eine DOS-Datei sollte man nur verwenden, wenn es erforderlich ist. Der Zugriff auf eine GEOS Datendatei ist genau so einfach wie der auf eine DOS-Datei, aber man kann ein Token und ein CreatorToken vergeben, so dass die Verknüpfung mit dem zugehörigen Programm ohne Eingriff in die GEOS.INI erfolgt.

Wie am Anfang des Kapitels beschrieben beschränkt sich die Anpassung des Dokument-Interfaces auf die folgenden Routinen:

  • DoInitDocumentGuardian

  • BasicCreateNewDoc

  • DoUpdateDocButtons

  • DoReadDataFromDoc

  • DoSaveDataToDoc

  • DoEnterDocumentPath

  • DoSetDocModified

  • DoRevertDoc


In der Routine DoInitDocumentGuardian müssen alle das Dokument betreffenden Daten an das eigene Programm angepasst werden. Wichtig ist, dass der Wert für CreatorToken dem AppToken-Statement im UI-Code des Application-Objekts entspricht.

SUB DoInitDocumentGuardian(guardian as object)
DIM dc as DocumentConfigStruct

  guardian.buttonhandler = DocumentAndToolButtonHandler

  dc.noDocumentString$ = "leer"
  dc.nameForNew$ = "Notiz "

  dc.fileType = GFT_DATA
  dc.creatorToken.tokenChars = "YNot"
  dc.creatorToken.manufid = 16600
  dc.token.tokenChars = "YNOD"
  dc.token.manufid = 16600

  dc.matchFlags = DOC_MATCH_TOKEN
  guardian.ConfigData = dc

END SUB


In der Routine BasicCreateNewDoc muss der Teil angepasst werden, der für das Initialisieren des neu angelegten Dokuments zuständig ist. Im Yellow Notes Beispiel wird dazu die Sub YNotesInitializeDocument aufgerufen. Sie belegt eine NotesData-Struktur mit den Standardfarben und einem leeren Text und schreibt sie dann in die neu angelegt Datei. Token und CreatorToken werden automatisch vom DocumentGuardian-Objekt gesetzt.

SUB YNotesInitializeDocument ()
DIM notes as NotesData

  notes.textColor = BLACK
  notes.backColor = YELLOW
  notes.text = ""

  FileSetPos DocumentObj.documentHandle , 0
  FileWrite DocumentObj.documentHandle , notes, sizeof(NotesData)

END SUB


Die Routine DoUpdateDocButtons hat die Aufgabe, die UI des Programms zu enablen oder zu disablen, je nachdem ob ein Dokument offen ist und welchen Status es hat. Im Yellow Notes Beispiel ruft sie dazu die SUB YNotesUpdateUI, die im Folgenden gezeigt ist. DocumentObj.documentState ist ungleich Null, wenn ein Dokument offen ist. Dann wird die UI enabled, ansonsten wird sie disabled.

SUB YNotesUpdateUI ()

  IF DocumentObj.documentState THEN
    YNotesText.enabled = TRUE
    YNotesColorMenu.enabled = TRUE
  ELSE
    YNotesText.enabled = FALSE
    YNotesColorMenu.enabled = FALSE
  End IF
  
END SUB


Die Routine DoReadDataFromDoc wird jedes Mal gerufen, wenn Daten aus der Datei gelesen werden sollen oder ein Dokument geschlossen wurde. Sie liest die NotesData-Struktur aus der Datei und verteilt die Informationen an die entsprechenden UI-Objekte. Falls keine Datei offen ist muss sie dafür sorgen, dass das Objekt YNotesText leer ist. Das Enablen bzw. Disablen der UI-Objekte übernimmt die Routine DoUpdateDocButtons.

SUB DoReadDataFromDoc ()
DIM notes as NotesData

  IF DocumentObj.documentHandle == NullFile() THEN
    YNotesText.text$ = ""
  ELSE
    ' Text und Farben updaten
    FileSetPos DocumentObj.documentHandle, 0
    notes = FileRead DocumentObj.documentHandle,sizeof(NotesData)
    YNotesText.textColor = notes.textColor
    YNotesText.backColor = notes.backColor
    YNotesText.text$ = notes.text
    YNotesTextColor.csColor = notes.textColor
    YNotesBackColor.csColor = notes.backColor
  End IF

END SUB


Die Routine DoSaveDataToDoc schreibt die aktuellen Daten in die Datei, indem sie die Struktur "notes" mit den aktuellen Werten, gelesen vom Textobjekt und den ColorSelektoren, belegt und sie dann mit einem einzigen FileWrite in die Datei schreibt. Wir dürfen natürlich nicht vergessen vorher mit FileSetPos die korrekte Schreibposition anzuwählen.

SUB DoSaveDataToDoc (fh as FILE)
DIM notes AS NotesData

  notes.textColor = YNotesText.textColor
  notes.backColor = YNotesText.backColor
  notes.text = YNotesText.text$
  FileSetPos fh, 0
  FileWrite fh, notes, sizeof(NotesData)
  
END SUB


DoEnterDocumentPath wechselt in dem Pfad, in dem die Dateien angelegt werden sollen. Wir entscheiden uns bei neuen Dateien für den GEOS-Top-Ordner und für die Notiz-Dateien selbst für den Document-Ordner ohne Unterordner.

SUB DoEnterDocumentPath (forNew as Integer)

  IF forNew THEN
    SetStandardPath SP_TOP
  ELSE
    SetStandardPath SP_DOCUMENT
  END IF
  
END SUB


In der Routine DoSetDocModified gibt es eine wichtige Anpassung: Wir müssen sicherstellen, dass nach dem Speichern des Dokuments (in diesem Fall wird DoSetDocModified automatisch mit dem Parameter FALSE aufgerufen) die nächste Nutzereingabe wieder den OnModified Handler des Textobjekts ruft. Dazu setzen wir den modified-Status des Textobjekts zurück.

SUB DoSetDocModified (modi as INTEGER)

  IF modi THEN
    ' ist schon "modified"? => Return
    IF DocumentObj.documentState AND DOCS_MODIFIED THEN RETURN
    DocumentObj.SetDocumentState DOCS_MODIFIED, 0
  ELSE
    ' ist schon "not modified"? => Return
    IF (DocumentObj.documentState AND DOCS_MODIFIED) = 0 THEN RETURN
    DocumentObj.SetDocumentState 0, DOCS_MODIFIED
    YNotesText.modified = FALSE
  End IF

  DoUpdateDocButtons

END SUB


Da sich das Zurücksetzen des Dokuments auf den letzten gespeicherten Stand in unserem einfachen Konzept darauf beschränkt, die die Dokument-Daten aus der (ungeänderten) Datei wieder auszulesen, hat die Routine DoRevertDoc nichts zu tun. Sie könnte komplett aus dem Code entfernt werden.

SUB DoRevertDoc ()
END SUB


Zusätzlich benötigt das Yellow Notes Beispiel ein paar weitere Routinen. Die wichtigsten davon sind der OnModified-Handler des YNotesText-Objekts und der ColorChangedHander der beiden ColorSelektoren, da diese das Dokument als "modified" markieren müssen, wenn der Nutzer etwas ändert.

Der OnModified Handler des Textobjekts ist sehr einfach. Er ruft nur DoSetDocModified (TRUE). Diese Routine informiert das DocumentGuardian-Objekt und ruft DoUpdateDocButtons. Mehr ist nicht zu tun.

TEXTACTION TextModifedHandler
  DoSetDocModified (TRUE)
END ACTION


Beide ColorSelektoren haben den gleichen ColorChangedHander. Seine Aufgabe ist es, dem Text-Objekt eine neue Vorder- und Hintergrundfarbe zuzuweisen sowie das Dokument als "geändert" zu markieren.

ColorSelector Objekte haben die Eigenart, dass der Handler öfter gerufen wird, als es für unsere Zwecke sinnvoll ist, z.B. wenn sie erstmalig auf dem Schirm erscheinen. Das könnte dazu führen, dass das Dokument als "geändert" markiert wird, obwohl es eigentlich nicht geändert wurde. Deswegen fragen wir die aktuellen Farben des Textobjekts ab und rufen DoSetDocModified (TRUE) nur dann, wenn sie sich wirklich geändert haben.

COLORACTION NewColorHandler
dim tc, bc

  tc = YNotesText.textColor
  bc = YNotesText.backColor

  YNotesText.textColor = YNotesTextColor.csIndexColor
  YNotesText.backColor = YNotesBackColor.csIndexColor

  IF (tc <> YNotesText.textColor) OR (bc <> YNotesText.backColor) THEN
    DoSetDocModified (TRUE)
  End IF
  
END ACTION
Den kompletten Quellcode für dieses Beispiel sowie die Iconeditor-Datei mit den Iconbildern findet man im Ordner "Beispiele\Objekte\Dateiarbeit".

^

Weiter...