Autor Beitrag
Henry
ontopic starontopic starontopic starontopic starhalf ontopic starofftopic starofftopic starofftopic star
Beiträge: 619

WinXP Prof. SP3
D6 Prof.
BeitragVerfasst: Fr 10.10.03 15:20 
Hallo,

ich habe begonnen in eines meiner Programme eine Pluginschnitstelle einzubauen. Nun habe ich in meinem Programmverzeichnis einen Ordner \Plugin in den die Plugins (DLLs) vom User hineinkopiert werden sollen. Soweit ist es auch kein Problem, aber ich möchte die Möglichkeit schaffen das sich jeder User nur die Plugins integrieren kann die er gerne nutzen möchte. Im HTML-Editor Phase5 wird ein Menüeintrag eingefügt wenn eine neue DLL in den Pluginordner kopiert wurde.

So ungefähr würde ich das auch gerne lösen, denn sonst müste ich ja zu jedem neuen Plugin eine neue Version der Software erstellen damit der eintrag vorhanden ist. Ich weiß nur leider nicht wie ich es machen kann das immer wenn eine neue DLL im Ordner hinzugefügt wird ein neuer Eintrag in meinem Menü auftaucht und dann auch noch die Funktion in der entsprechenden DLL aufruft.

Kann mir da jemand helfen?

Danke im voraus

Moderiert von user profile iconTino: Überflüssige Absätze entfernt.

_________________
mfg Henry
bIce
ontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic starofftopic star
Beiträge: 55



BeitragVerfasst: Fr 10.10.03 16:50 
Ich vestehe nicht ganz wo das Problem ist. Alle PlugIn-Dlls müssen die gleiche Schnittstelle haben.
Bsp:
ausblenden Delphi-Quelltext
1:
2:
3:
4:
function Init: boolean; stdcall;
function GetName(var name: string) : Boolean; stdcall;
function GetIcon(var icon: TIcon) : Boolean; stdcall;
function OnCall(event: Integer): Boolean; stdcall;

Zurückgegeben wird ob die Funktion erfolgreich ausgeführt wurde oder nicht.

Dann in der Hauptanwendung musst Du beim Laden, alle Dlls laden. Den Namen und das Icon abholen und daraus ein Menü erstellen. Beim Schließen alle Dlls wieder freigeben.

Du musst Noch z.B. ein Array mit HMODULE erstellen, wo die Handles zu den Dlls gespeichert werden. und den Index an TMenuItem.Tag zuweisen, dann eine Ereignisbehandlungsroutine für OnClick erstellen, wo mit Hilfe des TMenuItem.Tag und dem Array auf OnCall zugegriffen wird. Wie man Dlls lädt/freigibt, und funktionen benutzt muss ich wohl nicht erklären. Wenn du es nicht weißt dann schaue in der Hilfe nach LoadLibrary/FreeLibrary/GetProcAddress

Moderiert von user profile iconTino: Delphi-Tags hinzugefügt.

_________________
Gott mag anscheinend einfache Leute, denn er hat viele von ihnen erschaffen.
Henry Threadstarter
ontopic starontopic starontopic starontopic starhalf ontopic starofftopic starofftopic starofftopic star
Beiträge: 619

WinXP Prof. SP3
D6 Prof.
BeitragVerfasst: Fr 10.10.03 19:16 
Danke Dir erstmal für die Antwort.

Das Die schnittstelle bei allen DLLs gleich sein muß ist klar.
Mein Problem besteht darin, das Menü zu erstellen.

Wie kann ich denn eine unbekannte Anzahl an DLLs laden von denen ich den Dateinamen nicht kenne?
Dann ist mir auch nicht ganz klar wie ich mit dem die Menüeinträge erstelle und das OnClick- ereignis für die einzelnen Einträge definiere.

Für die ersten Versuche habe ich mit folgender Funktion die DLL dynamisch eingebunden

ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
Procedure DLLAufruf(Dateiname, Funktionsname : String);
 VAR Plugin    : TPlugin1;
     FunctPtr  : TFarProc;
     DLLHandle : THandle;

  Begin
   DLLHandle := LoadLibrary(PChar(Dateiname));
   FunctPtr  := GetProcAddress(DLLHandle, PChar(Funktionsname));
   if FunctPtr <> nil then
    Begin
     @Plugin := FunctPtr;
     Plugin; //Aufruf der DLL-Funktion. Mit Übergabe der Parameter
     FunctPtr := nil;
    End
     Else
       Showmessage('Plugin nicht gefunden!');
       FreeLibrary(DLLHandle);
 End;

_________________
mfg Henry
bIce
ontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic starofftopic star
Beiträge: 55



BeitragVerfasst: Fr 10.10.03 23:04 
Um an den Dateinamen zu kommen musst Du wissen, wo die Dll gespeichert ist. Dann kannst Du FindFirst/FindNext/FindClose-Verwenden um die Namen zu erhalten. Um sich ganz sicher zu sein, dass es sich dabei um ein PlugIn handelt musst Du die dll laden, und versuchen alle notwendigen Funktionen abzufragen. Wenn es ein PlugIn ist dann wird es im Array gespeichert.
Der Übersichtslichkeitshalber habe ich mich nur auf das Notwendigste beschränkt. Ich habe es nicht auf irgendwelche Fehler geprüft, falls etwas nicht funktionieren sollte, dann lass es mich wissen.
ausblenden volle Höhe Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:
78:
79:
80:
81:
82:
83:
84:
85:
86:
87:
88:
89:
90:
91:
92:
93:
94:
95:
96:
97:
98:
99:
100:
101:
102:
103:
104:
105:
106:
107:
108:
// Speichert alle Pfade zu Dlls, die gleichzeitig PlugIns sind
// Path ist der Pfad wo sich die Plugins befinden müssen
procedure GetPlugInNames(path: Stringconst items : TStringList);
var
   sr : TSearchRec;
begin
   path := IncludeTrailingBackslash(path)
   if FindFirst(path + '*.dll', faAnyFile, sr) = 0 then begin
      repeat
         if IsPlugIn(path + sr.Name) then
            items.Add(path +sr.Name);
      until FindNext(sr) <> 0;
      FindClose(sr);
    end;
end;

// Prüft ob es sich bei der Dll um ein PlugIn handelt
function IsPlugIn(path: String): Boolean;
var
   h : HMODULE;
   name: TGetNameFunc; 
   evt : TOnCallFunc; 
begin
   Result := false;
   h := LoadLibrary(PChar(path));
   if h = nil then exit;
   name := TGetNameFunc(GetProcAddress(h, 'GetName'));
   if name = nil then exit;
   evt := TOnCallFunc(GetProcAdress(h, 'OnCall'));
   if evt = nil then exit;
   
   Result := true;
end;

// Klasse des Fensters das die PlugIns verwaltet
TForm1 = class(TForm)
...
private
   // Hier ein statisches Array, dass die PlugIns verwaltet.
   plugIns_ : array[0..1024of HMODULE;
   count_ : Integer; // Anzahl der PlugIns.
   // Ereignisbehandlung für die Menüs, der PlugIns
   class procedure OnPlugInMenuClick(sender : TObject);
   // Lädt alle PlugIns
   procedure LoadAllPlugIns;
   // Gibt alle PlugIns frei
   procedure FreeAllPlugIns;
end;

// Ereignisbehandlung für die Menüs, der PlugIns
class procedure TForm1.OnPlugInMenuClick(sender : TObject);
var
      evt : TOnCallFunc;
begin
   if sender is TMenuItem then
      with sender as TMenuItem do
         if (Tag >= 0and (Tag < count_) then begin
            evt := TOnCallFunc(GetProcAddress(plugIns_[Tag], 'OnCall'));
            if evt <> nil then
               evt;
         end;
      end;
end;

   // Lädt alle PlugIns
procedure TForm1.LoadAllPlugIns;
var
   files : TStringList;
   i : Integer;   
   nameF: TGetNameFunc;
   name : String;
   menu : TMenuItem;
begin
   count_ := 0;
   files := TStringList.Create;
   try
      GetPlugInNames(ExtractFilePath(Application.Name) + 'PlugIns', files);
      for i:=0 to files.Count - 1 do begin
         plugIns_[count_] := LoadLibrary(files.Strings[i]);

         nameF:= TInitFunc(GetProcAdress(h, 'Init'));
         if not initF(name) then
            FreeLibrary(plugIns_[count_])
         else begin
            menu := TMenuItem.Create(mnuHauptEintrag);
            menu.Caption := name;
            menu.OnClick := OnPlugInMenuClick;
            menu.Tag := count_;
            mnuHauptEintrag.Items.Add(menu);
         end;
         Inc(count_);
      end;
   finally
      files.Free;
   end;
end;

// Gibt alle PlugIns frei
// dabei beschränkt sich die Prozedur nur auf das Freigeben der Dlls, 
// und nicht das Löschen der Menüs
procedure TForm1.FreeAllPlugIns;
var 
   i : Integer;
begin
   for i := 0 to count_ - 1 do
      FreeLibrary(plugIns_[i]);
   count_ := 0;
end;

_________________
Gott mag anscheinend einfache Leute, denn er hat viele von ihnen erschaffen.


Zuletzt bearbeitet von bIce am So 12.10.03 00:15, insgesamt 4-mal bearbeitet
Henry Threadstarter
ontopic starontopic starontopic starontopic starhalf ontopic starofftopic starofftopic starofftopic star
Beiträge: 619

WinXP Prof. SP3
D6 Prof.
BeitragVerfasst: Fr 10.10.03 23:11 
Danke Dir.

Ich werde mich mal damit ein wenig beschäftigen.
Melde mich nochmal falls ich noch Probleme habe.

_________________
mfg Henry
Henry Threadstarter
ontopic starontopic starontopic starontopic starhalf ontopic starofftopic starofftopic starofftopic star
Beiträge: 619

WinXP Prof. SP3
D6 Prof.
BeitragVerfasst: Sa 11.10.03 23:51 
Hallo,

Ich bin noch dabei mich mit den Code auseinanderzusetzen und mir eine eigene Unit zu entwickeln.
Dein Codebeispeil hilft mir übrigens sehr dabei.
Nun bin ich auf folgendes Problem gestoßen.

Die Procedure wo ich mir meine Menüeinträge für die Plugins erzeuge sieht an der Problemstelle so aus:

ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
For i := 0 to Items.Count - 1 do
  Begin
   DLLHandle     := LoadLibrary(PChar(Items.Strings[i]));
   GetPluginName := TGetPluginName(GetProcAddress(DLLHandle,'PluginName'));

   NewItem         := TMenuItem.Create(nil);
   Newitem.Caption := GetPluginname;
   NewItem.Tag     := Anzahl;
   NewItem.OnClick := PluginsOnClick;
   PluginMenu.Add(NewItem);
   Inc(anzahl);
  End;//For...


Wenn ich aber mein Projekt Compilieren möchte kommt an der Stelle
ausblenden Delphi-Quelltext
1:
  NewItem.OnClick := PluginsOnClick;					

die Fehlermeldung:
[Fehler] Plugin.pas(49): Inkompatible Typen: Methodenzeiger und reguläre Prozedur

Kommentiere ich die Stelle aus, funktioniert alles wie gewünscht, nützt nur nichts einen Eintrag zu erzeugen der nichts aufruft:)
Wie kann ich denn im Code oben meine Procedure aufrufen die bei OnClick aufgerufen werden soll? Was mache ich falsch? Irgendwie muß es doch möglich sein, das ich eine Procedure oder Funcktion aufrufen kann, trotzdem ich den Menüeintrag erst zur Laufzeit erzeuge.

Könnte mir da nochmal jemand auf die Sprünge helfen?

Danke im voraus

_________________
mfg Henry
bIce
ontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic starofftopic star
Beiträge: 55



BeitragVerfasst: So 12.10.03 00:01 
Henry hat folgendes geschrieben:

Wenn ich aber mein Projekt Compilieren möchte kommt an der Stelle
ausblenden Delphi-Quelltext
1:
  NewItem.OnClick := PluginsOnClick;					

die Fehlermeldung:
[Fehler] Plugin.pas(49): Inkompatible Typen: Methodenzeiger und reguläre Prozedur

Kommentiere ich die Stelle aus, funktioniert alles wie gewünscht, nützt nur nichts einen Eintrag zu erzeugen der nichts aufruft:)

Schreibe vor der Definition/Deklaration von PluginsOnClick "class" (Ohne Anführungsstriche). Oder siehe den Quelltext von mir. Ich habe ihn angepasst.

_________________
Gott mag anscheinend einfache Leute, denn er hat viele von ihnen erschaffen.
Henry Threadstarter
ontopic starontopic starontopic starontopic starhalf ontopic starofftopic starofftopic starofftopic star
Beiträge: 619

WinXP Prof. SP3
D6 Prof.
BeitragVerfasst: So 12.10.03 00:06 
Danke,

ich werde es mal versuchen

_________________
mfg Henry
Henry Threadstarter
ontopic starontopic starontopic starontopic starhalf ontopic starofftopic starofftopic starofftopic star
Beiträge: 619

WinXP Prof. SP3
D6 Prof.
BeitragVerfasst: So 12.10.03 00:35 
Wenn ich einfach Class davor schreibe, dann kommt folgende Fehlermeldung

[Fehler] Plugin.pas(17): Deklaration von 'PluginsOnClick' unterscheidet sich von vorheriger Deklaration

Kann es daran liegen, das ich alle meine Funktionen die ich für meine Pluginbearbeitung verwenden möchte in einer eigenen Unit habe und nicht wie Du in der Unit des Formulars?
Denn ich möchte der Übersicht halber eine eigene Unit dafür haben, die alles enthält was ich brauche, dann kann ich es einfacher in andere Anwendungen übernehmen und muß nicht immer bei null anfangen.

Die Declaration sieht nun bei mir so aus
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
  Interface

   ...

  Class Procedure PluginsOnClick(Sender: TObject);
  Procedure LoadPlugins(Pfad: String; Pluginmenu:TMenuItem);



und die Procedure so:
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
Class Procedure PluginsOnClick(Sender: TObject);
Begin
 If Sender is TMenuItem then
  Begin
   With Sender as TmenuItem do
    Begin
     
    End;
  End;

End;


Wie gesagt alles in einer eigenen Unit, falls es einen Unterschied machen sollte.

_________________
mfg Henry
maximus
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 896

Win XP, Suse 8.1
Delphi 4/7/8 alles prof
BeitragVerfasst: So 12.10.03 11:42 
IMAO ist das mit dem 'class' quatsch...wüsste nicht was das zum problem beiträgt?

ausblenden Quelltext
1:
[Fehler] Plugin.pas(49): Inkompatible Typen: Methodenzeiger und reguläre Prozedur					


Das bedeutet, dass du 'PluginsOnClick' als methode in einer klasse definieren musst:
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
Procedure TDeineKlasse.PluginsOnClick(Sender: TObject); 
Begin 
 If Sender is TMenuItem then 
  Begin 
   With Sender as TmenuItem do 
    Begin 
      
    End
  End

End;


dann kannst du dein event endlich zuweisen :wink:

_________________
mfg.
mâximôv
bIce
ontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic starofftopic star
Beiträge: 55



BeitragVerfasst: So 12.10.03 13:40 
maximus hat folgendes geschrieben:
IMAO ist das mit dem 'class' quatsch...wüsste nicht was das zum problem beiträgt?

Stimmt, weiß garnicht mehr wie ich darauf gekomment bin ?;)
class macht es nur zu einer statischen Methode, was gar nicht erforderlich ist.
[code]


Zitat:

Das bedeutet, dass du 'PluginsOnClick' als methode in einer klasse definieren musst:
ausblenden Delphi-Quelltext
1:
Procedure TDeineKlasse.PluginsOnClick(Sender: TObject);					


So stand es schon seit längerem da. Ist Henry vielleicht nicht aufgefallen.

_________________
Gott mag anscheinend einfache Leute, denn er hat viele von ihnen erschaffen.
Henry Threadstarter
ontopic starontopic starontopic starontopic starhalf ontopic starofftopic starofftopic starofftopic star
Beiträge: 619

WinXP Prof. SP3
D6 Prof.
BeitragVerfasst: So 12.10.03 21:50 
Hallo ich bin es wieder :)

Ich bin mit eurer Hilfe schon ein großes Stück weiter gekommen.

Es ist aber ein weiteres Problem aufgetreten.

Ich möchte einfach eine Funktion in der PluginDLL aufrufen die mir ein True zurück giebt
wenn es ein Plugin ist.
Wenn es aber keine PluginDLL ist, dann existiert die Function ja nicht, deshalb bekomme ich eine Fehlermeldung. Giebt es eine Möglichkeit festzustellen ob die funktion existiert? So wie:
ausblenden Delphi-Quelltext
1:
 FileExists('../datei.dll')					

So das das Programm nicht versucht die Funtion aufzurufen und dann abstürtzt.

So siet die betreffende Stelle bei mir aus. Nur wenn die Function in der DLL nicht existiert, dann giebt es einen Absturz des Programmes.
ausblenden Delphi-Quelltext
1:
2:
  GetPluginName := TGetPluginName(GetProcAddress(Plugins_[Count_],'PluginName'));
  Init := TInit(GetProcAddress(Plugins_[Count_],'Init'));

Vieleicht könntet Ihr mir da nochmal unter die Arme greifen

_________________
mfg Henry
Henry Threadstarter
ontopic starontopic starontopic starontopic starhalf ontopic starofftopic starofftopic starofftopic star
Beiträge: 619

WinXP Prof. SP3
D6 Prof.
BeitragVerfasst: Di 14.10.03 20:15 
Das Problem ist gelöst, danke allen die mir geholfen haben.

EDIT: Meine Lösung die ich nun nutze ist:,
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
If GetProcAddress(Plugins_[Count_],'PluginName') = NIL then
 Begin
  // Wenn Function nicht Vorhanden
 End
  Else
   Begin
    // Ereignis wenn Function vorhanden
   End;

Falls jemand anders auch soetwas sucht, und demjenigen somit auch geholfen ist.

_________________
mfg Henry
bIce
ontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic starofftopic star
Beiträge: 55



BeitragVerfasst: Mi 15.10.03 18:31 
Henry hat folgendes geschrieben:

[...]
So das das Programm nicht versucht die Funtion aufzurufen und dann abstürtzt.

So siet die betreffende Stelle bei mir aus. Nur wenn die Function in der DLL nicht existiert, dann giebt es einen Absturz des Programmes.
[...]

Wozu willst Du eigentlich eine Spezielle Funktion, um zu überprüfen ob es ein PlugIn ist? Es wäre doch besser wenn Du einfach prüfst ob alle anderen Erforderliche Schnittstellen vorhanden sind. So wie in der oben geposteten Funktion IsPlugIn().

Wenn Du das Modell aber in mehreren Programmen , dann könntest Du eine ID für bestimmtes Programm verwenden, diese IDs könntest Du z.B. (wie in COM) mit CoCreateGuid() erzeugen.

_________________
Gott mag anscheinend einfache Leute, denn er hat viele von ihnen erschaffen.
Henry Threadstarter
ontopic starontopic starontopic starontopic starhalf ontopic starofftopic starofftopic starofftopic star
Beiträge: 619

WinXP Prof. SP3
D6 Prof.
BeitragVerfasst: Mi 15.10.03 19:27 
bIce hat folgendes geschrieben:

[...]
Wozu willst Du eigentlich eine Spezielle Funktion, um zu überprüfen ob es ein PlugIn ist? Es wäre doch besser wenn Du einfach prüfst ob alle anderen Erforderliche Schnittstellen vorhanden sind. So wie in der oben geposteten Funktion IsPlugIn().
[...]

Da hast Du recht, letzendlich werde ich es wohl so machen.

bIce hat folgendes geschrieben:

Wenn Du das Modell aber in mehreren Programmen , dann könntest Du eine ID für bestimmtes Programm verwenden, diese IDs könntest Du z.B. (wie in COM) mit CoCreateGuid() erzeugen.

Mit einer Guid habe ich mich auch noch nicht befasst, aber ist denn Die nicht bei jedem erzeugen anders?
Ich werde aber mal in der Hilfe nachschauen.
Da ich versuche meine Pluginschnittstelle erweiterbar zu gestallten, werde ich erstmal eine erste Version zum laufen bringen. Dann werde ich das ganze immer weiter ausbauen. Dazu sind natürlich alle Tips die ich bekommen kann nützlich für mich.
So lernt man ja auch dazu.

_________________
mfg Henry
Knulli
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Beiträge: 116
Erhaltene Danke: 2

Win2k, Win7, Win10
D5, D2005, D2006, D2007, D10.4.2
BeitragVerfasst: Fr 23.03.07 14:54 
Hallo Forum,

ist zwar schon ne Weile her, das hier diskutiert wurde, möchte trotzdem mal sehen, ob der Thread "noch lebt".

Frage: Könnte man in der (PlugIn-)DLL nicht auch eine einzige Funktion nur schreiben, die eine Referenz auf ein TMyPlugIn-Objekt zurückgibt? Alle eigentlich benötigten Funktionalitäten wären dann über die Member (Methoden und Eigenschaften) der Klasse "erreichbar". Stellt sich natürlich die Frage, was mit Methoden/Eigenschaften ist, die neu hinzukommen. Was steckt eigentlich alles an Informationen in so einem Parameter bzw. Rückgabewert vom Typ "TMyPlugIn"? Nur der Klassenname oder auch Informationen über alle Member? Hab mal irgendwann was über Late-Binding gehört, wo erst zur Laufzeit ermittelt wird, ob es ein bestimmtes Member in einer Klasse gibt. Wie kann ich sowas in Delphi erzwingen? Wenn das gehen würde, hätte man ja mit "TMyPlugIn" eine wunderbare, unendlich erweiterbare PlugIn-Schnittstelle.

Ich hoffe, nichts durcheinandergerbracht zu haben und hoffe auf Meinungen/Ideen/Kritik.

mfg Knulli
HelgeLange
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starofftopic star
Beiträge: 735
Erhaltene Danke: 6

Windows 7
Delphi7 - Delphi XE
BeitragVerfasst: Fr 23.03.07 18:18 
rein technisch gesehen kannst du ein Standard-Object-Typ erzeugen und ihm mit Dispatch selbst-definierte Nachrichten schicken. Regiert das Object drauf (liefert ein bestimmtes Ergebnis zurück), wird diese Funktion von dem Object unterstützt. So kannst Du Dir eine Feature-Matrix für jedes geladene Plugin erstellen.

Das Problem wird sein, dass Du immer hinterher-hinkst. Gerade, wenn Du von anderen Programmierern Plugins akzeptieren willst. Solche Plugins haben einfach den Nachteil, sehr starr zu sein, da sie immer vom Code des Loaders abhängig sind und wie er die Schnittstelle interpretiert. Und je mehr du einbaust in die Schnittstelle (je mehr Funktionen), desto komplizierter wird das "Flöhe-Hüten" ;)

Schau doch mal in den Thread "ERP Framework Components" (in "Kommerzielle Projekte"), dort habe ich ein wenig beschrieben, wie ich das gelöst habe, vielleicht hilft es Dir etwas ? Ansonsten frag halt hier nochmal :)

_________________
"Ich bin bekannt für meine Ironie. Aber auf den Gedanken, im Hafen von New York eine Freiheitsstatue zu errichten, wäre selbst ich nicht gekommen." - George Bernhard Shaw