Delphi-PRAXiS

Delphi-PRAXiS (https://www.delphipraxis.net/forum.php)
-   Object-Pascal / Delphi-Language (https://www.delphipraxis.net/32-object-pascal-delphi-language/)
-   -   Object mit SendMessage an MainThread senden (https://www.delphipraxis.net/180871-object-mit-sendmessage-mainthread-senden.html)

Captnemo 26. Jun 2014 10:16

Delphi-Version: 5

Object mit SendMessage an MainThread senden
 
Hi,

ich möchte ein Object aus meinem Thread an den Hauptthread senden.

So hab ich mir das vorgestellt. (TLogMessage ist hier nur ein Beispiel, was ich zum Testen genommen habe).

Verschicken:
Delphi-Quellcode:
var
  LogMsg: TLogMessage;
const
   PM_MsgFromThread = WM_USER +4;

procedure TComThread.SendMsgToMain(LogText: string; Debug: Boolean);
begin
  LogMsg:=TLogMessage.Create;
  LogMsg.sLogText:=LogText;
  LogMsg.bIsDebug:=Debug;
  SendMessage(FMainFormHandle, PM_MsgFromThread, Integer(@LogMsg), 0);
end;
Empfangen im Hauptthread:
Delphi-Quellcode:
procedure Tfrm_main.LogMsgFromThread(var msg: TMessage);
var
  P: TLogMessage;
  fdebug: Boolean;
  fLogText: string;
begin
  P:=Pointer(msg.WParam);
  fdebug:=P.bIsDebug;
  fLogText:=P.sLogText;
  //P.Free;                     //Ich müßte das Object auch wieder freigeben.
end;
Ich befürchte aber, dass ich es mal komplett falsch mache.
Auf P.bIsDebug kann ich noch zugreifen, und bei P.sLogText knallt es mit einer Zugriffsverletzung.

himitsu 26. Jun 2014 10:38

AW: Object mit SendMessage an MainThread senden
 
Integer ist nicht gut, stell dir mal vor man verwendet den Code mal im Win64.
NativeInt, oder für Pointer besser IntPtr, oder im Fall von SendMessage/PostMessage den OriginalTyp LRESULT.
Delphi-Quellcode:
function SendMessage(hWnd: HWND; Msg: UINT; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;


Was mag wohl passieren, wenn mal zwei Threads zugleich diese Variable nutzen wollen?



Objekte sind schon Pointer und lassen sich problemlos in "Integer" konvertieren, aber egal wie, man muß das schon richtig machen. :stupid:
Delphi-Quellcode:
LPARAM(@LogMsg) => PPointer(msg.LParam)^
LPARAM(LogMsg) => PPointer(msg.LParam) // ohne externe Variable
Also warum ist LogMsg eine globale Variable, und warum gibt es die überhaupt? :stupid:



Und ja, das müsstest du. (Free)

Captnemo 26. Jun 2014 11:11

AW: Object mit SendMessage an MainThread senden
 
Zitat:

Zitat von himitsu (Beitrag 1263537)
Objekte sind schon Pointer und lassen sich problemlos in "Integer" konvertieren, aber egal wie, man muß das schon richtig machen. :stupid:
Delphi-Quellcode:
LPARAM(@LogMsg) => PPointer(msg.LParam)^
LPARAM(LogMsg) => PPointer(msg.LParam) // ohne externe Variable
Also warum ist LogMsg eine globale Variable, und warum gibt es die überhaupt? :stupid:



Und ja, das müsstest du. (Free)

Global, so wie ich es aus dem verstanden habe, was ich gelesen habe, muß sie ja global sein, damit sie im Heap liegt und auch von anderen Threads überhaupt gelesen werden kann, oder ist das jetzt wieder falsch?

Aber ich hab jetzt meinen Fehler gefunden, lag in der Deklaration von P
Delphi-Quellcode:
  P: ^TLogMessage;
So funktioniert's zwar, aber beim Free hab ich immer noch eine Exception.

Das mit den Int64 macht natürlich Sinn.
Also so:
Delphi-Quellcode:
SendMessage(FMainFormHandle, PM_MsgFromThread, Intptr(@LogMsg), 0);
?

Leider hab ich Probleme, deine durchaus sinnvollen und hilfreichen Anmeldungen in meinen oben beschriebenen Code umzusetzen. Weil ich tatsächlich nicht weiß, ob ich das jetzt richtig interpretiert habe. Problematisch ist halt immer, wenn es oft mehrere Möglichkeiten gibt, genau das gleiche zu machen. Und wenn man dann in dem Thema nicht fit ist, dann würfelt man (in dem Fall ich) halt schnell was durcheinander.

stoxx 26. Jun 2014 12:23

AW: Object mit SendMessage an MainThread senden
 
müsste eigenlich sein: (ohne @)

Delphi-Quellcode:
Integer(LogMsg)
Weil ein Object ist schon selbst ein Pointer.
Ein TObject auf Integer zu casten hat eigentlich immer funktioniert.

Captnemo 26. Jun 2014 12:39

AW: Object mit SendMessage an MainThread senden
 
Dann bekomme ich wieder beim zugriff auf sLogText eine Zugriffsverletzung.

Ich hab's jetzt so:

Senden:
Delphi-Quellcode:
procedure TComThread.SendMsgToMain(LogText: string; Debug: Boolean);
begin
  LogMsg:=TLogMessage.Create;
  LogMsg.sLogText:=LogText;
  LogMsg.bIsDebug:=Debug;
  SendMessage(FMainFormHandle, PM_MsgFromThread, Intptr(@LogMsg), 0);
end;
Empfangen:
Delphi-Quellcode:
var
  f: TextFile;
  fname: string;
  P: ^TLogMessage;
  fdebug: Boolean;
  fLogText: string;
begin
  P:=Pointer(msg.WParam);
  fdebug:=P.bIsDebug;
  fLogText:=P.sLogText;
  //TLogMessage(p).free;
end;
Funktioniert auch gut. Aber ich müßte das Object auch mal wieder Freigeben. Aber das Problem ist, ich weißt nicht wirklich wie?
So wie oben? oder p.free?

Blup 26. Jun 2014 13:41

AW: Object mit SendMessage an MainThread senden
 
Du versendest die Adresse einer lokalen Variablen (@LogMsg) in der die Adresse des Objektes gespeichert ist.

Sinnvoller ist es natürlich die Adresse des Objektes zu versenden.
Delphi-Quellcode:
SendMessage(FMainFormHandle, PM_MsgFromThread, Integer(Pointer(LogMsg)), 0);

{...}
var
  P: TLogMessage;
begin
  P := Pointer(msg.WParam);
  {...}
end;
Vermutlich ist dir nicht klar, das diese Nachricht mit SendMessage noch innerhalb des Absender-Threads ausgeführt wird!
Wenn das Objekt beim Empfänger nur wärend der Verarbeitung der Nachricht benötigt wird, sollte es beim Absender danach wieder freigegeben werden.
Es ist ja nicht sichergestellt, dass der Empfänger die Nachricht auch in jedem Fall erhält.
Delphi-Quellcode:
  LogMsg:=TLogMessage.Create;
  try
    SendMessage(FMainFormHandle, PM_MsgFromThread, Integer(Pointer(LogMsg)), 0);
  finally
    LogMsg.Free;
  end;
Im Gegensatz dazu wird die Nachricht mit Postmessage erst einmal in die Nachrichtenschlange des Fensters geschoben und dort vieleicht irgendwann vom Hauptthread abgeholt.
Hier muss der Empfänger die Freigabe übernehmen. Allerdings ist auch hier nicht sichergestellt, dass die Nachricht den Empfänger erreicht. Dann würde ein Speicherleck entstehen.

Für diesen Fall ist es möglich ein eigene Verwaltung der Nachrichtenobjekte anzulegen (z.B. eine Objektliste auf die aus beiden Threads nur über eine CriticalSection zugegriffen wird). Der Mainthread wird als Observer per Postmessage benachrichtigt, dass neue Nachrichtenobjekte eingegangen sind, muss diese aber selbst abholen. Nicht verarbeitete Nachrichtenobjekte können so auch z.B. beim Beenden des Threads oder spätestens bei der Freigabe der Nachrichtenverwaltung freigegeben werden.

stoxx 26. Jun 2014 13:45

AW: Object mit SendMessage an MainThread senden
 
jetzt übergibst Du einen Pointer von einem Pointer .. also doppelt gemoppelt.
So wie ich das geschrieben habe, geht es auf jeden Fall. Ich habe für sowas sogar extra eine Klasse gebaut, die (urpsrünglich) normale Events als asynchrone Events genau nach dieser Methode vom Thread an den Hauptthread schickt.

Umwandlung dann natürlich:
... nicht Pointer() sondern TLogMessage()


Delphi-Quellcode:
var
  logMessage: TLogMessage;
  fdebug: Boolean;
  fLogText: string;
begin
  logMessage := TLogMessage(msg.WParam);

mit dem doppelt gemoppelten Pointer in Deiner jetzigen Version wäre das dann:

Delphi-Quellcode:
logMessage := TLogMessage(msg.WParam^);

jojo-sp 26. Jun 2014 13:53

AW: Object mit SendMessage an MainThread senden
 
Blup hat recht und ist die beste Lösung,

Du läufst Gefahr, wenn du z.B. den Text direkt in einem Label ausgeben möchtest, dass du deinen Thread mit der VCL synchronisieren musst und je nach Menge der Messages deinen Thread damit auch ausbremst.

Generier eine
Delphi-Quellcode:
ObjectList<TLogMessage>
und schließe einen Gegenseitigen Zugriff auf die Liste mit den Critical Sections aus.

Du brauchst dann auch nur den Index der neuesten Nachricht an den Empfänger senden anstatt des Objects oder eines Pointers auf das Object.

Klaus01 26. Jun 2014 13:56

AW: Object mit SendMessage an MainThread senden
 
Zitat:

Zitat von jojo-sp (Beitrag 1263567)
Du brauchst dann auch nur den Index der neuesten Nachricht an den Empfänger senden anstatt des Objects oder eines Pointers auf das Object.

.. wenn zwischenzeitlich nichts aus der Liste gelöscht wird,
ansonsten ist der überlieferte Index nicht mehr korrekt.

Grüße
Klaus

jojo-sp 26. Jun 2014 14:00

AW: Object mit SendMessage an MainThread senden
 
Stimmt!

Eigentlich sollte der Index eh irrelevant sein, da der Empfänger alle Nachrichten nacheinander abarbeitet (arbeiten sollte) und dann entfernt.

stoxx 26. Jun 2014 14:05

AW: Object mit SendMessage an MainThread senden
 
Zitat:

Zitat von jojo-sp (Beitrag 1263567)

Du läufst Gefahr, wenn du z.B. den Text direkt in einem Label ausgeben möchtest, dass du deinen Thread mit der VCL synchronisieren musst und je nach Menge der Messages deinen Thread damit auch ausbremst.

nein, läuft er nicht, genau deswegen nimmt er ja den Weg über Postmessage.
Muss nix synchronisiert werden. Postmessage geht übers Betriebssystem und kommt im Hauptthread der Anwendung wieder an und der Text kann dort in einem Label angezeigt werden.
Man spart sich den Aufruf von synchronize, der WorkerThread wird demnach auch nicht für einen kurzen Moment angehalten.
Bei hochperformanten Anwendungen sollte man dies so lösen.

Blup 26. Jun 2014 14:10

AW: Object mit SendMessage an MainThread senden
 
Zitat:

Zitat von stoxx (Beitrag 1263573)
nein, läuft er nicht, genau deswegen nimmt er ja den Weg über Postmessage.

Dazu muss erst mal auf PostMessage umgestellt werden, jetzt steht da noch SendMessage ...

Sir Rufo 26. Jun 2014 14:52

AW: Object mit SendMessage an MainThread senden
 
Und wozu das mit dem Rüberreichen über einen Pointer und woanders freigeben?
Geht das nicht noch etwas komplizierter?
Warum kein Delphi-Referenz durchsuchenTThread.Queue?

Es liest sich sehr unausgegoren und fehleranfällig an.

Blup 26. Jun 2014 15:22

AW: Object mit SendMessage an MainThread senden
 
Queue ist komfortabler aber erst in neueren Delphiversion möglich. Im Prinzip arbeitet es genauso wie Postmessage.
Das Problem Objekte sicher von einem Thread an einen anderen weiter zu reichen, wird dadurch allein aber nicht gelöst.

Sir Rufo 26. Jun 2014 15:51

AW: Object mit SendMessage an MainThread senden
 
Zitat:

Zitat von Blup (Beitrag 1263589)
Queue ist komfortabler aber erst in neueren Delphiversion möglich. Im Prinzip arbeitet es genauso wie Postmessage.
Das Problem Objekte sicher von einem Thread an einen anderen weiter zu reichen, wird dadurch allein aber nicht gelöst.

Delphi XE4 ist aber dafür schon neu genug ;)

jojo-sp 26. Jun 2014 15:57

AW: Object mit SendMessage an MainThread senden
 
Captnemo hat am Anfang angegeben er verwendet D5 ;-)

himitsu 26. Jun 2014 17:37

AW: Object mit SendMessage an MainThread senden
 
Zitat:

Zitat von stoxx (Beitrag 1263558)
müsste eigenlich sein: (ohne @)

Delphi-Quellcode:
Integer(LogMsg)
Weil ein Object ist schon selbst ein Pointer.
Ein TObject auf Integer zu casten hat eigentlich immer funktioniert.

Nja, man kann einen Pointer einem Objekt zuweisen ... Delphi interpretiert das dann sozusagen als TObject, welches intern ja ebenfalls ein Pointer ist.

Aber wie ich vorher shcon sagte:
Wenn man die globale Variable referenziert (@) und darauf den Zeiger überbergibt, dann muß am Ziel auch dieser Zeiger erstmal dereferenziert (^) werden,
und wenn man direkt den Objektzeiger übergibt, dann muß nur gecastet werden.



Und da Pointer was Böses sind, sollte man Diese möglicht vermeiden und schon gibt es keine Probleme mehr. :mrgreen:

Mavarik 26. Jun 2014 17:48

AW: Object mit SendMessage an MainThread senden
 
Warum TLogMessage als Object?

Delphi-Quellcode:
type
  PLogMessage   = ^TLogMessageMSG;
  TLogMessageMSG = record
                     sLogText : String[255];
                     bIsDebug : boolean;
                   end;
var
  LogMsg: PLogMessage;
const
   PM_MsgFromThread = WM_USER +4;

procedure TComThread.SendMsgToMain(LogText: string; Debug: Boolean);
begin
  New(LogMsg);
  LogMsg^.sLogText:=LogText;
  LogMsg^.bIsDebug:=Debug;
  SendMessage(frm_Main.Handle, PM_MsgFromThread,0,LParam(LogMsg));
end;
Empfangen im Hauptthread:
Delphi-Quellcode:
procedure Tfrm_main.LogMsgFromThread(var msg: TMessage);
var
  P: PLogMessage;
  fdebug: Boolean;
  fLogText: string;
begin
 try
   P := PLogMessage(MSG.LParam);
   fdebug:=P.bIsDebug;
   fLogText:=P.sLogText;
 finally
   Dispose(P);
 end;
end;
Mavarik

himitsu 26. Jun 2014 20:56

AW: Object mit SendMessage an MainThread senden
 
Ob Object oder Record-Pointer, ist fast egal. (der kleiner Overhead des Objects fällt hier praktisch garnicht auf)

Aber wenn schon globale Variable, warum dann einen Record-Pointer?
Der Record direkt als Variable und keiner muß den mehr freigeben und auch vorher nicht erstellen.

PS: Wenn es die globale Variable gibt, dann fällt eigentlich sofort auf, daß man sich hier den Parameter sparen kann, da man auch gleich auf die Variable zugreifen kann. :stupid:
Und schon fällt noch besser auf, warum diese Variable besser vernichtet gehört.

Medium 27. Jun 2014 10:24

AW: Object mit SendMessage an MainThread senden
 
Eine Liste mit durch Critical Sections gesichertem Zugriff ist imho fast die einzige vernünftige Art Daten unter verschiedenen Threads auszutauschen. Dem Formular dann nur noch per PostMessage() parameterlos mitteilen, dass es da neues drin gibt, und dieses arbeitet immer schon Index 0 ab und löscht aus der Liste bis sie leer ist. FiFo-Style.

Am Rande: SendMessage() ist in Threads, wenn ich mich nicht grad irre, eigentlich sogar ein ziemliches NoGo, da alles was die Empfänger dann tun auf ein Mal im Thread-Kontext statt findet. Und das kann je nach dem auch schon mal so richtig böse knallen, da es OS Ressourcen gibt, die nur im erstellenden Kontext verwendet werden dürfen. Davon ab pausiert der Thread bis alle Handler von den Empfängern fertig sind, und das ist ja nun auch nicht ganz der Sinn von Parallelisierung.

Captnemo 27. Jun 2014 10:50

AW: Object mit SendMessage an MainThread senden
 
So hab ich es jetzt letztlich im Moment auch gelöst. Im MainTread eine Tobjectlist, vom Thread innerhalb ein TCriticalSection Daten hinzu, und per postmessage benachrichtigen.

Aber ich will mir auch nochmal die TThread.Queue anschauen, denn das find auch ganz interessant.

himitsu 27. Jun 2014 14:44

AW: Object mit SendMessage an MainThread senden
 
Nein, SendMessage/PostMessage ist in Threads kein Problem.

Beides trägt die Message in den MessageQueue des Threads ein, in welchem die Komponente erstellt wurde, an die die Message geht.
Verarbeitet wird die Message dann immer von der Messagebehandlung im zugehörigen Thread (meist der Hauptthread).


TThread.Queue ist wie Synchronize, außer daß die Prozedur nicht sofort verarbeitet wird. (von dem "Bug" abgesehn, wenn man was vom MainThread aus daran übergibt)

stoxx 27. Jun 2014 15:32

AW: Object mit SendMessage an MainThread senden
 
Zitat:

Zitat von Medium (Beitrag 1263623)
Eine Liste mit durch Critical Sections gesichertem Zugriff ist imho fast die einzige vernünftige Art Daten unter verschiedenen Threads auszutauschen. Dem Formular dann nur noch per PostMessage() parameterlos mitteilen, dass es da neues drin gibt, und dieses arbeitet immer schon Index 0 ab und löscht aus der Liste bis sie leer ist. FiFo-Style.

kann man zustimmen


Zitat:

Zitat von Medium (Beitrag 1263623)
Davon ab pausiert der Thread bis alle Handler von den Empfängern fertig sind, und das ist ja nun auch nicht ganz der Sinn von Parallelisierung.

besser Postmessage, oder SendMessageTimeOut verwenden, da bei SendMessage bis zur Bestätigung der Verarbeitung gewartet wird, da hätte man dann auch gleich synchronize verwenden können, wo der Thread ebenso bis zum Ende der Verarbeitung angehalten wird.


Zitat:

Zitat von Medium (Beitrag 1263623)
Am Rande: SendMessage() ist in Threads, wenn ich mich nicht grad irre, eigentlich sogar ein ziemliches NoGo, da alles was die Empfänger dann tun auf ein Mal im Thread-Kontext statt findet. Und das kann je nach dem auch schon mal so richtig böse knallen, da es OS Ressourcen gibt, die nur im erstellenden Kontext verwendet werden dürfen.

nein, das stimmt nicht, das ist kein Problem.
PostMessage, SendMessage usw geht in de Queue vom jeweiligem Thread.
Überprüfen kannst Du, von welchem Thread die Funktion aufgerufen wird mit: GetCurrentThreadID

Wenn ein eigener Thread wirklich seine Nachrichten auch behandeln will, sofern er überhaupt welche bekommt, dann mit PeekMessage, TranslateMessage in der Execute Schleife von TThread.

Medium 27. Jun 2014 16:23

AW: Object mit SendMessage an MainThread senden
 
Danke für die Aufklärung! War bisher zu faul das genauer zu testen. Das Problem was ich mal hatte wodurch ich dazu kam, war gelöst als ich noch eine ganze Menge mehr umgebaut habe. Das war dann also schon mal nicht der Suppenspucker.

Sir Rufo 27. Jun 2014 17:42

AW: Object mit SendMessage an MainThread senden
 
Um noch einmal auf das Über- und Freigeben von Instanzen durch, von, über in Verbindung von Thread und Mainthread zu sprechen zu kommen ...

Was soll denn hier erreicht werden?

Nehmen wir einmal das Beispiel mit
Delphi-Quellcode:
TLogMessage
.

Wir haben da also einen Thread, der irgendwas bearbeitet und zwischendurch immer wieder etwas melden soll. Dieser Thread soll aber durch das Senden dieser LogMessage nicht mehr als nötig ausgebremst werden (seine Hauptaufgabe besteht eben nicht im Versenden dieser LogMessage). Ok.

Warum versendet dieser Thread diese LogMessage denn dann überhaupt?

Geben wir diesem Thread doch einen Kumpel an die Hand, der dieses Versenden für ihn übernimmt.
Und dieser Kumpel darf ja auch gerne wiederum ein Thread sein (macht ja nichts).
Delphi-Quellcode:
type
  TBaseMessage = class abstract
  end;

  TNotifyMessageEvent = reference to procedure( Sender : TObject; AMsg : TBaseMessage );

  TMessageService = class( TThread )
  private
    FCS : TCriticalSection;
    FEvent : TEvent;
    FMessages : TQueue<TBaseMessage>; <--- da kommen die rein
    FOnMessage : TNotifyMessageEvent;
    function GetOnMessage : TNotifyMessageEvent;
    procedure SetOnMessage( const Value : TNotifyMessageEvent );
    procedure DoSendMessage( AMessage : TBaseMessage );
  protected
    procedure Execute; override;
    procedure TerminatedSet; override;
  public
    constructor Create;
    destructor Destroy; override;

    procedure AddMessage( AMessage : TBaseMessage );

    property OnMessage : TNotifyMessageEvent read GetOnMessage write SetOnMessage;
  end;

constructor TMessageService.Create;
begin
  inherited Create( False );
  FCS := TCriticalSection.Create;
  FEvent := TEvent.Create( nil, False, False, '' );
  FMessages := TObjectQueue<TBaseMessage>.Create;
end;

destructor Destroy;
begin

  inherited;
  FMessages.Free;
  FEvent.Free;
  FCS.Free;
end;

function TMessageService.GetOnMessage : TNotifyMessageEvent;
begin
  FCS.Enter;
  try
    Result := FOnMessage;
  finally
    FCS.Leave;
  end;
end;

procedure TMessageService.SetOnMessage( const Value : TNotifyMessageEvent );
begin
  FCS.Enter;
  try
    FOnMessage := Value;
  finally
    FCS.Leave;
  end;
end;

procedure TMessageService.AddMessage( AMessage : TBaseMessage );
begin
  FCS.Enter;
  try
    FMessages.Enqueue( AMessage );
    FEvent.SetEvent; // <-- Den MessageService aufwecken
  finally
    FCS.Leave;
  end;
end;

procedure function TMessageService.GetOnMessage : TNotifyMessageEvent;
begin
  FCS.Enter;
  try
    Result := FOnMessage;
  finally
    FCS.Leave;
  end;
end;

procedure TMessageService.TerminatedSet;
begin
  inherited;
  FEvent.SetEvent;
end;

procedure TMessageService.DoSendMessage( AMessage : TBaseMessage );
var
  LOnMessage : TNotifyMessageEvent;
begin
  LOnMessage := OnMessage;
  if Assigned( LOnMessage ) then
    LOnMessage( Self, AMessage );
end;

procedure TMessageService.Execute;
var
  LMessage : TBaseMessage;
begin
  inherited;
  while not Terminated do
  begin
    FEvent.WaitFor; // Wir warten, bis sich was tut
    if not Terminated then
    begin
      // Nachricht aus der Queue holen
      FCS.Enter;
      try
        LMessage := FMessages.Extract;
        // Wenn noch Nachrichten in der Queue sind, dann den Event wieder setzen
        if FMessages.Count > 0 then
          FEvent.SetEvent;
      finally
        FCS.Leave;
      end;
      // Nachricht versenden
      Synchronize( procedure begin DoSendMessage( LMessage ); end );
      // Nachricht freigeben
      LMessage.Free;

    end;
  end;
Dieser MessageService übernimmt die Verwaltung für die Message-Instanz, liefert diese Message per Synchronize an wen auch immer aus. Das hat aber dann mit dem eigentlichen Thread, der da am arbeiten ist schon nichts mehr zu tun.

Schon ist der Drops gelutscht und jeder macht nur ein wenig und nicht alles oder kreuz und quer durcheinander gewürfelt.


Alle Zeitangaben in WEZ +1. Es ist jetzt 22:01 Uhr.

Powered by vBulletin® Copyright ©2000 - 2025, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024 by Thomas Breitkreuz