Tutorial Komponentenentwicklung

Aus DGL Wiki
Wechseln zu: Navigation, Suche

Komponenten Tutorial

Einleitung

Erst mal willkommen bei meinem Komponeten-Tutorial. Ich möchte in diesem Tutorial nicht absolutes Basiswissen über die Komponenten-Programmierung unter Delphi vermitteln, sondern denjenigen, die schon entsprechende Einsteigertutorials gelesen haben (wie etwa das auf Delphi-Treff.de) noch etwas tiefer in die Materie einführen.

Eigene Visuelle Komponeten

Wenn man seine Komponente komplett selber zeichnen möchte, so empfiehlt es sich diese von TGraphicControl oder TCustomControl abzuleiten. TCustomControl ist im Gegensatz zu TGrapicControl ein Nachfahre von TWinControl, und erbt von ihm ein (Fenster-) Handle. Dieses Handle ist eine Nummer, welche Komponenten von Windows erhalten um sie eindeutig zu bezeichnen. Nur wenn die Komponente ein Handle hat, kann Windows mit ihr etwas anfangen.

Möchte man nur statisch etwas anzeigen, wie etwa ein Label, so kann man auch nur TGraphicControl als Vorfahren wählen. Solche Komponenten ohne Handle werden von Windows nicht unterstützt, und sind nur eine Technik von Delphi, um dem Entwickler einfachere Gestaltungsmöglichkeiten zu bieten. Es ist ja schließlich viel einfacher, eine Komponente auf der Form zu platzieren, als diese mit Zeichenbefehlen selbst zu zeichnen.

Um selbst festzulegen, wie die Komponente gezeichnet wird, kann man bei Nachfahren von TGraphicControl oder TCustomControl ganz einfach die procedure Paint überschreiben.

TMeineKomponente = class(TGraphicControl)
protected
  procedure Paint; override;

Gezeichnet wird dann über die Canvas-Eigenschaft der Komponente. Dabei sollte man nicht vergessen mit inherited die Paint Procedure des Vorfahren aufzurufen.

procedure TMeineKomponente.Paint;
begin
  inherited;//Paint Procedure des Vorfarhen aufrufen.
  Canvas.//irgendwas

Eine Abfrage der Eigenschaft ComponentState ermöglicht es uns, die Komponente zur Entwicklungszeit anders zu zeichnen.

procedure TMeineKomponente.Paint;
begin
  inherited;//Zeichen Procedure des Vorfahren aufrufen.
  if csDesigning in ComponentState then //Wenn sich die Komponente in Entwicklung befindet.
  begin
    {Zeichung eines gestrichelten Rahmens}
    Canvas.Brush.style := bsClear;//Durchsichtiges Rechteck
    Canvas.Pen.style := psDashDot;//Gestrichelte Linen
    Canvas.Rectangle(0,0,width,height);//Rechteck zeichnen

    {Namen der Komponente in die Mitte schreiben}
    canvas.TextOut((width - Canvas.TextWidth(Name)) div 2,(height - Canvas.TextHeight(Name)) div 2,name);

    {Keine weitern Zeichnungen mehr ausführen}
    exit;//Verlässt die Procedure
  end;
  //Normale Zeichen-Anweisungen
end;

Eigenschafts-Editoren

Als erstes sollten wir der Frage nachgehen, was ein Eigenschaftseditor überhaupt ist : Wie schon fast zu erraten, handelt es sich hierbei um eine Möglichkeit, eine als veröffentlicht (published) deklarierte Eigenschaft im Objektinspektor zu bearbeiten. In Delphi sind schon diverse Eigenschaftseditoren vorhanden um die häufigsten Eigenschaftstypen abzudecken. Wenn ihr einer Komponente eine als published deklarierte Eigenschaft vom Typ Integer verpasst, so wird standardgemäß der Editor "TIntegerProperty" genutzt um die Eigenschaft im Objektinspektor darzustellen. Dieser Editor ist ein Eingabefeld für Zahlen.

Eigenschaftseditoren können aber nicht nur solche Eingabefelder sein, sondern können auch eine ganze Reihe anderer Bearbeitungsmöglichkeiten bieten. Ein richtig multifunktionaler Eigenschaftseditor ist zum Beispiel "TColorProperty". So kann man entweder eine hexadezimale Zahl oder einen Farbnamen eingeben, die entsprechende Farbe über eine Liste auswählen oder gar per Doppelklick einen Farbdialog öffenen.

Ein solcher Eigenschaftseditor ist programmiertechnisch gesehen nichts anderes als ein Nachfahre der Klasse "TPropertyEditor", welcher in einer Register-Prozedur in Delphi eingebunden wurde. Den Code von einem solchen Eigenschaftseditor sollte man in eine Unit packen, welche nur von Delphi genutzen Code enthält. Für das beiliegende Beispiel TFilePropertyEditor nehmen wir direkt TPropertyEditor als Vorfahren.

TFileNameProperty = class (TPropertyEditor)
  {...}
end;

In welcher Form der Eigenschaftseditor angezeigt wird, bestimmt der Rückgabewert der Funktion GetAttributes. Um diese zu ändern überschreiben wir die Funktion einfach.

TFileNameProperty = class (TPropertyEditor)
public
  function GetAttributes: TPropertyAttributes; override;
  {...}
end;

Dem Eigenschaftseditor werde ich nun zusätzlich zu dem Standardeingabefeld noch per paDialog die Möglichkeit geben, auf Doppelklicks oder einen (nun evtl. erschienen) Buttonklick zu reagieren. paMultiSelect bewirkt schlussendlich noch, dass die Eigenschaft auch noch sichtbar ist, wenn mehre Komponenten ausgewählt sind. Weitere mögliche Parameter sind der Delphi Hilfe zu entnehmen.

function TFileNameProperty.GetAttributes: TPropertyAttributes;
begin
  Result := [paMultiSelect, paDialog];
end;

Um auf das neu geschaffene Editierereignis zu reagieren, überschreiben wir die Procedure Edit;

procedure TFileNameProperty.Edit;
var
  OpenDialog:TOpenDialog;
begin
  OpenDialog := TOpenDialog.create(Application);//Den OpenDialog erstellen
  try
    //Den akuellen Wert übergeben
    OpenDialog.FileName := GetStrValue;
    //Anzeigen und bei Erfolg Wert übernehmen
    if OpenDialog.Execute then SetStrValue(OpenDialog.FileName);
  finally
    OpenDialog.Free; //Den OpenDialog freigeben.
  end;
end;

Mit GetStrValue und SetStrValue kann der Wert einer Stringeigenschaften gelesen und geschrieben werden. Für andere Eigenschaftstypen stehen andere Funktionen zur Verfügung. An dieser Stelle können natürlich auch eigene Formulare eingeblendet werden.

Durch überschreiben von GetValue kann man regeln, was für ein Text auf der rechten Seite im Objektinspektor angezeigt wird. Ein Überscheiben von SetValue hingegen legt fest, wie auf eingegebene Werte reagiert werden soll. In unserem Fall soll der Eigenschaftsinhalt direkt angezeigt und übernommen werden.

function TFileNameProperty.GetValue: string;
begin
  result := GetStrValue ;
end;

procedure TFileNameProperty.SetValue(const Value: string);
begin
  SetStrValue(Value);
end;

Wie ihr euch vielleicht noch erinnert, haben wir am Anfang in GetAttributes festgelegt, dass die Eigenschaft auch bei mehrfacher Auswahl sichtbar bleibt. Nun liegt es an, uns Delphi zu erklären, wann zwei Eigenschaften wirklich identisch sind. Denn schließlich sagt der im Objektinspektor angezeigte Wert nichts über den tatsächlichen Inhalt der Eigenschaft. Durch ein Überschreiben der Funktion AllEqual können wir nun unsere eigene Prüfung definieren.

function TFileNameProperty.AllEqual: Boolean;
var
  Nr1:String;
  I:Integer;
begin
  AllEqual := true;
  Nr1 := GetstrValue ;
  // Ersten Eigenschafts-Wert mit restlichen Werten vergleichen.
  // Die Anzahl der zu überprüfenden Eigenschaften liefert PropCount.
  for I:= 1 to PropCount-1 do
    if Nr1 <> GetstrValueat(I) then
    begin
      AllEqual := false;
      break;
    end;
end;

Nach der Erstellung unserer Eigenschaftseditor-Klasse müssen wir nur noch festlegen, für welche Eigenschaften und für welche Komponeten der Editor genutzt werden soll. Dies geschieht durch eine Registrierung des Editors über einen Aufruf von RegisterPropertyEditor inherhalb der Register-Prozedur.

procedure Register;
begin
  RegisterPropertyEditor(TypeInfo(TFileName),nil,'FileName',TFileNameProperty);
end;

Der erste übergebene Parameter ist der Typ der Eigenschaft für den der Editor gelten soll. Mit dem zweiten Parameter wird festgelegt, ob der Editor nur für die Eigenschaft einer bestimmten Komponente oder für alle (durch Angabe von nil) gelten soll. Der dritte Parameter legt fest, ob die Eigenschaft einen bestimmten Namen haben muss (Ein leerer String bedeutet keine Festlegung). Der Letzte Parameter beinhaltet nun die Editorklasse selbst.

Komponenten-Editoren

Bei den Komponenteneditoren verhält sich vieles wie bei den Eigenschaftseditoren. Sie dienen nur dazu, dem Progammierer mehr Möglichkeiten zu geben um Komponenten zu bearbeiten. Oder auch die Arbeit mit Komponenten zu vereinfachen.

Mit Hilfe eines Komponenteneditors kann festgelegt werden:

  • Was bei einem Doppelklick auf die Komponente passiert.
  • Welche Zusatzoptionen der Benutzer im PopUp-Menü der Komponente hat.
  • Wie die Komponente in die Zwischenablage kopiert wird.

Das Doppelklickverhalten festlegen

Möchte man, dass bei einem Doppelklick auf die Komponente der Code für ein bestimmtes Ereignis generiert wird, so leitet man seinen Komponenteneditor nicht von TComponentEditor ,sondern von seinem Nachfahren TDefaultEditor ab. Auf diese Weise kann man die Prozedur EditProperty überschreiben.

{Die Ereignis-Auswahl bei Doppelklick auf TGLControl}
TGLControlEditor = Class (TDefaultEditor)
public
  procedure EditProperty(PropertyEditor: TPropertyEditor; var Continue, FreeEditor: Boolean); override;
end;

Diese Prozedur sieht eigentlich immer gleich aus:

procedure TGLControlEditor.EditProperty(PropertyEditor: TPropertyEditor; var Continue, FreeEditor: Boolean);
begin
  // Falls die Eigenschaft ein Ereignis ist
  if (PropertyEditor.ClassName = 'TMethodProperty') and
  // und sie OnDraw heißt
  (PropertyEditor.GetName = 'OnDraw') then
  // dann wird die gleiche Procedure beim Vorfahren aufgerufen
  // um für dieses Ereignis entsprechenden Code zu generieren.
  inherited EditProperty(PropertyEditor, Continue, FreeEditor);
end;

Möchte man keinen Code generieren, sondern etwa einen Dialog öffen, so kann man TComponentEditor als Vorfahren wählen und die Prozedur Edit überschreiben.

TGLControlEditor = Class (TComponentEditor)
public
  procedure Edit; override;
end;

Was ihr nun in dieser Prozedur macht ist euch überlassen. Wie ihr einen Dialog anzeigt, habe ich ja schon bei den Eigenschaftseditoren erklärt.

Das Popupmenu erweitern

Als Erstes müssen wir festlegen, wie viele eigene Einträge angezeigt werden sollen. Dazu überscheiben wir die Funktion GetVerbCount von TComponentEditor.

TFarbAuswahlComponentEditor = class(TComponentEditor)
  function GetVerbCount: Integer; override;
  {...}
end;

function TFarbAuswahlComponentEditor.GetVerbCount: Integer;
begin
  result := 3;//Anzahl der Menupunkte; In unserm Fall drei (0..2)
end;

Durch überschreiben der GetVerb Funktion kann nun festgelegt werden, wie die neuen Menüpunkte heißen:

function TFarbAuswahlComponentEditor.GetVerb(Index: Integer): string;
begin
  Case Index of
    0: Result := 'Farbenauswahl 1' ;
    1: Result := 'Farbenauswahl 2' ;
    2: Result := 'Farbenauswahl 3';
  else
    Result := '?';
  end;
end;

Damit die Menüpunkte auch Sinn machen, sollte natürlich auch noch festgelegt werden was passiert, wenn man auf sie klickt. Und wieder einmal gilt es, eine Procedure zu überschreiben :

procedure TFarbAuswahlComponentEditor.ExecuteVerb(Index: Integer);
begin
  if not (Component is TViereck) then
  begin
    ShowMessage('Dieser Komponenten Editor ist nicht TViereck zugeordnet');
  end
  else
    Case Index of
      0://Klick auf den Ersten Menu Punkt
      begin
        (Component as TViereck).Farben.Rahmen := clBlack;
        (Component as TViereck).Farben.Innen := clWhite;
      end;
      1://Klick auf den zweiten Menu Punkt
      begin
        (Component as TViereck).Farben.Rahmen := clYellow;
        (Component as TViereck).Farben.Innen := clNavy;
      end;
      2://Klick auf den dritten Menu Punkt
      begin
        (Component as TViereck).Farben.Rahmen := clBtnFace ;
        (Component as TViereck).Farben.Innen := clBtnFace ;
      end;
    else
      ShowMessage('Diesen Menupunkt gibt es nicht');
    end;
end;

Auf diese Weise ist es möglich, einfache Popup-Menüs zu basteln.

So einfach ist es, ein eigenes Popup-Menü zu erstellen, wobei ich aber hoffe, das ihr sinnvollere Anwendungen dafür finden werdet als ich in diesem Beispiel.

Die Komponente in der Zwischenablage

Möchte man noch zusätlich eigene Formate beim Kopieren in die Zwischenablage angeben, so kann man die Prozedur Copy überschreiben. Ein Beispiel zu dieser Procedure findet sich in der Delphi-Hilfe.

Den Editor in Delphi einbinden

Genauso wie Eigenschaftseditoren, werden Komponenteneditoren innerhalb der Prozedur Register registiert. Die Prozedur, die darin aufgerufen wird, ist logischerweise RegisterComponentEditor.

procedure Register;
begin
  RegisterComponentEditor(TViereck, TFarbauswahlComponentEditor);
end;

Der erste Parameter ist die Komponente für die der Komponenten-Editor gelten soll und der zweite der Komponenteneditor selbst.

Eigene Eignschafts-Klassen

Ein ganzes, eigenes Objekt im Objektinspektor anzeigen zu lassen ist eigentlich kein Problem : Man muss dazu eigentlich nur wissen, dass man seine Eigenschaftsklasse von TPersistent ableiten muss.

TGraphColors=class(TPersistent)
  {...}
end;

Im privaten Bereich der Komponente speichert man dann die Adresse des Objektes. Im veröffentlichten Bereich deklariert man die Eigenschaft unter der Benutzung einer Prozedur zum Setzen der Eigenschaft.

TGraph = class(TGraphicControl)
private
  FColors:TGraphColors;
  {...}
  procedure SetColors(NewColors:TGraphColors);
public
  {...}
  property Colors : TGraphColors read FColors write SetColors;

Das übernehmen der Werte läuft allerdings etwas anders ab als man vielleicht auf den ersten Blick vermuten würde. Statt den Zeiger auf das neue Objekt zu richten, nimmt das alte Objekt mittels der Assign Prozedur (von TPersistent) die Daten vom Neuen an.

procedure TGraph.SetColors(NewColors:TGraphColors);
begin
  FColors.Assign(NewColors);
end;

Um auf Eigenschafts-Änderungen des Eigenschaftobjektes reagieren zu können, kann man sich eine Art Ereignis einrichten. Also einen Zeiger auf eine Methode :

TGraphColors=class(TPersistent)
private
  FBackGround:TColor;
  FPen:TColor;
  procedure SetBackGround(NewColor:TColor);
  procedure SetPen(NewColor:TColor);
  OnChange:procedure of object; //< Variable zur Speicherung eines Methoden-Zeigers
published
  property BackGround:TColor read FBackGround write SetBackGround;
  property Pen:TColor read FPen write SetPen;
end;

TGraph = class(TGraphicControl)
private
  FGraphFunction:TGraphFunction;
  procedure SetColors(NewColors:TGraphColors);
published
  property Colors : TGraphColors read FColors write SetColors;
end;

procedure TGraphColors.SetBackGround(NewColor:TColor);
begin
  FBackGround:= NewColor;
  if Assigned(@OnChange) then OnChange;//<Falls eine Methode zugewiesen wurde aufrufen
end;

procedure TGraphColors.SetPen(NewColor:TColor);
begin
  FPen := NewColor;
  if Assigned(@OnChange) then OnChange;//<Falls eine Methode zugewiesen wurde aufrufen
end;

Constructor TGraph.Create(AOwner:TComponent);
begin
  inherited;
  FColors := TGraphColors.Create;
  FColors.OnChange := Repaint;//Methode zur Verarbeitung von Veränderungen in FColors festlegen
  {...}
end;

Interaktive Komponenten

Komponenten, die nur etwas anzeigen, mögen zwar auch ihre Daseinsberechtigung haben, allerdings möchte man dem Benutzer auch oft die Möglichkeit bieten, die Komponente in irgendeiner Weise zu beeinflussen. Glücklicherweise bieten die Vorfahren unserer Komponente gleich reihenweise Prozeduren und Funktionen zum überschreiben, um auf verschiedenste Ereignisse zu reagieren. Wobei diese meist vor den Funktionen aufgerufen werden, auf welche die "Ereignis"-Eigenschaften zeigen, oder diese sogar erst aufrufen.

Allgemeine Ereignisse

Jede visuelle Komponente hat TControl als Vorfahren und somit ein paar praktische Proceduren zum überschreiben. Wie etwa: Bei einer Größen-Veränderung wäre es für eine OpenGL-Komponente sicherlich nicht schlecht die Matrizen anzupassen.

procedure TGLControl.Resize;
begin
  if FOpenGLAktiviert then
  begin
    glViewport(0, 0, ClientWidth, ClientHeight); // Setzt den neuen Viewport
    glMatrixMode(GL_PROJECTION); // Projektions Matrix auswählen
    glLoadIdentity; //Idenditätsmatrix laden
    gluPerspective(45.0, ClientWidth/ClientHeight, 1, 100.0); // Perspektive den neuen Maßen anpassen.
    glMatrixMode(GL_MODELVIEW); // Zurück zur Modelview Matrix
    glLoadIdentity(); //Idenditätsmatrix laden
  end;
  inherited; //Die Vorfahren das Ereignis behandeln lassen.
end;

Der Eingabefokus

Wie schon oben erwähnt können nur Komponenten mit Fenster-Handle den Eingabefokus erhalten. Ob eine Funktion den Eingabefokus hat, kann mit der Funktion Focused ermittelt werden. Um besser klar zu machen welche Komponente gerade ausgwählt ist, sollte man die Komponente, wenn sie den Eingabefokus hat, anders zeichnen.

procedure TNachfahrvonTWinControl.Paint;
begin
  inherited;
  {Komponente zeichen}
  if Focused then //Komponente hat den Eingabefokus
  begin
    {Hervorhebung zeichnen}
  end;
end;

Um auf erhalten und entfernen des Eingabefokuses reagieren zu können, bietet TWinControl die Prozeduren DoExit und DoEnter zum überschreiben an.

procedure TDrehElement.DoExit;
begin
  inherited;
  Refresh;
end;
procedure TDrehElement.DoEnter;
begin
  inherited;
  Refresh;
end;

Möchte man, dass die Komponente den Eingabefokus erhält, so ruft man einfach SetFocus auf.

procedure TNachfahrvonTWinControl.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
inherited;
if not Focused then SetFocus;
end;

Windows-Botschaften

Als Erstes muss ich wahrscheinlich klären, was Windows-Botschaften überhaupt sind : Wie schon oben erwähnt, erhält jedes Steuerelement unter Windows ein Handle, welches es genau kennzeichnet. Jedem dieser Handle ist eine Art Postfach zugeordnet, in dem andere Anwendungen oder auch Windows Nachrichten/Befehle hinterlassen können. Eine solche Nachricht kann ein Beenden- oder Zeichenbefehl sein, oder auch eine Benachrichtung über eine Manipulation der Scroll-Leiste.

Das Besondere an diesen Windows-Messages ist, dass sie, wenn sie mit SendMessage gesendet wurden, dem Absender ein Ergebnis mitteilen. Auf diese Weise kann der Absender überprüfen, ob die Nachricht überhaupt behandelt wurde. Ein praktisches Beispiel dafür wäre vielleicht WM_GETDLGCODE, welche nachfrägt, ob Benachrichtungen über das Drücken besonderer Tasten erwünscht sind.

MeineKomponente=class(TCustomControl)
private
  procedure WMGetDlgCode(var Message: TWMGetDlgCode); message WM_GETDLGCODE;
  {...}

procedure TCodeEditor.WMGetDlgCode(var Message: TWMGetDlgCode);
begin
  // Benachrichten wenn Pfeiltasten oder ähnliches gedrückt wurde.
  Message.Result := DLGC_WANTARROWS or DLGC_WANTTAB;
end;

Natürlich gibt es auch Nachrichten, bei denen eine Rückantwort eher weniger Sinn macht.

TLaufSchrift = class(TCustomControl)
private
  procedure WMTimer(var Message:TWMTimer);message WM_TIMER;
  {...}
end;

procedure TLaufSchrift.WMTimer(var Message:TWMTimer);
begin
  if Message.TimerID = My_TimerID then
  begin
    FVerschoben := FVerschoben+FSpeed;
    Refresh;
  end
  else //Unbekannte TimerID
    inherited;//Die Vorfahren werden nach einer Methode mit der gleichen BotschaftsID durchsucht.
end;

Ach ja, diese Möglichkeit auf solche Nachrichten zu reagieren nennt man Botschaftsbehandlungsroutinen. Sie sind der einfachste Weg solche Botschaften zu behandeln. Alternativ dazu kann man auch die Procedure WndProc überschreiben, oder der Eigenschaft WindowProc eine andere Behandlungsprozedur zuweisen.

Veränderungen anzeigen

Möchte man, dass sich das Aussehen der Komponente ändert, so muss man sie zumindest teilweise neu zeichnen. Praktischerweise bietet TControl gleich schon den entsprechenden Befehl, um den Bereich, den die Komponente bedeckt, neu zu zeichnen. Dieser Befehl nennt sich Repaint und macht im wesentlichen nichts anderes, als Invalidate und Update aufzurufen. Wem das Wort Repaint nicht gefällt, der kann alternativ auch die procedure Refresh aufrufen, welche wiederum Repaint aufruft (Eigentlich völlig sinnlos).

procedure TGraph.SetTransparent(NewValue:Boolean);
begin
  FTransparent := NewValue;
  Repaint;//Komponente sofort neu zeichnen
end;

Die Zeichengeschwindikeit kann man einmal erhöhen, in dem man in ControlStyle festlegt, dass die Komponente ihren rechteckigen Bereich vollkommen ausfüllt. Auf diese Weise braucht das, was die Komponente verdeckt, nicht umsonst gezeichnet werden.

constructor TDrehElement.Create(AOwner: TComponent);
begin
inherited;
//Das Steuerelement füllt sein Client-Rechteck vollständig aus.
ControlStyle := ControlStyle + [csOpaque];
{...}
end;

Bei dem kompletten Neuzeichnen größerer Flächen kann es passieren, dass man den Zeichenprozess in verschieden Zwischenstufen sieht, was auch gerne als Flackern bezeichnet wird. Eine Technik namens Doublebuffering, welche auch in den meisten OpenGL-Anwendungen genutzt wird, schafft hier Abhilfe.

Für Komponenten reicht es, die Eigenschaft Doublebuffered auf true zu setzen.

constructor TDrehElement.Create(AOwner: TComponent);
begin
  inherited;
  DoubleBuffered := True;//Doublebuffering nutzen.
  {...}
end;

Schlussworte

Mit diesen wenigen Grundlagen, die ich euch hier heute vorgestellt habe, ist es schon möglich gute und neue Komponten zu programmieren. Wie die meisten anderen Tutorialschreiber sicherlich auch, würde ich mich freuen sehen zu können, dass dieses Tutorial euch bei der Programmierung geholfen hat. Die dazu gehörenden Quelltextbeispiele stehen übrigens unter der Mozilla Public Lizenz, sodass eine entsprechende Nennung im About-Bereich euer Anwendungen reicht um ihn nutzen zu dürfen. Also dann viel Spaß beim Programmieren...

Euer Flo

Dateien


Vorhergehendes Tutorial:
Tutorial Debugging
Nächstes Tutorial:
Tutorial Multithreading

Schreibt was ihr zu diesem Tutorial denkt ins Feedbackforum von DelphiGL.com.
Lob, Verbesserungsvorschläge, Hinweise und Tutorialwünsche sind stets willkommen.