15. Implementieren eines Dokument-Interfaces

^

15.1 Konzeptionelles

Im PC/GEOS SDK wird die Arbeit mit Dokumenten durch drei Objektklassen realisiert. Diese Klassen erzeugen die benötigte UI selbst und arbeiten eng zusammen. Dazu müssen bestimmte Messages in bestimmter Weise gehandelt werden. Das ist so in R-BASIC nicht realisierbar. Deswegen muss sowohl die UI als auch die Routinen mit BASIC-Mitteln nachgebildet werden.

Dieses Kapitel beschreibt, wie man zur Realisierung eines Dokument-Interfaces unter R-BASIC vorgehen muss. Alle hier beschriebenen Routinen und UI-Objekte sind dem Beispiel "Dokument Interface" entnommen, das sich im Ordner "R-BASIC\Beispiel\Objekte\Dateiarbeit" befindet. Dort finden Sie auch die hier aus Platzgründen nicht aufgeführten Objekte.

Um ein eigenes Dokument-Interface zu erstellen, sollten Sie in R-BASIC im Menü "Extras" den Punkt "Code-Sequenz" -> "Dokument-Interface" verwenden. Dort werden die in diesem Kapitel beschriebenen Routinen bzw. UI-Objekte bereitgestellt und automatisch in Ihren Code eingefügt. Dabei können Sie auswählen, ob Sie das komplette hier beschriebene Interface implementieren wollen oder nur Teile davon. Wenn Sie einmal verstanden haben, wie das Prinzip geht, können Sie auch leicht weitere Features, die hier nicht besprochen sind, hinzufügen.

Unser Dokument-Interface soll folgendes leisten:

Standard-Dateioperationen:
  • Neu, Öffnen, Schließen, letzter Stand, Speichern, Speichern unter

  • Quick-Backup: Backup anlegen und aus Backup wieder herstellen.

  • Muster: Als Muster speichern, Muster öffnen

  • Verschieben nach ... , Kopieren nach ..., Umbenennen

  • Bearbeiten der Benutzernotizen

  • Die Funktionen "Import" und "Benutzerebene ändern" werden zur Demonstration vorbereitet, aber nicht implementiert.
Fähigkeiten:
  • Arbeit mit DOS- und GEOS-Dateien (auch mit schreibgeschützten) ist möglich

    Wenn die aktuelle Datei ungeändert ist kann man "Neu" und "Öffnen" anwählen ohne die aktuelle Datei vorher schließen zu müssen.

  • Es kann immer nur eine Datei gleichzeitig offen sein.

  • Das Programm kann darauf reagieren, wenn der Nutzer eine verknüpfte Datei doppelklickt. Für DOS-Dateien muss der Nutzer dazu das Programm über einen entsprechenden Eintrag in der GEOS.INI mit einer Dateierweiterung verknüpfen. Für GEOS Dateien setzt das Programm das Token und das Creator-Token der Datei automatisch.

  • Jedes Mal, wenn eine Applikations-spezifische Operation nötig ist, z.B. Daten in die Datei schreiben, wird statt dessen eine MessageBox aufgerufen.

  • Ein offenes Dokument soll einen Systemrestart "überleben".
Software Entscheidungen:
  • Alle Buttons aus dem Datei-Menü und aus der Toolbar bekommen einen actionData-Wert, der ihre Funktion beschreibt. Ein einziger, zentraler Action-Handler (DocumentAndToolButtonHandler), der von allen Buttons aktiviert wird, ruft dann die entsprechenden Routinen auf.

  • Eine einzige zentrale Routine (DoUpdateDocButtons) ist dafür zuständig die zum Dokument-Interface gehörenden Buttons an den aktuellen Zustand des Dokuments anzupassen.

  • Änderungen werden erst dann in die Datei geschrieben, wenn der Nutzer dies explizit anweist. In diesem Kontext bedeutet der Terminus "das Dokument wurde geändert", dass der Nutzer die Daten im Programm verändert hat. Er bedeutet nicht, dass die Änderungen schon in die Datei geschrieben wurden!

    Wenn Sie VM-Dateien verwenden, dürfen Sie von diesem Prinzip abweichen.


Grundstruktur des Dokument-Interface

Das folgende Bild zeigt alle Routinen, die zur vollständigen Implementation der oben genannten Fähigkeiten notwendig sind.

Bild

Die unten links angeordneten Routinen werden an vielen Stellen aufgerufen. Die Zuordnung erfolgt deshalb nicht durch Pfeile, sondern durch die in den Kästchen vermerkten Buchstaben. Zum Beispiel rufen alle Routinen, die ein "R" im Kästchen haben die Routine DoReadDataFromDoc.

Im Bild sind drei Gruppen von Routinen zu sehen:

  • Routinen, die in Cyan unterlegt sind, sind ActionHandler. Sie werden vom System gerufen und müssen als die passenden Actionhandler in das Application-Objekt eingebunden werden.

  • Routinen, die in Rot unterlegt sind, sind programmspezifisch. Sie müssen diese Routinen anpassen bzw. erweitern, um die Funktionen Ihres Programms zu realisieren.

  • Ob die Routinen, die in Gelb unterlegt sind, geändert / angepasst werden müssen, hängt von der Komplexität Ihres Programms ab.

  • Routinen, die in Grau unterlegt sind, enthalten allgemeingültigen Code. Im Normalfall ist es nicht notwendig, diese Routinen anzupassen.

Bei der Verwaltung von Dokumenten fallen eine Reihe von Aufgaben an, die unabhängig von der Art und der Struktur der eigentlichen Dokumentdatei sind. R-BASIC unterstützt das durch die Bereitstellung einer Objektklasse und einer Library. Beide arbeiten eng zusammen.

Die Objektklasse DocumentGuardian erleichtert Ihnen den Umgang mit Dokumenten, indem Sie allgemeine Informationen, die bei der Arbeit mit Dokumenten anfallen, verwalten. Dazu zählen z.B. der Name und der Pfad zur Dokumentendatei sowie das FileHandle der offenen Datei. Außerdem können Objekte der Klasse DocumentGuardian vorhandenen Dokumente öffnen, neue Dokumente anlegen und offene Dokumente schließen. Dabei berücksichtigen sie z.B. den Dateityp und das Token, behandeln schreibgeschützte Dateien korrekt und vieles mehr. Auf diese Weise entlasten diese Objekte den BASIC-Programmierer von einer Vielzahl von Standardaufgaben.

Die Library DocumentTools stellt eine Reihe von Funktionen bereit, die Sie bei der Arbeit mit Dokumenten unterstützen. Dazu gehören zum Beispiel die typischen Dialogboxen zum Öffnen oder Speichern einer Datei. Um die DocumentTools Library nutzen zu können müssen Sie sie ihn Ihr Programm einbinden:

Include "DocumentTools"
Die DocumentTools Library muss separat heruntergeladen werden, sie ist nicht Teil des R-BASIC Standard-Pakets. Wenn Sie mit VM-Dateien als Dokumente arbeiten wollen müssen Sie außerdem die VMFiles Library herunterladen und includen.

Um die in diesem Kapitel beschriebenen Funktionen und Zusammenhänge zu verstehen, sollten Sie zumindest den einführenden Abschnitt zum DocumentGuardian-Objekt (Objekt Handbuch, Kapitel 4.13), sowie das Kapitel 1 (Konzeptionelles) aus dem Handbuch der DocumentTools Library gelesen haben.

^

15.2 Die UI: Datei-Menü und Toolbar

Das Dateimenü und die Toolbar bestehen aus einer Reihe von Buttons. Die Grafiken für die Buttons finden wir im Ordner "Icon Tool Graphics\Document", einem Unterordner von "USERDATA\R-BASIC\IMAGES".

Bild

Bild

Jeder Button hat als ActionHandler den zentralen Handler DocumentAndToolButtonHandler gesetzt und der actionData-Wert bestimmt die Funktion des Buttons. Aus Platzgründen sind nur einige der Objekte gezeigt.

Group DocumentMenuButtons
  Children = MenuNewOpenButton, MenuOpenButton, MenuCloseButton, MenuSaveButton, 
             MenuSaveAsButton, MenuBackupGroup, MenuOtherGroup
End OBJECT

Button MenuNewOpenButton
  Caption$ = "Neu/Öffnen ", 0
  ActionHandler = DocumentAndToolButtonHandler
  actionData = ID_NEW_OPEN_DIALOG
  BringsUpWindow
End OBJECT

Button MenuOpenButton
  Caption$ = "Öffnen " , 1
  ActionHandler = DocumentAndToolButtonHandler
  actionData = ID_OPEN_DOC
  BringsUpWindow
End OBJECT

...


Einzige Besonderheit im UI-Code der Group DocumentToolButtons ist der Hint ExpandHeight bei der Group und den Buttons selbst, der bewirkt, dass sich die Buttons vergrößern wenn sie gemeinsam mit größeren Objekten in der Group DocumentToolGroup verwendet werden.

Group DocumentToolGroup
  Children = DocumentToolButtons
  ExpandWidth
End OBJECT

Group DocumentToolButtons
  Children = ToolNewButton, ToolOpenButton, ToolTemplateButton,
             ToolCloseButton, ToolSaveButton, ToolBackupButton
  OrientChildren = ORIENT_HORIZONTALLY
  MakeToolbox
  ExpandHeight
End OBJECT

Button ToolNewButton
  CaptionImage = "Icon Tool Graphics\\Document\\NEW.GIF"
  ActionHandler = DocumentAndToolButtonHandler
  actionData = ID_NEW_DOC
  ExpandHeight
End OBJECT

Button ToolOpenButton
  CaptionImage = "Icon Tool Graphics\\Document\\OPEN.GIF"
  ActionHandler = DocumentAndToolButtonHandler
  actionData = ID_OPEN_DOC
  ExpandHeight
End OBJECT

...

^

15.3 Kernroutinen und Tools

Dieser Abschnitt behandelt grundlegende Routinen, die zur Implementation der in den folgenden Abschnitten besprochenen Funktionen benötigt werden. Im Einzelnen sind das
  • der Actionhandler DocumentAndToolButtonHandler. Im Normalfall müssen Sie an dieser Routine nichts ändern. Nur bei sehr komplexen Programmen kann manchmal nötig sein, sie anzupassen. In diese Gruppe gehört auch die Sub DoRevertDoc. Sie wird im Kapitel 15.6 (Letzter Stand) beschrieben.

  • die Subs DoSetDocModified, DoInitDocumentGuardian, DoUpdateDocButtons, DoReadDataFromDoc, DoSaveDataToDoc, DoReadCachedData, DoSaveCachedData und DoEnterDocumentPath. Sie alle müssen angepasst werden, um die Funktionen Ihres Programms zu implementieren.
Der zentrale Handler DocumentAndToolButtonHandler handelt jeden Klick auf einen Button im Datei-Menü. Um die Buttons zu unterscheiden, muss jeder Button einen actionData-Wert gesetzt haben, der seine Funktion beschreibt. Die entsprechenden Konstanten sind in der Library DocumentTools definiert und auch dort beschrieben.

Der Handler ist im Folgenden in Teilen wiedergegeben. In der ON - SWICTH Anweisung werden die Anweisungen entsprechend dem actionData Wert des gedrückten Buttons ausgeführt. Die dort verwendeten Routinen haben häufig selbsterklärende Namen. Sie werden weiter unten ausführlich beschrieben.

Hingewiesen werden soll auf drei Dinge:

  1. Im Fall ID_NEW_OPEN_DIALOG wird nur die NewOpenDialogBox geöffnet, danach ist der Handler beendet. R-BASIC läuft dann solange im Leerlauf bis der Nutzer einen Button aus diesem Dialog anklickt. Das ist so gewollt.

  2. Die Routine BasicCloseDoc schließt das aktuelle Dokument. Sie erwartet einen Parameter, der bestimmt, ob versucht werden soll, eventuell geänderte Daten zu speichern. Sollte es ein Problem dabei geben liefert BasicCloseDoc TRUE zurück und wir können die gewünschte Aktion abbrechen.

    Im Gegensatz zu den meisten anderen Routinen updatet BasicCloseDoc die UI nicht. Deswegen muss das hier erledigt werden. Der Grund dafür ist, dass BasicCloseDoc von verschiedenen anderen Routinen gerufen wird, z.B. wenn ein neues Dokument geöffnet wird.

  3. Wir brauchen keine Abfragen, ob die gewünschte Aktion gerade erlaubt ist. Das erledigt die Routine DoUpdateDocButtons für uns, indem sie Buttons, die gerade nicht erlaubte Aktionen ausführen würden, disabled.
BUTTONACTION DocumentAndToolButtonHandler
DIM err

ON ACTIONData SWITCH
  CASE ID_NEW_OPEN_DIALOG:
    DTShowNewOpenDialog(ConvertObjForSDK(DocumentObj), NOF_NEW_OPEN_TEMPLATE +
    NOF_CONFIG + NOF_IMPORT, "")
  END CASE

  CASE ID_NEW_DOC:
    BasicCreateNewDoc
  END CASE

  CASE ID_OPEN_DOC:
    BasicOpenDoc
  END CASE

  CASE ID_CLOSE_DOC:
    err = BasicCloseDoc(TRUE)   ! Änderungen speichern
    IF err THEN RETURN
    DoReadDataFromDoc           ' UI updaten
    DoUpdateDocButtons
  End CASE

  CASE ID_SAVE_DOC:
    BasicSaveDoc
  END CASE

  ... usw. ...
END SWICTH

END ACTION

Die Routine DoSetDocModified ermöglicht die Verwaltung der Information, ob das aktuelle Dokument geändert wurde oder nicht. Das ermöglicht z.B. der Routine BasicCloseDoc den Nutzer zu fragen, ob er die Änderungen speichern möchte oder nicht. Für diesen Zweck verwaltet das DocumentGuardian-Objekt das Bit DOCS_MODIFIED in seiner Instancevariablen documentState. Der Parameter "modi" bestimmt, ob das Dokument als "modifiziert" (modi = TRUE) oder als "nicht modifiziert" (modi = FALSE) gekennzeichnet werden soll. Entsprechend setzt die Methode SetDocumentState dieses Bit oder setzt es zurück. Abschließend wird mit DoUpdateDocButtons die UI angepasst.

Sie sollten die Routine DoSetDocModified jedes Mal rufen, wenn der Nutzer Daten des Dokuments ändert. Typische Situationen sind der OnModified-Handler von Text-Objekten, die Apply-Handler der Objektklassen Number, Memo, InputLine sowie der Listenobjekte oder der ColorChangedHandler von ColorSelector-Objekten. Die Routine fragt jeweils selbst ab, ob sich der "modifiziert"-Zustand geändert hat und kehrt sofort zurück, wenn dem nicht so ist.

Wichtig: Nehmen wir an, Sie haben ein Text-Objekt (Memo, InputLine), und das Dokument soll "modifiziert" sein, wenn der Nutzer etwas eingibt.

Text-Objekte senden ihre OnModified-Message aber nur aus, wenn der Nutzer "erstmalig" etwas eingibt, das heißt wenn sie vom Zustand "nicht modifiziert" in den Zustand "modifiziert" wechseln. Deswegen müssen Sie den "modified" Zustand der betroffenen Textobjekte zurücksetzen, wenn DoSetDocModified mit dem Parameter modi=FALSE gerufen wird.

Der Apply-Handler von Listen- und Number-Objekten wird im Allgemeinen jedes Mal aufgerufen, wenn er Nutzer etwas ändert. Deswegen ist es normalerweise nicht nötig, den Modified-Zustand dieser Objekte hier zu ändern.

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 "nicht modified"? => Return
    IF (DocumentObj.documentState AND DOCS_MODIFIED) = 0 THEN RETURN
    DocumentObj.SetDocumentState 0, DOCS_MODIFIED
  End IF

  ' -X- Bei Bedarf: weitere Aktionen.
  ' -> den "Modified" Status von Memo und InputLine,
  '    manchmal auch von Number und Listen-Objekten anpassen
  ' Sie sollten hier NICHT DoReadDataFromDoc rufen
  DoUpdateDocButtons

END SUB

Die Sub DoInitDocumentGuardian initialisiert das DocumentGuardian-Objekt mit den Daten, die Ihrem Programm entsprechen. Im Beispiel soll das DocumentGuardian-Objekt eine GEOS Datendatei (fileType = GFT_DATA) mit dem Token "PHO2", 5 und dem CreatorToken "PHON",5 verwalten. Es ist wichtig, dass das hier angegeben Token mit dem DocToken-Statement und das CreatorToken mit dem AppToken-Statement des Application-Objekts im UI-Code übereinstimmen.

Mit der Anweisung "guardian.ConfigData = dc" werden die Einstellungen an das DocumentGuardian-Objekt übertragen.

Der ButtonHandler wird von der DocumentTools-Library für den "Neuen/Öffnen" Dialog benötigt.

SUB DoInitDocumentGuardian(guardian as object)
DIM dc AS DocumentConfigStruct

  guardian.ButtonHandler = DocumentAndToolButtonHandler

  dc.noDocumentString$ = "kein Dokument"
  dc.templateFolder$ = "DemoTemplates"
  dc.nameForNew$ = "Unbenanntes Dokument "

  dc.fileType = GFT_DATA
  dc.creatorToken.tokenChars = "PHON"
  dc.creatorToken.manufid = 5
  dc.token.manufid = 5
  dc.token.tokenChars = "PHO2"
  dc.matchFlags = DOC_MATCH_TOKEN

  guardian.ConfigData = dc

END SUB

Eine der wichtigsten Routinen ist die Sub DoUpdateDocButtons. Sie hat die Aufgabe, die Dokument-Buttons aus dem Datei-Menü und aus der Dokument-Toolbar sowie ihre eigene UI entsprechend der aktuellen Situation zu enablen oder zu disablen. Dadurch sorgt diese Routine dafür, dass einzelne Funktionen nur dann aufgerufen werden können, wenn sie zulässig sind. Zum Beispiel wird der "Speichern" Button disabled, wenn keine Datei offen ist. Im Allgemeinen müssen Sie diese Routine ergänzen. Ein typischer Fall ist, dass Sie UI Objekte haben (z.B. ein Memo-Objekt), in die der Nutzer Daten für das aktuelle Dokument eingeben kann. Diese UI muss disabled werden, wenn kein Dokument offen ist.

Die Entscheidung, ob ein Button enabled sein soll oder nicht kann komplex und unübersichtlich sein. Deswegen stellt die DocumentTools Library die Routine DTFindEnabled bereit, die genau diese Information liefert. Welcher Button gemeint ist wird über eine Konstante bestimmt, die in der Library definiert ist. Sie wird auch für den actionData-Wert des Buttons benutzt wird. Der folgende Code zeigt die Routine in Ausschnitten. DTFindEnabled erwartet eine Referenz auf das DocumentGuardian-Objekt, die mit der BASIC-Routine ConvertObjForSDK konvertiert wurde. Die Variable docObj speichert die konvertierte Referenz auf das Objekt.

Die Anweisung "DemoPrimary.Caption2$ = DocumentObj.documentName$" bewirkt, dass der Name des aktuell offenen Dokuments in der Titelzeile des Primary-Objekts angezeigt wird. Hier müssen Sie den Namen ihres eigenen Primary-Objekts verwenden.

SUB DoUpdateDocButtons ()
DIM docObj as Object
DIM docState

  docObj = ConvertObjForSDK(DocumentObj)

  MenuNewOpenButton.enabled = DTFindEnabled(docObj, ID_NEW_OPEN_DIALOG)
  MenuOpenButton.enabled = DTFindEnabled(docObj, ID_OPEN_DOC)
  MenuSaveAsButton.enabled = DTFindEnabled(docObj, ID_SAVE_AS_DOC)
  MenuCloseButton.enabled = DTFindEnabled(docObj, ID_CLOSE_DOC)
  ToolNewButton.enabled = DTFindEnabled(docObj, ID_NEW_DOC)
  ToolOpenButton.enabled = DTFindEnabled(docObj, ID_OPEN_DOC)
  ToolCloseButton.enabled = DTFindEnabled(docObj, ID_CLOSE_DOC)

  ... usw ...

  DemoPrimary.Caption2$ = DocumentObj.documentName$

  ... hier eigene UI enablen/disablen ...

END SUB

Die Sub DoReadDataFromDoc hat die Aufgabe die im Dokument gespeicherten Daten auszulesen und darzustellen. Weil das immer vom konkreten Programm abhängt erinnert uns eine MsgBox daran, dass wir hier noch etwas programmieren müssen. Wichtig ist, dass wir den Fall "Kein Dokument offen" ebenfalls berücksichtigen. Das können wir durch Abfrage der Instancevariablen documentState auf Null oder wie im Beispiel durch Abfrage der Instancevariablen documentHandle auf NullFile() tun. Häufig werden hier UI-Objekte "geleert", Listen mit Null Einträgen versehen, Texte gelöscht usw.
SUB DoReadDataFromDoc ()

  IF DocumentObj.documentHandle == NullFile() THEN
    MsgBox "DoReadDataFromDoc: Keine Datei offen. UI anpassen."
  ELSE
    MsgBox "DoReadDataFromDoc: Hier Daten aus der Datei lesen und
            zugehörige UI anpassen."
  End IF

END SUB

Die Routine DoSaveDataToDoc wird jedes Mal gerufen, wenn Daten dauerhaft in der Datei gespeichert werden sollen. Für VM-Dateien heißt dass, das VMSave gerufen werden muss. In welche Datei die Daten geschrieben werden sollen wird durch den übergebenen Parameter fh bestimmt. Sie dürfen hier NICHT auf DocumentObj.documentHandle zurückgreifen, weil die Routine je nach Kontext (z.B. erstellen eines Backups) auch für andere Dateien gerufen wird.
SUB DoSaveDataToDoc (fh as FILE)
  MsgBox "DoSaveDataToDoc: Hier Daten in der Datei fh speichern"
  ' bei Bedarf: FileCommit (fh)
END SUB

Beim Herunterfahren von GEOS müssen Dokument-Daten, die in globalen Variablen zwischengespeichert sind, an einem sicheren Platz abgelegt werden. Ein sicherer Speicherplatz für globalen Variablen sind Instancevariablen von Objekten. Diese werden vom System automatisch gesichert. Wenn man genau eine globale Struktur, hat bietet sich die Instancevariable privData des Document- Guardian-Objekts dafür geradezu an.

Die Routine DoSaveCachedData wird vom OnExit handler gerufen und muss die globalen Variablen sichern. Das Gegenstück DoReadCachedData wird vom OnStartup Handler gerufen und muss die globalen Variablen wieder herstellen. Für den Fall, dass Sie keine Dokument-Daten in globalen Variablen zwischenspeichern, können Sie diese Routinen löschen.

SUB DoSaveCachedData (docObj as Object)
  ' Globals Struktur in "privData" speichern
  ' -X- docObj.privData = globalData, sizeof(GlobalDataStruct)
END SUB 'DoSaveCachedData
SUB DoReadCachedData (docObj as Object)
  ' DoReadCachedData: Hier globale Variablen wiederherstellen.
  ' Die UI muss nicht upgedatet werden
  ' -X- globalData = docObj.privData
END SUB 'DoReadCachedData


Das Tool DoEnterDocumentPath wählt den Pfad an, in dem die Dokumente gespeichert werden sollen. Weil das von Ihrem Programm abhängt, müssen Sie diese Routine ändern. Wenn der Parameter forNew TRUE ist, wird der Pfad angewählt, in dem neu angelegte Dokumente gespeichert werden sollen. Häufig ist das der GEOS-Top-Ordner (SP_TOP). Wenn der forNew FLASE ist, wählt die Routine den Ordner an, in dem standardmäßig benannte Dokumente abgelegt werden. Die Anweisung CreateDir stellt sicher, dass der Ordner existiert. CreateDir hat kein Problem damit, wenn der Ordner bereits existiert. Sie können die letzten beiden Statements auskommentieren, um den Dokument-Ordner direkt zu verwenden.

SUB DoEnterDocumentPath (forNew as Integer)

  IF forNew THEN
    SetStandardPath SP_TOP
  ELSE
    SetStandardPath SP_DOCUMENT
    CreateDir "Subfolder"
    SetCurrentPath "Subfolder"
  END IF

END SUB

^

Weiter...