Base Handbuch

Kapitel 9
Makros

LibreOffice 24.2

Copyright

Dieses Dokument unterliegt dem Copyright © 2024. Die Beitragenden sind unten aufgeführt. Sie dürfen dieses Dokument unter den Bedingungen der GNU General Public License (http://www.­gnu.org/licenses/gpl.html), Version 3 oder höher, oder der Creative Commons Attribution License (http://creativecommons.org/licenses/by/3.0/), Version 3.0 oder höher, verändern und/oder weitergeben.

Warennamen werden ohne Gewährleistung der freien Verwendbarkeit benutzt.

Fast alle Hardware- und Softwarebezeichnungen und weitere Stichworte und sonstige Angaben, die in diesem Buch verwendet werden, sind als eingetragene Marken geschützt.

Da es nicht möglich ist, in allen Fällen zeitnah zu ermitteln, ob ein Markenschutz besteht, wird das Symbol (R) in diesem Buch nicht verwendet.

Mitwirkende/Autoren

Robert Großkopf

Jost Lange

Jochen Schiffers

Jürgen Thomas

Michael Niedermair

 

Rückmeldung (Feedback)

Kommentare oder Vorschläge zu diesem Dokument können Sie in deutscher Sprache an die Adresse discuss@de.libreoffice.org senden.

Alles, was an eine Mailingliste geschickt wird, inklusive der E-Mail-Adresse und anderer persönlicher Daten, die die E-Mail enthält, wird öffentlich archiviert und kann nicht gelöscht werden. Also, schreiben Sie mit Bedacht!

Datum der Veröffentlichung und Softwareversion

Veröffentlicht am 01.02.2024. Basierend auf der Version LibreOffice 24.2.

Inhalt

Allgemeines zu Makros

Der Makro-Editor

Benennung von Modulen, Dialogen und Bibliotheken

Makros in Base

Makros benutzen

Makros zuweisen

Ereignisse eines Formulars beim Öffnen oder Schließen des Fensters

Ereignisse eines Formulars bei geöffnetem Fenster

Ereignisse innerhalb eines Formulars

Bestandteile von Makros

Der «Rahmen» eines Makros

Variablen definieren

Arrays definieren

Zugriff auf das Formular

Zugriff auf Elemente eines Formulars

Zugriff auf die Datenbank

Die Verbindung zur Datenbank

SQL-Befehle

Vorbereitete SQL-Befehle mit Parametern

Datensätze lesen und benutzen

Mithilfe des Formulars

Ergebnis einer Abfrage

Mithilfe eines Kontrollfelds

Datensätze wechseln und bestimmte Datensätze ansteuern

Datensätze bearbeiten – neu anlegen, ändern, löschen

Inhalt eines Kontrollfelds ändern

Zeile einer Datenmenge ändern

Zeilen anlegen, ändern, löschen

Kontrollfelder prüfen und ändern

Englische Bezeichner in Makros

Eigenschaften bei Formularen und Kontrollfeldern

Schrift

Formular

Einheitlich für alle Arten von Kontrollfeld

Einheitlich für viele Arten von Kontrollfeld

Textfeld – weitere Angaben

Numerisches Feld

Datumsfeld

Zeitfeld

Währungsfeld

Formatiertes Feld

Listenfeld

Kombinationsfeld

Markierfeld, Optionsfeld

Maskiertes Feld

Tabellenkontrollfeld

Beschriftungsfeld

Gruppierungsrahmen

Schaltfläche

Navigationsleiste

Methoden bei Formularen und Kontrollfeldern

In einer Datenmenge navigieren

Datenzeilen bearbeiten

Einzelne Werte bearbeiten

Parameter für vorbereitete SQL-Befehle

Arbeit mit UNO-Befehlen in Formularen

Bedienbarkeit verbessern

Automatisches Aktualisieren von Formularen

Filtern von Datensätzen

Daten über den Formularfilter filtern

Filterdialog über einen Button starten

Durch Datensätze mit der Bildlaufleiste scrollen

Daten aus Textfeldern auf SQL-Tauglichkeit vorbereiten

Beliebige SQL-Kommandos speichern und bei Bedarf ausführen

Werte in einem Formular vorausberechnen

Die aktuelle Office-Version ermitteln

Wert von Listenfeldern ermitteln

Listenfelder durch Eingabe von Anfangsbuchstaben einschränken

Listenfelder mit eingeschränkter Auswahl

Listenfelder zur Mehrfachauswahl nutzen

Datumswert aus einem Formularwert in eine Datumsvariable umwandeln

Eingabemöglichkeiten in Feldern einschränken

Suchen von Datensätzen

Suchen in Formularen und Ergebnisse farbig hervorheben

Rechtschreibkontrolle während der Eingabe

Kombinationsfelder als Listenfelder mit Eingabemöglichkeit

Textanzeige im Kombinationsfeld

Fremdschlüsselwert vom Kombinationsfeld zum numerischen Feld übertragen

Kontrollfunktion für die Zeichenlänge der Kombinationsfelder

Datensatzaktion erzeugen

Navigation von einem Formular zum anderen

Datensatz im Formular direkt öffnen

Tabellen, Abfragen, Formulare und Berichte öffnen

Hierarchische Listenfelder

Filterung des Formulars mit hierarchischen Listenfeldern

Hierarchische Listenfelder in der Formulareingabe nutzen

Zeiteingaben mit Millisekunden

Ein Ereignis – mehrere Implementationen

Eingabekontrolle bei Formularen

Erforderliche Eingaben absichern

Fehlerhafte Eingaben vermeiden

Abspeichern nach erfolgter Kontrolle

Primärschlüssel aus Nummerierung und Jahreszahl

Datenbankaufgaben mit Makros erweitert

Verbindung mit Datenbanken erzeugen

Daten von einer Datenbank in eine andere kopieren

Direkter Import von Daten aus Calc

Zugriff auf Abfragen

Datenbanksicherungen erstellen

Interne Datenbanken sicher schließen

Tabellenindex heruntersetzen bei Autowert-Feldern

Drucken aus Base heraus

Druck von Berichten aus einem internen Formular heraus

Start, Formatierung, direkter Druck und Schließen des Berichts

Druck von Berichten aus einem externen Formular heraus

Serienbriefdruck aus Base heraus

Drucken über Textfelder

Drucken über Tabellen in Writer

Einfacher Druck von Text in Tabellen

Tabellendruck mit formatierten Zellen

Tabellendruck mit Bildern in der Tabelle

Rechnungen mit Übertrag im Tabellendruck

Aufruf von Anwendungen zum Öffnen von Dateien

Aufruf eines Mailprogramms mit Inhaltsvorgaben

Aufruf einer Kartenansicht zu einer Adresse

Mauszeiger ändern

Änderung beim Überfahren eines Links

Änderung bei gedrückter Strg-Taste und Mausklick

Formulare ohne Symbolleisten präsentieren

Formulare ohne Symbolleisten in einem Fenster

Formulare im Vollbildmodus

Formular direkt beim Öffnen der Datenbankdatei starten

Markierfelder durch Schaltflächen ersetzen

MySQL-Datenbank mit Makros ansprechen

MySQL-Code in Makros

Temporäre Tabelle als individueller Zwischenspeicher

Filterung über die Verbindungsnummer

Gespeicherte Prozeduren

Automatischer Aufruf einer Prozedur

Übertragung der Ausgabe einer Prozedur in eine temporäre Tabelle

PostgreSQL und Makros

Autowertrückgabe mit Returning

Datentyp «Array»

Dialoge

Dialoge starten und beenden

Einfacher Dialog zur Eingabe neuer Datensätze

Dialog zum Bearbeiten von Daten in einer Tabelle

Dialog zum Bearbeiten von Daten aus einer Tabellenübersicht

Fortschrittsbalken für den Ablauf mehrerer Prozeduren

Fehleinträge von Tabellen mit Hilfe eines Dialogs bereinigen

Makrozugriff mit Access2Base

Python als Makrosprache für Datenbanken

Speicherort für das erste Python Makro

Speicherort für Module zum Importieren in ein Makro

Verwaltung von Modulen in Bibliotheken

Abfrage an eine geöffnete Datenbankdatei

Abfrage an eine registrierte Datenbank

Fertige Module in die Base-Datei einbinden

Allgemeines zu Makros

Prinzipiell kommt eine Datenbank unter Base ohne Makros aus. Irgendwann kann aber das Bedürfnis kommen,

Es ist natürlich jedem selbst überlassen, wie intensiv er/sie Makros in Base nutzen will. Makros können zwar die Bedienbarkeit verbessern, sind aber auch immer mit geringen, bei ungünstiger Programmierung auch stärkeren Geschwindigkeitseinbußen des Programms verbunden. Es ist immer besser, zuerst einmal die Möglichkeiten der Datenbank und die vorgesehenen Einstellmöglichkeiten in Formularen auszureizen, bevor mit Makros zusätzliche Funktionen bereitgestellt werden. Makros sollten deshalb auch immer wieder mit größeren Datenbanken getestet werden, um ihren Einfluss auf die Verarbeitungsgeschwindigkeit abschätzen zu können.

Makros werden über den Weg Extras → Makros → Makros verwalten → LibreOffice Basic... erstellt. Es erscheint ein Fenster, das den Zugriff auf alle Makros ermöglicht. Makros für Base werden meistens in dem Bereich gespeichert, der dem Dateinamen der Base-Datei entspricht.

 

Über den Button Neu im Fenster «LibreOffice Basic Makros» wird ein zweites Fenster geöffnet. Hier wird lediglich nach der Bezeichnung für das Modul (Ordner, in dem das Makro abgelegt wird) gefragt. Der Name kann gegebenenfalls auch noch später geändert werden.

Sobald dies bestätigt wird, erscheint der Makro-Editor und auf seiner Eingabefläche wird bereits der Start und das Ende für eine Prozedur angegeben:

  1. 1 REM  *****  BASIC  ***** 

  2. 2    

  3. 3 Sub Main 

  4. 4    

  5. 5 End Sub 

Um Makros, die dort eingegeben wurden, nutzen zu können, sind folgende Schritte notwendig:

Einige Grundprinzipien zur Nutzung des Basic-Codes in LibreOffice:

Zu weiteren Details siehe auch das Handbuch «Erste Schritte Makros mit LibreOffice».

Makros in diesem Kapitel sind entsprechend den Vorgaben aus dem Makro-Editor von LibreOffice eingefärbt:
  1. Makro-Bezeichner
    Makro-Kommentar
    Makro-Operator
    Makro-Reservierter-Ausdruck
    Makro-Zahl
    Makro-Zeichenkette

 

Bezeichner können frei gewählt werden, sofern sie nicht einem reservierten Ausdruck entsprechen. Viele Makros sind in dieser Anleitung mit an die deutsche Sprache angelehnten Bezeichnern versehen. Dies führte bei der englischsprachigen Übersetzung allerdings zu zusätzlichen Problemen. Deshalb sind die Bezeichner in neueren Makros an die englische Sprache angelehnt.

Die hier aufgezeigten Makros sind nahezu ausschließlich innerhalb der Base-Datei gespeichert und dort auch getestet. So kann z.B. der Kontakt zu einer Datenbank mit ThisDatabaseDocument nur innerhalb einer Base-Datei hergestellt werden. Sollen die Makros außerhalb der Datei unter Meine Makros und Dialoge gespeichert werden, so kann eventuell statt ThisDatabaseDocument einfach ThisComponent zum Ziel führen. Es kann aber auch sein, dass dann bestimmte Methoden einfach nicht zur Verfügung stehen.

Der Makro-Editor

 

Der Objektkatalog auf der linken Seite zeigt alle zur Zeit verfügbaren Bibliotheken und darin Module an, die über ein Ereignis aufgerufen werden können. Meine Makros & Dialoge ist für alle Dokumente eines Benutzers verfügbar. LibreOffice Makros & Dialoge sind für alle Benutzer des Rechners und auch anderer Rechner nutzbar, da sie standardmäßig mit LibreOffice installiert werden. Hinzu kommen noch die Bibliotheken, die in dem jeweiligen Dokument, hier Medien_mit_Makros.odb, abgespeichert sind.

Prinzipiell ist es zwar möglich, aus allen verfügbaren Bibliotheken die Module und die darin liegenden Makros zu nutzen. Für eine sinnvolle Nutzung empfiehlt es sich aber nicht, Makros aus anderen Dokumenten zu nutzen, da diese eben nur bei Öffnung des entsprechenden Dokumentes verfügbar sind. Ebenso ist es nicht empfehlenswert, Bibliotheken aus «Meine Makros & Dialoge» einzubinden, wenn die Datenbankdatei auch an andere Nutzer weitergegeben werden soll. Ausnahmen können hier Erweiterungen («Extensions») sein, die dann mit der Datenbankdatei weiter gegeben werden.

In dem Eingabebereich wird aus dem Modul Aktualisierung die Prozedur Ausleihe_aktualisieren angezeigt. Eingegebene Zeilen enden mit einem Return. Groß- und Kleinschreibung sowie Einrückung des Codes sind in Basic beliebig. Lediglich der Verweis auf Zeichenketten, z.B. "Filter", muss genau der Schreibweise in dem dort gemeinten Formular entsprechen.

Makros können schrittweise für Testzwecke durchlaufen werden. Entsprechende Veränderungen der Variablen werden im Beobachter angezeigt.

Benennung von Modulen, Dialogen und Bibliotheken

Die Benennung von Modulen, Dialogen und Bibliotheken sollte erfolgen, bevor irgendein Makro in die Datenbank eingebunden wird. Sie definieren schließlich den Pfad, in dem das auslösende Ereignis nach dem Makro sucht.

Innerhalb einer Bibliothek kann auf alle Makros der verschiedenen Module zugegriffen werden. Sollen Makros anderer Bibliotheken genutzt werden, so müssen diese extra geladen werden:

  1. 1 GlobalScope.BasicLibraries.LoadLibrary("Tools") 

lädt die Bibliothek «Tools», die eine Bibliothek von LibreOffice Makros ist.

 

Über Extras → Makros verwalten → LibreOffice Basic → Verwalten kann der obige Dialog aufgerufen werden. Hier können neue Module und Dialoge erstellt und mit einem Namen versehen werden. Die Namen können allerdings nicht hier, sondern nur in dem Makroeditor selbst verändert werden.

 

Im Makroeditor wird mit einem rechten Mausklick auf die Reiter mit der Modulbezeichnung direkt oberhalb der Suchleiste ein Kontextmenü geöffnet, das u.a. die Änderung des Modulnamens ermöglicht.

 

Neue Bibliotheken können innerhalb der Base-Datei angelegt werden. Die Bezeichnung «Standard» der ersten erstellten Bibliothek lässt sich nicht ändern. Die Namen der weiteren Bibliotheken sind frei wählbar, anschließend aber auch nicht änderbar. In eine Bibliothek können Makros aus anderen Bibliotheken importiert werden. Sollte also der dringende Wunsch bestehen, eine andere Bibliotheksbezeichnung zu erreichen, so müsste eine neue Bibliothek mit diesem Namen erstellt werden und sämtlicher Inhalt der alten Bibliothek in die neue Bibliothek exportiert werden. Dann kann anschließend die alte Bibliothek gelöscht werden.

Bei der Bibliothek «Standard» ist es nicht möglich, ein Kennwort zu setzen. Sollen Makros vor den Blicken des normalen Nutzers verborgen bleiben, so muss dafür eine neue Bibliothek erstellt werden. Diese lässt sich dann mit einem Kennwort schützen.

Makros in Base

Makros benutzen

Der «direkte Weg» über Extras → Makros → Makros ausführen ist zwar auch möglich, aber bei Base-Makros nicht üblich. Ein Makro wird in der Regel einem Ereignis zugeordnet und durch dieses Ereignis gestartet.

Der «direkte Weg» ist vor allem dann nicht möglich auch nicht zu Testzwecken , wenn eines der Objekte thisComponent (siehe den Abschnitt Zugriff auf das Formular) oder oEvent (siehe den Abschnitt Zugriff auf Elemente eines Formulars) benutzt wird.

Makros zuweisen

Damit ein Makro durch ein Ereignis gestartet werden kann, muss es zunächst definiert werden (siehe den einleitenden Abschnitt Allgemeines zu Makros). Dann kann es einem Ereignis zugewiesen werden. Dafür gibt es vor allem zwei Stellen.

Ereignisse eines Formulars beim Öffnen oder Schließen des Fensters

Maßnahmen, die beim Öffnen oder Schließen eines Formulardokuments erledigt werden sollen, werden so registriert:

 

Dann kann diese Zuweisung mit OK bestätigt werden.

Die Einbindung von Makros in das Datenbankdokument erfolgt auf dem gleichen Weg. Nur stehen hier teilweise andere Ereignisse zur Wahl.

Beim Starten eines Datenbankdokumentes kann, wenn die Makrounterstützung nicht aktiviert wurde, die Nachfrage kommen, ob Makros ausgeführt werden. Wird in einem Datenbankdokument das Ereignis Ansicht wurde erzeugt gewählt, so wird dieses Ereignis nicht ausgeführt, auch wenn die Frage nach dem Ausführen der Makros bejaht wurde.
Das Ereignis Dokument öffnen wird hingegen ausgeführt. Soll also auf jeden Fall beim Start des Datenbankdokumentes ein Makro ausgeführt werden, so sollte dieses Ereignis gewählt werden.

Ereignisse eines Formulars bei geöffnetem Fenster

Nachdem das Fenster für die gesamten Inhalte des Formulars (Formulardokument) geöffnet wurde, kann auf die einzelnen Elemente des Formulardokuments zugegriffen werden. Hierzu gehören auch die dem Formular zugeordneten Formularelemente.

 

Die Formularelemente können, wie in obigem Bild, über den Formularnavigator angesteuert werden. Sie sind genauso gut über jedes einzelne Kontrollfeld der Formularoberfläche über das Kontextmenü des Kontrollfeldes zu erreichen.

Die unter Formular-Eigenschaften → Ereignisse aufgezeigten Ereignisse finden statt während das Formularfenster geöffnet ist. Sie können für jedes Formular oder Unterformular des Formularfensters separat ausgewählt werden.

Der Gebrauch des Begriffes «Formular» ist bei Base leider nicht eindeutig. Der Begriff wird zum einen für das Fenster benutzt, das zur Eingabe von Daten geöffnet wird. Zum anderen wird der Begriff für das Element genutzt, das mit diesem Fenster eine bestimmte Datenquelle (Tabelle oder Abfrage) verbindet.
Es können auf einem Formularfenster sehr wohl mehrere Formulare mit unterschiedlichen Datenquelle untergebracht sein. Im Formularnavigator steht zuerst immer der Begriff «Formulare», dem dann in einem einfachen Formular lediglich ein Formular untergeordnet wird.
Das Formularfenster wird in dieser Dokumentation zur besseren Unterscheidung deshalb oft auch als «Formulardokument» bezeichnet.

Ereignisse innerhalb eines Formulars

Alle anderen Makros werden bei den Eigenschaften von Teilformularen und Kontrollfeldern über das Register Ereignisse registriert.

 

Über mehrfaches OK wird diese Zuweisung bestätigt.

Bestandteile von Makros

In diesem Abschnitt sollen einige Teile der Makro-Sprache erläutert werden, die in Base vor allem bei Formularen immer wieder benutzt werden. (Soweit möglich und sinnvoll, werden dabei die Beispiele der folgenden Abschnitte benutzt.)

Der «Rahmen» eines Makros

Die Definition eines Makros beginnt mit dem Typ des Makros SUB oder FUNCTION   und endet mit END SUB bzw. END FUNCTION. Einem Makro, das einem Ereignis zugewiesen wird, können Argumente (Werte) übergeben werden; sinnvoll ist aber nur das Argument oEvent. Alle anderen Routinen, die von einem solchen Makro aufgerufen werden, können abhängig vom Zweck mit oder ohne Rückgabewert definiert werden und beliebig mit Argumenten versehen werden.

  1. 1 SUB Ausleihe_aktualisieren 

  2. 2 END SUB 

     

  3. 1 SUB Zu_Formular_von_Formular(oEvent AS OBJECT) 

  4. 2 END SUB 

     

  5. 1 FUNCTION Loeschen_bestaetigen(oEvent AS OBJECT) AS BOOLEAN 

  6. 2    Loeschen_bestaetigen = FALSE 

  7. 3 END FUNCTION 

Es ist hilfreich, diesen Rahmen sofort zu schreiben und den Inhalt anschließend einzufügen. Bitte vergessen Sie nicht, Kommentare zur Bedeutung des Makros nach dem Grundsatz «so viel wie nötig, so wenig wie möglich» einzufügen. Außerdem unterscheidet Basic nicht zwischen Groß- und Kleinschreibung. In der Praxis werden feststehende Begriffe wie SUB vorzugsweise groß geschrieben, während andere Bezeichner Groß- und Kleinbuchstaben mischen.

Variablen definieren

Im nächsten Schritt werden am Anfang der Routine mit der DIM-Anweisung die Variablen, die innerhalb der Routine vorkommen, mit dem jeweiligen Datentyp definiert. Basic selbst verlangt das nicht, sondern akzeptiert, dass während des Programmablaufs neue Variablen auftreten. Der Programmcode ist aber «sicherer», wenn die Variablen und vor allem die Datentypen festgelegt sind. Viele Programmierer verpflichten sich selbst dazu, indem sie Basic über Option Explicit gleich zu Beginn eines Moduls mitteilen: Erzeuge nicht automatisch irgendwelche Variablen, sondern nutze nur die, die ich auch vorher definiert habe.

  1. 1 DIM oDoc AS OBJECT 

  2. 2 DIM oDrawpage AS OBJECT 

  3. 3 DIM oForm AS OBJECT 

  4. 4 DIM sName AS STRING 

  5. 5 DIM bOKEnabled AS BOOLEAN, iCounter AS INTEGER, dBirthday AS DATE 

Die Deklaration von Variablen ist in Einzelzeilen oder zusammengefasst in einer Zeile, getrennt durch ein Komma, möglich.

Für die Namensvergabe stehen nur Buchstaben (AZ oder az), Ziffern und der Unterstrich '_' zur Verfügung, aber keine Umlaute oder Sonderzeichen. (Unter Umständen ist das Leerzeichen zulässig. Sie sollten aber besser darauf verzichten.) Das erste Zeichen muss ein Buchstabe sein.

Üblich ist es, durch den ersten Buchstaben den Datentyp deutlich zu machen.1 Dann erkennt man auch mitten im Code den Typ der Variablen. Außerdem sind «sprechende Bezeichner» zu empfehlen, sodass die Bedeutung der Variablen schon durch den Namen erkannt werden kann.

Die Liste der möglichen Datentypen in Star-Basic steht im Anhang des Handbuches. An verschiedenen Stellen sind Unterschiede zwischen der Datenbank, von Basic und der LibreOffice-API zu beachten. Darauf wird bei den Beispielen hingewiesen.

Arrays definieren

Gerade für Datenbanken ist die Sammlung von mehreren Variablen in einem Datensatz von Bedeutung. Werden mehrere Variablen zusammen in einer gemeinsamen Variablen gespeichert, so wird dies als ein Array bezeichnet. Ein Array muss definiert werden, bevor Daten in das Array geschrieben werden können.

  1. 1 DIM arDaten() 

erzeugt eine leeres Array.

  1. 2 arDaten = array("Lisa","Schmidt") 

So wird ein Array auf eine bestimmte Größe von 2 Elementen festgelegt und gleichzeitig mit Daten versehen.

Über

  1. 3 print arDaten(0), arDaten(1) 

werden die beiden definierten Inhalte auf dem Bildschirm ausgegeben. Die Zählung für die Felder in Arrays beginnt hier mit 0.

  1. 1 DIM arDaten(2) 

  2. 2 arDaten(0) = "Lisa" 

  3. 3 arDaten(1) = "Schmidt" 

  4. 4 arDaten(2) = "Köln" 

Dies erstellt ein Array, in dem 3 Elemente beliebigen Typs gespeichert werden können, also z.B. ein Datensatz mit den Variablen «Lisa» «Schmidt» «Köln». Mehr passt leider in dieses Array nicht hinein. Sollen mehr Elemente gespeichert werden, so muss das Array vergrößert werden. Wird während der Laufzeit eines Makros allerdings die Größe eines Arrays einfach nur neu definiert, so ist das Array anschließend leer wie eben ein neues Array.

  1. 5 ReDIM Preserve arDaten(3) 

  2. 6 arDaten(3) = "18.07.2003" 

Durch den Zusatz Preserve werden die vorherigen Daten beibehalten, das Array also tatsächlich zusätzlich um die Datumseingabe, hier in Form eines Textes, erweitert.

Das oben aufgeführte Array kann leider nur einen einzelnen Satz an Daten speichern. Sollen stattdessen, wie in einer Tabelle einer Datenbank, mehrere Datensätze gespeichert werden, so muss dem Array zu Beginn eine zusätzliche Dimension hinzugefügt werden.

  1. 1 DIM arDaten(2,1) 

  2. 2 arDaten(0,0) = "Lisa" 

  3. 3 arDaten(1,0) = "Schmidt" 

  4. 4 arDaten(2,0) = "Köln" 

  5. 5 arDaten(0,1) = "Egon" 

  6. 6 arDaten(1,1) = "Müller" 

  7. 7 arDaten(2,1) = "Hamburg" 

Auch hier gilt bei einer Erweiterung über das vorher definierte Maß hinaus, dass der Zusatz Preserve die vorher eingegebenen Daten mit übernimmt.

Zugriff auf das Formular

Das Formular liegt in dem momentan aktiven Dokument. Der Bereich, der dargestellt wird, wird als drawpage bezeichnet. Der Behälter, in dem alle Formulare aufbewahrt werden, heißt forms – im Formularnavigator ist dies sozusagen der oberste Begriff, an den dann sämtliche Formulare angehängt werden. Die o.g. Variablen erhalten auf diesem Weg ihre Werte:

  1. 1 oDoc = thisComponent 

  2. 2 oDrawpage = oDoc.drawpage 

  3. 3 oForm = oDrawpage.forms.getByName("Filter") 

Das Formular, auf das zugegriffen werden soll, ist hier mit dem Namen «Filter» versehen. Dies ist der Name, der auch im Formularnavigator in der obersten Ebene sichtbar ist. (Standardmäßig erhält das erste Formular den Namen «MainForm».) Unterformulare liegen – hierarchisch angeordnet – innerhalb eines Formulars und können Schritt für Schritt erreicht werden:

  1. 4 DIM oSubForm AS OBJECT 

  2. 5 DIM oSubSubForm AS OBJECT 

  3. 6 oSubForm = oForm.getByName("Leserauswahl") 

  4. 7 oSubSubForm = oSubForm.getByName("Leseranzeige") 

Anstelle der Variablen in den «Zwischenstufen» kann man auch direkt zu einem bestimmten Formular gelangen. Ein Objekt der Zwischenstufen, das mehr als einmal verwendet wird, sollte selbständig deklariert und zugewiesen werden. (Im folgenden Beispiel wird oSubForm nicht mehr benutzt.)

  1. 6 oForm = thisComponent.drawpage.forms.getByName("Filter") 

  2. 7 oSubSubForm = oForm.getByName("Leserauswahl").getByName("Leseranzeige") 

Sofern ein Name ausschließlich aus Buchstaben und Ziffern besteht (keine Umlaute, keine Leer- oder Sonderzeichen), kann der Name in der Zuweisung auch direkt verwendet werden:
  1. 6 oForm = thisComponent.drawpage.forms.Filter 

  2. 7 oSubSubForm = oForm.Leserauswahl.Leseranzeige 

 

Einen anderen Zugang zum Formular ermöglicht das auslösende Ereignis für das Makro.

Startet ein Makro über ein Ereignis des Formulars, wie z. B. Formular-Eigenschaften → Vor der Datensatzaktion, so wird das Formular selbst folgendermaßen erreicht:

  1. 1 SUB MakrobeispielBerechne(oEvent AS OBJECT) 

  2. 2    oForm = oEvent.Source 

  3. 3    ... 

  4. 4 END SUB 

Startet ein Makro über ein Ereignis eines Formularfeldes, wie z. B. Eigenschaften: Textfeld → Bei Fokusverlust, so kann sowohl das Formularfeld als auch das Formular ermittelt werden:

  1. 1 SUB MakrobeispielBerechne(oEvent AS OBJECT) 

  2. 2    oFeld = oEvent.Source.Model 

  3. 3    oForm = oFeld.Parent 

  4. 4    ... 

  5. 5 END SUB 

Die Zugriffe über das Ereignis haben den Vorteil, dass kein Gedanke darüber verschwendet werden muss, ob es sich bei dem Formular um ein Hauptformular oder Unterformular handelt. Auch interessiert der Name des Formulars für die Funktionsweise des Makros nicht.

Zugriff auf Elemente eines Formulars

In gleicher Weise kann man auf die Elemente eines Formulars zugreifen: Deklarieren Sie eine entsprechende Variable als object und suchen Sie das betreffende Kontrollfeld innerhalb des Formulars:

  1. 1 DIM btnOK AS OBJECT  ' Button »OK» 

  2. 2 btnOK = oSubSubForm.getByName("Schaltfläche 1")   ' aus dem Formular Leseranzeige 

Dieser Weg funktioniert immer dann, wenn bekannt ist, mit welchem Element das Makro arbeiten soll. Wenn aber im ersten Schritt zu prüfen ist, welches Ereignis das Makro gestartet hat, ist der o.g. Weg über oEvent sinnvoll. Dann wird die Variable innerhalb des Makro-"Rahmens" deklariert und beim Start des Makros zugewiesen. Die Eigenschaft Source liefert immer dasjenige Element, das das Makro gestartet hat; die Eigenschaft Model beschreibt das Kontrollfeld im Einzelnen:

  1. 1 SUB Auswahl_bestaetigen(oEvent AS OBJECT) 

  2. 2    DIM btnOK AS OBJECT 

  3. 3    btnOK = oEvent.Source.Model 

  4. 4 END 

Mit dem Objekt, das man auf diesem Weg erhält, werden die weiteren angestrebten Maßnahmen ausgeführt.

Bitte beachten Sie, dass auch Unterformulare als Bestandteile eines Formulars gelten.

Zugriff auf die Datenbank

Normalerweise wird der Zugriff auf die Datenbank über Formulare, Abfragen, Berichte oder die Serienbrief-Funktion geregelt, wie es in allen vorhergehenden Kapiteln beschrieben wurde. Wenn diese Möglichkeiten nicht genügen, kann ein Makro auch gezielt die Datenbank ansprechen, wofür es mehrere Wege gibt.

Die Verbindung zur Datenbank

Das einfachste Verfahren benutzt dieselbe Verbindung wie das Formular, wobei oForm wie oben bestimmt wird:

  1. 1 DIM oConnection AS OBJECT 

  2. 2 oConnection = oForm.activeConnection() 

Oder man holt die Datenquelle, also die Datenbank, durch das Dokument und benutzt die vorhandene Verbindung auch für das Makro:

  1. 1 DIM oDatasource AS OBJECT 

  2. 2 DIM oConnection AS OBJECT 

  3. 3 oDatasource = thisComponent.Parent.dataSource 

  4. 4 oConnection = oDatasource.getConnection("","") 

Ein weiterer Weg stellt sicher, dass bei Bedarf die Verbindung zur Datenbank hergestellt wird:

  1. 1 DIM oDatasource AS OBJECT 

  2. 2 DIM oConnection AS OBJECT 

  3. 3 oDatasource = thisComponent.Parent.CurrentController 

  4. 4 IF NOT (oDatasource.isConnected()) THEN oDatasource.connect() 

  5. 5 oConnection = oDatasource.ActiveConnection() 

Die IF-Bedingung bezieht sich hier nur auf eine Zeile. Deshalb ist END IF nicht erforderlich.

Wenn das Makro durch die Benutzeroberfläche nicht aus einem Formulardokument heraus gestartet werden soll, ist folgende Variante geeignet. Dazu muss das Makro innerhalb der Base-Datei gespeichert werden.

  1. 1 DIM oDatasource AS OBJECT 

  2. 2 DIM oConnection AS OBJECT 

  3. 3 oDatasource = thisDatabaseDocument.CurrentController 

  4. 4 IF NOT (oDatasource.isConnected()) THEN oDatasource.connect() 

  5. 5 oConnection = oDatasource.ActiveConnection() 

Der Zugriff auf Datenbanken außerhalb der aktuellen Datenbank ist folgendermaßen möglich:

  1. 1 DIM oDatabaseContext AS OBJECT 

  2. 2 DIM oDatasource AS OBJECT 

  3. 3 DIM oConnection AS OBJECT 

  4. 4 oDatabaseContext = createUnoService("com.sun.star.sdb.DatabaseContext") 

  5. 5 oDatasource = oDatabaseContext.getByName("angemeldeter Name der Datenbank in LO") 

  6. 6 oConnection = oDatasource.GetConnection("","") 

Auch die Verbindung zu nicht in LO angemeldete Datenbanken ist möglich. Hier muss dann lediglich statt des angemeldeten Namens der Pfad zur Datenbank mit «file:///..../Datenbank.odb» angegeben werden.

Ergänzende Hinweise zur Datenbankverbindung stehen im Abschnitt Verbindung mit Datenbanken erzeugen.

SQL-Befehle

Die Arbeit mit der Datenbank erfolgt über SQL-Befehle. Ein solcher muss also erstellt und an die Datenbank geschickt werden; je nach Art des Befehls wird das Ergebnis ausgewertet und weiter verarbeitet. Mit der Anweisung createStatement wird das Objekt dafür erzeugt:

  1. 1 DIM oSQL_Statement AS OBJECT   ' das Objekt, das den SQL-Befehl ausführt 

  2. 2 DIM stSql AS STRING            ' Text des eigentlichen SQL-Befehls 

  3. 3 DIM oResult AS OBJECT          ' Ergebnis für executeQuery 

  4. 4 DIM iResult AS INTEGER         ' Ergebnis für executeUpdate 

  5. 5 oSQL_Statement = oConnection.createStatement() 

Um Daten abzufragen, wird mit dem Befehl die Methode executeQuery aufgerufen und ausgeführt; das Ergebnis wird anschließend ausgewertet. Tabellennamen und Feldnamen werden üblicherweise in doppelte Anführungszeichen gesetzt. Diese müssen im Makro durch weitere doppelte Anführungszeichen maskiert werden, damit sie im Befehl erscheinen.

  1. 6 stSql = "SELECT * FROM ""Tabelle1""" 

  2. 7 oResult = oSQL_Statement.executeQuery(stSql) 

Um Daten zu ändern also für INSERT, UPDATE oder DELETE oder um die Struktur der Datenbank zu beeinflussen,  wird mit dem Befehl die Methode executeUpdate aufgerufen und ausgeführt. Je nach Art des Befehls und der Datenbank erhält man kein nutzbares Ergebnis (ausgedrückt durch die Zahl 0) oder die Anzahl der bearbeiteten Datensätze.

  1. 8 stSql = "DROP TABLE ""Suchtmp"" IF EXISTS" 

  2. 9 iResult = oSQL_Statement.executeUpdate(stSql) 

Der Vollständigkeit halber sei noch ein Spezialfall erwähnt: Wenn oSQL_Statement unterschiedlich für SELECT oder für andere Zwecke benutzt wird, steht die Methode execute zur Verfügung. Diese benutzen wir nicht; wir verweisen dazu auf die API-Referenz.

SQL-Befehle, die so abgesandt werden, entsprechen nicht genau dem, was z. B. bei den Abfragen über direkte SQL-Ausführung erreicht wird. Eine Abfrage wie
… "Name" LIKE '%*%'
gibt nicht nur die Namen mit einem '*' wieder, da intern aus dem '*' ein '%' erstellt wird.
Um wirklich das gleiche Verhalten mit direkter SQL-Ausführung zu erhalten, muss
oSQL_Statement.
EscapeProcessing = False
nach der Erstellung von oSQL_Statement und vor der Ausführung des Codes eingefügt werden:
  1. 1 oSQL_Statement = oConnection.createStatement() 

  2. 2 oSQL_Statement.EscapeProcessing = False 

 

Vorbereitete SQL-Befehle mit Parametern

In allen Fällen, in denen manuelle Eingaben der Benutzer in einen SQL-Befehl übernommen werden, ist es einfacher und sicherer, den Befehl nicht als lange Zeichenkette zu erstellen, sondern ihn vorzubereiten und mit Parametern zu benutzen. Das vereinfacht die Formatierung von Zahlen, Datumsangaben und auch Zeichenketten (die ständigen doppelten Anführungszeichen entfallen) und verhindert Datenverlust durch böswillige Eingaben.

Bei diesem Verfahren wird zunächst das Objekt für einen bestimmten SQL-Befehl erstellt und vorbereitet:

  1. 1 DIM oSQL_Statement AS OBJECT                ' das Objekt, das den SQL-Befehl ausführt 

  2. 2 DIM stSql AS STRING                                        ' Text des eigentlichen SQL-Befehls 

  3. 3 stSql = "UPDATE ""Verfasser"" " _ 

  4. 4    & "SET ""Nachname"" = ?, ""Vorname"" = ?" _ 

  5. 5    & "WHERE ""ID"" = ?" 

  6. 6 oSQL_Statement = oConnection.prepareStatement(stSql) 

Das Objekt wird mit prepareStatement erzeugt, wobei der SQL-Befehl bereits bekannt sein muss. Jedes Fragezeichen markiert eine Stelle, an der später vor der Ausführung des Befehls ein konkreter Wert eingetragen wird. Durch das «Vorbereiten» des Befehls stellt sich die Datenbank darauf ein, welche Art von Angaben in diesem Fall zwei Zeichenketten und eine Zahl vorgesehen ist. Die verschiedenen Stellen werden durch die Position (ab 1 gezählt) unterschieden.

Anschließend werden mit passenden Anweisungen die Werte übergeben und danach der SQL-Befehl ausgeführt. Die Werte werden hier aus Kontrollfeldern des Formulars übernommen, können aber auch aus anderen Makro-Elementen stammen oder im Klartext angegeben werden:

  1. 7 oSQL_Statement.setString(1, oTextfeld1.Text)   ' Text für den Nachnamen 

  2. 8 oSQL_Statement.setString(2, oTextfeld2.Text)   ' Text für den Vornamen 

  3. 9 oSQL_Statement.setLong(3, oZahlenfeld1.Value)  ' Wert für die betreffende ID 

  4. 10 iResult = oSQL_Statement.executeUpdate 

Die vollständige Liste der Zuweisungen findet sich im Abschnitt Parameter für vorbereitete SQL-Befehle.

Es ist auch möglich, direkt mehrere Datensätze über einen vorbereiteten SQL-Befehl einzufügen:

  1. 11 stSql = "UPDATE ""Person"" " _ 

  2. 12    & "SET ""Name"" = ?" _ 

  3. 13    & "WHERE ""ID"" = ?" 

  4. 14 oSQL_Statement = oConnection.prepareStatement(stSql) 

  5. 15 oSQL_Statement.setString(1, "Bill") 

  6. 16 oSQL_Statement.setLong(2, 1) 

  7. 17 oSQL_Statement.addBatch() 

  8. 18 oSQL_Statement.setString(1, "Michaela") 

  9. 19 oSQL_Statement.setLong(2, 2) 

  10. 20 oSQL_Statement.addBatch() 

  11. 21 oSQL_Statement.executeBatch() 

Mit clearBatch könnte der gesamte Inhalt vor der Ausführung auch wieder zurückgenommen werden.

Wer sich weiter über die Vorteile dieses Verfahrens informieren möchte, findet hier Erläuterungen:

Datensätze lesen und benutzen

Es gibt abhängig vom Zweck mehrere Wege, um Informationen aus einer Datenbank in ein Makro zu übernehmen und weiter zu verarbeiten.

Bitte beachten Sie: Wenn hier von einem «Formular» gesprochen wird, kann es sich auch um ein Unterformular handeln. Es geht dann immer um dasjenige (Teil-) Formular, das mit einer bestimmten Datenmenge verbunden ist.

Mithilfe des Formulars

Der aktuelle Datensatz und seine Daten stehen immer über das Formular zur Verfügung, das die betreffende Datenmenge (Tabelle, Abfrage, Ansicht (View)) anzeigt. Dafür gibt es mehrere Methoden, die mit get und dem Datentyp bezeichnet sind, beispielsweise diese:

  1. 1 DIM ID AS LONG 

  2. 2 DIM sName AS STRING 

  3. 3 DIM dValue AS CURRENCY 

  4. 4 DIM dEintritt AS NEW com.sun.star.util.Date 

  5. 5 ID = oForm.getLong(1) 

  6. 6 sName = oForm.getString(2) 

  7. 7 dValue = oForm.getDouble(4) 

  8. 8 dEintritt = oForm.getDate(7) 

Bei allen diesen Methoden ist jeweils die Nummer der Spalte in der Datenmenge anzugeben gezählt ab 1.

Bei allen Methoden, die mit Datenbanken arbeiten, wird ab 1 gezählt. Das gilt sowohl für Spalten als auch für Zeilen.

Soll anstelle der Spaltennummern mit den Spaltennamen der zugrundeliegenden Datenmenge (Tabelle, Abfrage, Ansicht (View)) gearbeitet werden, so kann die Spaltennummer über die Methode findColumn ermittelt werden. Hier ein Beispiel zum Auffinden der Spalte "Name":

  1. 1 DIM sName AS STRING 

  2. 2 DIM nName AS STRING 

  3. 3 nName = oForm.findColumn("Name") 

  4. 4 sName = oForm.getString(nName) 

Das Ergebnis ist immer ein Wert des Typs der Methode, wobei die folgenden Sonderfälle zu beachten sind.

Die vollständige Liste dieser Methoden findet sich im Abschnitt Datenzeilen bearbeiten.

Sollen Werte aus einem Formular für direkte Weiterverarbeitung in SQL genutzt werden (z.B. für die Eingabe der Daten in eine andere Tabelle), so ist es wesentlich einfacher, nicht nach dem Typ der Felder zu fragen.
Das folgende Makro, an Eigenschaften: Schaltfläche → Ereignisse → Aktion ausführen gekoppelt, liest das erste Feld des Formulars aus – unabhängig von dem für die Weiterverarbeitung in Basic erforderlichen Typ.
  1. 1 SUB WerteAuslesen(oEvent AS OBJECT) 

  2. 2    DIM oForm AS OBJECT 

  3. 3    DIM stFeld1 AS STRING 

  4. 4    oForm = oEvent.Source.Model.Parent 

  5. 5    stFeld1 = oForm.getString(1) 

  6. 6 END SUB 

 

Grundsätzlich können alle Felder mit getString ausgelesen werden. Um sie in Makros weiter zu verarbeiten, müssen die erhaltenen Strings dann gegebenenfalls anschließend in die jeweiligen Variablen umgewandelt werden.
Das Auslesen mit getString hat den Vorteil, dass auch leere Felder einwandfrei ermittelt werden können. Werden die (passenden) Variablen stattdessen z.B. über getInt ausgelesen, so ergibt selbst ein leeres Feld '0'. Dies täuscht also vor, dass in dem Feld eben diese Zahl enthalten war. So etwas ist besonders lästig, wenn eben auch der Wert '0' selbst vorkommt – bei der internen Hsqldb z. B. beim automatisch hoch zählenden Schlüsselwert als Startwert.
Alternativ muss immer mit wasNull nachgesehen werden, ob denn der Inhalt vielleicht leer gewesen ist.

Ergebnis einer Abfrage

In gleicher Weise kann die Ergebnismenge einer Abfrage benutzt werden. Im Abschnitt SQL-Befehle steht die Variable oResult für diese Ergebnismenge, die üblicherweise so oder ähnlich ausgelesen wird:

  1. 1 WHILE oResult.next         ' einen Datensatz nach dem anderen verarbeiten 

  2. 2    rem übernimm die benötigten Werte in Variablen 

  3. 3    stVar = oResult.getString(1) 

  4. 4    inVar = oResult.getLong(2) 

  5. 5    boVar = oResult.getBoolean(3) 

  6. 6    rem mach etwas mit diesen Werten 

  7. 7 WEND 

Je nach Art des SQL-Befehls, dem erwarteten Ergebnis und dem Zweck kann vor allem die WHILE-Schleife verkürzt werden oder sogar entfallen. Aber grundsätzlich wird eine Ergebnismenge immer nach diesem Schema ausgewertet.

Soll nur der erste Datensatz ausgewertet werden, so wird mit

  1. 1 oResult.next 

zuerst die Zeile auf diesen Datensatz bewegt und dann mit

  1. 2 stVar = oResult.getString(1) 

z.B. der Inhalt des ersten Datenfeldes gelesen. Die Schleife entfällt hier.

  1. 3 IF wasNull THEN 

  2. 4    ... 

  3. 5 END IF 

Mit dieser Nachfrage würde die gerade getätigte Abfrage zu stVar darauf überprüft, ob sie nach SQL-Standard NULL gewesen ist.

Die Abfrage zu dem obigen Beispiel hat in der ersten Spalte einen Text, in der zweiten Spalte einen Integer-Zahlenwert (Integer aus der Datenbank entspricht Long in Basic) und in der dritten Spalte ein Ja/Nein-Feld. Die Felder werden durch den entsprechenden Indexwert angesprochen. Der Index für die Felder beginnt hier, im Gegensatz zu der sonstigen Zählung bei Arrays, mit dem Wert '1'.

In dem so erstellten Ergebnis ist allerdings keine Navigation möglich. Nur einzelne Schritte zum nächsten Datensatz sind erlaubt. Um innerhalb der Datensätze navigieren zu können, muss der ResultSetType bei der Erstellung der Abfrage bekannt sein. Hierauf wird über

  1. 1 oSQL_Anweisung.ResultSetType = 1004 

oder

  1. 1 oSQL_Anweisung.ResultSetType = 1005 

zugegriffen. Der Typ 1004 - SCROLL_INTENSIVE erlaubt eine beliebige Navigation. Allerdings bleibt eine Änderung an den Originaldaten während des Auslesens unbemerkt. Der Typ 1005 – SCROLL_SENSITIVE berücksichtigt zusätzlich gegebenenfalls Änderungen an den Originaldaten, die das Abfrageergebnis beeinflussen könnten.

Soll zusätzlich in dem Ergebnissatz eine Änderung der Daten ermöglicht werden, so muss die ResultSetConcurrency vorher definiert werden. Die Update-Möglichkeit wird über

  1. 1 oSQL_Anweisung.ResultSetConcurrency = 1008 

hergestellt. Der Typ 1007 - READ_ONLY ist hier die Standardeinstellung.

Die Anzahl der Zeilen, die die Ergebnismenge enthält, kann nur nach Wahl der entsprechenden Typen so bestimmt werden:

  1. 1 DIM iResult AS LONG 

  2. 2 IF oResult.last THEN           ' gehe zum letzten Datensatz, sofern möglich 

  3. 3    iResult = oResult.getRow    ' die laufende Nummer ist die Anzahl 

  4. 4 ELSE 

  5. 5    iResult = 0 

  6. 6 END IF 

Sollen viele Daten über die Schleife WHILE oResult.next ausgelesen werden, so macht sich hier ein Geschwindigkeitsunterschied bei verschiedenen Datenbanken deutlich bemerkbar. Die interne Firebird Datenbank arbeitet hier deutlich schneller als die interne Hsqldb. Selbst bei gleichen Datenbanken kann es abhängig vom Treiber zu deutlichen Unterschieden kommen. Bei der MariaDB mit direkter Verbindung ist die Geschwindigkeit auf dem Level der Firebird Datenbank. Mit der JDBC-Verbindung hingegen ist das Auslesen so langsam wie mit der Hsqldb.
Dieses Verhalten kann schon bei einer Abfrage getestet werden, wenn zum Ausführen die direkte SQL-Verbindung genutzt wird. Das Scrollen zum letzten Datensatz braucht bei vielen Zeilen deutlich länger mit der internen Hsqldb und MariaDB über die JDBC-Verbindung als mit der internen Firebird Datenbank und MariaDB mit der direkten Verbindung.

Mithilfe eines Kontrollfelds

Wenn ein Kontrollfeld mit einer Datenmenge verbunden ist, kann der Wert auch direkt ausgelesen werden, wie es im nächsten Abschnitt beschrieben wird. Das ist aber teilweise mit Problemen verbunden. Sicherer ist neben dem Verfahren Mithilfe des Formulars  der folgende Weg, der für verschiedene Kontrollfelder gezeigt wird:

  1. 1 sValue = oTextField.BoundField.Text        ' Beispiel für ein Textfeld 

  2. 2 nValue = oNumericField.BoundField.Value    ' Beispiel für ein numerisches Feld 

  3. 3 dValue = oDateField.BoundField.Date        ' Beispiel für ein Datumsfeld 

BoundField stellt dabei die Verbindung her zwischen dem (sichtbaren) Kontrollfeld und dem eigentlichen Inhalt der Datenmenge.

Datensätze wechseln und bestimmte Datensätze ansteuern

Im vorletzten Beispiel wurde mit der Methode Next von einer Zeile der Ergebnismenge zur nächsten gegangen. In gleicher Weise gibt es weitere Maßnahmen und Prüfungen, und zwar sowohl für die Daten eines Formulars angedeutet durch die Variable oForm als auch für eine Ergebnismenge. Beispielsweise kann man beim Verfahren Automatisches Aktualisieren von Formularen den vorher aktuellen Datensatz wieder markieren:

  1. 1 DIM loRow AS LONG 

  2. 2 loRow = oForm.getRow()   ' notiere die aktuelle Zeilennummer 

  3. 3 oForm.reload()           ' lade die Datenmenge neu 

  4. 4 oForm.absolute(loRow)    ' gehe wieder zu der notierten Zeilennummer 

Im Abschnitt In einer Datenmenge navigieren stehen alle dazu passenden Methoden.

Die Methode mit getRow() funktioniert aber nur dann einwandfrei, wenn es sich bei dem aktuellen Datensatz nicht um einen neuen Datensatz handelt. Deswegen hier eine weitere Möglichkeit der Navigation über den Bookmark-Befehl. Damit lässt sich der Cursor auch dann sicher positionieren, wenn bei einer neuen Zeile getRow() 0 ermittelt.

  1. 1 DIM var AS VARIANT 

  2. 2 var = oForm.getBookmark() 

  3. 3 loRow = oForm.getRow() 

  4. 4 oForm.reload() 

  5. 5 IF loRow = 0 THEN 

  6. 6    oForm.MoveToInsertRow() 

  7. 7 ELSE 

  8. 8    oForm.MoveToBookmark(var) 

  9. 9 END IF 

Bei einem neuen Datensatz wird die Zeile mit '0' angegeben. In diesem Fall wird dann der Cursor nach dem Neuladen mit MoveToInsertRow auf diese Position gesetzt. Mit MoveToBookmark würde hier der Cursor auf die letzte Zeile mit Inhalt gesetzt.

Datensätze bearbeiten neu anlegen, ändern, löschen

Um Datensätze zu bearbeiten, müssen mehrere Teile zusammenpassen: Eine Information muss vom Anwender in das Kontrollfeld gebracht werden; das geschieht durch die Tastatureingabe. Anschließend muss die Datenmenge «dahinter» diese Änderung zur Kenntnis nehmen; das geschieht durch das Verlassen eines Feldes und den Wechsel zum nächsten Feld. Und schließlich muss die Datenbank selbst die Änderung erfahren; das erfolgt durch den Wechsel von einem Datensatz zu einem anderen.

Bei der Arbeit mit einem Makro müssen ebenfalls diese Teilschritte beachtet werden. Wenn einer fehlt oder falsch ausgeführt wird, gehen Änderungen verloren und «landen» nicht in der Datenbank. In erster Linie muss die Änderung nicht in der Anzeige des Kontrollfelds erscheinen, sondern in der Datenmenge. Es ist deshalb sinnlos, die Eigenschaft Text des Kontrollfelds zu ändern. Diese Eigenschaft dient nur zur Anzeige des Wertes.

Bitte beachten Sie, dass nur Datenmengen vom Typ «Tabelle» problemlos geändert werden können. Bei anderen Datenmengen ist dies nur unter besonderen Bedingungen möglich.

Inhalt eines Kontrollfelds ändern

Wenn es um die Änderung eines einzelnen Wertes geht, wird das über die Eigenschaft BoundField des Kontrollfelds mit einer passenden Methode erledigt. Anschließend muss nur noch die Änderung an die Datenbank weitergegeben werden. Beispiel für ein Datumsfeld, in das das aktuelle Datum eingetragen werden soll:

  1. 1 DIM unoDate AS NEW com.sun.star.util.Date 

  2. 2 unoDate.Year = Year(Date) 

  3. 3 unoDate.Month = Month(Date) 

  4. 4 unoDate.Day = Day(Date) 

  5. 5 oDateField = oForm.getByName("Datum") 

  6. 6 oDateField.BoundField.updateDate( unoDate ) 

  7. 7 oForm.updateRow()       ' Weitergabe der Änderung an die Datenbank 

Für BoundField wird diejenige der updateXxx-Methoden aufgerufen, die zum Datentyp des Feldes passt hier geht es um einen Date-Wert. Als Argument wird der gewünschte Wert übergeben hier das aktuelle Datum, konvertiert in die vom Makro benötigte Schreibweise. Die entsprechende Erstellung des Datums kann auch durch die Formel CDateToUnoDate erreicht werden:

  1. 1 oDateField = oForm.getByName("Datum") 

  2. 2 oDateField.BoundField.updateDate( CDateToUnoDate(NOW()) ) 

  3. 3 oForm.updateRow()       ' Weitergabe der Änderung an die Datenbank 

Zeile einer Datenmenge ändern

Wenn mehrere Werte in einer Zeile geändert werden sollen, ist der vorstehende Weg ungeeignet. Zum einen müsste für jeden Wert ein Kontrollfeld existieren, was oft nicht gewünscht oder sinnvoll ist. Zum anderen muss man sich für jedes dieser Felder ein Objekt «holen». Der einfache und direkte Weg geht über das Formular, beispielsweise so:

  1. 1 DIM unoDate AS NEW com.sun.star.util.Date 

  2. 2 unoDate.Year = Year(Date) 

  3. 3 unoDate.Month = Month(Date) 

  4. 4 unoDate.Day = Day(Date) 

  5. 5 oForm.updateDate(3, unoDate ) 

  6. 6 oForm.updateString(4, "ein Text") 

  7. 7 oForm.updateDouble(6, 3.14) 

  8. 8 oForm.updateInt(7, 16) 

  9. 9 oForm.updateRow() 

Für jede Spalte der Datenmenge wird die zum Datentyp passende updateXxx-Methode aufgerufen. Als Argumente werden die Nummer der Spalte (ab 1 gezählt) und der jeweils gewünschte Wert übergeben. Anschließend muss nur noch die Änderung an die Datenbank weitergegeben werden.

Zeilen anlegen, ändern, löschen

Die genannten Änderungen beziehen sich immer auf die aktuelle Zeile der Datenmenge des Formulars. Unter Umständen muss vorher eine der Methoden aus In einer Datenmenge navigieren aufgerufen werden. Es werden also folgende Maßnahmen benötigt:

  1. 1.Wähle den aktuellen Datensatz. 

  2. 2.Ändere die gewünschten Werte, wie im vorigen Abschnitt beschrieben. 

  3. 3.Bestätige die Änderungen mit folgendem Befehl:
    oForm.updateRow()  

  4. 4.Als Sonderfall ist es auch möglich, die Änderungen zu verwerfen und den vorherigen Zustand wiederherzustellen:
    oForm.cancelRowUpdates()  

Für einen neuen Datensatz gibt es eine spezielle Methode (vergleichbar mit dem Wechsel in eine neue Zeile im Tabellenkontrollfeld). Es werden also folgende Maßnahmen benötigt:

  1. 1.Bereite einen neuen Datensatz vor:
    oForm.moveToInsertRow()  

  2. 2.Trage alle vorgesehenen und benötigten Werte ein. Dies geht ebenfalls mit den updateXxx-Methoden, wie im vorigen Abschnitt beschrieben. 

  3. 3.Bestätige die Neuaufnahme mit folgendem Befehl:
    oForm.insertRow()  

  4. 4.Die Neuaufnahme kann nicht einfach rückgängig gemacht werden. Stattdessen ist die soeben neu angelegte Zeile wieder zu löschen. 

Für das Löschen eines Datensatzes gibt es einen einfachen Befehl; es sind also folgende Maßnahmen nötig:

  1. 1.Wähle wie für eine Änderung den gewünschten Datensatz und mache ihn zum aktuellen. 

  2. 2.Bestätige die Löschung mit folgendem Befehl:
    oForm.deleteRow()  

Damit eine Änderung in die Datenbank übernommen wird, ist sie durch updateRow bzw. insertRow ausdrücklich zu bestätigen. Während beim Betätigen des Speicher-Buttons die passende Funktion automatisch ermittelt wird, muss vor dem Abspeichern ermittelt werden, ob der Datensatz neu ist (Insert) oder ein bestehender Datensatz bearbeitet wurde (Update).
  1. 1 IF oForm.isNew THEN 

  2. 2    oForm.insertRow() 

  3. 3 ELSE 

  4. 4    oForm.updateRow() 

  5. 5 END IF 

 

Kontrollfelder prüfen und ändern

Neben dem Inhalt, der aus der Datenmenge kommt, können viele weitere Informationen zu einem Kontrollfeld gelesen, verarbeitet und geändert werden. Das betrifft vor allem die Eigenschaften, die im Kapitel «Formulare» aufgeführt werden. Eine Übersicht steht im Abschnitt Eigenschaften bei Formularen und Kontrollfeldern.

In mehreren Beispielen des Abschnitts Bedienbarkeit verbessern wird die Zusatzinformation eines Feldes benutzt:

  1. 1 SUB Main(oEvent AS OBJECT) 

  2. 2    DIM stTag AS STRING 

  3. 3    stTag = oEvent.Source.Model.Tag 

Die Eigenschaft Text kann wie im vorigen Abschnitt erläutert nur dann sinnvoll geändert werden, wenn das Feld nicht mit einer Datenmenge verbunden ist. Aber andere Eigenschaften, die «eigentlich» bei der Formulardefinition festgelegt werden, können zur Laufzeit angepasst werden. Beispielsweise kann in einem Beschriftungsfeld die Textfarbe gewechselt werden, wenn statt einer Meldung ein Hinweis oder eine Warnung angezeigt werden soll:

  1. 1 SUB showWarning(oField AS OBJECT, iType AS INTEGER) 

  2. 2    SELECT CASE iType 

  3. 3       CASE 1  

  4. 4          oField.TextColor = RGB(0,0,255)   ' 1 = blau 

  5. 5       CASE 2  

  6. 6          oField.TextColor = RGB(255,0,0)   ' 2 = rot 

  7. 7       CASE ELSE 

  8. 8          oField.TextColor = RGB(0,255,0)   ' 0 = grün (weder 1 noch 2) 

  9. 9    END SELECT 

  10. 10 END SUB 

Englische Bezeichner in Makros

Während der Formular-Designer in der deutschen Version auch deutsche Bezeichnungen für die Eigenschaften und den Datenzugriff verwendet, müssen in Basic englische Begriffe verwendet werden. Diese sind in den folgenden Übersichten aufgeführt.

Eigenschaften, die üblicherweise nur in der Formular-Definition festgelegt werden, stehen nicht in den Übersichten. Gleiches gilt für Methoden (Funktionen und Prozeduren), die nur selten verwendet werden oder für die kompliziertere Erklärungen nötig wären.

Die Übersichten nennen folgende Angaben:

Weitere Informationen finden sich vor allem in der API-Referenz mit Suche nach der englischen Bezeichnung des Kontrollfelds. Gut geeignet, um herauszufinden, welche Eigenschaften und Methoden denn eigentlich bei einem Element zur Verfügung stehen, ist auch das Tool Xray .

  1. 1 SUB Main(oEvent AS OBJECT) 

  2. 2    Xray(oEvent) 

  3. 3 END SUB 

Hiermit wird die Erweiterung Xray aus dem Aufruf heraus gestartet.

Eigenschaften bei Formularen und Kontrollfeldern

Das «Modell» eines Kontrollfelds beschreibt seine Eigenschaften. Je nach Situation kann der Wert einer Eigenschaft nur gelesen und nur geändert werden. Die Reihenfolge orientiert sich an den Aufstellungen «Eigenschaften der Kontrollfelder» im Kapitel «Formular».

Wenn mit

  1. 1 oFeld = oForm.getByName("Name des Kontrollfeldes") 

auf ein Kontrollfeld zugegriffen wird, so werden die Eigenschaften einfach durch ein Anhängen an dieses Objekt mit einem Punkt als Verbinder angesprochen:

  1. 1 oFeld.FontHeight = 16 

definiert also z. B. die Schriftgröße in 16 Punkten.

Schrift

In jedem Kontrollfeld, das Text anzeigt, können die Eigenschaften der Schrift angepasst werden.

Name

Datentyp

L/S

Eigenschaft

FontName

string

L+S

Schriftart.

FontHeight

single

L+S

Schriftgröße.

FontWeight

single

L+S

Schriftstärke.

FontSlant

integer

L+S

Art der Schrägstellung.

FontUnderline

integer

L+S

Art der Unterstreichung.

FontStrikeout

integer

L+S

Art des Durchstreichens.

Formular

Englische Bezeichnung: Form

Name

Datentyp

L/S

Eigenschaft

ApplyFilter

boolean

L+S

Filter aktiviert.

Filter

string

L+S

Aktueller Filter für die Datensätze.

FetchSize

long

L+S

Anzahl der Datensätze, die «am Stück» geladen werden.

Row

long

L

Nummer der aktuellen Zeile.

RowCount

long

L

Anzahl der Datensätze. Entspricht der Anzeige der Gesamtdatensätze in der Navigationsleiste. Da nicht direkt alle Datensätze über FetchSize in den Cache gelesen werden steht hier z.B. '41*', obwohl die Tabelle deutlich mehr Datensätze hat. RowCount gibt dann leider auch nur '41' aus.

Einheitlich für alle Arten von Kontrollfeld

Englische Bezeichnung: Control siehe auch FormComponent

Name

Datentyp

L/S

Eigenschaft

Name

string

L+(S)

Bezeichnung für das Feld.

Enabled

boolean

L+S

Aktiviert: Feld kann ausgewählt werden.

EnableVisible

boolean

L+S

Sichtbar: Feld wird dargestellt.

ReadOnly

boolean

L+S

Nur lesen: Inhalt kann nicht geändert werden.

TabStop

boolean

L+S

Feld ist in der Tabulator-Reihenfolge erreichbar.

Align

integer

L+S

Horizontale Ausrichtung:
0 = links, 1 = zentriert, 2 = rechts

BackgroundColor

long

L+S

Hintergrundfarbe.

Tag

string

L+S

Zusatzinformation.

HelpText

string

L+S

Hilfetext als «Tooltip».

Einheitlich für viele Arten von Kontrollfeld

Name

Datentyp

L/S

Eigenschaft

Text

string

(L+S)

Inhalt des Feldes aus der Anzeige. Bei Textfeldern nach dem Lesen auch zur weiteren Verarbeitung geeignet, andernfalls nur in Ausnahmefällen.

Spin

boolean

L+S

Drehfeld eingeblendet (bei formatierten Feldern).

TextColor

long

L+S

Textfarbe.

DataField

string

L

Name des Feldes aus der Datenmenge

BoundField

object

L

Objekt, das die Verbindung zur Datenmenge herstellt und vor allem dem Zugriff auf den Feldinhalt dient.

Textfeld weitere Angaben

Englische Bezeichnung: TextField

Name

Datentyp

L/S

Eigenschaft

String

string

L+S

Inhalt des Feldes aus der Anzeige.

MaxTextLen

integer

L+S

Maximale Textlänge.

DefaultText

string

L+S

Standardtext.

MultiLine

boolean

L+S

Mehrzeilig oder einzeilig.

EchoChar

(integer)

L+S

Zeichen für Kennwörter (Passwort-Eingabe verstecken).

Numerisches Feld

Englische Bezeichnung: NumericField

Name

Datentyp

L/S

Eigenschaft

ValueMin

double

L+S

Minimalwert zur Eingabe.

ValueMax

double

L+S

Maximalwert zur Eingabe.

Value

double

L+(S)

Aktueller Wert
nicht für Werte aus der Datenmenge verwenden.

ValueStep

double

L+S

Intervall bei Verwendung mit Mausrad oder Drehfeld.

DefaultValue

double

L+S

Standardwert.

DecimalAccuracy

integer

L+S

Nachkommastellen.

ShowThousandsSeparator

boolean

L+S

Tausender-Trennzeichen anzeigen.

Datumsfeld

Englische Bezeichnung: DateField

Datumswerte werden als Datentyp long definiert und im ISO-Format YYYYMMDD angezeigt, also 20120304 für den 04.03.2012. Zur Verwendung dieses Typs zusammen mit getDate und updateDate sowie dem Typ com.sun.star.util.Date verweisen wir auf die Beispiele.

Name

Daten-typ

Datentyp ab LO 4.1.1

L/S

Eigenschaft

DateMin

long

com.sun.star.util.Date

L+S

Minimalwert zur Eingabe.

DateMax

long

com.sun.star.util.Date

L+S

Maximalwert zur Eingabe.

Date

long

com.sun.star.util.Date

L+(S)

Aktueller Wert
nicht für Werte aus der Datenmenge verwenden.

DateFormat

integer

 

L+S

Datumsformat nach Festlegung des Betriebssystems:
0 = kurze Datumsangabe (einfach)
1 = kurze Datumsangabe tt.mm.jj (Jahr zweistellig)
2 = kurze Datumsangabe tt.mm.jjjj (Jahr vierstellig)
3 = lange Datumsangabe (mit Wochentag und Monatsnamen)
Weitere Möglichkeiten sind der Formulardefinition oder der
API-Referenz zu entnehmen.

DefaultDate

long

com.sun.star.util.Date

L+S

Standardwert.

DropDown

boolean

 

L+S

Aufklappbaren Monatskalender anzeigen.

Zeitfeld

Englische Bezeichnung: TimeField

Auch Zeitwerte werden als Datentyp long definiert.

Name

Daten-typ

Datentyp ab LO 4.1.1

L/S

Eigenschaft

TimeMin

long

com.sun.star.util.Time

L+S

Minimalwert zur Eingabe.

TimeMax

long

com.sun.star.util.Time

L+S

Maximalwert zur Eingabe.

Time

long

com.sun.star.util.Time

L+(S)

Aktueller Wert
nicht für Werte aus der Datenmenge verwenden.

TimeFormat

integer

 

L+S

Zeitformat:
0 = kurz als hh:mm (Stunde, Minute, 24 Stunden)
1 = lang als hh:mm:ss (dazu Sekunden, 24 Stunden)
2 = kurz als hh:mm (12 Stunden AM/PM)
3 = lang als hh:mm:ss (12 Stunden AM/PM)
4 = als kurze Angabe einer Dauer
5 = als lange Angabe einer Dauer

DefaultTime

long

com.sun.star.util.Time

L+S

Standardwert.

Währungsfeld

Englische Bezeichnung: CurrencyField

Ein Währungsfeld ist ein numerisches Feld mit den folgenden zusätzlichen Möglichkeiten.

Name

Datentyp

L/S

Eigenschaft

CurrencySymbol

string

L+S

Währungssymbol (nur zur Anzeige).

PrependCurrencySymbol

boolean

L+S

Anzeige des Symbols vor der Zahl.

Formatiertes Feld

Englische Bezeichnung: FormattedControl

Ein formatiertes Feld wird wahlweise für Zahlen, Währungen oder Datum/Zeit verwendet. Sehr viele der bisher genannten Eigenschaften gibt es auch hier, aber mit anderer Bezeichnung.

Name

Datentyp

L/S

Eigenschaft

CurrentValue

variant

L

Aktueller Wert des Inhalts; der konkrete Datentyp hängt vom Inhalt des Feldes und dem Format ab.

EffectiveValue

L+(S)

EffectiveMin

double

L+S

Minimalwert zur Eingabe.

EffectiveMax

double

L+S

Maximalwert zur Eingabe.

EffectiveDefault

variant

L+S

Standardwert.

FormatKey

long

L+(S)

Format für Anzeige und Eingabe. Es gibt kein einfaches Verfahren, das Format durch ein Makro zu ändern.

EnforceFormat

boolean

L+S

Formatüberprüfung: Bereits während der Eingabe sind nur zulässige Zeichen und Kombinationen möglich.

Listenfeld

Englische Bezeichnung: ListBox

Der Lese- und Schreibzugriff auf den Wert, der hinter der ausgewählten Zeile steht, ist etwas umständlich, aber möglich.

Name

Datentyp

L/S

Eigenschaft

ListSource

array of string

L+S

Datenquelle: Herkunft der Listeneinträge oder Name der Datenmenge, die die Einträge liefert.

ListSourceType

integer

L+S

Art der Datenquelle:
0 = Werteliste
1 = Tabelle
2 = Abfrage
3 = Ergebnismenge eines SQL-Befehls
4 = Ergebnis eines Datenbank-Befehls
5 = Feldnamen einer Datenbank-Tabelle

StringItemList

array of string

L

Listeneinträge, die zur Auswahl zur Verfügung stehen.

ItemCount

integer

L

Anzahl der vorhandenen Listeneinträge.

ValueItemList

array of string

L

Liste der Werte, die über das Formular an die Tabelle weitergegeben werden.

DropDown

boolean

L+S

Aufklappbar.

LineCount

integer

L+S

Anzahl der angezeigten Zeilen im aufgeklappten Zustand.

MultiSelection

boolean

L+S

Mehrfachselektion vorgesehen.

SelectedItems

array of integer

L+S

Liste der ausgewählten Einträge, und zwar als Liste der Positionen in der Liste aller Einträge.

Das (erste) ausgewählte Element aus dem Listenfeld erhält man auf diesem Weg:

  1. 1 oControl = oForm.getByName("Name des Listenfelds") 

  2. 2 sEintrag = oControl.ValueItemList( oControl.SelectedItems(0) ) 

Seit LO 4.1 wird direkt der Wert ermittelt, der bei einem Listenfeld an die Datenbank weitergegeben wird.
  1. 1 oControl = oForm.getByName("Name des Listenfelds") 

  2. 2 iD = oControl.getCurrentValue() 

 

Soll für die Einschränkung einer Auswahlmöglichkeit die Abfrage für ein Listenfeld ausgetauscht werden, so ist dabei zu beachten, dass es sich bei dem Eintrag um ein «array of string» handelt:

  1. 1 SUB Listenfeldfilter 

  2. 2    DIM stSql(0) AS STRING 

  3. 3    DIM oDoc AS OBJECT 

  4. 4    DIM oDrawpage AS OBJECT 

  5. 5    DIM oForm AS OBJECT 

  6. 6    DIM oFeld AS OBJECT 

  7. 7    oDoc = thisComponent 

  8. 8    oDrawpage = oDoc.drawpage 

  9. 9    oForm = oDrawpage.forms.getByName("MainForm") 

  10. 10    oFeld = oForm.getByname("Listenfeld") 

  11. 11    stSql(0) = "SELECT ""Name"", ""ID"" FROM ""Filter_Name"" ORDER BY ""Name""" 

  12. 12    oFeld.ListSource = stSql 

  13. 13    oFeld.refresh 

  14. 14 END SUB 

Soll der gerade geänderte Wert eines Listenfeldes ausgelesen werden, der noch nicht im Formular abgespeichert ist, so geht dies über die Listenposition:
  1. 1 SUB Kontofilter_Feldstart(oEvent AS OBJECT) 

  2. 2    DIM oFeld AS OBJECT 

  3. 3    DIM inID AS INTEGER 

  4. 4    oFeld = oEvent.Source.Model 

  5. 5    inID = oFeld.ValueItemList(oEvent.Selected)  

  6. 6    ... 

  7. 7 END SUB 

 

Kombinationsfeld

Englische Bezeichnung: ComboBox

Trotz ähnlicher Funktionalität wie beim Listenfeld weichen die Eigenschaften teilweise ab.

Hier verweisen wir ergänzend auf das Beispiel Kombinationsfelder als Listenfelder mit Eingabemöglichkeit.

Name

Datentyp

L/S

Eigenschaft

Autocomplete

boolean

L+S

Automatisch füllen.

StringItemList

array of string

L+S

Listeneinträge, die zur Auswahl zur Verfügung stehen.

ItemCount

integer

L

Anzahl der vorhandenen Listeneinträge.

DropDown

boolean

L+S

Aufklappbar.

LineCount

integer

L+S

Anzahl der angezeigten Zeilen im aufgeklappten Zustand.

Text

string

L+S

Aktuell angezeigter Text.

DefaultText

string

L+S

Standardeintrag.

ListSource

string

L+S

Name der Datenquelle, die die Listeneinträge liefert.

ListSourceType

integer

L+S

Art der Datenquelle; gleiche Möglichkeiten wie beim Listenfeld (nur die Auswahl «Werteliste» wird ignoriert).

Markierfeld, Optionsfeld

Englische Bezeichnungen: CheckBox (Markierfeld) bzw. RadioButton (Optionsfeld; auch «Option Button» möglich)

Name

Datentyp

L/S

Eigenschaft

Label

string

L+S

Titel (Beschriftung)

State

short

L+S

Status
0 = nicht ausgewählt
1 = ausgewählt
2 = unbestimmt

MultiLine

boolean

L+S

Wortumbruch (bei zu langem Text).

RefValue

string

L+S

Referenzwert

Maskiertes Feld

Englische Bezeichnung: PatternField

Neben den Eigenschaften für «einfache» Textfelder sind folgende interessant.

Name

Datentyp

L/S

Eigenschaft

EditMask

string

L+S

Eingabemaske.

LiteralMask

string

L+S

Zeichenmaske.

StrictFormat

boolean

L+S

Formatüberprüfung bereits während der Eingabe.

Tabellenkontrollfeld

Englische Bezeichnung: GridControl

Name

Datentyp

L/S

Eigenschaft

Count

long

L

Anzahl der Spalten.

ElementNames

array of string

L

Liste der Spaltennamen.

HasNavigationBar

boolean

L+S

Navigationsleiste vorhanden.

RowHeight

long

L+S

Zeilenhöhe.

Beschriftungsfeld

Englische Bezeichnung: FixedText auch Label ist üblich

Name

Datentyp

L/S

Eigenschaft

Label

string

L+S

Der angezeigte Text.

MultiLine

boolean

L+S

Wortumbruch (bei zu langem Text).

Gruppierungsrahmen

Englische Bezeichnung: GroupBox

Keine Eigenschaft dieses Kontrollfelds wird üblicherweise durch Makros bearbeitet. Wichtig ist der Status der einzelnen Optionsfelder.

Schaltfläche

Englische Bezeichnungen: CommandButton für die grafische Schaltfläche ImageButton

Name

Datentyp

L/S

Eigenschaft

Label

string

L+S

Titel Text der Beschriftung.

State

short

L+S

Standardstatus «ausgewählt» bei «Umschalten».

MultiLine

boolean

L+S

Wortumbruch (bei zu langem Text).

DefaultButton

boolean

L+S

Standardschaltfläche

Navigationsleiste

Englische Bezeichnung: NavigationBar

Weitere Eigenschaften und Methoden, die mit der Navigation zusammenhängen z.B. Filter und das Ändern des Datensatzzeigers –, werden über das Formular geregelt.

Name

Datentyp

L/S

Eigenschaft

IconSize

short

L+S

Symbolgröße.

ShowPosition

boolean

L+S

Positionierung anzeigen und eingeben.

ShowNavigation

boolean

L+S

Navigation ermöglichen.

ShowRecordActions

boolean

L+S

Datensatzaktionen ermöglichen.

ShowFilterSort

boolean

L+S

Filter und Sortierung ermöglichen.

Methoden bei Formularen und Kontrollfeldern

Die Datentypen der Parameter werden durch Kürzel angedeutet:

In einer Datenmenge navigieren

Diese Methoden gelten sowohl für ein Formular als auch für die Ergebnismenge einer Abfrage.

Mit «Cursor» ist in den Beschreibungen der Datensatzzeiger gemeint.

Name

Datentyp

Beschreibung

Prüfungen für die Position des Cursors

isBeforeFirst

boolean

Der Cursor steht vor der ersten Zeile, wenn der Cursor nach dem Einlesen noch nicht gesetzt wurde.

isFirst

boolean

Gibt an, ob der Cursor auf der ersten Zeile steht.

isLast

boolean

Gibt an, ob der Cursor auf der letzten Zeile steht.

isAfterLast

boolean

Der Cursor steht hinter der letzten Zeile, wenn er von der letzten Zeile aus mit next weiter gesetzt wurde.

getRow

long

Nummer der aktuellen Zeile

Setzen des Cursors
Beim Datentyp boolean steht das Ergebnis «true» dafür, dass das Navigieren erfolgreich war.

beforeFirst

Wechselt vor die erste Zeile.

first

boolean

Wechselt zur ersten Zeile.

previous

boolean

Geht um eine Zeile zurück.

next

boolean

Geht um eine Zeile vorwärts.

last

boolean

Wechselt zur letzten Zeile.

afterLast

Wechselt hinter die letzte Zeile.

absolute(n)

boolean

Geht zu der Zeile mit der angegebenen Nummer.

relative(n)

boolean

Geht um eine bestimmte Anzahl von Zeilen weiter:
bei positivem Wert von n vorwärts, andernfalls zurück.

Maßnahmen zum Status der aktuellen Zeile

refreshRow

Liest die Werte der aktuellen Zeile neu ein. Nach dem Abspeichern der Zeile sind das auch die aktuellen Werte.

rowInserted

boolean

Gibt an, ob es sich um eine neue Zeile handelt.

rowUpdated

boolean

Gibt an, ob die aktuelle Zeile geändert wurde.

rowDeleted

boolean

Gibt an, ob die aktuelle Zeile gelöscht wurde.

Datenzeilen bearbeiten

Die Methoden zum Lesen stehen bei jedem Formular und bei einer Ergebnismenge zur Verfügung. Die Methoden zum Ändern und Speichern gibt es nur bei einer Datenmenge, die geändert werden kann (in der Regel also nur bei Tabellen, nicht bei Abfragen).

Name

Datentyp

Beschreibung

Maßnahmen für die ganze Zeile

insertRow

Speichert eine neue Zeile.

updateRow

Bestätigt Änderungen der aktuellen Zeile.

deleteRow

Löscht die aktuelle Zeile.

cancelRowUpdates

Macht Änderungen der aktuellen Zeile rückgängig.

moveToInsertRow

Wechselt den Cursor in die Zeile für einen neuen Datensatz.

moveToCurrentRow

Kehrt nach der Eingabe eines neuen Datensatzes zurück zur vorherigen Zeile.

 

Name

Datentyp

Beschreibung

Werte lesen

getString(c)

string

Liefert den Inhalt der Spalte als Zeichenkette. Kann auch zum Auslesen aller anderen Spalten genutzt werden und hat den Vorteil, dass leere Felder direkt bestimmt werden können. Strings können immer noch später in andere Datentypen umgewandelt werden.

getBoolean(c)

boolean

Liefert den Inhalt der Spalte als Wahrheitswert.

getByte(c)

byte

Liefert den Inhalt der Spalte als einzelnes Byte.

getShort(c)

short

Liefert den Inhalt der Spalte als ganze Zahl.

getInt(c)

integer

getLong(c)

long

getFloat(c)

float

Liefert den Inhalt der Spalte als Dezimalzahl von einfacher Genauigkeit.

getDouble(c)

double

Liefert den Inhalt der Spalte als Dezimalzahl von doppelter Genauigkeit. Wegen der automatischen Konvertierung durch Basic ist dies auch für decimal- und currency-Werte geeignet.

getBytes(c)

array of bytes

Liefert den Inhalt der Spalte als Folge einzelner Bytes.

getDate(c)

Date

Liefert den Inhalt der Spalte als Datumswert.

getTime(c)

Time

Liefert den Inhalt der Spalte als Zeitwert.

getTimestamp(c)

DateTime

Liefert den Inhalt der Spalte als Zeitstempel (Datum und Zeit).

In Basic selbst werden Datums- und Zeitwerte einheitlich mit dem Datentyp DATE verarbeitet. Für den Zugriff auf die Datenmenge gibt es verschiedene Datentypen: com.sun.star.util.Date für ein Datum, com.sun.star.util.Time für eine Zeit, com.sun.star.util.DateTime für einen Zeitstempel.

getBinaryStream(c)

Object

Liest den Inhalt eines Binärfeldes (z.B. Bild) aus. So ein Inhalt könnte als Datei gespeichert werden.

wasNull

boolean

Gibt an, ob der Wert der zuletzt gelesenen Spalte NULL war. Beim Auslesen z.B. mit getInt wird sonst für ein leeres Feld grundsätzlich '0' weitergegeben.

Werte speichern

updateNull(c)

Setzt den Inhalt der Spalte c auf NULL.

updateBoolean(c,b)

Setzt den Inhalt der Spalte c auf den Wahrheitswert b.

updateByte(c,x)

Speichert in Spalte c das angegebene Byte x.

updateShort(c,n)

Speichert in Spalte c die angegebene ganze Zahl n.

updateInt(c,n)

updateLong(c,n)

updateFloat(c,n)

Speichert in Spalte c die angegebene Dezimalzahl n.

updateDouble(c,n)

updateString(c,s)

Speichert in Spalte c die angegebene Zeichenkette s.

updateBytes(c,x)

Speichert in Spalte c das angegebene Byte-Array x.

updateDate(c,d)

Speichert in Spalte c das angegebene Datum d.

updateTime(c,d)

Speichert in Spalte c den angegebenen Zeitwert d.

updateTimestamp(c,d)

Speichert in Spalte c den angegeb. Zeitstempel d.

Einzelne Werte bearbeiten

Mit diesen Methoden wird über BoundField aus einem Kontrollfeld der Inhalt der betreffenden Spalte gelesen oder geändert. Diese Methoden entsprechen fast vollständig denen im vorigen Abschnitt; die Angabe der Spalte entfällt.

Name

Datentyp

Beschreibung

Werte lesen

getString

string

Liefert den Inhalt der Spalte als Zeichenkette.

getBoolean

boolean

Liefert den Inhalt der Spalte als Wahrheitswert.

getByte

byte

Liefert den Inhalt der Spalte als einzelnes Byte.

getShort

short

Liefert den Inhalt der Spalte als ganze Zahl.

getInt

integer

getLong

long

getFloat

float

Liefert den Inhalt der Spalte als Dezimalzahl von einfacher Genauigkeit.

getDouble

double

Liefert den Inhalt der Spalte als Dezimalzahl von doppelter Genauigkeit. Wegen der automatischen Konvertierung durch Basic ist dies auch für decimal- und currency-Werte geeignet.

getBytes

array of bytes

Liefert den Inhalt der Spalte als Folge einzelner Bytes.

getDate

Date

Liefert den Inhalt der Spalte als Datumswert.

getTime

Time

Liefert den Inhalt der Spalte als Zeitwert.

getTimestamp

DateTime

Liefert den Inhalt der Spalte als Zeitstempel (Datum und Zeit).

In Basic selbst werden Datums- und Zeitwerte einheitlich mit dem Datentyp DATE verarbeitet. Für den Zugriff auf die Datenmenge gibt es verschiedene Datentypen: com.sun.star.util.Date für ein Datum, com.sun.star.util.Time für eine Zeit, com.sun.star.util.DateTime für einen Zeitstempel.

getBinaryStream(c)

Object

Liest den Inhalt eines Binärfeldes (z.B. Bild) aus. So ein Inhalt könnte als Datei gespeichert werden.

wasNull

boolean

Gibt an, ob der Wert der zuletzt gelesenen Spalte NULL war.

Werte speichern

updateNull

Setzt den Inhalt der Spalte auf NULL.

updateBoolean(b)

Setzt den Inhalt der Spalte auf den Wahrheitswert b.

updateByte(x)

Speichert in der Spalte das angegebene Byte x.

updateShort(n)

Speichert in der Spalte die angegebene ganze Zahl n.

updateInt(n)

updateLong(n)

updateFloat(n)

Speichert in der Spalte die angegebene Dezimalzahl n.

updateDouble(n)

updateString(s)

Speichert in der Spalte die angegebene Zeichenkette s.

updateBytes(x)

Speichert in der Spalte das angegebene Byte-Array x.

updateDate(d)

Speichert in der Spalte das angegebene Datum d.

updateTime(d)

Speichert in der Spalte den angegebenen Zeitwert d.

updateTimestamp(d)

Speichert in der Spalte den angegebenen Zeitstempel d.

Parameter für vorbereitete SQL-Befehle

Die Methoden, mit denen die Werte einem vorbereiteten SQL-Befehl siehe Vorbereitete SQL-Befehle mit Parametern  übergeben werden, sind ähnlich denen der vorigen Abschnitte. Der erste Parameter mit i bezeichnet nennt seine Nummer (Position) innerhalb des SQL-Befehls.

Name

Datentyp

Beschreibung

setNull(i, n)

Setzt den Inhalt der Spalte auf NULL
n bezeichnet den SQL-Datentyp gemäß API-Referenz.

setBoolean(i, b)

Fügt den angegebenen Wahrheitswert b in den SQL-Befehl ein.

setByte(i, x)

Fügt das angegebene Byte x in den SQL-Befehl ein.

setShort(i, n)

Fügt die angegebene ganze Zahl n in den SQL-Befehl ein.

setInt(i, n)

setLong(i, n)

setFloat(i, n)

Fügt die angegebene Dezimalzahl n in den SQL-Befehl ein.

setDouble(i, n)

setString(i, s)

Fügt die angegebene Zeichenkette s in den SQL-Befehl ein.

setBytes(i, x)

Fügt das angegebene Byte-Array x in den SQL-Befehl ein.

setDate(i, d)

Fügt das angegebene Datum d in den SQL-Befehl ein.

setTime(i, d)

Fügt den angegebenen Zeitwert d in den SQL-Befehl ein.

setTimestamp(i, d)

Fügt den angegebenen Zeitstempel d in den SQL-Befehl ein.

clearParameters

Entfernt die bisherigen Werte aller Parameter eines SQL-Befehls.

Arbeit mit UNO-Befehlen in Formularen

Über den Makrorekorder können die Befehle ausgelesen werden, die z. B. mit den Buttons aus der Navigationsleiste der Formulare verbunden sind. Diese Befehle haben häufig eine umfassendere Funktion als die Funktionen, die sonst für Makros vorgesehen sind.

  1. 1 SUB FormularNeuLadenKontrollfelderAktualisieren 

  2. 2    DIM oDocument AS OBJECT 

  3. 3    DIM oDispatcher AS OBJECT 

  4. 4    DIM Array() 

  5. 5    oDocument = ThisComponent.CurrentController.Frame 

  6. 6    oDispatcher = createUnoService("com.sun.star.frame.DispatchHelper") 

  7. 7    oDispatcher.executeDispatch(oDocument, ".uno:Refresh", "", 0, Array()) 

  8. 8 END SUB 

Über den Dispatcher wird das Formular neu geladen und die Formularfelder aktualisiert. Würde nur das Formular über oForm.reload() neu eingelesen, nachdem aus einem anderen Formular heraus der Inhalt eines Listenfeldes geändert wurde, so würde die Änderung in dem Formular nicht angezeigt. Hier müsste auch noch jedes einzelne Feld mit oFeld.refresh() neu eingelesen werden.

Auch das Sichern eines Datensatz ist über .uno:RecSave einfacher als mittels der direkten Ansprache des Formulars. Bei der direkten Ansprache des Formulars muss erst geklärt werden, ob es sich um einen neuen Datensatz handelt, für den dann ein Insert durchgeführt wird, oder ob es sich um einen bestehenden Datensatz handelt, der dann ein Update erfordert.

Eine Übersicht über verschiedene UNO-Befehle befindet sich im Anhang des Handbuches. UNO-Befehle können auch z. B. über Extras → Anpassen → Symbolleisten → Beschreibung ermittelt werden.

Bedienbarkeit verbessern

Als erste Kategorie werden verschiedene Möglichkeiten vorgestellt, die zur Verbesserung der Bedienbarkeit von Base-Formularen dienen. Sofern nicht anders erwähnt, sind diese Makros Bestandteil der Beispieldatenbank «Medien_mit_Makros.odb».

Automatisches Aktualisieren von Formularen

Oft wird in einem Formular etwas geändert und in einem zweiten, auf der gleichen Seite liegenden Formular, soll die Änderung anschließend erscheinen. Hier hilft bereits ein kleiner Codeschnipsel, um das betreffende Anzeigeformular zu aktualisieren.

  1. 1 SUB Aktualisieren 

Zuerst wird einmal das Makro benannt. Die Standardbezeichnung für ein Makro ist SUB. Dies kann groß oder klein geschrieben sein, Mit SUB wird eine Prozedur durchgeführt, die nach außen in der Regel keinen Wert zurück gibt. Weiter unten wird im Gegensatz dazu einmal eine Funktion beschrieben, die im Unterschied dazu Rückgabewerte erzeugt.

Das Makro hat jetzt den Namen «Aktualisieren». Um sicher zu gehen, dass keine Variablen von außen eingeschleust werden, gehen viele Programmierer so weit, dass sie Basic über Option Explicit gleich zu Beginn mitteilen: Erzeuge nicht automatisch irgendwelche Variablen, sondern nutze nur die, die ich auch vorher definiert habe.

Deshalb werden jetzt standardmäßig erst einmal die Variablen deklariert. Bei allen hier deklarierten Variablen handelt es sich um Objekte (nicht z.B. Zahlen oder Texte), so dass der Zusatz AS OBJECT hinter der Deklaration steht. Um später noch zu erkennen, welchen Typ eine Variable hat, ist vor die Variablenbezeichnung ein « gesetzt worden. Prinzipiell ist aber die Variablenbezeichnung nahezu völlig frei wählbar.

  1. 2    DIM oDoc AS OBJECT 

  2. 3    DIM oDrawpage AS OBJECT 

  3. 4    DIM oForm AS OBJECT 

Das Formular liegt in dem momentan aktiven Dokument. Der Behälter, in dem alle Formulare aufbewahrt werden, wird als Drawpage bezeichnet. Im Formularnavigator ist dies sozusagen der oberste Begriff, an den dann sämtliche Formulare angehängt werden.

Das Formular, auf das zugegriffen werden soll, ist hier mit den Namen "Anzeige" versehen. Dies ist der Name, der auch im Formularnavigator sichtbar ist. So hat z.B. das erste Formular standardmäßig erst einmal den Namen "MainForm".

  1. 5    oDoc = thisComponent 

  2. 6    oDrawpage = oDoc.Drawpage 

  3. 7    oForm = oDrawpage.forms.getByName("Anzeige") 

Nachdem das Formular jetzt ansprechbar ist und der Punkt, an dem es angesprochen wurde, in der Variablen oForm gespeichert wurde, wird es jetzt mit dem Befehl reload() neu geladen.

  1. 8    oForm.reload() 

  2. 9 END SUB 

Die Prozedur hat mit SUB begonnen. Sie wird mit END SUB beendet.

Dieses Makro kann jetzt z.B. ausgelöst werden, wenn die Abspeicherung in einem anderen Formular erfolgt. Wird z.B. in einem Kassenformular an einer Stelle die Anzahl der Gegenstände und (über Barcodescanner) die Nummer eingegeben, so kann in einem anderen Formular im gleichen geöffneten Fenster hierdurch der Kassenstand, die Bezeichnung der Ware usw. nach dem Abspeichern sichtbar gemacht werden.

Filtern von Datensätzen

Der Filter selbst funktioniert ja schon ganz ordentlich in einer weiter oben beschriebenen Variante im Kapitel «Datenfilterung». Die untenstehende Variante ersetzt den Abspeicherbutton und liest die Listenfelder neu ein, so dass ein gewählter Filter aus einem Listenfeld die Auswahl in dem anderen Listenfeld einschränken kann.2
  1. 1 SUB Filter 

  2. 2    DIM oDoc AS OBJECT 

  3. 3    DIM oDrawpage AS OBJECT 

  4. 4    DIM oForm1 AS OBJECT 

  5. 5    DIM oForm2 AS OBJECT 

  6. 6    DIM oFeldList1 AS OBJECT 

  7. 7    DIM oFeldList2 AS OBJECT 

  8. 8    oDoc = thisComponent 

  9. 9    oDrawpage = oDoc.drawpage 

Zuerst werden die Variablen definiert und auf das Gesamtformular zugegriffen. Das Gesamtformular besteht aus den Formularen "Filter" und "Anzeige". Die Listenfelder befinden sich in dem Formular "Filter" und sind mit dem Namen "Liste_1" und "Liste_2" versehen.

  1. 10    oForm1 = oDrawpage.forms.getByName("Filter") 

  2. 11    oForm2 = oDrawpage.forms.getByName("Anzeige") 

  3. 12    oFeldList1 = oForm1.getByName("Liste_1") 

  4. 13    oFeldList2 = oForm1.getByName("Liste_2") 

Zuerst wird der Inhalt der Listenfelder an das darunterliegende Formular mit commit() weitergegeben. Die Weitergabe ist notwendig, da ansonsten die Änderung eines Listenfeldes bei der Speicherung nicht berücksichtigt wird. Genau genommen müsste der commit() nur auf dem Listenfeld ausgeführt werden, das gerade betätigt wurde. Danach wird der Datensatz mit updateRow() abgespeichert. Es existiert ja in unserer Filtertabelle prinzipiell nur ein Datensatz, und der wird zu Beginn einmal geschrieben. Dieser Datensatz wird also laufend durch ein Update-Kommando überschrieben.

  1. 14    oFeldList1.commit() 

  2. 15    oFeldList2.commit() 

  3. 16    oForm1.updateRow() 

Die Listenfelder sollen einander beeinflussen. Wird in einem Listenfeld z.B. eingegrenzt, dass an Medien nur CDs angezeigt werden sollen, so muss das andere Listenfeld bei den Autoren nicht noch sämtliche Buchautoren auflisten. Eine Auswahl im 2. Listenfeld hätte dann allzu häufig ein leeres Filterergebnis zur Folge. Daher müssen die Listenfelder jetzt neu eingelesen werden. Genau genommen müsste der refresh() nur auf dem Listenfeld ausgeführt werden, das gerade nicht betätigt wurde.

Anschließend wird das Formular2, das den gefilterten Inhalt anzeigen soll, neu geladen.

  1. 17    oFeldList1.refresh() 

  2. 18    oFeldList2.refresh() 

  3. 19    oForm2.reload() 

  4. 20 END SUB 

Soll mit diesem Verfahren ein Listenfeld von der Anzeige her beeinflusst werden, so kann das Listenfeld mit Hilfe verschiedener Abfragen bestückt werden.

Die einfachste Variante ist, dass sich die Listenfelder mit ihrem Inhalt aus dem Filterergebnis versorgen. Dann bestimmt der eine Filter, aus welchen Datenbestand anschließend weiter gefiltert werden kann.

  1. 1 SELECT  

  2. 2    "Feld_1" || ' - ' || "Anzahl" AS "Anzeige",  

  3. 3    "Feld_1"  

  4. 4 FROM  

  5. 5    ( SELECT COUNT( "ID" ) AS "Anzahl", "Feld_1" FROM "Suchtabelle"
         GROUP BY "Feld_1" )  

  6. 6 ORDER BY "Feld_1" 

Es wird der Feldinhalt und die Trefferzahl angezeigt. Um die Trefferzahl zu errechnen, wird eine Unterabfrage gestellt. Dies ist notwendig, da sonst nur die Trefferzahl ohne weitere Information aus dem Feld in der Listbox angezeigt würde.

Das Makro erzeugt durch dieses Vorgehen ganz schnell Listboxen, die nur noch mit einem Wert gefüllt sind. Steht eine Listbox nicht auf NULL, so wird sie schließlich bei der Filterung bereits berücksichtigt. Nach Betätigung der 2. Listbox stehen also bei beiden Listboxen nur noch die leeren Felder und jeweils 1 angezeigter Wert zur Verfügung. Dies mag für eine eingrenzende Suche erst einmal praktisch erscheinen. Was aber, wenn z.B. in einer Bibliothek die Zuordnung zur Systematik klar war, aber nicht eindeutig, ob es sich um ein Buch, eine CD oder eine DVD handelt? Wurde einmal die Systematik angewählt und dann die 2. Listbox auf CD gestellt so muss, um auch die Bücher zu sehen, die 2. Listbox erst einmal wieder auf NULL gestellt werden, um dann auch die Bücher anwählen zu können. Praktischer wäre, wenn die 2. Listbox direkt die verschiedenen Medienarten anzeigen würde, die zu der Systematik zur Verfügung stehen – natürlich mit den entsprechenden Trefferquoten.

Um dies zu erreichen, wurde die folgende Abfrage konstruiert, die jetzt nicht mehr direkt aus dem Filterergebnis gespeist wird. Die Zahlen für die Treffer müssen anders ermittelt werden.

  1. 1 SELECT  

  2. 2    COALESCE( "Feld_1" || ' - ' || "Anzahl", 'leer - ' || "Anzahl" )
         AS "Anzeige",  

  3. 3    "Feld_1"  

  4. 4 FROM  

  5. 5    ( SELECT COUNT( "ID" ) AS "Anzahl", "Feld_1" FROM "Tabelle"  

  6. 6    WHERE "ID" IN  

  7. 7       ( SELECT "Tabelle"."ID" FROM "Filter", "Tabelle"  

  8. 8          WHERE "Tabelle"."Feld_2" = COALESCE( "Filter"."Filter_2",  

  9. 9          "Tabelle"."Feld_2" ) )  

  10. 10    GROUP BY "Feld_1"  

  11. 11    ) 

  12. 12 ORDER BY "Feld_1" 

Diese doch sehr verschachtelte Abfrage kann auch unterteilt werden. In der Praxis bietet es sich häufig an, die Unterabfrage in einer Tabellenansicht ('VIEW') zu erstellen. Das Listenfeld bekommt seinen Inhalt dann über eine Abfrage, die sich auf diesen 'VIEW' bezieht.

Die Abfrage im Einzelnen:

Die Abfrage stellt 2 Spalten dar. Die erste Spalte enthält die Ansicht, die die Person sieht, die das Formular vor sich hat. In der Ansicht werden die Inhalte des Feldes und, mit einem Bindestrich abgesetzt, die Treffer zu diesem Feldinhalt gezeigt. Die zweite Spalte gibt ihren Inhalt an die zugrundeliegende Tabelle des Formulars weiter. Hier steht nur der Inhalt des Feldes. Die Listenfelder beziehen ihre Inhalte dabei aus der Abfrage, die als Filterergebnis im Formular dargestellt wird. Nur diese Felder stehen schließlich zur weiteren Filterung zur Verfügung.

Als Tabelle, aus der diese Informationen gezogen werden, liegt eine Abfrage vor. In dieser Abfrage werden die Primärschlüsselfelder gezählt (SELECT COUNT( "ID" ) AS "Anzahl"). Dies geschieht gruppiert nach der Bezeichnung, die in dem Feld steht (GROUP BY "Feld_1"). Als zweite Spalte stellt diese Abfrage das Feld selbst als Begriff zur Verfügung. Diese Abfrage wiederum basiert auf einer weiteren Unterabfrage:

  1. 1 SELECT "Tabelle"."ID"  

  2. 2 FROM "Filter", "Tabelle"  

  3. 3 WHERE "Tabelle"."Feld_2" = COALESCE( "Filter"."Filter_2",
      "Tabelle"."Feld_2" ) 

Diese Unterabfrage bezieht sich jetzt auf das andere zu filternde Feld. Prinzipiell muss das andere zu filternde Feld auch zu den Primärschlüsselnummern passen. Sollten noch mehrere weitere Filter existieren, so ist diese Unterabfrage zu erweitern:

  1. 1 SELECT "Tabelle"."ID"  

  2. 2 FROM "Filter", "Tabelle"  

  3. 3 WHERE "Tabelle"."Feld_2" = COALESCE( "Filter"."Filter_2",
         "Tabelle"."Feld_2" ) 

  4. 4    AND 

  5. 5    "Tabelle"."Feld_3" = COALESCE( "Filter"."Filter_3",
         "Tabelle"."Feld_3" ) 

Alle weiteren zu filternden Felder beeinflussen, was letztlich in dem Listenfeld des ersten Feldes, "Feld_1", angezeigt wird.

Zum Schluss wird die gesamte Abfrage nur noch nach dem zugrundeliegenden Feld sortiert.

Wie letztlich die Abfrage aussieht, die dem anzuzeigenden Formular zugrunde liegt, ist im Kapitel «Datenfilterung» nachzulesen.

Mit dem folgenden Makro kann über das Listenfeld gesteuert werden, welches Listenfeld abgespeichert werden muss und welches neu eingelesen werden muss.

Die Variablen für das Array werden in den Eigenschaften des Listenfeldes unter Zusatzinformationen abgelegt. Die erste Variable enthält dort immer den Namen des Listenfeldes selbst, die weiteren Variablen die Namen aller anderen Listenfelder, getrennt durch Kommata.

  1. 1 SUB Filter_Zusatzinfo(oEvent AS OBJECT) 

  2. 2    DIM oDoc AS OBJECT 

  3. 3    DIM oDrawpage AS OBJECT 

  4. 4    DIM oForm1 AS OBJECT 

  5. 5    DIM oForm2 AS OBJECT 

  6. 6    DIM stTag AS String 

  7. 7    stTag = oEvent.Source.Model.Tag 

Ein Array (Ansammlung von Daten, die hier über Zahlenverbindungen abgerufen werden können) wird gegründet und mit den Feldnamen der Listenfelder gefüllt. Der erste Name ist der Name des Listenfelds, das mit der Aktion (Event) verbunden ist.

  1. 8    aList() = Split(stTag, ",") 

  2. 9    oDoc = thisComponent 

  3. 10    oDrawpage = oDoc.drawpage 

  4. 11    oForm1 = oDrawpage.forms.getByName("Filter") 

  5. 12    oForm2 = oDrawpage.forms.getByName("Anzeige") 

Das Array wird von seiner Untergrenze (LBound()) bis zu seiner Obergrenze (UBound()) in einer Schleife durchlaufen. Alle Werte, die in den Zusatzinformationen durch Komma getrennt erschienen, werden jetzt nacheinander weitergegeben.

  1. 13    FOR i = LBound(aList()) TO Ubound(aList()) 

  2. 14       IF i = 0 THEN 

Das auslösende Listenfeld muss abgespeichert werden. Es hat die Variable aList(0). Zuerst wird die Information des Listenfeldes auf die zugrundeliegende Tabelle übertragen, dann wird der Datensatz gespeichert.

  1. 15          oForm1.getByName(aList(i)).commit() 

  2. 16          oForm1.updateRow() 

  3. 17       ELSE 

Die anderen Listenfelder müssen neu eingelesen werden, da sie ja in Abhängigkeit vom ersten Listenfeld jetzt andere Werte abbilden.

  1. 18          oForm1.getByName(aList(i)).refresh() 

  2. 19       END IF 

  3. 20    NEXT 

  4. 21    oForm2.reload() 

  5. 22 END SUB 

Die Abfragen für dieses besser nutzbare Makro sind natürlich die gleichen wie in diesem Abschnitt zuvor bereits vorgestellt.

Daten über den Formularfilter filtern

Alternativ zu dieser Vorgehensweise ist es auch möglich, die Filterfunktion des Formulars direkt zu bearbeiten.

  1. 1 SUB FilterSetzen 

  2. 2    DIM oDoc AS OBJECT 

  3. 3    DIM oForm AS OBJECT 

  4. 4    DIM oFeld  AS OBJECT 

  5. 5    DIM stFilter As String 

  6. 6    oForm = thisComponent.Drawpage.Forms.getByName("MainForm") 

  7. 7    oFeld = oForm.getByName("Filter") 

  8. 8    stFilter = oFeld.Text 

  9. 9    oForm.filter = " UPPER(""Name"") LIKE '%'||'" + UCase(stFilter) + "'||'%'" 

  10. 10    oForm.ApplyFilter = TRUE 

  11. 11    oForm.reload() 

  12. 12 End Sub 

Das Feld wird im Formular aufgesucht, der Inhalt ausgelesen. Der Filter wird entsprechend gesetzt. Die Filterung wird angeschaltet und das Formular neu geladen.

  1. 1 SUB FilterEntfernen 

  2. 2    DIM oForm AS OBJECT 

  3. 3    oForm = thisComponent.Drawpage.Forms.getByName("MainForm") 

  4. 4    oForm.ApplyFilter = False 

  5. 5    oForm.reload() 

  6. 6 END SUB 

Die Beendigung des Filters kann natürlich auch über die Navigationsleiste erfolgen. In diesem Fall wird einfach ein weiteres Makro genutzt.

Über diese Filterfunktion kann ein Formular auch direkt mit einem Filter z. B. für nur einen Datensatz gestartet werden. Aus dem startenden Formular wird ein Wert (z.B. ein Primärschlüssel für den aktuellen Datensatz) ausgelesen und an das Zielformular als Filterwert weiter gegeben.

Filterdialog über einen Button starten

Wird ein Formular geöffnet, so stehen über die Navigationsleiste verschiedene Filtermöglichkeiten zur Verfügung. Während in Tabellen, Abfaregn und über das Formularkontrollelement ein Filterdialog zur Verfügung steht, ist dieser über die Navigationsleiste des Formularfensters durch den Formularbasierten Filter ersetzt worden. Auch über eine einfache Schaltfläche lässt sich dieser Dialog nicht starten. Hier kann zur Zeit nur mit einem Makro nachgeholfen werden.

  1. 1 SUB FilterDefault(oEvent AS OBJECT) 

  2. 2         oForm = oEvent.Source.Model.Parent 

  3. 3         oController = thisComponent.getCurrentController() 

  4. 4         oFormController = oController.getFormController(oForm) 

  5. 5         oFormController.FormOperations.execute(16) 

  6. 6 END SUB 

Das Makro wird von der Schaltfläche aus über Ereignisse → Aktion ausführen gestartet. Über das auslösende Ereignis wird das zugrundeliegende Formular ermittelt. Anschließend wird über den Controller die Kontrolle über die Elemente des Formulars übernommen, die dort prinzipiell zur Verfügung stehen. Zu den möglichen FormOperations gehört mit der Short-Variablen 16 der Dialog zum Setzen eines Filters.

 

Der Filterdialog kann über den Button gestartet werden und die Filterundg wird mit OK direkt vollzogen.

Die folgenden Variablen stehen über die com::sun::star::form::runtime::FormFeature Constant Group Reference zur Verfügung:

Nr.

Befehl

Bedeutung

1

MoveAbsolute

  1. 5 DIM v as new com.sun.star.beans.NamedValue 

  2. 6 v.Name = "Position" 

  3. 7 v.Value = 5 

  4. 8 oFormController.FormOperations.executeWithArguments(1, Array(v)) 

Gehe zu dem Datensatz, der über ein Array angegeben werden muss

2

TotalRecord

  1. 5 oState = oFormController.FormOperations.getState(2) 

  2. 6 Msgbox "Gesamtzahl der Datensätze: "& cStr(oState.State) 

Zeige die Anzahl der gesamten Datensätze an – leider auch hier nur wie in der Navigationsleiste mit '41 *'

3

MoveToFirst

Gehe zum ersten Datensatz

4

MoveToPrevious

Gehe zum vorhergehenden Datensatz

5

MoveToNext

Gehe zum nächsten Datensatz

6

MoveToLast

Gehe zum letzten Datensatz

7

MoveToInsertRow

Gehe zum Einfügen eines neuen Datensatzes

8

SaveRecordChanges

Speichere die Datensatzänderung

9

UndoRecordChanges

Mache die Änderungen rückgängig

10

DeleteRecord

Lösche den Datensatz

11

ReloadForm

Lies das Formular neu ein

12

SortAscending

Sortiere aufsteigend

13

SortDescending

Sortiere absteigend

14

InteractiveSort

Öffne Dialog zum Sortieren

15

AutoFilter

Automatischer Filter, vorher in ein gewünschtes Feld klicken.

16

InteractiveFilter

Öffne Dialog zum Filtern

17

ToggleApplyFilter

Stelle den Filter ein oder aus

18

RemoveFilterAndSort

Nimm Filterung und Sortierung zurück

19

RefreshCurrentControl

Lies den Inhalt des Listenfeldes oder Kombinationsfeldes neu ein.

Durch Datensätze mit der Bildlaufleiste scrollen

Die Bildlaufleiste lässt sich nur über Makros nutzen. Das folgende Beispiel3 zeigt auf, wie mit so einer Bildlaufleiste durch Datensätze gescrollt werden kann. Die Bildlaufleiste kann dann statt der Navigationsleiste zur Navigation durch die Datensätze genutzt werden.
  1. 1 GLOBAL loPos AS LONG 

Die Position des aktuellen Datensatzes wird als globale Variable gespeichert, damit sie aus allen Prozeduren gelesen und in allen Prozeduren geändert werden kann.

  1. 1 SUB MaxRow(oEvent AS OBJECT) 

  2. 2    'Auslösen durch das Formular "Beim Laden" und "Nach der Datensatzaktion" 

  3. 3    DIM oForm AS OBJECT 

  4. 4    DIM oScrollField AS OBJECT 

  5. 5    DIM loMax AS LONG 

  6. 6    oForm = oEvent.Source 

  7. 7    oScrollField = oForm.getByName("Bildlaufleiste") 

  8. 8    loPos = oForm.getRow 

  9. 9    oForm.last 

  10. 10    loMax = oForm.getRow 

  11. 11    oForm.absolute(loPos) 

  12. 12    oScrollField.ScrollValueMax = loMax 

  13. 13    oScrollField.ScrollValue = loPos 

  14. 14 END SUB 

Die Gesamtzahl der Datensätze kann nicht über die Funktion RowCount des Formulars ermittelt werden, da diese Funktion nur die Datensätze zählt, die bereits in den Cache geladen wurden. Deswegen wird hier zuerst die aktuelle Zeilennummer des Formulars ausgelesen, dann ans Ende der einzulesenden Daten gesprungen und die dortige Zeilennummer als Maximalwert ermittelt. Anschließend muss wieder auf die ursprüngliche Zeile mit oForm.absolute() zurückgesprungen werden.

Der Bildlaufleiste wird der ermittelte maximale Wert als ScrollValueMax und die aktuelle Position als loPos mitgeteilt. Geschieht das letzte nicht, so stimmt die Position innerhalb der Bildlaufleiste nicht unbedingt mit dem aktuellen Datensatz überein.

  1. 1 SUB Navigation(oEvent AS OBJECT) 

  2. 2    'Auslösen durch das Formular "Nach dem Datensatzwechsel" 

  3. 3    'Synchronisiert die Scrollstellung mit der Position des Datensatzes 

  4. 4    DIM oForm AS OBJECT 

  5. 5    DIM oScrollField AS OBJECT 

  6. 6    oForm = oEvent.Source 

  7. 7    oScrollField = oForm.getByName("Bildlaufleiste") 

  8. 8    loPos = oForm.getRow 

  9. 9    IF loPos = 0 THEN 

  10. 10       'Bei einem neuen Datensatz wird über getRow '0' ermittelt.  

  11. 11       'In dem Datensatzanzeiger soll stattdessen die maximale Zahl an Zeilen  

  12. 12       ''RowCount' um '1' erhöht werden 

  13. 13       loPos = oForm.RowCount + 1 

  14. 14    END IF 

  15. 15    oScrollField.ScrollValue = loPos 

  16. 16 END SUB 

Wird durch die Datensätze navigiert, so muss die Anzeige der Bildlaufleiste und die Anzeige in der Navigationsleiste immer übereinstimmen. Deshalb wird nach dem Datensatzwechsel immer die aktuelle Zeilennummer ermittelt. Für die aktuelle Zeilennummer wird '0' ausgegeben, wenn der Cursor zur Neuaufnahme eines Datensatzes über die letzte Zeile hinaus geht. In diesem Fall soll aber die Bildlaufleiste nicht auf die Startposition zurückspringen sondern wie die Navigationsleiste um '1' oberhalb des bisherigen maximalen Wertes positioniert werden.

  1. 1 SUB FormScroll(oEvent AS OBJECT) 

  2. 2    'Auslösen durch die Bildlaufleiste "Beim Justieren" 

  3. 3    DIM oForm AS OBJECT 

  4. 4    DIM oScrollAction AS OBJECT 

  5. 5    oScrollAction = oEvent.Source 

  6. 6    oForm = oScrollAction.Model.Parent 

  7. 7    loPos = oScrollAction.getValue() 

  8. 8    oForm.absolute(loPos) 

  9. 9 END SUB 

In dieser Prozedur wird aus der Bildlaufleiste ein neuer Wert für die Zeile des Formulars ermittelt. Über getValue() wird der Wert aus der Bildlaufleiste ausgelesen und dem Formular über oForm.absolute(loPos) zugewiesen.

Daten aus Textfeldern auf SQL-Tauglichkeit vorbereiten

Beim Speichern von Daten über einen SQL-Befehl können vor allem Hochkommata (') Probleme bereiten, wie sie z.B. in Namensbezeichnungen wie O'Connor vorkommen können. Dies liegt daran, dass Texteingaben in Daten in '' eingeschlossen sind. Hier muss eine Funktion eingreifen und die Daten entsprechend vorbereiten.

  1. 1 FUNCTION String_to_SQL(st AS STRING) 

  2. 2    IF InStr(st,"'") THEN 

  3. 3       st = Replace(st,"'","''") 

  4. 4    END IF 

  5. 5    String_to_SQL = st 

  6. 6 END FUNCTION 

Es handelt sich hier um eine Funktion. Eine Funktion nimmt einen Wert auf und liefert anschließend auch einen Gegenwert zurück.

Der übergebende Text wird zuerst einmal daraufhin untersucht, ob er ein Hochkomma enthält. Ist dies der Fall, so wird so wird das eine Hochkomma durch zwei Hochkommata ersetzt. Der SQL-Code wird so maskiert.

Die Funktion übergibt ihr Ergebnis durch den folgenden Aufruf:

  1. 1 stTextneu = String_to_SQL(stTextalt) 

Es wird also einfach nur die Variable stTextalt überarbeitet und der entsprechende Wert wieder in der Variablen stTextneu gespeichert. Dabei müssen die Variablen gar nicht unterschiedlichen heißen. Der Aufruf geht praktischer direkt mit:

  1. 1 stText = String_to_SQL(stText) 

Diese Funktion wird in den nachfolgenden Makros immer wieder benötigt, damit Hochkommata auch über SQL abgespeichert werden können.

Beliebige SQL-Kommandos speichern und bei Bedarf ausführen

Über das Abfragemodul können nur Abfragen gestartet werden. SQL-Code, der auch verändernd auf Daten wirkt, ist dort nicht ausführbar, sofern nicht der Datenbanktreiber das, wie bei dem direkten Treiber für MySQL, zulässt. Der über Extras → SQL zur Verfügung stehende Editor lässt zwar die Ausführung von Code zu, bietet aber keine Speichermöglichkeit. Laufend wiederkehrende Befehle müssen so umständlich irgendwo separat abgespeichert werden und können dann in den Editor über die Zwischenablage kopiert werden. Das folgende Makro schafft hier Abhilfe.4

Der für eine Operation wie UPDATE, DELETE oder INSERT gedachte Code wird in einer Tabelle abgelegt, deren erstes Feld den Primärschlüssel "ID" und deren zweites Feld den SQL-Code enthält. Der jeweils aktuelle Datensatz wird in einem Formular ausgewählt und dann über einen Button ausgeführt.

  1. 1 SUB ChangeData(oEvent AS OBJECT) 

  2. 2    DIM oConnection AS OBJECT 

  3. 3    DIM oForm AS OBJECT 

  4. 4    DIM stSql AS STRING 

  5. 5    DIM oSql_Statement AS OBJECT 

  6. 6    DIM inValue AS INTEGER 

  7. 7    oForm = oEvent.Source.Model.Parent 

  8. 8    oConnection = oForm.activeConnection() 

  9. 9    stSQL = oForm.getString(2) 

  10. 10    inValue = MsgBox("Soll der SQL-Code" & CHR(13) & stSQL & CHR(13) &
         
    "ausgeführt werden?", 20, "SQL-Code ausführen") 

  11. 11    IF inValue = 6 THEN 

  12. 12       oSQL_Statement = oConnection.createStatement() 

  13. 13       oSQL_Statement.execute(stSql) 

  14. 14    END IF 

  15. 15 END SUB 

Nach der Definition der Variablen wird das aktuelle Formular über das auslösende Ereignis des Buttons ermittelt. Die Verbindung zur Datenbank wird aus dem Formular ausgelesen. Als zweites Feld steht in der Tabelle der gewünschte SQL-Code.

Zur Sicherheit wird dieser SQL-Code zuerst in einer Messagebox noch einmal dargestellt. Wird hier nicht mit Ja bestätigt, so wird der Code nicht ausgeführt. Diese Bestätigung wird über den Rückgabewert der Messagebox ermittelt (6 entspricht Ja). Anschließend wird zuerst die Verbindung für das Statement hergestellt und dann das Statement ausgeführt.

Zur Ausführung eines SQL-Kommados stehen die folgenden Methoden zur Verfügung5:
executeQuery(stSql) erwartet eine Rückgabe eines Wertes
executeUpdate(stSql) für INSERT, UPDATE oder DELETE erwartet keine Rückgabe
execute(stSql) kann beliebig viele Rückgaben verarbeiten
Das ist in Zusammenhang mit der
Firebird Datenbank wichtig, weil die zur Zeit (LO 7.4) so reagiert, dass z. B. Bei ALTER-Anweisungen eine (unverständliche) Rückmeldung erfolgt und damit die obige Prozedur scheinbar einen Fehler erzeugt, wenn sie mit executeUpdate(stSql) abläuft.

Werte in einem Formular vorausberechnen

Werte, die über die Datenbankfunktionen berechnet werden können, werden in der Datenbank nicht extra gespeichert. Die Berechnung erfolgt allerdings nicht während der Eingabe im Formular, sondern erst nachdem der Datensatz abgespeichert ist. Solange das Formular aus einem Tabellenkontrollfeld besteht, mag das nicht so viel ausmachen. Schließlich kann direkt nach der Eingabe ein berechneter Wert ausgelesen werden. Bei Formularen mit einzelnen Feldern bleibt der vorherige Datensatz aber nicht unbedingt sichtbar. Hier bietet es sich an, die Werte, die sonst in der Datenbank berechnet werden, direkt in entsprechenden Feldern anzuzeigen.6

Die folgenden drei Makros zeigen, wie so etwas vom Prinzip her ablaufen kann. Beide Makros sind mit dem Verlassen bestimmter Felder gekoppelt. Dabei ist auch berücksichtigt, dass hinterher eventuell Werte in einem bereits bestehenden Feld geändert werden.

  1. 1 SUB Berechnung_ohne_MWSt(oEvent AS OBJECT) 

  2. 2    DIM oForm AS OBJECT 

  3. 3    DIM oFeld AS OBJECT 

  4. 4    DIM oFeld2 AS OBJECT 

  5. 5    oFeld = oEvent.Source.Model 

  6. 6    oForm = oFeld.Parent 

  7. 7    oFeld2 = oForm.getByName("Preis_ohne_MWSt") 

  8. 8    oFeld2.BoundField.UpdateDouble(oFeld.getCurrentValue / 1.19) 

  9. 9    IF NOT IsEmpty(oForm.getByName("Anzahl").getCurrentValue()) THEN 

  10. 10       Berechnung_gesamt2(oForm.getByName("Anzahl")) 

  11. 11    END IF 

  12. 12 END SUB 

Ist in einem Feld «Preis» ein Wert eingegeben, so wird beim Verlassen des Feldes das Makro ausgelöst. Im gleichen Formular wie das Feld «Preis» liegt das Feld «Preis_ohne_MWSt». Für dieses Feld wird mit BoundField.UpdateDouble der berechnete Preis ohne Mehrwertsteuer festgelegt. Das Datenfeld dazu entstammt einer Abfrage, bei der vom Prinzip her die gleiche Berechnung, allerdings bei bereits gespeicherten Daten, durchgeführt wird. Auf diese Art und Weise wird der berechnete Wert sowohl während der Eingabe als auch später während der Navigation durch die Datensätze sichtbar, ohne abgespeichert zu werden.

Ist bereits im Feld «Anzahl» ein Wert enthalten, so wird eine Folgerechnung auch für die damit verbundenen Felder durchgeführt.

  1. 1 SUB Berechnung_gesamt(oEvent AS OBJECT) 

  2. 2    oFeld = oEvent.Source.Model 

  3. 3    Berechnung_gesamt2(oFeld) 

  4. 4 END SUB 

Diese kurze Prozedur dient nur dazu, die Auslösung der Folgeprozedur vom Verlassen des Formularfeldes «Anzahl» weiter zu geben. Die Angabe könnte genauso gut mit Hilfe der Bestimmung des Feldes über die Drawpage in der Folgeprozedur integriert werden.

  1. 1 SUB Berechnung_gesamt2(oFeld AS OBJECT) 

  2. 2    DIM oForm AS OBJECT 

  3. 3    DIM oFeld2 AS OBJECT 

  4. 4    DIM oFeld3 AS OBJECT 

  5. 5    DIM oFeld4 AS OBJECT 

  6. 6    oForm = oFeld.Parent 

  7. 7    oFeld2 = oForm.getByName("Preis") 

  8. 8    oFeld3 = oForm.getByName("Preis_gesamt_mit_MWSt") 

  9. 9    oFeld4 = oForm.getByName("MWSt_gesamt") 

  10. 10    oFeld3.BoundField.UpdateDouble(oFeld.getCurrentValue * oFeld2.getCurrentValue) 

  11. 11    oFeld4.BoundField.UpdateDouble(oFeld.getCurrentValue * oFeld2.getCurrentValue -
         
    oFeld.getCurrentValue * oFeld2.getCurrentValue / 1.19) 

  12. 12 END SUB 

Diese Prozedur ist lediglich eine Prozedur, bei der mehrere Felder berücksichtigt werden sollen. Die Prozedur wird aus einem Feld «Anzahl» gestartet, das die Anzahl bestimmter gekaufter Waren vorgeben soll. Mit Hilfe dieses Feldes und des Feldes «Preis» wird jetzt der «Preis_gesamt_mit_MWSt» und die «MWSt_gesamt» berechnet und in die entsprechenden Felder übertragen.

Nachteil in den Prozeduren und auch bei Abfragen: Der Steuersatz wird hier fest einprogrammiert. Besser wäre eine entsprechende Angabe dazu in Verbindung mit dem Preis, da ja Steuersätze unterschiedlich sein können und auch nicht immer konstant sind. In dem Fall müsste eben der Mehrwertsteuersatz aus einem Feld des Formulars ausgelesen werden.

Die aktuelle Office-Version ermitteln

Mit der Version 4.1 sind Änderungen bei Listenfeldern und Datumswerten vorgenommen worden, die es erforderlich machen, vor der Ausführung eines Makros für diesen Bereich zu erkunden, welche Office-Version denn nun verwendet wird. Dazu dient der folgende Code:

  1. 1 FUNCTION OfficeVersion() 

  2. 2    DIM aSettings, aConfigProvider 

  3. 3    DIM aParams2(0) AS NEW com.sun.star.beans.PropertyValue 

  4. 4    DIM sProvider$, sAccess$ 

  5. 5    sProvider = "com.sun.star.configuration.ConfigurationProvider" 

  6. 6    sAccess = "com.sun.star.configuration.ConfigurationAccess" 

  7. 7    aConfigProvider = createUnoService(sProvider) 

  8. 8    aParams2(0).Name = "nodepath" 

  9. 9    aParams2(0).Value = "/org.openoffice.Setup/Product" 

  10. 10    aSettings = aConfigProvider.createInstanceWithArguments(sAccess, aParams2()) 

  11. 11    OfficeVersion() = array(aSettings.ooName,aSettings.ooSetupVersionAboutBox) 

  12. 12 END FUNCTION 

Diese Funktion gibt ein Array wieder, das als ersten Wert z.B. "LibreOffice" und als zweiten Wert die detaillierte Version, z.B. "4.1.5.2" ausgibt.

Wert von Listenfeldern ermitteln

Mit LibreOffice 4.1 wird der Wert, den Listenfelder an die Datenbank weitergeben, über «CurrentValue» ermittelt. In Vorversionen, auch OpenOffice oder AOO, ist dies nicht der Fall. Die folgende Funktion soll dem Rechnung tragen. Die ermittelte LO-Version muss daraufhin untersucht werden, ob sie nach der Version 4.0 entstanden ist.

  1. 1 FUNCTION ID_Ermittlung(oFeld AS OBJECT) AS INTEGER 

  2. 2    a() = OfficeVersion() 

  3. 3    IF a(0) = "LibreOffice" AND ((LEFT(a(1),1) = 4 AND RIGHT(LEFT(a(1),3),1) > 0)
         
    OR LEFT(a(1),1) > 4) THEN 

  4. 4       stInhalt = oFeld.currentValue 

  5. 5    ELSE 

Vor LO 4.1 wird der Wert, der weiter gegeben wird, aus der Werteliste des Listenfeldes ausgelesen. Der sichtbar ausgewählte Datensatz ist SelectedItems(0). '0', weil auch mehrere Werte in einem Listenfeld ausgewählt werden könnten.

  1. 6       stInhalt = oFeld.ValueItemList(oFeld.SelectedItems(0)) 

  2. 7    END IF 

  3. 8    IF IsEmpty(stInhalt) THEN 

Mit -1 wird ein Zahlenwert weiter gegeben, der nicht als AutoWert verwendet wird, also in vielen Tabellen nicht als Fremdschlüssel existiert.

  1. 9       ID_Ermittlung = -1 

  2. 10    ELSE 

  3. 11       ID_Ermittlung = Cint(stInhalt) 

Der Text wird in eine Integer-Variable umgewandelt.

  1. 12    END IF 

  2. 13 END FUNCTION 

Die Funktion gibt den Wert als Integer wieder. Meist werden für Primärschlüssel ja automatisch hoch zählende Integer-Werte verwendet. Für eine Verwendung von Fremdschlüsseln, die diesem Kriterium nicht entsprechen, muss die Ausgabe der Variablen entsprechend angepasst werden.

Der angezeigte Wert eines Listenfeldes lässt sich weiterhin über die Ansicht des Feldes ermitteln:

  1. 1 SUB Listenfeldanzeige 

  2. 2    DIM oDoc AS OBJECT 

  3. 3    DIM oForm AS OBJECT 

  4. 4    DIM oListbox AS OBJECT 

  5. 5    DIM oController AS OBJECT 

  6. 6    DIM oView AS OBJECT 

  7. 7    oDoc = thisComponent 

  8. 8    oForm = oDoc.Drawpage.Forms(0) 

  9. 9    oListbox = oForm.getByName("Listenfeld") 

  10. 10    oController = oDoc.getCurrentController() 

  11. 11    oView = oController.getControl(oListbox) 

  12. 12    print "Angezeigter Inhalt: " & oView.SelectedItem 

  13. 13 END SUB 

Es wird über den Controller auf die Ansicht des Formulars zugegriffen. Damit wird ermittelt, was auf der sichtbaren Oberfläche tatsächlich erscheint. Der ausgewählte Wert ist der SelectedItem.

Auch der direkte Weg über die Liste der zur Anzeige zur Verfügung stehenden Inhalte ist möglich:

  1. 1 SUB ListView 

  2. 2    oField = thisComponent.Drawpage.Forms(0).getByName("Listfield") 

  3. 3    stView = oField.StringItemList(oField.SelectedItems(0)) 

  4. 4    msgbox stView 

  5. 5 END SUB 

Die Position des ausgewählten Werts steht in oField.SelectedItems(0), da es sich bei Datenbanken um ein Listenfeld handelt, das keine Mehrfachselektion erlaubt. Mit der Position wird in der Liste der zur Anzeige zur Verfügung stehenden Inhalte ermittelt, welcher Inhalt an dieser Stelle steht. Beide Listen sind Arrays, so dass sie die entsprechende Zahlenangabe für die Position, beginnend mit 0, benötigen.

Listenfelder durch Eingabe von Anfangsbuchstaben einschränken

Manchmal kann es vorkommen, dass der Inhalt für Listenfelder unübersichtlich groß wird. Damit eine Suche schneller zum Erfolg führt, wäre es sinnvoll, hier den Inhalt des Listenfeldes nach Eingabe eines oder mehrerer Buchstaben einzugrenzen. Das Listenfeld selbst wird mit einem SQL-Befehl versehen, der eine Filterung unterschiedlicher Listenfeldabfragen vereinfacht7. Hier könnte z.B. stehen:
  1. 1 SELECT Tab".* FROM (SELECT "Name" AS "Field", "ID", "Name" AS "Sort" FROM "Tabelle") AS "Tab" ORDER BY "Field" 

Dadurch wird klar, an welcher Stelle der Code später durch die Filterung ersetzt werden muss. Die Bedingungen der inneren Abfrage bleiben unberührt. Soll zuerst nur ein begrenzter Teil des gesamten Inhaltes angezeigt werden, so kann auch ein LIMIT für die Abfrage gesetzt werden. Durch die Zuweisung eines Alias in der Unterabfrage kann auf die Ermittlung von entsprechenden Feldnamen verzichtet werden. «Field» steht hier immer für das Feld, das gefiltert werden soll, «Sort» immer für die Sortierung. Das vereinfacht den Code für das Makro erheblich.

Das folgende Makro ist dafür an Eigenschaften: Listenfeld → Ereignisse → Taste losgelassen gekoppelt.

  1. 1 GLOBAL stListStart AS STRING 

  2. 2 GLOBAL lZeit AS LONG 

Zuerst werden globale Variablen erstellt. Diese Variablen sind notwendig, damit nicht nur nach einem Buchstaben, sondern nach dem Betätigen weiterer Tasten schließlich auch nach einer Buchstabenkombination gesucht werden kann.

In der globalen Variablen stListStart werden die Buchstaben in der eingegebenen Reihenfolge gespeichert.

Die globale Variable lZeit wird mit der aktuellen Zeit in Sekunden versorgt. Bei einer längeren Pause zwischen den Tastatureingaben soll die Variable stListStart wieder zurückgesetzt werden können. Deswegen wird jeweils der Zeitunterschied zur vorhergehenden Eingabe abgefragt.

  1. 1 SUB ListFilter(oEvent AS OBJECT) 

  2. 2    oFeld = oEvent.Source.Model 

  3. 3    IF oEvent.KeyCode < 538 OR oEvent.KeyCode = 1283 OR oEvent.KeyCode = 1284  

  4. 4       OR oEvent.KeyCode = 1281 THEN 

Das Makro wird durch einen Tastendruck ausgelöst. Eine Taste hat innerhalb der API einen bestimmten Zahlencode, der unter com>sun>star>awt>Key nachgeschlagen werden kann. Sonderzeichen wie das «ä», «ö» und «ü» haben den KeyCode 0, alle anderen Schriftzeichen und Zahlen haben einen KeyCode kleiner als 538. Den KeyCode 1283 belegt   (Rücktaste). Wird dieser Code mit ausgelesen, so können auch Korrekturen durchgeführt werden. Mit dem KeyCode 1284 wird auch die Leertaste in die möglichen Zeichen aufgenommen. Hinter dem KeyCode 1281 steckt ESC. Die Taste soll zum Zurücksetzen des Listenfeldes dienen.

Die Abfrage des KeyCode ist hier wichtig, da auch der Schritt mit der Tabulatortaste auf das Auswahlfeld natürlich das Makro auslöst. Der KeyCode für die Tabulatortaste liegt allerdings bei 1282, so dass der weitere Code der Prozedur hier nicht ausgeführt wird.

  1. 5       DIM stSql(0) AS STRING 

Der SQL-Code für das Listenfeld wird in einem Array gespeichert. Das Array hat im Falle des SQL-Codes aber nur ein Datenfeld. Deshalb ist das Array direkt auf stSql(0) begrenzt.

Entsprechend muss auch beim Auslesen des SQL-Codes aus dem Listenfeld darauf geachtet werden, dass der SQL-Code nicht direkt als Text zugänglich ist. Stattdessen ist der Code in einem Array als einziger Eintrag vorhanden: oFeld.ListSource(0).

Der SQL-Code wird nach der Deklaration der Variablen für die weitere Verwendung aufgesplittet. Der Code wird direkt nach der Klammer für die Unterabfrage aufgetrennt. Die Unterabfrage hat hier den Alias AS "Tab".

  1. 6       DIM stText AS STRING 

  2. 7       DIM stFeld AS STRING 

  3. 8       DIM stQuery AS STRING 

  4. 9       DIM ar() 

  5. 10       ar() = Split(oFeld.ListSource(0), " AS ""Tab""") 

Für die Filterung der inneren Abfrage wird eine Bedingung erforderlich. An die bestehende Abfareg wird also zusammen mit dem Alias WHERE angehängt:

  1. 11       stQuery = ar(0) & " AS ""Tab"" WHERE" 

  2. 12       IF oEvent.KeyCode = 1281 THEN        'Taste ESC 

  3. 13          stListStart = "" 'Bei ESC wieder den gesamten Inhalt anzeigen 

  4. 14       ELSEIF lZeit > 0 AND Timer() - lZeit < 5 THEN 

  5. 15          stListStart = stListStart & oEvent.KeyChar 

  6. 16       ELSE 

  7. 17          stListStart = oEvent.KeyChar 

  8. 18       END IF 

  9. 19       lZeit = Timer() 

Zuerst wird die Bedingung abgefragt, ob ESC gedrückt wurde. Hier kann nicht der KeyChar an die Abfrage weiter gegeben werden. Stattdessen muss ein leerer String weitergegeben werden, der die Suchergebnisse auf alle Daten ausdehnt. Ist bereits einmal eine Zeit in der globalen Variablen abgespeichert worden und beträgt die Distanz zu dieser Zeit zum Zeitpunkt der Eingabe weniger als 5 Sekunden, so wird der eingegebene Buchstabe an die vorher eingegebenen Buchstaben angehängt. Anderenfalls wird der eingegebene Buchstabe als einzige (neue) Eingabe verstanden. Das Listenfeld wird dann einfach neu nach dem entsprechenden Buchstaben gefiltert. Anschließend wird die aktuelle Zeit wieder in der globalen Variablen lZeit gespeichert.

  1. 20       stText = LCase( stListStart & "%") 

  2. 21       stSql(0) = stQuery+"LOWER(""Field"") LIKE '"+stText+"' ORDER BY ""Sort""" 

  3. 22       oFeld.ListSource = stSql 

  4. 23       oFeld.refresh 

  5. 24    END IF 

  6. 25 END SUB 

Der SQL-Code wird schließlich zusammengefügt. Die Kleinschreibweise des Feldinhaltes wird mit der Kleinschreibweise des eingegebenen Buchstabens verglichen. Der Code wird dem Listenfeld hinzugefügt und das Listenfeld aufgefrischt, so dass nur noch der gefilterte Inhalt nachgeschlagen werden kann.

Soll eventuell nicht nach den Anfangsbuchstaben sondern ganz allgemein im angezeigten Inhalt gesucht werden, so muss lediglich die Variable «stText» von LCase( stListStart & "%") zu Lcase("%" & stListStart & "%") umgewandelt werden.

Listenfelder mit eingeschränkter Auswahl

Manchmal darf von einem Listenfeld nicht mehrmals der gleiche Wert ausgewählt werden. So sollte z. B. eine Person nicht doppelt einer Gruppe zugewiesen werden oder, wie im folgenden Screenshot, ein Studienfach mehrmals in der Liste der Studienfächer auftauchen.

 

In dem Screenshot wird das untere Tabellenkontrollfeld durch das darüberstehende Listenfeld mit Inhalten versehen. Ist das Listenfeld in dem Tabellenkontrollfeld eingebaut, so kann ein «Studienfach» mehrfach ausgewählt werden. Die Tabellenkonstruktion ist aber zur Sicherheit bereits so erstellt, dass der eingefügte Wert zusammen mit einem Wert des darüber liegenden Hauptformulars den Primärschlüssel bildet. Wird jetzt «Biologie» noch einmal gewählt, so hängt die Eingabe. Keine Rückmeldung, nur über ESC geht es wieder aus dieser Situation heraus.

 

Das Listenfeld zeigt jetzt nur die möglichen Einträge an. Das ist sowohl der aktuelle Datensatz als auch die anderen Fächer, die noch nicht ausgewählt wurden. Das Tabellenkontrollfeld kann nur zum Löschen von Datensätzen genutzt werden.

Wird ein neuer Datensatz bearbeitet, so stehen nur die zusätzlich verfügbaren Inhalte zur Auswahl. Die Fächer «Biologie» und «Chemie» erscheinen hier nicht.

Eine solche Konstruktion ist nicht innerhalb eines Tabellenkontrollfeldes möglich. Die Listenfelder werden hier nicht gleichbleibend aktualisiert, so dass zwischendurch die aktuell ausgewählten Fächer verschwinden würden, obwohl bereits ein entsprechender Fremdschlüssel abgespeichert wurde.

Der in dem Listenfeld stehende SQL-Code für die Anzeige der Liste sieht so aus:

  1. 1 SELECT "Tab".* FROM  

  2. 2    (SELECT "Studienfach" AS "Field", 

  3. 3    "ID" AS "Key", 

  4. 4    "Studienfach" AS "Sort" FROM "tbl_Studienfach") AS "Tab" 

  5. 5 ORDER BY "Sort" 

In der Unterabfrage wird über ein Alias dem anzuzeigenden Inhalt, dem weiter zu gebendem Schlüsselwert und der beabsichtigten Sortierung ein fester Begriff zugeordnet. So kann auch bei komplizierterem Code über z. B. mehrere Tabellen immer Bezug auf diese Schlüsselpositionen genommen werden: "Field", "Key" und "Sort". Der eigentliche Code für das Listenfeld kann über den Alias für die Unterabfrage AS "Tab" von den über Makros hinzugefügten Bedingungen abgetrennt werden.

In den Zusatzinformationen zu dem Listenfeld steht ein Code, der in der Prozedur «Listenfeldfilter» zur Anzeige des zu dem aktuellen Datensatz passenden Listeninhaltes benötigt wird:

  1. 1 SELECT "Studienfach_ID" FROM "tbl_LehrerIn_Studienfach" WHERE "L_ID" 

Die folgende Prozedur wird über Formulareigenschaften → Ereignisse → Nach dem Datensatzwechsel bei jedem neuen Datensatz wieder aufgerufen.

  1. 1 SUB Listenfeldfilter(oEvent AS OBJECT) 

  2. 2    DIM oListField AS OBJECT 

  3. 3    DIM oForm AS OBJECT 

  4. 4    DIM stID AS STRING 

  5. 5    DIM stIDCurrent AS STRING 

  6. 6    DIM stNotIn AS STRING 

  7. 7    DIM stSql(0) AS STRING 

  8. 8    oForm = oEvent.Source 

  9. 9    IF hasUnoInterfaces(oForm, "com.sun.star.form.XForm" ) THEN 

  10. 10       IF NOT oForm.Parent.isBeforeFirst() AND NOT oForm.Parent.isAfterLast() THEN 

  11. 11          stID = oForm.Parent.getByName("ID").BoundField.getString 

  12. 12          IF stID <> "" THEN 

  13. 13             oListField = oForm.getByname("lboStart") 

  14. 14             stIDCurrent = oListField.BoundField.getString 

  15. 15             IF stIDCurrent = "" THEN stIDCurrent = "-1" 

  16. 16             stNotIn = oListField.Tag 

  17. 17             stSource = oListField.ListSource(0) 

  18. 18             ar() = Split(stSource, " AS ""Tab""") 

  19. 19             stSource = ar(0) & " AS ""Tab"" WHERE ( ""Key"" NOT IN
                  ( "
    & stNotIn & " = '" & stID & "' )
                  OR ""Key"" = '"
    & stIDCurrent & "' ) ORDER BY ""Sort""" 

  20. 20             stSql(0) = stSource 

  21. 21             oListField.ListSource = stSql 

  22. 22             oListField.refresh 

  23. 23          END IF 

  24. 24       END IF 

  25. 25    END IF 

  26. 26 END SUB 

Der Code für die Listenfelder ist so vorgegeben, dass nur die unbekannten Werte nachgeliefert werden müssen. Dabei ist der Code für das Listenfeld nach dem Format SELECT * FROM (SELECT …) erstellt.Die folgende Aliassetzungen werden vorgenommen: SELECT "Name" AS "Field", "ID" AS "Key", "Name" AS "Sort". Der Ergänzungscode beginnt mit AS "Tab". Der Alias ist notwendig, da bei machen Datenbanken (z.B. MariaDB) eine Unterabfrage nicht ohne Alias angenommen wird.

In Zeile 11 wird auf ein fest im übergeordneten Formular befindlichen Feld Bezug genommen. Hier wird extra die Bezeichnung der Formularfeldes genutzt, da der entsprechende Eintrag ja nicht unbedingt auch in der Datenquelle für das übergeordnete Formular mit dem Namen Verzeichnet ist. Nur wenn dieser Verbindungseintrag von Hauptformular zu Unterformular vorhanden ist wird der weitere Code des Makros ausgeführt.

Auch in Zeile 13 ist wieder ein Feld mit dem Feldnamen vorgegeben. Dies ist das Listenfeld, das zur Anzeige des Inhaltes dient. Es soll also immer die Bezeichnung «lboStart» erhalten.

Ist noch kein Eintrag für den aktuellen Datensatz erfolgt, so ist der aktuelle Wert des Listenfeldes leer. Hier wird dann stattdessen der Wert '-1' angenommen, der standardmäßig bei automatisch hoch zählenden Feldern nicht vorkommt.

Die Variable stNotIn speichert den Inhalt aus den Zusatzinformationen des Listenfeldes (Zeile 16). Anschließend wird der SQL-Code des Listenfeldes ausgelesen und so in ein Array eingelesen, dass das erste Element des Arrays den Startcode des Listenfeldes wiedergibt (Zeilen 17 und 18).

Schließlich wird der Code mit den entsprechenden Variablen neu zusammengefügt, so dass nur die Inhalte angeboten werden, die noch nicht ausgewählt wurden. Zusätzlich wird lediglich der Inhalt aufgenommen, der im aktuellen Datensatz bei einer vorhergehenden Auswahl bereits abgespeichert wurde. Ohne diese Ergänzung wäre das Listenfeld bei bestehenden Datensätzen ja grundsätzlich leer.

Der zusammengesetzte Inhalt sieht dann bei z. B. «ID = 1» und «stIDCurrent = 3» für die oben genannte Abfrage so aus:

  1. 1 SELECT "Tab".* FROM  

  2. 2    (SELECT "Studienfach" AS "Field", 

  3. 3    "ID" AS "Key", 

  4. 4    "Studienfach" AS "Sort" FROM "tbl_Studienfach") AS "Tab" 

  5. 5    WHERE "Key" NOT IN  

  6. 6       (SELECT "Studienfach_ID" FROM "tbl_LehrerIn_Studienfach"
            WHERE "L_ID" = '1')
     

  7. 7       OR "Key" = '3' 

  8. 8 ORDER BY "Sort" 

Das Listenfeld sollte lediglich eine Prozedur erhalten, die zum Abspeichern des Inhaltes führt. Sie ist im Listenfeld über Eigenschaften → Listenfeld → Ereignisse → Modifiziert verknüpft.

  1. 1 SUB ListenfeldSpeichern(oEvent AS OBJECT) 

  2. 2    oListField = oEvent.Source.Model 

  3. 3    oForm = oListField.Parent 

  4. 4    oListField.commit 

  5. 5    IF oForm.IsNew THEN 

  6. 6       oForm.insertRow 

  7. 7    ELSE 

  8. 8       oForm.updateRow 

  9. 9    END IF 

  10. 10    IF oForm.isLast THEN         

  11. 11       oForm.moveToInsertRow 

  12. 12    ELSE 

  13. 13       oForm.next 

  14. 14    END IF 

  15. 15 END SUB 

Über das auslösende Listenfeld wird auf das Formular zugegriffen. Der Inhalt des Listenfeldes wird in das Formular übertragen und abhängig davon, ob der Datensatz schon existiert oder neu ist abgespeichert.

Ab Zeile 10 wird lediglich der nächste Datensatz in dem Unterformular angesteuert. Solange Daten vorhanden sind kann dies mit «Next» geschehen. Steht der Datensatzzeiger aber auf dem letzten Datensatz, dann wird als nächster Datensatz ein neuer Datensatz eingefügt.

So kann das Listenfeld in dem obigen Beispiel durch mehrfache Betätigung ohne zusätzliche Bewegung der Maus dazu genutzt werden, alle erforderlichen Studienfächer auszuwählen.

Listenfelder zur Mehrfachauswahl nutzen

Die Nutzung der Mehrfachauswahl von Listenfeldern zeigt das folgende Formular8:
 

Die Mehrfachauswahl funktioniert zusammen mit Base nur über Makros. In dem obigen Formular werden zuerst die Termindaten eingetragen und abgespeichert, damit für das weitere Verfahren die Nummer des Primärschlüssels der Tabelle "Termine" über das Makro ausgelesen werden kann. Die Inhalte des darunter stehenden Listenfeldes werden beim Klick auf die entsprechenden Werte in Verbindung mit Ctrl oder Alt markiert und direkt gespeichert.

Die zugrunde liegende Datenbank hat den folgenden Aufbau:

 

Über die Makros muss die Tabelle "rel_Termine_Personen" beschrieben werden. Nach einem Wechsel des Datensatzes muss außerdem aus dieser Tabelle ausgelesen werden, auf welche Datensätze das Listenfeld einzustellen ist. Das Listenfeld kann dabei nicht mit dieser Tabelle verbunden werden, da in ein Datenbankfeld nur ein Wert eingetragen bzw. ausgelesen werden kann.

Die Prozedur «AuswahlSpeichern» wird in den Eigenschaften des Listenfeldes mit Ereignisse → Modifiziert verbunden.

  1. 1 SUB AuswahlSpeichern(oEvent AS OBJECT) 

  2. 2    DIM oConnection AS OBJECT 

  3. 3    DIM oSQL_Statement AS OBJECT 

  4. 4    DIM oField AS OBJECT 

  5. 5    DIM oForm AS OBJECT 

  6. 6    DIM stID AS STRING 

  7. 7    DIM i AS INTEGER 

  8. 8    DIM k AS INTEGER 

  9. 9    oField = oEvent.Source.Model 

  10. 10    oForm = oField.Parent 

  11. 11    stID = oForm.getString(oForm.FindColumn("ID") 

Obwohl die im Feld "ID" gespeicherte Variable eine Integer-Variable ist, wird sie hier als String ausgelesen. Eine String-Variable kann besser für die folgende Bedingung als leer erkannt werden, da die Integer-Variable durch die vorherige Definition als Integer den Wert '0' annimmt, wenn in "ID" gar kein Wert steht.

  1. 12    IF stID <> "" THEN 

  2. 13       oConnection = oForm.activeConnection() 

  3. 14       oSQL_Statement = oConnection.createStatement() 

Die Verbindung zur Datenbank wird über die Formularverbindung geholt, Das SQL-Statement wird vorbereitet. Danach werden einfach alle bisherigen Datensätze mit Personen, die zu dem Termin gehören, gelöscht.

  1. 15       FOR k = LBound(oField.ValueItemList()) TO UBound(oField.ValueItemList()) 

  2. 16          stSql = "DELETE FROM ""rel_Termine_Personen"" WHERE ""Termine_ID"" =
               '"+stID+"'" 

  3. 17          oSQL_Statement.executeUpdate(stSql) 

  4. 18       NEXT 

Anschließend werden alle ausgewählten Werte neu in die Tabelle "rel_Termine_Personen" geschrieben

  1. 19       FOR i = LBound(oField.SelectedValues()) TO UBound(oField.SelectedValues()) 

  2. 20          stSql = "INSERT INTO ""rel_Termine_Personen"" (""Termine_ID"",
               ""Personen_ID"") VALUES ('"+stID+"','"+oField.SelectedValues(i)+"')" 

  3. 21          oSQL_Statement.executeUpdate(stSql) 

  4. 22       NEXT 

  5. 23    END IF 

  6. 24 END SUB 

Die Prozedur «AuswahlErstellen» wird in den Eigenschaften des Formulars mit Ereignisse → Nach dem Datensatzwechsel verbunden.

  1. 1 SUB AuswahlErstellen(oEvent AS OBJECT) 

  2. 2    DIM aFields() 

  3. 3    DIM aValues() 

  4. 4    DIM oConnection AS OBJECT 

  5. 5    DIM oSQL_Statement AS OBJECT 

  6. 6    DIM oResult AS OBJECT 

  7. 7    DIM oForm AS OBJECT 

  8. 8    DIM oField AS OBJECT 

  9. 9    DIM stSql AS STRING 

  10. 10    DIM stID AS STRING 

  11. 11    DIM i AS INTEGER 

  12. 12    DIM k AS INTEGER 

  13. 13    oForm = oEvent.Source 

  14. 14    oConnection = oForm.activeConnection() 

  15. 15    oSQL_Statement = oConnection.createStatement() 

  16. 16    aFields = Array("ListePersonen") 

Die Listenfelder im Formular werden in ein Array geschrieben. Dies hat den Vorteil, dass gleich mehrere Listenfelder mit entsprechenden Inhalten gefüllt werden können.

  1. 17    stID = oForm.getString(oForm.FindColumn("ID") 

  2. 18    IF stID <> "" THEN 

Eine Schleife über alle Listenfelder wird gestartet. In diesem Fall läuft die Schleife ein einziges Mal durch.

  1. 19       FOR i = LBound(aFields()) TO UBound(aFields()) 

Der SQL-Code für die bestehende Auswahl der Personen zu dem betreffenden Termin wird erstellt.

  1. 20          stSql = "SELECT ""Personen_ID"" FROM ""rel_Termine_Personen"" WHERE
               ""Termine_ID"" = '"+stID+"'" 

Die Abfrage wird gestartet und das Ergebnis in ein Array lesen.

  1. 21          oResult = oSQL_Statement.executeQuery(stSql) 

  2. 22          k = 0 

  3. 23          WHILE oResult.Next 

  4. 24             ReDim Preserve aValues(k) 

  5. 25             aValues(k) = oResult.getString(1) 

  6. 26             k = k + 1 

  7. 27          WEND 

Das Ergebnisarray wird in die Feldeigenschaften übernommen, so dass die Werte angezeigt werden. Dabei ist zu beachten, dass die Eigenschaft selectedValues() erst in LO-Versionen ab LO 4.1 und nicht unter AOO sowie älteren LO-Versionen bekannt ist. Hier müsste also gegebenenfalls nachgebessert werden.

  1. 28          oField = oForm.GetByName(aFields(i)) 

  2. 29          oField.selectedValues() = aValues() 

  3. 30          NEXT 

  4. 31    END IF 

  5. 32 END SUB 

Datumswert aus einem Formularwert in eine Datumsvariable umwandeln

  1. 1 FUNCTION Datumswert(oFeld AS OBJECT) AS DATE 

  2. 2    a() = OfficeVersion() 

  3. 3    IF a(0) = "LibreOffice" AND (LEFT(a(1),1) = 4 AND RIGHT(LEFT(a(1),3),1) > 0)
         
    OR LEFT(a(1),1) > 4 THEN 

Hier werden alle Versionen ab 4.1 durch die oben vorgestellte Funktion «OfficeVersion()» abgefangen. Dazu wird die Version in ihre Bestandteile aufgesplittet. Die Hauptversion und die erste Unterversion werden abgefragt. Das funktioniert vorerst bis zur LO-Version 9 einwandfrei.

  1. 4       DIM stMonat AS STRING 

  2. 5       DIM stTag AS STRING 

  3. 6       stMonat = Right(Str(0) & Str(oFeld.CurrentValue.Month),2) 

  4. 7       stTag =  Right(Str(0) & Str(oFeld.CurrentValue.Day),2) 

  5. 8       Datumswert = CDateFromIso(oFeld.CurrentValue.Year & stMonat & stTag) 

  6. 9    ELSE 

  7. 10       Datumswert = CDateFromIso(oFeld.CurrentValue) 

  8. 11    END IF 

  9. 12 END FUNCTION 

Das Datum wird seit LO 4.1.2 als Array im Formularfeld gespeichert. Mit dem aktuellen Wert kann also nicht auf das Datum selbst zugegriffen werden. Entsprechend ist es neu aus den Werten für Tag, Monat und Jahr zusammen zu setzen, damit anschließend in Makros damit weiter gearbeitet werden kann.

Eingabemöglichkeiten in Feldern einschränken

Numerische Felder, formatierbare Felder und Datumsfelder lassen es zu, dass mit den Pfeiltasten die Werte geändert werden können. In einem Forum kam die Nachfrage, ob das nicht irgendwie unterbunden werden könnte. Folgendes Makro verhindert nicht die Änderung, macht sie aber direkt wieder rückgängig. Es muss an das Ereignis Taste gedrückt gebunden werden.

  1. 1 SUB KeyTest(oEvent AS OBJECT) 

  2. 2    IF oEvent.KeyCode = 1025 OR oEvent.KeyCode = 1024 THEN 

  3. 3       oEvent.Source.Model.reset() 

  4. 4    ELSE 

  5. 5       oEvent.Source.Model.commit 

  6. 6    END IF 

  7. 7 END SUB 

Die beiden Pfeiltasten werden abgefangen und das Feld auf den Ausgangswert zurückgestellt. Bei der Betätigung jeder anderen Taste wird der Wert direkt festgeschrieben und ist nicht mehr über reset() erreichbar. Andernfalls würden alle nicht abgespeicherten Eingaben des Feldes mit den Pfeiltasten wieder aufgehoben.

Suchen von Datensätzen

Ohne Makro funktioniert das Suchen von Datensätzen auch. Hier ist aber die entsprechende Abfrage äußerst unübersichtlich zu erstellen. Da könnte eine Schleife mittels Makro Abhilfe schaffen.

Die folgende Variante liest die Felder einer Tabelle aus, gründet dann intern eine Abfrage und schreibt daraus schließlich eine Liste der Primärschlüsselnummern der durchsuchten Tabelle auf, auf die der Suchbegriff zutrifft. Für die folgende Beschreibung existiert eine Tabelle "Suchtmp", die aus einem per Autowert erstellten Primärschlüsselfeld "ID" und einem Feld "Nr." besteht, in das die aus der zu durchsuchenden Tabelle gefundenen Primärschlüssel eingetragen werden. Der Tabellenname wird dabei der Prozedur am Anfang als Variable mitgegeben.

Um ein entsprechendes Ergebnis zu bekommen, muss die Tabelle natürlich nicht die Fremdschlüssel, sondern entsprechende Feldinhalte in Textform enthalten. Dafür ist gegebenenfalls eine Tabellenansicht (View) zu erstellen, auf die das Makro auch zugreifen kann.9
  1. 1 SUB Suche(stTabelle AS STRING) 

  2. 2    DIM oDatenquelle AS OBJECT 

  3. 3    DIM oVerbindung AS OBJECT 

  4. 4    DIM oSQL_Anweisung AS OBJECT 

  5. 5    DIM stSql AS STRING 

  6. 6    DIM oAbfrageergebnis AS OBJECT 

  7. 7    DIM oDoc AS OBJECT 

  8. 8    DIM oDrawpage AS OBJECT 

  9. 9    DIM oForm AS OBJECT 

  10. 10    DIM oForm2 AS OBJECT 

  11. 11    DIM oFeld AS OBJECT 

  12. 12    DIM stInhalt AS STRING 

  13. 13    DIM arInhalt() AS STRING 

  14. 14    DIM inI AS INTEGER 

  15. 15    DIM inK AS INTEGER 

  16. 16    oDoc = thisComponent 

  17. 17    oDrawpage = oDoc.drawpage 

  18. 18    oForm = oDrawpage.forms.getByName("Suchform") 

  19. 19    oFeld = oForm.getByName("Suchtext") 

  20. 20    stInhalt = oFeld.getCurrentValue() 

  21. 21    stInhalt = LCase(stInhalt) 

Der Inhalt des Suchtext-Feldes wird hier von vornherein in Kleinbuchstaben umgewandelt, damit die anschließende Suchfunktion nur die Kleinschreibweisen miteinander vergleicht.

  1. 22    oDatenquelle = ThisComponent.Parent.DataSource 

  2. 23    oVerbindung = oDatenquelle.GetConnection("","") 

  3. 24    oSQL_Anweisung = oVerbindung.createStatement() 

Zuerst wird einmal geklärt, ob überhaupt ein Suchbegriff eingegeben wurde. Ist das Feld leer, so wird davon ausgegangen, dass keine Suche vorgenommen wird. Alle Datensätze sollen angezeigt werden; eine weitere Abfrage erübrigt sich.

Ist ein Suchbegriff eingegeben worden, so werden die Spaltennamen der zu durchsuchenden Tabelle ausgelesen, um auf die Felder mit einer Abfrage zugreifen zu können.

  1. 25    IF stInhalt <> "" THEN 

  2. 26       stInhalt = String_to_SQL(stInhalt) 

  3. 27       stSql ="SELECT ""COLUMN_NAME"" FROM ""INFORMATION_SCHEMA"".""SYSTEM_COLUMNS""
            WHERE ""TABLE_NAME"" = '" + stTabelle + "' ORDER BY ""ORDINAL_POSITION""" 

  4. 28       oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql) 

Der SQL-Code müsste für Firebird hier angepasst werden:
  1. 27 stSql = "SELECT RDB$FIELD_NAME FROM RDB$RELATION_FIELDS WHERE  

       RDB$RELATION_NAME = '" + stTabelle + "' ORDER BY  

       RDB$Field_POSITION" 

 

SQL-Formulierungen müssen in Makros wie normale Zeichenketten zuerst einmal in doppelten Anführungsstrichen gesetzt werden. Feldbezeichnungen und Tabellenbezeichnungen stehen innerhalb der SQL-Formulierungen in der Regel bereits in doppelten Anführungsstrichen. Damit letztlich ein Code entsteht, der auch diese Anführungsstriche weitergibt, müssen für Feldbezeichnungen und Tabellenbezeichnungen diese Anführungsstriche verdoppelt werden.
Aus stSql = "SELECT ""Name"" FROM ""Tabelle"";"
wird, wenn es mit dem Befehl msgbox stSql auf dem Bildschirm angezeigt wird,
SELECT "Name" FROM "Tabelle";

Der Zähler des Arrays, in das die Feldnamen geschrieben werden, wird zuerst auf 0 gesetzt. Dann wird begonnen die Abfrage auszulesen. Da die Größe des Arrays unbekannt ist, muss immer wieder nachjustiert werden. Deshalb beginnt die Schleife damit, über ReDim Preserve arInhalt(inI) die Größe des Arrays festzulegen und den vorherigen Inhalt dabei zu sichern. Anschließend werden die Felder ausgelesen und der Zähler des Arrays um 1 heraufgesetzt. Damit kann dann das Array neu dimensioniert werden und wieder ein weiterer Wert abgespeichert werden.

  1. 29       InI = 0 

  2. 30       WHILE oAbfrageergebnis.next 

  3. 31          ReDim Preserve arInhalt(inI) 

  4. 32          arInhalt(inI) = oAbfrageergebnis.getString(1) 

  5. 33          inI = inI + 1 

  6. 34       WEND 

  7. 35       stSql = "DROP TABLE ""Suchtmp"" IF EXISTS" 

  8. 36       oSQL_Anweisung.executeUpdate (stSql) 

Jetzt wird die Abfrage in einer Schleife zusammengestellt, die anschließend an die zu Beginn angegebene Tabelle gestellt wird. Dabei werden alle Schreibweisen untersucht, da auch der Inhalt des Feldes in der Abfrage auf Kleinbuchstaben umgewandelt wird.

Die Abfrage wird direkt so gestellt, dass die Ergebniswerte in der Tabelle "Suchtmp" landen. Dabei wird davon ausgegangen, dass der Primärschlüssel an der ersten Position der Tabelle steht (arInhalt(0)).

  1. 37       stSql = "SELECT """+arInhalt(0)+"""INTO ""Suchtmp"" FROM """+stTabelle+"""
            WHERE " 

  2. 38       FOR inK = 0 TO (inI - 1) 

  3. 39          stSql = stSql+"LOWER("""+arInhalt(inK)+""") LIKE '%"+stInhalt+"%'" 

  4. 40          IF inK < (inI - 1) THEN 

  5. 41             stSql = stSql+" OR " 

  6. 42          END IF 

  7. 43       NEXT 

  8. 44       oSQL_Anweisung.executeQuery(stSql) 

  9. 45    ELSE 

  10. 46       stSql = "DELETE FROM ""Suchtmp""" 

  11. 47       oSQL_Anweisung.executeUpdate (stSql) 

  12. 48    END IF 

Das Anzeigeformular muss neu geladen werden. Es hat als Datenquelle eine Abfrage, in diesem Beispiel "Suchabfrage"

  1. 49    oForm2 = oDrawpage.forms.getByName("Anzeige") 

  2. 50    oForm2.reload() 

  3. 51 End Sub 

Damit wurde eine Tabelle erstellt, die nun in einer Abfrage ausgewertet werden soll. Die Abfrage ist dabei möglichst so zu fassen, dass sie anschließend noch editiert werden kann. Im Folgenden also  ein Abfragecode:

  1. 1 SELECT * FROM "Suchtabelle"  

  2. 2 WHERE "Nr." IN ( SELECT "Nr." FROM "Suchtmp" )  

  3. 3    OR "Nr." =  

  4. 4       CASE WHEN ( SELECT COUNT( "Nr." ) FROM "Suchtmp" ) > 0
         THEN '0' ELSE "Nr." END 

Alle Elemente der "Suchtabelle" werden dargestellt. Auch der Primärschlüssel. Keine andere Tabelle taucht in der direkten Abfrage auf; somit ist auch kein Primärschlüssel einer anderen Tabelle nötig, damit das Abfrageergebnis weiterhin editiert werden kann.

Der Primärschlüssel ist in dieser Beispieltabelle unter dem Titel "Nr." abgespeichert. Durch das Makro wird genau dieses Feld ausgelesen. Es wird jetzt also zuerst nachgesehen, ob der Inhalt des Feldes "Nr." in der Tabelle "Suchtmp" vorkommt. Bei der Verknüpfung mit 'IN' werden ohne weiteres auch mehrere Werte erwartet. Die Unterabfrage darf also auch mehrere Datensätze liefern.

Bei größeren Datenmengen wird der Abgleich von Werten über die Verknüpfung IN aber zusehends langsamer. Es bietet sich also nicht an, für eine leere Eingabe in das Suchfeld einfach alle Primärschlüsselfelder der "Suchtabelle" in die Tabelle "Suchtmp" zu übertragen und dann auf die gleiche Art die Daten anzusehen. Stattdessen erfolgt bei einer leeren Eingabe eine Leerung der Tabelle "Suchtmp", so dass gar keine Datensätze mehr vorhanden sind. Hierauf zielt der zweite Bedingungsteil:

  1. 3    OR "Nr." =  

  2. 4       CASE WHEN ( SELECT COUNT( "Nr." ) FROM "Suchtmp" ) > 0
         THEN '-1' ELSE "Nr." END 

Wenn in der Tabelle "Suchtmp" ein Datensatz gefunden wird, so ist das Ergebnis der ersten Abfrage größer als 0. Für diesen Fall gilt: "Nr." = '-1' (hier steht am Besten ein Zahlenwert, der als Primärschlüssel nicht vorkommt, also z.B. '-1' ). Ergibt die Abfrage genau 0 (Dies ist der Fall wenn keine Datensätze da sind), dann gilt "Nr." = "Nr.". Es wird also jeder Datensatz dargestellt, der eine "Nr." hat. Da "Nr." der Primärschlüssel ist, gilt dies also für alle Datensätze.

Suchen in Formularen und Ergebnisse farbig hervorheben

Bei größeren Inhalten eines Textfeldes ist oft unklar, an welcher Stelle denn nun die Suche den Treffer zu verzeichnen hat. Da wäre es doch gut, wenn das Formular den entsprechenden Treffer auch markieren könnte. So sollte das dann im Formular aussehen:

 
Um so ein Formular zum Laufen zu bringen, bedarf es ein paar zusätzlicher Griffe in die Trickkiste.10

Die Funktionsweise eines solchen Suchfeldes wurde bereits bei den Abfragetechniken erklärt: Es wird eine Filtertabelle erstellt. Über ein Formular wird nur der aktuelle Wert des einzigen Datensatzes in dieser Tabelle neu geschrieben. Das Hauptformular wird über eine Abfrage mit dem entsprechenden Inhalt versorgt. Die Abfrage sieht im obigen Fall so aus:

  1. 1 SELECT "ID", "Memo"  

  2. 2 FROM "Tabelle"  

  3. 3 WHERE LOWER ( "Memo" ) LIKE '%' || LOWER (  

  4. 4    ( SELECT "Suchtext" FROM "Filter" WHERE "ID" = TRUE ) ) || '%' 

Wird ein Suchtext eingetragen, so werden nur die Datensätze der Tabelle "Tabelle" angezeigt, bei denen der Text im Feld "Memo" vorkommt. Dabei ist Groß- und Kleinschreibung egal.

Wird kein Suchtext eingetragen, so werden alle Datensätze der Tabelle "Tabelle" angezeigt. Da der Primärschlüssel dieser Tabelle auch in der Abfrage enthalten ist, ist die Abfrage außerdem editierbar.

 

In dem Formular ist neben dem Feld «ID» für den Primärschlüsseleintrag nur ein Feld «MemoFormat», das über Eigenschaften → Allgemein → Text-Typ → Mehrzeilig mit Formatierungen so eingestellt ist, dass es überhaupt Farben innerhalb von schwarzem Text darstellen kann. Die genaue Betrachtung der Eigenschaften des Textfeldes zeigt, dass der Reiter Daten fehlt. Daten lassen sich über ein Feld, das zusätzlich formatierbar ist, nicht eingeben. Das ist wohl dadurch begründet, dass die Datenbank selbst auch solche Formatierungen nicht speichert. Und trotzdem ist es durch den entsprechenden Makroeinsatz möglich, Text in dieses Feld hinein zu bekommen, ihn zu markieren und bei Änderungen auch wieder aus dem Feld hinaus in die Datenbank zu befördern.

Die Prozedur «InhaltUebertragen» dient dazu, den Inhalt aus dem Datenbankfeld "Memo" in das formatierbare Textfeld «MemoFormat» zu übertragen und so zu formatieren, dass bei einem entsprechenden Eintrag im Suchfeld der dazugehörige Begriff hervorgehoben wird.

Die Prozedur ist an das folgende Ereignis gebunden: Formular → Ereignisse → Nach dem Datensatzwechsel

  1. 1 Sub InhaltUebertragen(oEvent AS OBJECT)         

  2. 2    DIM inMemo AS INTEGER 

  3. 3    DIM oFeld AS OBJECT 

  4. 4    DIM stSuchtext AS STRING 

  5. 5    DIM oCursor AS OBJECT 

  6. 6    DIM inSuch AS INTEGER 

  7. 7    DIM inSuchAlt AS INTEGER 

  8. 8    DIM inLen AS INTEGER 

  9. 9    oForm = oEvent.Source 

  10. 10    inMemo = oForm.findColumn("Memo") 

  11. 11    oFeld = oForm.getByName("MemoFormat") 

  12. 12    oFeld.Text = oForm.getString(inMemo) 

Zuerst werden die Variablen definiert. Anschließend wird über das Formular das Tabellenfeld "Memo" gesucht und aus diesem Feld über getString() der entsprechende Text des Feldes "Memo" der Tabelle "Tabelle" ausgelesen. Der entsprechende Feldinhalt wird in das Feld übertragen, das sich formatieren lässt, aber keine Verbindung zur Datenbank hat: «MemoFormat».

Bei Tests ist es zuerst vorgekommen, dass sich das Formular zwar öffnete, aber leider die Formularleiste am unteren Rand des Formulars nicht mehr aufgebaut wurde. Deswegen erfolgt hier ein sehr kurzer Wartebefehl von 5/1000 Sekunden. Danach wird aus dem parallel zum «Formular» liegenden «FormularFilter» der angezeigte Inhalt als Suchtext ausgelesen.

  1. 13    Wait 5 

  2. 14    stSuchtext = oForm.Parent.getByName("FormularFilter").getByName("Suche").Text 

Um Textteile formatieren zu können muss ein (nicht sichtbarer) TextCursor in dem Feld erstellt werden, das den Text enthält. Die Darstellung des Textes in der Standardversion hat eine serifenbetonte Schriftart in 12-Punkt-Größe, die in anderen Formularteilen nicht unbedingt vorkommt und über das Formularfeld auch nicht direkt abwählbar ist. In dieser Prozedur wird direkt zu Beginn der Text einmal auf die gewünschte Darstellungsart eingestellt. Erfolgt dies nicht schon zu Beginn, so wird wegen der unterschiedlichen Formatierungen der oberer Textrand in dem Feld erst einmal angeschnitten. Die erste Zeile war in Versuchen nur zu 2/3 lesbar.

Damit der Cursor (wieder nicht sichtbar) den Text markiert, wird er zuerst an den Anfang gesetzt und mit dem Zusatz true weiterbewegt zum Endpunkt, der ebenfalls den Zusatz true hat. Dann erfolgt die Zuweisung der notwendigen Eigenschaften wie Schriftgröße, Schriftstil, Schriftfarbe oder auch Schriftdicke. Anschließend wird der Cursor wieder zur Startposition gesetzt.

  1. 15    oCursor = oFeld.createTextCursor() 

  2. 16    oCursor.gotoStart(true) 

  3. 17    oCursor.gotoEnd(true) 

  4. 18    oCursor.CharHeight = 10 

  5. 19    oCursor.CharFontName = "Arial, Helvetica, Tahoma" 

  6. 20    oCursor.CharColor = RGB(0,0,0) 

  7. 21    oCursor.CharWeight = 100.000000        'com::sun::star::awt::FontWeight 

  8. 22    oCursor.gotoStart(false) 

Enthält das Feld Text und ist ein Eintrag zum Suchen vorhanden, so wird jetzt der Text nach dem Suchbegriff durchsucht. Die äußere Schleife fragt erst einmal nur nach der Bedingung, die nächste Schleife klärt noch einmal, ob der Suchtext denn tatsächlich in dem Text enthalten ist, der in «MemoFormat» steht. Diese Einstellung könnte auch unterlassen werden, da die Abfrage, auf der das Formular basiert, nur solchen Text anzeigt, auf den diese Bedingung zutrifft.

  1. 23    IF oFeld.Text <> "" AND stSuchtext <> "" THEN 

  2. 24       IF inStr(oFeld.Text, stSuchtext) THEN 

  3. 25          inSuch = 1 

  4. 26          inSuchAlt = 0 

  5. 27          inLen = Len(stSuchtext) 

Der Text wird nach dem Suchtext durchsucht. Dies erfolgt in einer Schleife, die dann endet, wenn keine weitere Trefferposition mehr angezeigt wird. InStr() liefert dabei die Fundstelle des ersten Zeichens des Suchtextes, in der aufgezeigten Fassung unabhängig von Groß- und Kleinschreibung. Die Schleife wird dadurch gesteuert, dass der Suchbeginn inSuch bei jedem Schleifenende in der Summe um 1 erhöht wird (erste Schleifenzeile -1, letzte Schleifenzeile +2). Bei jedem Durchgang wird der Cursor mit oCursor.goRight(Position, false) zuerst ohne zu markieren an die Startstelle gesetzt, dann um die Länge des Suchtextes weiter mit der Markierungsaufforderung nach rechts gesetzt. Dann wird die gewünschte Formatierung (blau, etwas dicker) vorgenommen und der Cursor wieder für den nächsten Start an den Startpunkt der Markierung zurückgesetzt.

  1. 28          DO WHILE inStr(inSuch, oFeld.Text, stSuchtext) > 0 

  2. 29             inSuch = inStr(inSuch, oFeld.Text, stSuchtext) - 1 

  3. 30             oCursor.goRight(inSuch-inSuchAlt,false) 

  4. 31             oCursor.goRight(inLen,true) 

  5. 32             oCursor.CharColor = RGB(102,102,255) 

  6. 33             oCursor.CharWeight = 110.000000 

  7. 34             oCursor.goLeft(inLen,false) 

  8. 35             inSuchAlt = inSuch 

  9. 36             inSuch = inSuch + 2 

  10. 37          LOOP 

  11. 38       END IF 

  12. 39    END IF 

  13. 40 End Sub 

Die Prozedur «InhaltSchreiben» dient dazu, den Inhalt aus dem formatierbaren Textfeld «MemoFormat» in die Datenbank zu übertragen. Dies erfolgt in dieser Fassung unabhängig davon, ob eine Änderung vorgenommen wurde.

Die Prozedur ist an das folgende Ereignis gebunden: Formular → Ereignisse → Vor dem Datensatzwechsel

  1. 1 Sub InhaltSchreiben(oEvent AS OBJECT) 

  2. 2    DIM oForm AS OBJECT 

  3. 3    DIM inMemo AS INTEGER 

  4. 4    DIM loID AS LONG 

  5. 5    DIM oFeld AS OBJECT 

  6. 6    DIM stMemo AS STRING 

  7. 7    oForm = oEvent.Source 

  8. 8    IF InStr(oForm.ImplementationName, "ODatabaseForm") THEN 

Das auslösende Ereignis ist doppelt belegt. Nur der Implementationsname, der auf ODatabaseForm endet, gibt den richtigen Zugriff auf den Datensatz.

  1. 9       IF NOT oForm.isBeforeFirst() AND NOT oForm.isAfterLast() THEN 

Beim Einlesen, auch beim Reload des Formulars, steht der Cursor vor dem ersten Datensatz. Würde jetzt ein Schreibversuch unternommen, dann erscheint die Meldung «ungüliger Cursorstatus».

  1. 10          inMemo = oForm.findColumn("Memo") 

  2. 11          loID = oForm.findColumn("ID") 

  3. 12          oFeld = oForm.getByName("MemoFormat") 

  4. 13          stMemo = oFeld.Text 

  5. 14          IF stMemo <> "" THEN 

  6. 15             oForm.updateString(inMemo,stMemo) 

  7. 16          END IF 

  8. 17          IF stMemo <> "" AND oForm.getString(loID) <> "" THEN 

  9. 18             oForm.UpdateRow() 

  10. 19          END IF 

  11. 20       END IF 

  12. 21    END IF 

  13. 22 End Sub 

Das Tabellenfeld "Memo" wird aus der Datenquelle des Formulars herausgesucht. Ebenso das Feld "ID". Befindet sich im Feld «MemoFormat» Text, so wird er mit oForm.updateString() in das Feld "Memo" der Datenquelle übertragen. Nur wenn bereits ein Eintrag im Feld "ID" existiert, also der Primärschlüssel belegt ist, erfolgt ein Update. Ansonsten wird ja sowieso ein neuer Datensatz über die Formularfunktionen eingefügt, da das Formular die Änderung entsprechend bemerkt und eine Abspeicherung selbständig vornimmt.

Rechtschreibkontrolle während der Eingabe

Auf mehrzeilige Textfelder mit Formatierungen greift auch dieses Makro zu. Entsprechend muss auch, wie bei dem vorherigen Kapitel, der Inhalt bei jedem Datensatzwechsel zuerst geschrieben und danach der Inhalt des neuen Datensatzes in das Formularfeld geladen werden. Die Prozeduren «InhaltUebertragen» und «InhaltSchreiben» unterscheiden sich höchstens in dem Punkt, dass die Suchfunktion ausgeklammert werden kann.11
 

Die Rechtschreibkontrolle wird in dem obigen Formular dadurch ausgelöst, dass in dem Formularfeld entweder eine Leertaste oder ein Return betätigt wird. Sie läuft also nach der Beendigung eines Wortes jedes Mal ab und könnte gegebenenfalls auch noch mit dem Fokusverlust des Formularfeldes gekoppelt werden, damit auch das letzte Wort sicher überprüft wird.

Die Prozedur ist an das folgende Ereignis gebunden: Formular → Ereignisse → Taste losgelassen

  1. 1 SUB MarkierungFehlerDirekt(oEvent AS OBJECT) 

  2. 2    GlobalScope.BasicLibraries.LoadLibrary("Tools") 

Es wird die Funktion RTrimStr zum Entfernen von Satzzeichen am Ende vor Worten benötigt. Sonst werden alle Worte, denen ein Komma, Punkt oder irgendein anderes Satzzeichen folgt, als falsch angesehen. Mit LTrimChar müssen außerdem Klammern zum Beginn des Wortes entfernt werden.

  1. 3    DIM aProp() AS NEW com.sun.star.beans.PropertyValue 

  2. 4    DIM oLinuSvcMgr AS OBJECT 

  3. 5    DIM oSpellChk AS OBJECT 

  4. 6    DIM oFeld AS OBJECT 

  5. 7    DIM arText() 

  6. 8    DIM stWort AS STRING 

  7. 9    DIM inlenWort AS INTEGER 

  8. 10    DIM ink AS INTEGER 

  9. 11    DIM i AS INTEGER 

  10. 12    DIM oCursor AS OBJECT 

  11. 13    DIM stText AS OBJECT 

  12. 14    oLinguSvcMgr = createUnoService("com.sun.star.linguistic2.LinguServiceManager") 

  13. 15    IF NOT IsNull(oLinguSvcMgr) THEN 

  14. 16       oSpellChk = oLinguSvcMgr.getSpellChecker() 

  15. 17    END IF 

Zuerst werden alle Variablen deklariert. Danach wird auf das Rechtschreibüberprüfungsmodul SpellChecker zugegriffen. Mit diesem Modul werden anschließend die einzelnen Worte auf ihre Richtigkeit hin überprüft.

  1. 18    oFeld = oEvent.Source.Model 

  2. 19    ink = 0 

  3. 20    IF oEvent.KeyCode = 1280 OR oEvent.KeyCode = 1284 THEN 

Das Ereignis, das das Makro auslöst, ist ein Tastendruck. Zu dem Ereignis wird ein Code für jede Taste mitgeliefert, der KeyCode. Der KeyCode für die (Eingabetaste) ist 1280, der für die Leertaste ist 1284. Wie viele andere Informationen sind diese Informationen einfach durch das Tool «Xray» gewonnen worden. Wird also eine Leertaste oder die (Eingabetaste) betätigt, so wird die Rechtschreibung überprüft. Sie startet also zu jedem Wortende. Lediglich die Überprüfung für das letzte Wort ist so nicht automatisch möglich.

Bei jedem Durchlauf werden alle Worte des Textes überprüft. Die Überprüfung einzelner Worte könnte eventuell auch möglich sein, bedeutet aber erheblich mehr Aufwand.

Der Text wird also in Worte aufgesplittet. Trenner ist hier das Leerzeichen. Vorher müssen allerdings noch Trennungen an Zeilenumbrüchen erzeugt werden, die sonst später als ein Wort wahrgenommen werden.

  1. 21       stText = Join(Split(oFeld.Text,CHR(10))," ") 

  2. 22       stText = Join(Split(stText,CHR(13))," ") 

  3. 23       arText = Split(RTrim(stText)," ") 

  4. 24       FOR i = LBound(arText) TO Ubound(arText) 

  5. 25          stWort = arText(i) 

  6. 26          inlenWort = len(stWort) 

  7. 27          stWort = Trim( RtrimStr( RtrimStr( RtrimStr( RtrimStr( RtrimStr(
               RtrimStr(stWort,","), "."),"?"),"!"),"."),")")) 

  8. 28          stWort = LTrimChar(stWort,"(") 

Das einzelne Wort wird ausgelesen, seine ungekürzte Länge ist notwendig für die folgenden Bearbeitungsschritte. Nur so kann die Position des Wortes innerhalb des gesamten Textes bestimmt werden, die auch für die gezielte Markierung von Schreibfehlern gebraucht wird.

Mit Trim werden Leerzeichen entfernt, mit der Funktion RTrimStr Kommas und Satzzeichen am Ende des Textes, mit der Funktion LTrimChar Zeichen am Anfang des Textes.

  1. 29          IF stWort <> "" THEN 

  2. 30             oCursor = oFeld.createTextCursor() 

  3. 31             oCursor.gotoStart(false) 

  4. 32             oCursor.goRight(ink,false) 

  5. 33             oCursor.goRight(inLenWort,true) 

  6. 34             If Not oSpellChk.isValid(stWort, "de", aProp()) Then 

  7. 35                oCursor.CharUnderline = 9 

  8. 36                oCursor.CharUnderlineHasColor = True 

  9. 37                oCursor.CharUnderlineColor = RGB(255,51,51) 

  10. 38             ELSE 

  11. 39                oCursor.CharUnderline = 0 

  12. 40             END IF 

  13. 41          END IF 

  14. 42          ink = ink + inLenWort + 1 

  15. 43       NEXT 

  16. 44    END IF 

  17. 45 END SUB 

Hat das Wort einen Inhalt, so wird zuerst einmal ein Textcursor erstellt. Der Textcursor wird ohne Markierung an den Start des Textes in dem Eingabefeld gesetzt. Dann geht es, immer noch ohne Markierung, um den Betrag nach rechts im Text vorwärts, der in der Variablen ink gespeichert ist. Diese Variable ist am Anfang 0, nach Durchlaufen der ersten Schleife dann so groß wie das vorhergehende Wort lang war +1 für das angehängte Leerzeichen. Dann wird der Cursor mit Markierung um die Länge des aktuellen Wortes weiter gesetzt. Erfolgt jetzt eine Änderung der Buchsta­beneigenschaften, so betrifft diese nur den markierten Bereich.

Der Spellchecker startet. Als Variablen müssen das Wort und der Landescode übergeben werden. Ohne Landescode ist alles richtig. Das Array bleibt in der Regel leer.

Ist das Wort nicht in den Lexika eingetragen, so wird es mit einer roten Wellenlinie versehen. Die Wellenlinie entspricht hier der '9'. Ist das Wort eingetragen, so wird statt einer Wellenlinie keine Linie ('0') gezeichnet. Dieser Schritt ist notwendig, weil sonst ein einmal als falsch erkanntes Wort bei einer Korrektur auch weiterhin mit der roten Wellenlinie gekennzeichnet würde. Eine rote Wellenlinie würde nie aufgehoben, da es keine entgegengesetzte Formatierung gibt.

Kombinationsfelder als Listenfelder mit Eingabemöglichkeit

Aus Kombinationsfeldern und Tabellenfeldern aus dem Formular kann direkt eine Tabelle mit einem Datensatz versehen und der entsprechende Primärschlüssel in eine andere Tabelle eingetragen werden.12

Das Modul «Comboboxen» macht aus den Formularfeldern zur Eingabe und Auswahl von Werten (Kombinationsfelder) Listenfelder mit Eingabemöglichkeiten. Dazu werden neben den Kombinationsfeldern im Formular die jeweils an die zugrundeliegende Tabelle zu übergebenden Schlüsselfeldwerte in den Tabellenspalten abgelegt, die dem Formular zugrunde liegen. Die Schlüssel aus den Tabellenspalten werden beim Start des Formulars ausgelesen und das Kombinationsfeld auf den entsprechenden Inhalt eingestellt. Wird der Inhalt des Kombinationsfeldes geändert, so wird er neu abgespeichert und der neue Primärschlüssel zum Abspeichern in der Haupttabelle in das entsprechende numerische Fremdschlüsselfeld übertragen.

Werden statt der Tabellen entsprechend konstruierte eingabefähige Abfragen erstellt, so kann der Text, den die Kombinationsfelder darstellen sollen, direkt aus den Abfragen ermittelt werden. Ein Makro ist dann für diesen Arbeitsschritt nicht notwendig.

Voraussetzung für die Funktionsweise des Makros ist, dass alle Primärschlüssel der Tabellen, die in den Kombinationsfeldern als Datenquellen auftauchen, mit einem automatisch hochzählenden Autowert versehen sind. Außerdem ist als Bezeichnung hier vorausgesetzt, dass die Primärschlüssel den Namen "ID" tragen.

Textanzeige im Kombinationsfeld

Diese Prozedur soll Text in den Kombinationsfeldern nach den Werten der (unsichtbaren) Fremdschlüssel-Felder aus dem Hauptformular einstellen. Dabei werden gegebenenfalls auch Listenfelder berücksichtigt, die sich auf 2 unterschiedliche Tabellen beziehen. Dies kann z.B. dann sein, wenn bei einer Ortsangabe die Postleitzahl vom Ort abgetrennt wurde. Dann wird die Postleitzahl aus einer Tabelle ausgelesen, in der auch ein Fremdschlüssel für den Ort liegt. Im Listenfeld werden Postleitzahl und Ort zusammen angezeigt.

  1. 1 SUB TextAnzeigen(oEvent AS OBJECT) 

Dieses Makro sollte an das folgende Ereignis des Formulars gebunden werden: 'Nach dem Datensatzwechsel'

Das Makro wird direkt aus dem Formular angesprochen. Über das auslösende Ereignis werden die gesamten notwendigen Variablen für das Makro ermittelt.

Die Variablen werden deklariert. Einige Variablen sind in einem separaten Modul bereits global deklariert und werden hier nicht noch einmal erwähnt.

  1. 2    DIM oForm AS OBJECT 

  2. 3    DIM oFeld AS OBJECT 

  3. 4    DIM oFeldList AS OBJECT 

  4. 5    DIM stAbfrage AS STRING 

  5. 6    DIM stFeldWert AS STRING 

  6. 7    DIM stFeldID AS STRING 

  7. 8    DIM inCom AS INTEGER 

  8. 9    oForm = oEvent.Source 

Das Formular startet das Ereignis. Es ist die Quelle für das das Makro auslösende Ereignis.

In dem Formular befindet sich ein verstecktes Kontrollelement, aus dem hervorgeht, wie die verschiedenen Kombinationsfelder in diesem Formular heißen. Nacheinander werden dann in dem Makro die Kombinationsfelder abgearbeitet.

  1. 10    aComboboxen() = Split(oForm.getByName("Comboboxen").Tag,",") 

  2. 11    FOR inCom = LBound(aComboboxen) TO Ubound(aComboboxen)
         ...
      NEXT inCom         

Aus den Zusatzinformationen («Tag») des versteckten Kontrollelementes wird die Bezeichnung der Kombinationsfelder ermittelt. Sie sind dort durch Kommas voneinander getrennt aufgeschrieben. Die Namen der Felder werden in ein Array geschrieben und nacheinander in einer Schleife abgearbeitet. Die Schleife endet mit der Bezeichnung NEXT … .

Das Kombinationsfeld, das jetzt statt eines Listenfeldes existiert, wird anschließend als oFeldList bezeichnet. Der Fremdschlüssel wird über die Bezeichnung des Tabellenfeldes, die in den Zusatzinformationen des Kombinationsfeldes steht, aus der Tabellenspalte des Formulars ermittelt.

  1. 12       oFeldList = oForm.getByName(Trim(aComboboxen(inCom))) 

  2. 13       stFeldID = oForm.getString(oForm.findColumn(oFeldList.Tag)) 

  3. 14       oFeldList.Refresh() 

Das Kombinationsfeld wird mit Refresh() neu eingelesen. Es kann ja sein, dass sich der Inhalt des Feldes durch Neueingaben geändert hat. Diese müssen schließlich verfügbar gemacht werden.

Die Abfrage, die zur Ermittlung des anzuzeigenden Inhaltes des Kombinationsfeldes notwendig ist, wird aus der Abfrage des Kombinationsfeldes und dem ermittelten Wert des Fremdschlüssels erstellt. Damit der SQL-Code brauchbar wird, wird zuerst eine eventuelle Sortieranweisung entfernt. Anschließend wird nachgesehen, ob bereits eine Beziehungsdefinition (beginnend mit WHERE) existiert. Da die InStr()-Funktion standardmäßig keinen Unterschied zwischen Groß- und Kleinschreibung macht, werden hier gleich alle Schreibweisen abgedeckt. Existiert eine Beziehungsdefinition, so enthält die Abfrage Felder aus zwei unterschiedlichen Tabellen. Es muss jetzt die Tabelle herausgesucht werden, aus der der Fremdschlüssel für die Verbindung zur Verfügung gestellt wird. Das Makro funktioniert hier nur mit Hilfe der Information, dass der Primärschlüssel einer jeden Tabelle "ID" heißt.

Existiert keine Beziehungsdefinition, so beruht die Abfrage nur auf einer Tabelle. Die Tabelleninformation kann entfallen, die Bedingung direkt mit dem Fremdschlüsselwert zusammen formuliert werden.

  1. 15       IF stFeldID <> "" THEN 

  2. 16          stAbfrage = oFeldList.ListSource 

  3. 17          IF InStr(stAbfrage,"order by") > 0 THEN 

  4. 18             stSql = Left(stAbfrage, InStr(stAbfrage,"order by")-1) 

  5. 19          ELSE  

  6. 20             stSql = stAbfrage 

  7. 21          END IF 

  8. 22          IF InStr(stSql,"where") THEN 

  9. 23             st = Right(stSql, Len(stSql)-InStr(stSql,"where")-4) 

  10. 24             IF InStr(Left(st, InStr(st,"=")),".""ID""") THEN 

  11. 25                a() = Split(Right(st, Len(st)-InStr(st,"=")-1),".") 

  12. 26             ELSE 

  13. 27                a() = Split(Left(st, InStr(st,"=")-1),".") 

  14. 28             END IF 

  15. 29             stSql = stSql + "AND "+a(0)+".""ID"" = "+stFeldID 

  16. 30          ELSE 

  17. 31             stSql = stSql + "WHERE ""ID"" = "+stFeldID 

  18. 32          END IF 

Jedes Feld und jeder Tabellenname muss bereits in der SQL-Eingabe mit doppelten Anführungsstrichen oben versehen werden. Da bereits Anführungsstriche einfacher Art in Basic als die Einführung zu Text interpretiert werden, sind diese bei der Weitergabe des Codes nicht mehr sichtbar. Erst bei einer Doppelung der Anführungsstriche wird ein Element mit einfachen Anführungsstrichen weitergegeben. ""ID"" bedeutet also, dass in der Abfrage auf das Feld "ID" (mit einfachen Anführungsstrichen für die SQL-Verbindung) zugegriffen wird.

Die in der Variablen stSql abgespeicherte Abfrage wird jetzt ausgeführt und das Ergebnis dieser Abfrage in der Variablen oAbfrageergebnis gespeichert.

  1. 33          oDatenquelle = ThisComponent.Parent.CurrentController 

  2. 34          IF NOT (oDatenquelle.isConnected()) Then 

  3. 35             oDatenquelle.connect() 

  4. 36          End IF 

  5. 37          oVerbindung = oDatenquelle.ActiveConnection() 

  6. 38          oSQL_Anweisung = oVerbindung.createStatement() 

  7. 39          oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql) 

Das Abfrageergebnis wird über eine Schleife ausgelesen. Hier könnten, wie in einer Abfrage aus der GUI, mehrere Felder und Datensätze dargestellt werden. Von der Konstruktion der Abfrage her wird aber nur ein Ergebnis erwartet. Dieses Ergebnis wird in der ersten Spalte (1) der Abfrage zu finden sein. Es ist der Datensatz, der den anzuzeigenden Inhalt des Kombinationsfeldes wiedergibt. Der Inhalt ist ein Textinhalt (getString()), deshalb hier oAbfrageergebnis.getString(1).

  1. 40          WHILE oAbfrageergebnis.next 

  2. 41             stFeldWert = oAbfrageergebnis.getString(1) 

  3. 42          WEND 

Das Kombinationsfeld muss jetzt auf den aus der Abfrage sich ergebenden Textwert eingestellt werden.

  1. 43          oFeldList.Text = stFeldWert 

  2. 44       ELSE 

Falls überhaupt kein Wert in dem Feld für den Fremdschlüssel oFeld vorhanden ist, ist auch die ganze Abfrage nicht gelaufen. Das Kombinationsfeld wird jetzt auf eine leere Anzeige eingestellt.

  1. 45          oFeldList.Text = "" 

  2. 46       END IF 

  3. 47    NEXT inCom 

  4. 48 END SUB 

Diese Prozedur erledigt jetzt also den Kontakt von dem in der Datenquelle des Formulars abgelegten Fremdschlüssel zu dem Kombinationsfeld. Für die Anzeige der richtigen Werte im Kombinationsfeld würde das ausreichen. Eine Abspeicherung von neuen Werten hingegen benötigt eine weitere Prozedur.

Fremdschlüsselwert vom Kombinationsfeld zum numerischen Feld übertragen

Wird nun ein neuer Wert ausgewählt oder neu in das Kombinationsfeld eingegeben (nur wegen dieser Eigenschaft wurde ja das Makro konstruiert), so muss der entsprechende Primärschlüssel als Fremdschlüssel in die dem Formular zugrundeliegende Tabelle eingetragen werden.

  1. 1 SUB TextAuswahlWertSpeichern(oEvent AS OBJECT) 

Dieses Makro sollte an das folgende Ereignis des Formulars gebunden werden: 'Vor der Datensatzaktion'.

Nach Deklaration der Variablen (hier nicht weiter aufgeführt) wird zuerst differenziert, bei welchem Ereignis genau das Makro überhaupt ablaufen soll. Vor der Datensatzaktion werden zwei Implementationen nacheinander aufgerufen. Für das Makro selbst ist es wichtig, das Formularobjekt zu erhalten. Das geht prinzipiell über beide Implementationen, aber eben auf unterschiedliche Weise. Es wird hier die Implementation mit dem Namen "ODatabaseForm" herausgefiltert.

  1. 2    IF InStr(oEvent.Source.ImplementationName,"ODatabaseForm") THEN
         
    ...
      END IF
    END
    SUB 

In diese Schleife wird der gleiche Start wie bei der Prozedur TextAnzeigen eingebaut:

  1. 3       oForm = oEvent.Source 

  2. 4       aComboboxen() = Split(oForm.getByName("Comboboxen").Tag,",") 

  3. 5       FOR inCom = LBound(aComboboxen) TO Ubound(aComboboxen)
            ...
         NEXT inCom 

Das Feld oFeldList zeigt den Text an. Es kann in einem Tabellenkontrollfeld liegen. Dann kann nicht direkt vom Formular auf das Feld zugegriffen werden. In den Zusatzinformationen des versteckten Kontrollfeldes «Comboboxen» ist für diesen Fall der Pfad zum Kombinationsfeld über «Tabellenkontrollfeld>Kombinationsfeld» eingetragen. Durch Aufsplittung dieses Eintrages wird ermittelt, wie das Kombinationsfeld anzusprechen ist.

  1. 6          a() = Split(Trim(aComboboxen(inCom)),">") 

  2. 7          IF Ubound(a) > 0 THEN 

  3. 8             oFeldList = oForm.getByName(a(0)).getByName(a(1)) 

  4. 9          ELSE 

  5. 10             oFeldList = oForm.getByName(a(0)) 

  6. 11          END IF 

Anschließend wird die Abfrage aus dem Kombinationsfeld ausgelesen und in ihre Einzelteile zerlegt. Bei einfachen Kombinationsfeldern wären die notwendigen Informationen lediglich der Feldname und der Tabellenname:

  1. 1 SELECT "Feld" FROM "Tabelle" 

Dies könnte gegebenenfalls noch durch eine Sortierung erweitert sein. Sobald zwei Felder in dem Kombinationsfeld zusammen dargestellt werden, muss aber bereits bei den Feldern zur Trennung entsprechend mehr Aufwand getrieben werden:

  1. 1 SELECT "Feld1"||' '||"Feld2" FROM "Tabelle" 

Diese Abfrage fasst zwei Felder zusammen und setzt dazwischen eine Leertaste ein. Da der Trenner eine Leertaste ist, wird in dem Makro nach so einem Trenner gesucht und danach der Text in zwei Teile gesplittet. Das funktioniert natürlich nur dann einwandfrei, wenn "Feld1" nicht bereits Text enthalten soll, der eine Leertaste erlaubt. Sonst wird z.B. aus dem Vornamen «Anne Marie» und dem Nachnamen «Müller» durch das Makro der Vorname «Anne» und der Nachname «Marie Müller». Für solch einen Fall sollte ein passender Trenner eingesetzt werden, der dann auch vom Makro gefunden werden kann. Bei Namen ist dies z. B. ein Komma: «Nachname, Vorname».

Noch komplizierter wird es, wenn die beiden enthaltenen Felder aus zwei verschiedenen Tabellen stammen:

  1. 1 SELECT "Tabelle1"."Feld1"||' > '||"Tabelle2"."Feld2"  

  2. 2 FROM "Tabelle1", "Tabelle2"  

  3. 3 WHERE "Tabelle1"."ID" = "Tabelle2"."FremdID"  

  4. 4 ORDER BY "Tabelle1"."Feld1"||' > '||"Tabelle2"."Feld2" ASC 

Hier müssen die Felder voneinander getrennt, die Tabellenzuordnungen zu den Feldern erfasst und die Fremdschlüsselzuweisung ermittelt werden.

  1. 12          stAbfrage = oFeldList.ListSource 

  2. 13          aFelder() = Split(stAbfrage, """") 

  3. 14          stInhalt = "" 

  4. 15          FOR i=LBound(aFelder)+1 TO UBound(aFelder) 

Der Inhalt der Abfrage wird von Ballast befreit. Die Teile werden anschließend über eine nicht übliche Zeichenkombination zu einem Array wieder zusammengefügt.«FROM» trennt die sichtbare Feldanzeige von der Tabellenbezeichnung. «WHERE» trennt die Beziehungsdefinition von der Tabellenbezeichnung. Joins werden nicht unterstützt.

  1. 16             IF Trim(UCASE(aFelder(i))) = "ORDER BY" THEN 

  2. 17                EXIT FOR 

  3. 18             ELSEIF Trim(UCASE(aFelder(i))) = "FROM" THEN 

  4. 19                stInhalt = stInhalt+" §§ " 

  5. 20             ELSEIF Trim(UCASE(aFelder(i))) = "WHERE" THEN 

  6. 21                stInhalt = stInhalt+" §§ " 

  7. 22             ELSE 

  8. 23                stInhalt = stInhalt+Trim(aFelder(i)) 

  9. 24             END IF 

  10. 25          NEXT i 

  11. 26          aInhalt() = Split(stInhalt, " §§ ") 

Die sichtbare Feldanzeige wird gegebenenfalls in Inhalte aus verschiedenen Feldern aufgeteilt:

  1. 27          aErster() = Split(aInhalt(0),"||") 

  2. 28          IF UBound(aErster) > 0 THEN 

  3. 29             IF UBound(aInhalt) > 1 THEN 

Der erste Teil enthält mindestens 2 Felder. Die Felder haben zu Beginn eine Tabellenbezeichnung. Der zweite Teil enthält zwei Tabellenbezeichnungen, die aber schon aus dem ersten Teil ermittelt werden können. Der dritte Teil enthält eine Beziehung über einen Fremdschlüssel mit «=» getrennt:

  1. 30                aTest() = Split(aErster(0),".") 

  2. 31                NameTabelle1 = aTest(0) 

  3. 32                NameTabellenFeld1 = aTest(1) 

  4. 33                Erase aTest 

  5. 34                Feldtrenner = Join(Split(aErster(1),"'"),"") 

  6. 35                aTest() = Split(aErster(2),".") 

  7. 36                NameTabelle2 = aTest(0) 

  8. 37                NameTabellenFeld2 = aTest(1) 

  9. 38                Erase aTest 

  10. 39                aTest() = Split(aInhalt(2),"=") 

  11. 40                aTest1() = Split(aTest(0),".") 

  12. 41                IF aTest1(1) <> "ID" THEN 

  13. 42                   NameTab12ID = aTest1(1) 

  14. 43                   IF aTest1(0) = NameTabelle1 THEN 

  15. 44                   Position = 2 

  16. 45                   ELSE 

  17. 46                      Position = 1 

  18. 47                   END IF 

  19. 48                ELSE 

  20. 49                   Erase aTest1 

  21. 50                   aTest1() = Split(aTest(1),".") 

  22. 51                   NameTab12ID = aTest1(1)         

  23. 52                   IF aTest1(0) = NameTabelle1 THEN 

  24. 53                      Position = 2 

  25. 54                   ELSE 

  26. 55                      Position = 1 

  27. 56                   END IF 

  28. 57                END IF 

  29. 58             ELSE 

Der erste Teil enthält zwei Feldbezeichnungen ohne Tabellenbezeichnung mit Trenner, der zweite Teil enthält die Tabellenbezeichnung. Ein dritter Teil ist nicht vorhanden:

  1. 59                NameTabellenFeld1 = aErster(0) 

  2. 60                Feldtrenner = Join(Split(aErster(1),"'"),"") 

  3. 61                NameTabellenFeld2 = aErster(2) 

  4. 62                NameTabelle1 = aInhalt(1) 

  5. 63             END IF 

  6. 64          ELSE 

Es existiert nur ein Feld, das aus einer Tabelle stammt:

  1. 65             NameTabellenFeld1 = aErster(0) 

  2. 66             NameTabelle1 = aInhalt(1) 

  3. 67          END IF 

Die maximale Zeichenlänge, die eine Eingabe haben darf, wird im Folgenden mit der Funktion Spaltengroesse ermittelt. Das Kombinationsfeld kann hier mit seiner Beschränkung nicht sicher weiterhelfen, da ja ermöglicht werden soll, gleichzeitig 2 Felder in einem Kombinationsfeld einzutragen.

  1. 68          LaengeFeld1 = Spaltengroesse(NameTabelle1,NameTabellenFeld1) 

  2. 69          IF NameTabellenFeld2 <> "" THEN 

  3. 70             IF NameTabelle2 <> "" THEN 

  4. 71                LaengeFeld2 = Spaltengroesse(NameTabelle2,NameTabellenFeld2) 

  5. 72             ELSE 

  6. 73                LaengeFeld2 = Spaltengroesse(NameTabelle1,NameTabellenFeld2) 

  7. 74             END IF 

  8. 75          ELSE 

  9. 76             LaengeFeld2 = 0 

  10. 77          END IF 

Der Inhalt des Kombinationsfeldes wird ausgelesen:

  1. 78          stInhalt = oFeldList.getCurrentValue() 

Der angezeigte Inhalt des Kombinationsfeldes wird ausgelesen. Leertasten und nicht druckbare Zeichen am Anfang und Ende der Eingabe werden gegebenenfalls entfernt.

  1. 79          stInhalt = Trim(stInhalt) 

  2. 80          IF stInhalt <> "" THEN 

  3. 81             IF NameTabellenFeld2 <> "" THEN 

Wenn ein zweites Tabellenfeld existiert, muss der Inhalt des Kombinationsfeldes gesplittet werden. Um zu wissen, an welcher Stelle die Aufteilung vorgenommen werden soll, ist der Feldtrenner von Bedeutung, der der Funktion als Variable mitgegeben wird. Ein Leerzeichen aus dem Feldtrenner wird bei der Funktion «Split» nicht direkt erkannt. Deswegen wird das ASCII-Zeichen noch einmal in den entsprechenden Feldtrenner umgewandelt.

  1. 82                IF ASC(Feldtrenner) = 32 THEN 

  2. 83                   Feldtrenner = " "  

  3. 84                END IF 

  4. 85                a_stTeile = Split(stInhalt, Feldtrenner, 2) 

Der letzte Parameter weist darauf hin, dass maximal 2 Teile erzeugt werden.

Abhängig davon, welcher Eintrag mit dem Feld 1 und welcher mit dem Feld 2 zusammenhängt, wird jetzt der Inhalt des Kombinationsfeldes den einzelnen Variablen zugewiesen. «Position = 2» wird hier als Zeichen dafür genommen, dass an zweiter Position der Inhalt für das Feld 2 steht. Das ist auch dann der Fall, wenn beide Felder aus einer Tabelle stammen.

  1. 86                IF Position = 2 OR Position = 0 THEN 

  2. 87                   stInhalt = Trim(a_stTeile(0)) 

  3. 88                   IF UBound(a_stTeile()) > 0 THEN 

  4. 89                      stInhaltFeld2 = Trim(a_stTeile(1)) 

  5. 90                   ELSE 

  6. 91                      stInhaltFeld2 = "" 

  7. 92                   END IF 

  8. 93                   stInhaltFeld2 = Trim(a_stTeile(1)) 

  9. 94                ELSE 

  10. 95                   stInhaltFeld2 = Trim(a_stTeile(0)) 

  11. 96                   IF UBound(a_stTeile()) > 0 THEN 

  12. 97                      stInhalt = Trim(a_stTeile(1)) 

  13. 98                   ELSE 

  14. 99                      stInhalt = "" 

  15. 100                   END IF 

  16. 101                   stInhalt = Trim(a_stTeile(1)) 

  17. 102                END IF 

  18. 103             END IF 

Es kann passieren, dass bei zwei voneinander zu trennenden Inhalten die Größeneinstellung des Kombinationsfeldes (Textlänge) nicht zu einem der abzuspeichernden Tabellenfelder passt. Bei Kombinationsfeldern für nur ein Feld wird dies in der Regel durch Einstellungen im Formularkontrollfeld erledigt. Hier muss hingegen ein eventueller Fehler abgefangen werden. Es wird darauf hingewiesen, wie lang der Inhalt für das jeweilige Feld sein darf.

  1. 104             IF (LaengeFeld1 > 0 AND Len(stInhalt) > LaengeFeld1) OR
                  (LaengeFeld2 > 0 AND Len(stInhaltFeld2) > LaengeFeld2) THEN 

Wenn die Feldlänge des 1. oder 2. Teiles zu groß ist, wird erst einmal ein Standardtext in je einer Variablen abgespeichert. CHR(13) fügt hier einen Zeilenumbruch hinzu.

  1. 105                stmsgbox1 = "Das Feld " + NameTabellenFeld1 + " darf höchstens " +
                     LaengeFeld1 + "Zeichen lang sein." + CHR(13) 

  2. 106                stmsgbox2 = "Das Feld " + NameTabellenFeld2 + " darf höchstens " +
                     LaengeFeld2 + "Zeichen lang sein." + CHR(13) 

Sind beide Feldinhalte zu lang, so wird der Text mit beiden Feldinhalten ausgegeben.

  1. 107                IF (LaengeFeld1 > 0 AND Len(stInhalt) > LaengeFeld1) AND
                     (LaengeFeld2 > 0 AND Len(stInhaltFeld2) > LaengeFeld2) THEN 

  2. 108                   msgbox ("Der eingegebene Text ist zu lang." + CHR(13) +
                        stmsgbox1 + stmsgbox2 + "Bitte den Text kürzen.",
                        64,"Fehlerhafte Eingabe") 

Die Anzeige erfolgt mit der Funktion msgbox(). Sie erwartet zuerst einen Text, dann optional einen Zahlenwert (der zu einer entsprechenden Darstellungsform gehört) und schließlich optional einen Text als Überschrift über dem Fenster. Das Fenster hat hier also die Überschrift "Fehlerhafte Eingabe", die '64' fügt das Informationssymbol hinzu.

Im Folgenden werden alle auftretenden weiteren Fälle zu großer Textlänge abgearbeitet.

  1. 109                ELSEIF (LaengeFeld1 > 0 AND Len(stInhalt) > LaengeFeld1) THEN 

  2. 110                   msgbox ("Der eingegebene Text ist zu lang." + CHR(13) +
                        stmsgbox1 + "Bitte den Text kürzen.",64,"Fehlerhafte Eingabe") 

  3. 111                ELSE 

  4. 112                   msgbox ("Der eingegebene Text ist zu lang." + CHR(13) +
                        stmsgbox2 + "Bitte den Text kürzen.",64,"Fehlerhafte Eingabe") 

  5. 113                END IF 

  6. 114             ELSE 

Liegt kein zu langer Text vor, so kann die Funktion weiter durchlaufen. Ansonsten endet sie hier.

Jetzt werden die Inhaltseingaben so maskiert, dass eventuell vorhandene Hochkommata keine Fehlermeldung erzeugen.

  1. 115                stInhalt = String_to_SQL(stInhalt) 

  2. 116                IF stInhaltFeld2 <> "" THEN 

  3. 117                   stInhaltFeld2 = String_to_SQL(stInhaltFeld2) 

  4. 118                END IF 

Zuerst werden Variablen vorbelegt, die anschließend per Abfrage geändert werden können. Die Variablen inID1 und inID2 sollen den Inhalt der Primärschlüsselfelder der beiden Tabellen speichern. Da bei einer Abfrage, die kein Ergebnis wiedergibt, durch Basic einer Integer-Variablen 0 zugewiesen wird, dies aber für das Abfrageergebnis auch bedeuten könnte, dass der ermittelte Primärschlüssel eben den Wert 0 hat, wird die Variable auf jeweils -1 voreingestellt. Diesen Wert nimmt ein Autowert-Feld bei der HSQLDB nicht automatisch an.

Anschließend wird die Datenbankverbindung erzeugt, soweit sie nicht schon besteht.

  1. 119                inID1 = -1 

  2. 120                inID2 = -1 

  3. 121                oDatenquelle = ThisComponent.Parent.CurrentController 

  4. 122                If NOT (oDatenquelle.isConnected()) Then 

  5. 123                   oDatenquelle.connect() 

  6. 124                End If 

  7. 125                oVerbindung = oDatenquelle.ActiveConnection() 

  8. 126                oSQL_Anweisung = oVerbindung.createStatement() 

  9. 127                IF NameTabellenFeld2 <> "" AND NOT IsEmpty(stInhaltFeld2) AND
                     NameTabelle2 <> "" THEN 

Wenn ein zweites Tabellenfeld existiert, muss zuerst die zweite Abhängigkeit geklärt werden. Zuerst wird überprüft, ob für den zweiten Wert in der Tabelle 2 bereits ein Eintrag existiert. Existiert dieser Eintrag nicht, so wird er eingefügt.

Beispiel: Die Tabelle 2 ist die Tabelle "Ort". In ihr werden also Orte abgespeichert. Ist z.B. ein Eintrag für den Ort 'Rheine' vorhanden, so wird der entsprechende Primärschlüssel­eintrag ausgelesen. Ist der Eintrag 'Rheine' nicht vorhanden, wird er eingefügt und anschließend der beim Einfügen erzeugte Primärschlüsselwert festgestellt.

  1. 128                   stSql = "SELECT ""ID"" FROM """ + NameTabelle2 + """ WHERE """ +
                        NameTabellenFeld2 + """='" + stInhaltFeld2 + "'" 

  2. 129                   oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql) 

  3. 130                   WHILE oAbfrageergebnis.next 

  4. 131                      inID2 = oAbfrageergebnis.getInt(1) 

  5. 132                   WEND 

  6. 133                   IF inID2 = -1 THEN 

  7. 134                      stSql = "INSERT INTO """ + NameTabelle2 + """ (""" +
                           NameTabellenFeld2 + """) VALUES ('" + stInhaltFeld2 + "') " 

  8. 135                      oSQL_Anweisung.executeUpdate(stSql) 

  9. 136                      stSql = "CALL IDENTITY()" 

Ist der Inhalt in der entsprechenden Tabelle nicht vorhanden, so wird er eingefügt. Der dabei entstehende Primärschlüsselwert wird anschließend ausgelesen. Ist der Inhalt bereits vorhanden, so wird der Primärschlüsselwert durch die vorangehende Abfrage ermittelt. Die Funktion geht hier von automatisch erzeugten Primärschlüsselfeldern (IDENTITY) aus.

Die Funktion CALL IDENTITY() ist in Firebird unbekannt. Firebird hat hierfür eigentlich die Funktion RETURNING vorgesehen, die direkt an den SQL-Befehl angehängt wird und der Ausgabe den Primärschlüsselwert mitgibt. Leider funktioniert dies unter LO zur Zeit nicht. Stattdessen muss über eine separate Abfrage der gerade erzeugte Primärschlüsselwert ermittelt werden.

  1. 137                      oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql) 

  2. 138                      WHILE oAbfrageergebnis.next 

  3. 139                                 inID2 = oAbfrageergebnis.getInt(1) 

  4. 140                      WEND 

  5. 141                   END IF 

Der Primärschlüssel aus dem zweiten Wert wird in der Variablen 'inID2' zwischengespeichert. Jetzt wird überprüft, ob eventuell dieser Schlüsselwert bereits in der Tabelle 1 zusammen mit dem Eintrag aus dem ersten Feld vorhanden ist. Ist diese Kombination nicht vorhanden, so wird sie neu eingefügt.

Beispiel: Für den Ort 'Rheine' aus der Tabelle 2 können in der Tabelle 1 mehrere Postleitzahlen verfügbar sein. Ist die Kombination '48431' und 'Rheine' vorhanden, so wird nur der Primär­schlüssel aus der Tabelle 1 ausgelesen, in der die Postleitzahlen und der Fremdschlüssel aus der Tabelle 2 gespeichert wurden.

  1. 142                   stSql = "SELECT ""ID"" FROM """ + NameTabelle1 + """ WHERE """ +
                        NameTab12ID + """='" + inID2 + "' AND """ +
                        NameTabellenFeld1 + """ = '" + stInhalt + "'" 

  2. 143                   oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql) 

  3. 144                   WHILE oAbfrageergebnis.next 

  4. 145                      inID1 = oAbfrageergebnis.getInt(1) 

  5. 146                   WEND 

War der Inhalt der ersten Tabelle noch nicht vorhanden, so wird der Inhalt neu abgespeichert (INSERT).

Beispiel: Existiert bereits die Postleitzahl '48429' in Kombination mit dem Fremdschlüssel aus der Tabelle 2 "Ort", so wird auf jeden Fall ein neuer Datensatz erzeugt, wenn jetzt die Postleitzahl '48431' auftaucht. Der vorhergehenden Datensatz wird also nicht auf die neue Postleitzahl geändert. Schließlich sind durch die n:1-Verknüpfung der Tabellen mehrere Postleitzahlen für einen Ort ermöglicht worden.

  1. 147                   IF inID1 = -1 THEN 

  2. 148                      stSql = "INSERT INTO """ + NameTabelle1 + """ (""" +
                           NameTabellenFeld1 + """,""" + NameTab12ID + """)
                           VALUES ('" + stInhalt + "','" + inID2 + "') " 

  3. 149                      oSQL_Anweisung.executeUpdate(stSql) 

Der Primärschlüssel der ersten Tabelle muss schließlich wieder ausgelesen werden, damit er in die dem Formular zugrundeliegende Tabelle übertragen werden kann.

  1. 150                      stSql = "CALL IDENTITY()" 

  2. 151                      oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql) 

  3. 152                      WHILE oAbfrageergebnis.next 

  4. 153                         inID1 = oAbfrageergebnis.getInt(1) 

  5. 154                      WEND 

  6. 155                   END IF 

  7. 156                END IF 

Für den Fall, dass beide in dem Kombinationsfeld zugrundeliegenden Felder in einer Tabelle gespeichert sind (z. B. Nachname, Vorname in der Tabelle Name) muss eine andere Abfrage erfolgen:

  1. 157                IF NameTabellenFeld2 <> "" AND NameTabelle2 = "" THEN 

  2. 158                   stSql = "SELECT ""ID"" FROM """ + NameTabelle1 + """ WHERE """ +
                        NameTabellenFeld1 + """='" + stInhalt + "' AND """ +
                        NameTabellenFeld2 + """='" + stInhaltFeld2 + "'" 

  3. 159                   oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql) 

  4. 160                   WHILE oAbfrageergebnis.next 

  5. 161                      inID1 = oAbfrageergebnis.getInt(1) 

  6. 162                   WEND 

  7. 163                   IF inID1 = -1 THEN 

Wenn eine zweite Tabelle nicht existiert:

  1. 164                      stSql = "INSERT INTO """ + NameTabelle1 + """ (""" +
                           NameTabellenFeld1 + """,""" + NameTabellenFeld2 + """)
                           VALUES ('" + stInhalt + "','" + stInhaltFeld2 + "') " 

  2. 165                      oSQL_Anweisung.executeUpdate(stSql) 

Anschließend wird das Primärschlüsselfeld wieder ausgelesen.

  1. 166                      stSql = "CALL IDENTITY()" 

  2. 167                      oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql) 

  3. 168                      WHILE oAbfrageergebnis.next 

  4. 169                                 inID1 = oAbfrageergebnis.getInt(1) 

  5. 170                      WEND 

  6. 171                   END IF 

  7. 172                END IF 

  8. 173                IF NameTabellenFeld2 = "" THEN 

Jetzt wird der Fall geklärt, der der einfachste ist: Das 2. Tabellenfeld existiert nicht und der Eintrag ist noch nicht in der Tabelle vorhanden. In das Kombinationsfeld ist also ein einzelner neuer Wert eingetragen worden.

  1. 174                   stSql = "SELECT ""ID"" FROM """ + NameTabelle1 + """ WHERE """ +
                        NameTabellenFeld1 + """='" + stInhalt + "'" 

  2. 175                   oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql) 

  3. 176                   WHILE oAbfrageergebnis.next 

  4. 177                      inID1 = oAbfrageergebnis.getInt(1) 

  5. 178                   WEND 

  6. 179                   IF inID1 = -1 THEN 

Wenn ein zweites Tabellenfeld nicht existiert, wird der Inhalt neu eingefügt ...

  1. 180                      stSql = "INSERT INTO """ + NameTabelle1 + """ (""" +
                           NameTabellenFeld1 + """) VALUES ('" + stInhalt + "') " 

  2. 181                      oSQL_Anweisung.executeUpdate(stSql) 

… und die entsprechende ID direkt wieder ausgelesen. (Hsqldb, Firebird)

  1. 182                      stSql = "CALL IDENTITY()" 

  2. 183                      oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql) 

  3. 184                      WHILE oAbfrageergebnis.next 

  4. 185                                 inID1 = oAbfrageergebnis.getInt(1) 

  5. 186                      WEND 

  6. 187                   END IF 

  7. 188                END IF 

Der Wert des Primärschlüsselfeldes muss ermittelt werden, damit er in die Haupttabelle des Formulars übertragen werden kann.

Anschließend wird der aus all diesen Schleifen ermittelte Primärschlüsselwert in das Feld der Haupttabelle und die darunterliegende Datenbank übertragen. Mit findColumn wird das mit dem Formularfeld verbundene Tabellenfeld erreicht. Mit updateLong wird eine Integer-Zahl (siehe «Datentypen in StarBasic») diesem Feld zugewiesen.

  1. 189                oForm.updateLong(oForm.findColumn(oFeldList.Tag),inID1) 

  2. 190             END IF 

  3. 191          ELSE 

Ist kein Primärschlüsselwert einzutragen, weil auch kein Eintrag in dem Kombinationsfeld erfolgte oder dieser Eintrag gelöscht wurde, so ist auch der Inhalt des Feldes zu löschen. Mit updateNULL() wird das Feld mit dem datenbankspezifischen Ausdruck für ein leeres Feld, NULL, versehen.

  1. 192             oForm.updateNULL(oForm.findColumn(oFeldList.Tag),NULL) 

  2. 193          END IF 

  3. 194     NEXT inCom 

  4. 195    END IF 

  5. 196 END SUB 

Kontrollfunktion für die Zeichenlänge der Kombinationsfelder

Die folgende Funktion soll die Zeichenlänge der jeweiligen Tabellenspalten ermitteln, damit zu lange Eingaben nicht einfach gekürzt werden. Der Typ FUNCTION wurde hier wegen der Rückgabewerte gewählt.

  1. 1 FUNCTION Spaltengroesse(Tabellenname AS STRING, Feldname AS STRING) AS INTEGER 

  2. 2    oDatenquelle = ThisComponent.Parent.CurrentController 

  3. 3    If NOT (oDatenquelle.isConnected()) Then 

  4. 4       oDatenquelle.connect() 

  5. 5    End If 

  6. 6    oVerbindung = oDatenquelle.ActiveConnection() 

  7. 7    oSQL_Anweisung = oVerbindung.createStatement() 

  8. 8    stSql = "SELECT ""COLUMN_SIZE"" FROM ""INFORMATION_SCHEMA"".""SYSTEM_COLUMNS""
         WHERE ""TABLE_NAME"" = '"
    + Tabellenname + "' AND ""COLUMN_NAME"" = '"
         + Feldname + "'" 

  9. 9    oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql) 

  10. 10    WHILE oAbfrageergebnis.next 

  11. 11       i = oAbfrageergebnis.getInt(1) 

  12. 12    WEND 

  13. 13    Spaltengroesse = i 

  14. 14 END FUNCTION 

Für Firebird muss der SQL-Code angepasst werden:
  1. 8 stSql = "SELECT B.RDB$FIELD_LENGTH
      FROM RDB$RELATION_FIELDS AS A, RDB$FIELDS AS B
      WHERE A.RDB$FIELD_SOURCE = B.RDB$FIELD_NAME
      AND A.RDB$RELATION_NAME = '"
    + Tabellenname + "'
      AND A.RDB$FIELD_NAME = '"
    + Feldname + "'" 

Datensatzaktion erzeugen

Dieses Makro sollte an das Ereignis Bei Fokuserhalt des Listenfeldes gebunden werden. Es ist notwendig, damit auf jeden Fall bei einer Änderung des Listenfeldinhaltes die Speicherung abläuft. Ohne dieses Makro wird keine Änderung in der Tabelle erzeugt, die für Base wahrnehmbar ist, da die Combobox mit dem Formular nicht verbunden ist.

Dieses Makro stellt direkt die Eigenschaft des Formulars um.

  1. 1 SUB Datensatzaktion_erzeugen(oEvent AS OBJECT) 

  2. 2    DIM oForm AS OBJECT 

  3. 3    oForm = oEvent.Source.Model.Parent 

  4. 4    oForm.IsModified = TRUE 

  5. 5 END SUB 

Bei Formularen, die bereits ihren Inhalt auch für die Kombinationsfelder aus Abfragen erhalten, ist dieses Makro nicht notwendig. Änderungen in den Kombinationsfeldern werden direkt registriert.

Navigation von einem Formular zum anderen

Ein Formular soll über ein entsprechendes Ereignis geöffnet werden.

Im Formularkontrollfeld wird unter Formular-Eigenschaften Zusatzinformationen (Tag) der Name des Formulars eintragen. Hier können auch weitere Informationen eingetragen werden, die über den Befehl Split() anschließend voneinander getrennt werden.

  1. 1 SUB Zu_Formular_von_Formular(oEvent AS OBJECT) 

  2. 2    DIM stTag AS String 

  3. 3    stTag = oEvent.Source.Model.Tag 

  4. 4    aForm() = Split(stTag, ",") 

Das Array wird gegründet und mit den Formularnamen gefüllt, in diesem Fall zuerst in dem zu öffnenden Formular und als zweites dem aktuellen Formular, dass nach dem Öffnen des anderen geschlossen werden soll. Existiert ein zweiter Eintrag nicht, so wird durch das Makro nur ein neues Formular geöffnet.

  1. 5    ThisDatabaseDocument.FormDocuments.getByName( Trim(aForm(0)) ).open 

  2. 6    IF Ubound(aForm()) > 0 THEN 

  3. 7       ThisDatabaseDocument.FormDocuments.getByName( Trim(aForm(1)) ).close 

  4. 8    END IF 

  5. 9 END SUB 

Soll grundsätzlich das aktuelle Formular geschlossen werden, so braucht nur in den Zusatzinformationen des Buttons für das folgende Makro das Zielformular angegeben zu werden:

  1. 1 SUB Zu_Formular_von_Formular(oEvent AS OBJECT) 

  2. 2    DIM stZiel AS String 

  3. 3    aFormStart() = Split(thisComponent.Title, thisComponent.UntitledPrefix) 

  4. 4    stZiel = oEvent.Source.Model.Tag 

  5. 5    ThisDatabaseDocument.FormDocuments.getByName( Trim(stZiel) ).open 

  6. 6    ThisDatabaseDocument.FormDocuments.getByName( Trim(aFormStart(1)) ).close 

  7. 7 END SUB 

Soll stattdessen nur beim Schließen ein anderes Formular geöffnet werden, weil z.B. ein Hauptformular existiert und alle anderen Formulare von diesem aus über entsprechende Buttons angesteuert werden, so ist das folgende Makro einfach an das Formular unter Extras → Anpassen → Ereignisse → Dokument wird geschlossen anzubinden:

  1. 1 SUB Hauptformular_oeffnen 

  2. 2    ThisDatabaseDocument.FormDocuments.getByName( "Hauptformular" ).open 

  3. 3 END SUB 

Wenn die Formulardokumente innerhalb der *.odb-Datei in Verzeichnissen sortiert sind, so muss das Makro für den Formularwechsel etwas umfangreicher sein:

  1. 1 SUB Zu_Formular_von_Formular_mit_Ordner(oEvent AS OBJECT) 

  2. 2    REM Das zu öffenende Formular wird als erstes angegeben. 

  3. 3    REM Liegt ein Formular in einem Ordner, so ist die Beziehung über "/" zu  

  4. 4    REM definieren, so dass der Unterordner zu finden ist. 

  5. 5    DIM stTag AS STRING 

  6. 6    stTag = oEvent.Source.Model.Tag 'Tag wird unter den Zusatzinformationen
         eingegeben
     

  7. 7    aForms() = Split(stTag, ",")        'Hier steht zuerst der Formularname für das neue
         Formular, dann der für das alte Formular
     

  8. 8    aForms1() = Split(aForms(0),"/") 

  9. 9    aForms2() = Split(aForms(1),"/") 

  10. 10    IF UBound(aForms1()) = 0 THEN 

  11. 11       ThisDatabaseDocument.FormDocuments.getByName( Trim(aForms1(0)) ).open 

  12. 12    ELSE 

  13. 13       ThisDatabaseDocument.FormDocuments.getByName( Trim(aForms1(0)) ).getByName(
           
    Trim(aForms1(1)) ).open 

  14. 14    END IF 

  15. 15    IF UBound(aForms2()) = 0 THEN 

  16. 16       ThisDatabaseDocument.FormDocuments.getByName( Trim(aForms2(0)) ).close 

  17. 17    ELSE 

  18. 18       ThisDatabaseDocument.FormDocuments.getByName( Trim(aForms2(0)) ).getByName(
           
    Trim(aForms2(1)) ).close 

  19. 19    END IF 

  20. 20 END SUB 

Formulardokumente, die in einem Verzeichnis liegen, werden in den Zusatzinformationen als Verzeichnis/Formular angegeben. Dies muss umgewandelt werden zu ...getByName("Verzeichnis").getByName("Formular").

Datensatz im Formular direkt öffnen

Wird von einem Formular zum anderen gesprungen, so kann das Zielformular natürlich über eine Filtertabelle mit einem bestimmten Schlüssel versehen werden und direkt nur mit einem Datensatz geöffnet werden. Manchmal ist es aber erforderlich, direkt eine Übersicht über mehrere Datensätze zu haben und dennoch den korrekten Datensatz direkt in einem Tabellenkontrollfeld angezeigt zu bekommen. Das im folgenden vorgestellte Makro springt bei entsprechender Vorgabe direkt zu dem gewünschten Datensatz.

  1. 1 SUB DatumsAenderung(oEvent AS OBJECT) 

  2. 2    DIM oDoc AS OBJECT 

  3. 3    DIM oDrawpage AS OBJECT 

  4. 4    DIM oForm AS OBJECT 

  5. 5    DIM oDatField AS OBJECT 

  6. 6    DIM oConnection AS OBJECT 

  7. 7    DIM oSQL_Statement AS OBJECT 

  8. 8    DIM oResult AS OBJECT 

  9. 9    DIM iRow AS INTEGER 

  10. 10    DIM stDatum AS STRING 

  11. 11    DIM stSql AS STRING 

  12. 12    oDatField = oEvent.Source.Model 

  13. 13    stDatum = oDatField.CurrentValue.Year & "-" &
         
    Right("0" & oDatField.CurrentValue.Month , 2) & "-" &
         
    Right("0" & oDatField.CurrentValue.Day , 2)  

  14. 14    oDoc = thisComponent 

  15. 15    oDrawpage = oDoc.drawpage 

  16. 16    oForm = oDrawpage.forms.getByName("Uebersicht") 

  17. 17    oConnection = oForm.activeConnection() 

  18. 18    oSQL_Statement = oConnection.createStatement() 

  19. 19    stSql = oForm.SingleSelectQueryComposer.Query 

  20. 20    oResult = oSQL_Statement.executeQuery(stSql) 

  21. 21    Do  

  22. 22       oResult.next 

  23. 23       iRow = iRow +1 

  24. 24       IF oResult.isLast THEN Exit Do 

  25. 25    LOOP UNTIL stDatum = oResult.getString(2)  

  26. 26    oForm.last 

  27. 27    oForm.absolute(iRow) 

  28. 28 END SUB 

Bei diesem Beispiel wird zuerst aus einem Formularfeld für ein Datum ein bestimmter Wert ausgelesen. Dieser Datumswert wird in die für SQL übliche Schreibweise umgewandelt. Das Formular soll auf den ersten Datensatz eingestellt werden, der mit dem Datumswert übereinstimmt.

Das Formular wird angesteuert. Für die Ermittlung der Position des Datensatzes ist es nun wichtig, genau zu wissen, mit welchem SQL-Kommando denn der Inhalt des Formulars gefüllt wurde. Dieses SQL-Kommando befindet sich nicht eindeutig in oForm.Command. Das ist lediglich das Kommando, das bei der Erstellung des Formulars ausgesucht wird. Im Formular selbst kann aber noch sortiert und gefiltert werden. Entweder müssten also zusätzlich zu dem Command noch die Filterung und Sortierung berücksichtigt werden oder nach einer fertigen Zusammenstellung in den Formulareigenschaften gesucht werden. Die fertige Zusammenstellung unter Berücksichtigung von Filter und Sortierung befindet sich in oForm.SingleSelectQueryComposer.Query. Dieser Composer steht nur dann zur Verfügung, wenn die Abfrage nicht im direkten SQL-Modus ausgeführt wird.

In der dem Formular zugrundeliegenden Abfrage wird jetzt nach dem Datum gesucht. Dazu wird mit DO ... LOOP UNTIL eine Schleife durchlaufen, die dann endet, wenn der in der Abfrage an 2. Position stehende Datumswert genau der Vorgabe entspricht, oder wenn der letzte Datensatz erreicht ist. Für jede neu ausgelesene Zeile wird der Zähler iRow um '1' erhöht.

Zum Schluss wird das Formular auf den letzten Datensatz eingestellt und von dort aus dann zurück auf die ermittelte Datenzeile. Das hat den Vorteil, dass das Datum in diesem Fall im Tabellenkontrollfeld auf jeden Fall oben steht und außerdem noch klar ist, wie viele Datensätze denn im Moment über das Formular verfügbar sind.

Tabellen, Abfragen, Formulare und Berichte öffnen

Ähnlich wie im vorhergehenden Kapitel lassen sich von einem Formular aus auch Berichte öffnen. Berichte sind wie Formulare in die Base-Datei eingebundene separate Dokumente. Statt FormDocuments ist hier lediglich ReportDocuments einzutragen. Außerdem ist noch darauf zu achten, dass sowohl Formulare als auch Berichte in Unterverzeichnissen liegen können. Schwieriger ist es hingegen, auch auf Tabellen, Abfragen und Ansichten zuzugreifen, da diese nicht als separate Dokumente vorliegen.

  1. 1 SUB Navigation(oEvent AS OBJECT) 

  2. 2    DIM stTag AS STRING 

  3. 3    DIM inType AS INTEGER 

  4. 4    stTag = oEvent.Source.Model.Tag 

  5. 5    aOpen() = Split(stTag, ",") 

  6. 6    SELECT CASE Trim(aOpen(0)) 

  7. 7       CASE "form", "report" 

  8. 8          REM Forms and Reports could be saved also in subfolders. 

  9. 9          aForms1() = Split(Trim(aOpen(1)),"/") 

  10. 10          IF Trim(aOpen(0)) = "form" THEN 

  11. 11             oDoc = ThisDatabaseDocument.FormDocuments 

  12. 12          ELSE 

  13. 13             oDoc = ThisDatabaseDocument.ReportDocuments 

  14. 14          END IF 

  15. 15          IF Ubound(aForms1()) > 0 THEN 

  16. 16             oDoc.getByName( Trim(aForms1(0)) ).getByName( Trim(aForms1(1)) ).open 

  17. 17          ELSE 

  18. 18             oDoc.getByName( Trim(aForms1(0)) ).open 

  19. 19          END IF 

  20. 20          IF Trim(aOpen(0)) = "form" AND Ubound(aOpen()) > 1 THEN 

  21. 21             REM The Form, which starts the Macro, could also be closed ... 

  22. 22             aForms2() = Split(Trim(aOpen(2)),"/") 

  23. 23             IF Ubound(aForms2()) > 0 THEN 

  24. 24                ThisDatabaseDocument.FormDocuments.
                     
    getByName( Trim(aForms2(0)) ).getByName( Trim(aForms2(1)) ).close 

  25. 25             ELSE 

  26. 26                ThisDatabaseDocument.FormDocuments.
                     
    getByName( Trim(aForms2(0)) ).close 

  27. 27             END IF 

  28. 28          END IF 

  29. 29          EXIT SUB 

  30. 30       CASE "query" 

  31. 31          inType = 1 

  32. 32          Open_Table_Query_View(Trim(aOpen(1)),inType) 

  33. 33       CASE "table" 

  34. 34          inType = 0 

  35. 35          Open_Table_Query_View(Trim(aOpen(1)),inType) 

  36. 36    END SELECT 

  37. 37 END SUB 

Über die Prozedur Navigation wird das Makro gestartet. Von den Buttons wird aus den Zusatzinformationen (Tag) die Information ausgelesen, ob ein Formular (form), ein Bericht (report) usw. aufgerufen werden soll. Der Name des Formulars, Berichtes usw. wird in den Zusatzinformationen durch ein Komma von dieser Information getrennt.

Enthält der erste Teil des daraus ermittelten Arrays die Bezeichnung form, so wird anschließend das Formular geöffnet. Entsprechendes gilt für die Bezeichnung report, die den SELECT CASE für den Bericht ergibt.

Für Abfragen und Tabellen muss ein anderer Weg beschritten werden. Hier wird sowohl der Name der Abfrage bzw. Tabelle als auch eine Integer-Zahl an die folgende Prozedur Open_Table_Query_View weitergegeben.

  1. 1 SUB Open_Table_Query_View(stName AS STRING, inType AS INTEGER) 

  2. 2    DIM oController AS OBJECT 

  3. 3    DIM oConnection AS OBJECT 

  4. 4    oController = ThisDatabasedocument.CurrentController 

  5. 5    IF NOT oController.isconnected THEN oController.connect 

  6. 6    oConnection = oController.ActiveConnection 

  7. 7    DIM URL AS NEW com.sun.star.util.URL 

  8. 8    DIM Args(5) AS NEW com.sun.star.beans.PropertyValue 

  9. 9    URL.Complete = ".component:DB/DataSourceBrowser" 

  10. 10    Dispatch = StarDesktop.queryDispatch(URL,"_Blank",8) 

  11. 11    Args(0).Name = "ActiveConnection"  

  12. 12    Args(0).Value = oConnection 

  13. 13    Args(1).Name = "CommandType" 

  14. 14    Args(1).Value = inType   '0=Table 1=SQL_Query 2=Command 

  15. 15    Args(2).Name = "Command" 

  16. 16    Args(2).Value = stName 

  17. 17    Args(3).Name = "ShowMenu" 

  18. 18    Args(3).Value = True 

  19. 19    Args(4).Name = "ShowTreeView" 

  20. 20    Args(4).Value = False 

  21. 21    Args(5).Name = "ShowTreeViewButton" 

  22. 22    Args(5).Value = False 

  23. 23    Dispatch.dispatch(URL, Args) 

  24. 24 END SUB 

Zuerst wird die Verbindung zur Datenbank hergestellt, sofern sie noch nicht existiert. Diese Verbindung muss mit einigen zusätzlichen Informationen, unter anderem der Art des zu öffnenden Elementes (Tabelle oder Abfrage) sowie dem Namen des Elementes, in einem Array weiter gegeben werden.

Die Tabelle bzw. Abfrage wird schließlich über den queryDispatch mit dem Kommando dispatch geöffnet.

Hierarchische Listenfelder

Einstellungen in einem Listenfeld sollen die Einstellungen in einem zweiten Listenfeld direkt beeinflussen. Auf einfachere Art und Weise wurde dies schon bei der Filterung von Datensätzen weiter oben beschrieben. Jetzt soll aber hinzu kommen, dass das erste Listenfeld den Inhalt des zweiten Listenfeldes beeinflusst, der wiederum den Inhalt des dritten Listenfeldes beeinflusst usw.13
 
 

In diesem Beispiel enthält Listenfeld 1 alle Jahrgänge der Schule. Die Klassen der jeweiligen Jahrgänge sind durch Buchstaben kenntlich gemacht. Die Namen enthalten die Schülerinnen und Schüler der Klasse.

Unter normalen Umständen zeigt das Listenfeld für den Jahrgang alle 13 Jahrgänge an. Das Listenfeld für die Klasse alle Buchstaben und das Listenfeld für die Schüler und Schülerinnen alle Schüler und Schülerinnen der Schule.

Wird mit hierarchischen Listenfeldern gearbeitet, so wird nach Auswahl des Jahrgangs das Listenfeld für die Klasse eingegrenzt. Es werden nur noch die Klassenbezeichnungen angezeigt, die es in dem Jahrgang tatsächlich gibt. So könnte eben bei steigender Schüler- und Schülerinnenzahl die Anzahl der Klassen im Jahrgang ebenfalls steigen. Das letzte Listenfeld, die Namen, ist jetzt bereits stark eingegrenzt. Statt alle vermutlich deutlich über 1000 Schüler und Schülerinnen anzuzeigen, listet das letzte Feld nur noch die ca. 30 Schüler und Schülerinnen der einen letztlich ausgewählten Klasse auf.

Zum Beginn steht nur die Auswahl des Jahrgangs zur Verfügung. Ist ein Jahrgang ausgewählt, so steht die (bereits eingeschränkte) Auswahl der Klasse zur Verfügung. Erst zum Schluss wird schließlich das Listenfeld für die Namen freigegeben.

Wird das Listenfeld des Jahrgangs geändert, so muss der Durchlauf wieder wie vorher starten. Wird nur das Listenfeld der Klasse geändert, so muss der Wert des Jahrgangs für das letzte Listenfeld der Namen weiter gelten.

Filterung des Formulars mit hierarchischen Listenfeldern

Um solch eine Funktion bereitzustellen, muss innerhalb eines Formulars eine Variable zwischengespeichert werden. Dies erfolgt in einem versteckten Kontrollfeld.

Der Makrostart wird an die Veränderung des Inhaltes eines Listenfeldes gekoppelt: Eigenschaft Listenfeld → Ereignisse → Modifiziert. In den Zusatzinformationen des Listenfeldes werden die notwendige Variablen gespeichert.

Hier der beispielhafte Inhalt der Zusatzinformationen:
Jahrgang,verstecktes Kontrollfeld,Listenfeld 2

Das aktuelle Listenfeld ist als «Listenfeld 1» bezeichnet. Dieses Listenfeld stellt den Inhalt des Tabellenfeldes «Jahrgang» dar. Nach diesem Eintrag muss also das darauffolgende Listenfeld gefiltert werden. Das versteckte Kontrollfeld ist in diesem Fall auch gleich mit dem entsprechenden Namen gekennzeichnet. Und schließlich wird noch darauf hingewiesen, dass ein 2. Listenfeld, «Listenfeld 2», existiert, an das die Filterung weiter gegeben wird.

  1. 1 SUB Hierarchisches_Kontrollfeld(oEvent AS OBJECT) 

  2. 2    DIM oDoc AS OBJECT 

  3. 3    DIM oDrawpage AS OBJECT 

  4. 4    DIM oForm AS OBJECT 

  5. 5    DIM oFeldHidden AS OBJECT 

  6. 6    DIM oFeld AS OBJECT         

  7. 7    DIM oFeld1 AS OBJECT         

  8. 8    DIM stSql AS STRING 

  9. 9    DIM aInhalt() 

  10. 10    DIM stTag AS STRING 

  11. 11    oFeld = oEvent.Source.Model 

  12. 12    stTag = oFeld.Tag 

  13. 13    oForm = oFeld.Parent 

  14. 14    REM Tag wird unter den Zusatzinformationen eingegeben 

  15. 15    REM Hier steht: 

  16. 16    REM 0. Feldname des zu filterndes Feldes in der Tabelle,  

  17. 17    REM 1. Feldname des versteckten Konrollfeldes, das den Filterwert speichern
         soll,
     

  18. 18    REM 2. eventuell weiteres Listfeld 

  19. 19    REM Der Tag wird von dem auslösenden Element ausgelesen. Die Variable wird an
         die Prozedur weitergegeben, die gegebenenfalls alle weiteren Listenfelder
         einstellt.
     

  20. 20    aFilter() = Split(stTag, ",") 

  21. 21    stFilter = "" 

Nachdem die Variablen deklariert wurden, wird der Inhalt des Tags in ein Array übertragen. So kann auf die einzelnen Elemente zugegriffen werden. Anschließend wird der Zugang zu den verschiedenen Feldern im Formular deklariert.

Das Listenfeld wird aus dem Aufruf heraus ermittelt. Aus dem Listenfeld wird der Wert ausgelesen. Nur wenn dieser Wert einen Inhalt hat, wird er mit dem Feldnamen des zu filternden Feldes, in unserem Beispiel «Jahrgang», zu einer SQL-Bedingung kombiniert. Ansonsten bleibt der Filter leer. Sind die Listenfelder zur Filterung eines Formulars gedacht, dann ist kein verstecktes Kontrollfeld vorhanden. Unter dieser Bedingung wird der Filterwert direkt im Formular gespeichert.

  1. 22    IF Trim(aFilter(1)) = "" THEN 

  2. 23       IF oFeld.getCurrentValue <> "" THEN 

  3. 24          stFilter = """"+Trim(aFilter(0))+"""='"+oFeld.getCurrentValue()+"'" 

Existiert bereits vorher ein Filter (weil es sich z.B. um das Listenfeld 2 handelt, das jetzt betätigt wurde), so wird der neue Inhalt an den vorherigen angehängt, der in dem versteckten Feld zwischengespeichert wurde.

  1. 25          IF oForm.Filter <> ""
               
    AND InStr(oForm.Filter, """"+Trim(aFilter(0))+"""='") = 0 THEN 

  2. 26             stFilter = oForm.Filter + " AND " + stFilter 

Dies darf allerdings nur dann geschehen, wenn das gleiche Feld noch nicht gefiltert wurde. Schließlich ist z.B. bei einer Filterung nach dem «Jahrgang» kein Datensatz unter «Name» mehr zu erwarten, wenn zusätzlich eine weitere Filterung nach «Jahrgang» erfolgt. Eine Person kann immer nur in einem «Jahrgang» existieren. Es muss also ausgeschlossen werden, dass in der Filterung der Filtername bereits vorkommt.

Existiert bereits ein Filter und kommt das Feld, nach dem gefiltert werden soll, bereits im Filter vor, so muss die vorherige Filterung ab diesem Feldnamen gelöscht und die neue Filterung eingefügt werden.

  1. 27          ELSEIF oForm.Filter <> "" THEN 

  2. 28             stFilter = Left(oForm.Filter,  

  3. 29             InStr(oForm.Filter, """"+Trim(aFilter(0))+"""='")-1) + stFilter 

  4. 30          END IF 

  5. 31       END IF 

Anschließend wird der Filter in das Formular eingetragen. Dieser Filter kann auch leer sein, wenn direkt das erste Listenfeld ohne Inhalt gewählt wurde.

  1. 32       oForm.Filter = stFilter 

  2. 33       oForm.reload() 

Die gleiche Prozedur wird durchlaufen, wenn nicht ein Formular direkt gefiltert werden soll. In dem Fall wird der Filterwert in einem versteckten Kontrollfeld zwischengespeichert.

  1. 34    ELSE 

  2. 35       oFeldHidden = oForm.getByName(Trim(aFilter(1))) 

  3. 36       IF oFeld.getCurrentValue <>"" THEN 

  4. 37          stFilter = """"+Trim(aFilter(0))+"""='"+oFeld.getCurrentValue()+"'" 

  5. 38          IF oFeldHidden.HiddenValue <> ""
               
    AND InStr(oFeldHidden.HiddenValue, """"+Trim(aFilter(0))+"""='") = 0
               
    THEN 

  6. 39             stFilter = oFeldHidden.HiddenValue + " AND " + stFilter 

  7. 40          ELSEIF oFeldHidden.HiddenValue <> "" THEN 

  8. 41             stFilter = Left(oFeldHidden.HiddenValue,
                 
    InStr(oFeldHidden.HiddenValue, """"+Trim(aFilter(0))+"""='")-1) +
                 
    stFilter 

  9. 42          END IF 

  10. 43       END IF 

  11. 44       oFeldHidden.HiddenValue = stFilter 

  12. 45    END IF 

Ist in den Zusatzinformationen ein 4. Eintrag (Arraynummerierung beginnt bei 0!) vorhanden, so muss das folgende Listenfeld jetzt auf den entsprechenden Eintrag des aufrufenden Listenfeldes eingestellt werden.

  1. 46    IF UBound(aFilter()) > 1 THEN 

  2. 47       oFeld1 = oForm.getByName(Trim(aFilter(2))) 

  3. 48       aFilter1() = Split(oFeld1.Tag,",") 

Die notwendigen Daten für die Filterung werden aus den Zusatzinformationen («Tag») des entsprechenden Listenfeldes ausgelesen. Leider ist es nicht möglich, lediglich den SQL-Code in dem Listenfeld neu zu schreiben und anschließend das Listenfeld einzulesen. Vielmehr müssen die entsprechenden Werte direkt in das Listenfeld geschrieben werden.

Bei der Erstellung des Codes wird davon ausgegangen, dass die Tabelle, auf der das Formular beruht, die gleiche ist, auf der auch die Listenfelder beruhen. Für eine Weitergabe von Fremdschlüsseln an die Tabelle ist so ein Listenfeld also erst einmal nicht gedacht.

  1. 49       IF oFeld.getCurrentValue <> "" THEN 

  2. 50          stSql = "SELECT DISTINCT """+Trim(aFilter1(0))+""" FROM """+oForm.Command+
               
    """ WHERE "+stFilter+" ORDER BY """+Trim(aFilter1(0))+"""" 

  3. 51          oDatenquelle = ThisComponent.Parent.CurrentController 

  4. 52          If NOT (oDatenquelle.isConnected()) THEN 

  5. 53             oDatenquelle.connect() 

  6. 54          END IF 

  7. 55          oVerbindung = oDatenquelle.ActiveConnection() 

  8. 56          oSQL_Anweisung = oVerbindung.createStatement() 

  9. 57          oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql) 

Die Werte werden in ein Array eingelesen. Das Array wird anschließend direkt in das Listenfeld übertragen. Die entsprechenden Zähler für das Array werden durch die Schleife kontinuierlich erhöht.

  1. 58          inZaehler = 0 

  2. 59          WHILE oAbfrageergebnis.next 

  3. 60             ReDim Preserve aInhalt(inZaehler) 

  4. 61             aInhalt(inZaehler) = oAbfrageergebnis.getString(1) 

  5. 62             inZaehler = inZaehler+1 

  6. 63          WEND 

  7. 64       ELSE 

  8. 65          aInhalt(0) = "" 

  9. 66       END IF 

  10. 67       oFeld1.StringItemList = aInhalt() 

Der Inhalte des Listenfeldes wurde neu erstellt. Das Listenfeld muss neu eingelesen werden. Anschließend wird anhand der Zusatzinformationen des neu eingestellten Listenfeldes jedes eventuell weiter folgende Listenfeld entsprechend geleert, indem eine Schleife für alle folgenden Listenfelder gestartet wird, bis eben ein letztes Listenfeld keinen 4. Eintrag in den Zusatzinformationen enthält.

  1. 68       oFeld1.refresh() 

  2. 69       WHILE UBound(aFilter1()) > 1 

  3. 70          DIM aLeer() 

  4. 71          oFeld2 = oForm.getByName(Trim(aFilter1(2))) 

  5. 72          DIM aFilter1() 

  6. 73          aFilter1() = Split(oFeld2.Tag,",") 

  7. 74          oFeld2.StringItemList = aLeer() 

  8. 75          oFeld2.refresh() 

  9. 76       WEND 

  10. 77    END IF 

  11. 78 END SUB 

Die sichtbaren Inhalte des Listenfeldes werden in oFeld1.StringItemList gespeichert. Soll zusätzlich auch ein Wert gespeichert werden, der als Fremdschlüssel an die darunterliegende Tabelle weitergegeben wird, wie bei Listenfeldern in Formularen üblich, so ist dieser Wert in der Abfrage zusätzlich zu ermitteln und anschließend mit oFeld1.ValueItemList abzuspeichern.

Für so eine Erweiterung sind allerdings zusätzliche Variablen notwendig wie z.B. neben der Tabelle, in der die Werte des Formulars gespeichert werden, noch die Tabelle, aus der die Listenfeldinhalte gelesen werden.

Besondere Aufmerksamkeit ist dabei der Formulierung des Filters zu widmen.

  1. 1    stFilter = """"+Trim(aFilter(1))+"""='"+oFeld.getCurrentValue()+"'" 

funktioniert dann nur noch, wenn es sich bei der zugrundeliegenden LO-Version um eine Version ab LO 4.1 handelt, da hier als CurrentValue() der Wert wiedergegeben wird, der auch abgespeichert wird – nicht der Wert, der lediglich angezeigt wird. Damit das einwandfrei über verschiedene Versionen hinweg funktioniert, sollte unter Eigenschaften: Listenfeld → Daten → Gebundenes Feld → '0' angegeben sein.

Hierarchische Listenfelder in der Formulareingabe nutzen

Auch bei der Eingabe von Formularen können solche hierarchischen Listenfelder genutzt werden. Die hier aufgeführten Makros erledigen dabei nur die notwendigen Grundlagen14. Die Felder für das 3. Listenfeld werden z.B. nicht automatisch zurückgestellt, wenn aus dem ersten Listenfeld ein neuer Wert ausgesucht wird.

Das Makro ist an Eigenschaften: Listenfeld → Ereignisse → Vor dem Aktualisieren gebunden. Diese Eigenschaft steht bei Listenfeldern auch innerhalb von Tabellenkontrollfeldern zur Verfügung. So ist das Makro sowohl bei Tabellenkontrollfeldern als auch bei einfachen Formularfeldern universell nutzbar.

  1. 1 SUB Listenfeldfilter(oEvent AS OBJECT) 

  2. 2    DIM stSql(0) AS STRING 

  3. 3    DIM oForm AS OBJECT 

  4. 4    DIM oFeld AS OBJECT 

  5. 5    DIM oFeld2 AS OBJECT 

  6. 6    DIM stFeld AS STRING 

  7. 7    DIM stWert AS STRING 

  8. 8    DIM stFeld2 AS STRING 

  9. 9    DIM stSqlFeld2 AS STRING 

  10. 10    oFeld = oEvent.Source 

  11. 11    stFeld = oFeld.DataField 

  12. 12    stWert = oFeld.CurrentValue 

  13. 13    oForm = oFeld.Parent ' oForm ist bei Tabellenkontrollfelder das Tabellenobjekt 

  14. 14    stFeld2 = oFeld.Tag 

  15. 15    oFeld2 = oForm.getByname(stFeld2) 

  16. 16    stSqlFeld2 = oFeld2.ListSource(0) 

In dem auslösenden Feld steht lediglich in den Zusatzinformationen der Name des Formularfeldes, das neu eingestellt werden soll.

Aus dem auslösenden Feld wird das Datenfeld ausgelesen. Sollte hier in der Datenbank für das Feld eine andere Bezeichnung gewählt worden sein als für die Abfrage im folgenden Listenfeld notwendig, so muss dies ebenfalls in den Zusatzinformationen aufgeführt werden.

Aus dem auslösenden Feld wird auch der momentane Wert ausgelesen. Das ist seit der Version LO 4.1 der Wert, der tatsächlich in der Datenbank abgespeichert wird.

Das Zielfeld wird angesteuert und der Inhalt der dort enthaltenen Abfrage über ListSource(0) ausgelesen. Da es sein kann, dass durch eine vorherige Betätigung des auslösenden Feldes hier bereits eine WHERE-Bedingung steht, muss diese gegebenenfalls in der folgenden Schleife entfernt werden.

  1. 17    IF InStr(stSqlFeld2, "WHERE") THEN 

  2. 18       ar = Split(stSqlFeld2, "WHERE") 

  3. 19       stSqlFeld2 = ar(0) 

  4. 20    END IF 

Zum Schluss wird der Code als der erste Wert des Arrays stSql zusammengestellt. Er wird als ListSource an das Zielfeld übergeben. Das Feld wird mit einem refresh auf den neuen Inhalt eingestellt.

  1. 21    stSql(0) = stSqlFeld2 & " WHERE """+stFeld+""" = '"+stWert+"'" 

  2. 22    oFeld2.ListSource = stSql 

  3. 23    oFeld2.refresh 

  4. 24 END SUB 

Wir so ein Makro für Listenfelder in einem Tabellenkontrollfeld genutzt, so muss Formulareigenschaften → Daten → Daten ändern → 'Nein' ausgewählt sein. Sonst werden die Listenfelder in den vorhergehenden Formularfeldern geändert und können dort gegebenenfalls die alten Daten nicht mehr anzeigen.

Die folgenden beiden Makros nutzen eine Filtertabelle. Das hat den Vorteil, dass in den Listenfeldern beliebiger SQL-Code stehen kann. Die Felder müssen lediglich in dem SQL-Code einen Verweis auf den Tabellenwert stehen haben. Das obere Beispiel funktioniert so wie aufgeschrieben hingegen nur, wenn der SQL-Code direkt nach der Tabellenbenennung endet und keine WHERE-Bedingung und keine Sortierung enthält.

  1. 1 SUB Listfeldfilter_Tabelle(oEvent AS OBJECT) 

  2. 2    DIM oDatasource AS OBJECT 

  3. 3    DIM oConnection AS OBJECT 

  4. 4    DIM oSQL_Statement AS OBJECT 

  5. 5    DIM oForm AS OBJECT 

  6. 6    DIM oFeld AS OBJECT 

  7. 7    DIM oFeld2 AS OBJECT 

  8. 8    DIM stFeld AS STRING 

  9. 9    DIM stWert AS STRING 

  10. 10    DIM stSql AS STRING         

  11. 11    oFeld = oEvent.Source 

  12. 12    stFeld = oFeld.DataField 

  13. 13    stWert = oFeld.CurrentValue 

  14. 14    oForm = oFeld.Parent ' oForm ist bei Tabellenkontrollfelder das Tabellenobjekt 

  15. 15    stFeld2 = oFeld.Tag 

  16. 16    oFeld2 = oForm.getByname(stFeld2) 

  17. 17    oDatasource = thisDatabaseDocument.CurrentController 

  18. 18    IF NOT (oDatasource.isConnected()) THEN oDatasource.connect() 

  19. 19    oConnection = oDatasource.ActiveConnection() 

  20. 20    oSQL_Statement = oConnection.createStatement() 

  21. 21    stSql ="UPDATE ""Filter"" SET """+stFeld+""" = '"+stWert+"' WHERE ""ID"" = TRUE" 

  22. 22    oSQL_Statement.executeUpdate(stSql) 

  23. 23    oFeld2.refresh 

  24. 24 END SUB 

Die Prozedur «Listfeldfilter_Tabelle» startet wie die vorhergehende Prozedur. Der SQL-Code des Zielfeldes spielt hier gar keine Rolle. Er muss lediglich so gestaltet sein, dass er durch Werte in der Tabelle "Filter" beeinflusst wird. Hier der Beispielcode für das Listenfeld, das die Klasse ausgeben soll:

  1. 1 SELECT "Klasse", "ID" FROM "Klasse"  

  2. 2 WHERE "J_ID" =
      COALESCE ( ( SELECT "J_ID" FROM "Filter" WHERE "ID" = TRUE ), "J_ID" )  

  3. 3 ORDER BY "Klasse" ASC 

In dem Makro wird jetzt schlicht z.B. der Inhalt des Feldes "J_ID" aus der Tabelle "Filter" neu beschrieben und das Ziellistenfeld neu eingelesen.

Der Reset des Filters ist notwendig, damit bei der nächsten Eingabe wieder alle Werte in den Listenfeldern vorhanden sind. Er wird deshalb an die Formulareigenschaften → Ereignisse → Vor dem Datensatzwechsel gebunden.

  1. 1 SUB Filter_Reset(oEvent AS OBJECT) 

  2. 2    DIM oConnection AS OBJECT 

  3. 3    DIM oSQL_Statement AS OBJECT 

  4. 4    DIM oForm AS OBJECT 

  5. 5    DIM stSql AS STRING 

  6. 6    oForm = oEvent.Source 

  7. 7    IF inStr(oForm.ImplementationName,"ODatabaseForm") THEN 

  8. 8       oConnection = oForm.activeConnection() 

  9. 9       oSQL_Statement = oConnection.createStatement() 

  10. 10       stSql = "UPDATE ""Filter"" SET ""J_ID"" = NULL, ""K_ID"" = NULL
            WHERE ""ID"" = TRUE"
     

  11. 11       oSQL_Statement.executeUpdate(stSql) 

  12. 12    END IF 

  13. 13 END SUB 

Zeiteingaben mit Millisekunden

Um Zeiten im Millisekunden-Bereich zu speichern, ist in der Tabelle ein Timestamp-Feld erforderlich, das zudem per SQL separat darauf eingestellt wird (siehe «Zeitfelder in Tabellen») (Hsqldb, Firebird erlaubt auch Millisekunden für normale Zeiten). Ein solches Feld kann vom Formular aus mit einem formatierten Feld beschrieben werden, das auch das Format MM:SS,00 anbietet. Allerdings scheitert der erste Schreibversuch daran, dass der Eingabe der entsprechende Datumszusatz fehlt. Dies kann mit dem folgenden Makro erreicht werden, das an die Formulareigenschaften → Ereignisse → Vor der Datensatzaktion gebunden wird:

  1. 1 SUB Timestamp 

  2. 2    DIM unoStmp AS NEW com.sun.star.util.DateTime 

  3. 3    DIM oDoc AS OBJECT 

  4. 4    DIM oDrawpage AS OBJECT 

  5. 5    DIM oForm AS OBJECT 

  6. 6    DIM oFeld AS OBJECT 

  7. 7    DIM stZeit AS STRING 

  8. 8    DIM ar() 

  9. 9    DIM arMandS() 

  10. 10    DIM loNano AS LONG 

  11. 11    DIM inSecond AS INTEGER 

  12. 12    DIM inMinute AS INTEGER 

  13. 13    oDoc = thisComponent 

  14. 14    oDrawpage = oDoc.Drawpage 

  15. 15    oForm = oDrawpage.Forms.getByName("MainForm") 

  16. 16    oFeld = oForm.getByName("Zeit") 

  17. 17    stZeit = oFeld.Text 

Die Variablen werden vorher deklariert. Nur wenn das Feld «Zeit» einen Inhalt hat, wird der weitere Code ausgeführt. Sonst tritt der Mechanismus des Formulars in Kraft, der das Feld auf NULL setzt.

  1. 18    IF stZeit <> "" THEN 

  2. 19       ar() = Split(stZeit,",") 

  3. 20       loNano = CLng(ar(1)&"0000000") 

  4. 21       arMandS() = Split(ar(0),":") 

  5. 22       inSecond = Cint(arMandS(1)) 

  6. 23       inMinute = Cint(arMandS(0)) 

Die Einträge aus dem Feld «Zeit» werden in ihre Bestandteile zerlegt.

Zuerst werden die Hundertstelsekunden abgetrennt und mit so vielen Nullen rechts aufgefüllt, dass sich insgesamt eine neunstellige Zahl ergibt. Eine so hohe Zahl kann nur in einer Long-Variablen gespeichert werden.

Anschließend werden aus dem verbleibenden Rest durch eine Trennung am Trennzeichen «:» die Minuten von den Sekunden getrennt und in Integer-Zahlen umgewandelt.

  1. 24       WITH unoStmp 

  2. 25          .NanoSeconds = loNano 

  3. 26          .Seconds = inSecond 

  4. 27          .Minutes = inMinute 

  5. 28          .Hours = 0 

  6. 29          .Day = 30 

  7. 30          .Month = 12 

  8. 31          .Year = 1899 

  9. 32       END WITH 

Dem Zeitstempel wird nun das Standarddatum 30.12.1899 zugewiesen, das dem Standard-Startdatum von LibreOffice entspricht. Hier kann natürlich auch das aktuelle Datum mitgespeichert werden.

Aktuelles Datum ermitteln und speichern:
  1. 1 DIM Jetzt AS DATE 

  2. 2 Jetzt = Now() 

  3. 3 WITH unoStmp 

  4. 4    .NanoSeconds = loNano 

  5. 5    .Seconds = inSecond 

  6. 6    .Minutes = inMinute 

  7. 7    .Hours = Hour(Jetzt) 

  8. 8    .Day = Day(Jetzt) 

  9. 9    .Month = Month(Jetzt) 

  10. 10    .Year = Year(Jetzt) 

  11. 11 END WITH 

 
  1. 33       oFeld.BoundField.updateTimestamp(unoStmp) 

  2. 34    END IF 

  3. 35 END SUB 

Anschließend wird der erzeugte Zeitstempel über updateTimestamp in das Feld übertragen und mit dem Formular abgespeichert.

In älteren Anleitungen wird hier statt NanoSeconds der Begriff HundrethSeconds verwendet. Dieser entspricht aber nicht der API von LibreOffice und erzeugt deshalb nur Fehlermeldungen.

Ein Ereignis – mehrere Implementationen

Bei Formularen kommt es vor, dass ein Makro, mit einem Ereignis verknüpft, gleich zweimal ausgeführt wird. Dies liegt daran, dass mehrere Prozesse gleichzeitig z.B. mit dem Abspeichern eines geänderten Datensatzes verbunden sind. Die unterschiedlichen Ursachen für so ein Ereignis lassen sich folgendermaßen ermitteln:

  1. 1 SUB Ereignisursache_ermitteln(oEvent AS OBJECT) 

  2. 2    DIM oForm AS OBJECT 

  3. 3    oForm = oEvent.Source 

  4. 4    MsgBox oForm.ImplementationName 

  5. 5 END SUB 

Beim Abspeichern eines geänderten Datensatzes ergeben sich so zwei Implementationsnamen:  org.openoffice.comp.svx.FormController und com.sun.star.comp.forms.ODatabaseForm. Über diese Namen kann jetzt gesteuert werden, dass ein Makro letztlich nur einmal den ganzen Code durchläuft. Die doppelte Durchführung ist oft nur eine (kleine) Bremse im Programmablauf. Sie kann aber auch dazu führen, dass sich z.B. ein Cursor nicht nur einen, sondern gleich zwei Datensätze zurück bewegt. Die Implementationen lassen auch nur bestimmte Befehle zu, so dass eine Kenntnis des Namens der Implementation von Bedeutung sein kann.

Sicherer soll hier die Abfrage sein, ob eine der Implementationen ein bestimmtes UnoInterface benutzt. Das ist fest in der API verankert und wird damit wohl nicht geändert:

  1. 1 IF hasUnoInterfaces(oForm, "com.sun.star.form.XForm" ) THEN 

weist darauf hin, dass es sich um die Implementation com.sun.star.comp.forms.ODatabaseForm handelt. Auch die Ermittlung des ServiceName kann eine klare Abgrenzung bewirken. Die SupportedServiceNames sind in einem Array in oForm enthalten. Über

  1. 1 IF oForm.supportsService("com.sun.star.form.component.DataForm") THEN 

kann hier ODatabaseForm ermittelt werden.

Eingabekontrolle bei Formularen

Ein Formular sollte für die Eingabe so weit wie möglich abgesichert sein, bevor die Daten in die Datenbank geschrieben werden. Dies erfolgt natürlich schon allein dadurch, dass Felder des Formulars passend zu den Inhalten aus der Datenbank gewählt werden. Auch lassen sich Felder so einstellen, dass sie eine zwingende Eingabe benötigen. Diese zwingende Eingabe muss zur Zeit allerdings auch in der Tabelle der Datenbank definiert sein. Die diesem Abschnitt zugrundeliegende Datenbank15 zeigt fehlende Eingabe direkt an und vermeidet in einigen Feldern auch eventuell fehlerhafte Eingaben.
 

Mehrere Elemente des Formulars fallen sofort auf:

Erforderliche Eingaben absichern

Zu beginn werden einige globale Variablen festgelegt. Die Standardfarbe für den Rahmen und den Hintergrund eines Feldes muss verfügbar sein, ebenso die Farbe, in der der Rahmen erscheinen soll, wenn eine Eingabe notwendig ist. Alle Formularfelder, bei denen zum Start des Formulars Daten → Eingabe erforderlich → Ja eingestellt ist, werden in einem zentralen Array gespeichert. Ohne diese Speicherung wäre es nicht möglich, die erforderliche Eingabe z.B. für die Adresse ein- und wieder auszuschalten.

  1. 1 GLOBAL loBorderDefault AS LONG 

  2. 2 GLOBAL loBorderInputRequired AS LONG 

  3. 3 GLOBAL loColorStandard AS LONG 

  4. 4 GLOBAL arFormInputRequired() 

Die globalen Variablen werden beim Öffnen des Formulardokumentes mit Inhalt versehen. Dies regelt die Prozedur «FormVars».

Die Farbvariablen werden direkt festgelegt. Anschließend wird das gesamte Formular durchgegangen und alle Felder einzeln untersucht. Nur die Felder, die zu dem DataAwareControlModel gehören, können auch Daten aufnehmen. Andere Felder wie Beschriftungsfelder, Buttons oder versteckte Felder können nicht für eine Eingabe genutzt werden.

Jetzt kann noch vorkommen, dass bei einem Feld zwar die Eingabe notwendig ist, leider aber keine Umrandungsfarbe einstellbar ist. Deswegen werden schließlich in das Array für die als notwendig zu versehenden Eingaben nur die übernommen, die auch die Eigenschaft BorderColor unterstützen.

  1. 1 SUB FormVars(oEvent AS OBJECT) 

  2. 2    DIM oForm AS OBJECT, oField AS OBJECT 

  3. 3    DIM k AS INTEGER, i AS INTEGER         

  4. 4    loBorderDefault = RGB(192,192,192) 'Grau 

  5. 5    loBorderInputRequired = RGB(255,0,0) 'Rot 

  6. 6    loColorStandard = RGB(250,250,250) 'sehr helles Grau 

  7. 7    oForm = oEvent.Source 

  8. 8    FOR i = 0 TO oForm.Count - 1 

  9. 9       oField = oForm.getByIndex(i) 

  10. 10       IF oField.supportsService("com.sun.star.form.DataAwareControlModel") THEN 

  11. 11          IF oField.InputRequired THEN 

  12. 12             IF oField.getPropertySetInfo.hasPropertyByName("BorderColor") THEN 

  13. 13                REDIM PRESERVE arFormInputRequired(k) 

  14. 14                arFormInputRequired(k) = oField.Name 

  15. 15                k = k + 1 

  16. 16             END IF 

  17. 17          END IF 

  18. 18       END IF 

  19. 19    NEXT 

  20. 20    FormChange(oEvent) 

  21. 21 END SUB 

In der vorhergehenden Prozedur wird bereits die Prozedur «FormChange» aufgerufen. Mit dieser Prozedur wird die Kennzeichnung der notwendigen Eingaben vorgenommen.

  1. 1 SUB FormChange(oEvent AS OBJECT) 

  2. 2    DIM oForm AS OBJECT, oField AS OBJECT 

  3. 3    DIM i AS INTEGER, n AS INTEGER, k AS INTEGER 

  4. 4    DIM stTest AS STRING 

  5. 5    DIM a(), aa(), ab() 

  6. 6    oForm = oEvent.Source 

In der ersten Schleife durch das Array der Formularfelder, bei denen eine Eingabe nötig ist, wird überprüft, ob das Feld einen Wert enthält. Hier muss zwischen Feldern unterschieden werden, die eine Verbindung zur Datenbank haben und solchen, die ohne Verbindung zur Datenbank existieren (Kombinationsfeld, für das der Fremdschlüssel über Makro ermittelt wird). Die einfache Abfrage nach CurrentValue führt bei Bildfeldern zu einem Fehler, weil dort diese Eigenschaft nicht existiert. Ist dies nicht der Fall, dann wird rot umrandet. Ist dies der Fall, dann wird die Standardumrandung gewählt.

  1. 7    FOR i = LBound(arFormInputRequired()) TO UBound(arFormInputRequired()) 

  2. 8       oField = oForm.getByName(arFormInputRequired(i)) 

  3. 9       oField.InputRequired = True 

  4. 10       IF NOT IsNULL(oField.BoundField) THEN 

  5. 11          IF oField.BoundField.String = "" THEN 

  6. 12             oField.BorderColor = loBorderInputRequired 

  7. 13          ELSE 

  8. 14             oField.BorderColor = loBorderDefault 

  9. 15          END IF 

  10. 16       ELSEIF oField.CurrentValue = "" THEN 

  11. 17          oField.BorderColor = loBorderInputRequired 

  12. 18       ELSE 

  13. 19          oField.BorderColor = loBorderDefault 

  14. 20       END IF 

  15. 21    NEXT 

Die darauffolgende zweite Schleife ist nur deswegen notwendig, weil das Formular ein Feld enthält, das die Eingabe für die Adresse ausschließt. In Abhängigkeit von diesem Feld muss also noch einmal überprüft werden, welche Felder denn jetzt eine Eingabe erfordern und mit einer roten Umrandung gezeigt werden müssen.

  1. 22    FOR i = LBound(arFormInputRequired()) TO UBound(arFormInputRequired()) 

  2. 23       oField = oForm.getByName(arFormInputRequired(i)) 

  3. 24       IF NOT IsNULL(oField.BoundField) THEN 

  4. 25          stTest = oField.BoundField.String 

  5. 26       ELSE 

  6. 27          stTest = oField.CurrentValue 

  7. 28       END IF 

  8. 29       IF stTest <> "" AND oField.Tag <> "" THEN 

In den Feldern, für die eine Eingabe notwendig ist, wird vermerkt von welchem Feld diese Eingabe abhängt. In der Beispieldatenbank steht in den Zusatzinformationen von «Nachname» notrequired[txtFirma]. Das soll bedeuten: Ist ein Nachname eingetragen, so ist bei der Firma kein Eintrag mehr notwendig. Entsprechend steht in den Zusatzinformationen von «Firma» notrequired[txtNachname]. Auch für andere Bereiche wurde in der Beispieldatenbank nach einem Kennwort eine Liste der entsprechenden Felder in eckigen Klammern gewählt. In den Zusatzinformationen können so mehrere Kennworte mit entsprechenden Listen untergebracht werden. Die abschließende eckige Klammer ist der Trenner, nach dem jetzt zuerst einmal gesucht wird:

  1. 30          a = split(oField.Tag,"]") 

  2. 31          FOR n = LBound(a()) TO Ubound(a())-1 

Da die abschließende Klammer auch am Ende aller Eintragungen steht ist das letzte Arrayelement auf jeden Fall leer. Die Schleife muss also nur bis zum vorletzten Arrayelement laufen.

Enthält das Arrayelement den Begriff «notrequired», so wird hier jetzt weiter nach den enthaltenen Felder gesucht. Zuerst wird mit Hilfe der geöffneten eckigen Klammer das Kennwort von den Feldbezeichnungen getrennt, dann werden die Feldbezeichnungen getrennt, sofern überhaupt innerhalb der eckigen Klammern mehrerer Bezeichnungen, getrennt durch ein Komma, existieren.

Die Felder, bei denen jetzt kein Eintrag mehr notwendig sind, werden mit einem normalen Standardrahmen versehen. Die erforderliche Eingabe wird auf False gestellt.

  1. 32             IF InStr(a(n),"notrequired") THEN 

  2. 33                aa = split(a(n),"[") 

  3. 34                ab = split(aa(1),",") 

  4. 35                FOR k = LBound(ab()) TO UBound(ab()) 

  5. 36                   oField = oForm.getByName(ab(k)) 

  6. 37                   oField.BorderColor = loBorderDefault 

  7. 38                   oField.InputRequired = False 

  8. 39                NEXT 

  9. 40             END IF 

  10. 41          NEXT 

  11. 42       END IF 

  12. 43    NEXT 

  13. 44 END SUB 

Die folgende Prozedur «NotRequired entspricht in Teilen der vorhergehenden Prozedur. Sie wird allerdings beim Verlassen eines Formularfeldes, nicht beim Wechsel eine Formulars aufgerufen. Hier wird nach dem Verlassen ein anderes Feld auf Eingabe erforderlich → Nein gesetzt, wenn das Ausgangsfeld einen Inhalt enthält. Enthält es keinen Inhalt, so wird bei Eingabe erforderlich → Ja gesetzt. Entsprechend werden auch die Rahmenfarben angepasst.

  1. 1 SUB NotRequired(oEvent AS OBJECT) 

  2. 2    DIM oFieldStart AS OBJECT, oForm AS OBJECT 

  3. 3    DIM n AS INTEGER, k AS INTEGER 

  4. 4    DIM a(), aa(), ab() 

  5. 5    oFieldStart = oEvent.Source.Model 

  6. 6    oForm = oFieldStart.Parent 

  7. 7    a = split(oFieldStart.Tag,"]") 

  8. 8    FOR n = LBound(a()) TO UBound(a())-1 

  9. 9       IF InStr(a(n),"notrequired") THEN 

  10. 10          aa = split(a(n),"[") 

  11. 11          ab = split(aa(1),",") 

  12. 12          FOR k = LBound(ab()) TO UBound(ab()) 

  13. 13             oField = oForm.getByName(ab(k)) 

  14. 14             IF oFieldStart.CurrentValue <> "" THEN 

  15. 15                oField.BorderColor = loBorderDefault 

  16. 16                oField.InputRequired = False 

  17. 17             ELSE 

  18. 18                oField.BorderColor = loBorderInputRequired 

  19. 19                oField.InputRequired = True 

  20. 20             END IF 

  21. 21          NEXT 

  22. 22       END IF 

  23. 23    NEXT 

  24. 24 END SUB 

Mit der Prozedur «EnableDisable» werden Felder abhängig von einem anderen Feld so eingeschaltet, dass gegebenenfalls keine Eingabe mehr notwendig ist. So steht in den Zusatzinformationen zu dem Markierfeld «Keine Adresse» inaktiv[txtStraße,comPLZOrt]. Es sollen also die Felder für die «Straße» und für «PLZ – Ort» inaktiv gesetzt werden, wenn das Markierfeld ausgewählt wurde (State = True)

Der Zugriff ist hier gleich dem der vorhergehenden Prozeduren. Wenn die Eingabe nicht mehr möglich sein soll, dann werden sowohl Rahmen als auch Hintergrundfarbe des Feldes auf die Standardrahmenfarbe eingestellt.

  1. 1 SUB EnableDisable(oEvent AS OBJECT) 

  2. 2    DIM oForm AS OBJECT, oField AS OBJECT 

  3. 3    DIM stTag AS STRING 

  4. 4    DIM i AS INTEGER, k AS INTEGER 

  5. 5    DIM a(), aa(), ab() 

  6. 6    oForm = oEvent.Source.Model.Parent 

  7. 7    stTag = oEvent.Source.Model.Tag 

  8. 8    a = split(stTag,"]") 

  9. 9    FOR i = LBound(a()) TO UBound(a())-1 

  10. 10       IF InStr(a(i),"inaktiv") THEN 

  11. 11          aa = split(a(i),"[") 

  12. 12          ab = split(aa(1),",") 

  13. 13          FOR k = LBound(ab()) TO UBound(ab()) 

  14. 14             oField = oForm.getByName(ab(k)) 

  15. 15             IF oEvent.Source.Model.State THEN 

  16. 16                oField.Enabled = False 

  17. 17                oField.BorderColor = loBorderDefault 

  18. 18                oField.BackgroundColor = loBorderDefault 

  19. 19             ELSE 

  20. 20                oField.Enabled = True 

  21. 21                oField.BorderColor = loBorderInputRequired 

  22. 22                oField.BackgroundColor = loColorStandard 

  23. 23             END IF 

  24. 24          NEXT 

  25. 25       END IF 

  26. 26    NEXT 

  27. 27 END SUB 

Die Prozedur «FieldRequired» wird an die Felder gebunden, bei denen die Eingabe zu Beginn auf erforderlich gesetzt wurde. Enthält das Feld keinen Wert, so wird beim Fokusverlust der Rahmen rot dargestellt. Umgekehrt wird der Rahmen auf die Normalfarbe gesetzt, wenn das Feld Inhalt enthält. Ist außerdem in den Zusatzinformationen des Feldes noch das Stichwort 'notrequired' enthalten, dann wird die Prozedur «NotRequired» anschließend gestartet.

  1. 1 SUB FieldRequired(oEvent AS OBJECT) 

  2. 2    DIM oField AS OBJECT 

  3. 3    oField = oEvent.Source.Model 

  4. 4    IF oField.CurrentValue <> "" THEN 

  5. 5       oField.BorderColor = loBorderDefault 

  6. 6    ELSE 

  7. 7       oField.BorderColor = loBorderInputRequired 

  8. 8    END IF 

  9. 9    IF inStr(oField.Tag,"notrequired") THEN 

  10. 10       NotRequired(oEvent) 

  11. 11    END IF 

  12. 12 END SUB 

Fehlerhafte Eingaben vermeiden

In der Beispieldatenbank ist für mehrere Felder ein Prozedur eingebaut, die eine fehlerhafte Eingabe so weit wie möglich verhindern soll. Die folgende Prozedur erledigt dies für die Eingabe der IBAN. Sie ist an ein maskiertes Feld gebunden und wird beim Verlassen des Feldes aufgerufen.

  1. 1 SUB IBANValid(oEvent AS OBJECT) 

  2. 2    DIM oField AS OBJECT, oForm AS OBJECT, oController AS OBJECT, oView AS OBJECT 

  3. 3    DIM stMsg AS STRING, stText AS STRING, stLand AS STRING, stPruef AS STRING 

  4. 4    DIM i AS INTEGER 

  5. 5    DIM a() 

  6. 6    oField = oEvent.Source.Model 

  7. 7    stText = oField.Text 

Nur wenn das Feld Text enthält soll die Prozedur auch ablaufen. Das bedeutet, wenn nur einmal der Cursor in dem maskierten Feld gelandet ist und keine Eingabe gemacht wurde ist auch nichts zu überprüfen. Bei der ersten Eingabe, die auch ruhig wieder gelöscht werden kann, würde allerdings die Eingabemaske als Text angesehen. Der Text würde, sofern ein Eintrag fehlt, jetzt mindestens einen Unterstrich '_' enthalten. Außerdem würde der Beginn des Textes 'DE' lauten.

  1. 8    IF stText <> "" THEN 

  2. 9       IF inStr(stText,"_") THEN 

  3. 10          IF Val(Mid(stText,3)) > 0 THEN 

  4. 11             stMsg = "Die IBAN ist zu kurz." 

Aus dem Text wird ab dem 3. Zeichen versucht, den Wert einer Dezimalzahl auszulesen. Schließlich wird die IBAN ab dem 3. Zeichen nur aus Zahlen zusammengesetzt. Leerzeichen ignoriert die Funktion Val(). Ist der Wert größer als 0 und enthält der Text gleichzeitig Unterstriche, so ist die IBAN-Angabe zu kurz. Ist der Wert 0, so soll keine Eingabe erfolgt sein. Das Feld wird mit dem Kommando reset zurückgesetzt und erscheint als leerer Text.

  1. 12          ELSE 

  2. 13             oField.reset 

  3. 14          END IF 

Enthält das maskierte Feld an jeder Stelle Zeichen, so ist prinzipiell das Format korrekt. In Vierergruppen sind die Zahlen gebündelt eingegeben und vollständig. Die erste Vierergruppe enthält die Landesbezeichnung und die zweistellige Prüfziffer. Die Gruppen werden hier als ein Array aufgetrennt. Trenner ist standardmäßig das Leerzeichen.

  1. 15       ELSE 

  2. 16          a = split(stText) 

  3. 17          stLand = "1314" & Right(a(0),2) 

Der Landescode wird in Zahlen umgesetzt. 'A': '10', 'B': '11', 'C': '12', 'D': '13', 'E': '14' usw., so dass aus 'DE' die Kombination '1314' wird. Dieser Kombination wird noch die Prüfziffer hinzugefügt. Die Berechnung der Korrektheit der Prüfziffer erfolgt nach dem Prinzip

  1. 1.alle Zahlen ab der 3. Zahl zusammen mit der Länderzahl und der Prüfziffer ergeben die Gesamtzahl 

  2. 2.Die Gesamtzahl wird durch 97 geteilt 

  3. 3.Aus der Ganzzahldivision muss ein Rest von 1 hervorgehen. 

Leider ist dieses Verfahren nicht so einfach möglich, weil die Gesamtzahl zu groß ist. Der Variablentyp LONG kann maximal 2147483648 annehmen, aber nicht 24 Stellen. Das Rechenverfahren wird hier wie eine schriftliche Division in der Schule umgesetzt: Rest berechnen, weitere Werte hinzuholen, Rest berechnen usw. Nur bei der ersten Berechnung können hier 8 Zahlen aus dem Array übernommen werden. Ist dort der Rest über 21, so würde bereits die zweite Teilberechnung fehl schlagen. Deshalb wird bei den folgenden Teilberechnung jeweils nur mit einer Zugabe von einem Arrayelement, das eben 4 Zahlen enthält, weiter gerechnet.

  1. 18          i = cLng(a(1) & a(2)) Mod 97 

  2. 19          i = cLng(i & a(3)) Mod 97 

  3. 20          i = cLng(i & a(4)) Mod 97 

  4. 21          i = cLng(i & a(5)) Mod 97 

  5. 22          i = cLng(i & stLand) Mod 97 

  6. 23          IF i <> 1 THEN 

Ist die Prüfung fehl geschlagen, weil der Rest nicht gleich '1' ist, so wird hier als kleiner Zusatz noch die eventuell mögliche Prüfziffer berechnet. Dies ist bei einer angenommenen Prüfziffer von '00' der Rest zu '98'. Eine Gewähr für eine korrekte IBAN bietet dies aber nicht, da ja der Fehler auch an anderer Stelle innerhalb der IBAN liegen kann.

Ist die Prüfung fehl geschlagen, so muss eine Fehlermeldung auf dem Bildschirm präsentiert werden.

  1. 24             stPruef = "131400" 

  2. 25             i = cLng(a(1) & a(2)) Mod 97 

  3. 26             i = cLng(i & a(3)) Mod 97 

  4. 27             i = cLng(i & a(4)) Mod 97 

  5. 28             i = cLng(i & a(5)) Mod 97 

  6. 29             i = cLng(i & stPruef) Mod 97 

  7. 30             i = 98 - i 

  8. 31             stMsg "Die IBAN ist fehlerhaft." & CHR(13) & "Die Prüfziffer müsste "  

  9. 32                & i & " sein." 

  10. 33          END IF 

  11. 34       END IF 

Erfolgte eine Fehlermeldung, so darf die Prüfung hiermit aber nicht abgeschlossen sein. Der Cursor muss so lange ist das Eingabefeld zurückgesetzt werden bis die Prüfung das Ergebnis annimmt. Jede Prüfroutine sucht also bei einem Fehler anschließend den Controller des Dokumentes auf und setzt den Cursor in das Feld zurück.

  1. 35       IF stMsg <> "" THEN 

  2. 36          msgbox (stMsg, 0, "Eingabe fehlerhaft") 

  3. 37          oForm = oField.Parent 

  4. 38          oController = thisComponent.getCurrentController() 

  5. 39          oView = oController.getControl(oForm.getByname(oField.Name)) 

  6. 40          oView.setFocus 

  7. 41       END IF 

  8. 42    END IF 

  9. 43 END SUB 

Abspeichern nach erfolgter Kontrolle

Die Felder, bei denen eine Eingabe notwendig ist, verhindern nicht, dass eine Person dennoch eine Abspeicherung der Daten vornehmen will. Mit der folgenden Funktion «SaveRequired» wird jetzt noch einmal überprüft, ob in allen Feldern, für die eine Eingabe notwendig ist, auch eine Eingabe steht. Ansonsten wird die Speicherung unterbrochen und eine Fehlermeldung ausgegeben.

  1. 1 FUNCTION SaveRequired(oEvent AS OBJECT) AS BOOLEAN 

  2. 2    DIM oForm AS OBJECT, oField AS OBJECT 

  3. 3    DIM stLabel AS STRING 

  4. 4    DIM k AS INTEGER, i AS INTEGER 

  5. 5    SaveRequired = True 

  6. 6    oForm = oEvent.Source 

  7. 7    IF oForm.ImplementationName = "org.openoffice.comp.svx.FormController" THEN 

Beim Abspeichern wird zuerst der «FormController» aktiviert. Anschließend auch noch die Implementation für «ODatabaseForm». Das Auslesen der Felder ist hier unterschiedlich, so dass direkt der «FormController» zur Auswertung genutzt wird. Für «oDatabaseForm» muss die Funktion grundsätzlich True wiedergeben. In dem «FormController» sind die einzelnen Felder nur über das Model erreichbar.

  1. 8       FOR i = 0 TO oForm.Model.Count - 1 

  2. 9          oField = oForm.Model.getByIndex(i) 

  3. 10          IF oField.supportsService("com.sun.star.form.DataAwareControlModel") THEN 

  4. 11             IF oField.InputRequired AND NOT IsNULL(oField.BoundField) THEN 

  5. 12                IF oField.BoundField.String = "" THEN 

  6. 13                   stLabel = stLabel & ", " & oField.LabelControl.Label 

  7. 14                   k = k + 1 

  8. 15                END IF 

  9. 16             ELSEIF oField.InputRequired THEN 

  10. 17                IF oField.CurrentValue = "" THEN 

  11. 18                   stLabel = stLabel & ", " & oField.LabelControl.Label 

  12. 19                   k = k + 1 

  13. 20                END IF 

  14. 21             ELSE 

  15. 22             END IF 

  16. 23          END IF 

  17. 24       NEXT 

Die Schleife erfolgt hier in zwei Schritten. In dem Formular befindet sich ein Bildfeld, das die Eigenschaft CurrentValue nicht bedienen kann. Es befindet sich aber auch ein Kombinationsfeld darin, das gar nicht an die zugrundeliegende Tabelle gekoppelt ist und damit kein gebundenes Feld der Tabelle ansprechen kann.

Ist der Zähler größer als 1, so muss eine Fehlermeldung erfolgen. Hier wurde bereits über die den Feldern zugewiesenen Beschriftungsfelder (Label) entsprechend die lesbare Bezeichnung der leeren Felder herausgesucht. «stLabel» endet allerdings mit einem Komma, gefolgt von einer Leertaste. Dies ist für den Abschluss des Strings überflüssig und wird abgetrennt.

  1. 25       IF k > 0 THEN 

  2. 26          stLabel = Right(stLabel, Len(stLabel)-2) 

  3. 27          IF k = 1 Then 

  4. 28             stText = "Für das Feld " 

  5. 29          ELSE 

  6. 30             stText = "Für die Felder " 

  7. 31          END IF 

  8. 32          MsgBox ("Alle rot umrandeten Felder müssen einen Inhalt aufweisen." &
               
    CHR(13) & stText & stLabel & " ist eine Eingabe erforderlich." ,
               
    0 ,"Speichern gestoppt") 

  9. 33          SaveRequired = False 

  10. 34       END IF 

  11. 35    END IF 

  12. 36 END FUNCTION 

Bei einem Fehler gibt die Funktion False zurück. Die Abspeicherung wird unterbrochen und eine Suche nach den Fehlern kann beginnen.

Primärschlüssel aus Nummerierung und Jahreszahl

Bei der Erstellung von Rechnungen werden jährlich Bilanzen gezogen. Das führt manchmal zu dem Wunsch, die Rechnungstabellen einer Datenbank nach Jahren getrennt zu sichern und jedes Jahr mit einer neuen Tabelle zu beginnen.

Die folgende Makrolösung geht einen anderen Weg. Sie schreibt automatisch den Wert für das Feld «ID» in die Tabelle, berücksichtigt dabei aber das «Jahr», das in der Tabelle als zweiter Primärschlüssel existiert. So tauchen dann in der Tabelle als Primärschlüssel z.B. die folgenden Werte auf:16

Jahr

ID

2014

1

2014

2

2014

3

2015

1

2015

2

Damit lässt sich eine auf das Jahr bezogene Übersicht auch in den Dokumenten besser erzeugen.

  1. 1 SUB Datum_aktuell_ID_einfuegen 

  2. 2    DIM oDatenquelle AS OBJECT 

  3. 3    DIM oVerbindung AS OBJECT 

  4. 4    DIM oSQL_Anweisung AS OBJECT 

  5. 5    DIM stSql AS STRING 

  6. 6    DIM oAbfrageergebnis AS OBJECT 

  7. 7    DIM oDoc AS OBJECT 

  8. 8    DIM oDrawpage AS OBJECT 

  9. 9    DIM oForm AS OBJECT 

  10. 10    DIM oFeld1 AS OBJECT 

  11. 11    DIM oFeld2 AS OBJECT 

  12. 12    DIM oFeld3 AS OBJECT 

  13. 13    DIM inIDneu AS INTEGER 

  14. 14    DIM inYear AS INTEGER 

  15. 15    DIM unoDate 

  16. 16    oDoc = thisComponent 

  17. 17    oDrawpage = oDoc.drawpage 

  18. 18    oForm = oDrawpage.forms.getByName("MainForm")  

  19. 19    oFeld1 = oForm.getByName("fmtJahr")  

  20. 20    oFeld2 = oForm.getByName("fmtID")         

  21. 21    oFeld3 = oForm.getByName("datDatum") 

  22. 22    IF IsEmpty(oFeld2.getCurrentValue()) THEN 

  23. 23       IF IsEmpty(oFeld3.getCurrentValue()) THEN 

  24. 24          unoDate = createUnoStruct("com.sun.star.util.Date") 

  25. 25          unoDate.Year = Year(Date) 

  26. 26          unoDate.Month = Month(Date) 

  27. 27          unoDate.Day = Day(Date) 

  28. 28          inYear = Year(Date) 

  29. 29       ELSE 

  30. 30          inYear = oFeld3.CurrentValue.Year 

  31. 31       END IF 

  32. 32       oDatenquelle = ThisComponent.Parent.CurrentController 

  33. 33       If NOT (oDatenquelle.isConnected()) THEN 

  34. 34          oDatenquelle.connect() 

  35. 35       END IF 

  36. 36       oVerbindung = oDatenquelle.ActiveConnection() 

  37. 37       oSQL_Anweisung = oVerbindung.createStatement() 

  38. 38       stSql = "SELECT MAX( ""ID"" )+1 FROM ""Auftraege"" WHERE ""Jahr"" = '"
           
    + inYear + "'" 

  39. 39       oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql) 

  40. 40       WHILE oAbfrageergebnis.next 

  41. 41          inIDneu = oAbfrageergebnis.getInt(1) 

  42. 42       WEND 

  43. 43       IF inIDneu = 0 THEN 

  44. 44          inIDneu = 1 

  45. 45       END IF 

  46. 46       oFeld1.BoundField.updateInt(inYear) 

  47. 47       oFeld2.BoundField.updateInt(inIDneu) 

  48. 48       IF IsEmpty(oFeld3.getCurrentValue()) THEN 

  49. 49          oFeld3.BoundField.updateDate(unoDate) 

  50. 50       END IF 

  51. 51    END IF 

  52. 52 END SUB 

Alle Variablen werden deklariert. Die Formularfelder in dem Hauptformular werden angesteuert. Der Rest des Codes läuft nur ab, wenn der Eintrag für das Formularfeld «fmtID» noch leer ist. Dann wird, wenn nicht schon ein Datum eingegeben wurde, zuerst ein Datumsstruct erstellt, um das aktuelle Datum und das aktuelle Jahr in die entsprechenden Felder übertragen zu können. Anschließend wird der Kontakt zu der Datenbank aufgebaut, sofern noch kein Kontakt existiert. Es wird zu dem höchsten Eintrag des Feldes "ID", bezogen auf das Jahr des Datumsfeldes, der Wert '1' addiert. Bleibt die Abfrage leer, so existiert noch kein Eintrag in dem Feld "ID". Jetzt könnte genauso gut '0' direkt in das Formularfeld «fmtID» eingetragen werden. Die Nummerierung für die Aufträge sollte aber mit '1' beginnen, so dass der Variablen «inIDneu» eine '1' zugewiesen wird.

Die ermittelten Werte für das Jahr, die ID und, sofern nicht bereits ein Datum eingetragen wurde, das aktuelle Datum, werden schließlich in das Formular übertragen.

Im Formular sind die Felder für die Primärschlüssel "ID" und "Jahr" schreibgeschützt. Die Zuweisung kann so nur durch das Makro erfolgen.

Datenbankaufgaben mit Makros erweitert

Verbindung mit Datenbanken erzeugen

  1. 1 oDatasource = ThisComponent.Parent.DataSource 

  2. 2 IF NOT oDatasource.IsPasswordRequired THEN 

  3. 3    oConnection = oDatasource.GetConnection("","") 

Hier wäre es möglich, fest einen Benutzernamen und ein Passwort einzugeben, wenn eine Passworteingabe erforderlich wäre. In den Klammer steht dann ("Benutzername","Passwort").

Statt einen Benutzernamen und ein Passwort in Reinschrift einzutragen, wird für diesen Fall der Dialog für den Passwortschutz aufgerufen:

  1. 4 ELSE 

  2. 5    oAuthentication = createUnoService("com.sun.star.sdb.InteractionHandler") 

  3. 6    oConnection = oDatasource.ConnectWithCompletion(oAuthentication) 

  4. 7 END IF 

Dies funktioniert aber nicht, wenn bei der Verbindung bereits eine Benutzername- und Passworteingabe in Base vorgegeben wurde. Hier muss mit

  1. oDatasource.Password = "mein Passwort"

das Passwort der Verbindung mitgegeben werden. Dann erscheint der Dialog nicht mehr. Anschließend muss noch mit der Datenquelle verbunden werden, um direkt auf z.B. ein Formular zugreifen zu können:

  1. 8 ThisComponent.CurrentController.Connect() 

Wird allerdings von einem Formular innerhalb der Base-Datei auf die Datenbank zugegriffen, so reicht bereits

  1. 1 oDatasource = ThisComponent.Parent.CurrentController 

  2. 2 IF NOT (oDatasource.isConnected()) Then 

  3. 3    oDatasource.connect() 

  4. 4 End IF 

  5. 5 oConnection = oDatasource.ActiveConnection() 

Die Datenbank ist hier bekannt, ein Nutzername und ein Passwort sind nicht erforderlich, da diese bereits in den Grundeinstellungen von Base für die internen Datenbankversionen ausgeschaltet sind.

Für Formulare außerhalb von Base wird die Verbindung über das erste Formular hergestellt:

  1. 1 oDatasource = ThisComponent.Drawpage.Forms(0) 

  2. 2 oConnection = oDatasource.ActiveConnection 

Es ist auch möglich, eine Datenbankverbindung ohne eine vorliegende Datenbankdatei zu erzeugen. Dies dürfte dann sinnvoll sein, wenn nur einzelne Informationen aus einer Datenquelle ausgelesen werden sollen und so etwas wie abgespeicherte Abfragen, Formulare und Berichte nicht benötigt werden.
Siehe hierzu die Ausführungen von Andrew Pitonyak in
https://www.pitonyak.org/database/AndrewBase.pdf . Im Kapitel «Connections without a data source» ab S. 91 wird hier eine Verbindung über
  1. 1 oManager = CreateUnoService("com.sun.star.sdbc.DriverManager") 

 

Daten von einer Datenbank in eine andere kopieren

Die interne Datenbank ist erst einmal eine Ein-Benutzer-Datenbank. Die Daten werden innerhalb der *.odb-Datei abgespeichert. Ein Austausch von Daten zwischen verschiedenen Datenbankdateien ist eigentlich nicht vorgesehen, über Export und Import allerdings möglich.

Manchmal werden aber auch *.odb-Dateien so eingesetzt, dass ein möglichst automatischer Datenaustausch von einer Datenbankdatei zu einer anderen erfolgen soll. Die folgende Prozedur kann da hilfreich sein.17

Nach der Deklaration der Variablen wird der Pfad der aktuellen Datenbankdatei von einem Button im Formular aus ausgelesen. Von dem Pfad wird der Dateiname abgetrennt. Die Zieldatei für die Daten befindet sich ebenfalls in dem Verzeichnis. Der Name dieser Datei wird jetzt an den Pfad angehängt, damit der Kontakt zur Zieldatenbankdatei erstellt werden kann.

Der Kontakt zur Ausgangsdatenbank wird im Verhältnis zum Formular ermittelt, in dem der Button liegt: ThisComponent.Parent.CurrentController. Der Kontakt zur externen Datenbank wird über den DatabaseContext und den Pfad zur Datenbank erstellt.

  1. 1 SUB Datenkopie 

  2. 2    DIM oDatabaseContext AS OBJECT 

  3. 3    DIM oDatenquelle AS OBJECT 

  4. 4    DIM oDatenquelleZiel AS OBJECT 

  5. 5    DIM oVerbindung AS OBJECT 

  6. 6    DIM oVerbindungZiel AS OBJECT 

  7. 7    DIM oDB AS OBJECT 

  8. 8    DIM oSQL_Anweisung AS OBJECT 

  9. 9    DIM oSQL_AnweisungZiel AS OBJECT 

  10. 10    DIM oAbfrageergebnis AS OBJECT 

  11. 11    DIM oAbfrageergebnisZiel AS OBJECT 

  12. 12    DIM stSql AS String 

  13. 13    DIM stSqlZiel AS String 

  14. 14    DIM inID AS INTEGER 

  15. 15    DIM inIDZiel AS INTEGER 

  16. 16    DIM stName AS STRING 

  17. 17    DIM stOrt AS STRING 

  18. 18    oDB = ThisComponent.Parent 

  19. 19    stDir = Left(oDB.Location,Len(oDB.Location)-Len(ConvertToURL(oDB.Title))+8) 

  20. 20    stDir = ConvertToUrl(stDir & "ZielDB.odb") 

  21. 21    oDatenquelle = ThisComponent.Parent.CurrentController 

  22. 22    If NOT (oDatenquelle.isConnected()) THEN 

  23. 23       oDatenquelle.connect() 

  24. 24    END IF 

  25. 25    oVerbindung = oDatenquelle.ActiveConnection() 

  26. 26    oDatabaseContext = createUnoService("com.sun.star.sdb.DatabaseContext") 

  27. 27    oDatenquelleZiel = oDatabaseContext.getByName(stDir) 

  28. 28    oVerbindungZiel = oDatenquelleZiel.GetConnection("","") 

  29. 29    oSQL_Anweisung = oVerbindung.createStatement() 

  30. 30    stSql = "SELECT * FROM ""Tabelle""" 

  31. 31    oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql) 

  32. 32    WHILE oAbfrageergebnis.next 

  33. 33       inID = oAbfrageergebnis.getInt(1) 

  34. 34       stName = oAbfrageergebnis.getString(2) 

  35. 35       stOrt = oAbfrageergebnis.getString(3) 

  36. 36       oSQL_AnweisungZiel = oVerbindungZiel.createStatement() 

  37. 37       stSqlZiel = "SELECT ""ID"" FROM ""Tabelle"" WHERE ""ID"" = '"+inID+"'" 

  38. 38       oAbfrageergebnisZiel = oSQL_AnweisungZiel.executeQuery(stSqlZiel) 

  39. 39       inIDZiel = - 1 

  40. 40       WHILE oAbfrageergebnisZiel.next                 

  41. 41          inIDZiel = oAbfrageergebnisZiel.getInt(1) 

  42. 42       WEND 

  43. 43       IF inIDZiel = - 1 THEN 

  44. 44          stSqlZiel = "INSERT INTO ""Tabelle"" (""ID"",""Name"",""Ort"") VALUES
               ('"
    +inID+"','"+stName+"','"+stOrt+"')" 

  45. 45          oSQL_AnweisungZiel.executeUpdate(stSqlZiel) 

  46. 46       END IF 

  47. 47    WEND 

  48. 48 END SUB 

Die komplette Tabelle der Ausgangsdatenbank wird ausgelesen und Zeile für Zeile anschließend über den Kontakt zur Zieldatenbank in die Tabelle der Zieldatenbank eingefügt. Vor dem Einfügen wird allerdings getestet, ob der Wert für den Primärschlüssel bereits vorhanden ist. Ist der Schlüsselwert vorhanden, so wird der Datensatz nicht kopiert.

Hier könnte gegebenenfalls auch eingestellt werden, dass statt einer Kopie des Datensatzes ein Update des bereits existierenden Datensatzes erfolgen soll. Auf jeden Fall wird so sichergestellt, dass die Zieldatenbank die Datensätze mit den entsprechenden Primärschlüsseln der Quelldatenbank enthält.

Direkter Import von Daten aus Calc

Häufig passiert es, dass Calc statt einer Datenbank zur Eingabe von Daten in eine Tabelle genutzt wird. Solche Daten lassen sich dann über die Zwischenablage oder per Drag-and-Drop in eine Base-Tabelle einlesen. Auch der Export in eine bereits in Base eingebundene *.csv-Textdatei ist möglich.

Soll allerdings Calc auf Dauer zur Dateneingabe genutzt werden und die Daten regelmäßig aus Calc ausgelesen werden, so ist der Kopierschritt vielleicht zu umständlich. Hier setzt das folgende Makro an, das aus einem Formular heraus über einen Button gestartet wird.18

Das Makro geht von folgenden Voraussetzungen aus:

  1. 1.Die Daten liegen auf dem ersten Tabellenblatt des Calc-Dokuments 

  2. 2.Auf diesem Tabellenblatt liegen nur die Daten in einer Spalte, nicht zusätzliche Einträge. 

  3. 3.Die erste Datenzeile enthält die Feldbenennungen, die genau den Feldbezeichnungen in der Base-Tabelle entsprechen. 

Die Dateneingabe muss nicht links oben auf dem Tabellenblatt erfolgen. Auch müssen die Felder nicht die gleiche Reihenfolge wie in der Tabelle haben. Spalten, deren Spaltenüberschrift nicht Feldern der Tabelle entspricht, werden ignoriert.

  1. 1 Sub CalcDataImport(oEvent AS OBJECT, stBaseTab AS STRING, stCalcTab AS STRING) 

  2. 2    DIM oDatasource AS OBJECT 

  3. 3    DIM oConnection AS OBJECT 

  4. 4    DIM oSQL_Command AS OBJECT 

  5. 5    DIM oResult AS OBJECT 

  6. 6    DIM oDB AS OBJECT 

  7. 7    DIM oDoc AS OBJECT 

  8. 8    DIM oDocView AS OBJECT 

  9. 9    DIM oRange AS OBJECT 

  10. 10    DIM Arg() 

  11. 11    DIM aColumn() 

  12. 12    DIM aColumns() 

  13. 13    DIM aType() 

  14. 14    DIM stSql AS STRING 

  15. 15    DIM stRow AS STRING 

  16. 16    DIM stDir AS STRING 

  17. 17    DIM stColumns AS STRING 

  18. 18    DIM stStartCol AS STRING 

  19. 19    DIM stEndCol AS STRING 

  20. 20    DIM stStartRow AS STRING 

  21. 21    DIM stEndRow AS STRING 

  22. 22    DIM stRange AS STRING 

  23. 23    DIM inCounter AS INTEGER 

  24. 24    DIM loColumns AS LONG 

  25. 25    DIM loRows AS LONG 

  26. 26    DIM loID AS LONG 

  27. 27    oDatasource = ThisComponent.Parent.CurrentController 

  28. 28    If NOT (oDatasource.isConnected()) THEN 

  29. 29       oDatasource.connect() 

  30. 30    END IF 

  31. 31    oConnection = oDatasource.ActiveConnection() 

  32. 32    oSQL_Command = oConnection.createStatement() 

Nach der Deklaration der Variablen werden Spaltennamen und Spaltentypen aus der vorgegebenen Tabelle der Datenbank ausgelesen. Der Primärschlüssel der Tabelle mit der Bezeichnung "ID" wird als Integer-Feld unabhängig von der Calc-Tabelle erstellt.

  1. 33    stSql = "SELECT COLUMN_NAME, TYPE_NAME FROM INFORMATION_SCHEMA.SYSTEM_COLUMNS
         WHERE TABLE_NAME = '"
    +stBaseTab+"' AND NOT COLUMN_NAME = 'ID'" 

  2. 34    oResult = oSQL_Command.executeQuery(stSql) 

  3. 35    inCounter = 0 

  4. 36    stColumns = "" 

Die Spaltennamen und Spaltentypen werden ausgelesen und in getrennten Arrays gespeichert.

Für Firebird muss der SQL-Code angepasst werden. Vor allem die Zuordnung der Datentypen in SQL-Format ist umständlicher:
  1. 33 stSql = "SELECT TRIM(""a"".RDB$FIELD_NAME), TRIM(CASE
      ""b"".RDB$FIELD_TYPE||'|'||
      COALESCE(""b"".RDB$FIELD_SUB_TYPE,0) "
    + _
      "WHEN '7|0' THEN 'SMALLINT' "+ _
      "WHEN '8|0' THEN 'INTEGER' "+ _
      "WHEN '8|1' THEN 'NUMERIC' "+ _
      "WHEN '8|2' THEN 'DECIMAL' "+ _
      "WHEN '10|0' THEN 'FLOAT' "+ _
      "WHEN '12|0' THEN 'DATE' "+ _
      "WHEN '13|0' THEN 'TIME' "+ _
      "WHEN '14|0' THEN 'CHAR' "+ _
      "WHEN '16|0' THEN 'BIGINT' "+ _
      "WHEN '35|0' THEN 'TIMESTAMP' "+ _
      "WHEN '37|0' THEN 'VARCHAR' "+ _
      "WHEN '261|0' THEN 'BLOB' "+ _
      "WHEN '261|1' THEN 'BLOB Text' "+ _
      "WHEN '261|2' THEN 'BLOB BLR' "+ _
      "WHEN '261|3' THEN 'BLOB ACL' "+ _
      "END) AS ""SQL_Datentyp"" "+ _
      "FROM RDB$RELATION_FIELDS AS ""a"", RDB$FIELDS AS ""b"" "+ _
      "WHERE ""a"".RDB$FIELD_SOURCE = ""b"".RDB$FIELD_NAME "+ _
      "AND ""a"".RDB$RELATION_NAME = '"+stBaseTab+"' "+ _
      "AND ""a"".RDB$FIELD_NAME <> 'ID'" 


Die Felder müssen außerdem mit TRIM eingeschränkt werden, da Firebird die Ausgabe mit fester Zeichenlänge macht und einfach Leerzeichen hinten anfügt. Für die Auswertung im Makro und den Vergleich ist das unpraktikabel.
  1. 37    WHILE oResult.next 

  2. 38       ReDim Preserve aColumn(inCounter) 

  3. 39       ReDim Preserve aType(inCounter) 

  4. 40       aColumn(inCounter) = oResult.getString(1) 

  5. 41       aType(inCounter) = oResult.getString(2) 

  6. 42       inCounter = inCounter+1 

  7. 43    WEND 

Der zur Zeit höchste Eintrag für den Primärschlüsselwert wird ermittelt und um 1 erhöht. In diesem Beispiel wird also der Schlüsselwert nicht automatisch von der HSQLDB hoch geschrieben, sondern über das Makro verwaltet. Dies geschieht hier zu Testzwecken, da die Tabelle laufend unter unterschiedlichen Kriterien testweise eingelesen wurde. Entsprechend könnte natürlich auch der Tabellenindex heruntergesetzt werden, wie dies in dem Makro «Tabellenindex_runter» passiert.

  1. 44    stSql = "SELECT MAX(""ID"") FROM """+stBaseTab+"""" 

  2. 45    oResult = oSQL_Command.executeQuery(stSql) 

  3. 46    WHILE oResult.next 

  4. 47       loID = oResult.getInt(1) + 1 

  5. 48    WEND 

Der Pfad zur Calc-Datei wird anhand der Lage der Base-Datei im Dateisystem ermittelt. Die Calc-Datei liegt hier im gleichen Verzeichnis wie die Base-Datei.

Anschließend wird die Calc-Datei geladen und gleich unsichtbar geschaltet, damit sie sich nicht in den Vordergrund schiebt.

  1. 49    oDB = ThisComponent.Parent 

  2. 50    stDir = Left(oDB.Location,Len(oDB.Location)-Len(ConvertToURL(oDB.Title))+8) 

  3. 51    stDir = ConvertToUrl(stDir & "Daten_Calc.ods") 

  4. 52    oDoc = StarDesktop.loadComponentFromURL(stDir,"_blank", 0, Arg() ) 

  5. 53    oDocView = oDoc.CurrentController.Frame.ContainerWindow 

  6. 54    oDocView.Visible = False 

Die Position des beschrifteten Bereiches des Tabellenblattes wird ermittelt. Dies geschieht über die ColumnDescriptions und RowDescriptions. Sie geben genau die Anzahl der beschrifteten Spalten und Zeilen wieder. Außerdem kann darüber die Bezeichnung der Spalte und der Zeile wie z.B. «B2» ausgelesen werden.

  1. 55    loColumns = uBound(oDoc.Sheets.getByName(stCalcTab).ColumnDescriptions)
         
    ' Anzahl der Spalten 

  2. 56    stStartCol = split(oDoc.Sheets.getByName(stCalcTab).ColumnDescriptions(0))(1) 

  3. 57    stEndCol = split(oDoc.Sheets.getByName(stCalcTab).
         
    ColumnDescriptions(loColumns))(1) 

  4. 58    loRows = uBound(oDoc.Sheets.getByName(stCalcTab).RowDescriptions)       
         
    ' Anzahl der Zeilen 

  5. 59    stStartRow = split(oDoc.Sheets.getByName(stCalcTab).RowDescriptions(0))(1) 

  6. 60    stEndRow = split(oDoc.Sheets.getByName(stCalcTab).RowDescriptions(loRows))(1) 

  7. 61    stRange = stStartCol & stStartRow & ":" & stEndCol & StEndRow 

Der Bereich wird als String zusammengesetzt. Dies erfolgt in der gleichen Art und Weise wie bei der Benennung innerhalb von Calc, also z.B. «A1:C7». Die Daten aus so einem Bereich können mit der Funktion GetDataArray() ausgelesen werden.

  1. 62    oRange = oDoc.Sheets.getByName(stCalcTab).getCellRangeByName(stRange) 

  2. 63    aDat = oRange.getDataArray() 

Die Spaltennamen stehen in der ersten Zeile. Sie können eine andere Reihenfolge haben, als dies innerhalb der Tabelle von Base vorgesehen wird. Deshalb werden diese Bezeichnungen für einen späteren Vergleich in einem separaten Array gespeichert. Sie werden in der folgenden Schleife nicht als Werte zum Einlesen in die Tabelle abgefragt. Deshalb beginnt die Schleife mit i=1.

  1. 64    aColumns = aDat(0) 

  2. 65    FOR i = 1 TO uBound(aDat) 

  3. 66       aRow = aDat(i) 

  4. 67       stColumns = """ID""" 

  5. 68       stRow = loID 

  6. 69       FOR k = 0 TO loColumns 

  7. 70          FOR n = 0 TO uBound(aColumn) 

Im folgenden werden die Spaltenbezeichnungen aus Calc mit denen der Base-Tabelle verglichen. Die Base-Tabelle enthält hier neben Textspalten auch eine Spalte für einen Währungsbetrag, die als DECIMAL definiert ist, sowie ein Datum.

Bei dem Währungsbetrag muss das Dezimalkomma gegebenenfalls zu einem Dezimalpunkt umgewandelt werden. Deshalb hier die Funktion Cdbl, gekoppelt mit der Funktion Str.

Bei dem Datumsfeld gibt Calc den Inhalt als ISO-Zahlencode aus. Dieser Code muss zuerst daraufhin überprüft werden, ob denn überhaupt ein Datum daraus gebildet werden kann. Ist dies möglich, so wird ein SQL-konformes Datum im Format YYYY-MM-DD zusammengestellt, das auch bei einstelligen Tageswerten und Monatswerten nicht versagt.

  1. 71             IF aColumns(k) = aColumn(n) THEN 

  2. 72                IF aType(n) = "DECIMAL" THEN 

  3. 73                   IF aRow(k) <> "" THEN 

  4. 74                      aRow(k) = Str(CDbl(aRow(k))) 

  5. 75                   END IF 

  6. 76                END IF 

  7. 77                IF aType(n) = "DATE" THEN 

  8. 78                   IF isDate(CDate(aRow(k))) AND aRow(k) <> "" THEN 

  9. 79                      aRow(k) = Year(CDate(aRow(k))) & "-"
                           
    & Right("0" & Month(CDate(aRow(k))), 2) &"-"
                           &
    Right("0" & Day(CDate(aRow(k))), 2) 

  10. 80                   ELSE 

  11. 81                      aRow(k) = "" 

  12. 82                   END IF 

  13. 83                END IF 

  14. 84                IF aType(n) = "TIME" THEN 

  15. 85                   IF isDate(CDate(aRow(k))) AND aRow(k) <> "" THEN 

  16. 86                      aRow(k) = Right("0" & Hour(CDate(aRow(k))), 2) & ":"
                           
    & Right("0" & Minute(CDate(aRow(k))), 2) & ":"
                           &
    Right("0" & Second(CDate(aRow(k))), 2) 

  17. 87                   ELSE 

  18. 88                      aRow(k) = "" 

  19. 89                   END IF 

  20. 90                END IF 

  21. 91                IF aType(n) = "TIMESTAMP" 

  22. 92                   IF isDate(CDate(aRow(k))) AND aRow(k) <> "" THEN 

  23. 93                      aRow(k) = Year(CDate(aRow(k))) & "-"
                           &
    Right("0" & Month(CDate(aRow(k))), 2) & "-"
                           &
    Right("0" & Day(CDate(aRow(k))), 2) & " "
                           &
    Right("0" & Hour(CDate(aRow(k))), 2) & ":"
                           &
    Right("0" & Minute(CDate(aRow(k))), 2) & ":"
                           &
    Right("0" & Second(CDate(aRow(k))), 2) 

  24. 94                   ELSE 

  25. 95                      aRow(k) = "" 

  26. 96                   END IF 

  27. 97                END IF 

Ist der Zellinhalt leer oder für ein Dezimalfeld, Datumsfeld, Zeitfeld oder Timestampfeld gegebenenfalls nicht gültig, so wird an die Base-Tabelle NULL weitergegeben. Andernfalls wird der Inhalt in Hochkommata gesetzt. Anschließend werden die Inhalte über Kommata miteinander verbunden. Auch die Bezeichnung der Spalten erfolgt in der Reihenfolge, in der sie aus der Calc-Tabelle ausgelesen wurden.

  1. 98                IF aRow(k) = "" THEN 

  2. 99                   aRow(k) = "NULL" 

  3. 100                ELSE 

  4. 101                   aRow(k) = "'" & aRow(k) & "'" 

  5. 102                END IF 

  6. 103                stRow = stRow & "," & aRow(k) 

  7. 104                stColumns = stColumns & ",""" & aColumns(k) & """" 

  8. 105             END IF 

  9. 106          NEXT 

  10. 107       NEXT 

  11. 108       stSql = "INSERT INTO """+stBaseTab+""" ("& stColumns &")
            VALUES ("
    & stRow &")" 

  12. 109       oSQL_Command.executeUpdate(stSql) 

Der Primärschlüsselwert wird hier innerhalb des Makros um 1 heraufgesetzt.

  1. 110       loID = loID + 1 

  2. 111    NEXT 

  3. 112    oDoc.close(True)        ' Schließen des Calc-Documentes 

  4. 113    oEvent.Source.Model.parent.reload()        ' Neuladen des Formulars 

  5. 114 End Sub 

Ist der gesamte Inhalt aus dem Calc-Tabellenblatt ausgelesen, so wird das unsichtbare Dokument geschlossen. Anschließend wird das Formular, in dem sich der auslösende Button befindet, neu eingelesen. Dabei wird das Formular über den Button mit oEvent.Source.Model.parent ermittelt.

Die Prozedur wird über eine andere Prozedur gestartet. Dadurch kann die Prozedur auch für mehrere Tabellen der Calc-Datei bzw. der Base-Datei genutzt werden:

  1. 1 SUB Import1_Start(oEvent AS OBJECT) 

  2. 2    CalcDataImport(oEvent, "tbl_Dez_Dat_NULL", "Tab_Dez_Dat_NULL") 

  3. 3 END SUB 

Das auslösende Ereignis wird einfach weiter gereicht. Als zweiter Parameter wird der Tabellenname der Base-Tabelle angegeben. Als dritter Parameter erfolgt die Angabe des Tabellennamens aus der Calc-Datei.

Zugriff auf Abfragen

Abfragen lassen sich in der grafischen Benutzeroberfläche einfacher zusammenstellen als den gesamten Text in Makros zu übertragen, zumal dann auch noch innerhalb des Makros alle Felder- und Tabellenbezeichnungen in zweifach doppelte Anführungszeichen gesetzt werden müssen.

  1. 1 SUB Abfrageninhalt 

  2. 2    DIM oDatenDatei AS OBJECT 

  3. 3    DIM oAbfragen AS OBJECT 

  4. 4    DIM stQuery AS STRING 

  5. 5    oDatenDatei = ThisComponent.Parent.CurrentController.DataSource 

  6. 6    oAbfragen = oDatenDatei.getQueryDefinitions() 

  7. 7    stQuery = oAbfragen.getByName("Query").Command 

  8. 8    msgbox stQuery 

  9. 9 END SUB 

Aus einem Formular heraus wird auf den Inhalt der *.odb-Datei zugegriffen. Die Abfragen werden über getQueryDefinitions() ermittelt. Die SQL-Formulierung der Abfrage "Query" wird über den Zusatz Command bereit gestellt. Command kann schließlich dazu genutzt werden, eine entsprechende Abfrage auch innerhalb eines Makros weiter zu nutzen.

Allerdings muss bei der Nutzung des SQL-Codes der Abfrage darauf geachtet werden, dass sich der Code nicht wiederum auf eine Abfrage bezieht. Das führt dann unweigerlich zu der Meldung, dass die (angebliche) Tabelle der Datenbank unbekannt ist. Einfacher ist es daher, aus Abfragen Ansichten zu erstellen und entsprechend auf die Ansichten in Makros zuzugreifen.

Soll eine Abfrage in einem Formular weiter genutzt werden, so geht dies über die COMMAND-Variable des Formulars:

  1. 8 oForm.Command = stQuery 

  2. 9 oForm.CommandType = com.sun.star.sdb.CommandType.COMMAND 

  3. 10 oForm.reload() 

Auch die Änderung von Abfragen über ein Makro ist über die Abfragedefinition möglich:

  1. 7 oAbfrage = oAbfragen.getByName("Query") 

  2. 8 oAbfrage.Command = "SELECT …" 

Mit Hilfe solch eine Konstruktion kann z. B. anschließend ein Bericht gestartet werden, der sich auf die Abfrage bezieht. Dadurch kann eine entsprechende Filterung der Daten vorgenommen werden ohne dass eine separate Filtertabelle erstellt werden muss. Dies ist vor allem dann von Vorteil, wenn es sich bei der zugrundeliegenden Datenbank nicht um eine Datenbank handelt, die Abfragen über mehrere Tabellen zulässt. Dies ist beispielsweise bei dBase der Fall. dBase-Tabellen lassen sich nicht in einer Abfrage kombinieren, über Makros dann aber sehr wohl entsprechend filtern, indem der Wert einer Makroabfrage anschließend dem Code der bestehenden Abfrage hinzugefügt wird.

Datenbanksicherungen erstellen

Vor allem beim Erstellen von Datenbanken kann es hin und wieder vorkommen, dass die *.odb-Datei unvermittelt beendet wird. Vor allem beim Berichtsmodul ist ein häufigeres Abspeichern nach dem Editieren sinnvoll.

Ist die Datenbank erst einmal im Betrieb, so kann sie durch Betriebssystemabstürze beschädigt werden, wenn der Absturz gerade während des Schließens der Base-Datei erfolgt. Schließlich wird in diesem Moment der Inhalt der Datenbank in die Datei zurückgeschrieben.

Außerdem gibt es die üblichen Verdächtigen für plötzlich nicht mehr zu öffnende Dateien wie Festplattenfehler usw. Da kann es dann nicht schaden, eine Sicherheitskopie möglichst mit dem aktuellsten Datenstand parat zu haben. Der Datenbestand ändert sich allerdings nicht, während die *.odb-Datei geöffnet ist. Deshalb kann die Sicherung direkt mit dem Öffnen der Datei verbunden werden. Es werden einfach Kopien der Datei in das unter Extras → Optionen → LibreOffice → Pfade angegebene Backup-Verzeichnis erstellt. Existiert das Verzeichnis noch nicht, so wird es erstellt. Das Makro beginnt nach einer voreingestellten Zahl an Kopien (inMax) damit, die jeweils älteste Variante zu überschreiben.

Die Prozedur «Datenbankbackup» wird unter Extras → Anpassen → Ereignisse → Dokument öffnen eingebunden.

  1. 1 SUB Datenbankbackup(inMax AS INTEGER) 

  2. 2    DIM oPath AS OBJECT 

  3. 3    DIM oDoc AS OBJECT 

  4. 4    DIM sTitel AS STRING 

  5. 5    DIM sUrl_Ziel AS STRING 

  6. 6    DIM sUrl_Start AS STRING 

  7. 7    DIM i AS INTEGER 

  8. 8    DIM k AS INTEGER 

  9. 9    oDoc = ThisComponent 

  10. 10    sTitel = oDoc.Title 

  11. 11    sUrl_Start = oDoc.URL 

  12. 12    DO WHILE sUrl_Start = "" 

  13. 13       oDoc = oDoc.Parent 

  14. 14       sTitel = oDoc.Title 

  15. 15       sUrl_Start = oDoc.URL 

  16. 16    LOOP 

Wird das Makro direkt beim Start der *.odb-Datei ausgeführt, so stimmen sTitel und sUrl_Start. Wird es hingegen von einem Formular aus ausgeführt, so muss erst einmal ermittelt werden, ob überhaupt eine URL verfügbar ist. Ist die URL leer, so wird eine Ebene höher (oDoc.Parent) nach einem Wert nachgesehen.

  1. 17    oPath = createUnoService("com.sun.star.util.PathSettings") 

  2. 18    MkDir(oPath.Backup) 

  3. 19    FOR i = 1 TO inMax + 1 

  4. 20       IF NOT FileExists(oPath.Backup & "/" & i & "_" & sTitel) THEN 

  5. 21          IF i > inMax THEN 

  6. 22             FOR k = inMax - 1 TO 1 STEP -1 

  7. 23                IF FileDateTime(oPath.Backup & "/" & k & "_" & sTitel) <=
                     
    FileDateTime(oPath.Backup & "/" & k+1 & "_" & sTitel) THEN 

  8. 24                   IF k = 1 THEN 

  9. 25                      i = k 

  10. 26                      EXIT FOR 

  11. 27                   END IF 

  12. 28                ELSE 

  13. 29                   i = k + 1 

  14. 30                   EXIT FOR 

  15. 31                END IF 

  16. 32             NEXT 

  17. 33          END IF 

  18. 34          EXIT FOR 

  19. 35       END IF 

  20. 36    NEXT 

  21. 37    sUrl_Ziel = oPath.Backup & "/" & i & "_" & sTitel 

  22. 38    FileCopy(sUrl_Start,sUrl_Ziel) 

  23. 39 END SUB 

Werden vor der Ausführung der Prozedur «Datenbankbackup» während der Nutzung von Base die Daten aus dem Cache in die Datei zurückgeschrieben, so kann ein entsprechendes Backup auch z.B. nach einer bestimmten Nutzerzeit oder durch Betätigung eines Buttons sinnvoll sein. Das Zurückschreiben regelt die folgende Prozedur:

  1. 1 SUB Daten_aus_Cache_schreiben 

  2. 2    DIM oDaten AS OBJECT 

  3. 3    DIM oDataSource AS OBJECT 

  4. 4    oDaten = ThisDatabaseDocument.CurrentController 

  5. 5    IF NOT ( oDaten.isConnected() ) THEN oDaten.connect() 

  6. 6    oDataSource = oDaten.DataSource 

  7. 7    oDataSource.flush 

  8. 8 END SUB 

Für Firebird lässt sich dieser Code gut nutzen um beim Schließen der Datenbankdatei die Daten automatisch zu schreiben. Sonst erscheint nach Datenänderungen nämlich die Aufforderung zum Speichern, da das im Gegensatz zu internen Hsqldb nicht automatisch abläuft.
Die Prozedur «Daten_aus_Cache_schreiben» wird unter
Extras → Anpassen → Ereignisse → Ansicht wird geschlossen eingebunden. Dann erfolgt die Datenspeicherung vor der Abfrage zur Speicherung der Datei.

Soll alles zusammen aus einem Formular heraus über einen Button gestartet werden, so müssen beide Prozeduren über eine weitere Prozedur angesprochen werden:

  1. 1 SUB Backup_sofort 

  2. 2    Daten_aus_Cache_schreiben 

  3. 3    Datenbankbackup(10) 

  4. 4 END SUB 

Gerade bei einem Sicherungsmakro ist es vielleicht sinnvoll, das Makro über die Symbolleiste der Datenbank erreichbar zu machen. Dies geschieht im Hauptfenster der Base-Datei unter Extras → Anpassen → Symbolleisten. Dort wird als Bereich die aktuelle Datenbankdatei, als Kategorie «Makros» und als Ziel die für alle Bereiche zuständige Symbolleiste «Standard» ausgesucht.

 

Bei den Makros sind die verfügbaren allgemeinen Makros sowie die Makros aus der Datenbankdatei auswählbar. Aus den Datenbankmakros wird die Prozedur «Backup_Sofort» ausgesucht.

 

Der Befehl ist jetzt in der Symbolleiste an erster Stelle verfügbar. Um jetzt die Prozedur auszuführen genügt ein Auslösen des Buttons in der Symbolleiste.

Jetzt bietet es sich noch an, dem Befehl ein Symbol zuzuweisen. Über Ändern → Symbol austauschen wird der folgende Dialog geöffnet.

 

Hier wird jetzt ein passendes Symbol gesucht. Es kann auch ein eigenes Symbol erstellt und eingebunden werden.

 

Das Symbol erscheint anschließend statt der Benennung der Prozedur. Die Benennung wird als Tooltip angezeigt.

Interne Datenbanken sicher schließen

Die internen Datenbanken müssen vor dem Schließen erst einmal den Inhalt aus dem Speicher zurück in den Datenbankcontainer, die *.odb-Datei, schreiben. Ein einfaches oDoc.close(True) führt hier bei dem Datenbankdokument dazu, dass es zwar geschlossen wird, die Änderungen seit dem letzten Speichern nicht mehr gesichert sind. Hierzu muss über oDoc.Datasource.flush der Speicherinhalt in die interne Datenbank geschrieben und dann die Speicherung vollzogen werden. Die obige Prozedur Daten_aus_Cache_schreiben muss also für das Schließen zum Schluss nach dem Schreiben aus dem Cache nur um den Eintrag ThisDatabaseDocument.close(True) erweitert werden.

Tabellenindex heruntersetzen bei Autowert-Feldern

Werden viele Daten aus Tabellen gelöscht, so stören sich Nutzer häufig daran, dass die automatisch erstellten Primärschlüssel einfach weiter hochgezählt werden, statt direkt an den bisher höchsten Schlüsselwert anzuschließen. Die folgende Prozedur liest für eine Tabelle den bisherigen Höchstwert des Feldes "ID" aus und stellt den nächsten Schlüsselstartwert um 1 höher als das Maximum ein.

Heißt das Primärschlüsselfeld nicht "ID", so müsste das Makro entsprechend angepasst werden.

  1. 1 SUB Tabellenindex_runter(stTabelle AS STRING) 

  2. 2    REM Mit dieser Prozedur wird das automatisch hochgeschriebene
         Primärschlüsselfeld mit der vorgegebenen Bezeichnung "ID" auf den niedrigst
         möglichen Wert eingestellt.
     

  3. 3    DIM stAnzahl AS STRING 

  4. 4    DIM inAnzahl AS INTEGER 

  5. 5    DIM inSequence_Value AS INTEGER 

  6. 6    oDatenquelle = ThisComponent.Parent.CurrentController
         ' Zugriffsmöglichkeit aus dem Formular heraus 

  7. 7    IF NOT (oDatenquelle.isConnected()) THEN 

  8. 8       oDatenquelle.connect() 

  9. 9    END IF 

  10. 10    oVerbindung = oDatenquelle.ActiveConnection() 

  11. 11    oSQL_Anweisung = oVerbindung.createStatement() 

  12. 12    stSql = "SELECT MAX(""ID"") FROM """+stTabelle+""""
         
    ' Der höchste in "ID" eingetragene Wert wird ermittelt 

  13. 13    oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql)   ' Abfrage starten und
         den Rückgabewert in einer Variablen oAbfrageergebnis speichern
     

  14. 14    WHILE oAbfrageergebnis.next 

  15. 15       stAnzahl = oAbfrageergebnis.getString(1)        ' Erstes Datenfeld wird ausgelesen 

  16. 16    WEND   ' nächster Datensatz, in diesem Fall nicht mehr erforderlich, da nur ein
         Datensatz existiert
     

  17. 17    IF stAnzahl = "" THEN        ' Falls der höchste Wert gar kein Wert ist, also die
         Tabelle leer ist wird der höchste Wert als -1 angenommen
     

  18. 18       inAnzahl = -1 

  19. 19    ELSE 

  20. 20       inAnzahl = cInt(stAnzahl) 

  21. 21    END IF 

  22. 22    inSequence_Value = inAnzahl+1        ' Der höchste Wert wird um 1 erhöht 

  23. 23    REM Ein neuer Befehl an die Datenbank wird vorbereitet. Die ID wird als neu
         startend ab inAnzahl+1 deklariert.
     

  24. 24    REM Diese Anweisung hat keinen Rückgabewert, da ja kein Datensatz ausgelesen
         werden muss
     

  25. 25    oSQL_Anweisung1 = oVerbindung.createStatement() 

  26. 26    oSQL_Anweisung1.executeQuery("ALTER TABLE """ + stTabelle + """  

  27. 27       ALTER COLUMN ""ID"" RESTART WITH " + inSequence_Value + "") 

  28. 28 END SUB 

Drucken aus Base heraus

Der Standard, um aus Base heraus ein druckbares Dokument zu erzeugen, ist die Nutzung eines Berichtes. Daneben gibt es noch Möglichkeiten, Tabellen und Abfragen einfach nach Calc zu kopieren und dort zum Druck aufzubereiten. Auch der direkte Druck eines Formularinhaltes vom Bildschirm ist natürlich möglich.

Druck von Berichten aus einem internen Formular heraus

Um einen Bericht zu starten, muss normalerweise die Benutzeroberfläche von Base aufgesucht werden. Ein Mausklick auf den Berichtsnamen startet dann die Ausführung des Berichtes. Einfacher geht es natürlich, den Bericht direkt aus dem Formular heraus zu starten:

  1. 1 SUB Berichtsstart 

  2. 2    ThisDatabaseDocument.ReportDocuments.getByName("Bericht").open 

  3. 3 END SUB 

Sämtliche Berichte werden über ReportDocuments mit ihrem Namen angesprochen. Mit open werden sie geöffnet. Wird ein Bericht jetzt an eine Abfrage gebunden, die über das Formular gefiltert wird, so kann auf diese Art und Weise der zum aktuellen Datensatz anfallende Ausdruck erfolgen.

Start, Formatierung, direkter Druck und Schließen des Berichts

Noch schöner ist es, wenn der Bericht direkt an den Drucker geschickt wird. Die folgende Kombination an Prozeduren legt sogar noch ein paar kleine Features zu. Sie selektiert zuerst den aktiven Datensatz des Formulars, formatiert danach den Bericht um, indem die Felder für den Text auf automatische Höhe eingestellt werden, um anschließend den Bericht zu starten. Schließlich wird der Bericht auch noch gedruckt und ggf. noch als *.pdf-Dokument abgespeichert. Und all das passiert nahezu vollständig im Hintergrund, da der Bericht direkt nach dem Öffnen auf unsichtbar geschaltet und nach dem Ausdruck wieder geschlossen wird. Anregungen für die verschiedenen Prozeduren stammen hier von Andrew Piontak, Thomas Krumbein und Lionel Elie Mamane.

  1. 1 SUB BerichtStart(oEvent AS OBJECT) 

  2. 2    DIM oForm AS OBJECT 

  3. 3    DIM stSql AS STRING 

  4. 4    DIM oDatenquelle AS OBJECT 

  5. 5    DIM oVerbindung AS OBJECT 

  6. 6    DIM oSQL_Anweisung AS OBJECT 

  7. 7    DIM oReport AS OBJECT 

  8. 8    DIM oReportView AS OBJECT 

  9. 9    oForm = oEvent.Source.model.parent 

  10. 10    stSql = "UPDATE ""Filter"" SET ""Integer"" = '" +
         
    oForm.getInt(oForm.findColumn("ID")) + "' WHERE ""ID"" = TRUE" 

  11. 11    oDatenquelle = ThisComponent.Parent.CurrentController 

  12. 12    If NOT (oDatenquelle.isConnected()) THEN 

  13. 13       oDatenquelle.connect() 

  14. 14    END IF 

  15. 15    oVerbindung = oDatenquelle.ActiveConnection() 

  16. 16    oSQL_Anweisung = oVerbindung.createStatement() 

  17. 17    oSQL_Anweisung.executeUpdate(stSql) 

  18. 18    oReport = ThisDatabaseDocument.ReportDocuments.getByName("Berichtsname").open 

  19. 19    oReportView = oReport.CurrentController.Frame.ContainerWindow 

  20. 20    oReportView.Visible = False 

  21. 21    BerichtZeilenhoeheAuto(oReport) 

  22. 22 END SUB 

Die Prozedur «BerichtStart» wird mit einem Button innerhalb eines Formulars verknüpft. Es kann dabei über den Button der Primärschlüssel des aktuellen Datensatzes des Formulars ausgelesen werden. Dies geschieht hier über das auslösende Ereignis, von dem heraus auf das Formular oForm geschlossen wird. Der Name des Schlüsselfeldes ist hier mit "ID" angegeben. Über oForm.getInt(oForm.findColumn("ID")) wird aus dem Feld der Schlüsselwert als Integer-Wert gelesen. Dieser Wert wird anschließend in einer Filtertabelle abgespeichert. Diese Filtertabelle steuert über eine Abfrage, dass nur der aktuelle Datensatz des Formulars für den Bericht verwendet wird.

Ohne Bezug auf das Formular könnte auch nur der Bericht aufgerufen werden. Dabei ist der aufgerufene Bericht gleich als Objekt ansprechbar (oReport). Anschließend wird das Fenster auf unsichtbar eingestellt. Dies geht leider nicht direkt mit dem Aufruf, so dass ganz kurz das Fenster erscheint, dann aber ggf. in Ruhe im Hintergrund mit dem entsprechenden Inhalt gefüllt wird.

Anschließend wird die Prozedur «BerichtZeilenhoeheAuto» gestartet. Dieser Prozedur wird der Hinweis auf den geöffneten Bericht mitgegeben.

Diese Prozedur ist ab LO 6.4 nicht mehr notwendig. Seitdem ist im Report-Designer für aufgezogene Felder eine automatische Größeneinstellung möglich. Aber Achtung: Das funktioniert nur ab LO 6.4 und neuer. Die Höhe wird bei älteren Versionen nicht automatisch eingestellt.

Die Zeilenhöhe kann beim Berichtsentwurf bis LO 6.4 nicht automatisch angepasst werden. Ist zu viel Text für ein Feld vorgesehen, so wird der Text abgeschnitten und darauf mit Hilfe eines roten Dreiecks hingewiesen. Solange dies nicht funktioniert, stellt die folgende Prozedur sicher, dass z.B. in allen Tabellen mit der Bezeichnung «Detail» die automatische Höhe eingeschaltet wird.

  1. 1 SUB BerichtZeilenhoeheAuto(oReport AS OBJECT) 

  2. 2    DIM oTables AS OBJECT 

  3. 3    DIM oTable AS OBJECT 

  4. 4    DIM inT AS INTEGER 

  5. 5    DIM inI AS INTEGER 

  6. 6    DIM oRows AS OBJECT 

  7. 7    DIM oRow AS OBJECT 

  8. 8    oTables = oReport.getTextTables() 

  9. 9    FOR inT = 0 TO oTables.count() - 1 

  10. 10       oTable = oTables.getByIndex(inT) 

  11. 11       IF Left$(oTable.name, 6) = "Detail" THEN 

  12. 12          oRows = oTable.Rows 

  13. 13          FOR inI = 0 TO oRows.count - 1 

  14. 14             oRow = oRows.getByIndex(inI) 

  15. 15             oRow.IsAutoHeight = True 

  16. 16          NEXT inI 

  17. 17       ENDIF 

  18. 18    NEXT inT 

  19. 19    BerichtDruckenUndSchliessen(oReport) 

  20. 20 END SUB 

Bei dem Entwurf des Berichtes muss darauf geachtet werden, dass alle in einer Zeile des Bereiches «Detail» befindlichen Felder tatsächlich die gleiche Höhe haben. Sonst kann es zusammen mit der Automatik passieren, dass ein Feld plötzlich auf die doppelte Zeilenhöhe gesetzt wird.

Nachdem in allen Tabellen mit der Bezeichnung «Detail» die automatische Höhe eingestellt wurde, wird anschließend der Bericht über die Prozedur «BerichtDruckenUndSchliessen» weiter an den Drucker geschickt.

Das Array Props enthält die verschiedenen Werte, die mit dem Drucker bei einem Dokument verbunden sind. Für den Druckbefehl ist hier lediglich der Name des Standarddruckers wichtig. Das Berichtsdokument soll so lange geöffnet bleiben, bis der Druck tatsächlich abgeschlossen ist. Dies geschieht, indem dem Druckbefehl der Name und der Befehl «Warte, bis ich fertig bin» (Wait) mitgegeben wird.

  1. 1 Sub BerichtDruckenUndSchliessen(oReport AS OBJECT) 

  2. 2    DIM Props 

  3. 3    DIM stDrucker AS STRING 

  4. 4    Props = oReport.getPrinter() 

  5. 5    stDrucker = Props(0).value 

  6. 6    DIM arg(1) AS NEW com.sun.star.beans.PropertyValue 

  7. 7    arg(0).name = "Name"         

  8. 8    arg(0).value = "<" & stDrucker & ">" 

  9. 9    arg(1).name = "Wait"         

  10. 10    arg(1).value = True 

  11. 11    oReport.print(arg())         

  12. 12    oReport.close(true) 

  13. 13 End Sub 

Erst wenn der Druck komplett an den Drucker abgeschickt wurde, wird das Dokument geschlossen.

Zu Einstellungen des Druckers siehe Drucker und Druckeinstellungen aus der LibreOffice API.

Soll statt des Drucks oder zusätzlich zu dem Druck auch eine *.pdf-Datei des Dokumentes als Sicherungskopie abgelegt werden, so wird darauf mit der Methode storeToURL() zugegriffen:

  1. 1 Sub BerichtAlsPDFspeichern(oReport AS OBJECT) 

  2. 2    DIM stUrl AS STRING 

  3. 3    DIM arg(0) AS NEW com.sun.star.beans.PropertyValue 

  4. 4    arg(0).name = "FilterName"         

  5. 5    arg(0).value = "writer_pdf_Export" 

  6. 6    stUrl = ConvertToURL("file:///....") 

  7. 7    oReport.storeToURL(stUrl, arg()) 

  8. 8 End Sub 

Bei der URL muss natürlich eine komplette URL-Adresse angegeben werden. Noch sinnvoller ist es, diese Adresse z.B. gekoppelt mit einem unverwechselbaren Merkmal des gedruckten Dokumentes zu versehen, wie z.B. der Rechnungsnummer. Sonst könnte es passieren, dass eine Sicherungsdatei beim nächsten Druck einfach überschrieben wird.

Druck von Berichten aus einem externen Formular heraus

Schwierig wird es, wenn mit externen Formularen gearbeitet wird. Die Berichte liegen dann in der *.odb-Datei und sind auch über den Datenquellenbrowser erst einmal nicht verfügbar.

  1. 1 SUB Berichtsstart(oEvent AS OBJECT) 

  2. 2    DIM oFeld AS OBJECT 

  3. 3    DIM oForm AS OBJECT 

  4. 4    DIM oDocument AS OBJECT 

  5. 5    DIM oDocView AS OBJECT 

  6. 6    DIM Arg() 

  7. 7    oFeld = oEvent.Source.Model 

  8. 8    oForm = oFeld.Parent 

  9. 9    sURL = oForm.DataSourceName 

  10. 10    oDocument = StarDesktop.loadComponentFromURL(sURL, "_blank", 0, Arg() ) 

  11. 11    oDocView = oDocument.CurrentController.Frame.ContainerWindow 

  12. 12    oDocView.Visible = False 

  13. 13    oDocument.getCurrentController().connect 

  14. 14    Wait(100) 

  15. 15    oDocument.ReportDocuments.getByName("Bericht").open 

  16. 16    oDocument.close(True) 

  17. 17 END SUB 

Der Bericht wird von einem Button des externen Formulars gestartet. Über den Button wird das Formular ermittelt, in dem der Pfad zur *.odb-Datei verzeichnet ist: oForm.DataSourceName. Anschließend wird mit loadComponentFromUrl die *.odb-Datei geöffnet. Die Datei soll nur im Hintergrund liegen. Deshalb wird gleich auf die Ansicht zugegriffen und die Oberfläche der *.odb-Datei auf Visible = False gestellt. Dies sollte auch direkt beim Aufruf über die Argumentenliste Arg() funktionieren, brachte aber bei Tests nicht den gewünschten Erfolg.

Wird jetzt direkt der Bericht des geöffneten Dokumentes aufgerufen, so ist die Datenbankverbindung noch nicht verfügbar. Der Bericht erscheint nur mit einem grauen Hintergrund und LibreOffice verzeichnet einen Absturz. Schon eine kleine Wartezeit von 100 Millisekunden ( Wait(100) ) löst dieses Problem. Hier müssen praktische Tests zeigen, wie kurz diese Zeit eingestellt werden kann. Anschließend wird der Bericht gestartet. Da es sich bei dem ausgeführten Bericht um eine separate Textdatei handelt, kann die geöffnete *.odb-Datei anschließend geschlossen werden. Mit oDocument.close(True) wird der Befehl an die *.odb-Datei weiter gegeben. Die Datei wird allerdings erst dann geschlossen, wenn sie nicht mehr aktiv z.B. Daten an die Berichtsdatei weiter geben muss.

Mit einem entsprechenden Zugriff können auch die Formulare innerhalb der *.odb-Datei gestartet werden. Hier sollte dann aber das Schließen des Dokumentes unterbleiben.

Deutlich schneller als mit dem Report-Designer und trotzdem gut gestaltet geht der Druck aber über Makros mit Hilfe von Serienbrieffunktionen oder Textfeldern.

Serienbriefdruck aus Base heraus

Manchmal reicht einfach ein Bericht nicht aus, um sauber Briefe an die Adressaten zu erstellen. Schon die Textfelder in einem Bericht sind hier in der Nutzung doch sehr eingeschränkt. Hierzu wird dann ein Serienbrief im Writer erstellt. Es muss aber nicht sein, dass erst der Writer geöffnet wird, dort dann über den Serienbriefdruck alle Auswahlmöglichkeiten und Eingaben gemacht werden und schließlich der Druck erfolgt. Dies alles geht auch per Makro direkt aus Base heraus.

  1. 1 SUB Serienbriefdruck 

  2. 2    DIM oMailMerge AS OBJECT 

  3. 3    DIM aProps() 

  4. 4    oMailMerge = createunoservice("com.sun.star.text.MailMerge") 

Als Name der Datenquelle wird der Name angegeben, unter dem die Datenbank in LO angemeldet ist. Dieser Name muss nicht identisch mit dem Dateinamen sein. Der Anmeldename in diesem Beispiel lautet "Adressen"

  1. 5    oMailMerge.DataSourceName = "Adressen" 

Die Pfadbeschreibung mit der Serienbriefdatei erfolgt in der Art der jeweiligen Betriebssystemumgebung, hier ab dem Wurzelpfad eines Linux-Systems.

  1. 6    oMailMerge.DocumentURL = ConvertToUrl("home/user/Dokumente/Serienbrief.odt") 

Der Typ des Kommandos wird festgelegt. '0' steht für eine Tabelle, '1' für eine Abfrage und '2' für ein direktes SQL-Kommando.

  1. 7    oMailMerge.CommandType = 1 

Hier wurde eine Abfrage gewählt, die den Namen "Serienbriefabfrage" trägt.

  1. 8    oMailMerge.Command = "Serienbriefabfrage" 

Über den Filter wird festgelegt, für welche Datensätze aus der Serienbriefabfrage ein Druck erfolgen soll. Dieser Filter könnte z.B. über ein Formularfeld aus Base heraus an das Makro weitergegeben werden. Mit dem Primärschlüssel eines Datensatzes könnte so der Ausdruck eines einzelnen Dokumentes erfolgen.

In diesem Beispiel wird aus der "Serienbriefabfrage" das Feld "Geschlecht" aufgesucht und dort nach Datensätzen gesucht, die in diesem Feld mit einem 'm' versehen sind.

  1. 9    oMailMerge.Filter = """Geschlecht""='m'" 

Es gibt die Ausgabetypen Drucker (1), Datei (2) und Mail (3). Hier wurde zu Testzwecken die Ausgabe in eine Datei gewählt. Diese Datei wird in dem angegebenen Pfad abgespeichert. Für jeden Serienbriefdatensatz wird ein Druck erzeugt. Damit dieser Druck unterscheidbar ist, wird das Feld "Nachname" in den Dateinamen aufgenommen.

  1. 10    oMailMerge.OutputType = 2 

  2. 11    oMailMerge.OutputUrl = ConvertToUrl("home/user/Dokumente") 

  3. 12    oMailMerge.FileNameFromColumn = True 

  4. 13    oMailMerge.Filenameprefix = "Nachname" 

  5. 14    oMailMerge.execute(aProps()) 

  6. 15 END SUB 

Wird allein der Filter über ein Formular bestückt, so kann auf diese Art und Weise, also ohne die Öffnung des Writer-Dokuments, ein Serienbriefdruck erfolgen. Mit der entsprechenden Eingabe von anderen Parametern ist es auch möglich, direkt eine Mail mit persönlichem Anhang zu erstellen. Siehe zu den Parametern https://api.libreoffice.org/docs/idl/ref/servicecom_1_1sun_1_1star_1_1text_1_1MailMerge.html.

Drucken über Textfelder

Über Einfügen → Feldbefehl → Funktionen → Platzhalter wird im Writer eine Vorlage für das zukünftig zu druckende Dokument erstellt. Die Platzhalter sollten dabei sinnvollerweise mit dem Namen versehen werden, den die Felder auch in der Datenbank bzw. der Tabelle/Abfrage für das Formular haben, aus dem heraus das Makro aufgerufen wird.

Für einfache Zwecke wird «Text» als Typ für den Platzhalter gewählt.

In dem Makro wird der Pfad zur Vorlage hinterlegt. Es wird ein neues Dokument «Unbenannt1.odt» erstellt. Vom Makro werden die Platzhalter über die Abfrage des Inhaltes des aktuellen Datensatzes des Formulars befüllt. Das offene Dokument kann nun noch nach Belieben verändert werden.

In der Beispieldatenbank «Beispiel_Datenbank_Serienbrief_direkt.odb» wird gezeigt, wie mit Hilfe von Textfeldern und einem Zugriff auf eine in der Vorlage bereits vorgesehenen Tabelle eine komplette Rechnung erstellt werden kann. Im Gegensatz zum Report-Designer sind bei dieser Form der Rechnungserstellung die entsprechenden Felder für den Tabelleninhalt nicht in der Höhe begrenzt. Deshalb wird immer aller Text angezeigt.

Hier Teile des Codes, der im Wesentlichen diesem Beitrag von DPunch zu verdanken ist: http://de.openoffice.info/viewtopic.php?f=8&t=45868#p194799

  1. 1 SUB Textfelder_Fuellen 

  2. 2    oForm = thisComponent.Drawpage.Forms.MainForm 

  3. 3    IF oForm.RowCount = 0 THEN 

  4. 4       msgbox "Kein Datensatz zum Drucken vorhanden" 

  5. 5       EXIT SUB 

  6. 6    END IF 

Das Hauptformular wird angesteuert. Hier könnte auch die Lage des auslösenden Buttons das Formular selbst ermitteln. Anschließend wird geklärt, ob in dem Formular überhaupt Daten liegen, die einen Druck ermöglichen.

  1. 7    oColumns = oForm.Columns 

  2. 8    oDB = ThisComponent.Parent 

Der Zugriff auf die URL ist nicht vom Formular aus direkt möglich. Es muss auf den darüber liegenden Frame der Datenbank Bezug genommen werden.

  1. 9    stDir = Left(oDB.Location,Len(oDB.Location)-Len(ConvertToURL(oDB.Title))+8) 

Der Titel der Datenbank wird von der URL abgetrennt.

  1. 10    stDir = stDir & "Beispiel_Textfelder.ott" 

Die Vorlage wird aufgesucht und geöffnet

  1. 11    DIM args(0) AS NEW com.sun.star.beans.PropertyValue 

  2. 12    args(0).Name = "AsTemplate" 

  3. 13    args(0).Value = True 

  4. 14    oNewDoc = StarDesktop.loadComponentFromURL(stDir,"_blank",0,args) 

Die Textfelder werden eingelesen

  1. 15    oTextfields = oNewDoc.Textfields.createEnumeration 

  2. 16    DO WHILE oTextfields.hasMoreElements 

  3. 17       oTextfield = oTextfields.nextElement   

  4. 18       IF oTextfield.supportsService("com.sun.star.text.TextField.JumpEdit") THEN 

  5. 19          stColumnname = oTextfield.PlaceHolder 

Placeholder ist die Benennung für das Textfeld

  1. 20          IF oColumns.hasByName(stColumnname) THEN 

Wenn der Name des Textfeldes gleich dem Spaltennamen der Daten ist, die dem Formular zugrunde liegen, wird der Inhalt aus der Datenbank auf das Feld in dem Textdokument übertragen.

  1. 21             inIndex = oForm.findColumn(stColumnname) 

  2. 22             oTextfield.Anchor.String = oForm.getString(inIndex) 

  3. 23          END IF 

  4. 24       END IF 

  5. 25    LOOP 

  6. 26 END SUB 

Drucken über Tabellen in Writer

In Tabellenansichten zu drucken liegt bei den vielen Tabellen in einem Datenbankdokument ja nahe. Eine solche Konstruktion über den Report-Designer zu bewerkstelligen ist allerdings eine echte Geduldsprobe. Schon die Positionierung der Felder zusammen mit den Tabellenköpfen zu einem einheitlichen Bild ist wegen der internen Maße in Punkt statt in Zentimeter ein Problem. Eine Umrandungsfunktion für die Zellen gibt es nicht, eine senkrechte oder waagerechte Linie gibt es nur in der Farbe Schwarz als Haarlinie. Und ist so ein Dokument schließlich erstellt, dann dauert der Bericht deutlich länger als mit einer vorgefertigten Writer-Tabelle, weil ein Bericht aus vielen kleinen Tabellen zusammengesetzt wird. Der Druck über Tabellen im Writer19 wird im Folgenden aufeinander aufbauend an Beispielen erklärt.

Einfacher Druck von Text in Tabellen

 

Für den Druck wird eine Vorlage erstellt, die lediglich aus der Überschrift und einer leeren Tabelle besteht. Die Tabelle besteht in der Vorlage aus der Titelzeile und einer weiteren Zeile für den Inhalt. Die entsprechende Formatierung wird in der Vorlage vorgenommen. Der Tabelle wird dabei über Tabelleneigenschaften → Tabelle → Eigenschaften → Name ein fester Name zugewiesen. Über diesen festen Namen wird die Tabelle im Makro gefunden und mit Inhalt gefüllt.

  1. 1 SUB FillTable 

  2. 2    DIM oDB AS OBJECT, oNewDoc AS OBJECT, oTables AS OBJECT, oTable AS OBJECT 

  3. 3    DIM oRows AS OBJECT, oDatasource AS OBJECT, oConnection AS OBJECT 

  4. 4    DIM oSQL_Statement AS OBJECT, oResult AS OBJECT 

  5. 5    DIM stDir AS STRING, stSql AS STRING, stText AS STRING 

  6. 6    DIM i AS INTEGER, k AS INTEGER, inCols AS INTEGER 

Der Zugriff auf die Datenbankdatei erfolgt vom Formular aus. Die *.odb-Datei ist Parent des Formulardokumentes. Anschließend wird daraus der Pfad ermittelt, in dem die Vorlagendatei liegt. In diesem Beispiel liegt die Vorlage im gleichen Pfad wie Datenbankdatei.

  1. 7    oDB = ThisComponent.Parent 

  2. 8    stDir = Left(oDB.Location,Len(oDB.Location)-Len(ConvertToURL(oDB.Title))+8) 

Der Titel der Datenbank wird von der URL abgetrennt. Die Längenermittlung mit ConvertToURL ist notwendig, falls der Titel Leerzeichen enthält.

  1. 9    stDir = stDir & "PrintStart.ott" 

Die Vorlage wird geöffnet und das Writer-Dokument damit erstellt.

  1. 10    DIM args(0) AS NEW com.sun.star.beans.PropertyValue 

  2. 11    args(0).Name = "AsTemplate" 

  3. 12    args(0).Value = True 

  4. 13    oNewDoc = StarDesktop.loadComponentFromURL(stDir,"_blank",0,args) 

Die Vorlage enthält eine Tabelle, die den Namen «Printout_Addresses» hat. Diese Tabelle besteht aus 2 Zeilen - der Titelzeile und der ersten Zeile für den Inhalt

  1. 14    oTables = oNewDoc.getTextTables 

  2. 15    oTable = oTables.getByName("Printout_Addresses") 

  3. 16    inCols = oTable.Columns.Count 

Mit der Variablen i wird die Inhaltszeile angesteuert. Die Zeilenzählung bei Tabellen beginnt mit der Zeile 0.

  1. 17    i = 1 

  2. 18    oDatasource = ThisComponent.Parent.CurrentController 

  3. 19    If NOT (oDatasource.isConnected()) THEN 

  4. 20       oDatasource.connect() 

  5. 21    END IF 

  6. 22    oConnection = oDatasource.ActiveConnection() 

  7. 23    oSQL_Statement = oConnection.createStatement() 

  8. 24    stSql = "SELECT * FROM ""tbl_Adressen""" 

  9. 25    oResult = oSQL_Statement.executeQuery(stSql) 

Die Daten aus "tbl_Adressen" werden ausgelesen. Anschließend werden die Inhalte direkt in die entsprechenden Zellen der Tabelle als Text eingefügt.

  1. 26    WHILE oResult.next 

  2. 27       FOR k = 0 TO inCols-1 

  3. 28          stText = oResult.getString(k+1)         

  4. 29          oTable.getCellByPosition(k,i).setString(stText) 

  5. 30       NEXT 

Für jeden neuen Datensatz muss eine zusätzliche Inhaltszeile erstellt werden.

  1. 31       oRows = oTable.getRows() 

  2. 32       oRows.insertByIndex(oRows.getCount(),1)  

  3. 33       i = i + 1 

  4. 34    WEND 

Durch die Schleife bleibt zum Schluss eine leere Inhaltszeile übrig. Diese Zeile könnte weiter genutzt werden (z.B. für eine Summierung bei Rechnungen). Hier ist die Zeile nicht nötig und wird deswegen entfernt.

  1. 35    oRows.removeByIndex(oRows.getCount()-1,1) 

  2. 36 END SUB 

Tabellendruck mit formatierten Zellen

 

Bereits mit einer kleinen Erweiterung lässt sich das entsprechende Format für die Spalten erstellen. Hierzu wird in der Vorlage die Spalte für das Datum (Zelle A2) als Datum formatiert, die Spalte für den Betrag (Zelle C2) als Währungsfeld formatiert. Außerdem wird noch die Spalte für den Betrag rechtsbündig gesetzt.

Alle weiteren durch das Makro hinzugefügten Zeilen übernehmen die Formatierung aus der vorhergehenden Zeile. Der Makro-Code wird um einige Zeilen ergänzt.

  1. 24    stSql = "SELECT ""Datum"", ""Erläuterung"", ""Betrag"" FROM ""tbl_Konto""" 

  2. 25    arFormat = array("Date","String","Number") 

  3. 26    oResult = oSQL_Statement.executeQuery(stSql) 

Die Daten aus "tbl_Konto" werden als Text ausgelesen. Anschließend werden die Inhalte in Abhängigkeit vom gewünschten Format (siehe das Array in Zeile 25) als Wert oder Text eingefügt. Die Werte für das Datum und die Zahl werden über die Funktionen CDate und Val berechnet. Das Einfügen der Werte erfolgt im Gegensatz zum Text mit setValue.

  1. 27    WHILE oResult.next 

  2. 28       FOR k = 0 TO inCols-1 

  3. 29          stText = oResult.getString(k+1) 

  4. 30          SELECT CASE arFormat(k) 

  5. 31             CASE "Date" 

  6. 32                oTable.getCellByPosition(k,i).setValue(CDate(stText)) 

  7. 33             CASE "Number" 

  8. 34                oTable.getCellByPosition(k,i).setValue(Val(stText)) 

  9. 35             CASE "String" 

  10. 36                oTable.getCellByPosition(k,i).setString(stText) 

  11. 37          END SELECT 

  12. 38       NEXT 

  13. 39       oRows = oTable.getRows() 

  14. 40       oRows.insertByIndex(oRows.getCount(),1)  

  15. 41       i = i + 1 

  16. 42    WEND 

Tabellendruck mit Bildern in der Tabelle

 

Das Einfügen von Bildern aus der Datenbank in die Tabelle ist etwas aufwändiger. Sind die Bilder in die Tabelle der Datenbank eingelesen, so müssen sie erst einmal als externe Bilder ausgelesen werden. Danach erfolgt dann das Einfügen des Bildes in die Tabelle. Damit das Bild auch so wie im Screenshot positioniert wird muss zum Schluss noch die Verankerung von der Verankerung Am Absatz zur Verankerung Als Zeichen geändert werden.

  1. 28       FOR k = 0 TO inCols-1 

  2. 29          SELECT CASE arFormat(k) 

  3. 30             CASE "Date" 

  4. 31                stText = oResult.getString(k+1) 

  5. 32                oTable.getCellByPosition(k,i).setValue(CDate(stText)) 

  6. 33             CASE "Number" 

  7. 34                stText = oResult.getString(k+1) 

  8. 35                oTable.getCellByPosition(k,i).setValue(Val(stText)) 

  9. 36             CASE "String" 

  10. 37                stText = oResult.getString(k+1) 

  11. 38                oTable.getCellByPosition(k,i).setString(stText) 

  12. 39             CASE "Image" 

  13. 40                stText = oResult.getString(k+1) 

  14. 41                ImportImage(stText,oNewDoc,oTable.getCellByPosition(k,i)) 

  15. 42             CASE "ImageIntern" 

  16. 43                oStream = oResult.getBinaryStream(k+1) 

  17. 44                ImportImageIntern(oStream,oNewDoc,oTable.getCellByPosition(k,i)) 

  18. 45          END SELECT 

  19. 46       NEXT 

in der Hauptprozedur werden lediglich zwei zusätzliche Formattypen eingefügt.

Ist in der Datenbank lediglich der Link zu dem Bild abgespeichert, dann wird aus der Abfrage ein Text ausgelesen und dieser Link zusammen mit dem Objekt des Writer-Dokumentes und der genauen Position an die Prozedur ImportImage weiter gereicht.

Handelt es sich dagegen um ein Bild, das in die Datenbank eingelesen wurde, so muss der Inhalt über getBinaryStream ausgelesen werden. Dieser Stream wird genutzt, um ihn als Bilddatei im temporären Pfad von LibreOffice ab zu speichern.

  1. 1 SUB ImportImageIntern(oStream,oNewDoc,oPos) 

  2. 2    DIM oPath AS OBJECT, oSimpleFileAccess AS OBJECT 

  3. 3    DIM stPath AS STRING 

Das ausgelesene Bild wird im temporären Pfad unter dem Namen DbFile zwischengespeichert. Anschließend wird die Prozedur aufgerufen, die das Bild in das Writer-Dokument übernimmt.

  1. 4    oPath = createUnoService("com.sun.star.util.PathSettings") 

  2. 5    stPath = oPath.Temp & "/DbFile" 

  3. 6    oSimpleFileAccess = createUnoService("com.sun.star.ucb.SimpleFileAccess") 

  4. 7    oSimpleFileAccess.writeFile(stPath, oStream) 

  5. 8    ImportImage(stPath,oNewDoc,oPos) 

  6. 9 END SUB 

Die Prozedur zum Einfügen des Bildes in die Tabelle benötigt die Informationen über den Pfad zum Bild, das Writer-Dokument und die genaue Position innerhalb der Tabelle.

  1. 1 SUB ImportImage(stPath,oNewDoc,oPos) 

  2. 2    DIM oCursor AS OBJECT, oTextRange AS OBJECT, oDocCtrl AS OBJECT 

  3. 3    DIM oDispatcher AS OBJECT, oGraphic AS OBJECT 

Der sichtbare Cursor wird in die Tabellenzelle gesetzt. Zuerst wird die Position am Ende des Absatzes gewählt und dann der Cursor positioniert.

  1. 4    oCursor = oNewDoc.CurrentController.ViewCursor 

  2. 5    oTxtRange = oPos.getEnd 

  3. 6    oCursor.goToRange(oTxtRange,False) 

Um das Bild einzufügen muss auf den den Controller zugegriffen werden. Anschließend wird über den Dispatcher mit uno:InsertGraphic das Bild eingelesen. Die wichtigsten Eigenschaften sind hier benannt: Der Pfad zum Bild und dass das Bild nicht als Verknüpfung eingefügt wird. Prinzipiell ist auch das Einlesen von Bildern über die Drawpage der Seite möglich. Nur nimmt der Dispatcher hier sehr viel Arbeit ab: Das Bild wird korrekt positioniert, die Größe des Bildes wird maximal der Spaltenbreite angepasst und die Größenverhältnisse werden automatisch beibehalten.

  1. 7    oDocCtrl   = oNewDoc.CurrentController.Frame 

  2. 8    oDispatcher = createUnoService("com.sun.star.frame.DispatchHelper") 

  3. 9    DIM args(1) AS NEW com.sun.star.beans.PropertyValue 

  4. 10    args(0).Name = "FileName" 

  5. 11    args(0).Value = stPath 

  6. 12    args(1).Name = "AsLink" 

  7. 13    args(1).Value = false 

  8. 14    oDispatcher.executeDispatch(oDocCtrl, ".uno:InsertGraphic", "", 0, args()) 

Auf die gerade eingefügte Grafik wird über den Index zugegriffen, damit der Anker gesetzt werden kann. Die Standardverankerung an den Absatz erzeugt sonst eine leere Zeile unterhalb des Bildes, so dass der Abstand zur Tabellenzeile nach unten zu groß wird.

  1. 15    oGraphic =
         
    oNewDoc.getGraphicObjects.getByIndex(oNewDoc.getGraphicObjects.Count - 1) 

Mit AnchorType = 1 wird die Verankerung als Zeichen gesetzt. Der Cursor wird direkt nach dem Bild positioniert und erzeugt keinen zusätzlichen Rand.

  1. 16    oGraphic.AnchorType = 1 

  2. 17 END SUB 

Hier die verschiedenen möglichen Ankertypen als Zusatzinformation:

Siehe: https://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1text.html#a470b1caeda4ff15fee438c8ff9e3d834

Rechnungen mit Übertrag im Tabellendruck

 

Eine Rechnung mit Übertrag auszudrucken ist über den Report-Designer nur möglich, wenn die Zeilen nicht automatisch anwachsen. Dann kann genau vorausberechnet werden, an welcher Stelle der Seitenumbruch erfolgt und wann dann ein Übertrag eingeblendet werden muss. Sobald die Zeilen der Tabelle sich automatisch an den Textinhalt anpassen und damit größer werden können funktioniert ein Ausdruck wie im obigen Screenshot nicht mehr mit dem Report-Designer.

Die ursprüngliche Prozedur für das Einfügen von Inhalt in eine Tabelle wird erweitert. Zuerst wird das Linienformat für die Absetzung von Schlusssumme und eventuell Überträgen (TopBorder, BottomBorder) erstellt.

  1. 18    oBorder = CreateUnoStruct("com.sun.star.table.BorderLine2") 

  2. 19    oBorder.Color = 0 

  3. 20    oBorder.LineWidth = 1 

  4. 21    WHILE oResult.next 

  5. 22       FOR k = 0 TO inCols-1 

  6. 23          stText = oResult.getString(k+1) 

  7. 24          SELECT CASE arFormat(k) 

  8. 25             CASE "Number" 

  9. 26                doValue = Val(stText) 

  10. 27                oTable.getCellByPosition(k,i).setValue(doValue) 

In der 4. Spalte soll die Aufsummierung erfolgen:

  1. 28                IF k = 3 THEN 

Die Summe des vorhergehenden Durchgangs wird in doCarryOver gespeichert. Der gerade neu ausgelesene Wert wird zu der vorhergehenden Summe addiert und in doVal gespeichert.

  1. 29                   doCarryOver = doVal 

  2. 30                   doVal = doVal + doValue 

  3. 31                END IF 

  4. 32             CASE "String" 

  5. 33                oTable.getCellByPosition(k,i).setString(stText) 

  6. 34          END SELECT 

  7. 35       NEXT 

  8. 36       oRows = oTable.getRows() 

  9. 37       oRows.insertByIndex(oRows.getCount(),1) 

  10. 38       i = i + 1 

Nachdem eine neue Tabellenzeile eingefügt wurde muss der sichtbare Cursor in diese Zeile gesetzt werden. Für die neue Zeile wird die Seitenzahl bestimmt. Ist die Seitenzahl größer als die vorhergehende Seitenzahl, so muss ein Übertrag eingefügt werden. Der Übertrag muss vor der vorhergehenden Zeile erfolgen.

  1. 39       oPos = oTable.getCellByPosition(0,i) 

  2. 40       oTxtRange = oPos.getEnd 

  3. 41       oCursor.goToRange(oTxtRange,False) 

  4. 42       IF oCursor.Page > inPageStart THEN 

Es werden 2 leere Zeile oberhalb des letzten Eintrags eingefügt. Beide Zeilen erhalten den Eintrag "Übertrag:" in der 3. Spalte und den Wert für den Übertrag in der 4. Spalte. Für die entsprechenden Zellen wird das Absatzformat "Übertrag" gesetzt. Achtung: Die Schriftart für das Absatzformat "Übertrag" darf für die Positionierung nicht größer sein als die für den Tabelleninhalt.

  1. 43          FOR ink = 1 TO 2 

  2. 44             oRows = oTable.getRows() 

  3. 45             oRows.insertByIndex(oRows.getCount()-2,1) 

  4. 46             oTable.getCellByPosition(2,i-1).setString("Übertrag:") 

  5. 47             oTable.getCellByPosition(3,i-1).setValue(doCarryOver) 

  6. 48             oTable.getCellByPosition(2,i-1).getEnd.ParaStyleName = "Übertrag"         

  7. 49             oTable.getCellByPosition(3,i-1).getEnd.ParaStyleName = "Übertrag" 

  8. 50             i = i + 1 

  9. 51          NEXT 

Der Cursor wird jetzt von der unteren leeren Tabellenzeile 2 Zeilen nach oben auf die 2. Zeile für den Übertrag gesetzt. Die 2. Zeile für den Übertrag darf nicht auf der gleichen Seite wie die 1. Zeile für den Übertrag stehen. Die 2. Zeile kann zufällig auf der gleichen Seite wie die erste Zeile stehen, wenn der letzte inhaltliche Eintrag einen Absatzumbruch enthält. In dem Fall nimmt die Tabellenzeile für den letzten Eintrag eine größere Höhe in Anspruch. Die 1. Zeile für den Übertrag steht auf inPageStart.

  1. 52          oCursor.goUp(2,False) 

  2. 53          DO WHILE oCursor.Page = inPageStart 

Es werden so lange zwischen der 1. Zeile für den Übertrag und der 2. Zeile für den Übertrag Tabellenzeilen eingefügt, bis die 2. Zeile auf der Folgezeile steht.

  1. 54             oRows = oTable.getRows() 

  2. 55             oRows.insertByIndex(oRows.getCount()-3,1) 

  3. 56             oPos = oTable.getCellByPosition(0,i-1) 

  4. 57             oTxtRange = oPos.getEnd 

  5. 58             oCursor.gotoRange(oTxtRange,false) 

  6. 59             i = i + 1 

  7. 60          LOOP 

Die jetzt letzte Seite wird als inStartPage zur neuen Vergleichsseite für den nächsten Übertrag.

  1. 61          inPageStart = oCursor.Page 

  2. 62       END IF 

  3. 63    WEND 

Durch die Schleife bleibt eine leere Inhaltszeile übrig. Diese Zeile wird für die Summierung genutzt. Wie beim Übertrag erfolgen hier die Einträge und die Formatierungen. Zusätzlich wird die oben stehende Umrandung als Abgrenzung für den Gesamtpreis in die Zellen eingefügt.

  1. 64    oTable.getCellByPosition(2,i).setString("Gesamt:") 

  2. 65    oTable.getCellByPosition(3,i).setValue(doVal) 

  3. 66    oTable.getCellByPosition(2,i).getEnd.ParaStyleName = "Übertrag" 

  4. 67    oTable.getCellByPosition(3,i).getEnd.ParaStyleName = "Übertrag" 

  5. 68    oTable.getCellByPosition(2,i).TopBorder = oBorder 

  6. 69    oTable.getCellByPosition(3,i).TopBorder = oBorder 

  7. 70 END SUB 

Aufruf von Anwendungen zum Öffnen von Dateien

Mit dieser Prozedur kann durch einen Mausklick in ein Textfeld ein Programm aufgerufen werden, das im eigenen Betriebssystem mit der Dateinamensendung verbunden ist. Damit werden auch Links ins Internet oder sogar das Öffnen des Mailprogramms mit einer bestimmten Mailadresse möglich, die gerade in der Datenbank gespeichert wurde.

Siehe zu diesem Abschnitt auch die Beispieldatenbank «Beispiel_Mailstart_Dateiaufruf.odb»20
  1. 1 SUB Link_oeffnen 

  2. 2    DIM oDoc AS OBJECT 

  3. 3    DIM oDrawpage AS OBJECT 

  4. 4    DIM oForm AS OBJECT 

  5. 5    DIM oFeld AS OBJECT 

  6. 6    DIM oShell AS OBJECT 

  7. 7    DIM stFeld AS STRING 

  8. 8    oDoc = thisComponent 

  9. 9    oDrawpage = oDoc.Drawpage 

  10. 10    oForm = oDrawpage.Forms.getByName("Formular") 

  11. 11    oFeld = oForm.getByName("Adresse") 

Aus dem benannten Feld wird der Inhalt ausgelesen. Dies kann eine Webadresse, beginnend mit 'http://', eine Mailadresse mit einem '@' oder ein einfaches Dokument sein, das durch eine entsprechende Pfadangabe aufgesucht werden soll (z.B. externe Bilder, *.pdf-Dateien, Tondokumente …).

Der Verwendung von Sonderzeichen in den Pfaden und Dateinamen sollte hier möglichst vermieden werden. Die Links sind dann teilweise auf diese Art nicht mehr aufrufbar.

  1. 12    stFeld = oFeld.Text 

  2. 13    IF stFeld = "" THEN 

  3. 14       EXIT SUB 

  4. 15    END IF 

Ist das Feld leer, so soll das Makro sofort enden. Bei der Eingabe passiert es ja sehr oft, dass Felder mit der Maus aufgesucht werden. Ein Mausklick in das Feld, um dort zum ersten Mal schreiben zu können, soll aber noch nicht das Makro ausführen.

Jetzt wird gesucht, ob in dem Feld ein '@' enthalten ist. Dies deutet auf eine E-Mail-Adresse hin. Es soll also das Mailprogramm gestartet werden und eine Mail an diese Mailadresse gesandt werden.

  1. 16    IF InStr(stFeld,"@") THEN 

  2. 17       stFeld = "mailto:"+stFeld 

Ist kein '@' vorhanden, so wird der Begriff so konvertiert, dass die Datei im Dateisystem gefunden werden kann. Steht ein 'http://' am Anfang, so wird bei dieser Funktion nicht im Dateisystem, sondern über den Webbrowser direkt im Internet nachgesehen. Ansonsten beginnt der erstellte Pfad anschließend mit dem Begriff 'file:///'

  1. 18    ELSEIF InStr(stFeld,"http") = 0 THEN 

  2. 19       stFeld = convertToUrl(stFeld) 

  3. 20    ELSE 

  4. 21    END IF 

Bei der Verwendung von Sonderzeichen in URLs kann es sinnvoll sein, die Konvertierung für den Pfad zu unterlassen. Der Shell-Befehl funktioniert auch mit der systeminternen Schreibweise. Hier müsste dann allerdings separat für die Endungen 'http://' und 'https://' eine Konvertierung vorgenommen werden. Jetzt wird das Programm aufgesucht, das in dem eigenen Betriebssystem mit der entsprechenden Dateiendung verbunden ist. Bei dem Stichwort 'mailto:' ist dies das Mailprogramm, bei 'http://' der Webbrowser und bei allen anderen ist die Entscheidung des Systems mit den Endungen der Datei verbunden.

  1. 22    oShell = createUnoService("com.sun.star.system.SystemShellExecute") 

  2. 23    oShell.execute(stFeld,,0) 

  3. 24 END SUB 

Aufruf eines Mailprogramms mit Inhaltsvorgaben

Eine Erweiterung des vorhergehenden Beispiels zum Programmaufruf stellt dieser Aufruf eines Mailprogramms mit Vorgaben in der Betreffzeile und inhaltlichen Vorgaben dar.

Siehe auch zu diesem Abschnitt die Beispieldatenbank «Mailstart_Dateiaufruf.odb»

Der Mailaufruf erfolgt mit 'mailto:Empfänger?subject= &body= &cc= &bcc= '. Die letzten beiden Eingaben sind im Formular allerdings nicht aufgeführt. Anhänge kommen in der Definition von 'mailto' nicht vor. Manchmal funktioniert allerdings 'attachment='.

  1. 1 SUB Mail_Aufruf 

  2. 2    DIM oDoc AS OBJECT 

  3. 3    DIM oDrawpage AS OBJECT 

  4. 4    DIM oForm AS OBJECT 

  5. 5    DIM oFeld1 AS OBJECT 

  6. 6    DIM oFeld2 AS OBJECT 

  7. 7    DIM oFeld3 AS OBJECT 

  8. 8    DIM oFeld4 AS OBJECT 

  9. 9    DIM oShell AS OBJECT 

  10. 10    DIM stFeld1 AS STRING 

  11. 11    DIM stFeld2 AS STRING 

  12. 12    DIM stFeld3 AS STRING 

  13. 13    DIM stFeld4 AS STRING 

  14. 14    oDoc = thisComponent 

  15. 15    oDrawpage = oDoc.Drawpage 

  16. 16    oForm = oDrawpage.Forms.getByName("Formular") 

  17. 17    oFeld1 = oForm.getByName("E_Mail_Adresse") 

  18. 18    oFeld2 = oForm.getByName("E_Mail_Betreff") 

  19. 19    oFeld3 = oForm.getByName("E_Mail_Inhalt") 

  20. 20    stFeld1 = oFeld1.Text 

  21. 21    IF stFeld1 = "" THEN 

  22. 22       msgbox "Keine Mailadresse vorhanden." & CHR(13) &
           
    "Das Mailprogramm wird nicht aufgerufen" , 48, "Mail senden" 

  23. 23       EXIT SUB 

  24. 24    END IF 

Die Konvertierung zu URL ist notwendig, damit Sonderzeichen und Zeilenumbrüche den Aufruf nicht stören. Dabei wird allerdings auch der Begriff 'file:///' vorangestellt. Diese 8 Zeichen zu Beginn werden nicht mit übernommen. Die Umwandlung ist bei der Verwendung von SimpleSystemMail/SimpleCommandMail (siehe den folgenden Hinweis) nicht erforderlich.

  1. 25    stFeld2 = Mid(ConvertToUrl(oFeld2.Text),9) 

  2. 26    stFeld3 = Mid(ConvertToUrl(oFeld3.Text),9) 

Im Gegensatz zum einfachen Programmaufruf werden hier Details des Mailaufrufes über den Aufruf des Mailprogramms mitgegeben.

  1. 27    oShell = createUnoService("com.sun.star.system.SystemShellExecute") 

  2. 28    oShell.execute("mailto:" + stFeld1 + "?subject=" + stFeld2 + "&body=" +
         stFeld3,,0) 

  3. 29 END SUB 

Das Versenden von Mails mit Hilfe des Mailprogramms kann auch mit folgendem Code erfolgen. Ab LO 4.2 kann hier auch über das Attribut Body der Inhalt der Mail mit eingefügt werden. Dieser Code ermöglicht außerdem das Anfügen von Anhängen. Für mehrere Anhänge muss einfach das Array erweitert werden. Auch Adressen im CC sowie im BCC werden in ein Array geschrieben.
  1. 27    DIM attachs(0) 

  2. 28    IF GetGuiType() = 1 THEN 

  3. 29       oMailer =
           
    createUnoService("com.sun.star.system.SimpleSystemMail")
           
    ' Sonst Linux/Mac 

  4. 30       ELSE 

  5. 31       oMailer =
           
    createUnoService("com.sun.star.system.SimpleCommandMail") 

  6. 32    END IF 

  7. 33    oMailProgramm = oMailer.querySimpleMailClient() 

  8. 34    oNeueNachricht = oMailProgramm.createSimpleMailMessage() 

  9. 35    oNeueNachricht.setRecipient(stFeld1) 

  10. 36    oNeueNachricht.setSubject(stFeld2) 

  11. 37    oNeueNachricht.Body = stFeld3 

  12. 38    attachs(0) = "file:///..." 

  13. 39    oNeueNachricht.setAttachement(attachs()) 

  14. 40    oMailprogramm.sendSimpleMailMessage(oNeueNachricht, 0 ) 

  15. 41 END SUB 

 

Aufruf einer Kartenansicht zu einer Adresse

Eine Datenbank enthält lauter Adressen. Jetzt soll zu einer Adresse aufgezeigt werden, in welcher Umgebung denn das Haus liegt. Die folgende Prozedur «MapPosition» wird mit einem Button gestartet, der in dem gleichen Formular liegt, in dem die Angaben zur Adresse verzeichnet sind.21
  1. 1 SUB MapPosition(oEvent AS OBJECT) 

  2. 2    DIM oForm AS OBJECT, oShell AS OBJECT 

  3. 3    DIM i AS INTEGER 

  4. 4    DIM stLink AS STRING, stTag AS STRING 

  5. 5    DIM arFields() 

  6. 6    stTag = oEvent.Source.Model.Tag 

  7. 7    oForm = oEvent.Source.Model.Parent 

  8. 8    arFields = Split(stTag,",") 

In den Zusatzinformationen des Buttons sind, durch Kommas getrennt, die Namen der Felder aufgeführt, die zusammen die Adresse ergeben. Dies sind in der Beispieldatenbank comPLZOrt,txtStraße. Das erste Feld ist ein Kombinationsfeld, das die Postleitzahl und den Ort enthält, das zweite Feld enthält die Straße und die Hausnummer. Die beiden Feldbezeichnungen werden voneinander getrennt und in ein Array geschrieben.

  1. 9    FOR i = LBound(arFields) TO UBound(arFields) 

  2. 10       IF stLink = "" THEN 

  3. 11          stLink = oForm.getByName(arFields(i)).CurrentValue 

  4. 12       ELSE 

  5. 13          stLink = stLink & "+" & oForm.getByName(arFields(i)).CurrentValue 

  6. 14       END IF 

  7. 15    NEXT i 

Die Inhalte der beiden Felder werden ausgelesen und mit einem + verbunden in der Variablen stLink gespeichert. Dieser Suchstring wird jetzt in den Link für nominatim.openstreetmap.org eingefügt. Beim Einfügen wird darauf geachtet, dass auch die Leerzeilen in dem String mit + ausgefüllt werden. Dies geschieht, indem der String einfach einmal an den Leerzeichen durch Split aufgetrennt wird und dann wieder über Join mit einem + die Teile verbunden werden.

  1. 16    IF stLink <> "" THEN 

  2. 17       stLink = "https://nominatim.openstreetmap.org/search.php?q=" &
           
    Join(Split(stLink),"+") & "&polygon_geojson=1&viewbox=" 

  3. 18       oShell = createUnoService("com.sun.star.system.SystemShellExecute") 

  4. 19       oShell.execute(stLink,,0) 

  5. 20    END IF 

  6. 21 END SUB 

Die weiteren Elemente des Links sind lediglich aus dem Link entstanden, den die Website bei direkter Nutzung der Suchfunktion angibt. Wie in den vorhergehenden Beispielen wird dieser Link über die SystemShell gestartet. Dort wird dann der Browser aufgerufen, der bei einer in der Karte verzeichneten Adresse die auch direkt findet.22

Mauszeiger ändern

Manchmal erscheint es sinnvoll, die Mauszeiger so anzupassen, dass sie Zusatzinformationen zur Verwendung des Inhaltes eines Feldes geben.

Änderung beim Überfahren eines Links

Im Internet üblich, bei Base nachgebaut: Der Mauszeiger fährt über ein Textfeld und verändert seine Form zu einer zeigenden Hand. Der enthaltene Text kann jetzt noch in den Eigenschaften des Feldes zu der Farbe Blau und unterstrichen geändert werden – schon ist der Eindruck eines Links aus dem Internet perfekt. Jeder Nutzer erwartet nach einem Klick, dass sich ein externes Programm öffnet.

Siehe auch zu diesem Abschnitt die Beispieldatenbank «Mailstart_Dateiaufruf.odb»23.

Diese kurze Prozedur sollte mit dem Ereignis 'Maus innerhalb' des Textfeldes verbunden werden.

  1. 1 SUB Mauszeiger(oEvent AS OBJECT) 

  2. 2    REM Siehe auch Standardbibliotheken: Tools → ModuleControls → SwitchMousePointer 

  3. 3    DIM oPointer AS OBJECT 

  4. 4    oPointer = createUnoService("com.sun.star.awt.Pointer") 

  5. 5    oPointer.setType(27) 'Typen in com.sun.star.awt.SystemPointer 

  6. 6    oEvent.Source.Peer.SetPointer(oPointer) 

  7. 7 END SUB 

Änderung bei gedrückter Strg-Taste und Mausklick

  1. 1 SUB Mauszeiger(oEvent AS OBJECT) 

  2. 2    DIM oPointer AS OBJECT 

  3. 3    oPointer = createUnoService("com.sun.star.awt.Pointer") 

  4. 4    IF oEvent.Modifiers = 2 THEN  

  5. 5       'KeyModifier (ohne: 0 | Shift: 1 | Ctrl: 2 | Alt: 4 …),
            Typen in com.sun.star.awt.KeyModifier
     

  6. 6       oPointer.setType(0) 'Typen in com.sun.star.awt.SystemPointer 

  7. 7    ELSE 

  8. 8       oPointer.setType(3) 

  9. 9    END IF 

  10. 10    oEvent.Source.Peer.SetPointer(oPointer) 

  11. 11 END SUB 

Über den KeyModifier wird ermittelt, ob eine der entsprechenden Tasten zusätzlich zu dem Mausklick an der Auslösung des Makros beteiligt war. Hier wurde mit '2' als zusätzliche Taste STRG ausgewählt. Wird STRG nicht gedrückt, so wird auf den Textcursor geschaltet.

Formulare ohne Symbolleisten präsentieren

Neunutzer von Base sind häufig irritiert, dass z.B. eine Menüleiste existiert, diese aber im Formular so gar nicht verfügbar ist. Diese Menüleisten können auf verschiedene Arten ausgeblendet werden. Am erfolgreichsten unter allen LO-Versionen sind die beiden im Folgenden vorgestellten Vorgehensweisen.

Fenstergrößen und Symbolleisten werden in der Regel über ein Makro beeinflusst, das in einem Formulardokument unter Extras → Anpassen → Ereignisse → Dokument öffnen gestartet wird. Gemeint ist hier das Dokument, nicht ein einzelnes Haupt- oder Unterformular.

Formulare ohne Symbolleisten in einem Fenster

Ein Fenster lässt sich in der Größe variieren. Über den entsprechenden Button lässt es sich auch schließen. Diese Aufgaben übernimmt der Window-Manager des jeweiligen Betriebssystems. Lage und Größe des Fensters auf dem Bildschirm kann beim Start über ein Makro mitgegeben werden.

  1. 1 SUB Symbolleisten_Ausblenden 

  2. 2    DIM oFrame AS OBJECT 

  3. 3    DIM oWin AS OBJECT 

  4. 4    DIM oLayoutMng AS OBJECT 

  5. 5    DIM aElemente() 

  6. 6    oFrame = StarDesktop.getCurrentFrame() 

Diese Startvariante ist für eigenständige Formulare geeignet, nicht aber für Formulare in der Basedatei. In der Basedatei würde dort das Hauptfenster, nicht aber die dem untergeordneten Formulare ohne Symbolleiste versehen. Dort erfolgt der Start über

  1. 1 SUB Symbolleisten_Ausblenden(oEvent AS OBJECT) 

  2. 2    DIM oFrame AS OBJECT 

  3. 3    DIM oWin AS OBJECT 

  4. 4    DIM oLayoutMng AS OBJECT 

  5. 5    DIM aElemente() 

  6. 6    oFrame = oEvent.Source.CurrentController.Frame 

Der Titel für das Formular wird in der Titelleiste des Fensters angezeigt.

  1. 7    oFrame.setTitle "Mein Formular" 

  2. 8    oWin = oFrame.getContainerWindow() 

Das Fenster wird auf die maximale Größe eingestellt. Dies entspricht nicht dem Vollbildmodus, da z.B. eine Kontrollleiste noch sichtbar ist und das Fenster eine Titelleiste hat, über die die Größe des Fensters geändert und das Fenster geschlossen werden kann.

  1. 9    oWin.IsMaximized = true 

Es besteht auch die Möglichkeit, das Fenster in einer ganz bestimmten Größe und mit einer festen Position darzustellen. Dies würde mit 'oWin.setPosSize(0,0,600,400,15)' geschehen. Hier wird das Fenster an der linken oberen Ecke des Bildschirms mit einer Breite von 600 Punkten und einer Höhe von 400 Punkten dargestellt. Die letzte Ziffer weist darauf hin, dass alle Punkte angegeben wurden. Sie wird als 'Flag' bezeichnet. Das 'Flag' wird aus den folgenden Werten über eine Summierung berechnet: x=1, y=2, Breite=4, Höhe=8. Da x, y, Breite und Höhe angegeben sind, hat das 'Flag' die Größe 1 + 2 + 4 + 8 = 15.

Da es inzwischen auch viele verschiedene Bildschirmauflösungen gibt und die Fenstergröße eventuell angepasst werden soll, hier die Ermittlung der Bildschirmauflösung in dpi: 'oWin.Info.PixelPerMeterX * 2.54/100'. Die Auflösung wird genauso für die y-Achse angegeben ist aber vermutlich immer gleich.

  1. 10    oLayoutMng = oFrame.LayoutManager 

  2. 11    aElemente = oLayoutMng.getElements() 

  3. 12    FOR i = LBound(aElemente) TO UBound(aElemente) 

  4. 13       IF aElemente(i).ResourceURL =
            "private:resource/toolbar/formsnavigationbar" THEN 

  5. 14       ELSE 

  6. 15          oLayoutMng.hideElement(aElemente(i).ResourceURL) 

  7. 16       END IF 

  8. 17    NEXT 

  9. 18    ThisComponent.CurrentController.Sidebar.Visible = False 

  10. 19    ThisComponent.CurrentController.ViewSettings.ZoomValue = 200 

  11. 20    ThisComponent.CurrentController.ViewSettings.ShowRulers = False 

  12. 21    ThisComponent.CurrentController.ViewSettings.ShowParaBreaks = False 

  13. 22 END SUB 

Wenn es sich um die Navigationsleiste handelt, soll nichts geschehen. Das Formular soll schließlich bedienbar bleiben, wenn nicht das Kontrollfeld für die Navigationsleiste eingebaut und die Navigationsleiste sowieso ausgeblendet wurde. Nur wenn es sich nicht um die Navigationsleiste handelt, soll die entsprechende Leiste verborgen werden. Deswegen erfolgt zu dieser Bedingung keine Aktion. Neben den Symbolleisten wird anschließend noch die Seitenleiste unsichtbar gemacht, in diesem Beispiel dann auch noch der ZoomValue eingestellt, die Lineale links und oben ausgeblendet und die Absatzmarke nicht mehr angezeigt, falls sonst im Writer eben Formatierungszeichen angezeigt werden.

Bei unterschiedlichen Bildschirmen kann es passieren, dass der voreingestellte ZoomValue nicht die gleiche Formularansicht wiedergibt. Hier kann das Auslesen von Bildschirmbreite und Bildschirmhöhe helfen:

  1. 18    ' Ausschnitt mit allen Elementen des Formulars: 1487*765  

  2. 19    ' bei 96 dpi, 3779 PixelPerMeter – gezoomte Bildschirme haben mehr dpi 

  3. 20    inDpiX = 1440 \ TwipsPerPixelX() 

  4. 21    inDpiY = 1440 \ TwipsPerPixelY() 

  5. 22    inx = Int(oWin.Info.Width * 100 * 96 / (1487 * inDpiX)) 

  6. 23    iny = Int(oWin.Info.Height * 100 * 96 / (765 * inDpiY)) 

  7. 24    IF inx < iny THEN 

  8. 25       inZoom = inx 

  9. 26    ELSE 

  10. 27       inZoom = iny 

  11. 28    END IF 

Über einen Screenshot wurde die Größe des Formulars (links oben unterhalb der Symbolleisten beginnen bis rechts unten incl. des letzten Elementes; ggf. auch die Navigationsleiste mit einbeziehen) in Pixeln bei einem ZoomValue von 100 ermittelt. Das Verhältnis von tatsächlicher Bildschirmbreite zu Formularbreite bzw. Bildschirmhöhe zu Formularhöhe soll den neuen Prozentwert für den ZoomValue bestimmen. Damit auch das gesamte Formular auf den Bildschirm passt soll der kleinere Wert übernommen werden. Das Ganze wird in die vorhergehende Prozedur nach der Deklaration von oWin eingebaut. Statt der '200' für den ZoomValue steht dort dann eben inZoom. Alle hier auftauchenden Zahlenvariablen sind Ganzzahlen im Integer-Format.

Bei Addons im Bereich der Symbolleisten wird die Eigenschaft ResourceURL leider etwas hinter einer Integer-Variablen versteckt. Hier ist dann zur Bestimmung der URL der folgende Weg notwendig:

  1. 16    obj = aElemente(i) 

  2. 17    invoc = CreateUnoService("com.sun.star.script.Invocation") 

  3. 18    invocCurrObj = invoc.createInstanceWithArguments(Array(obj)) 

  4. 19    ResourceURL= invocCurrObj.getValue("ResourceURL") 

  5. 20    oLayoutMng.hideElement(ResourceURL) 

Dieser Code sollte gegebenenfalls in die FOR-Schleife vor END IF eingefügt werden.

Werden die Symbolleisten nicht wieder direkt beim Beenden des Formulars eingeblendet, so bleiben sie weiterhin verborgen. Sie können natürlich über Ansicht → Symbolleisten wieder aufgerufen werden. Etwas irritierend ist es jedoch, wenn gerade die Standardleiste (Ansicht → Symbolleisten → Standardleiste) oder die Statusleiste (Ansicht → Statusleiste) fehlt.

Mit dieser Prozedur werden die Symbolleisten aus dem Versteck ('hideElement') wieder hervorgeholt ('showElement'). Der Kommentar enthält die Leisten, die oben als sonst fehlende Leisten am ehesten auffallen.

  1. 1 SUB Symbolleisten_Einblenden 

  2. 2    DIM oFrame AS OBJECT 

  3. 3    DIM oLayoutMng AS OBJECT 

  4. 4    DIM aElemente() 

  5. 5    oFrame = StarDesktop.getCurrentFrame() 

  6. 6    oLayoutMng = oFrame.LayoutManager 

  7. 7    aElemente = oLayoutMng.getElements() 

  8. 8    FOR i = LBound(aElemente) TO UBound(aElemente) 

  9. 9       oLayoutMng.showElement(aElemente(i).ResourceURL) 

  10. 10    NEXT 

  11. 11    ' eventuell fehlende wichtige Elemente: 

  12. 12    ' "private:resource/toolbar/standardbar" 

  13. 13    ' "private:resource/statusbar/statusbar" 

  14. 14    ThisComponent.CurrentController.Sidebar.Visible = True 

  15. 15    ThisComponent.CurrentController.ViewSettings.ZoomValue = 100 

  16. 16    ThisComponent.CurrentController.ViewSettings.ShowRulers = True 

  17. 17    ThisComponent.CurrentController.ViewSettings.ShowParaBreaks = True 

  18. 18 END SUB 

Die Makros werden an die Eigenschaften des Formularfensters gebunden: Extras → Anpassen → Ereignisse → Dokument öffnen → Symbolleisten_Ausblenden bzw. … Dokument wird geschlossen → Symbolleisten_Einblenden

Auch diese Prozedur sowie die folgende muss in internen Formularen von Base anders gestartet werden, da sonst das Hauptfenster eingestellt wird.

  1. 1 SUB Symbolleisten_Einblenden(oEvent AS OBJECT)
     
    ' … 

  2. 2    oFrame = oEvent.Source.CurrentController.Frame 

Leider tauchen häufig Symbolleisten trotzdem nicht wieder auf. In hartnäckigen Fällen kann es daher helfen, nicht die Elemente auszulesen, die der Layoutmanager bereits kennt, sondern definitiv bestimmte Symbolleisten erst zu erstellen und danach schließlich zu zeigen:

  1. 1 Sub Symbolleisten_Einblenden 

  2. 2    DIM oFrame AS OBJECT 

  3. 3    DIM oLayoutMng AS OBJECT 

  4. 4    DIM i AS INTEGER 

  5. 5    DIM aElemente(5) AS STRING 

  6. 6    oFrame = StarDesktop.getCurrentFrame() 

  7. 7    oLayoutMng = oFrame.LayoutManager 

  8. 8    aElemente(0) = "private:resource/menubar/menubar" 

  9. 9    aElemente(1) = "private:resource/statusbar/statusbar" 

  10. 10    aElemente(2) = "private:resource/toolbar/formsnavigationbar" 

  11. 11    aElemente(3) = "private:resource/toolbar/standardbar" 

  12. 12    aElemente(4) = "private:resource/toolbar/formdesign" 

  13. 13    aElemente(5) = "private:resource/toolbar/formcontrols" 

  14. 14    FOR i = LBound(aElemente) TO UBound(aElemente) 

  15. 15       IF NOT(oLayoutMng.requestElement(aElemente(i))) THEN 

  16. 16          oLayoutMng.createElement(aElemente(i)) 

  17. 17       END IF 

  18. 18    oLayoutMng.showElement(aElemente(i)) 

  19. 19    NEXT 

  20. 20    ThisComponent.CurrentController.Sidebar.Visible = True 

  21. 21    ThisComponent.store() 

  22. 22 END SUB 

Die darzustellenden Symbolleisten werden explizit benannt. Ist eine der entsprechenden Symbolleisten nicht für den Layoutmanager vorhanden, so wird sie zuerst über createElement erstellt und danach über showElement gezeigt. Deshalb muss das Dokument anschließend abgespeichert werden. Diese Prozedur muss über Extras → Anpassen → Ereignisse → Dokument wird geschlossen → Symbolleisten_Einblenden eingebunden werden.

Formulare im Vollbildmodus

Beim Vollbildmodus wird der gesamte Bildschirm vom Formular bedeckt. Hier steht keine Kontrollleiste o.ä. mehr zur Verfügung, die gegebenenfalls anzeigt, ob noch irgendwelche anderen Programme laufen.

  1. 1 FUNCTION Fullscreen(boSwitch AS BOOLEAN) 

  2. 2    DIM oDispatcher AS OBJECT 

  3. 3    DIM Props(0) AS NEW com.sun.star.beans.PropertyValue 

  4. 4    oDispatcher = createUnoService("com.sun.star.frame.DispatchHelper") 

  5. 5    Props(0).Name = "FullScreen" 

  6. 6    Props(0).Value = boSwitch 

  7. 7    oDispatcher.executeDispatch(ThisComponent.CurrentController.Frame,  

  8. 8       ".uno:FullScreen", "", 0, Props()) 

  9. 9 END FUNCTION 

Diese Funktion wird durch die folgenden Prozeduren eingeschaltet. In den Prozeduren läuft gleichzeitig die vorhergehende Prozedur zum Ausblenden der Symbolleisten ab – sonst erscheint die Symbolleiste, mit der der Vollbildmodus wieder ausgeschaltet werden kann. Auch dies ist eine Symbolleiste, wenn auch nur mit einem Symbol.

  1. 1 SUB Vollbild_ein 

  2. 2    Fullscreen(true) 

  3. 3    Symbolleisten_Ausblenden 

  4. 4 END SUB 

Aus dem Vollbild-Modus geht es wieder heraus über die 'ESC'-Taste. Wenn stattdessen ein Button mit einem entsprechenden Befehl belegt werden soll, so reichen auch die folgenden Zeilen:

  1. 1 SUB Vollbild_aus 

  2. 2    Fullscreen(false) 

  3. 3    Symbolleisten_Einblenden 

  4. 4 END SUB 

Formular direkt beim Öffnen der Datenbankdatei starten

Wenn jetzt schon die Symbolleisten weg sind oder gar das Formular im Vollbildmodus erscheint, dann müsste nur noch die Datenbankdatei beim Öffnen direkt in dieses Formular hinein starten. Der einfache Befehl zum Öffnen von Formularen reicht dabei leider nicht aus, da die Datenbankverbindung beim Öffnen des Base-Dokumentes noch nicht besteht.

Das folgende Makro wird über Extras → Anpassen → Ereignisse → Dokument öffnen gestartet. Dabei ist Speichern in → Datenbankdatei.odb zu wählen.

  1. 1 SUB Formular_Direktstart 

  2. 2    DIM oDatenquelle AS OBJECT 

  3. 3    oDatenquelle = ThisDatabaseDocument.CurrentController 

  4. 4    If NOT (oDatenquelle.isConnected()) THEN 

  5. 5       oDatenquelle.connect() 

  6. 6    END IF 

  7. 7    ThisDatabaseDocument.FormDocuments.getByName("Formularname").open 

  8. 8    REM alternativ geht auch:
         'oDatenquelle.loadComponent(com.sun.star.sdb.application.DatabaseObject.FORM,
         "Formularname",FALSE)
     

  9. 9 END SUB 

Zuerst muss der Kontakt mit der Datenquelle hergestellt werden. Der Controller hängt ebenso mit ThisDatabaseDocument zusammen wie das Formular. Anschließend kann das Formular gestartet werden und liest auch die Datenbankinhalte aus.

Markierfelder durch Schaltflächen ersetzen

Markierfelder und auch Optionsfelder sind von der Größe und dem Erscheinungsbild her nicht bearbeitbar. Die folgende Lösung erstellt statt Markierfeldern Schaltflächen, die mit einem entsprechenden Symbol versehen sind und wie gewohnt als Markierfelder ansprechbar sind.

Buttons mit Symbolen aus Fonts statt Markierfelder: Vergrößerbar und optisch anpassbar.
 
 

Zuerst werden globale Variablen für die beiden Zeichen erstellt, die auf den Buttons abgebildet werden sollen. Die Variablen werden in der Prozedur «BoolStart» mit dem entsprechenden Inhalt versehen, der in dem Beispiel je einem UTF8-Zeichen entspricht.

  1. 1 GLOBAL stChecked AS STRING 

  2. 2 GLOBAL stUnChecked AS STRING 

Damit das Makro nicht speziell auf ein Formular angepasst ist wird der Name des nachgebauten Markierfeldes in einem versteckten Kontrollfeld «hidCheckbox» notiert; bei mehreren Feldern sind diese durch ein Semikolon getrennt. In den Zusatzinformationen jedes einzelnen nachgebauten Markierfeldes steht dann der Name des dazugehörigen Datenfeldes aus der Datenquelle.

Das Einstellen des Wertes des Boolean-Feldes erfolgt beim Wechsel des Datensatzes im Formular. Die Prozedur wird also über Ereignisse → Nach dem Datensatzwechsel ausgelöst.

  1. 1 SUB BoolStart(oEvent AS OBJECT) 

  2. 2    DIM oForm AS OBJECT 

  3. 3    DIM oField AS OBJECT 

  4. 4    DIM stCheckbox AS STRING 

  5. 5    DIM stCheck AS STRING 

  6. 6    DIM stValue AS STRING 

  7. 7    DIM arCheck() 

  8. 8    oForm = oEvent.Source 

Damit das Makro nur dann durchläuft, wenn wirklich die entsprechende Checkbox verfügbar ist, wird hier das entsprechende UnoInterface abgefragt. Anschließend wird die Variable für «True» und «False» mit dem entsprechenden Zeichen versehen. Schließlich wird aus dem versteckten Feld ausgelesen, welche anderen Felder jetzt Markierfelder darstellen sollen.

  1. 9    IF hasUnoInterfaces( oForm, "com.sun.star.form.XForm" ) THEN 

  2. 10       stChecked = "✅" 

  3. 11       stUnChecked = "⛔"  

  4. 12       stCheckbox = oForm.getByName("hidCheckbox").HiddenValue 

  5. 13       arCheck = split(stCheckbox,";") 

  6. 14       FOR n = LBound(arCheck()) TO UBound(arCheck()) 

  7. 15          oField = oForm.getByName(Trim(arCheck(n))) 

  8. 16          stCheck = oField.Tag 

Wenn es sich um einen leeren Datensatz handelt (letzter neuer Datensatz), dann soll der Wert für das Feld 'False' sein. Es wird also kein Markierfeld mit 3 verschiedenen Einstellmöglichkeiten dargestellt.

Ist der ausgelesene Wert aus der Tabelle 'True', dann wird das Markierfeld mit dem entsprechenen Symbol versehen. Für alle anderen Werte wird 'false' angenommen.

  1. 17          IF oForm.IsRowCountFinal AND oForm.RowCount = 0 THEN 

  2. 18             stValue = "false" 

  3. 19          ELSE 

  4. 20             stValue = oForm.getString(oForm.findColumn(stCheck)) 

  5. 21          END IF 

  6. 22          IF stValue = "true" THEN 

  7. 23             oField.Label = stChecked 

  8. 24          ELSE 

  9. 25             oField.Label = stUnChecked 

  10. 26          END IF 

  11. 27       NEXT 

  12. 28    END IF 

  13. 29 END SUB 

Der Button wird in der Regel durch die Maus ausgelöst. Damit aber nicht ein Ansteuern über den Tabulator, das auch das Ereignis → Taste gedrückt auslöst, das Feld umstellt, muss hier vorher der KeyCode für den Tablulator, 1282, siehe http://api.libreoffice.org/docs/idl/ref/namespacecom_1_1sun_1_1star_1_1awt_1_1Key.html herausgefiltert werden. Dies wurde hier in eine separate Prozedur ausgelagert, da die Schaltfläche auch über die Maus ausgelöst werden kann.

  1. 1 SUB BoolChangeKey(oEvent AS OBJECT) 

  2. 2    IF oEvent.KeyCode <> 1282 THEN 

  3. 3       BoolChange(oEvent) 

  4. 4    END IF 

  5. 5 END SUB 

Wenn die Beschriftung des Feldes auf 'stChecked' steht, dann wird sie durch dieses Makro umgestellt auf 'stUnChecked' außerdem wird das Datenfeld auf 'false' eingestellt.

Das Makro wird von der Schaltfläche über Ereignisse → Taste gedrückt (Umweg über SUB BoolChangeKey) und über Ereignisse → Maustaste gedrückt ausgelöst.

  1. 1 SUB BoolChange(oEvent AS OBJECT) 

  2. 2    DIM oField AS OBJECT 

  3. 3    DIM oForm AS OBJECT 

  4. 4    oField = oEvent.Source.Model 

  5. 5    oForm = oField.Parent 

  6. 6    IF oField.Label = stChecked THEN 

  7. 7       oField.Label = stUnChecked 

  8. 8       oForm.updateBoolean(oForm.findColumn(oField.Tag),false) 

  9. 9    ELSE 

  10. 10       oField.Label = stChecked 

  11. 11       oForm.updateBoolean(oForm.findColumn(oField.Tag),true) 

  12. 12    END IF 

  13. 13 END SUB 

MySQL-Datenbank mit Makros ansprechen

Sämtliche bisher vorgestellten Makros wurden mit der internen HSQLDB verbunden. Bei der Arbeit mit externen Datenbanken sind ein paar Änderungen und Erweiterungen notwendig.

MySQL-Code in Makros

Wird die interne Datenbank angesprochen, so werden die Tabellen und Felder mit doppelten Anführungszeichen gegenüber dem SQL-Code abgesetzt:

  1. 1 SELECT "Feld" FROM "Tabelle" 

Da in Makros der SQL-Befehl Text darstellt, müssen die doppelten Anführungszeichen zusätzlich maskiert werden:

  1. 1 stSQL = "SELECT ""Feld"" FROM ""Tabelle""" 

MySQL-Abfragen können hingegen anders maskiert werden:

  1. 1 SELECT `Feld` FROM `Datenbank`.`Tabelle` 

Durch diese andere Form der Maskierung wird daraus im Makro-Code:

  1. 1 stSql = "SELECT `Feld` FROM `Datenbank`.`Tabelle`" 

Temporäre Tabelle als individueller Zwischenspeicher

In den vorhergehenden Kapiteln wurde häufiger eine einzeilige Tabelle zum Suchen oder Filtern von Tabellen genutzt. In einem Mehrbenutzersystem kann darauf nicht zurückgegriffen werden, da sonst andere Nutzer von dem Filterwert eines anderen Nutzers abhängig würden. Temporäre Tabellen sind in MySQL nur für den Nutzer der gerade aktiven Verbindung zugänglich, so dass für die Such- und Filterfunktionen auf diese Tabellenform zugegriffen werden kann.

Diese Tabellen können natürlich nicht vorher erstellt worden sein. Sie müssen beim Öffnen der Base-Datei erstellt werden. Deshalb ist das folgende Makro mit dem Öffnen der *.odb-Datei zu verbinden:

  1. 1 SUB CreateTempTable 

  2. 2    oDatenquelle = thisDatabaseDocument.CurrentController 

  3. 3    IF NOT (oDatenquelle.isConnected()) THEN oDatenquelle.connect() 

  4. 4    oVerbindung = oDatenquelle.ActiveConnection() 

  5. 5    oSQL_Anweisung = oVerbindung.createStatement() 

  6. 6    stSql = "CREATE TEMPORARY TABLE IF NOT EXISTS `Suchtmp` (`ID` INT PRIMARY KEY,
         `Name` VARCHAR(50))"
     

  7. 7    oSQL_Anweisung.executeUpdate(stSql) 

  8. 8 END SUB 

Zum Start der *.odb-Datei besteht noch keine Verbindung zur externen MySQL-Datenbank. Die Verbindung muss erst einmal hergestellt werden. Dann wird eine temporäre Tabelle mit entsprechend notwendigen Feldern erstellt.

Leider zeigt Base die temporären Tabellen nicht im Tabellencontainer an. Es kann über Abfragen auf diese Tabellen zugegriffen werden. Der Zugriff ist allerdings nur lesend möglich, so dass neue Inhalte für diese Tabellen nur über die direkte SQL-Eingabe oder über Makros erfolgen kann. Für einen einfachen Filterzugriff bietet sich deshalb an, statt einer temporären Tabelle eine feste Tabelle zu nutzen, in der die Filterinhalte zusammen mit der Verbindungsnummer (CONNECTION_ID) gespeichert werden.

Filterung über die Verbindungsnummer

Hier wird die Filtertabelle bereits vorher über die GUI erstellt. Die Tabelle wird beim Öffnen der Datenbankdatei allerdings direkt mit entsprechendem Inhalt versorgt:

  1. 1    stSql = "REPLACE INTO `Filter` (`Connection_ID`,`Name`)
         VALUES(CONNECTION_ID(),NULL)"
     

Die Tabelle ist jetzt auch in Formularen beschreibbar und kann entsprechend einfacher genutzt werden. Für andere Nutzer ist jetzt allerdings sichtbar, nach welchen Begriffen der einzelnen Nutzer gerade sucht. Prinzipiell lässt sich aber der entsprechende auf den einzelnen Nutzer festgelegte Datensatz immer über CONNECTION_ID() ermitteln.

Wird die Datenbankdatei wieder geschlossen, so kann auch die Filter-Tabelle entsprechend bereinigt werden:

  1. 1 SUB DeleteFilter 

  2. 2    oDatasource = thisDatabaseDocument.CurrentController 

  3. 3    IF NOT (oDatasource.isConnected()) THEN oDatasource.connect() 

  4. 4    oConnection = oDatasource.ActiveConnection() 

  5. 5    oSQL_Command = oConnection.createStatement() 

  6. 6    stSql = "DELETE FROM `Filter` WHERE `Connection_ID` = CONNECTION_ID()" 

  7. 7    oSQL_Command.executeUpdate(stSql) 

  8. 8 END SUB 

Gespeicherte Prozeduren

In MySQL/MariaDB können Prozeduren gespeichert werden. Sollen diese Prozeduren zu bestimmten Zeiten ablaufen, so können sie über Extras → SQL mit dem Befehl CALL `Prozedurname`(); aufgerufen werden. Erstellen solche Prozeduren von sich aus eine Ergebnismenge in einer temporären Tabelle, so lässt sich diese temporäre Tabelle als nicht bearbeitbare Informationsquelle nutzen.

Automatischer Aufruf einer Prozedur

Die folgende Prozedur AlleNamen() könnte beim Laden eines Formulars ausgelöst werden. Sie muss ablaufen, bevor das Formular selbst Inhalt laden will. Kann das nicht erfolgen, so muss zusätzlich auf das auslösende Formular über das Ereignis Bezug genommen werden und das Formular nach der Ausführung der Prozedur erneut geladen werden.

  1. 1 SUB ProcExecute 

  2. 2    oDatasource = thisDatabaseDocument.CurrentController 

  3. 3    IF NOT (oDatasource.isConnected()) THEN oDatasource.connect() 

  4. 4    oConnection = oDatasource.ActiveConnection() 

  5. 5    oSQL_Command = oConnection.createStatement() 

  6. 6    oSql_Command.executeUpdate("CALL `AlleNamen`();") 

  7. 7 END SUB 

Die Prozedur ersetzt lediglich den Umweg, das Kommando CALL `AlleNamen`(); über Extras → SQL eingeben zu müssen. Die Prozedur wird ohne Rückgabewert genutzt. Der Rückgabewert muss per SQL in der Prozedur selbst definiert sein.

Übertragung der Ausgabe einer Prozedur in eine temporäre Tabelle

Dieses Makro geht davon aus, dass die gespeicherte Prozedur von MySQL/MariaDB einen Rückgabewert hat, der aber leider nicht über eine Abfrage, sondern nur direkt über SQL auf der Konsole direkt ausgegeben wird.

  1. 1 SUB ProcContentShow 

  2. 2    oDatasource = thisDatabaseDocument.CurrentController 

  3. 3    IF NOT (oDatasource.isConnected()) THEN oDatasource.connect() 

  4. 4    oConnection = oDatasource.ActiveConnection() 

  5. 5    oSQL_Command = oConnection.createStatement() 

  6. 6    oResult = oSql_Command.executeQuery("CALL `AlleNamen`();") 

  7. 7    stFields = "" 

  8. 8    FOR i = 1 TO oResult.Columns.Count 

  9. 9       stFields = stFields + "`" + oResult.Columns.ElementNames(i-1) + "` TINYTEXT," 

  10. 10    NEXT 

  11. 11    stFields = Left(stFields, Len(stFields)-1) 

  12. 12    stProcedure = "(" 

  13. 13    WHILE oResult.next 

  14. 14       FOR i = 1 TO oResult.Columns.Count 

  15. 15          stProcedure = stProcedure + "'" + oResult.getString(i) +"'," 

  16. 16       NEXT 

  17. 17       stProcedure =  Left(stProcedure, Len(stProcedure)-1) 

  18. 18       stProcedure = stProcedure + "),(" 

  19. 19    WEND 

  20. 20    stProcedure = Left(stProcedure, Len(stProcedure)-2) 

  21. 21    oSQL_Command.executeUpdate("DROP TEMPORARY TABLE IF EXISTS `TempNamen`") 

  22. 22    oSQL_Command.executeUpdate("CREATE TEMPORARY TABLE `TempNamen` ("+stFields+")") 

  23. 23    oSQL_Command.executeUpdate("INSERT INTO `TempNamen` VALUES "+stProcedure+";") 

  24. 24 END SUB 

Zuerst wird die Prozedur ausgeführt. Ein eventueller Rückgabewert wird in oResult gespeichert. Aus diesem Rückgabewert lassen sich die Spaltennamen (oResult.Columns.ElementNames()) und der Inhalt (oResult.getString()) auslesen. Die Feldtypen sind leider nicht zu ermitteln, so dass der Inhalt jeder Spalte einfach als Text interpretiert wird. Dieser Text wird als TINYTEXT mit einer Maximallänge von 255 Zeichen anschließend in einer temporären Tabelle gespeichert. Diese Tabelle kann dann zum Recherchieren genutzt werden.

PostgreSQL und Makros

Autowertrückgabe mit Returning

Existiert bei PostgreSQL ein AutoWert-Feld, so kann aus diesem Feld mit dem folgenden Befehl der gerade neu erstellte AutoWert ermittelt werden:

  1. 1 Sub Insert_Returning 

  2. 2    DIM oDatasource AS OBJECT 

  3. 3    DIM oConnection AS OBJECT 

  4. 4    DIM stSql AS STRING 

  5. 5    DIM oResult AS OBJECT 

  6. 6    DIM loID AS LONG 

  7. 7    oDatasource = thisDatabaseDocument.CurrentController 

  8. 8    IF NOT (oDatasource.isConnected()) THEN oDatasource.connect() 

  9. 9    oConnection = oDatasource.ActiveConnection() 

  10. 10    oSQL_Statement = oConnection.createStatement() 

  11. 11    stSql = "INSERT INTO ""Test1"" (""Neu"") Values('Ich') RETURNING ""ID""" 

  12. 12    oResult = oSQL_Statement.executeQuery(stSql) 

  13. 13    oResult.Next 

  14. 14    loID = oResult.getLong(1) 

  15. 15 End Sub 

Der zurückgegebene Schlüsselwert kann nur ausgelesen werden, wenn der Datensatz über executeQuery eingefügt wird.

Liegt die Tabelle nicht im Schema «public», dann ist der Name des Schemas mit aufzunehmen:

  1. 11    stSql = "INSERT INTO ""loffice"".""Test1"" (""Neu"") Values('Ich')
         RETURNING ""ID"""
     

fügt einen Wert in eine Tabelle ein, die in dem selbst erstellten Schema «loffice» liegt.

Datentyp «Array»

Mit PostgreSQL kann einem Feld auch der Datentyp «Array» zugewiesen werden24. Dies geht allerdings nur über Extras → SQL:
  1. 1 CREATE TABLE "public"."tbl_Array" ( 

  2. 2    "ID" int4 NOT NULL, 

  3. 3    "Nachname" varchar(100), 

  4. 4    "Vornamen" varchar(200)[], 

  5. 5    PRIMARY KEY ("ID")); 

In das Feld "Vornamen" können jetzt über eine geschweifte Klammer, getrennt mit Kommas, Array eingegeben werden. Diese Werte können in Abfragen einzeln ausgelesen werden.

  1. 1 SELECT "Nachname", "Vornamen"[1] FROM "tbl_Array" 

Dies gibt den ersten Vornamen in der Liste wieder.

Das Einfügen und auslesen von Werten bei diesen Feldern ist mit Makros etwas umständlich. Natürlich funktioniert die direkte Eingabe mit den geschweiften Klammern, aber bei prepared Statements hakt es:

  1. 1    DIM ar 

  2. 2    stSql = "INSERT INTO ""public"".""tbl_Array"" (""ID"", ""Nachname"",
         ""Vornamen"") VALUES (?, ?, ?)"
     

  3. 3    oSQL_Statement = oConnection.prepareStatement(stSql) 

  4. 4    oSQL_Statement.setLong(1, 2) 

  5. 5    oSQL_Statement.setString(2, "Big") 

  6. 6    ar = array("Will","John","Jack") 

  7. 7    oSQL_Statement.setArray(3, ar) 

Hier kommt es bei setArray direkt zum Crash: Bug 154464

Das Feld muss, wenn bereits ein Array vorgesehen ist, nicht über setArray mit Inhalt versehen werden, sondern über setString:

  1. 6    ar = array("Will","John","Jack") 

  2. 7    stAr = "{" 

  3. 8    FOR i = LBound(ar()) TO UBound(ar()) 

  4. 9       stAr = stAr & ar(i) & "," 

  5. 10    NEXT 

  6. 11    stAr = Left(stAr,Len(stAr)-1) & "}" 

  7. 12    oSQL_Statement.setString(3, stAr) 

Beim Auslesen der Werte über eine Abfrage in Makros funktioniert die entsprechende Methode getString allerdings nicht. Dies würde nur für einen Wert ("Vornamen"[1]), nicht aber für das Array funktionieren. Stattdessen müssen die Werte über getArray ausgelesen werden:

  1. 1    DIM ar 

  2. 2    DIM stAr 

  3. 3    stSql = "SELECT ""ID"", ""Nachname"", ""Vornamen"" FROM
         ""public"".""
    tbl_Array""" 

  4. 4    oResult = oSQL_Statement.executeQuery(stSql) 

  5. 5    WHILE oResult.Next 

  6. 6       loID = oResult.getLong(1) 

  7. 7       stSurname = oResult.getString(2) 

  8. 8       ar = oResult.getArray(3) 

  9. 9       stAr = ar.getArray(NULL) 

  10. 10       FOR i = LBound(stAr) TO UBound(stAr) 

  11. 11          PRINT stAr(i) 

  12. 12       NEXT 

  13. 13    WEND 

Die Werte aus dem Arrayfeld werden hier zu Demonstrationszwecken lediglich auf dem Bildschirm ausgegeben. Sie können entsprechend anderweitig umgeformt und ausgegeben werden.

Dialoge

Statt Formularen können für Base auch Dialoge zur Eingabe von Daten, zum Bearbeiten von Daten oder auch zur Wartung der Datenbank genutzt werden. Dialoge lassen sich auf das jeweilige Anwendungsgebiet direkt zuschneiden, sind aber natürlich nicht so komfortabel vordefiniert wie Formulare. Hier eine kurze Einführung, die mit einem recht komplexen Beispiel zur Datenbankwartung endet.

Dialoge starten und beenden

Zuerst muss für den Dialog25 ein entsprechender Ordner erstellt werden. Dies geschieht über Extras → Makros → Dialoge verwalten → Datenbankdatei → Standard → Neu. Der Dialog erscheint mit einer grauen Fläche und einer Titelleiste sowie einem Schließkreuz. Bereits dieser leere Dialog könnte jetzt aufgerufen und über das Schließkreuz wieder geschlossen werden.

Wird der Dialog angeklickt, so gibt es bei den allgemeinen Eigenschaften die Möglichkeit, die Größe und Position einzustellen. Außerdem kann dort der Inhalt des Titels «Dialoge starten» eingegeben werden.

 

In der am unteren Fensterrand befindlichen Symbolleiste befinden sich die verschiedensten Formular-Steuerelemente. Aus diesen Steuerelementen sind für den abgebildeten Dialog zwei Schaltflächen ausgesucht worden, von denen aus andere Dialoge gestartet werden sollen. Die Bearbeitung des Inhaltes und der Verknüpfung zu Makros ist gleich den Schaltflächen im Formular.

Die Lage der Deklaration der Variablen für den Dialog ist besonders zu beachten. Der Dialog wird als globale Variable gesetzt, damit auf ihn von unterschiedlichen Prozeduren aus zugegriffen werden kann. In diesem Falle ist der Dialog mit der Variablen oDialog0 versehen, weil es noch weitere Dialoge gibt, die einfach entsprechend durchnummeriert wurden.

  1. 1 DIM oDialog0 AS OBJECT 

Zuerst wird die Bibliothek für den Dialog geladen. Sie liegt in dem Verzeichnis «Standard», sofern bei der Erstellung des Dialogs keine andere Bezeichnung gewählt wurde. Der Dialog selbst ist über den Reiter mit der Bezeichnung «Dialog0» in dieser Bibliothek erreichbar. Mit Execute() wird der Dialog aufgerufen.

  1. 1 SUB Dialog0Start 

  2. 2    DialogLibraries.LoadLibrary("Standard") 

  3. 3    oDialog0 = createUnoDialog(DialogLibraries.Standard.Dialog0) 

  4. 4    oDialog0.Execute() 

  5. 5 END SUB 

Prinzipiell kann ein Dialog durch Betätigung des Schließkreuzes geschlossen werden. Soll dafür aber ein entsprechender Button vorgesehen werden, so reicht hier einfach der Befehl EndExecute() innerhalb einer Prozedur.

  1. 1 SUB Dialog0Ende 

  2. 2    oDialog0.EndExecute() 

  3. 3 END SUB 

Mit diesem Rahmen können beliebig gestaltete Dialoge gestartet und wieder geschlossen werden.

Einfacher Dialog zur Eingabe neuer Datensätze

 

Dieser Dialog stellt eine Vorstufe für den nächstfolgenden Dialog zur Bearbeitung von Datensätzen dar. Erst einmal sollen nur grundlegende Vorgehensweisen im Umgang mit der Tabelle einer Datenbank geklärt werden. Hier ist dies das Speichern von Datensätzen mit neuem Primärschlüssel bzw. das komplett neue Eingeben von Datensätzen. Wie weit so ein kleiner Dialog ausreichend für eine bestimmte Datenbankaufgabe ist, hängt natürlich von den Bedürfnissen des Nutzer ab.

Mit

  1. 1 DIM oDialog1 AS OBJECT 

wird wieder direkt auf der untersten Ebene des Moduls vor allen Prozeduren die globale Variable für den Dialog erstellt.

Der Dialog wird genauso gestartet und beendet wie der vorhergehende Dialog. Lediglich die Bezeichnung ändert sich von Dialog0 auf Dialog1. Die Prozedur zum Beenden des Dialogs ist mit dem Button Beenden verbunden.

Über den Button Neu werden alle Kontrollfelder des Dialogs durch die Prozedur «DatenfelderLeeren» von vorhergehenden Eingaben befreit:

  1. 1 SUB DatenfelderLeeren 

  2. 2    oDialog1.getControl("NumericField1").Text = "" 

  3. 3    oDialog1.getControl("TextField1").Text = "" 

  4. 4    oDialog1.getControl("TextField2").Text = "" 

  5. 5 END SUB 

Jedes Feld, das in einen Dialog eingefügt wird, ist über einen eigenen Namen ansprechbar. Im Gegensatz zu Feldern eines Formulars wird hier durch die Benutzeroberfläche darauf geachtet, dass keine Namen doppelt vergeben werden.

Über getControl wird zusammen mit dem Namen auf ein Kontrollfeld zugegriffen. Auch ein numerisches Feld hat hier die Eigenschaft Text zur Verfügung. Nur so lässt sich schließlich ein numerisches Feld leeren. Einen leeren Text gibt es, eine leere Nummer hingegen nicht – stattdessen müsste 0 in das Feld für den Primärschlüssel geschrieben werden.

Der Button Speichern löst schließlich die Prozedur «Daten1Speichern» aus:

  1. 1 SUB Daten1Speichern 

  2. 2    DIM oDatenquelle AS OBJECT 

  3. 3    DIM oVerbindung AS OBJECT 

  4. 4    DIM oSQL_Anweisung AS OBJECT 

  5. 5    DIM loID AS LONG 

  6. 6    DIM stVorname AS STRING 

  7. 7    DIM stNachname AS STRING 

  8. 8    loID = oDialog1.getControl("NumericField1").Value 

  9. 9    stVorname = oDialog1.getControl("TextField1").Text 

  10. 10    stNachname = oDialog1.getControl("TextField2").Text 

  11. 11    IF loID > 0 AND stNachname <> "" THEN 

  12. 12       oDatenquelle = thisDatabaseDocument.CurrentController 

  13. 13       If NOT (oDatenquelle.isConnected()) THEN 

  14. 14          oDatenquelle.connect() 

  15. 15       END IF 

  16. 16       oVerbindung = oDatenquelle.ActiveConnection() 

  17. 17       oSQL_Anweisung = oVerbindung.createStatement() 

  18. 18       stSql = "SELECT ""ID"" FROM ""Name"" WHERE ""ID"" = '"+loID+"'" 

  19. 19       oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql) 

  20. 20       WHILE oAbfrageergebnis.next 

  21. 21          MsgBox ("Der Wert für das Feld 'ID' existiert schon",16,
               
    "Doppelte Dateneingabe") 

  22. 22          EXIT SUB 

  23. 23       WEND 

  24. 24       stSql = "INSERT INTO ""Name"" (""ID"", ""Vorname"", ""Nachname"")
            VALUES ('"
    +loID+"','"+stVorname+"','"+stNachname+"')" 

  25. 25       oSQL_Anweisung.executeUpdate(stSql) 

  26. 26       DatenfelderLeeren 

  27. 27    END IF 

  28. 28 END SUB 

Wie in der Prozedur «DatenfelderLeeren» wird auf die Eingabefelder zugegriffen. Dieses Mal erfolgt der Zugriff allerdings lesend, nicht schreibend. Nur wenn das Feld «ID» eine Eingabe größer als 0 enthält und in dem Feld für den Nachnamen auch Text steht, soll der Datensatz weitergegeben werden. Die Null muss alleine schon deshalb ausgeschlossen werden, weil eine Zahlenvariable für Zahlen ohne Nachkommastellen grundsätzlich mit dem Wert 0 initialisiert wird. Auch bei einem leeren Feld würde also schließlich 0 zur Speicherung weitergegeben.

Sind die beiden Felder entsprechend mit Inhalt versehen, so wird eine Verbindung zur Datenbank aufgenommen. Da sich die Kontrollfelder nicht in einem Formular befinden, muss die Datenbankverbindung über thisDatabaseDocument.CurrentController hergestellt werden.

Zuerst wird eine Abfrage an die Datenbank geschickt, ob vielleicht ein Datensatz mit dem vorgegebenen Primärschlüssel schon existiert. Hat die Abfrage Erfolg, so wird eine Meldung über eine Messagebox ausgegeben, die mit einem Stopp-Symbol versehen ist (Code: 16) und die Überschrift «Doppelte Dateneingabe» trägt. Danach wird durch Exit SUB die Prozedur beendet.

Hat die Abfrage keinen Datensatz gefunden, der den gleichen Primärschlüssel hat, so wird der neue Datensatz über den Insert-Befehl in die Datenbank eingefügt. Anschließend wird über die Prozedur «DatenfelderLeeren» wieder ein leeres Formular präsentiert.

Dialog zum Bearbeiten von Daten in einer Tabelle

 

Dieser Dialog stellt schon deutlich mehr Möglichkeiten zur Verfügung als der vorhergehende Dialog. Hier lassen sich alle Datensätze anzeigen, durch Datensätze navigieren, Datensätze neu erstellen, ändern oder auch löschen. Natürlich ist der Code entsprechend umfangreicher.

Der Button Beenden ist mit der entsprechend auf den Dialog2 abgewandelten Prozedur des vorhergehenden Dialogs zur Eingabe neuer Datensätze verbunden. Hier werden nur die weiteren Buttons mit ihren entsprechenden Funktionen beschrieben.

Die Dateneingabe ist im Dialog so beschränkt, dass im Feld «ID» der Mindestwert auf '1' eingestellt wurde. Diese Einschränkung hat mit dem Umgang mit Variablen in Basic zu tun: Zahlenvariablen werden bei der Definition bereits mit '0' als Grundwert vorbelegt. Werden Zahlenwerte von leeren Feldern und Feldern mit '0' ausgelesen, so ist für Basic der anschließende Inhalt der Variablen gleich. Es müsste bei der Nutzung von '0' im Feld «ID» also zur Unterscheidung erst Text ausgelesen und vielleicht später in eine Zahl umgewandelt werden.

Der Dialog wird unter den gleichen Voraussetzungen geladen wie vorher auch. Hier wird allerdings die Ladeprozedur davon abhängig gemacht, ob die Variable, die der Prozedur «DatenLaden» mitgegeben wird, 0 ist.

  1. 1 SUB DatenLaden(loID AS LONG) 

  2. 2    DIM oDatenquelle AS OBJECT 

  3. 3    DIM oVerbindung AS OBJECT 

  4. 4    DIM oSQL_Anweisung AS OBJECT 

  5. 5    DIM stVorname AS STRING 

  6. 6    DIM stNachname AS STRING 

  7. 7    DIM loRow AS LONG 

  8. 8    DIM loRowMax AS LONG 

  9. 9    DIM inStart AS INTEGER 

  10. 10    oDatenquelle = thisDatabaseDocument.CurrentController 

  11. 11    If NOT (oDatenquelle.isConnected()) THEN 

  12. 12       oDatenquelle.connect() 

  13. 13    END IF 

  14. 14    oVerbindung = oDatenquelle.ActiveConnection() 

  15. 15    oSQL_Anweisung = oVerbindung.createStatement() 

  16. 16    IF loID < 1 THEN 

  17. 17       stSql = "SELECT MIN(""ID"") FROM ""Name""" 

  18. 18       oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql) 

  19. 19       WHILE oAbfrageergebnis.next 

  20. 20          loID = oAbfrageergebnis.getInt(1) 

  21. 21       WEND 

  22. 22       inStart = 1 

  23. 23    END IF 

Die Variablen werden deklariert. Die Datenbankverbindung wird, wie weiter oben erklärt, für den Dialog hergestellt. Zum Start ist loID 0. Für diesen Fall wird per SQL der niedrigste Wert für den Primärschlüssel ermittelt. Der entsprechende Datensatz soll in dem Dialog später angezeigt werden. Gleichzeitig wird die Variable inStart auf 1 gestellt, damit der Dialog später gestartet wird. Enthält die Tabelle keine Daten, so bleibt loID 0. Entsprechend muss auch nicht nach dem Inhalt und der Anzahl irgendwelcher Datensätze im Folgenden gesucht werden.

Nur wenn loID größer als 0 ist, wird zuerst mit einer Abfrage überprüft, welche Daten in dem Datensatz enthalten sind. Anschließend werden in einer zweiten Abfrage alle Datensätze für die Datensatzanzeige gezählt. Mit der dritten Abfrage wird die Position des aktuellen Datensatzes ermittelt, indem alle Datensätze, die einen kleineren oder eben den aktuellen Primärschlüssel haben, zusammengezählt werden.

  1. 24    IF loID > 0 THEN 

  2. 25       stSql = "SELECT * FROM ""Name"" WHERE ""ID"" = '"+loID+"'" 

  3. 26       oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql) 

  4. 27       WHILE oAbfrageergebnis.next 

  5. 28          loID = oAbfrageergebnis.getInt(1) 

  6. 29          stVorname = oAbfrageergebnis.getString(2) 

  7. 30          stNachname = oAbfrageergebnis.getString(3) 

  8. 31       WEND 

  9. 32       stSql = "SELECT COUNT(""ID"") FROM ""Name""" 

  10. 33       oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql) 

  11. 34       WHILE oAbfrageergebnis.next 

  12. 35          loRowMax = oAbfrageergebnis.getInt(1) 

  13. 36       WEND 

  14. 37       stSql = "SELECT COUNT(""ID"") FROM ""Name"" WHERE ""ID"" <= '"+loID+"'" 

  15. 38       oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql) 

  16. 39       WHILE oAbfrageergebnis.next 

  17. 40          loRow = oAbfrageergebnis.getInt(1) 

  18. 41       WEND 

  19. 42       oDialog2.getControl("NumericField1").Value = loID 

  20. 43       oDialog2.getControl("TextField1").Text = stVorname 

  21. 44       oDialog2.getControl("TextField2").Text = stNachname 

  22. 45    END IF 

  23. 46    oDialog2.getControl("NumericField2").Value = loRow 

  24. 47    oDialog2.getControl("NumericField3").Value = loRowMax 

  25. 48    IF loRow = 1 THEN 

  26. 49       ' Vorheriger Datensatz 

  27. 50       oDialog2.getControl("CommandButton4").Model.enabled = False 

  28. 51    ELSE 

  29. 52       oDialog2.getControl("CommandButton4").Model.enabled = True 

  30. 53    END IF 

  31. 54    IF loRow <= loRowMax THEN 

  32. 55       ' Nächster Datensatz | Neuer Datensatz | Löschen 

  33. 56       oDialog2.getControl("CommandButton5").Model.enabled = True 

  34. 57       oDialog2.getControl("CommandButton2").Model.enabled = True 

  35. 58       oDialog2.getControl("CommandButton6").Model.enabled = True 

  36. 59    ELSE 

  37. 60       oDialog2.getControl("CommandButton5").Model.enabled = False 

  38. 61       oDialog2.getControl("CommandButton2").Model.enabled = False 

  39. 62       oDialog2.getControl("CommandButton6").Model.enabled = False 

  40. 63    END IF 

  41. 64    IF inStart = 1 THEN 

  42. 65       oDialog2.Execute() 

  43. 66    END IF 

  44. 67 END SUB 

Die ermittelten Werte für die Formularfelder werden übertragen. Die Einträge für die Nummer des aktuellen Datensatzes sowie die Anzahl aller Datensätze werden auf jeden Fall mit einer Zahl versorgt. Ist kein Datensatz vorhanden, so wird hier über den Default-Wert für eine numerische Variable 0 eingefügt.

Die Buttons zum Navigieren > («CommandButton5») und < («CommandButton4») sind nur verfügbar, wenn es möglich ist, einen entsprechenden Datensatz über die Navigation zu erreichen. Ansonsten werden sie vorübergehend mit enabled = False deaktiviert. Gleiches gilt für die Buttons Neu und Löschen. Sie sollen dann nicht verfügbar sein, wenn die Zahl der angezeigten Zeilen höher ist als die maximal ermittelte Zeilenzahl. Dies ist für die Eingabe neuer Datensätze die Standardeinstellung dieses Dialogs.

Der Dialog soll möglichst nur dann gestartet werden, wenn er direkt aus einer Startdatei über DatenLaden(0) erstellt werden soll. Deshalb wurde die gesonderte Variable inStart mit dem Wert 1 zu Beginn der Prozedur versehen..

Über den Button < soll zu dem vorhergehenden Datensatz navigiert werden können. Der Button ist nur dann aktiv, wenn nicht bereits der erste Datensatz angezeigt wird. Zum Navigieren wird von dem aktuellen Datensatz der Wert für den Primärschlüssel aus dem Feld «NumericField1» ausgelesen.

Hier gilt es zwei Fälle zu unterscheiden:

  1. 1.Es wurde vorher vorwärts zu einer Neueingabe navigiert, so dass das entsprechende Feld keinen Wert enthält. loID gibt dann den Standardwert wieder, der durch die Definition als Zahlenvariable vorgegeben ist: 0.  

  2. 2.Ansonsten enthält loID einen Wert, der größer als 0 ist. Entsprechend kann über eine Abfrage die nächstkleinere «ID» ermittelt werden. 

  1. 1 SUB vorherigerDatensatz 

  2. 2    DIM loID AS LONG 

  3. 3    DIM loIDneu AS LONG 

  4. 4    loID = oDialog2.getControl("NumericField1").Value 

  5. 5    oDatenquelle = thisDatabaseDocument.CurrentController 

  6. 6    If NOT (oDatenquelle.isConnected()) THEN 

  7. 7       oDatenquelle.connect() 

  8. 8    END IF 

  9. 9    oVerbindung = oDatenquelle.ActiveConnection() 

  10. 10    oSQL_Anweisung = oVerbindung.createStatement() 

  11. 11    IF loID < 1 THEN 

  12. 12       stSql = "SELECT MAX(""ID"") FROM ""Name""" 

  13. 13    ELSE 

  14. 14       stSql = "SELECT MAX(""ID"") FROM ""Name"" WHERE ""ID"" < '"+loID+"'" 

  15. 15    END IF 

  16. 16    oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql) 

  17. 17    WHILE oAbfrageergebnis.next 

  18. 18       loIDneu = oAbfrageergebnis.getInt(1) 

  19. 19    WEND 

  20. 20    IF loIDneu > 0 THEN 

  21. 21       DatenLaden(loIDneu) 

  22. 22    END IF 

  23. 23 END SUB 

Bei einem leeren «ID»-Feld soll auf den Datensatz mit dem höchsten Wert in der Primärschlüsselnummer gewechselt werden. Können hingegen aus dem «ID»-Feld Daten entnommen werden, so wird der entsprechend nachrangige Wert für die "ID" ermittelt.

Das Ergebnis dieser Abfrage dient dazu, die Prozedur «DatenLaden» mit dem entsprechenden Schlüsselwert erneut durchlaufen zu lassen.

Über den Button > wird zum nächsten Datensatz navigiert. Diese Navigationsmöglichkeit steht nur zur Verfügung, wenn nicht bereits der Dialog für die Eingabe eines neuen Datensatzes geleert wurde. Dies ist natürlich auch beim Start und leerer Tabelle der Fall.

Zwangsläufig ist in dem Feld «NumericField1» ein Wert vorhanden. Von diesem Wert ausgehend kann also per SQL nachgesehen werden, welcher Primärschlüsselwert der nächsthöhere in der Tabelle ist. Bleibt die Abfrage leer, weil es keinen entsprechenden Datensatz gibt, so ist der Wert für loIDneu = 0. Ansonsten kann über die Prozedur «DatenLaden» der Inhalt des nächsten Datensatzes geladen werden.

  1. 1 SUB naechsterDatensatz 

  2. 2    DIM loID AS LONG 

  3. 3    DIM loIDneu AS LONG 

  4. 4    loID = oDialog2.getControl("NumericField1").Value 

  5. 5    oDatenquelle = thisDatabaseDocument.CurrentController 

  6. 6    If NOT (oDatenquelle.isConnected()) THEN 

  7. 7       oDatenquelle.connect() 

  8. 8    END IF 

  9. 9    oVerbindung = oDatenquelle.ActiveConnection() 

  10. 10    oSQL_Anweisung = oVerbindung.createStatement() 

  11. 11    stSql = "SELECT MIN(""ID"") FROM ""Name"" WHERE ""ID"" > '"+loID+"'" 

  12. 12    oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql) 

  13. 13    WHILE oAbfrageergebnis.next 

  14. 14       loIDneu = oAbfrageergebnis.getInt(1) 

  15. 15    WEND 

  16. 16    IF loIDneu > 0 THEN 

  17. 17       DatenLaden(loIDneu) 

  18. 18    ELSE 

  19. 19       Datenfelder2Leeren 

  20. 20    END IF 

  21. 21 END SUB 

Existiert beim Navigieren zum nächsten Datensatz kein weiterer Datensatz, so löst die Navigation die folgende Prozedur «Datenfelder2Leeren» aus, die zur Eingabe neuer Daten dient.

Mit der Prozedur «Datenfelder2Leeren» werden nicht nur die Datenfelder selbst geleert. Die Position des aktuellen Datensatzes wird um einen Datensatz höher als die maximale Datensatzzahl eingestellt. Das soll verdeutlichen, dass der aktuell bearbeitete Datensatz noch nicht in der Datenbank enthalten ist.

Sobald «Datenfelder2Leeren» ausgelöst wird, wird außerdem die Möglichkeit des Sprungs zum vorhergehenden Datensatz aktiviert. Sprünge zu einem nachfolgenden Datensatz, das erneute Aufrufen der Prozedur über Neu oder das Löschen sind deaktiviert.

  1. 1 SUB Datenfelder2Leeren 

  2. 2    loRowMax = oDialog2.getControl("NumericField3").Value 

  3. 3    oDialog2.getControl("NumericField1").Text = "" 

  4. 4    oDialog2.getControl("TextField1").Text = "" 

  5. 5    oDialog2.getControl("TextField2").Text = "" 

  6. 6    oDialog2.getControl("NumericField2").Value = loRowMax + 1 

  7. 7    oDialog2.getControl("CommandButton4").Model.enabled = True        ' Vorh. Datensatz 

  8. 8    oDialog2.getControl("CommandButton5").Model.enabled = False        ' Nächster Datens. 

  9. 9    oDialog2.getControl("CommandButton2").Model.enabled = False        ' Neuer Datensatz 

  10. 10    oDialog2.getControl("CommandButton6").Model.enabled = False        ' Löschen 

  11. 11 END SUB 

Das Speichern der Daten soll nur möglich sein, wenn in den Feldern für «ID» und «Nachname» ein Eintrag erfolgt ist. Ist diese Bedingung erfüllt, so wird überprüft, ob der Datensatz ein neuer Datensatz ist. Das funktioniert über den Datensatzanzeiger, der bei neuen Datensätzen so eingestellt wurde, dass er für den aktuellen Datensatz einen um 1 höheren Wert als den maximalen Wert an Datensätzen ausgibt.

Im Falle eines neuen Datensatzes gibt es weiteren Überprüfungsbedarf, damit eine Speicherung einwandfrei funktionieren kann. Kommt die Ziffer für den Primärschlüssel bereits einmal vor, so erfolgt eine Warnung. Wird die entsprechende Frage mit Ja bestätigt, so wird der alte Datensatz mit der gleichen Schlüsselnummer überschrieben. Ansonsten erfolgt keine Speicherung. Solange noch gar kein Datensatz in der Datenbank enthalten ist (loRowMax = 0) braucht diese Überprüfung nicht zu erfolgen. In dem Falle kann der Datensatz direkt als neuer Datensatz abgespeichert werden. Bei einem neuen Datensatz wird schließlich noch die Zahl der Datensätze um 1 erhöht und die Eingabe für den nächsten Datensatz frei gemacht.

Bei bestehenden Datensätzen wird einfach der alte Datensatz durch ein Update mit dem neuen Datensatz überschrieben.

  1. 1 SUB Daten2Speichern(oEvent AS OBJECT) 

  2. 2    DIM oDatenquelle AS OBJECT 

  3. 3    DIM oVerbindung AS OBJECT 

  4. 4    DIM oSQL_Anweisung AS OBJECT 

  5. 5    DIM oDlg AS OBJECT 

  6. 6    DIM loID AS LONG 

  7. 7    DIM stVorname AS STRING 

  8. 8    DIM stNachname AS STRING 

  9. 9    DIM inMsg AS INTEGER 

  10. 10    DIM loRow AS LONG 

  11. 11    DIM loRowMax AS LONG 

  12. 12    DIM stSql AS STRING 

  13. 13    oDlg = oEvent.Source.getContext() 

  14. 14    loID = oDlg.getControl("NumericField1").Value 

  15. 15    stVorname = oDlg.getControl("TextField1").Text 

  16. 16    stNachname = oDlg.getControl("TextField2").Text 

  17. 17    IF loID > 0 AND stNachname <> "" THEN 

  18. 18       oDatenquelle = thisDatabaseDocument.CurrentController 

  19. 19       If NOT (oDatenquelle.isConnected()) THEN 

  20. 20          oDatenquelle.connect() 

  21. 21       END IF 

  22. 22       oVerbindung = oDatenquelle.ActiveConnection() 

  23. 23       oSQL_Anweisung = oVerbindung.createStatement() 

  24. 24       loRow = oDlg.getControl("NumericField2").Value 

  25. 25       loRowMax = oDlg.getControl("NumericField3").Value 

  26. 26       IF loRowMax < loRow THEN 

  27. 27          IF loRowMax > 0 THEN 

  28. 28             stSql = "SELECT ""ID"" FROM ""Name"" WHERE ""ID"" = '"+loID+"'" 

  29. 29             oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql) 

  30. 30             WHILE oAbfrageergebnis.next 

  31. 31                inMsg = MsgBox ("Der Wert für das Feld 'ID' existiert schon." &
                     
    CHR(13) & "Soll der Datensatz überschrieben werden?",20,
                     
    "Doppelte Dateneingabe") 

  32. 32                IF inMsg = 6 THEN 

  33. 33                   stSql = "UPDATE ""Name"" SET ""Vorname""='"+stVorname+"',
                        ""Nachname""='"
    +stNachname+"' WHERE ""ID"" = '"+loID+"'" 

  34. 34                   oSQL_Anweisung.executeUpdate(stSql) 

  35. 35                   DatenLaden(loID) ' Beim Update wurde ein bestehender Datensatz
                        überschrieben. Neueinlasen zur Korrektur der Datensatzzahlen
     

  36. 36                END IF 

  37. 37                EXIT SUB 

  38. 38             WEND 

  39. 39          END IF 

  40. 40          stSql = "INSERT INTO ""Name"" (""ID"", ""Vorname"", ""Nachname"") VALUES
               ('"
    +loID+"','"+stVorname+"','"+stNachname+"')" 

  41. 41          oSQL_Anweisung.executeUpdate(stSql) 

  42. 42          oDlg.getControl("NumericField3").Value = loRowMax + 1
               
    ' Nach dem Insert existiert ein Datensatz mehr 

  43. 43          Datenfelder2Leeren
               ' Nach einem Insert wird grundsätzlich zum nächsten Insert geschaltet  

  44. 44       ELSE 

  45. 45          stSql = "UPDATE ""Name"" SET ""Vorname""='"+stVorname+"',
               ""Nachname""='"
    +stNachname+"' WHERE ""ID"" = '"+loID+"'" 

  46. 46          oSQL_Anweisung.executeUpdate(stSql) 

  47. 47       END IF 

  48. 48    END IF 

  49. 49 END SUB 

Die Löschprozedur ist mit einer Nachfrage versehen, die versehentliches Löschen verhindern soll. Dadurch, dass der Button deaktiviert wird, wenn die Eingabefelder leer sind, dürfte es nicht vorkommen, dass das Feld «NumericField1» leer ist. Deshalb könnte die Überprüfung der Bedingung IF loID > 0 auch entfallen.

Beim Löschen wird die Zahl der Datensätze um einen Datensatz herabgesetzt. Dies muss entsprechend mit loRowMax – 1 korrigiert werden. Anschließend wird der dem aktuellen Datensatz folgende Datensatz angezeigt.

  1. 1 SUB DatenLoeschen(oEvent AS OBJECT) 

  2. 2    DIM oDatenquelle AS OBJECT 

  3. 3    DIM oVerbindung AS OBJECT 

  4. 4    DIM oSQL_Anweisung AS OBJECT 

  5. 5    DIM oDlg AS OBJECT 

  6. 6    DIM loID AS LONG 

  7. 7    oDlg = oEvent.Source.getContext() 

  8. 8    loID = oDlg.getControl("NumericField1").Value 

  9. 9    IF loID > 0 THEN 

  10. 10       inMsg = MsgBox ("Soll der Datensatz wirklich gelöscht werden?",20,
           
    "Löschen eines Datensatzes") 

  11. 11       IF inMsg = 6 THEN 

  12. 12          oDatenquelle = thisDatabaseDocument.CurrentController 

  13. 13          If NOT (oDatenquelle.isConnected()) THEN 

  14. 14             oDatenquelle.connect() 

  15. 15          END IF 

  16. 16          oVerbindung = oDatenquelle.ActiveConnection() 

  17. 17          oSQL_Anweisung = oVerbindung.createStatement() 

  18. 18          stSql = "DELETE FROM ""Name"" WHERE ""ID"" = '"+loID+"'" 

  19. 19          oSQL_Anweisung.executeUpdate(stSql) 

  20. 20          loRowMax = oDlg.getControl("NumericField3").Value 

  21. 21          oDlg.getControl("NumericField3").Value = loRowMax - 1 

  22. 22          naechsterDatensatz 

  23. 23       END IF 

  24. 24    ELSE 

  25. 25       MsgBox ("Kein Datensatz gelöscht." & CHR(13) &
           
    "Es fehlt eine Datensatzauswahl.",64,"Löschung nicht möglich") 

  26. 26    END IF 

  27. 27 END SUB 

Bereits dieser kleine Dialog zur Bearbeitung von Daten zeigt, dass der Aufwand im Makrocode schon erheblich ist, um die Grundlagen einer Datenbearbeitung zu gewährleisten. Der Zugriff über ein Formular ist hier erheblich einfacher. Der Dialog kann dagegen recht flexibel an die Bedürfnisse des Programms angepasst werden. Nur ist das eben nicht für die Erstellung einer Datenbankbedienung im Schnellverfahren gedacht.

Dialog zum Bearbeiten von Daten aus einer Tabellenübersicht

 

Zusammen mit dem Tabellen-Steuerelement der Dialoge ist es möglich, aus einer vorhandenen Datenmenge Datensätze auszuwählen und zu bearbeiten, neue Datensätze einzufügen oder auch vorhandene Daten zu löschen. Das Tabellen-Steuerelement dient dabei zur Auswahl der Datensätze. Die Bearbeitung erfolgt wie bei den vorhergehenden Dialogen über einfache Formularfelder. Um die Verwaltung der Daten einfacher zu machen ist bei der verwendeten Tabelle ein automatisch erstellter Primärschlüssel verwendet worden.

 

Das Tabellen-Steuerelement bietet neben den in wechselnden Farben erscheinenden Tabellenzeilen auch die Möglichkeit, die Daten nach den Tabellenköpfen zu sortieren. Im Screenshot ist die aktuelle Sortierung für das Feld «Nachname» durch ein kleines grünes Dreieck zu erkennen.

  1. 1 DIM oDialog3 AS OBJECT 

     

  2. 1 SUB Dialog3Start 

  3. 2    DialogLibraries.LoadLibrary("Standard") 

  4. 3    oDialog3 = createUnoDialog(DialogLibraries.Standard.Dialog3) 

  5. 4    GridDatenZeigen 

  6. 5    oDialog3.Execute() 

  7. 6 END SUB 

     

  8. 1 SUB Dialog3Ende 

  9. 2    oDialog3.EndExecute() 

  10. 3 END SUB 

Wie bei den anderen Dialogen muss die Variable für den Dialog außerhalb der Prozeduren notiert werden, damit sie in dem gesamten Modul verfügbar ist. Über «Dialog3Start» wird der Dialog gestartet. Wichtig ist für das Tabellen-Kontrollfeld, dass die Prozedur «GridDatenZeigen» vor der Ausführung des Dialogs abläuft. Sonst bleibt die Tabelle leer.

Die Prozedur zum Beenden des Dialogs ist nur notwendig, wenn ein Button zum Beenden mit eingebaut werden soll. Das Schließen des Dialogs über das X erfolgt unabhängig von der Prozedur.

  1. 1 SUB GridDatenZeigen 

  2. 2    DIM oGrid AS OBJECT 

  3. 3    DIM oColumnModel AS OBJECT 

  4. 4    DIM oColumn1 AS OBJECT 

  5. 5    DIM oColumn2 AS OBJECT 

  6. 6    DIM oColumn3 AS OBJECT 

  7. 7    DIM oDataModel AS OBJECT 

  8. 8    DIM oDatenquelle AS OBJECT 

  9. 9    DIM oVerbindung AS OBJECT 

  10. 10    DIM oSQL_Anweisung AS OBJECT 

  11. 11    DIM stSql AS STRING 

  12. 12    DIM oAbfrageergebnis AS OBJECT 

  13. 13    DIM l AS LONG 

  14. 14    DIM stID AS STRING 

  15. 15    DIM stVorname AS STRING 

  16. 16    DIM stNachname AS STRING 

  17. 17    oGrid = oDialog3.Model.getByName("GridControl1") 

  18. 18    oColumnModel = oGrid.ColumnModel 

Nach Deklaration der Variablen wird auf die Tabelle Zugegriffen. Zuerst werden die Spalten der Tabelle erstellt. Die folgenden Einstellungen sind für jede Spalte notwendig. Sie können natürlich auch platzsparender über Arrays erzeugt werden.

  1. 19    oColumn1 = createUnoService("com.sun.star.awt.grid.GridColumn") 

  2. 20    oColumn1.Title = "ID" 

  3. 21    oColumn1.ColumnWidth = 20 

  4. 22    oColumn1.HorizontalAlign = 2 

  5. 23    oColumn1.Flexibility = False 

Die Ausrichtung in HorizontalAlign wird über die Zuweisung von Werten geregelt. '0' steht für linksbündig, '1' für zentriert und '2' für rechtsbündig. Wird die Flexibility nicht auf False gesetzt, dann wird die Spaltenbreite von ColumnWidth nicht richtig übertragen. Die erste Spalte wird durch die Automatik dann viel zu breit.

  1. 24    oColumn2 = createUnoService("com.sun.star.awt.grid.GridColumn") 

  2. 25    oColumn2.Title = "Vorname" 

  3. 26    oColumn2.ColumnWidth = 50 

  4. 27    oColumn2.HorizontalAlign = 0 

  5. 28    oColumn2.Flexibility = False 

  6. 29    oColumn3 = createUnoService("com.sun.star.awt.grid.GridColumn") 

  7. 30    oColumn3.Title = "Nachname" 

  8. 31    oColumn3.ColumnWidth = 50 

  9. 32    oColumn3.HorizontalAlign = 0 

  10. 33    oColumn3.Flexibility = False 

  11. 34    oColumnModel.AddColumn(oColumn1) 

  12. 35    oColumnModel.AddColumn(oColumn2) 

  13. 36    oColumnModel.AddColumn(oColumn3) 

Nachdem die Spalten mit den Benennungen erstellt wurden, werden die Daten hinzugefügt. Dazu wird zuerst die Datenbankverbindung überprüft und gegebenenfalls erzeugt. Die gesamten Daten werden abgefragt und Datensatz für Datensatz über addRow hinzugefügt. In der Klammer von addRow steht zuerst die Datensatznummer und danach ein Array mit den Inhalten des Datensatzes.

  1. 37    oDataModel = oGrid.GridDataModel 

  2. 38    oDatenquelle = thisDatabaseDocument.CurrentController 

  3. 39    If NOT (oDatenquelle.isConnected()) THEN 

  4. 40       oDatenquelle.connect() 

  5. 41    END IF 

  6. 42    oVerbindung = oDatenquelle.ActiveConnection() 

  7. 43    oSQL_Anweisung = oVerbindung.createStatement() 

  8. 44    stSql = "SELECT * FROM ""Name_ID_Autowert"" " 

  9. 45    oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql) 

  10. 46    l = 1 

  11. 47    WHILE oAbfrageergebnis.next 

  12. 48       stID = oAbfrageergebnis.getString(1) 

  13. 49       stVorname = oAbfrageergebnis.getString(2) 

  14. 50       stNachname = oAbfrageergebnis.getString(3) 

  15. 51       oDataModel.addRow (l, Array(stID, stVorname, stNachname)) 

  16. 52       l = l + 1 

  17. 53    WEND 

  18. 54 END SUB 

Mit der Prozedur GridRow werden die Daten aus dem markierten Datensatz in die Formularfelder oberhalb der Tabelle übertragen. Dadurch können dann Daten geändert oder gelöscht werden.

  1. 1 SUB GridRow(oEvent AS OBJECT) 

  2. 2    DIM oGrid AS OBJECT 

  3. 3    DIM loRow AS LONG 

  4. 4    DIM oDataModel AS OBJECT 

  5. 5    DIM stData AS STRING 

  6. 6    DIM oDatenquelle AS OBJECT 

  7. 7    DIM oVerbindung AS OBJECT 

  8. 8    DIM oSQL_Anweisung AS OBJECT 

  9. 9    DIM stSql AS STRING 

  10. 10    DIM oAbfrageergebnis AS OBJECT 

  11. 11    DIM loID AS LONG 

  12. 12    DIM stVorname AS STRING 

  13. 13    DIM stNachname AS STRING 

  14. 14    oGrid = oEvent.Source 

  15. 15    IF oGrid.hasSelectedRows THEN 

Die Klicks auf das Tabellen-Kontrollfeld lösen dieses Makro aus. Sie landen nicht unbedingt auf irgendeinem Datensatz sondern z.B. auch auf den Tabellenköpfen. Wenn der Hintergrund oder die Tabellenköpfe angeklickt wurden können keine Daten ausgelesen werden. Deswegen muss erst einmal klar sein, ob eine Zeile ausgewählt wurde.

Aus dem markierten Datensatz wird das erste Feld '0' über getCellData ausgewählt. In ihm ist in diesem Falle der Primärschlüssel gespeichert. Dadurch kann der Datensatz eindeutig erkannt werden. Neben den Inhalten aus der Datenbank wird auch die Zeilennummer aus dem Tabellen-Kontrollfeld ausgelesen und in einem versteckten Formularfeld zwischengespeichert.

  1. 16       loRow = oGrid.CurrentRow() 

  2. 17       oDataModel = oGrid.Model.GridDataModel 

  3. 18       stData = oDataModel.getCellData(0, loRow) 

  4. 19       oDatenquelle = thisDatabaseDocument.CurrentController 

  5. 20       If NOT (oDatenquelle.isConnected()) THEN 

  6. 21          oDatenquelle.connect() 

  7. 22       END IF 

  8. 23       oVerbindung = oDatenquelle.ActiveConnection() 

  9. 24       oSQL_Anweisung = oVerbindung.createStatement() 

  10. 25       stSql = "SELECT * FROM ""Name_ID_Autowert"" WHERE ""ID"" = '"+stData+"'" 

  11. 26       oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql) 

  12. 27       WHILE oAbfrageergebnis.next 

  13. 28          loID = oAbfrageergebnis.getLong(1) 

  14. 29          stVorname = oAbfrageergebnis.getString(2) 

  15. 30          stNachname = oAbfrageergebnis.getString(3) 

  16. 31       WEND 

  17. 32       oDialog3.getControl("NumericField1").Value = loID 

  18. 33       oDialog3.getControl("TextField1").Text = stVorname 

  19. 34       oDialog3.getControl("TextField2").Text = stNachname 

  20. 35       oDialog3.getControl("Zeilennummer").Value = loRow 

  21. 36    END IF 

  22. 37 END SUB 

Wird der Speicher-Button betätigt, so läuft die folgende Prozedur ab.

  1. 1 SUB GridDatenSpeichern 

  2. 2    DIM stID AS STRING 

  3. 3    DIM stVorname AS STRING 

  4. 4    DIM stNachname AS STRING 

  5. 5    DIM loRow AS LONG 

  6. 6    DIM oDatenquelle AS OBJECT 

  7. 7    DIM oVerbindung AS OBJECT 

  8. 8    DIM oSQL_Anweisung AS OBJECT 

  9. 9    DIM stSql AS STRING 

  10. 10    DIM oAbfrageergebnis AS OBJECT 

  11. 11    DIM oGridData AS OBJECT 

  12. 12    DIM l AS LONG 

  13. 13    stID = oDialog3.getControl("NumericField1").Text 

  14. 14    stVorname = oDialog3.getControl("TextField1").Text 

  15. 15    stNachname = oDialog3.getControl("TextField2").Text 

  16. 16    loRow = oDialog3.getControl("Zeilennummer").Value 

Die Einträge aus den Formularfeldern werden ausgelesen. Dabei erfolgt das Auslesen auch des numerischen Feldes für die 'ID' als Text. So kann auf ein leeres Feld anschließend besser überprüft werden. Bei einem leeren Feld für die 'ID' handelt es sich um einen neuen Datensatz. Bei einem belegten Feld um eine Änderung des Datensatzes.

  1. 17    oDatenquelle = thisDatabaseDocument.CurrentController 

  2. 18    If NOT (oDatenquelle.isConnected()) THEN 

  3. 19       oDatenquelle.connect() 

  4. 20    END IF 

  5. 21    oVerbindung = oDatenquelle.ActiveConnection() 

  6. 22    oSQL_Anweisung = oVerbindung.createStatement() 

  7. 23    IF stID <> "" THEN 

  8. 24       stSql = "UPDATE ""Name_ID_Autowert"" SET ""Vorname"" = '"+stVorname+"',
            ""Nachname"" = '"
    +stNachname+"' WHERE ID = '"+stID+"'" 

  9. 25    ELSE 

  10. 26       stSql = "INSERT INTO ""Name_ID_Autowert"" (""Vorname"", ""Nachname"") VALUES
            ('"
    +stVorname+"', '"+stNachname+"')" 

  11. 27    END IF 

  12. 28    oSQL_Anweisung.executeUpdate(stSql) 

Bei Firebird muss der Code für den INSERT angepasst werden. Statt der einfachen INSERT-Anweisung ist der folgende Weg nötig:

  1. 26       stSql = "SELECT NEXT VALUE FOR RDB$1 FROM RDB$DATABASE"
         oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql)
         
    oAbfrageergebnis.next
         
    loID = oAbfrageergebnis.getLong(1)
         
    stSql = "INSERT INTO ""Name_ID_Autowert"" (""ID"", ""Vorname"", ""Nachname"")
            VALUES ('"
    +stID+"','"+stVorname+"', '"+stNachname+"')" 

Der Name des Generator für die ID wird in einer Abfrage mit direktem SQL über

  1. 1 SELECT RDB$FIELD_NAME, RDB$RELATION_NAME, RDB$GENERATOR_NAME  

    FROM RDB$RELATION_FIELDS  

  2. 2 WHERE RDB$GENERATOR_NAME IS NOT NULL 

ermittelt. Über den Namen des Generators für die ID wird dann der nächste freie Wert des Generators abgefragt und in der Variablen loID zwischengespeichert. Dieser Wert ist damit vergeben und kann in dem folgenden INSERT für die "ID" genutzt werden. Dies ist zur Zeit die sicherste Variante, da RETURNING nicht funktioniert und die anschließende Abfrage des höchsten Wertes für "ID" nur dann sicher ist, wenn der Generator nicht zurückgesetzt wurde und das System kein Mehrbenutzersystem ist. In Mehrbenutzersystemen würde gegebenenfalls sonst die "ID" eines anderen eingefügten Datensatzes abgefragt.

Nach der Datensatzänderung oder dem Einfügen eines neuen Datensatzes muss auch das Tabellen-Kontrollfeld mit den neuen Daten versorgt werden. Bei der Änderung müssen die Felder über updateCellData mit den neuen Daten versorgt werden. Die erste Zahl in der Klammer steht hier für die Spalte, die zweite Zahl in der Klammer für den Datensatz.

  1. 29    oGridData = oDialog3.Model.getByName("GridControl1").GridDataModel 

  2. 30    IF stID <> "" THEN 

  3. 31       oGridData.updateCellData(1,loRow,stVorname)  

  4. 32       oGridData.updateCellData(2,loRow,stNachname)  

  5. 33    ELSE 

Beim Einfügen eines neuen Datensatzes muss zuerst ermittelt werden, wie der automatisch erstellte Primärschlüsselwert lautet. In diesem Fall wird gleich der gesamte Datensatz noch einmal eingelesen, so dass in dem Tabellen-Kontrollfeld auf jeden Fall das steht, was auch in der Datenbank erscheint.

Über addRow wird der Datensatz dem Tabellen-Kontrollfeld hinzugefügt. Anschließend werden noch die gerade ermittelten Werte für die 'ID' und auch die Zeilennummer in die Formularfelder eingefügt. Die Zeilennummer entspricht dabei der Gesamtzahl der Datensätze, da der neue Datensatz als letzte Zeile in das Tabellen-Kontrollfeld aufgenommen wird.

  1. 34       stSql = "SELECT ""Name_ID_Autowert"".*, (SELECT COUNT(""ID"") FROM
            ""Name_ID_Autowert"") FROM ""Name_ID_Autowert"" WHERE ""ID"" = IDENTITY()"
     

In Firebird wird hier IDENTITY() durch '"+loID+"' ersetzt. Die Variable wurde ja bereits ermittelt.

  1. 35       oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql) 

  2. 36       WHILE oAbfrageergebnis.next 

  3. 37          stID = oAbfrageergebnis.getString(1) 

  4. 38          stVorname = oAbfrageergebnis.getString(2) 

  5. 39          stNachname = oAbfrageergebnis.getString(3) 

  6. 40          l = oAbfrageergebnis.getLong(4) 

  7. 41       WEND 

  8. 42       oGridData.addRow (l, Array(stID, stVorname, stNachname)) 

  9. 43       oDialog3.getControl("NumericField1").Value = stID 

  10. 44       oDialog3.getControl("Zeilennummer").Value = l 

  11. 45    END IF 

  12. 46 END SUB 

Um neue Daten einzufügen müssen die Daten aus den Eingabefeldern entleert werden. Dies ist besonders wichtig bei dem Feld für den Primärschlüssel (das nicht beschreibbar ist) und dem versteckten Feld für die Zeilennummer.

  1. 1 SUB GridDatenNeu 

  2. 2    oDialog3.getControl("NumericField1").Text = "" 

  3. 3    oDialog3.getControl("TextField1").Text = "" 

  4. 4    oDialog3.getControl("TextField2").Text = "" 

  5. 5    oDialog3.getControl("Zeilennummer").Text = "" 

  6. 6    oDialog3.getControl("GridControl1").deselectAllRows 

  7. 7 END SUB 

Sämtliche Eingabefelder werden geleert. Damit nicht beim nächsten Klick, z.B. zur Sortierung der markierte Datensatz in der Eingabezeile auftaucht, wird für alle Zeilen in dem Tabellen-Steuerelement die Selektion aufgehoben.

Die folgende Prozedur zum Löschen ist bereits im 2. Dialog ähnlich enthalten. Es können nur die Daten gelöscht werden, die in den Formularfeldern angezeigt werden. Dafür wird der versteckte Wert des Feldes für die Zeilennummer ausgelesen. Nach einer Kontrollabfrage wird dann der entsprechende SQL-Befehl ausgeführt und die Zeile über removeRow aus dem Tabellen-Kontrollfeld entfernt.

  1. 1 SUB GridDatenLoeschen 

  2. 2    DIM loID AS LONG 

  3. 3    DIM loRow AS LONG 

  4. 4    DIM oGridData AS OBJECT 

  5. 5    DIM inMsg AS INTEGER 

  6. 6    DIM oDatenquelle AS OBJECT 

  7. 7    DIM oVerbindung AS OBJECT 

  8. 8    DIM oSQL_Anweisung AS OBJECT 

  9. 9    DIM stSql AS STRING 

  10. 10    loID = oDialog3.getControl("NumericField1").Value 

  11. 11    loRow = oDialog3.getControl("Zeilennummer").Value 

  12. 12    oGridData = oDialog3.Model.getByName("GridControl1").GridDataModel 

  13. 13    IF loRow > 0 THEN 

  14. 14       inMsg = MsgBox ("Soll der Datensatz wirklich gelöscht werden?",20,
           
    "Löschen eines Datensatzes") 

  15. 15       IF inMsg = 6 THEN         

  16. 16          oDatenquelle = thisDatabaseDocument.CurrentController 

  17. 17          If NOT (oDatenquelle.isConnected()) THEN 

  18. 18             oDatenquelle.connect() 

  19. 19          END IF 

  20. 20          oVerbindung = oDatenquelle.ActiveConnection() 

  21. 21          oSQL_Anweisung = oVerbindung.createStatement() 

  22. 22          stSql = "DELETE FROM ""Name_ID_Autowert"" WHERE ""ID"" = '"+loID+"'" 

  23. 23          oSQL_Anweisung.executeUpdate(stSql) 

  24. 24          oGridData.removeRow(loRow) 

  25. 25       END IF 

  26. 26    ELSE 

  27. 27       MsgBox ("Kein Datensatz gelöscht." & CHR(13) &
           
    "Es fehlt eine Datensatzauswahl.",64,"Löschung nicht möglich") 

  28. 28    END IF 

  29. 29    GridDatenNeu 

  30. 30 END SUB 

Fortschrittsbalken für den Ablauf mehrerer Prozeduren

Wird für den Ablauf mehrerer Prozeduren hintereinander eine längere Zeit benötigt, so neigt der Nutzer / die Nutzerin schnell dazu, von einem «Hängen» des Systems auszugehen. Da bietet es sich an klar zu zeigen, dass der Rechner noch beschäftigt ist. Der folgende Dialog wurde für das Einlesen von Daten in eine im Netz befindliche PostgreSQL-Datenbank genutzt.

 

Zwei Beschriftungsfelder und ein Fortschrittsbalken ergeben den Aufbau des gesamten Dialogs. Der Dialog zeigt über die Vorgangsnummer und den Fortschrittsbalken an, wie weit der Import bisher fortgeschritten ist.

  1. 1 GLOBAL oDialog1 AS OBJECT 

Zuerst wird eine allgemeingültige Variable für den Dialog erstellt. Dadurch kann von der Startprozedur aus der Code für die Änderung der Anzeige ausgelagert werden.

  1. 1 SUB Dialog1Start 

  2. 2    DIM inWidth AS INTEGER 

  3. 3    DIM inHeight AS INTEGER 

  4. 4    DIM inx AS INTEGER 

  5. 5    DIM iny AS INTEGER 

  6. 6    DialogLibraries.LoadLibrary("Standard") 

  7. 7    oDialog1 = createUnoDialog(DialogLibraries.Standard.D_Import) 

  8. 8    oFrame = ThisComponent.CurrentController.Frame 

  9. 9    oWin = oFrame.getContainerWindow() 

  10. 10    inWidth = 315 

  11. 11    inHeight = 100 

  12. 12    inx = Int((oWin.Size.Width - inWidth) / 2) 

  13. 13    iny = Int((oWin.Size.Height - inHeight) / 2) 

  14. 14    oDialog1.setPosSize(inx, iny, inWidth, inHeight, 15) 

  15. 15    oDialog1.setVisible(true) 

  16. 16    FOR i = 1 TO 13 

  17. 17       stLabel = "Vorgang " & i & " von 13" 

  18. 18       Progress(i*100/13, stLabel) 

  19. 19       SELECT CASE i 

  20. 20          CASE 1 

  21. 21             Import_Gemeinde 

  22. 22          CASE 2 

  23. 23             Import_Ort 

  24. 24          CASE 3
                

  25. 25          CASE 13 

  26. 26             Import_Eigentuemer 

  27. 27       END SELECT 

  28. 28    NEXT 

  29. 29    oDialog1.dispose() 

  30. 30 END SUB 

Zuerst werden in der Prozedur Dialog1Start die Variablen deklariert. Hier sind nicht alle Variablen aufgeführt, die im Weiteren Verwendung finden.

Die Zeilen 8 bis 14 dienen dazu, den Dialog auf dem Bildschirm zu zentrieren. Dazu wird die Größenvorgabe des Dialogs mit inWidth und inHeight sowie die Größenvorgabe des verfügbaren Platzes für das Fenster (oWin.Size.Width und oWin.Size.Height) benötigt. Der Parameter '15' bei oDialog1.setPosSize besagt, dass sowohl die Position als auch die Größe des Dialogs geändert werden.

In Zeile 15 wird der Dialog mit oDialog1.setVisible(true) sichtbar geschaltet. Würde hier mit Execute der Dialog gestartet, so könnten keine weiteren Prozeduren ablaufen.

Zeile 17 und 18 in der Schleife, die ab Zeile 16 beginnt beeinflussen das Erscheinungsbild des Beschriftungsfeldes direkt über dem Fortschrittsbalken sowie die Länge des Fortschrittsbalkens selbst. Die Änderung dieses Erscheinungsbildes ist in die Prozedur Progress ausgelagert:

  1. 1 SUB Progress(doVal AS DOUBLE, stLabel AS STRING) 

  2. 2    oDialog1.getControl("ProgressBar").setValue(doVal) 

  3. 3    oDialog1.getControl("lblProgressBar").Text = stLabel 

  4. 4 END SUB 

Der Fortschrittsbalken hat über das Dialogdesign die Bezeichnung "ProgressBar" erhalten. Der Balken wurde im Design für 100 Einheiten ausgelegt. Da insgesamt 13 Prozeduren nacheinander aufgerufen werden wurde entsprechend 100/13 als die Länge für den Ablauf der ersten Prozedur übernommen.

Das Beschriftungsfeld ist über das Dialogdesign mit "lblProgressBar" ansprechbar. Hier wird nur jeweils die Anzeige der aktuellen Vorgangsnummer geändert.

Mit jedem Schleifendurchgang ändert sich die Variable i. Entsprechend tritt ein anderer SELECT CASE ein. Die Prozeduren, die ab Zeile 20 aufgerufen werden, sind also schlicht durchnummeriert über CASE 1, CASE 2 usw. Die Schleife springt zum nächsten Wert, wenn die Prozedur abgelaufen ist.

Mit oDialog1.dispose() in Zeile 48 wird schließlich der Dialog wieder abgeschaltet. Jetzt könnte noch eine Messagebox den erfolgreichen Ablauf aller Prozeduren anzeigen.

Fehleinträge von Tabellen mit Hilfe eines Dialogs bereinigen

Fehleingaben in Feldern fallen häufig erst später auf. Manchmal müssen auch gleich mehrere Datensätze mit der gleichen Eingabe auf einmal geändert werden. Dies ist in der normalen Tabellenansicht umständlich, je mehr Änderungen vorgenommen werden müssen, da für jeden Datensatz einzeln eine Eingabe erforderlich ist.

Formulare könnten hier mit Makros greifen. Wird aber für viele Tabellen ein identisch aufgebautes Formular benötigt, so bietet sich an, dies mit Dialogen zu erledigen. Ein Dialog wird zu Beginn mit den notwendigen Daten zu der jeweiligen Tabelle versehen und kann so statt mehrerer Formulare genutzt werden.

Diese Dialoge müssen für Firebird wegen der unterschiedlichen Systemtabellen entsprechend angepasst werden. Zu den Systemtabellen siehe den Anhang dieses Handbuches.

Dialoge werden neben den Modulen für Makros abgespeichert. Ihre Erstellung erfolgt ähnlich der eines Formulars. Hier stehen auch weitgehend ähnliche Kontrollfelder zur Verfügung. Lediglich das Tabellenkontrollfeld aus dem Formular fehlt als besondere Eingabemöglichkeit.

Wird ein Dialog ausgeführt, so erscheinen die Kontrollfelder entsprechend der Einstellung der grafischen Benutzeroberfläche.

Der oben abgebildete Dialog der Beispieldatenbank soll dazu dienen, die Tabellen zu bearbeiten, die nicht direkt in einem der Formulare als Grundlage vorhanden sind. So ist z.B. die Medienart über ein Listenfeld zugänglich, in der Makro-Version bereits durch ein Kombinationsfeld. In der Makro-Version können die Inhalte der Felder zwar durch neue Inhalte ergänzt werden, eine Änderung alter Inhalte ist aber nicht möglich. In der Version ohne Makros erfolgt die Änderung über ein separates Tabellenkontrollfeld.

Während die Änderung noch ohne Makros recht einfach in den Griff zu bekommen ist, so ist es doch recht umständlich, die Medienart vieler Medien auf eine andere Medienart zu ändern. Angenommen, es gäbe die Medienarten 'Buch, gebunden', 'Buch, kartoniert', 'Taschenbuch' und 'Ringbuch'. Jetzt stellt sich nach längerem Betrieb der Datenbank heraus, dass muntere Zeitgenossen noch weitere ähnliche Medienarten für gedruckte Werke vorgesehen haben. Nur ist uns die Differenzierung viel zu weitgehend. Es soll also reduziert werden, am liebsten auf nur einen Begriff. Ohne Makro müssten jetzt die Datensätze in der Tabelle Medien (mit Hilfe von Filtern) aufgesucht werden und einzeln geändert werden. Mit Kenntnis von SQL geht dies über die SQL-Eingabe schon wesentlich besser. Mit einer Eingabe werden alle Datensätze der Tabelle Medien geändert. Mit einer zweiten SQL-Anweisung wird dann die jetzt überflüssige Medienart gelöscht, die keine Verbindung mehr zur Tabelle "Medien" hat. Genau dieses Verfahren wird mit diesem Dialog über Ersetzen durch: angewandt – nur dass eben die SQL-Anweisung erst über das Makro an die Tabelle "Medienart" angepasst wird, da das Makro auch andere Tabellen bearbeiten können soll.

Manchmal schleichen sich auch Eingaben in eine Tabelle ein, die im Nachhinein in den Formularen geändert wurden, also eigentlich gar nicht mehr benötigt werden. Da kann es nicht schaden, solche verwaisten Datensätze einfach zu löschen. Nur sind die über die grafische Oberfläche recht schwer ausfindig zu machen. Hier hilft wieder eine entsprechende SQL-Abfrage, die mit einer Löschanweisung gekoppelt ist. Diese Anweisung ist im Dialog je nach betroffener Tabelle unter Alle überflüssigen Eingaben löschen hinterlegt.

Sollen mit dem Dialog mehrere Änderungen durchgeführt werden, so ist dies über das Markierfeld Mehrere Datensätze bearbeiten anzugeben. Dann endet der Dialog nicht mit der Betätigung des Buttons OK.

Der Makrocode für diesen Dialog ist aus der Beispieldatenbank ersichtlich. Im Folgenden werden nur Ausschnitte daraus erläutert.

  1. 1 SUB Tabellenbereinigung(oEvent AS OBJECT) 

Das Makro soll über Einträge im Bereich Zusatzinformationen des jeweiligen Buttons gestartet werden.

  1. 2 0: Formular, 1: Unterformular, 2: UnterUnterformular, 3: Kombinationsfeld oder Tabellenkontrollfeld, 4: Fremdschlüsselfeld im Formular, bei Tabellenkontrollfeld leer, 5: Tabellenname Nebentabelle, 6: Tabellenfeld1 Nebentabelle, 7: Tabellenfeld2 Nebentabelle, ggf. 8: Tabellenname Nebentabelle für Tabellenfeld 2 

Die Einträge in diesem Bereich werden zu Beginn des Makros als Kommentar aufgelistet. Die damit verbundenen Ziffern geben die Ziffern wieder, unter denen der jeweilige Eintrag aus dem Array ausgelesen wird. Das Makro kann Listenfelder verarbeiten, die zwei Einträge, getrennt durch «>», enthalten. Diese beiden Einträge können auch aus unterschiedlichen Tabellen stammen und über eine Abfrage zusammengeführt sein, wie z.B. bei der Tabelle "Postleitzahl", die für die Orte lediglich das Fremdschlüsselfeld "Ort_ID" enthält, zur Darstellung des Ortes also die Tabelle "Ort" benötigt.

  1. 3    DIM aFremdTabellen(0, 0 to 1) 

  2. 4    DIM aFremdTabellen2(0, 0 to 1) 

Unter den zu Beginn definierten Variablen fallen zwei Arrays auf. Während normale Arrays auch durch den Befehl Split() während der Laufzeit der Prozedur erstellt werden können, müssen zweidimensionale Arrays vorher definiert werden. Zweidimensionale Arrays werden z.B. benötigt, um aus einer Abfrage mehrere Datensätze zu speichern, bei denen die Abfrage selbst über mehr als ein Feld geht. Die beiden obigen Arrays müssen Abfragen auswerten, die sich jeweils auf zwei Tabellenfelder beziehen. Deshalb werden sie in der zweiten Dimension mit 0 to 1 auf zwei unterschiedliche Inhalte festgelegt.

  1. 5    stTag = oEvent.Source.Model.Tag 

  2. 6    aTabelle() = Split(stTag, ", ") 

  3. 7    FOR i = LBound(aTabelle()) TO UBound(aTabelle()) 

  4. 8       aTabelle(i) = trim(aTabelle(i)) 

  5. 9    NEXT 

Die mitgegebenen Variablen werden ausgelesen. Die Reihenfolge steht im obigen Kommentar. Es gibt maximal 9 Einträge, wobei geklärt werden muss, ob ein 8. Eintrag für das Tabellenfeld2 und ein 9. Eintrag für eine zweite Tabelle existieren.

Wenn Werte aus einer Tabelle entfernt werden, so muss zuerst einmal berücksichtigt werden, ob sie nicht noch als Fremdschlüssel in anderen Tabellen existieren. In einfachen Tabellenkonstruktionen gibt es von einer Tabelle aus lediglich eine Fremdschlüsselverbindung zu einer anderen Tabelle. In der vorliegenden Beispieldatenbank aber wird z.B. die Tabelle "Ort" genutzt, um die Erscheinungsorte der Medien und die Orte für die Adressen zu speichern. Es wird also zweimal der Primärschlüssel der Tabelle "Ort" in unterschiedlichen Tabellen eingetragen. Diese Tabellen und Fremdschlüsselbezeichnungen könnten natürlich auch über die «Zusatzinformationen» eingegeben werden. Schöner wäre es aber, wenn sie universell für alle Fälle ermittelt werden. Dies geschieht durch die folgende Abfrage.

  1. 10    stSql = "SELECT ""FKTABLE_NAME"", ""FKCOLUMN_NAME"" FROM
         ""INFORMATION_SCHEMA"".""SYSTEM_CROSSREFERENCE"" WHERE ""PKTABLE_NAME"" = '"

         +
    aTabelle(5) + "'" 

In der Datenbank sind im Bereich "INFORMATION_SCHEMA" alle Informationen zu den Tabellen der Datenbank abgespeichert, so auch die Informationen zu den Fremdschlüsseln. Die entsprechende Tabelle, die diese Informationen enthält, ist über "INFORMATION_SCHEMA"."SYSTEM_CROSSREFERENCE" erreichbar. Mit "PKTABLE_NAME" wird die Tabelle erreicht, die ihren Primärschlüssel ("Primary Key") in die Beziehung mit einbringt. Mit "FKTABLE_NAME" wird die Tabelle erreicht, die diesen Primärschlüssel als Fremdschlüssel ("Foreign Key") nutzt. Über "FKCOLUMN_NAME" wird schließlich die Bezeichnung des Fremdschlüsselfeldes ermittelt.

Die Tabelle, die einen Primärschlüssel als Fremdschlüssel zur Verfügung stellt, befindet sich in dem vorher erstellten Array an der 6. Position. Da die Zählung mit 0 beginnt, wird der Wert aus dem Array mit aTabelle(5) ermittelt.

  1. 11    inZaehler = 0 

  2. 12    stFremdIDTab1Tab2 = "ID" 

  3. 13    stFremdIDTab2Tab1 = "ID" 

  4. 14    stNebentabelle = aTabelle(5) 

Bevor die Auslesung des Arrays gestartet wird, müssen einige Standardwerte gesetzt werden. Dies sind der Zähler für das Array, in das die Werte der Nebentabelle geschrieben werden, der Standardprimärschlüssel, wenn nicht der Fremdschlüssel für eine zweite Tabelle benötigt wird und die Standardnebentabelle, die sich auf die Haupttabelle bezieht, bei Postleitzahl und Ort z.B. die Tabelle für die Postleitzahl.

Bei der Verknüpfung von zwei Feldern zur Anzeige in den Listenfeldern kann es ja, wie oben erwähnt, zu einer Verknüpfung über zwei Tabellen kommen. Für die Darstellung von Postleitzahl und Ort lautet hier die Abfrage

  1. SELECT "Postleitzahl"."Postleitzahl" || ' > ' || "Ort"."Ort"

    FROM "Postleitzahl", "Ort"

    WHERE "Postleitzahl"."Ort_ID" = "Ort"."ID"

Die Tabelle, die sich auf das erste Feld bezieht (Postleitzahl), ist mit der zweiten Tabelle über einen Fremdschlüssel verbunden. Lediglich die Information der beiden Tabellen und der Felder "Postleitzahl" und "Ort" wurde dem Makro mitgegeben. Die Primärschlüssel sind standardmäßig in dem Beispiel mit der Bezeichnung "ID" versehen. Der Fremdschlüssel von "Ort" in "Postleitzahl" muss also über das Makro ermittelt werden.

Genauso muss über das Makro jede andere Tabelle ermittelt werden, mit der die Inhalte des Listenfeldes über Fremdschlüssel in Verbindung stehen.

  1. 15    oAbfrageergebnis = oSQL_Anweisung.executeQuery(stSql) 

  2. 16    WHILE oAbfrageergebnis.next 

  3. 17       ReDim Preserve aFremdTabellen(inZaehler,0 to 1) 

Das Array muss jedes Mal neu dimensioniert werden. Damit die alten Inhalte erhalten bleiben, erfolgt über (Preserve) eine Sicherung des vorherigen Inhaltes.

  1. 18       aFremdTabellen(inZaehler,0) = oAbfrageergebnis.getString(1) 

Auslesen des ersten Feldes mit dem Namen der Tabelle, die den Fremdschlüssel enthält. Ergebnis für die Tabelle "Postleitzahl" ist hier die Tabelle "Adresse".

  1. 19       aFremdTabellen(inZaehler,1) = oAbfrageergebnis.getString(2) 

Auslesen des zweiten Feldes mit der Bezeichnung des Fremdschlüsselfeldes. Ergebnis für die Tabelle "Postleitzahl" ist hier das Feld "Postleitzahl_ID" in der Tabelle "Adresse".

Für den Fall, dass dem Aufruf der Prozedur auch der Name einer zweiten Tabelle mitgegeben wurde, erfolgt die folgende Schleife. Nur wenn der Name der zweiten Tabelle als Fremdschlüsseltabelle für die erste Tabelle auftaucht, erfolgt hier eine Änderung der Standardeinträge. In unserem Fall kommt dies nicht vor, da die Tabelle "Ort" keinen Fremdschlüssel der Tabelle "Postleitzahl" enthält. Der Standardeintrag für die Nebentabelle bleibt also bei "Postleitzahl"; schließlich ist die Kombination von Postleitzahl und Ort eine Grundlage für die Adressentabelle, die einen Fremdschlüssel zu der Tabelle "Postleitzahl" enthält.

  1. 20       IF UBound(aTabelle()) = 8 THEN 

  2. 21          IF aTabelle(8) = aFremdTabellen(inZaehler,0) THEN 

  3. 22             stFremdIDTab2Tab1 = aFremdTabellen(inZaehler,1) 

  4. 23             stNebentabelle = aTabelle(8) 

  5. 24          END IF 

  6. 25       END IF 

  7. 26       inZaehler = inZaehler + 1 

Da eventuell noch weitere Werte auszulesen sind, erfolgt eine Erweiterung des Zählers zur Neudimensionierung des Arrays. Anschließend wird die Schleife beendet.

  1. 27    WEND 

Existiert im Aufruf der Prozedur ein zweiter Tabellenname, so wird die gleiche Abfrage jetzt mit dem zweiten Tabellennamen gestartet:

  1. 28    IF UBound(aTabelle()) = 8 THEN 

Der Ablauf ist identisch. Nur wird in der Schleife jetzt gesucht, ob vielleicht der erste Tabellenname als Fremdschlüssel-Tabellenname auftaucht. Das ist hier der Fall: Die Tabelle "Postleitzahl" enthält den Fremdschlüssel "Ort_ID" aus der Tabelle "Ort". Dieser Fremdschlüssel wird also jetzt der Variablen stFremdIDTab1Tab2 zugewiesen, so dass die Beziehung der Tabellen untereinander definiert werden kann.

  1. 29       IF aTabelle(5) = aFremdTabellen2(inZaehler,0) THEN 

  2. 30          stFremdIDTab1Tab2 = aFremdTabellen2(inZaehler,1) 

  3. 31       END IF 

Nach einigen weiteren Einstellungen zur korrekten Rückkehr nach Aufruf des Dialogs in die entsprechenden Formulare (Ermittlung der Zeilennummer des Formulars, damit nach einem Neueinlesen auf die Zeilennummer wieder gesprungen werden kann) startet die Schleife, die den Dialog gegebenenfalls wieder neu erstellt, wenn die erste Aktion erfolgt ist, der Dialog aber für weitere Aktionen offen gehalten werden soll. Die Einstellung zur Wiederholung erfolgt über das entsprechende Markierfeld.

  1. 32    DO 

Bevor der Dialog gestartet wird, wird erst einmal der Inhalt der Listenfelder ermittelt. Dabei muss berücksichtigt werden, ob die Listenfelder zwei Tabellenfelder darstellen und eventuell sogar einen Bezug zu zwei Tabellen haben.

  1. 33       IF UBound(aTabelle()) = 6 THEN 

Das Listenfeld bezieht sich nur auf eine Tabelle und ein Feld, da das Array bei dem Tabellenfeld1 der Nebentabelle endet.

  1. 34          stSql = "SELECT """ + aTabelle(6) + """ FROM """ + aTabelle(5)  

  2. 35             + """ ORDER BY """ + aTabelle(6) + """" 

  3. 36       ELSEIF UBound(aTabelle()) = 7 THEN 

Das Listenfeld bezieht sich auf zwei Tabellenfelder, aber nur auf eine Tabelle, da das Array bei dem Tabellenfeld2 der Nebentabelle endet.

  1. 37          stSql = "SELECT """ + aTabelle(6) + """||' > '||""" + aTabelle(7)  

  2. 38             + """ FROM """ + aTabelle(5) + """ ORDER BY """ + aTabelle(6) + """" 

  3. 39       ELSE 

Das Listenfeld hat zwei Tabellenfelder und zwei Tabellen als Grundlage. Diese Abfrage trifft also auf das Beispiel mit der Postleitzahl und den Ort zu.

  1. 40          stSql ="SELECT """ + aTabelle(5) + """.""" + aTabelle(6) + """||' > '||"""
               
    + aTabelle(8) + """.""" + aTabelle(7) + """ FROM """ + aTabelle(5)
               +
    """, """ + aTabelle(8) + """ WHERE """ + aTabelle(8) + """."""
               +
    stFremdIDTab2Tab1 + """ = """ + aTabelle(5) + """."""
               +
    stFremdIDTab1Tab2 + """ ORDER BY """ + aTabelle(6) + """" 

  2. 41       END IF 

Hier erfolgt die erste Auswertung zur Ermittlung von Fremdschlüsseln. Die Variablen stFremdIDTab2Tab1 und stFremdIDTab1Tab2 starten mit dem Wert "ID". Für stFremdIDTab1Tab2 wurde in der Auswertung der vorhergehenden Abfrage ein anderer Wert ermittelt, nämlich der Wert "Ort_ID". Damit ergibt die vorherige Abfragekonstruktion genau den Inhalt, der weiter oben bereits für Postleitzahl und Ort formuliert wurde – lediglich erweitert um die Sortierung.

Jetzt muss der Kontakt zu den Listenfeldern erstellt werden, damit diese mit dem Inhalt der Abfragen bestückt werden. Diese Listenfelder existieren noch nicht, da noch gar kein Dialog existiert. Dieser Dialog wird mit den folgenden Zeilen erst einmal im Speicher erstellt, bevor er tatsächlich auf dem Bildschirm ausgeführt wird.

  1. 42       DialogLibraries.LoadLibrary("Standard") 

  2. 43       oDlg = CreateUnoDialog(DialogLibraries.Standard.Dialog_Tabellenbereinigung) 

Anschließend werden Einstellungen für die Felder, die der Dialog enthält, ausgeführt. Hier als Beispiel das Auswahllistenfeld, das mit dem Ergebnis der obigen Abfrage bestückt wird:

  1. 44       oCtlList1 = oDlg.GetControl("ListBox1") 

  2. 45       oCtlList1.addItems(aInhalt(),0) 

Der Zugriff auf die Felder des Dialogs erfolgt über GetControl sowie die entsprechende Bezeichnung. Bei Dialogen ist es nicht möglich, für zwei Felder die gleichen Bezeichnungen zu verwenden, da sonst eine Auswertung des Dialoges problematisch wäre.

Das Listenfeld wird mit den Inhalten aus der Abfrage, die in dem Array aInhalt() gespeichert wurden, ausgestattet. Das Listenfeld enthält nur die darzustellenden Inhalte als ein Feld, wird also nur in der Position '0' bestückt.

Nachdem alle Felder mit den gewünschten Inhalten versorgt wurden, wird der Dialog gestartet.

  1. 46       Select Case oDlg.Execute() 

  2. 47       Case 1        'Case 1 bedeutet die Betätigung des Buttons "OK" 

  3. 48       Case 0        'Wenn Button "Abbrechen" 

  4. 49          inWiederholung = 0 

  5. 50       End Select 

  6. 51    LOOP WHILE inWiederholung = 1 

Der Dialog wird so lange durchgeführt, wie der Wert für inWiederholung auf '1' steht. Diese Setzung erfolgt mit dem entsprechenden Markierfeld.

Hier der Inhalt nach Betätigung des Buttons OK im Kurzüberblick:

  1. 52    Case 1 

  2. 53       stInhalt1 = oCtlList1.getSelectedItem() 'Wert aus Listbox1 auslesen ... 

  3. 54       REM ... und den dazugehoerigen ID-Wert bestimmen. 

Der ID-Wert des ersten Listenfeldes wird in der Variablen inLB1 gespeichert.

  1. 55       stText = oCtlText.Text        ' Den Wert des Feldes auslesen. 

Ist das Textfeld nicht leer, so wird nur der Eintrag im Textfeld erledigt. Weder das Listenfeld für eine andere Zuweisung noch das Markierfeld für eine Löschung aller Daten ohne Bezug werden berücksichtigt. Dies wird auch dadurch verdeutlicht, dass bei Texteingabe die anderen Felder inaktiv geschaltet werden.

  1. 56       IF stText <> "" THEN 

Ist das Textfeld nicht leer, dann wird der neue Wert anstelle des alten Wertes mit Hilfe des vorher ausgelesenen ID-Feldes in die Tabelle geschrieben. Dabei werden wieder zwei Einträge ermöglicht, wie dies auch in dem Listenfeld geschieht. Das Trennzeichen ist «>». Bei Zwei Einträgen in verschiedenen Tabellen müssen auch entsprechend zwei UPDATE-Kommandos gestartet werden, die hier gleichzeitig erstellt und, durch ein Semikolon getrennt, weitergeleitet werden.

  1. 57       ELSEIF oCtlList2.getSelectedItem() <> "" THEN 

Wenn das Textfeld leer ist und das Listenfeld 2 einen Wert aufweist, muss der Wert des Listenfeldes 1 durch den Wert des Listenfeldes 2 ersetzt werden. Das bedeutet, dass alle Datensätze der Tabellen, in denen die Datensätze der Listenfelder Fremdschlüssel sind, überprüft und gegebenenfalls mit einem geänderten Fremdschlüssel beschrieben werden müssen.

  1. 58       stInhalt2 = oCtlList2.getSelectedItem()
         
    REM Den Wert der Listbox auslesen.
         REM ID für den Wert das Listenfeld ermitteln. 

Der ID-Wert des zweiten Listenfeldes wird in der Variablen inLB2 gespeichert. Auch dieses erfolgt wieder unterschiedlich, je nachdem, ob ein oder zwei Felder in dem Listenfeld enthalten sind sowie eine oder zwei Tabellen Ursprungstabellen des Listenfeldinhaltes sind.

Der Ersetzungsprozess erfolgt danach, welche Tabelle als die Tabelle definiert wurde, die für die Haupttabelle den Fremdschlüssel darstellt. Für das oben erwähnte Beispiel ist dies die Tabelle "Postleitzahl", da die "Postleitzahl_ID" der Fremdschlüssel ist, der durch Listenfeld 1 und Listenfeld 2 wiedergegeben wird.

  1. 59    IF stNebentabelle = aTabelle(5) THEN 

  2. 60       FOR i = LBound(aFremdTabellen()) TO UBound(aFremdTabellen()) 

Ersetzen des alten ID-Wertes durch den neuen ID-Wert. Problematisch ist dies bei n:m-Beziehungen, da dann der gleiche Wert doppelt zugeordnet werden kann. Dies kann erwünscht sein, muss aber vermieden werden, wenn der Fremdschlüssel hier Teil des Primärschlüssels ist. So darf in der Tabelle "rel_Medien_Verfasser" ein Medium nicht zweimal den gleichen Verfasser haben, da der Primärschlüssel aus der "Medien_ID" und der "Verfasser_ID" gebildet wird. In der Abfrage werden alle Schlüsselfelder untersucht, die zusammen die Eigenschaft UNIQUE haben oder als Fremdschlüssel mit der Eigenschaft UNIQUE über einen Index definiert wurden.

Sollte also der Fremdschlüssel die Eigenschaft UNIQUE haben und bereits mit der gewünschten zukünftigen inLB2 dort vertreten sein, so kann der Schlüssel nicht ersetzt werden.

  1. 61 stSql = "SELECT ""COLUMN_NAME"" FROM ""INFORMATION_SCHEMA"".""SYSTEM_INDEXINFO""
      WHERE ""TABLE_NAME"" = '"
    + aFremdTabellen(i,0) + "' AND ""NON_UNIQUE"" = False
      AND ""INDEX_NAME"" = (SELECT ""INDEX_NAME"" FROM
      ""INFORMATION_SCHEMA"".""SYSTEM_INDEXINFO"" WHERE ""TABLE_NAME"" = '"

      +
    aFremdTabellen(i,0) + "' AND ""COLUMN_NAME"" = '" + aFremdTabellen(i,1) + "')" 

Mit "NON_UNIQUE" = False werden die Spaltennamen angegeben, die UNIQUE sind. Allerdings werden nicht alle Spaltennamen benötigt, sondern nur die, die gemeinsam mit dem Fremd­schlüsselfeld einen Index bilden. Dies ermittelt der «Subselect» mit dem gleichen Tabellennamen (der den Fremdschlüssel enthält) und dem Namen des Fremdschlüsselfeldes.

Wenn jetzt der Fremdschlüssel in der Ergebnismenge vorhanden ist, dann darf der Schlüsselwert nur dann ersetzt werden, wenn gleichzeitig andere Felder dazu benutzt werden, den entsprechenden Index als UNIQUE zu definieren. Hierzu muss beim Ersetzen darauf geachtet werden, dass die Einzigartigkeit der Indexkombination nicht verletzt wird.

  1. 62    IF aFremdTabellen(i,1) = stFeldbezeichnung THEN 

  2. 63       inUnique = 1 

  3. 64    ELSE  

  4. 65       ReDim Preserve aSpalten(inZaehler) 

  5. 66       aSpalten(inZaehler) = oAbfrageergebnis.getString(1) 

  6. 67       inZaehler = inZaehler + 1 

  7. 68    END IF 

Alle Spaltennamen, die neben dem bereits bekannten Spaltennamen des Fremdschlüsselfeldes als Index mit der Eigenschaft UNIQUE auftauchen, werden in einem Array abgespeichert. Da der Spaltenname des Fremdschlüsselfeldes auch zu der Gruppe gehört, wird durch ihn gekennzeichnet, dass die Einzigartigkeit bei der Datenänderung zu berücksichtigen ist.

  1. 69 IF inUnique = 1 THEN 

  2. 70    stSql = "UPDATE """ + aFremdTabellen(i,0) + """ AS ""a"" SET """
         +
    aFremdTabellen(i,1) + """='" + inLB2 + "' WHERE """ + aFremdTabellen(i,1)
         +
    """='" + inLB1 + "' AND ( SELECT COUNT(*) FROM """ + aFremdTabellen(i,0)
         +
    """ WHERE """ + aFremdTabellen(i,1) + """='" + inLB2 + "' )" 

  3. 71    IF inZaehler > 0 THEN 

  4. 72       stFeldgruppe = Join(aSpalten(), """||  ||""") 

Gibt es mehrere Felder, die neben dem Fremdschlüsselfeld gemeinsam einen UNIQUE-Index bilden, so werden die hier für eine SQL-Gruppierung zusammengeführt. Ansonsten erscheint als stFeldgruppe nur aSpalten(0).

  1. 73       stFeldbezeichnung = "" 

  2. 74       FOR ink = LBound(aSpalten()) TO UBound(aSpalten()) 

  3. 75          stFeldbezeichnung = stFeldbezeichnung + " AND """ + aSpalten(ink)
               +
    """ = ""a"".""" + aSpalten(ink) + """ " 

Die SQL-Teilstücke werden für eine korrelierte Unterabfrage zusammengefügt.

  1. 76       NEXT 

  2. 77       stSql = Left(stSql, Len(stSql) – 1) 

Die vorher erstellte Abfrage endet mit einer Klammer. Jetzt sollen noch Inhalte zu der Unterabfrage hinzugefügt werden. Also muss die Schließung wieder aufgehoben werden. Anschließend wird die Abfrage durch die zusätzlich ermittelten Bedingungen ergänzt.

  1. 78       stSql=stSql + stFeldbezeichnung + "GROUP BY (""" + stFeldgruppe + """) ) < 1" 

  2. 79    END IF 

Wenn die Feldbezeichnung des Fremdschlüssels nichts mit dem Primärschlüssel oder einem UNIQUE-Index zu tun hat, dann kann ohne weiteres auch ein Inhalt doppelt erscheinen

  1. 80    ELSE 

  2. 81       stSql = "UPDATE """ + aFremdTabellen(i,0) + """ SET """ + aFremdTabellen(i,1)
         +
    """='" + inLB2 + "' WHERE """ + aFremdTabellen(i,1) + """='" + inLB1 + "'" 

  3. 82 END IF 

  4. 83 oSQL_Anweisung.executeQuery(stSql) 

  5. 84 NEXT 

Das Update wird so lange durchgeführt, wie unterschiedliche Verbindungen zu anderen Tabellen vorkommen, d. h. die aktuelle Tabelle einen Fremdschlüssel in anderen Tabellen liegen hat. Dies ist z. B. bei der Tabelle "Ort" zweimal der Fall: in der Tabelle "Medien" und in der Tabelle "Postleitzahl".

Anschließend kann der alte Wert aus dem Listenfeld 1 gelöscht werden, weil er keine Verbindung mehr zu anderen Tabellen hat.

  1. 85 stSql = "DELETE FROM """ + aTabelle(5) + """ WHERE ""ID""='" + inLB1 + "'" 

  2. 86 oSQL_Anweisung.executeQuery(stSql) 

Das gleiche Verfahren muss jetzt auch für eine eventuelle zweite Tabelle durchgeführt werden, aus der die Listenfelder gespeist werden. In unserem Beispiel ist die erste Tabelle die Tabelle "Postleitzahl", die zweite Tabelle die Tabelle "Ort".

Wenn das Textfeld leer ist und das Listenfeld 2 ebenfalls nichts enthält, wird nachgesehen, ob eventuell das Markierfeld darauf hindeutet, dass alle überflüssigen Einträge zu löschen sind. Dies ist für die Einträge der Fall, die nicht mit anderen Tabellen über einen Fremdschlüssel verbunden sind.

  1. 87 ELSEIF oCtlCheck1.State = 1 THEN 

  2. 88    stBedingung = "" 

  3. 89    IF stNebentabelle = aTabelle(5) THEN 

  4. 90       FOR i = LBound(aFremdTabellen()) TO UBound(aFremdTabellen()) 

  5. 91          stBedingung = stBedingung + """ID"" NOT IN (SELECT """ +
               
    aFremdTabellen(i,1) + """ FROM """ + aFremdTabellen(i,0) + """) AND " 

  6. 92       NEXT 

  7. 93    ELSE 

  8. 94       FOR i = LBound(aFremdTabellen2()) TO UBound(aFremdTabellen2()) 

  9. 95          stBedingung = stBedingung + """ID"" NOT IN (SELECT """ +
               
    aFremdTabellen2(i,1) + """ FROM """ + aFremdTabellen2(i,0) + """) AND " 

  10. 96       NEXT 

  11. 97    END IF 

Das letzte AND muss abgeschnitten werden, da sonst die Löschanweisung mit einem AND enden würde:

  1. 98    stBedingung = Left(stBedingung, Len(stBedingung) - 4)  

  2. 99    stSql = "DELETE FROM """ + stNebentabelle + """ WHERE " + stBedingung + "" 

  3. 100    oSQL_Anweisung.executeQuery(stSql) 

Da nun schon einmal die Tabelle bereinigt wurde, kann auch gleich der Tabellenindex überprüft und gegebenenfalls nach unten korrigiert werden. Siehe hierzu die in dem vorhergehenden Kapitel Interne Datenbanken sicher schließen erwähnte Prozedur.

  1. 101    Tabellenindex_runter(stNebentabelle) 

Anschließend wird noch gegebenenfalls das Listenfeld des Formulars, aus dem der Tabellenbereinigungsdialog aufgerufen wurde, auf den neuesten Stand gebracht. Unter Umständen ist das gesamte Formular neu einzulesen. Hierzu wurde zu Beginn der Prozedur der aktuelle Datensatz ermittelt, so dass nach einem Auffrischen des Formulars der aktuelle Datensatz auch wieder eingestellt werden kann.

  1. 102    oDlg.endExecute()  'Dialog beenden ... 

  2. 103    oDlg.Dispose()     '... und aus dem Speicher entfernen 

  3. 104 END SUB 

Dialoge werden mit endExecute() beendet und mit Dispose() komplett aus dem Speicher entfernt.

Makrozugriff mit Access2Base

In LibreOffice ist seit der Version 4.2 die Erweiterung Access2Base integriert. Der Zugriff auf diese Bibliothek erfolgt über

  1. 1 Sub DBOpen(Optional oEvent As Object) 

  2. 2    If GlobalScope.BasicLibraries.hasByName("Access2Base") then 

  3. 3       GlobalScope.BasicLibraries.loadLibrary("Access2Base") 

  4. 4    End If 

  5. 5    Call Application.OpenConnection(ThisDatabaseDocument) 

  6. 6 End Sub 

Eine englischsprachige Beschreibung mit Beispielen ist auf der Seite http://www.access2base.com/access2base.html zu finden.

Die Bibliothek stellt nicht zusätzliche Funktionen zur Verfügung, sondern versucht, dem Anwender den Zugriff auf die Möglichkeiten der LibreOffice-API zu vereinfachen. Eine kurze Beschreibung ist auch in der Hilfe zu LO zu finden.

Python als Makrosprache für Datenbanken

Basic ist die Makrosprache, die LibreOffice standardmäßig unterstützt. Der Zugang über Basic ist deswegen am einfachsten gestaltet. Manchmal bieten andere Makrosprachen aber Bibliotheken an, die ebenfalls gerne mit LibreOffice genutzt werden. Dieses Kapitel soll nur den Einstieg dazu liefen, so dass auch Abfragen an Datenbanken über Python funktionieren.

Zum Start mit Python empfiehlt die Hilfe die Erweiterung APSO (https://extensions.libreoffice.org/extensions/apso-alternative-script-organizer-for-python). Diese Erweiterung wird auch im Folgenden teilweise genutzt.

Speicherort für das erste Python Makro

Makros in Python können nicht mit dem internen Editor über Module einfach erstellt und aufgerufen werden. Prinzipiell reicht zur Erstellung ein einfacher Texteditor aus. Die damit erzeugte Datei sollte lediglich die Endung *.py erhalten. Hier ein kleines Beispiel für ein neu geöffnetes Calc-Dokument:

  1. 1 def HelloWorld2Calc(): 

  2. 2    doc = XSCRIPTCONTEXT.getDocument() 

  3. 3    cell = doc.getCurrentSelection() 

  4. 4    cell.setString('Hallo Welt!') 

Das so erstellte Script wird als test.py abgespeichert. Jetzt muss der Pfad gesucht werden, der im eigenen Benutzerverzeichnis von LibreOffice liegt: …/libreoffice/4/user/Scripts/python/. Wird die Datei «test.py» in dem Verzeichnis python abgelegt, so kann sie über Extras → Makros → Makro ausführen… ausgeführt werden. Das Modul heißt «test» (von «test.py») und der Name des Makros, die Prozedur, ist «HelloWorld2Calc» (von der Zeile, die mit def beginnt).

Speicherort für Module zum Importieren in ein Makro

Um auf andere in Python erstellte Module zugreifen zu können müssen diese Module in einem Pfad liegen, den LibreOffice kennt. Andere *.py-Dateien im gleichen Verzeichnis python können nicht über einen import-Befehl in die eigene Datei importiert werden.

In dem Verzeichnis …/libreoffice/4/user/Scripts/python/ wird ein Unterverzeichnis pythonpath erstellt. In dieses Verzeichnis können andere *.py-Dateien kopiert werden, auf die das eigene Makro zugreifen können soll. Dort wird dann auch automatisch beim ersten Aufruf ein Unterverzeichnis __pycache__ erstellt, in dem die für Python ausführbaren Dateien abgelegt werden.

Verwaltung von Modulen in Bibliotheken

In Basic bilden mehrere Prozeduren («Makros») ein Modul. Mehrere Module bilden dann zusammen eine Bibliothek. Bei entsprechend vielen Modulen ist die Zusammenfassung in Bibliotheken sinnvoll. So eine Bibliothek wird einfach als zusätzliches Verzeichnis in …/libreoffice/4/user/Scripts/python/ erstellt. Die eigentliche Python-Datei mit der Endung *.py wird dann in diesem Verzeichnis abgelegt. Damit haben dann die Makros in Python die gleiche Verwaltungsstruktur wie die bisher bekannten Makros in StarBasic.

Abfrage an eine geöffnete Datenbankdatei

  1. 1 def query(): 

  2. 2     doc = XSCRIPTCONTEXT.getDocument() 

  3. 3     src = doc.CurrentController    

  4. 4     if not src.isConnected(): 

  5. 5         src.connect() 

  6. 6     if src.isConnected(): 

  7. 7         conn = src.ActiveConnection 

  8. 8         sqlStatement = conn.createStatement() 

  9. 9         sql = 'SELECT * FROM "tbl_Person"' 

  10. 10         result = sqlStatement.executeQuery(sql) 

  11. 11         result.next() 

  12. 12         content = result.getString(1) 

Die Prozedur wird als «query» gestartet. LibreOffice stellt über XSCRIPTCONTEXT einige wichtige Methoden zur Verfügung. Hier wird auf das aktuelle Dokument zugegriffen.

Bei sämtlichen Codes kommt es genau auf die Schreibweise (Groß- und Kleinschreibung) an.

Eine if-Schleife beginnt mit if, gefolgt von der Bedingung. Ist die Bedingung erfüllt, so wird alles ausgeführt, was nach dem Doppelpunkt notiert ist.

Die Verbindung zur geöffneten Datenbankdatei wird hergestellt. Wie in Basic wird eine Abfrage erstellt, das Ergebnis einer Abfrage zwischengespeichert und der erste Wert der Abfrage über result.next() angesteuert. Als Inhalt wird beispielhaft nur der Wert des ersten Feldes der ersten Zeile ausgelesen. Mit dem Inhalt kann jetzt weiter gearbeitet werden. Er lässt sich aber leider nicht so ohne Probleme sichtbar machen, da in Python die Ausgabe auf der Konsole erfolgen würde und die Funktion msgbox aus Basic unbekannt ist.

Die Funktion wird hier verfügbar gemacht, indem aus der Erweiterung «APSO» durch Entpacken die Dateien «apso_utils.py» und «theconsole.py» kopiert und in das Verzeichnis …/libreoffice/4/user/Scripts/python/pythonpath eingefügt werden. Jetzt wird der obige Pythoncode erweitert:

  1. 1 from apso_utils import msgbox 

     

  2. 1 def query(): 

  3. 2    doc = XSCRIPTCONTEXT.getDocument() 

  4. 3    src = doc.CurrentController    

  5. 4    if not src.isConnected(): 

  6. 5       src.connect() 

  7. 6    if src.isConnected(): 

  8. 7       conn = src.ActiveConnection 

  9. 8       sqlStatement = conn.createStatement() 

  10. 9       sql = 'SELECT * FROM "tbl_Person"' 

  11. 10       result = sqlStatement.executeQuery(sql) 

  12. 11       result.next() 

  13. 12       content = result.getString(1) 

  14. 13       msgbox(content) 

Aus der Datei «apso_utils» wird die Funktion msgbox importiert. Diese Funktion wird in Zeile 13 aufgerufen und zeigt den Ergebniswert an.

Die Datei «apso_utils» bietet noch mehr Funktionen wie das erleichterte Einbinden von xray oder mri, zwei Tools, die zur Untersuchung der zur Verfügung stehenden Objekte und Befehle genutzt werden können. Deswegen schien es mir sinnvoll, diese Datei für den weiteren Zugriff bereit zu stellen. Die Datei «apso_utils.py» benötigt die Datei «theconsole.py» für einige Funktionen. Deswegen musste auch diese Datei in das entsprechende Verzeichnis importiert werden.

Abfrage an eine registrierte Datenbank

In der registrierten Datenbank des folgenden Beispiels sind in einer Tabelle Bilder gespeichert, die ausgelesen werden sollen. Die folgende Prozedur liest die Bilder aus der Firebird-Datenbank aus und schreibst sie mit dem entsprechend angegebenen Namen in das temporäre Verzeichnis des genutzten Linux-Systems. Diese Prozedur stellt Methoden zum Auslesen zur Verfügung, die ich in Basic so nicht umgesetzt bekam. Dort konnte ich Bilder, die deutlich größer als ein Icon waren, nicht problemlos aus Firebird exportieren.

  1. 1 import io 

  2. 2 import uno 

     

  3. 1 def ConnectRegisteredDB(): 

  4. 2    ctx = uno.getComponentContext() 

  5. 3    smgr = ctx.getServiceManager() 

  6. 4    obj = smgr.createInstanceWithContext('com.sun.star.sdb.DatabaseContext', ctx) 

  7. 5    db = obj['Beispiel_Druck_Writer_Tabellen_FB'] 

  8. 6    conn = db.getConnection('', '') 

  9. 7    sql = 'SELECT "Name", "Bild" FROM "tbl_Image"' 

  10. 8    sqlStatement = conn.createStatement() 

  11. 9    result = sqlStatement.executeQuery(sql) 

  12. 10    while result.next(): 

  13. 11       name = result.getString(1) 

  14. 12       stream = result.getBlob(2) 

  15. 13       size, data = stream.readBytes(io.BytesIO(), stream.available()) 

  16. 14       myfile = open('/tmp/bild'+name, 'wb') 

  17. 15       myfile.write(data.value) 

  18. 16       conn.close() 

Zuerst werden Module eingebunden, die das Makro nutzen will. Die beiden zu importierenden Module stellt LibreOffice direkt zur Verfügung.

Die Zeilen 2 bis 4 sind direkt aus der Hilfe von LibreOffice entnommen. Sie entsprechen zusammen dem Befehl CreateUnoService aus Basic. Damit wird dann auf die registrierten Datenbanken zugegriffen. Die hier genutzte registrierte Datenbank heißt «Beispiel_Druck_Writer_Tabellen_FB» und ist zu dem Zeitpunkt nicht geöffnet. Aus der Datenbank wird ein Textfeld und ein Feld des Typs BLOB (für Bilder) ausgelesen. Dies geschieht für alle Datensätze mit einer while-Schleife (Zeile 10). Nur der Doppelpunkt kennzeichnet den Start der Schleife.

In Zeile 13 wird der Datenstrom ausgelesen. Diese Art des Auslesens ist mir unter Basic nicht geglückt.

Zeile 14 dient dazu eine Datei zum Schreiben mit binärem Inhalt 'wb' zu öffnen. In Zeile 15 wird dann der ausgelesene Datenstrom geschrieben. Zum Schluss wird dann die Verbindung zur Datenbank aufgehoben.

Fertige Module in die Base-Datei einbinden

Viele Makros für Base sind spezifisch an eine Datenbank angepasst. Dies liegt schon allein daran, dass Abfragen an unterschiedliche Datenquellen gestellt werden, Formulare unterschiedlich konstruiert sind usw. Auch für die Weitergabe von Base-Dateien ist es wichtig, dass die Funktionen erhalten bleiben. Deswegen müssen die fertigen Bibliotheken irgendwie in die Base-Datei eingebunden werden. Dies geschieht am einfachsten mit der Extension APSO:

 

Über Extras → Makros → Python-Scripte verwalten wird der Dialog von APSO gestartet. Das Modul aus dem Ordner Meine Makros wird ausgesucht. Mit einem rechten Mausklick auf das Modul (nicht auf eine der Prozeduren in dem Modul) wird das gesamte Modul in das geöffnete Datenbankdokument (hier: «Test.odb») kopiert. Die Bibliothek kann auch in der Datenbankdatei weiter bearbeitet, wieder exportiert oder auch gelöscht werden. Das Bearbeiten gestaltet sich hier natürlich etwas schwieriger, da die *.py-Dateien nicht mehr direkt über die Verzeichnisstruktur des Betriebssystems erreichbar sind sondern in der gepackten Base-Datei liegen.

Der Import in die Datenbankdatei kann natürlich auch über die Bordmittel des Betriebssystems erfolgen. Dafür reicht ein Packprogramm aus, mit dem die Datenbankdatei geöffnet werden kann.

 

In der *.odb_Datei muss für die Pythonscripte ein Verzeichnis «Scripts» und ein Unterverzeichnis «python» erstellt werden. In das Unterverzeichnis «python» werden dann alle benötigten Module geladen.

Mit APSO wird nur das aktuelle Modul in die Datenbankdatei kopiert. Werden aber für einzelne Prozeduren Module von außen importiert, wie dies in der Abfrage an eine geöffnete Datenbankdatei der Fall ist, dann fehlen diese Module auf anderen Systemen. Dort sind nur die Module vorhanden, die standardmäßig mit LibreOffice ausgeliefert wurden, nicht aber die in dem Beispiel genutzten Bibliotheken aus APSO. Die Funktion «msgbox» steht also anderen Nutzern nicht zur Verfügung. Hierfür müsste auch das Verzeichnis «pythonpath» mit den darin enthaltenen Dateien in die *.odb-Datei kopiert werden.

 

1Die Kennzeichnung sollte eventuell noch verfeinert werden, da mit nur einem Buchstaben zwischen dem Datentyp «Double» und dem Datentyp «Date» bzw. «Single» und «String» nicht unterschieden werden kann.

2Siehe zu diesem Abschnitt auch die Datenbank «Beispiel_Suchen_und_Filtern.odb», die diesem Handbuch beiliegt.

3Die Beispieldatenbank «Beispiel_Datensatz_scrollbar.odb» ist den Beispieldatenbanken für dieses Handbuch beigefügt.

4Die Beispieldatenbank «Beispiel_InsertUpdateDelete_SQL.odb» ist den Beispieldatenbanken für dieses Handbuch beigefügt.

5https://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1sdbc_1_1XStatement.html

6Siehe hierzu die Beispieldatenbank «Beispiel_Direktberechnung_im Formular.odb»

7Die Datenbank «Beispiel_Suchen_Filtern.odb» ist in den zusätzlichen Datenbanken mit besonderer Beschreibung jeder einzelnen Datenbank enthalten.

8Die Datenbank «Beispiel_Listenfeld_Mehrfachauswahl.odb» ist den Beispieldatenbanken für dieses Handbuch beigefügt.

9Siehe zu diesem Abschnitt auch die Datenbank «Beispiel_Suchen_und_Filtern.odb», die diesem Handbuch beiliegt.

10Siehe zu diesem Abschnitt auch die Datenbank «Beispiel_Autotext_Suchmarkierung_Rechtschreibung.odb», die dem Handbuch beiliegt.

11Siehe auch hierzu: «Beispiel_Autotext_Suchmarkierung_Rechtschreibung.odb»

12Die Beispieldatenbank «Beispiel_Combobox_Listfeld.odb» zum Einsatz von Kombinationsfeldern statt Listenfeldern ist den Beispieldatenbanken für dieses Handbuch beigefügt.

13Siehe hierzu die Beispieldatenbank «Beispiel_Suchen_und_Filter.odb»

14Siehe hierzu die Beispieldatenbank «Beispiel_hierarchische_Listenfelder.odb»

15Siehe hierzu die Beispieldatenbank «Beispiel_Formular_Eingabekontrolle.odb»

16Dem Handbuch liegt die Datenbank «Beispiel_Fortlaufende_Nummer_Jahr.odb» bei.

17Das Beispiel "Datenkopie_Quelle_Ziel" ist als gepacktes Verzeichnis diesem Handbuch beigefügt.

18Die Beispieldatenbank «Beispiel_Daten_Import.odb» liegt diesem Handbuch bei.

19Die Beispieldatenbank «Beispiel_Druck_Writer_Tabellen.odb» liegt den Beispieldatenbanken bei. Jede Druckform ist dort in einem separaten Modul unter gebracht und kommentiert. Notwendige Vorlagen liegen ebenfalls bei. Selbst die Erstellung eines Drucks ohne Vorlage ist in der Beispieldatenbank enthalten, hier aber nicht weiter erklärt. Damit können dann nacheinander mehrere Tabellen erstellt werden. Eine Gruppierung wie im Report-Designer ist möglich.

20In der Datenbank «Beispiel_Formular_Eingabekontrolle.odb» ist hierzu eine Prozedur enthalten, die alle Anwendungen öffnet, die irgendwie als Dateiendung mit dem System verbunden sind: Webseiten, Email-Programme, Bilddateien, Textdateien ...

21Siehe Beispiel_Formular_Eingabekontrolle.odb

22Bei Nutzung dieser Möglichkeit der Kartendarstellung sollten die Bedingungen der Website beachtet werden: https://operations.osmfoundation.org/policies/nominatim/ .

23Die Datenbank «Beispiel_Mailstart_Dateiaufruf.odb» ist diesem Handbuch beigefügt.

24Die Arrayfunktion ist nur beim direkten Treiber so implementiert, dass die entsprechenden Werte auch in der Tabelle direkt eingebbar und sichtbar sind. Der JDBC-Treiber unterstützt Arrays nicht in gleichem Umfang.

25Die Beispieldatenbank «Beispiel_Dialoge.odb» zu den folgenden Kapiteln ist den Beispieldatenbanken für dieses Handbuch beigefügt.