Geometrische Formen auswählen und vrschieben



  • Ist am Wochenende auch leider nix geworden, aber jetzt:

    Du solltest dich erst einmal von TImage und TShape verabschieden, dabei handelt es sich um VCL Komponenten, die auf anderen Komponente platziert werden können und Bilder oder einfache geometrische Formen darstellen können. Da du aber eine Art Malprogramm schreiben willst schränkt dich das in deinen Möglichkeiten/Erweiterbarkeit mit ziemlicher Sicherheit ein.

    Als erstes brauchst du eine abstrakte Basisklasse, von der all deine konkreten Formen ableiten. Für Kreis und Rechteck kann das ganze etwa so aussehen:

    Shape
           |
       +---+---+
       |       |
     Circle   Square
    

    Als nächstes musst du dir überlegen, welche Attribute welche Klasse hat (Farbe, Stiftdicke, etc), welche Gemeinsamkeiten bestehen, und ob eine feinere Hierachie notwendig ist.
    Sinnvolle Operationen für die Klasse Shape sind sicherlich

    • Bestimmung der Z-Position:
      Wenn du mehrere Formen zeichnen willst, die sich zumindest teilweise überlappen
      muss eine eindeutige Reihenfolge existieren, in der die Formen gezeichnet werden.
    • Bestimmung des umgebenden Rechtecks:
      Falls der Benutzer per Maus die Grösse einer Form verändern können soll brauchst du irgendeine Markierung für die aktuell ausgewählte Form. Das umgebende Rechteck kann man dann benutzen, um einen Rahmen um die Form zu zeichnen und entsprechende Markierungen an jeder Ecke und der Mitte jeder Seitenlinie einzuzeichnen, mit der man die Grösse der Form verändern kann.

    Als nächstes solltest du dir überlegen, wie die Darstellung deiner Formen auf dem Bildschirm durchgeführt werden soll. Dazu bieten sich zwei Techniken an:

    • Die Klasse Shape definiert eine rein virtuelle draw() Funktion, die ein Canvas erwartet. Alle konkreten Shape Klasse müssen diese draw Methode dann implementieren und sind selbst für ihre Darstellung verantwortlich.
    // Vektor mit Zeigern auf Shape Klassen, nach Z-Position sortiert
    // Indexzugriff statt Iteratoren, um das Beispiel möglichst verständlich zu 
    machen
    // TargetCanvas ist das Canvas, auf dem alle Shapes dargestellt werden
    std::vector<Shape*> Shapes;
    for( unsigned int ShapeIndex = 0; ShapeIndex < Shapes.size(); ++ShapeIndex )
    {
       Shape* CurrentShape = Shapes[ShapeIndex];
       CurrentShape->draw( TargetCanvas );
    }
    
    • Du verwendest das Besuchermuster, um den konkreten Shape Typ auf dem Zielcanvas anzuzeigen:
    class IShapeVisitor
    {
       virtual void visit( Circle* ) = 0;
       virtual void visit( Square* ) = 0;
    };
    
    class Shape
    {
       ...
       virtual void accept( IShapeVisitor& V ) = 0;
       ...
    };
    
    class Cirlce : public Shape
    {
       ...
       void accept( IShapeVisitor& V )
       {
          V.visit( *this );
       }
       ...
    };
    
    class Square : public Shape
    {
       ...
       void accept( IShapeVisitor& V )
       {
          V.visit( *this );
       }
    };
    
    struct CanvasShapeDrawer : public IShapeVisitor
    {
       TCanvas* Canvas_;
    
       CanvasShapeDrawer( TCanvas* Canvas ) : Canvas_( Canvas )
       {
       }
    
       void draw( Circle& C )
       {
          // Zeichne Kreis auf dem Canvas
       }
    
       void draw( Square& S )
       {
          // Zeichne Rechteck auf dem Canvas
       }
    };
    
    std::vector<Shape*> Shapes;
    CanvasShapeDrawer Drawer( TargetCanvas );
    
    for( unsigned int ShapeIndex = 0; ShapeIndex < Shapes.size(); ++ShapeIndex )
    {
       Shape* CurrentShape = Shapes[ShapeIndex];
       CurrentShape->accept( Drawer );
    }
    

    Welche Lösung jetzt besser ist bleibt Geschmackssache, ich tendiere aber zum Visitor Pattern, weil´s komplizierte ist. Nein, im Ernst, das Visitor Pattern erlaubt über seine offene Schnittstelle Erweiterungen, ohne dass man die Shape Klassen anfassen muss, im Gegenzug musst du aber alle IShapeVisitor Klassen anfassen, wenn eine neue Form hinzukommt.
    Wenn später mehr Shape Klassen hinzukommen und nur wenige Operationen solltest du die erste Lösung nehmen, wenn mehr Operationen hinzukommen und nur wenig neue Shape Klassen ist das Visitor Pattern die bessere Wahl.

    Der Rest ist eigentlich nur noch Fleissarbeit 😃



  • Bin mir jetzt nicht so ganz sicher das ob ich das jetzt richtig verstanden habe.

    Ich hab das jetzt so verstanden das ich das Rad neu erfinden soll und quasi TShape neu schreiben soll. Also nichts von irgend einer Komponente Ableiten??

    Ich glaub durch den klaasenaufbau blick so ein bischen durch, aber damit weiß ich jetzt nicht so ganz wo ich das einordnen soll bzw. wo das hin muss:

    std::vector<Shape*> Shapes;
    CanvasShapeDrawer Drawer( TargetCanvas );
    
    for( unsigned int ShapeIndex = 0; ShapeIndex < Shapes.size(); ++ShapeIndex )
    {
       Shape* CurrentShape = Shapes[ShapeIndex];
       CurrentShape->accept( Drawer );
    }
    

    Was mir noch fehlt bzw was ich da nicht so draus erkennen kann was jetzt genau der hintergrund (parent) von Shape ist.



  • Ja, nicht von TShape ableiten, weil du eigentlich keine Komponente haben willst, sondern ein Zeichenprogramm, das irgendwas auf den Bildschirm malt. Wie willst du denn zB. freies Zeichnen mit einem TShape handhaben?

    Die beiden Codeschnipsel, die ich gepostet habe, gehören (beinahe fast alle) in die OnPaint Methode des Canvas, auf dem du die Formen anzeigen willst. Du brauchst auf jeden Fall irgendeine Instanz, wo du deine Shape Objekte verwaltest. Sagen wir der Einfachheit halber als Member deines Formulars, in dem sich auch die Zeichfläche befindet. Ich habe TCanvas durch TPaintBox ersetzt, da sich TPaintBox besser eignet als TCanvas, um darauf herumzumalen.

    class TForm1 : public TForm
    {
    __published:
       // besser als TCanvas: TPaintBox
       TPaintBox* PaintBox1;
       void __fastcall OnPaintShapes()
       {
          PaintBoxShapeDrawer Drawer( PaintBox1 );
    
          for( unsigned int ShapeIndex = 0; ShapeIndex < Shapes.size(); ++ShapeIndex )
          {
             Shape* CurrentShape = Shapes[ShapeIndex];
             CurrentShape->accept( Drawer );
          }       
       }
    
       ...
    };
    

    Hast du Verständnisprobleme beim Visitor Pattern?



  • Das soll ja kein malprogramm werden. Freies zeichnen werde ich nicht brauchen. Was mir ein bisschen bauchschmerzen bereitet an deinem vorschalg ist das ich sehr viel neu schreiben mus was eigenlich schon exestiert. Eigenlich wäre mir das egal aber der zeitaktor spiel da ein kleine rolle.

    An der klassenstruktur verstehe ich noch nicht so ganz was nachher der "Besucher" und was das "Element" ist. So vom Logischen würde ich ja sagen das der Kreis oder Rechteck das Element ist, aber was ist dann der Besucher?

    Ist das so gedacht das ich nachher ein Objekt habe das alle Elemente (kreis, Rechteck) verwaltet?
    Mit anderen Worten ich setzte auf die PaintBox ein Objekt und aus diesem Objekt kann ich mir beliebig viele rechtecke und kreise raus ziehen??



  • 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:

    1. Beim Zeichnen musst du die hinteren Objekte zuerst behandeln, weil sie vielleicht von Objekten, die weiter vorne liegen, überdeckt werden.

    2. 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 aber 🙂

    So 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.


Anmelden zum Antworten