Geometrische Formen auswählen und vrschieben
-
Die Entscheidung liegt natürlich bei dir und ich will dir meinen Vorschlag auch nicht als die Non plus Ultra Lösung vorschreiben. Das einzige, das du neu programmieren musst, sind die Zeichenfunktionen der einzelnen Shapes, und das sind maximal Dreizeiler. Die komplette Verwaltung der Positionen im 3D Raum musst du auf jeden Fall selbst machen, da hilft dir auch kein TImage oder TShape weiter; für den Export als Vektordaten gilt das gleiche. Ob das jetzt also wirklich so viel Code ist, den du neu schreiben musst weiss ich nicht.
Zum Visitor Pattern:
Das Problem beim downcast eines Objekts einer abstrakten Basisklasse ist, dass man nicht weiß, von welchem Typ es denn jetzt genau ist. Mann kann natürlich so etwas wie eine Typ ID mitführen, oder einen dynamic_cast<> auf jeden möglichen Typ machen, aber das wird recht schnell unübersichtlich und schwer zu pflegen. Das Visitor Pattern stellt zur Compile Time sicher, dass alle möglichen Typen korrekt behandelt werden.
Als erstes braucht man ein Visitor Interface, das für jede mögliche Shape Klasse eine visit() Methode bereitstellt, damit alle Shape Klassen behandelt werden können (class IShapeVisitor).
Dann braucht man ein Interface, das sicherstellt, dass alle Shape Klassen die accept() Methode implementieren, damit der Aufruf für jedes Shape Objekt gelingt (class Shape ).
Als drittes und viertes benötigt man konkrete Shape und IShapeVisitor Klassen, um das Konzept umzusetzen (Circle, Square und PaintBoxShapeDrawer).
Ausgehend von einem abstrakten Shape und einem konkreten Visitor passiert folgendes:// konkreter IShapeVisitor. Als Argument nimmt der Konstruktor einen Zeiger auf // eine TPaintBox entgegen, auf deren Canvas später die Shapes gezeichnet // werden. PaintBoxShapeDrawer theDrawer( PaintBox1 ); // konkrete Shape Klasse Circle theCircle; // abstrakte Shape Klasse, konkreter Typ nur über RTTI feststellbar Shape* S = &C // Besuchermuster anwenden S->accept( theDrawer ); // accept ist pure virtual, daher wird Circle::accept aufgerufen: Circle::accept ( IShapeVisitor& V ) { // this ist der konkrete Shape Datentyp Circle, also wird die Methode // IShapeVisitor::visit( Circle& C ) aufgerufen. Da in diesem Beispiel // der Visitor V vom konkreten Typ PaintBoxShapeDrawer ist wird die // Methode PaintBoxShapeDrawer::visit( Circle& C ) aufgerufen. Damit // ist dem PaintBoxShapeDrawer bekannt, welchen Shape Typ er zeichnen soll // und er kann die für Circle nötigen Zeichenoperatioen mit den Parametern // des aktuellen Circle Objekts ausführen. V.visit( *this ); }
Durch diesen Mechanismus kann zur Compile Time sichergestellt werden, dass alle konkreten Shape Typen von allen konkreten IShapeVisitors behandelt werden. Man braucht weder Typen IDs noch RTTI, die korrekte Typidentifikation erledigt der Compiler für dich und bricht den Compilevorgang ab, wenn etwas fehlen sollte.
Ausserdem lässt sich neue Funktionalität für ALLE Shape Klassen hinzufügen, indem man einen neuen ShapeVisitor baut, der die entsprechende neue Funktion implementiert. In deinem Fall könnte das das Exportieren der Vektordaten sein.Zur zweiten Frage: Ja, du kannst damit beliebig viele verschiedene Shapes auf beliebig vielen Canvas/Paintbox Objekten anzeigen. Wenn du std::vector<Shape*> benutzt, dessen Elemente nach ihrer Z-Position sortiert sind, musst du zwei Fälle betrachten:
-
Beim Zeichnen musst du die hinteren Objekte zuerst behandeln, weil sie vielleicht von Objekten, die weiter vorne liegen, überdeckt werden.
-
Bei der Auswahl eines Shape durch einen Mausklick musst du den Vektor von vorne nach hinten durchsuchen, um das erste Objekt zu finden, auf das der Benutzer geklickt hat.
Eine "Verwaltung" im eigentlichen Sinne macht std::vector nicht, er speichert lediglich Objekte eines Typs. Da der Typ hier ein Zeiger auf eine abstrakte Basisklasse ist kann es sich bei den konkreten Objekten um alles handeln, was von Shape abgeleitet ist. Du musst lediglich sicherstellen, dass der Vektor immer hinsichtlich der Z-Position sortiert ist, ob auf- oder absteigend spielt keine Rolle (lediglich die Iterationsrichtung für 1) und 2) muss entsprechend sein).
Statt normaler raw Pointer sollte man natürlich shared_ptr benutzen, aber das ist das Sahnehäubchen, das man zum Schluss nachrüsten kann. Wichtig ist erst einmal, dass die Funktionalität da ist.
-
-
Hab jetzt mal ein bischen mit Ableitung von shape gespielt und muss feststellen das man wirklich schnell am ende der möglichkeiten ist.
wollte jetzt mal deine classenaufbau testen. Hab mal einfach alles kopiert und paar publics und ein paar ; rein gesetzt, aber der Compiler meckert das er Circle und Square nicht kennt, kann er ja auch noch nicht weils erst ein paar zeilen später definiert wird.
egal wie ich das sortiere immer fehlt irgendetwas ?
die classe IShapeVisitor und Circle stehen sich gegenseitig im weg.
Ich muss das ganze mal ausprobieren um das 100%ig zu verstehen.
-
Jo, du musst auf jeden Fall mit forward declarations arbeiten, sonst hast du zirkuläre Referenzen. Ich wollte dir dein Projekt ja auch nicht wegnehmen, sondern nur eine Lösung vorschlagen und an Beispielen verdeutlichen.
-
Ich hab das ganze jetzt kompiliert bekommen, aber angewendet bekomm ich das noch nicht. ich bin ein bischen verwirrtt was das mit Canvas und Paintbox angeht. Als erstes hast du das ganze mit canvas beschrieben und dann hast du auf paintbox gewechselt was das ganze ziemlich unverständlich für mich macht.
TPaintBox würde ich als "Zeichenfläche" also als Gesamthintergrund ansehen (z.B. wie bei Word die DINA4 Seite) und Canvas ist für mich ein Zeichentool. Also zum Striche malen auf Paintbox!!??
Bei verwendung von CanvasShapeDrawer bekomm ich nen Fehler "Instanz der abstrakten Klasse kann nicht erzeugt werden. Dabei ist CanvasShapeDrawer doch ein Struktur und keine Klasse?
Ich habe so ein Klassenaufbau noch nie gemacht. ich bin bis jetzt mit den BCB gegebenen Komponenten klar gekommen. Ist nicht so das ich gar nichts verstehe, aber ein Konkretes beispiel was ich auch testen kann und ich nachvollziehen kann was da passiert bringt mir am meisten. Wenn der Basisteil einmal läuft dann bekomm ich den rest hoffentlich auch selber hin.
Bin dir echt Dankbar das du dir soviel mühe mit mir gibst und mir hilfst das zu verstehen.
-
Ich hab das ganze jetzt mal so umgesetzt:
#include <vcl.h> #include <windows.h> class Circle; class Square; class IShapeVisitor; class CanvasShapeDrawer; class IShapeVisitor { public: virtual void visit( Circle* C) =0; virtual void visit( Square* S) =0; }; class Shape { public: virtual void accept( IShapeVisitor &V )=0 ; }; class Circle : public Shape { public: void accept( IShapeVisitor &V ) { V.visit( this ); } }; class Square : public Shape { public: void accept( IShapeVisitor &V ) { V.visit( this ); } }; class PaintBoxShapeDrawer : public IShapeVisitor { private: TCanvas* Canvas_; public: PaintBoxShapeDrawer( TPaintBox* Paint ) //: Canvas_( Canvas ) { /* Paint->Canvas->MoveTo(10,10); Paint->Canvas->LineTo(100,100); */ ; } void visit( Circle *C ) { ShowMessage("test"); // Canvas_->MoveTo(10,10); // Canvas_->LineTo(100,100); ; // Zeichne Kreis auf dem Canvas } void visit( Square *S ) { ShowMessage("test2"); ; // Zeichne Rechteck auf dem Canvas } };
Dann hab ich mir eine TPaintBox in mein Formular gezogen und in den header der Form das geschrieben:
public: // Benutzer Deklarationen std::vector<Shape*> Shapes; void __fastcall OnPaintShapes() { PaintBoxShapeDrawer Drawer( PaintBox1 ); for( unsigned int ShapeIndex = 0; ShapeIndex < Shapes.size(); ++ShapeIndex ) { Shape* CurrentShape = Shapes[ShapeIndex]; CurrentShape->accept( Drawer ); } };
OnPaintShapes() ruf ich mit einem Button auf!
Ich versteh noch nicht so ganz die zusammenhänge von dem ganzen. PaintBoxShapeDrawer ruft mir momentan einen Leeren Konstuktor auf,
CurrentShape->accept( Drawer ); scheint ins Leere zu laufen. Ich versteh momentan nicht was in welche Klasse bzw methode muss damit die so funktioniert wie sie funktionieren soll.
-
Ich muss dir recht geben, auf einem TCanvas zu zeichnen macht mehr Sinn, weil´s universeller ist und man sich damit nicht auf TPaintBox festlegt. Ich bleibe mal bei PaintBox, das lässt sich aber auch auf TCanvas übertragen.
Da über das Visitor Pattern kontextlos in bestimmte Methoden gesprungen wird musst du den Kontext z.B. über Member der Visitor Klasse bereitstellen.
Woher soll PaintBoxShapeDrawer::visit denn wissen, auf welchem Canvas gezeichnet werden soll? Daher erwartet der Konstruktor einen Zeiger auf eine PaintBox, die er sich als Member merkt.class PaintBoxShapeDrawer : public IShapeVisitor { private: // Zeiger auf TPaintBox, auf der gezeichnet wird TPaintBox* PaintBox_; public: PaintBoxShapeDrawer( TPaintBox* Paint ) : PaintBox_( Paint ) // Zeiger für späteren Gebrauch merken { } void visit( Circle *C ) { // hier wird auf dem Canvas der PaintBox ein Kreis gezeichnet, // dessen Aussehen von C bestimmt wird. } }
Habe mal wieder wenig Zeit, ich glaube, ich bau dir einfach mal das Gerüst kompilier- und ausführbar zusammen.
PS:
Hast du ein Element in den Vektor eingefügt? Bei einem leeren Vektor passiert natürlich nichts.
-
Nein hatte ich nicht.
Jetzt aberSo ruft der mir zumindestens die richtige visit methode auf.
void __fastcall TForm6::Button4Click(TObject *Sender) { Circle *Kreis = new Circle; Shapes.resize(1); Shapes[0]=Kreis; OnPaintShapes(); }
Ich denk mal das müsste so laufen. Dennoch versteh ich das noch nicht mit TPaintbox und TCanvas. PaintBox ist ein Objekt das ich mir auf die Form Legen kann um darauf herum zu malen, mit TCanvas kann ich zwar malen, aber wodrauf bezieht sich canvas bei den maßen ?
-
JBOpael schrieb:
Ich denk mal das müsste so laufen. Dennoch versteh ich das noch nicht mit TPaintbox und TCanvas. PaintBox ist ein Objekt das ich mir auf die Form Legen kann um darauf herum zu malen, mit TCanvas kann ich zwar malen, aber wodrauf bezieht sich canvas bei den maßen ?
Ich verstehe die Frage nicht. Wozu der Canvas da ist sollte doch inzwischen geklärt sein, und auch, wie und warum der den einzelnen visit Methoden bekanntgemacht wird.
-
TPaintBox beinhaltet ja Canvas demnach kann ich meine Formen auf die Paintbox zeichnen lassen. Aber TCanvas braucht ein übergeornetes Objekt (TForm, TPanel, TImage etc) oder irgendetwas wodrauf es malen kann.
Oder meintest du mit
DocShoe schrieb:
Ich muss dir recht geben, auf einem TCanvas zu zeichnen macht mehr Sinn, weil´s universeller ist ...
das ich im nachhinein Canvas noch einem Objekt zuordnen muss bzw. PaintBox->Canvas übergeben muss?
Ich denke mit der Funktionsweise der Klasse komme ich jetzt hoffentlich klar.
-
Ich danke dir für deine Hilfe!
Ich hab noch ein paar bücher gewälzt und das Internet durchsucht und ich denk mal das jetzt verstanden habe wie das ganze funktioniert.