AGB  ·  Datenschutz  ·  Impressum  







Anmelden
Nützliche Links
Registrieren
Zurück Delphi-PRAXiS Tutorials Delphi Tut: Erstellung von Komponenten

Tut: Erstellung von Komponenten

Ein Tutorial von sakura · begonnen am 21. Jun 2002 · letzter Beitrag vom 16. Okt 2002
Tutorial geschlossen
Benutzerbild von sakura
sakura
Registriert seit: 10. Jun 2002
Hi DP-ler,

wollen wir mal wieder. Die Beiträge zum Thema Threads habe ich leider nicht mehr und zur Zeit steht mir auch der Kragen nicht so sehr danach. Deshalb werde ich ganz einfach ein bisschen zum Thema Komponenten-Erstellung schreiben.

Wenn Ihr Wünsche habt, zu bestimmten Komponenten und Features, welche im Laufe der Zeit hier auftauchen sollten, so lasst es mich einfach wissen. Am besten per Private Message. Kommentare zu dem hier geschriebenen einfach in den Thread schreiben. Mit der Zeit werde ich diese dann zusammenfassen, damit der Überblick nicht verloren geht.

Nun aber zum Tutorial

In diesem Tutorial möchte ich Euch ein wenig der Komponentenentwicklung näher bringen. Angefangen werde ich mit ein wenig trockener Theorie und dann geht es langsam weiter. Anfangen werden wir mit einer non-visual Komponente, welche es ermöglicht Benutzereinstellungen in der Registry zu speichern, weiter geht es mit einer Graphic Control, welche sich auf dem Form darstellt, aber keinen Fokus erhalten kann. Anschliessend werden wir eine interaktive Komponente erstellen, mit Collections arbeiten und abschliessend eine DB-Komoponente.

Was sonst noch?

An den Bereich der Komponentenentwicklung schliessen sich weitere Delphi-Bereiche an
  • Exception Handling
  • Streaming
  • RTTI (Run-Time Type Information)
  • Interfaces
  • ActiveX Komponenten
  • ...

Einige dieser Bereiche werden wir gesondert betrachten und andere werden direkt in die oben genannten Bereich mit einfließen.

Was brauchen wir, um mitzumachen

Ein wenig Zeit, Geduld und Delphi. Für Teile des Kurses sind mindestens die Professional Versionen nötig. Das meiste sollte ab Version 4 laufen, erstellt werden alle Beispiele mit Delphi 5 Enterprise. In Delphi 6 gab es ein paar Umbenennungen und Teilungen der vorhandenen Units. Einfach ausprobieren... Ausserderm sollten die Grundlagen der Objekt-orientierten Programmierung vorliegen, damit wir nicht bei nil anfangen.

Meine Delphi Version ist Englisch, daher werden alle Screen Shots in Englisch sein. Wird schon schief gehen...
Ich bin nicht zurück, ich tue nur so
 
Benutzerbild von sakura
sakura

 
Delphi 11 Alexandria
 
#2
  Alt 21. Jun 2002, 13:46
Hi DP-ler,

getrennt von der Einleitung gibt es jetzt erst einmal die erste Runde Theorie. Damit es nicht zu trocken wird, fangen wir mit einer kleinen Grafik an, welche so ähnlich im Delphi 5 Entwickler Handbuch (31-2) zu finden ist, da das aber nicht bei jedem vorliegt, habe ich diese schnell noch einmal erstellt.

http://www.gatenetwork.com/delphi-sa...ts/classes.gif

Alle Klassen, welche in Delphi erstellt werden sind ultimativ von der Basis-Klasse TObject abgeleitet. Die Klasse TObject definiert die grundlegenden Methoden für jedes Objekt, welches in OO-Pascal (der Sprache von Delphi) erstellt wird.

Abgeleitet von TObject ist die Klasse TPersistent. Diese Klasse stellt grundlegende Methoden zum Kopieren (Assign/AssignTo) und zum Streamen nicht-publizierter Eigenschaften zur Verfügung. Die Basis dieser Klasse ermöglicht es der Delphi-IDE mit Drag-&-Drop und dem Object Inspector uns Programmierern mit der VCL zu arbeiten.

VCL

VCL ist die Abkürzung für die Visual Component Library. Die VCL ist die Sammlung aller installierten Komponenten, welche via Drag-&-Drop auf dem Form im Design-Modus angeordnet werden können. Die Nutzung der VCL macht es uns Programmierern leicht, sich auf das Eigentliche, die Erstellung der Programmlogik, zu konzentrieren, da die Erstellung der Benutzerschnittstellen in einem Bruchteil der Zeit erledigt ist.

Dieser Vorteil bringt natürlich auch seine (Geschmackssache: großen) Nachteile mit sich. Die Programme (EXE Dateien) werden um einiges größer, da immer das komplette Handling der VCL mitkompiliert wird. Aber wozu gibt es große Festplatten? Ausserdem werden die Programme meist auch etwas langsamer gegenüber nonVCL-Applikationen. Aber der Unterschied ist nur gering - und ausserdem gibt es schnelle Prozessoren. Wer trotzdem Interesse an nonVCL Programmen hat, sollte sich mal auf Luckies Seite umschauen.

Aber nun zurück zu den Komponenten und der VCL...

Was ist der Unterschied zwischen Komponenten und Controls?

Zurückblickend auf die obige Grafik ist die Antwort eigentlich schnell gefunden. Alle Controls sind Komponenten, da sie von TComponent abgeleitet werden, aber nicht alle Komponenten sind Controls. Was heisst das jetzt für uns?

Die Klasse TComponent führt alle Methoden ein, welche die Delphi IDE benötigt, um Komponenten in der VCL Palette darzustellen. Das heisst für uns, dass alle VCL Komponenten, welche wir erstellen wollen generell von der Klasse TComponent abgeleitet werden müssen. Ein solches Beispiel ist zum Beispiel die Komponente TTimer von der System Palette.

Eine Komponente, welche direkt von TComponent, nicht von TControl, abgeleitet wurde, wird im Delphi-Form-Designer generell durch ein kleines Quadrat dargestellt. Darunter erscheint, sofern man diese Option eingschaltet hat, der Name der Komponente. Diese Komponenten sind zur Laufzeit des Programmes nicht sichtbar und arbeiten im Hintergrund. Solche Komponenten werden direkt durch den Code vom Programmierer gesteuert. Auch die Dialog-Komponenten, wie zum Beispiel TOpenDialog gehören in die Gruppe der Komponenten, nicht zu den Controls. Die Komponente ist zur Laufzeit nicht zu sehen. Sie kann lediglich einen Dialog anzeigen, welcher ansonsten nicht viel mit der Komponente zu tun hat.

Was sind jetzt Controls? Eigentlich ist das jetzt recht einfach - der ganze Rest der Komponenten der VCL Palette, also all jene, welche auch zur Laufzeit zu sehen sind. Da wären die grafischen Controls wie das TLabel und TPaintBox und die "windowed" Controls wie TEdit und TButton.

TGraphicControl und TWinControl

Was soll dieser Unterschied? Eigentlich recht einfach. TGraphicControl ist die Basis aller Controls, welche sich auf dem Form darstellen können, aber keinen Windows-Handle haben und somit auch nicht den Fokus erhalten können.

TWinControl steht nicht für Controls welche durch das OS (Windwows) zur Verfügung gestellt werden, sondern für "windowed" Controls. Das heisst, diese Controls "registrieren" sich bei Windows und erhalten einen Windows-Handle. Dadurch sind solche Controls in der Lage Windows-Botschaften zu erhalten. Diese Controls können auch, sofern das vorgesehen ist, den Fokus durch den Benutzer erhalten.

Für nächsten Mittwoch ist die nächste Runde angesetzt. Dann werden wir nicht gleich mit dem Erstellen einer Komponente anfangen, sondern noch ein wenig Theorie pauken. Thema werden die Exceptions sein.

Bis dann!

[edit 1]Luckies Link auf den aktuellsten Stand gebracht [/edit]
Daniel W.
 
Benutzerbild von sakura
sakura

 
Delphi 11 Alexandria
 
#3
  Alt 24. Jun 2002, 15:42
Hi DP-ler,

wie versprochen fangen wir mit den Exceptions an. Exceptions sind Delphis Weg, um Fehler "abzufangen" und dem Anwender einer Anwendung darzustellen. Mit Hilfe der Konstrukte try...except...end und try...finally...end kann der Programmier im Falle einer Exception (eines Fehlers) diesen abfangen bzw. notwendige "Aufräumarbeiten" werden durchgeführt, bevor der Fehler angezeigt wird.

Dieser Abschnitt des Kurses bildet die Grundlage für den nächsten. Wir werden als erstes eine Komponente beginnen, welche es uns ermöglicht die Standardfehlermeldung von Delphi zu umgehen und eigene Wege zu beschreiten.

try...except...end

Es gibt in der Programmierung immer wieder Bereiche, in welchen man davon ausgehen muss, dass es füher oder später zu Problemen im Code kommen wird. Die wohl üblichste Fehlerquelle ist eine falsche Eingabe durch den Benutzer.
Fangen wir doch mal mit einem sehr einfachen Beispiel an. Der Nutzer soll eine Zahl eingeben, welche später für Rechenoperationen genutzt wird. Wenn der Nutzer aber etwas anderes (oder nichts) eingibt, dann liegt kein gültiger Wert für die Rechenoperation vor.
Um das Beispiel ein wenig zu verdeutlichen, werden wir die folgende Prozedur etwas genauer betrachten.
Code:
function DivideIt_01(Zahl: String): String;
begin
  Result := FloatToStr(1000 / StrToFloat(Zahl));
end;
Zugegeben, die Funktion ist sehr einfach. Aber anhand dieser Funktion lässt sich das ganze sehr leicht darstellen. Übergeben wird der Parameter Zahl als String. Zahl wird in eine Fließkommazahl (StrToFloat) umgewandelt. Dieser Wert wird dann genutzt als Teiler für den Wert 1000 genutzt.
Es gibt jetzt zwei typische, potentielle Fehlerquellen.
  • Der Wert des Parameters Zahl ist keine Fließkommazahl
  • Der Parameter Zahl ist '0' - dann erfolgt eine Division durch Null.
Die gezeigt Funktion würde den Fehler nicht erkennen. Dadurch würde der an die aufrufende Funktion weitergeleitet werden. Folgende Funktion zeigt die simpelste Möglichkeit diesen Fehler mit Hilfe eines try...except...end Blockes abzufangen.
Code:
function DivideIt_02(Zahl: String): String;
begin
  try
    Result := FloatToStr(1000 / StrToFloat(Zahl));
  except
    Result := 'Der übergebene Wert ist leider ungültig';
  end;
end;
Das Programm versucht die Anweisung Result := FloatToStr(1000 / StrToFloat(Zahl)); auszuführen. Sollte dabei ein Problem auftreten, wird der Code innerhalb des except Blockes ausgeführt. In diesem Fall geben wir einen "freundlichen" Hinweis zurück.

Hinweis: Der EXCEPT Block wird nur im Falle einer aufgetretenen Exception ausgeführt.

try...finally...end

Oft genug geschieht es in der Programmierung, dass wir einige Resourcen (Speicher) des Computers in Anspruch nehmen müssen. Anschließend sollten wir diese Resourcen stets wieder freigeben. Wieder ein denkbar einfaches Beispiel
Code:
function CountLines(FileName: String): Integer;
var
  Strings: TStringList;
begin
  // ressourcen reservieren
  Strings := TStringList.Create;
  // datei in die string liste laden
  Strings.LoadFromFile(FileName);
  // zeilenanzahl zurückliefern
  Result := Strings.Count;
  // string liste wieder zerstören
  Strings.Free;
end;
Dieses Beipsiel liefert uns die Anzahl der Zeilen in einer (Text-)Datei zurück. In diesem Beispiel nutze ich eine Stringliste. Als erstes muss diese initialisiert werden. Anschliessend kann die Datei geladen werden, um die Anzahl der Zeilen zu ermitteln. Am Ende wird die Stringliste wieder freigegeben.
Was passiert jetzt, wenn die Datei nicht existiert oder der Nutzer nicht das Recht hat diese zu öffnen? Es wird eine Exception (ein Fehler) in der Zeile Strings.LoadFromFile(FileName); erzeugt und die Verarbeitung abgebrochen. Die Zeile zum freigeben der Stringliste Strings.Free; wird nie ausgeführt, dass heißt, es ist ein kleines Speicherloch entstanden...
Da kommt der try...finally...end Block recht gelegen. Dieser Block wird immer ausgeführt. Damit können wir als Programmierer garantieren, dass alle Ressourcen wieder freigegeben werden. In unserem Beispiel sähe das wie folgt aus.
Code:
function CountLines(FileName: String): Integer;
var
  Strings: TStringList;
begin
  // ressourcen reservieren
  Strings := TStringList.Create;
  try
    // datei in die string liste laden
    Strings.LoadFromFile(FileName);
    // zeilenanzahl zurückliefern
    Result := Strings.Count;
  finally
    // string liste wieder zerstören
    Strings.Free;
  end;
end;
Auch wenn ein Fehler beim Laden der Datei auftritt, ist garantiert, dass die Stringliste zerstört wird, unabhängig davon, ob alle vorhergehenden Schritte ausgeführt worden sind, oder nicht.

Hinweis: Der FINALLY Block wird immer ausgeführt. Auch wenn mitten im Code Befehle zum Abbrechen der Prozedur oder einer Schleife (z.B. Exit, Break) auftauchen.

Exceptions erzeugen

Um das Erzeugen eigener Exceptions zu demonstrieren, kehren wir zu unserem ersten Beispiel zurück.
Code:
function DivideIt_03(Zahl: String): String;
begin
  if Zahl = '' then
    raise Exception.Create('Ein leerer Parameter ist unbrauchbar!');
  Result := FloatToStr(1000 / StrToFloat(Zahl));
end;
Das Erzeugen einer Exception ist denkbar einfach. Erstellt wird eine Exception durch die Einbindung des reservierten Wortes raise. Als Parameter wird ein Exception Object (bzw. ein davon abgeleitetes Objekt) erstellt. Im obigen Beispiel testen wir, ob ein leerer String übergeben wird; in diesem Fall erstellen eine Exception mit einer entsprechenden Fehlermeldung.

Eigene Exception-Typen deklarieren

Um im späteren Verlauf eines Programmes aufgetretene Fehler leichter zu erkennen, gibt Delphi uns die Möglichkeit eigene Fehlerklassen zu erstellen.
Code:
type
  EEmptyParam = class(Exception)
  public
    constructor Create;
  end;

{ EEmptyString }

constructor EEmptyParam.Create;
begin
  inherited Create('Ein leerer Parameter ist unbrauchbar!');
end;
In diesem Beispiel erstellen wir eine Fehlerklasse für leere (unbrauchbare) Parameter. Wenn wir diese Klasse als Fehlerklasse in unser letztes Beispiel integrieren, dann sieht die Funktion wie folgt aus.
Code:
function DivideIt_04(Zahl: String): String;
begin
  if Zahl = '' then
    raise EEmptyParam.Create;
  Result := FloatToStr(1000 / StrToFloat(Zahl));
end;
Für den Anwender des Programmes ergeben sich daraus keine Änderungen, aber sehr wohl für den Programmierer, wie wir im folgenden Abschnitt (Excpetions erkennen) sehen werden.

Exceptions erkennen

Nachdem wir die Exceptions verstanden, erstellt und abgefangen haben, müssen wir diese für korrektes und sinnvolles Handling nur noch wiedererkennen. Dazu gibt es in einem try...except...end Block die Möglichkeit eine Exception-Typ-Überprüfung vorzunehmen.
Code:
function DivideIt_05(Zahl: String): String;
begin
  try
    if Zahl = '' then
      raise EEmptyParam.Create;
    Result := FloatToStr(1000 / StrToFloat(Zahl));
  except
    // leere parameter erkennung
    on E: EEmptyParam do Result := 'Leerer Parameter';
    // division durch 0 erkannt
    on E: EZeroDivide do Result := 'Division durch Null (0)';
    // übergebener parameter ist keine zahl
    on E: EConvertError do Result := 'Keine Zahl (NaN)';
    // was gibt es sonst noch ???
    on E: Exception do Result := '??? ' + E.Message;
  end;
end;
Durch die Überprüfung on E: Exceptiontyp können wir den Typ eines Fehlers ermitteln und entsprechend reagieren. Die folgende Liste nennt die häufigsten Fehlertypen
  • EIntError
  • ERangeError
  • EIntOverflow
  • EMathError
  • EInvalidOp
  • EZeroDivide
  • EOverflow
  • EInvalidPointer
  • EInvalidCast
  • EConvertError
  • EAccessViolation
  • EStackOverflow
  • EWin32Error
  • ESafecallException

Das Beispiel...

zum downloaden

Der nächste Schritt

Im nächsten Teil dieses Tutorials fangen wir mit der Erstellung einer Komponente an. Diese Komponente ermöglicht es uns das Aussehen von Fehlermeldungen zu beeinflussen oder gar komplett Kontext-abhängige Fehlerbehandlungen durchzuführen.

Mal sehn was kommt...
Tippfehler gefixt! FuckRacism
Daniel W.
 
Benutzerbild von sakura
sakura

 
Delphi 11 Alexandria
 
#4
  Alt 26. Jun 2002, 15:42
Hi DP-ler,

jetzt geht es endlich richtig los, der erste echte Schritt in Richtung Komponentenerstellung. Ich habe mich entschieden, vor dem Debug Manager noch eine kleine Runde Komponenten-Basics einzuschieben. Sonst wäre es wahrscheinlich doch ein wenig schwer mitzukommen, wenn man noch nie Komponenten entwickelt hat. Und schließlich ist das ja auch das gewollte Publikum des Artikels.

Estellen einer Komponente

Fangen wir mal ganz vorne an und das auch praktisch. Delphi starten und/oder eventuell geöffnete Projekte schließen. Nochmal: ich nutze die englische Delphi Version, es sollte aber nicht schwer sein, entsprechende Schritte nachzuvollziehen.

Im Menü Datei|Neu wählen wir jetzt das Item Komponente aus und klicken den OK Schalter.
http://www.gatenetwork.com/delphi-sa...ourse03_01.gif

Anschließend präsentiert Delphi uns einen Dialog, der uns die Möglichkeit gibt, den "Rahmen" der Komponente zu erstellen. Dort füllen wir die Startdaten der Komponente ein.
http://www.gatenetwork.com/delphi-sa...ourse03_02.gif

Delphi erstellt uns jetzt eine kleine Unit, welche wie folgt aussieht.
Code:
unit SenselessSample;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs;

type
  TSenselessSample = class(TComponent)
  private
    { Private declarations }
  protected
    { Protected declarations }
  public
    { Public declarations }
  published
    { Published declarations }
  end;

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents('gate(n)etwork', [TSenselessSample]);
end;

end.
Damit ist der erste Schritt schon geschafft, wir haben eine Komponente, die, in ihrer jetzigen Form, völlig unnütz ist, aber bereits funktioniert. Aber den Test sparen wir uns noch ein paar Minuten. Etwas sinnvolles wäre doch ganz nett, oder?

Der Aufbau unserer Unit

Was sehen wir jetzt eigentlich? Die Unit für unsere Komponente ist genau wie jede andere Unit aufgebaut.
  • unit - Der Name der Unit, der gleich dem Namen des PAS Datei ist.
  • uses - Die am häufigsten genutzten Units werden automatisch eingebunden. Editieren dieser Liste ist, wie sonst auch, möglich.
  • type - Die Typen Deklarationen starten hier.
  • TSenselessSample = class(TComponent)An dieser Stelle deklarieren wir unsere Komponente. Abgeleitet von TComponent, ist es eine non-visual Komponente, welche der Anwender unseres Programmes nicht direkt zu sehen bekommt (Siehe 2. Posting).
    Folgende Bereiche gehören zu der Komponente.
    • private - In diesem Bereich werden alle Variablen und Methoden deklariert, welche nur innerhalb dieser Klasse für die direkte Verwendung zur Verfügung stehen.
    • protected - In diesem Bereich werden alle Eigenschaften, Variablen und Methoden deklariert, welche nur innerhalb dieser Klasse und aller davon abgeleiteten Klassen zur Verfügung stehen.
    • public - In diesem Bereich werden alle Eigenschaften und Methoden deklariert, welche auch ausserhalb der Klasse zur Verfügung stehen. Diese Eigenschaften und Methoden können somit überall im Projekt (in Verbindung mit einem Objekt der entsprechenden Klasse) verwendet werden. Es wird i.A. als schlechte Programmierpraktik angesehen, wenn in diesem Bereich Variablen deklariert werden, da die Klasse keine Kontrolle über diese haben würde.
      Code:
      Bsp:
      Memo1.Clear;
    • published - In diesem Bereich werden alle Eigenschaften, welche auch im Design-Modus im Object Inspektor manipuliert werden können. (z.B: TLabel.Caption, TEdit.Text; TForm.Icon, ...)
  • procedure Register; - Es wird die Prozedure deklariert, welche Delphi später aufruft, um unsere Komponente in die Palette zu laden.
  • implementation - Hier fängt der eigentliche Teil unserer Unit an - der ganze ausgeführte Code.
  • procedure Register; - Schon wieder. Na gut, hier melden wir die Komponente wirklich an. In diesem Fall wird eine Palette mit dem Namen "gate(n)etwork" eingerichtet.
  • end. - Alles hat mal ein Ende, auch unsere unit

Hinweis: Ein Hinweis sei mir noch gestattet. In den jeweiligen Abschnitten (private, protected, ...) müssen alle Variablen stets vor den Methoden (procedure, function) und Eigenschaften (property) deklariert werden.

"Filling in the meat"

Wollen wir dieser sinnlosen Komponente mal noch ein wenig Inhalt geben. Dazu deklarieren wir insgesamt sechs Eigenschaften. Zwei geben uns die Möglichkeit Fließkommazahlen entgegenzunehmen, vier weitere zeigen jeweils auf eine TLabel Komponente.

Dazu füllen wir den Bereich zwischen private und end; wie folgt aus.
Code:
  published
    { Published declarations }
    property X: Double;
    property Y: Double;
    property Result_Add: TLabel;
    property Result_Sub: TLabel;
    property Result_Mul: TLabel;
    property Result_Div: TLabel;
  end;
Der Bezeichner property teilt Delphi mit, das es sich um eine Eigenschaft der Klasse handelt. In diesem Fall habe ich die Eigenschaften X, Y, Result_Add, Result_Sub, Result_Mul und Result_Div deklariert. Noch sind wir aber nicht fertig. Jetzt positionieren wir den Cursor innerhalb des gerade eingefügten Bereiches und lassen Delphi ein bisschen für uns arbeiten. Einfach mal die Tasten [STRG]+[SHIFT]+[C] drücken und Delphi "completes" unsere Klasse und die ganze Unit.

Code:
unit SenselessSample;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs;

type
  TSenselessSample = class(TComponent)
  private
    FX: Double;
    FY: Double;
    FResult_Add: TLabel;
    FResult_Sub: TLabel;
    FResult_Mul: TLabel;
    FResult_Div: TLabel;
    procedure SetResult_Add(const Value: TLabel);
    procedure SetResult_Div(const Value: TLabel);
    procedure SetResult_Mul(const Value: TLabel);
    procedure SetResult_Sub(const Value: TLabel);
    procedure SetX(const Value: Double);
    procedure SetY(const Value: Double);
    { Private declarations }
  protected
    { Protected declarations }
  public
    { Public declarations }
  published
    { Published declarations }
    property X: Double read FX write SetX;
    property Y: Double read FY write SetY;
    property Result_Add: TLabel read FResult_Add write SetResult_Add;
    property Result_Sub: TLabel read FResult_Sub write SetResult_Sub;
    property Result_Mul: TLabel read FResult_Mul write SetResult_Mul;
    property Result_Div: TLabel read FResult_Div write SetResult_Div;
  end;

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents('gate(n)etwork', [TSenselessSample]);
end;

{ TSenselessSample }

procedure TSenselessSample.SetResult_Add(const Value: TLabel);
begin
  FResult_Add := Value;
end;

procedure TSenselessSample.SetResult_Div(const Value: TLabel);
begin
  FResult_Div := Value;
end;

procedure TSenselessSample.SetResult_Mul(const Value: TLabel);
begin
  FResult_Mul := Value;
end;

procedure TSenselessSample.SetResult_Sub(const Value: TLabel);
begin
  FResult_Sub := Value;
end;

procedure TSenselessSample.SetX(const Value: Double);
begin
  FX := Value;
end;

procedure TSenselessSample.SetY(const Value: Double);
begin
  FY := Value;
end;

end.
Da wir uns entschieden haben, TLabel zu nutzen müssen wir die uses Klausel ein wenig erweitern.
Code:
uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs;

wird zu

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls;
Was ist alles geschehen?

Schauen wir uns einfach mal die aufgefüllte Zeile einer property an.

Code:
    property X: Double;


wird zu

    property X: Double read FX write SetX;
Delphi hat für uns die üblichste Behandlungsart einer Eigenschaft eingetragen. Das heisst, wenn jemand die Eigenschaft X ermitteln will, so wird der Wert der Variablen FX direkt zurückgegeben. Delphi hat auch gleich die Variable FX für uns im private Bereich deklariert. Danke Borland. Ausserdem hat Delphi auch gleich definiert, wie Änderungen der Eigenschaft übernommen werden. Damit wir bei eventuellen Änderungen reagieren können, hat Delphi das Setzen der Variable nicht direkt erledigt, sondern eine "Wrapper" Methode SetX erstellt. Diese Methode übernimmt das Speichern der Änderungen.
Code:
procedure TSenselessSample.SetX(const Value: Double);
begin
  FX := Value;
end;
Diese Art der Variablenbehandlung kommt uns jetzt noch sehr zu gute.

"And Action!"

Jetzt fügen wir noch eine Methode zum private Bereich hinzu, damit wir die die Hauptfunktionalität noch angenehm verpacken können.
Code:
  private
    ...
    procedure UpdateLabels;
    { Private declarations }
  protected
Und noch einmal [STRG]+[SHIFT]+[C] für die automatische Code-[b]C[/c]ompletion drücken!
Code:
procedure TSenselessSample.UpdateLabels;
begin

end;
Jetzt haben wir unsere Hauptmethode fast fertig. Es müssen nur noch ein paar Zeilen Code hinein und das Ergebnis sieht wie folgt aus.
Code:
procedure TSenselessSample.UpdateLabels;
begin
  // summe ermitteln und darstellen
  if Assigned(FResult_Add) then
  try
    // label für summe wurde zugewiesen
    FResult_Add.Caption := FloatToStr(FX + FY);
  except
    on E: Exception do
      // oops
      FResult_Add.Caption := E.Message;
  end;
  // differenz ermitteln und darstellen
  if Assigned(FResult_Sub) then
  try
    // label für differenz wurde zugewiesen
    FResult_Sub.Caption := FloatToStr(FX - FY);
  except
    on E: Exception do
      // oops
      FResult_Sub.Caption := E.Message;
  end;
  // produkt ermitteln und darstellen
  if Assigned(FResult_Mul) then
  try
    // label für produkt wurde zugewiesen
    FResult_Mul.Caption := FloatToStr(FX * FY);
  except
    on E: Exception do
      // oops
      FResult_Mul.Caption := E.Message;
  end;
  // quotient ermitteln und darstellen
  if Assigned(FResult_Div) then
  try
    // label für quotient wurde zugewiesen
    FResult_Div.Caption := FloatToStr(FX / FY);
  except
    on E: Exception do
      // oops
      FResult_Div.Caption := E.Message;
  end;
end;
Für jedes Ergebnis überprüfen wir zuerst, ob ein entsprechendes Label vorhanden ist und anschließend versuchen wir das Ergebnis zu ermitteln und darzustellen. Wenn das fehlschlagen sollte, liefern wir eine entsprechende Fehlermeldung zurück.

Jetzt tragen wir in alle Set Methoden noch als letzten den Aufruf zu unserer UpdateLabels; Methode ein und sind soweit fertig. Einen Schönheitsfehler müssen wir noch beseitigen. Da es geschehen kann, dass eines der zugewiesenen Labels vom Form entfernt wird, müssen wir abschliessend noch dieses Ereignis abfangen und die entsprechden FResult_XXX Variabel auf nil setzen! Dazu überlaufen wir die Standard Notification Methode.
Code:
  protected
    { Protected declarations }
    procedure Notification(
      aComponent: TComponent; Operation: TOperation
    ); override;
Den kompletten Code könnt Ihr jetzt lesen.
Code:
unit SenselessSample;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls;

type
  TSenselessSample = class(TComponent)
  private
    FX: Double;
    FY: Double;
    FResult_Add: TLabel;
    FResult_Sub: TLabel;
    FResult_Mul: TLabel;
    FResult_Div: TLabel;
    procedure SetResult_Add(const Value: TLabel);
    procedure SetResult_Div(const Value: TLabel);
    procedure SetResult_Mul(const Value: TLabel);
    procedure SetResult_Sub(const Value: TLabel);
    procedure SetX(const Value: Double);
    procedure SetY(const Value: Double);
    procedure UpdateLabels;
    { Private declarations }
  protected
    { Protected declarations }
    procedure Notification(
      aComponent: TComponent; Operation: TOperation
    ); override;
  public
    { Public declarations }
  published
    { Published declarations }
    property X: Double read FX write SetX;
    property Y: Double read FY write SetY;
    property Result_Add: TLabel read FResult_Add write SetResult_Add;
    property Result_Sub: TLabel read FResult_Sub write SetResult_Sub;
    property Result_Mul: TLabel read FResult_Mul write SetResult_Mul;
    property Result_Div: TLabel read FResult_Div write SetResult_Div;
  end;

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents('gate(n)etwork', [TSenselessSample]);
end;

{ TSenselessSample }

procedure TSenselessSample.Notification(
  aComponent: TComponent; Operation: TOperation
);
begin
  inherited Notification(aComponent, Operation);
  if (Operation = opRemove) then
  begin
    if aComponent = FResult_Add then
      FResult_Add := nil;
    if aComponent = FResult_Sub then
      FResult_Sub := nil;
    if aComponent = FResult_Mul then
      FResult_Mul := nil;
    if aComponent = FResult_Div then
      FResult_Div := nil;
  end;
end;

procedure TSenselessSample.SetResult_Add(const Value: TLabel);
begin
  FResult_Add := Value;
  UpdateLabels;
end;

procedure TSenselessSample.SetResult_Div(const Value: TLabel);
begin
  FResult_Div := Value;
  UpdateLabels;
end;

procedure TSenselessSample.SetResult_Mul(const Value: TLabel);
begin
  FResult_Mul := Value;
  UpdateLabels;
end;

procedure TSenselessSample.SetResult_Sub(const Value: TLabel);
begin
  FResult_Sub := Value;
  UpdateLabels;
end;

procedure TSenselessSample.SetX(const Value: Double);
begin
  FX := Value;
  UpdateLabels;
end;

procedure TSenselessSample.SetY(const Value: Double);
begin
  FY := Value;
  UpdateLabels;
end;

procedure TSenselessSample.UpdateLabels;
begin
  // summe ermitteln und darstellen
  if Assigned(FResult_Add) then
  try
    // label für summe wurde zugewiesen
    FResult_Add.Caption := FloatToStr(FX + FY);
  except
    on E: Exception do
      // oops
      FResult_Add.Caption := E.Message;
  end;
  // differenz ermitteln und darstellen
  if Assigned(FResult_Sub) then
  try
    // label für differenz wurde zugewiesen
    FResult_Sub.Caption := FloatToStr(FX - FY);
  except
    on E: Exception do
      // oops
      FResult_Sub.Caption := E.Message;
  end;
  // produkt ermitteln und darstellen
  if Assigned(FResult_Mul) then
  try
    // label für produkt wurde zugewiesen
    FResult_Mul.Caption := FloatToStr(FX * FY);
  except
    on E: Exception do
      // oops
      FResult_Mul.Caption := E.Message;
  end;
  // quotient ermitteln und darstellen
  if Assigned(FResult_Div) then
  try
    // label für quotient wurde zugewiesen
    FResult_Div.Caption := FloatToStr(FX / FY);
  except
    on E: Exception do
      // oops
      FResult_Div.Caption := E.Message;
  end;
end;

end.
Installieren der Komponente

Der letzte wichtige Schritt ist die Installation der Komponente in der Delphi Palette. Ausserdem könnte man jetzt noch ein Icon für die Komponente erstellen, aber dazu kommen wir ein anderes Mal, wenn es nicht gerade diese sinnlose Komponente ist.

Um die Komponente zu installieren, wählt im Menü Komponenten|Komponente installieren. Delphi trägt automatisch alles ein und schlägt Euch das Standard User-Komponenten Packet vor. Das ist auch in Ordnung. Mit OK bestätigen.

http://www.gatenetwork.com/delphi-sa...ourse03_03.gif

Anschliessend seht Ihr das Fenster zum kompilieren und installieren des Packages.

http://www.gatenetwork.com/delphi-sa...ourse03_04.gif

"Kompilieren" und anschließend "Installieren" drücken. Fertig.

Der Test des Sinnlosen

Alle offenen Projekte schließen und eine neue Anwendung anlegen. 4 Labels auf das Form packen und von der "gate(n)etwork" Seite unsere Komponente auswählen und auf das Form packen. Jetzt nur noch die vier Labels den entsprechenden Eigenschaften zuordnen und ein wenig im Objekt Inspektor rumspielen.

Viel Spass, hier gibt es alles noch zum Download

Daniel W.
 
Benutzerbild von sakura
sakura

 
Delphi 11 Alexandria
 
#5
  Alt 16. Okt 2002, 12:06
Hi DP-ler,

lang ist es her, aber jetzt kann ich mich auch mal wieder diesem Tutorial ein wenig widmen. Seit dem letzten Mal ist nun auch Delphi 7 auf dem Markt und deshalb möchte ich, neben der normalen Komponentenerstellung auch ModelMaker etwas genauer unter die Lupe nehmen.

ModelMaker ist bereits seit mehreren Jahren verfügbar und liefert für Delphi ab Version 1 die Möglichkeit UML für Delphi zu erstellen, Projekte zu verwalten und sogar automatisch Hilfedateien für erstellte Komponenten zu erzeugen. Es wird also interessant. Für diejenigen, welche ein "ältere" Delphiversion nutzen, empfehle ich einen Besuch bei www.modelmakertools.com, dort könnt ihr u.a. eine Evaluierungsversion heruntersaugen.

Hinweis: Dieses Mal werde ich keine Neuerungen in der Komponentenenticklung zeigen, sondern lediglich unsere letzte Komponente (TSenselessSample) noch einmal erstellen und Zurhilfenahme von ModelMaker.

Let's get started

Startet also Delphi und aus der Delphi IDE heraus Modelmaker. Wer zwei Monitore an seinem Rechner hat, ist jetzt klar im Vorteil. Setzt Delphi auf die linke Seite und ModelMaker auf die rechte Seite. Ein Monitor: immer schön hin und her wechseln (Alt+Tab).

In ModelMaker wählt aus dem Menü File, den zweiten Punkt New from Default. Wie Ihr merkt, ich habe mal wieder die englische Delphiversion bestellt. Aber die mit der deutschen Variante werdens schon schaffen.

What you see is what you get

oder so ähnlich. Auf der linken Seite von ModelMaker seht ihr oben die verschiedenen Design-Module (Ansicht DM), welche Euch zur Verfügung stehen. Darunter befinden sich die Deklarationen (Ansicht Dekl) innerhalb der aktuell ausgewählten Klasse. Rechts ist die Arbeitsfläche.

Die Darstellung der Arbeitsfläche hängt generell von der aktuellen Aufgabe ab. Nach dem Laden des "Default Templates" seht ihr normalerweise ein einfaches Diagramm, welches die wichtigsten Klassen für Delphi Projekte darstellt.

Let's get started with working

Als wir die Komponente in Delphi erstellt haben, haben wir im Dial Neu den Punkt Component ausgewählt. Anschließend haben wir als Ancestor den Typ TComponent ausgewählt, den Namen unserer Komponente auf TSenselessSample gestzt, eine Quelldatei festgelegt, und ausgesucht, auf welcher Palettenseite die Komponente installiert wird. Diese Schritte werden wir jetzt in ModelMaker nachvollziehen.

In der Ansicht DM wählt die Klasse TComponent aus, drückt die rechte Maustaste und wählt den Menüpunkt Add Descendant, setzt den Namen auf TSenselessSample. ModelMaker fügt die neue Komponente automatisch an der richtigen Stelle in der Hierarchie ein. Via Drag-n-Drop zieht die neue Komponente in das Standard Diagramm. ModelMaker erkennt automatisch die Beziehung zur Basiskomponente und erstellt einen visuellen Link.

Übung

Ändert manuell den visuellen Link und beobachtet, wie sich die Struktur der Klassenübersicht automatisch anpasst.

Erstellen der Unit für die Komponente

Wählt in der Ansicht DM den Reiter Units aus. Im Bereich Classes not assigned to units ist unsere Komponente TSenselessSample zu sehen. Klickt mit der rechten Maustaste in die Ansicht und wählt Add Unit aus dem Kontextmenü.

Wählt einen neuen Namen und Ordner für die Unit aus. Im unteren Bereich fügt Ihr die TSenselessSample Komponente zur Unit hinzu und definiert auf welcher Palettenseite diese dargestellt werden soll. Soweit, so gut!

Automatisches erstellen der Unit

In der oberen Toolbar seht Ihr neben den Standard Neu, Laden, Speichern, ... Buttons ein geöffnetes und ein geschlossenes Schloss. Klickt auf das geöffnete Schloss, um ModelMaker zu erlauben, den Source Code zu generieren.

In der Ansicht DM, im Bereich Units, wählt die neue Unit aus, klickt mit der rechten Maustaste auf den entsprechenden Eintrag und wählt den Menüpunkt Enable Auto Generation. Ein grüner Pfeil ist davor zu sehen.

Im Hauptmenü klickt jetzt auf den Punkt Delphi und selektiert den Eintrag Open Unit. Jetzt könnte Ihr den erzeugten Quellcode in Delphi sehen. Sehr ähnlich dem Quellcode, wie wir ihn auch das letzte Mal erstellt haben. Aber noch ist nicht annähernd die Funktionalität vorhanden, wie wir diese bei der letzten Version von TSenselessSample hatten.

Erstellen der Properties

Ob Ihr jetzt in der Ansicht DM :: Classes, oder Ansicht DM :: Units arbeitet ist gar nicht so wichtig, wichtig ist jedoch, dass Ihr sicher stellt, dass unsere Komponente TSenselessSample ausgewählt ist.

Im unteren Bereich seht die Ansicht Dekl (Deklarationen). Die ersten vier Icons stehen für die Funktionen
  • Feld (Klassenvariable) hinzufügen
  • Methode hinzufügen
  • Property hinzufügen
  • Event hinzufügen

Über das dritte Icon fügen wir jetzt sechs Eigenschaften (Properties) hinzu.

  1. Name: X
    Visibility: published
    Data type: Double
    Read Access: Field
    Write Access: Method
    Write Code: Check
    Ext Write Code: Check

  2. Name: Y
    Visibility: published
    Data type: Double
    Read Access: Field
    Write Access: Method
    Write Code: Check
    Ext Write Code: Check

  3. Name: Result_Add
    Visibility: published
    Data type: User defined
    Data type Name: TLabel
    Read Access: Field
    Write Access: Method
    Write Code: Check
    Ext Write Code: Check

  4. Name: Result_Sub
    Visibility: published
    Data type: User defined
    Data type Name: TLabel
    Read Access: Field
    Write Access: Method
    Write Code: Check
    Ext Write Code: Check

  5. Name: Result_Mul
    Visibility: published
    Data type: User defined
    Data type Name: TLabel
    Read Access: Field
    Write Access: Method
    Write Code: Check
    Ext Write Code: Check

  6. Name: Result_Div
    Visibility: published
    Data type: User defined
    Data type Name: TLabel
    Read Access: Field
    Write Access: Method
    Write Code: Check
    Ext Write Code: Check

Wenn Ihr Euch jetzt den Source Code in Delphi anschaut, dann ist dieser jetzt bereits erheblich angewachsen. Alle Deklarationen sind bereits eingefügt und die wichtigsten Code Einträge (SetXXX) sind auch schon da.

Nur compilieren würde zur Zeit nicht gehen, da wir im Code auf den Datentyp (Klassentyp) TLabel verweisen. Deshalb müssen wir eine manuelle Anpassung im Code vornehmen. Diese darf aber nicht in Delphi geschehen, sondern muss in ModelMaker geschehen, da sonst bei der nächsten Erstellung des Source Codes die Änderung wieder verloren wäre.

Hinweis: Es gibt auch die Möglichkeit Code nach ModelMaker zu re-importieren, aber das gehört jetzt nicht zu diesem Kurs. Genaueres dazu findet Ihr auf der Companion Tools CD 1, bei den Dokumentationen. Da ist auch das komplette Handbuch für ModelMaker als PDF, mit knapp 120 Seiten Umfang.

Ändern des Unit Source Codes

Wählt in der Ansicht DM :: Units unsere Unit aus. Auf der Arbeitsfläche müsst Ihr, sofern es nicht bereits geschehen ist, oben den Reiter Unit Code anwählen. Jetzt seht Ihr den Unit Code, welcher als Vorlage für unsere Komponente dient. Dort seht Ihr auch diese uses Klausel, welche angepasst werden muss, wie auch schon im letzten Beitrag geschehen.

Code:
[b]uses[/b]
  SysUtils, Windows, Messages, Classes, Graphics, Controls,
  Forms, Dialogs;

[color=#0000FF] // wird zu // [/color]

[b]uses[/b]
  SysUtils, Windows, Messages, Classes, Graphics, Controls,
  Forms, Dialogs, StdCtrls;
Jetzt können wir den nächsten Schritt nehmen.

Die Methode UpdateLabels

Um für alle Labels Ergebnisse anzeigen zu können, fügen wir die Methode UpdateLabels zum Model. Anstatt das Icon Property in der Ansicht Dekl zu nutzen, klicken wir dieses Mal auf des zweite Icon (Methode). Der Dialog unterscheidet sich nur geringfügig von der letzten Version. Folgende Einstellungen sind vorzunehmen

  1. Name: UpdateLabels
    Visibility: private
    Data type: Void

Jetzt müssen wir noch den Code einfügen, welcher ausgeführt werden soll. Dazu klickt in der Arbeitsfläche auf den Reiter Implementation und fügt den folgenden Code ein.

Code:
  [color=#000080]// summe ermitteln und darstellen[/color]
  [b]if[/b] Assigned(FResult_Add) [b]then[/b]
  [b]try[/b]
    [color=#000080]// label für summe wurde zugewiesen[/color]
    FResult_Add.Caption := FloatToStr(FX + FY);
  [b]except[/b]
    [b]on[/b] E: Exception [b]do[/b]
      [color=#000080]// oops[/color]
      FResult_Add.Caption := E.Message;
  [b]end[/b];
  [color=#000080]// differenz ermitteln und darstellen[/color]
  [b]if[/b] Assigned(FResult_Sub) [b]then[/b]
  [b]try[/b]
    [color=#000080]// label für differenz wurde zugewiesen[/color]
    FResult_Sub.Caption := FloatToStr(FX - FY);
  [b]except[/b]
    [b]on[/b] E: Exception [b]do[/b]
      [color=#000080]// oops[/color]
      FResult_Sub.Caption := E.Message;
  [b]end[/b];
  [color=#000080]// produkt ermitteln und darstellen[/color]
  [b]if[/b] Assigned(FResult_Mul) [b]then[/b]
  [b]try[/b]
    [color=#000080]// label für produkt wurde zugewiesen[/color]
    FResult_Mul.Caption := FloatToStr(FX * FY);
  [b]except[/b]
    [b]on[/b] E: Exception [b]do[/b]
      [color=#000080]// oops[/color]
      FResult_Mul.Caption := E.Message;
  [b]end[/b];
  [color=#000080]// quotient ermitteln und darstellen[/color]
  [b]if[/b] Assigned(FResult_Div) [b]then[/b]
  [b]try[/b]
    [color=#000080]// label für quotient wurde zugewiesen[/color]
    FResult_Div.Caption := FloatToStr(FX / FY);
  [b]except[/b]
    [b]on[/b] E: Exception [b]do[/b]
      [color=#000080]// oops[/color]
      FResult_Div.Caption := E.Message;
  [b]end[/b];
Putting UpdateLabels to work

Jetzt müssen wir noch einstellen, das UpdateLabels immer aufgerufen wird, wenn sicher einer der Parameter (Werte, Labels) ändert. Dazu müsst Ihr jetzt in der Ansicht für jede der Zugriffsmethoden (Access Methods) (SetResult_Add, SetResult_Div, SetResult_Mul, SetResult_Sub, SetX, SetY) folgende Aufgaben erledigen.

Zugriffsmethode in der Ansicht Dekl markieren (auswählen). In der Arbeitsfläche den Reiter Implemantation auswählen. Links unten seht Ihr jetzt die aktuellen Code Abschnitte, in logische Bereiche getrennt.
Code:
  [color=#FF0000]// Abschnitt 1[/color]
  if FResult_Add <> Value then
  begin
  [color=#FF0000]// Abschnitt 2[/color]
    FResult_Add := Value;
  [color=#FF0000]// Abschnitt 3[/color]
  end;
Zwischen Abschnitt 2 und Abschnitt 3 müssen wir jetzt unseren Aufruf zur Methode UpdateLabels plazieren. Klickt dazu mit der rechten Maustaste in den linken unteren Bereich und wählt den Menüpunkt Add Section. Anschließend zieht diesen per Drag-n-Drop zwischen den zweiten und den dritten Bereich.

Im Vergleich zu den anderen Bereichen ist dieser nicht "rot-weiss" markiert, sonder grün. Grüne Bereiche sind User-Code, die "rot-weissen" Bereiche werden durch ModelMaker verwaltet. In unseren Bereich fügen wir jetzt einfach den Aufruf zur Methode UpdateLabels; ein.

Diesen Vorgang jetzt für die restlichen Zugriffsmethoden ausführen

Und nun noch die Methode Notification

Gleich der Methode UpdateLabels wird diese mit folgenden Paramtern eingerichtet.

  1. Name: Notification
    Visibility: protected
    Call inherited: Checked
    Inheritance restricted: Checked

Das Häckchen "Call inherited" fügt den Aufruf zur vorhergehenden Instanz der Methode ein.
Code:
[b]inherited[/b] Notification(aComponent, Operation);
Das Häckchen "Inheritance restricted" bewirkt, dass alle Einstellungen der Methode von der vorhergehenden Definition (TComponent) übernommen wird.

Jetzt müssen wir noch ein wenig User Code einpflegen. Also wieder in der Arbeitsfläche den Reiter Implementation auswählen, "Add Section" im Kontextmenü auswählen und den folgenden Code mit Copy & Paste einfügen.
Code:
  [b]if[/b] (Operation = opRemove) [b]then[/b]
  [b]begin[/b]
    [b]if[/b] aComponent = FResult_Add [b]then[/b]
      FResult_Add := [b]nil[/b];
    [b]if[/b] aComponent = FResult_Sub [b]then[/b]
      FResult_Sub := [b]nil[/b];
    [b]if[/b] aComponent = FResult_Mul [b]then[/b]
      FResult_Mul := [b]nil[/b];
    [b]if[/b] aComponent = FResult_Div [b]then[/b]
      FResult_Div := [b]nil[/b];
  [b]end[/b];
Fertig! Jetzt die Komponente, wie bereits im letzten Teil des Kurses beschrieben, installieren und glücklich sein.

Anbei ist das ModelMaker Projekt als Download. Mit dieser Datei könnt Ihr die Komponente vollständig erstellen.

Viel Spass,
bis bald.
......
Angehängte Dateien
Dateityp: zip 53_1034762781.zip (4,5 KB, 103x aufgerufen)
Daniel W.
 
Tutorial geschlossen

Themen-Optionen Tutorial durchsuchen
Tutorial durchsuchen:

Erweiterte Suche
Ansicht

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 04:16 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