AGB  ·  Datenschutz  ·  Impressum  







Anmelden
Nützliche Links
Registrieren
Zurück Delphi-PRAXiS Tutorials Delphi Datenbank, auch für kleine Anwendungen (Einstieg)
Tutorial durchsuchen
Ansicht
Themen-Optionen

Datenbank, auch für kleine Anwendungen (Einstieg)

Ein Tutorial von oldmax · begonnen am 10. Jul 2006
Antwort Antwort
oldmax
Registriert seit: 27. Apr 2006
Auch wenn's viele besser wissen, hier eine kleine Anleitung, um von dem üblichen File-System wegzukommen. Auch in kleinen Anwendungen macht eine einfache Datenbank durchaus Sinn und ist allemal besser. Mit dieser kleinen Anleitung will ich dem Anfänger einmal zeigen, das es gar nicht so schwierig ist, dieses Thema anzufassen. Es ist nicht so tiefgreifend, das der Stoff schwierig wird. Ich denke, die vorgeschlagenen Routinen lassen sich auch für abgeleitete Anwendungen ausbauen und manch einer wird nun die Welt der Datenbankabfragen für seine Programme entdecken.

Beginnen wir mal ganz von vorn.
Als Beispiel nehme ich eine Adressdatei.
Verwaltet werden soll:

Vorname, Name, Strasse, PLZ, Ort, Telefon und EMail


Zuerst nehmen wir ein Datenmodul. Dies bekommt man unter Menüpunkt Datei-> Neu-> Datenmodul

Wir benennen nun die Form mit MyForm
und das Datenmodul mit MyDaten


In das Datenmodul ziehen wir eine Database und zwei Query sowie eine UpDateSQL
Diese Objekte nennen wir QAdressen, QTemp für die Queries und UpAdressen für das UpdateSQL-Objekt.
Diese Elemente solltet ihr im Register Datenzugriff finden.

Ich hoffe, ihr könnt es nachvollziehen, denn ich arbeite mit Delphi 3 und 4, daher sind die Vorgehensweisen vielleicht schon ein wenig betagt.

Aber weiter
Wieder im Delphi Menü gibt es einen Punkt Datenbank -> Explorer
Hier ist der BDE - Explorer, der die Möglichkeit bietet, sich einen Treiber zu schaffen
Einfach unter Objekt -> neu und man erhält die Standartvorlage, einen Paradox-Treiber mit Namen Standart1.
Diesen kann man überschreiben, z.B. mit MySQLData. Es wird noch der Pfad erwartet, wo die Datenbank eingerichtet wird. Na ja, Datenbank ist etwas zuviel gesagt, aber immerhin, die Zugriffe sind (fast ) dieselben...

Anschließend wird der eingerichtete Treiber, wieder unter Objekt, -> speichern unter.. mit dem angegebenen Defaultwert gespeichert.
Damit ist der Treiber fertig. Was nun fehlt, ist die Verbindung vom der Database in MyDaten zur Datenbank. Um dies zu erreichen, klicken wir mit der rechten Maustaste hinein und erhalten ein Formular. In der Mitte ist eine Dropdownliste, in der wir unseren Treiber wiederfinden. Links das Feld wird mit einem Alias beschreiben, wie z.B. MyConnect. Rechts das Feld bleibt frei. Nun werden aus den beiden Checkboxen die Häkchen entfernt. Fertig.
Wir müssten nun im Objektinspektor der Database schon connected auf true setzen können, ohne das es Beschwerden gibt.
Bevor wir nun die QAdressen parametrieren, sollten wir unserer Datenbank eine Tabelle verpassen. Wir schreiben eine kleine Procedure in MyDaten und rufen diese über die MyDatenCreate-Routine auf
Hier ein Beispiel

Delphi-Quellcode:
Procedure TMyDaten.Generate_Tabelle;
Var Tabelle : TTable;
Begin
  Tabelle:=TTable.Create(MyDaten);
  Tabelle.DatabaseName:='MyConnect';
  Tabelle.TableName:='Adressen';
  Tabelle.TableType:=ttDefault;
  Tabelle.Name:='Adressen';
  Tabelle.FieldDefs.Clear;
  Tabelle.IndexDefs.Clear;
  if not Tabelle.exists then // geht nur ab Delphi 4, sonst weglassen und später ausklammern
  begin
    Tabelle.FieldDefs.Add('Ident',ftInteger,0,True); // ich mach mir immer einen eigenen Ident
    Tabelle.FieldDefs.Add('Vorname',ftString,50,False); // Tabellenfeld Vorname 50 Zeichen
    Tabelle.FieldDefs.Add('Name',ftString,50,False); // Tabellenfeld Name 50 Zeichen
    Tabelle.FieldDefs.Add('Strasse',ftString,30,False); // Tabellenfeld Strasse 30 Zeichen
    Tabelle.FieldDefs.Add('PLZ',ftString,6,False); // usw
    Tabelle.FieldDefs.Add('Ort',ftString,30,False);
    Tabelle.FieldDefs.Add('Telefon',ftString,30,False);
    Tabelle.FieldDefs.Add('EMail',ftString,30,False);
    Tabelle.IndexDefs.Add('', 'Ident', [ixPrimary, ixUnique]); // und setzte ihn auch als einziartig
    Tabelle.CreateTable;
    end;
  end;
  Tabelle.Free; // nicht vergessen, Speicher freigeben, Tabelle liegt nun im angegebenen Verzeichnis
end;

Hierbei ist wichtig, das MyDaten vor MyForm aufgerufen wird. Wir erreichen dies im Delphi-Menüe unter Optionen, indem wir bei Formularen MyDaten einfach nach oben schieben.
Nun können wir unsere Tabelle an die QAdressen anbinden. Wieder die rechte Maustaste in QAdressen und den SQL-Buider aufrufen. In Datenbank kommt über die DropDown Liste der Eintrag MyConnect. Danach müßte die Tabelle Personen im Tabellenfeld sichtbar, bzw.über die DropDown Liste erreichbar sein. Diese Tabelle holen wir uns und selektieren alle Felder. Anschließend schauen wir uns die SQL an und führen diese einmal aus. Nun können wir sicher sein, das unsere QAdressen mit der Tabelle verbunden ist. Folgende Anpassung nehmen wir nun im Objektinspektor von QAdressen vor:
Active Schalten wir auf true. Funktioniert ? Gut, weiter. CachedUpdate ebenfalls True und ganz unten Updateobject, da wählen wir aus der Dropdown das zugehörige SQLUpdate-Objekt, unsere UpAdressen. (dürfte nur eines drin sein, aber irgendwann sind's vielleicht mal mehr....)
Ok, jetzt noch einmal mit der rechten Maustaste in UpAdressen clicken und die UpdateSQL erzeugen. Dazu muß in den Schlüsselfeldern auf der linken Seite nur der Ident, in den UpdateFeldern rechte Seite alle Einträge markiert sein. Einmal bitteschön SQL erzeugen drücken und sich die SQL's für Insert, Modify und delete einmal ansehen, fertig.

Gut, wir haben eine Tabelle für Adressen. In diese möchten wir nun gern ein paar Daten schreiben. Daher wechseln wir in unsere Anwenderform und bauen die Speichern-Routine auf. Dazu muß man wissen, Der Eintrag, der gespeichert wird, ist entweder neu oder nur editiert und überarbeitet. Daher braucht man immer einen eindeutigen Schlüssel für einen neuen Datensatz und einen aktuellen Schlüssel für den editierten Datensatz. Um einen neuen Ident zu bekommen benutze ich eine temporäre Query, unsere QTemp.

Dann schreibe ich eine Funktion, deren Aufgabe es ist, eine Zahl zu liefern, die um eins größer ist, als der höchste Ident in der Tabelle.

Delphi-Quellcode:
Funktion TMyForm.New_Ident(TabellenName: String):Integer; // könnte dann auch für andere Tabellen funktionieren
Var SQLSatz : String;                // Ich nehme immer eine Variable zu hilfe            
Begin
   SQLSatz:='Select Ident from '+TabellenName';       //dadurch bleiben auch lange SQL's übersichtlich
   SQLSatz:=SQLSatz+' order by Ident';          // und natürlich nach ident sortiert
   MyFormDatamodule.QTemp.Close;
   MyFormDatamodule.QTemp.SQL.Clear;       // alte SQL - Anweisung löschen
   MyFormDatamodule.QTemp.SQL.Add(SQLSatz);    // und manchmal machts Sinn, hier nochmal zu prüfen
   MyFormDatamodule.QTemp.Open
   if MyFormDatamodule.QTemp.RecordCount>0 then
   begin
     MyFormDatamodule.QTemp.Last;          // Zeiger auf letzten Datensatz
     Result:=MyFormDatamodule.QTemp['Ident']+1;    // bedenkt, das der Ident nicht immer durchgängig ist
   end else Result =1;
end;

Nun setzen wir für jedes Tabellenfeld ein Edit-Feld und vergeben Namen

Ed_Ident, ED_Vorname, Ed_Name, ED_Strasse,ED_Plz,ED_Ort,Ed_Telefon und ED_EMail
ED_Ident wird auf Enabled:=False gesetzt, weil dort keine Eingaben gemacht werden darf.
Zusätzlich plazieren wir ein Butten mit Namen Bt_Neu und ein Button mit Namen BT_Save und eine Combobox mit Namen CB_Namen.
Mit Bt_Neu löschen wir alle Feldinhalte und rufen die Function New_Ident mit Angabe des Tabellennamens über die Zuweisung für Ed_Ident.Text auf.

Delphi-Quellcode:
procedure TMyForm.Bt_NeuClick(Sender: TObject);
begin
  ED_Ident.Text:=InttoStr(New_Ident('Adressen'));
  ED_Vorname.Text:='-';
  Ed_Name.Text:='-';
  ED_Strasse.Text:='-';
  ED_Plz.Text:='-';
  ED_Ort.Text:='-';
  Ed_Telefon.Text:='-';
  ED_EMail.Text:='-';
end;

Das wir den Tabellennamen mitgeben, hat den Vorteil, ich kann diese Funktion auch für andere Tabellen nutzen, vorausgesetzt, mein Indexfeld ist immer gleich benannt mit 'Ident'.

Anschließend füllen wir die restlichen Editfelder mit Werten. Stellt bitte sicher, das in keinem Editfeld ein Leerstring steht, manche Datenbanken mögen das gar nicht und bevor man da herumzaubert, ist es sinnvoll, in Felder mit Stringinhalten einfach '-' hineinzusetzen oder bei Zahlenwerten '0'. Dies kann wie bereits niedergeschrieben, in der Ereignisroutine der Bt_Neu geschehen.

Nun kommt die Routine Speichern dran:


Delphi-Quellcode:
Procedure TMyForm.Save_Adressen(Id: Integer;VName, ZName,Str,Plz,Ort,Tel,Mail:String);
Var SQLSatz: String;
Begin
   SQLSatz:=Select * From Adressen ';       // Tabellenname hier über Variable einzugeben macht keinen Sinn
SQLSatz:=SQLSatz+
' Where (Ident='''+IntToStr(ID)+''')'; // ich will den Eintrag mit der übergebenen ID
MyFormDatamodule.QAdressen.Close;         
MyFormDatamodule.QAdressen.SQL.Clear;       // alte SQL - Anweisung löschen
MyFormDatamodule.QAdressen.SQL.Add(SQLSatz);    // und manchmal machts Sinn, hier nochmal zu prüfen
MyFormDatamodule.QAdressen.Open
if MyFormDatamodule.QAdressen.RecordCount>0 then // ab hier ist klar, datensatz wurde editiert
begin
MyFormDatamodule.QAdressen.Edit;
MyFormDatamodule.QAdressen[
'Vorname']:=VName; // Werte aus den Editfeldern..
MyFormDatamodule.QAdressen[
'Name']:=ZName; // meckert nicht rum, ich weiß auch elegantere Lösungen
MyFormDatamodule.QAdressen[
'Strasse']:=Str; // ist aber für Anfänger besser, sie verstehen was da abläuft
MyFormDatamodule.QAdressen[
'Plz']:=Plz; // und jeder kann sich seine eigenen Routinen ableiten...
MyFormDatamodule.QAdressen[
'Ort']:=Ort;
MyFormDatamodule.QAdressen[
'Telefon']:=Tel;
MyFormDatamodule.QAdressen[
'EMail']:=Mail;
MyFormDatamodule.UpAdressen.Apply(ukModify); // Falls unbekannt, zieht kurz eine Query in die Form
                   // außerden ist die SQLUpdate umbenannt in UpAdressen
end else
Begin                   // hier ist ein neuer Datensatz erkannt
MyFormDatamodule.QAdressen.Append;
MyFormDatamodule.QAdressen[
'Ident']:=ID; // diese Zeile ist hier zusätzlich
MyFormDatamodule.QAdressen[
'Vorname']:=VName; // die restlichen Zuweisungen entsprechen denen der Edit..
MyFormDatamodule.QAdressen[
'Name']:=ZName;
MyFormDatamodule.QAdressen[
'Strasse']:=Str;
MyFormDatamodule.QAdressen[
'Plz']:=Plz;
MyFormDatamodule.QAdressen[
'Ort']:=Ort;
MyFormDatamodule.QAdressen[
'Telefon']:=Tel;
MyFormDatamodule.QAdressen[
'EMail']:=Mail;
MyFormDatamodule.UpAdressen.Apply(ukInsert);
end;
end;
Wenn unsere Daten in die Editfelder eingetragen sind, können wir mit Bt_Save die Speicherroutine aufrufen.

Delphi-Quellcode:
procedure TForm1.Bt_SaveClick(Sender: TObject);
begin
  Save_Adressen(StrToInt(Ed_Ident.Text),ED_Vorname.Text,ED_Name.Text,ED_Strasse.text,
                                         ED_Plz.Text,ED_Ort.Text,ED_Telefon.Text,Ed_EMail.Text);
end;
Natürlich sieht's ein bischen blöd aus, aber ich bin mir da sicher, es fällt euch was gescheiteres ein. Man hätte auch auf Parameter verzichten können und die Ed_xxx.Text - Felder direkt zuweisen können....

Also, ich mach es mit einem String, der die einzelnen Werte durch Semikolon getrennt in einem Rutsch rüberbringt. Innerhalb der Procedure wird er wieder zerlegt. Ist auch nicht soooo aufwändig, wie's erscheint. Aber das ist String-Bearbeitung. Da denkt selber mal drüber nach... und wenn's gar nicht klappt, na ja, dann fragt halt nach und ich schreib darüber ein separates Tutorial.

So, Fehlt noch die Lese-Procedure. Wir möchten wir einzelne Datensätze aufrufen, am besten ausgewählt aus einer sortierten Namensliste. Die hatten wir ja schon eingebaut und Cb_Namen genannt.

Nun gehen wir folgendermaßen vor:
Wir schreiben eine Leseroutine, die nur den Namen und den Ident lädt und die Werte in die Listen einträgt. Für den Namen nehmen wir, klar, die Combobox Cb_Namen, für den Ident eine StringList mit Namen AdrIdentList. Nun kann man über Cb_Namen einen Namen selektieren und den eigentlichen Datensatz holen. Auch hier verwenden wir zum Lesen aller Namen die QTemp und wir sortieren nach Namen.


Delphi-Quellcode:
Procedure TMyForm.Load_Adressen;
Var SQLSatz : String; // Meine Stringvariable zum Aufbau der SQL-Anweisung
      i : Integer; // Laufzeitvariable definiere ich immer lokal
Begin
   AdrIdentList.Clear; // diese Stringlist hier zuerst leeren
   CB_Namen.Items.Clear; // diese Combobox auch
   SQLSatz:=Select Ident, Name from Adressen '; // nur die beiden Felder Ident und Name
SQLSatz:=SQLSAtz+
' Order by Name'; // und sortiert nach Name
MyFormDatamodule.QTemp.Close;
MyFormDatamodule.QTemp.SQL.Clear;       // alte SQL - Anweisung löschen
MyFormDatamodule.QTemp.SQL.Add(SQLSatz);    // und manchmal machts Sinn, hier nochmal zu prüfen
MyFormDatamodule.QTemp.Open
if MyFormDatamodule.QTemp.RecordCount>0 then
begin
MyFormDatamodule.QTemp.First; // Datensatzzeiger auf ersten Eintrag
For i:=0 to MyFormDatamodule.QTemp.RecordCount-1 do // weil bei 0 angefangen, deshalb -1
begin
AdrIdentList.Add(IntToStr(MyFormDatamodule.QTemp[
'Ident'])); // hier wird die Ident-Liste gefüllt
Cb_Namen.Items.Add(MyFormDatamodule.QTemp[
'Name']); // und hier die Namen - Liste
MyFormDatamodule.QTemp.Next; // Zeiger auf nächsten Datensatz
end;
end;
end;

Nun kommt noch eine Procedure, die nur einen ausgewählten Eintrag zur Anzeige bringt. Dazu nehmen wir wieder die QAdressen

Delphi-Quellcode:
Procedure TMyForm.Show_Adresse(Id: Integer);
Var SQLSatz: String; // Meine Stringvariable zum Aufbau der SQL-Anweisung
Begin
   SQLSatz:=Select * From Adressen ';       // Tabellenname hier über Variable einzugeben macht keinen Sinn
SQLSatz:=SQLSatz+
' Where (Ident='''+IntToStr(ID)+''')'; // ich will den Eintrag mit der übergebenen ID
MyFormDatamodule.QAdressen.Close;         
MyFormDatamodule.QAdressen.SQL.Clear;       // alte SQL - Anweisung löschen
MyFormDatamodule.QAdressen.SQL.Add(SQLSatz);    // und manchmal machts Sinn, hier nochmal zu prüfen
MyFormDatamodule.QAdressen.Open
if MyFormDatamodule.QAdressen.RecordCount>0 then // ab hier ist klar, Daten vorhanden
begin
ED_Ident.Text:=IntToStr(MyFormDatamodule.QAdressen[
'Ident']);
ED_Vorname.Text:=MyFormDatamodule.QAdressen[
'Vorname'];
ED_Name.Text:=MyFormDatamodule.QAdressen[
'Name']:=ZName;
ED_Str.Text:=MyFormDatamodule.QAdressen[
'Strasse']:=Str;
ED_Plz.Text:=MyFormDatamodule.QAdressen[
'Plz'];
ED_Ort.Text:=MyFormDatamodule.QAdressen[
'Ort'];
ED_Tel.Text:=MyFormDatamodule.QAdressen[
'Telefon'];
Ed_Mail.Text:=MyFormDatamodule.QAdressen[
'EMail'];
end else
begin
Ed_Ident.Text:=
'0'; // und auch diesen Fall berücksichtigen....
ED_Vorname.Text:=
'-'; // das keine Daten gefunden werden
ED_Name.Text:=
'-';
ED_Str.Text:=
'-';
ED_Plz.Text:=:=
'-';
ED_Ort.Text:=
'-';
ED_Tel.Text:=
'-';
Ed_Mail.Text:=
'-';
end;
end;
Die Procedure Show_Adresse(x) rufen wir in der Ereignisroutine OnChange der Combobox auf. Dazu wissen wir, der Eintrag, den wir selektiert haben steht unter einem ItemIndex. Diesen können wir auch der Liste entnehmen und erhalten eine Integerzahl. An der gleichen Stelle steht in der AdrIdentList der Eintrag mit der Datensatznummer, dem Ident, und genau diesen brauchen wir auch wieder als Integer. Daher muß er vom String zurückgewandelt werden.

Aber so kompliziert ist das alles gar nicht, es geht in einer einzigen Zeile, das ich die Lese-Routine mit dem onChange der Combobox aufrufe;

Delphi-Quellcode:
procedure TMyForm.Cb_NamenChange(Sender: TObject);
begin
  Show_Adresse(StrToInt(AdrIdentList[CB_Namen.ItemIndex])); //geballte Ladung, kaum zu kommentieren...
end;
So geht's natürlich auch unter Verwendung lokaler Variablen

Delphi-Quellcode:
procedure TMyForm.Cb_NamenChange(Sender: TObject);
Var ListPos : Integer;
     Id_String : String;
     DatensatzNr : Integer;
begin
  ListPos:=CB_Namen.ItemIndex; // Position des Namens in der Liste
  Id_String:=AdrIdentList[ListPos]; // Inhalt aus der gleichen Position der AdrIdentList
  DatensatzNr:=StrToInt(Id_String); // String umwandeln in Integer
  Show_Adresse(DatensatzNr); // Aufruf der Laderoutine für einen den Datensatz, der zum Namen gehört
end;

Es macht vielleicht Sinn, die Variable DatensatzNr global zu vereinbaren, denn wenn ein Datensatz gelöscht wird, wäre diese Variable geeignet, übergeben zu werden. Bedenkt nur, das ihr beim Programmstart auch diese Variable setzt.
Hier versuche ich es noch einmal zu erklären, falls es doch noch nicht verstanden ist:
Show_Adresse soll einen Eintrag über Ident finden. Dieser Ident steht unter dem gleichen Index in der IdentList, wie der zugehörige Name. Und ItemIndex ist der Index, der uns den Eintrag aus der Identliste holt, allerdings als String, daher müssen wir noch diese Function StrToInt um das ganze basteln....
Ihr könnt aber auch gern lokale Variable vereinbaren und die Selektion schritt für Schritt machen.

Und nicht vergessen, die Stringlist bei den globalen Variablen deklarieren und in der FormCreate generieren !
Also,

Delphi-Quellcode:
var
 ..........
 AdrIdentList : TStringList;
  .........
und in FormCreate


Delphi-Quellcode:
Begin
  .........
  AdrIdentList:=TStringlist.Create;
  .........




Ach ja, fast hätt ich's vergessen. Da hatt doch neulich einer eurer Freunde den gesammten Biervorrat gekillt und ihr habt euch entschlossen, diesen Schmarotzer ein für allemal aus eurer Adressliste zu strei c h e n......, nur wie ?

Ok, das ist auch ganz schnell abgehakt:

Schnell noch ein Button Bt_Del in eure Form und eine Procedure

Delphi-Quellcode:
Procedure TMyForm.Del_Eintrag(EintragNr:Integer); // EintragNr wird zur eindeutigen Identifizierung benötigt.
Var SQLSatz: String; // Meine Stringvariable zum Aufbau der SQL-Anweisung
Begin
   SQLSatz:=Select * From Adressen ';       // Tabellenname hier über Variable einzugeben macht keinen Sinn
SQLSatz:=SQLSatz+
' Where (Ident='''+IntToStr(ID)+''')'; // ich will den Eintrag mit der übergebenen ID
MyDaten.QAdressen.Close;         
MyDaten.QAdressen.SQL.Clear;       // alte SQL - Anweisung löschen
MyDaten.QAdressen.SQL.Add(SQLSatz);    // und manchmal machts Sinn, hier nochmal zu prüfen
MyDaten.QAdressen.Open
if MyDaten.QAdressen.RecordCount>0 then // ab hier ist klar, Datensatz vorhanden
begin
..... // baut hier noch eine Sicherheitsabfrage ein.
// Messagedlg ist gut geeignet
If MessageDlg(
'soll er wirklich gehen ?',MtInformation,[mbYes,mbNo],0)=mrYes then
MyDaten/.UpAdressen(ukDelete); // bevor ihr eure Freunde in die Wüste schickt.....
end;
end;
Ich glaube, jetzt habt ihr einen kleinen Einstieg in Datenbankzugriffe über SQL bekommen. Ach ja, nach Speicher- und Löschaktionen solltet ihr unbedingt die Daten neu einlesen und die Listen aktualisieren.


nun noch ein Hinweis:
Um über den gesamten Umfang SQL Informationen zu bekommen, ich habe mit den Microsoft Unterlagen gearbeitet Sprachverzeichnis Datenzugriff Office 95. Da steht eine ganze Menge über SQL-Zugriffe drin.
Aber für ein Adressbuch sollte es erst einmal reichen.
Und nun viel Spaß


Gruß oldmax



Ps: Wer Schreibfehler findet, der darf sie behalten...
Noch ist mein Rechner mir zu Diensten.... ansonsten habe ich die Macht ihn zu vernichten !
 
Antwort Antwort


Forumregeln

Es ist dir nicht erlaubt, neue Themen zu verfassen.
Es ist dir nicht erlaubt, auf Beiträge zu antworten.
Es ist dir nicht erlaubt, Anhänge hochzuladen.
Es ist dir nicht erlaubt, deine Beiträge zu bearbeiten.

BB-Code ist an.
Smileys sind an.
[IMG] Code ist an.
HTML-Code ist aus.
Trackbacks are an
Pingbacks are an
Refbacks are aus

Gehe zu:

Impressum · AGB · Datenschutz · Nach oben
Alle Zeitangaben in WEZ +1. Es ist jetzt 07:13 Uhr.
Powered by vBulletin® Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024 by Thomas Breitkreuz