Überblick
Beispiele
Schnappschüsse
Vergleiche
Anwendungen
Herunterladen
Documentation
Basar
Status & Fahrplan
Häufig gestellte Fragen
Autoren & Lizenz
Forum
Ultimate++ finanziell unterstützen
Diese Seite durchsuchen
Sprache
Deutsch













SourceForge.net Logo



Übersicht über Ultimate++

 

Einige Leckerbissen vorweg

Ultimate++ verspricht eine drastische Reduzierung des Quellcode Umfangs für typische Desktop Anwendungen. Lass uns mit einem einfachen Beispiel anfangen - eine Anwendung, welche die Anzahl zwischen zwei Datumsangaben anzeigt.

 

 

Das Layout des Anwendungsfensters wird mit dem Ultimate++ "visual designer" erstellt:

 

 

Der eigentliche Quellcode für die Anwendung ist so komplex wie das hier:

 

#include <CtrlLib/CtrlLib.h>

 

#define LAYOUTFILE <Days/Days.lay>

#include <CtrlCore/lay.h>

 

class Days : public WithDaysLayout<TopWindow> {

public:

    void Compute();

 

    typedef Days CLASSNAME;

    Days();

};

 

void Days::Compute()

{

    result = IsNull(date1) || IsNull(date2) ? "" :

             Format("There is %d day(s) between %` and %`",

                   abs(Date(~date1) - Date(~date2)), ~date1, ~date2);

}

 

Days::Days()

{

    CtrlLayout(*this, "Days");

    date1 <<= THISBACK(Compute);

    date2 <<= THISBACK(Compute);

}

 

GUI_APP_MAIN

{

    Days().Run();

}

 

Alles gehört irgendwo dazu

In Ultimate ++ sind die meisten der Objekte an einen logischen Geltungsbereich gebunden. Als Ergebnis davon wirst du nicht viele neue Operatoren im Quellcode finden wenn du Ultimate++ benutzt und, außerhalb von Container, nahezu keine Operatoren um etwas zu löschen.

Das bedeutet natürlich nicht, dass du keine Zeiger verwenden darfst aber es ist eine gute Übung um Zeiger zu benutzen, die eben nur auf Dinge zeigen, und niemals um Heap Ressourcen zu verwalten. Dies verhindert auch jegliche Verwirrung in Bezug auf den Besitz des untergeordneten Objekts, auf den Zeitpunkt seiner Löschung usw. Wenn du Datensätze variabler Größe oder polymorphe Typen verwalten musst, solltest du lieber einen der Ultimate++ Container benutzen.

Um es zu erwähnen, es gibt in Ultimate++ keine gemeinsamen "smart Pointer" (wie die boost::shared_ptr) um Heap Ressourcen auf Schnittstellenebene zu verwalten. Sie werden nicht gebraucht und werden als schlechte Praxis angesehen.

In C++ hat sich dieser Ansatz als ebenso gut oder sogar besser herausgestellt als eine aus Restdaten zusammen gesammelte Sprachen wie Java oder C#. Während solche Sprachen in der Lage sind eine automatische Verwaltung von Heap Ressourcen zu bieten, bietet der U++ Ansatz eine sehr deterministische, automatische Verwaltung aller Ressourcen.

Ultimate++ Container

Ein Gesichtspunkt von Ultimate++ ist, eine menge Kritik mitzubringen: Ultimate++ verwendet nicht viele der Standard C++ Bibliotheken. Dafür gibt es allerdings schwerwiegende Gründe. STL, mit seiner vernichtenden Anforderung, dass jedes Element innerhalb eines Containers einen Kopier-Konstruktor haben muss, macht es ziemlich schwierig einen Standard-Container in der Oberflächenentwicklung zu verwenden.

Es gibt keine grundsätzliche Notwendigkeit für die Ultimate++ Container. Stattdessen gibt es zwei Gruppen von Ultimate++ Containern.

Die Vektor-Gruppe, mit der Notwendigkeit beweglich zu sein, erlaubt eine sehr schnelle Implementierung bestimmter Typen (z.B. das Einfügen von Elementen an beliebiger Position in Ultimate++ Vector<String> ist mehr als 10-mal schneller als die gleiche Operation mit einer typischen Implementierung über std::vector<std::string>).

Die Array-Gruppe hat keine Anforderungen an ihre Element-Typen im allgemeinen, zum Preis einer etwas geringeren Leistung.

Als ein Resultat dessen, kannst du in Ultimate++ zum Beispiel einen Container mit GUI Steuerelementen, die ganzzahlige Nummern bearbeiten (Array<EditInt> integer_editors), erstellen und auch über den Standard Ultimate++ Sortier-Algorithmus (Sort) sortieren. Um so etwas zu machen, würde man in STL Zeiger als Elemente (std::vector<EditInt *>), oder alternativ eine Art der "Smart Pointer" (zukünftig std:: boost::shared_ptr) benutzen müssen, aber beide Varianten erhöhen die Komplexität des Quellcodes und brechen die Ultimate++ Regel, dass alles irgendwo dazu gehört.

Wem die Steuerelemente (Widgets) gehören

Eines der Dinge, die wir im Laufe unserer zahllosen Experimente mit der C++ GUI herausgefunden haben, ist die Tatsache, dass der Werkzeugkasten für die Anwenderoberfläche (GUI) nicht die Steuerelemente selbst besitzen sollte. Die Objekte der GUI sollten immer dem Client gehören, einem Gültigkeitsbereich des Client-Codes zugeordnet (alles gehört irgendwo dazu). Der GUI Werkzeugkasten verweist nur auf sie, weder erstellt, noch zerstört er sie. Jedes Steuerelement kann seine Oberflächenfunktion in einem bestimmtem Kontext (wie in einigen Fenstern sichtbar zu sein) erfüllen, ist jedoch zur selben Zeit immer ein selbstständiges Gebilde mit seinen Attributen, die ohne Rücksicht auf den gegenwärtigen GUI Zustand, verändert oder abgefragt werden können.

Dies hat einige schwerwiegende Folgen. Die wichtigste ist, dass Ultimate++ nicht erfordert, dass Steuerelemente direkt auf dem Heap zugewiesen werden. Dies wiederum erlaubt es uns, GUI-Dialog-Strukturen in sehr effektiver Weise anzuordnen, statt

 

struct MyDialog {

    Option *option;

    EditField *edit;

    Button *ok;

};

 

benutzen wir:

 

struct MyDialog {

    Option option;

    EditField edit;

    Button ok;

};

 

Noch wichtiger ist es, dass die Lebenszeit dieser Steuerelemente nicht vom Lebenszyklus des "MyDialog"-GUI abhängt - "MyDialog" kann geschlossen oder noch nicht geöffnet sein, die Attribute der Steuerelemente sind trotzdem jederzeit adressierbar.

Dialog Templates sind C++ Templates

Jetzt, da wir den Grundstein gelegt haben, ist es an der Zeit die coolsten Aspekte der Ultimate++ GUI Programmierungs-Layout-Templates vorzustellen:

Wenn du ein Layout visuell über den Layout Designer von TheIDE erstellst (normalerweise, aber nicht zwingend, das Layout eines Dialoges), wird dieses Layout in deinem Quellcode als ein C++ Template wiedergespiegelt, welches sich von einer Steuerelement-basierten Klasse ableitet und alle Steuerelemente als Mitglieder-Variablen deklariert und eine passende Funktion (InitLayout), welche die Position des Steuerelements und alle vorgefertigten Attributstandards festlegt, wird angelegt.

Zum Beispiel würde so ein Template folgendermaßen aussehen:

 

template <class T>

struct WithMyDialogLayout : public T {

    Option option;

    EditField edit;

    Button ok;

};

 

template <class T>

void InitLayout(WithMyDialogLayout<T> *layout, ...);

// implementation details omitted

 

Der Grund, weshalb es als Template, und nicht als eine einfache Klasse oder Struktur bereitgestellt wird, ist, dass du dadurch jeden Steuerelement-Typ als Basisklasse nutzen kannst, nicht nur das eine, welches das Dialog-Fenster repräsentiert (TopWindow).

Dieser Ansatz bietet eine drastische Reduzierung der Komplexität - viele lästige Sachen, welche notwendig für die Identifizierung der Steuerelemente im Client-Code (wie die Steuerelement-ID oder die Namen) schienen, fallen einfach weg. Alles, womit du in Ultimate++ umgehen musst, sind deine Instanzenvariablen.

Value und Null

Ein Aspekt, der die Entwicklung in Ultimate++ sehr orthogonal macht, ist die Existenz des "Value" - der polymorphe Datentyp. Jeder der Ultimate++ Basistypen (int, double,String, Color, Rect, Font, Image etc...) kann in einem Value gespeichert oder daraus zurückgeliefert werden. Das Value selbst kann für den Typ des Wertes, den es enthält, abgefragt werden. Es ist folglich sehr einfach jeden selbsterstellten Typ Value kompatibel zu machen.

Ähnlich zu Konzept des Value ist das grundsätzliche Konzept des "empty value". Diese spezielle Ultimate++ Konstante "Null" repräsentiert einen leeren Wert. Die meisten, konkreten Typen unterstützen Null als Wert. Die Konstante Null ist ebenso für die grundsätzlichen Typen - int, double und int64 - als ein Wert definiert, der kleiner ist als jeder andere Wert für den jeweiligen Typ (zum Beispiel ist Null gleich INT_MIN für int). Um zu prüfen ob die Variable eines bestimmten Typs Null ist, kannst du die generische Funktion "IsNull" verwenden.

Der Typ Value (und Null) hat eine bemerkenswerte Auswirkung auf die Flexibilität der GUI. Viele Steuerelemente haben logischerweise ihre "natürlichen" Werte (für ein ganzzahliges Eingabefeld ist es die eingegebene Nummer, für ein Options-Steuerelement ist es entweder wahr (true) oder falsch (false), abhängig vom Status) und Ultimate++ ermöglicht einen einheitlichen Zugriff auf diese Werte über die virtuellen Methoden "Value" und "GetData" / "SetData". Zum Beispiel kann einen Dialog üblicherweise, durch das setzten von Null für alle Steuerelemente, aufräumen.

Display und Convert

Auf Display und Convert basierende Klassen verbessern Ultimate++'s Flexibilität weiter durch die Nutzung von Value.

Convert Klassen funktionieren als doppelt gerichtete Value zu Value Konverter. Üblicherweise, aber nicht zwingend, findet diese Konvertierung zwischen dem Wert eines logischen Datentyps und seiner textuellen Repräsentation statt (manchmal kann die Umwandlung der textuellen Repräsentation zum logischen Datentyp weggelassen werden). Beispiele sind ConvertInt oder ConvertDate.

Viele Ultimate++ Steuerelemente sind in der Lage, ihre Umwandlungsklassen als Eigenschaften zu verwenden. Ein Beispiel ist die Klasse des EditField's, ein generisches Eingabefeld. Durch zuweisen einer spezifischen, auf Convert basierenden Klasse, kannst du ihm beibringen Nummern, Datumswerte oder irgendetwas, das eine textuelle Repräsentation hat, zu bearbeiten.

Ziemlich ähnlich zu den auf Convert-Klassen sind die auf Display basierenden Klassen. Das sind Klassen, die beschreiben, wie Werte angezeigt werden sollen. Abermals nutzen viele Ultimate++ Steuerelemente die Display Klassen für ihre Eigenschaften. Zum Beispiel, um dem DropList Steuerelement (DropList ist dicht an etwas dran, was auf anderen Plattformen "combo box" genannt wird) "beizubringen", Farben anzuzeigen, ist alles was du tun musst, sein "Display" Attribut auf "DisplayColor" zu setzen (denke daran, Color ist Value kompatibel und die Liste von DropList besteht aus Values). Indessen kannst du die gleiche "DisplayColor" als eine Eigenschaft für viele andere Steuerelement-Klassen benutzen.

Callbacks

Während virtuelle Methoden eine großartige Möglichkeit bieten, die Eingabeschnittstelle der GUI Steuerelemente (wie Maus oder Tastatureingaben) zu strukturieren, muss der GUI Werkzeugkasten ebenso effiziente Instrumente für die Ausgabeschnittstelle bereitstellen (wenn du nicht weißt was eine Ausgabeschnittstelle ist: Wenn ein Button-Steuerelement (also ein Knopf) gedrückt wird, ist die Ausgabeschnittstelle dafür verantwortlich, diese Information an den Client-Code zu übergeben).

Unsere Lösung für diese Anforderung nennt sich Callback. Du kannst dir Callbacks als eine sehr vereinheitlichte Form von Funktionszeigern vorstellen. Jeder Callback repräsentiert eine bestimmte Aktion - üblicherweise umfasst dies, eine bestimmte Funktion oder eine bestimmte Objekt-Methode aufzurufen - das kann jederzeit geschehen.

Callbacks sind allgemein und können einige sehr interessante Arten annehmen. Zum Beispiel hat ein Typ von Callback die einfache Aufgabe zwei andere, vorgegebene Callbacks aufzurufen um ein sehr einfaches Werkzeug zur Gruppierung zu bieten. Es gibt Callbacks, die akzeptieren keine Parameter, rufen aber eine Funktion oder Methode mit einem Parameter auf, wenn sie aufgerufen werden - dieser zusätzliche Parameter wird innerhalb des Callbacks bei seiner Konstruktion gespeichert. Um dieses wichtige Feature zu veranschaulichen sieh dir den nachfolgenden Codeausschnitt an:

 

void MyDlg::SetEditorValue(int x)

{

    editor <<= x;

}

 

MyDlg::MyDlg()

{

    button1 <<= THISBACK1(SetEditorValue, 1);

    button2 <<= THISBACK1(SetEditorValue, 2);

 

In diesem Ausschnitt haben wir zwei Knöpfe und ein ganzzahliges Eingabefeld. Das Drücken auf den ersten oder zweiten Knopf setzt das Eingabefeld auf den Wert 1 beziehungsweise 2.

Es ist also sehr wichtig, dass Callbacks völlig von den Klassen entkoppelt sind. Während sie spezifische Methoden oder bestimmte Objekt Instanzen ansteuern können, gibt es keine Anforderungen an die Methode (abgesehen von der Signatur) oder die Klasse des Objekts.

Nur um es für diejenigen, die mit der Boost-Bibliothek vertraut sind, klarzustellen - ja, Callback Klassen sind im Grunde sehr ähnlich zu boost::function, mit einer, ein klein wenig für die Bedürfnisse des Ultimate++ Frameworks aufpolierten, Schnittstelle (sie sind beweglich - können in Vektoren speichert werden).

Ultimate++'s Ausstattung an Steuerelementen

Obwohl die U++ Steuerelement-Standardausstattung für uns weniger wichtig ist als die grundsätzlichen Prinzipien, teilweise aufgrund der Tatsache, dass das Erstellen neuer Steuerelement-Klassen in U++ oftmals eine banale Aufgabe ist, würde jede Beschreibung des Toolkits ohne sie unvollständig sein.

Also folgt hier eine unvollständige aber repräsentative Liste:

Label, Button und Option sind grundsätzlich altbekannte Steuerelemente

Switch wird üblicherweise als "eine Gruppe von radio-buttons" bezeichnet, trotzdem ist es in U++ ein einzelnes Steuerelement (auf diese Art und Weise ist das Auslesen eines Wertes aus einem Switch viel stimmiger).

EditField, EditInt, EditDouble, EditIntSpin, EditDate, EditString sind grundsätzliche Eingabefelder. Beachte, dass U++ eindeutige Inputfeld-Typen für spezielle Werttypen bietet.

LineEdit und DocEdit sind zwei Arten von Klartext-Editoren. LineEdit arbeitet mit Zeilen während DocEdit mit Absätzen arbeitet.

ScrollBar und ScrollBars. Obwohl ihr Namen selbsterklärend sind (Scrollbars ist nur ein Paar, bestehend aus einer vertikalen und einer horizontalen Scollbar), ist zu erwähnen, dass die U++ Scollbar auch alle Rechenoperationen für die Positionierung im sichtbarem Bereich.

Slider ist ein "analoges" Eingabe-Steuerelement, dessen Wert aus der Position des "thumb" bestimmt wird.

HeaderCtrl bietet den Header verschiedener Tabellen, nämlich von ArrayCtrl

ArrayCtrl ist möglicherweise das komplexeste und kompliziertes Steuerelement in Ultimate++. Es ist im Prinzip ein Tabellen-Steuerelement, welches benutzt wird um auf Value-Matrizen zu arbeiten. Es kann Values miteinander kombinieren um sie (unter Verwendung der Display-Klasse) als Spalten (ja, mehrere Values können in Reihe als einzelne Spalte, wenn nötig mit Convert, kombiniert werden) anzuzeigen und sie mit Hilfe von untergeordneten Controls (diejenigen, welche innerhalb der Tabelle unter der Benutzer "edit" Option zu finden sind, innerhalb der Tabelle immer sichtbar oder außerhalb in der Dialog Box, welche den Wert der aktuell ausgewählten Zeile anzeigt) zu bearbeiten.

Option, EditString, DropList, Switch und das ArrayCtrl in Aktion

SqlArray wird von ArrayCtrl abgeleitet und fügt dieser die Fähigkeit, als SQL Tabellen Editor mit master-detail Befähigung zu fungieren, hinzu.

Splitter wird benutzt um anzeigende Steuerelemente mit einer einstellbaren Leiste zu zerteilen.

ProgressIndicator kann benutzt werden um den Prozess langwieriger Operationen anzuzeigen.

TabCtrl wird für Dialoge mit Reitern (tabs) benutzt.

TreeCtrl wird benutzt um beliebige Baumstrukturen anzuzeigen.

ColorSelector, ColorPusher und ColorButton sind Steuerelemente um den Nutzer grafisch Farben auswählen zu lassen.

ColorButton

MenuBar und ToolBar sind etwas unkonventionell in der Handhabung in Ultimate++ da die Menü-Aktionen, repräsentiert durch Callbacks, an die Methode, welche die entsprechende Leiste aufbaut, weitergeleitet werden. Dies hat ein paar echte Vorteile - Status und Präsenz individueller Knöpfe oder Menüleisten-Items kann einfach, je nach dem Status der Anwendung, eingestellt werden. Es ist ebenso oftmals möglich, eine einzelne Methode zum Erstellen von ToolBar und MenuBar zu verwenden.

ColumnList zeigt Werte in einer, vom Nutzer einstellbare, Anzahl von Spalten an

FileList ist eine Variante der ColumnList um Dateilisten anzuzeigen

Und zum Abschluss hat Ultimate++ Werkzeuge um mit erweiterter Textformatierung umzugehen:

RichText ist eine Klasse, welche die Speicherung umfangreicher Text-Dokumente, Schrift und Absatzformatierung sowie Unterstützung für verschachtelte Tabellen, inbegriffen.

RichTextView ist ein Steuerelement um den Text eines RichText Elements anzuzeigen.

RichEdit ist ein vollständig ausgestattetes Schreibprogramm (mit Rechtschreibprüfung) im Standard - Steuerelemente - Paket, für jede U++ Anwendung bequem verfügbar.

RichEdit

 

Du kannst hier eine komplette, alphabetische List der grundsätzlichen U++ Steuerelemente finden.

SQL Programmierung

Einer der Anregungen hinter Ultimate++ war immer die Entwicklung von Enterprise Client-Server SQL Anwendungen. Durch die grundsätzliche Ultimate++ Philosophie glauben wir, einige außergewöhnliche Ergebnisse erreicht zu haben, hauptsächlich die einfachere Ultimate++/SQL Entwicklung als mit den zu SQL gehörenden Entwicklungswerkzeugen.

Natürlich ist SQL ein Gebiet, auf dem sich die Value Abstrahierung enorm auszahlt. Datenbankwerte auszulesen und sie in GUI Steuerelemente einzufügen war noch nie so trivial wie es in Ultimate++ ist.

Ein wichtiges Werkzeug in Bezug auf SQL ist die Idee des "SQL Ausdrucks". Der SQL Ausdruck ist ein Gebilde, welches eine SQL Anweisung darstellt. Ultimate++ bietet Möglichkeiten, einen SQL Ausdruck durch den Mechanismus des Überladens in C++ zu erstellen. Zum Beispiel könnte der Ultimate++ SQL Ausdruck wie folgt aussehen:

 

Select(NAME, SURNAME).From(PERSON).Where(PERSONID == personid);

 

wobei NAME, SURNAME, PERSON und PERSONID spezielle Werte vom Typ SqlId sind, während personid eine ganz gewöhnliche C++ Variable ist. Das wichtige dabei ist, dass der SQL Ausdruck durch kleinere Unterausdrücke aufgebaut werden kann - dies ist besonders wichtig wenn "Where" Bedingungen gebaut werden.

 

SqlBool where;

if(!IsNull(findname))

    where = NAME == findname;

if(!IsNull(findsurname))

    where = where && SURNAME == findsurname;

SqlExp exp = Select(PERSONID).From(PERSON).Where(where);

 

Wenn ein SQL Ausdruck bereit ist, ausgeführt zu werden, kann er über ein "Sql Zeiger Objekt", unter Verwendung des * Operators, ausgeführt werden. Danach kannst du das Ergebnis abrufen:

 

Sql sql;

sql * exp;

while(sql.Fetch()) {

    Sql sqlu;

    sqlu * Update(PERSON)(SALARY, SALARY + 100).Where(PERSONID == sql[0])

}

 

Ein anderes, effizientes Werkzeug ist das Konzept der Datenbank-Schema-Dateien. Das sind Dateien, die verwendet werden, um ein Datenbank-Modell über spezielle C-Makro Konstruktionen, festzulegen:

 

TABLE_(PERSON)

    INT_     (PERSONID) PRIMARY_KEY

    STRING_  (NAME, 200)

    STRING_  (SURNAME, 200)

    DOUBLE_  (SALARY)

END_TABLE

 

Diese Beschreibungs-Dateien werden dann benutzt um das Datenbank Modell auf dem SQL Server zu synchronisieren, Sqlld Kontanten, welche in SQL Ausdrücken benutzt werden, zu generieren und zu guter Letzt, um C++ Strukturen (nach den Tabellen mit einem vorangestelltem S_ bekannt) zu erstellen, welche benutzt werden können, einen SQL Ausdruck zu formen und die Resultate abzufragen.

 

S_PERSON person;

SQL * Select(person).From(PERSON);

while(SQL.Fetch(person))

    person_table.Add(person.PERSONID, person.NAME, person.SURNAME);

 

Dank des oben beschriebenem Value-Konzepts lassen sich die meisten Steuerelemente von Grund auf nahtlos mit SQL Code verbinden. Eines der Werkzeuge, welches diese Möglichkeiten ausnutzt, ist die SqlCtrls-Klasse, welche den Datenaustausch zwischen Dialog-Steuerelementen und Datenbankwerten bewerkstelligt:

 

void EditPerson(int persionid) {

    WithPersonLayout<TopWindow> dlg;

    SqlCtrls ctrls;

    ctrls(PERSON, dlg.person)(NAME, dlg.name)(SURNAME, dlg.surname);

    SQL * Select(ctrls).From(PERSON).Where(PERSONID == personid);

    ctrls.Fetch(SQL);

    if(dlg.Run() == IDOK)

        SQL * ctrls.Update(PERSON).Where(PERSONID == personid);

}

 

Zusammenfassung

In dieser Übersicht haben wir versucht, die aufregendsten Besonderheiten von Ultimate++ zusammenzufassen. Es gibt natürlich noch sehr viel mehr wichtige Merkmale wie verschiedene, interessante Implementierungstechniken wie Null-Overhead-Speicherzuweisung, perfekte Bild-Skalierung usw.

Von Anfang an haben wir selbst Ultimate++ benutzt um unsere Anwendungen für unsere Kunden zu erstellen. Genauso wie wir in den letzten Jahren niemals gezögert haben, unsere gesamte Code-Basis jedes Mal, wenn wir der Meinung waren dass ein wichtiger, oder auch weniger wichtiger Aspekt zu verbessern ist, anzupassen. Dies hat es uns erlaubt die Bibliothek zu entwickeln und zu ihrem derzeitigen Stand zu perfektionieren.

Jetzt, nach etwa 11 Jahren der Entwicklung, ist Ultimate++ eine ausgereifte Plattform, welche eine gewaltige Einsparung unserer Entwicklungskosten ermöglicht. Die meisten Schnittstellen scheinen fertig und optimal zu sein. Es gibt natürlich noch etwas Arbeit, die meiste in der Dokumentation der IDE.

Wenn du unsere Ultimate++ Art zu programmieren interessant findest, hindert dich nichts daran es herunterzuladen. Aber sei vorsichtig: Du solltest immer bereit sein ein paar alte Gewohnheiten und Denkweisen, wie "Das haben wir schon immer so gemacht", über Bord zu werden, sonst könnte es sein, dass du deine Chance, zusammen mit einer gesunden Respektlosigkeit gegenüber einigen achtbaren, gut etablierten Entwicklungs-Tools, eine Menge an Lohn zu erhalten, zunichtemachst.

 

Zuletzt geändert von koldo. Diese Seite gibt es auch in english, čeština, français und русский. Willst du mitmachen?. T++