Zeichnen mit MFC Erweiterung 4


Einleitung

Um diese Anleitung nicht zu lang werden zu lassen, schreibe ich immer nur die Dinge auf, die verändert werden müssen. Bei Codeabschnitten werden die Teile, die von mir verändert wurden fett geschrieben.

Beispiel

Aufgabe

Das Programm Zeichner soll so erweitert werden, dass nun zusätzlich zu den Strichen auch Kreise und Rechtecke gezeichnet werde können.

Lösung in Prosa

Zusätzliche Einführung

Vorher war es so, dass ich genau wusste, dass in der Objektliste (CObList) nur Linien (also Variablen(Instanzen) der Klasse Linie) vorhanden waren. Wenn ich also etwas mit GetNext herausholte, konnte ich sicher sein, dass es nichts anderes ist und konnte es desshalb auch ohne Probleme auf den Typ Linie konvertieren. Wenn ich es dem Benutzer aber ermöglichen will, auch andere Objekte zu zeichnen, kann ich mir dessen nicht mehr sicher sein. Es entsteht also das Problem, dass ich nicht weiß wenn ich etwas aus der Liste herausnehme, dass ich nicht weiß was es ist.
Für dieses Problem hat C++ eine Lösung: Polymorphie
Das heißt eigentlich nichts anderes, als dass ich eine Basisklasse anfertige (ZeichenObjekt), von der alle zu zeichnenden Objekte (Linie, Ellipse, Rechteck) abgeleitet sind. Nun kann ich wenigstens schon mal sicher sein, dass sich in der CObList nur Instanzen der Klasse ZeichenObjekt befinden, selbst wenn sie nur von dieser abgeleitet sind.

Man kann nun in der Basisklasse eine Methode(Funktion) erstellen, die anfänglich einfach nichts tut :-)
Diese kann aber in den abgeleiteten Funktionen überschrieben werden, so dass wenn ich diese Funktion von einer Klasse aufrufe, von der ich nur weiß dass sie ein ZeichenObjekt ist, der Compiler automatisch die Methode der Klasse aufruft (Linie, Ellipse, Rechteck) welchen Typs die Klasse wirklich ist.
Das kann genutzt werden, um es der jeweiligen Klasse zu überlassen sich selber zu zeichnen.
In der Basisklasse gibts dann einfach eine Methode:
-> virtual void zeichne(CDC* pDC){} virtual bedeutet, dass bei Aufruf der Methode der Basisklasse vom Compiler geprüft wird, ob es sich bei dem Objekt, auf dem diese Methode aufgerufen wird wirklich um ein Objekt der Basisklasse handelt, oder ob es sich um eine abgeleitete Klasse handelt. Ist es eine abgeleitete Klasse und wird in dieser Klasse diese Methode überschrieben, dann wird nicht die Methode der Basisklasse sondern die der abgeleiteten Klasse aufgerufen. Und in den Klassen, die von ihr ableiten wird diese Methode einfach überschrieben, das heißt es gibt eine Methode mit gleichem Namen und gleichen Parametern:
-> void zeichne(CDC* pDC){}

Nun gehts aber ans programmieren

  1. Zuerst wird wie gesagt eine neue Klasse mit dem Namen ZeichenObjekt gebraucht, von der ich dann alle anderen (Linie, Rechteck, Ellipse) ableiten kann.
  2. Nun werden noch 2 weitere Klassen für Rechteck und Ellipse gebraucht. Diese müssen von der Klasse ZeichenObjekt abgeleitet werden, nicht von CObject.
    Leider kann man die Klasse für die Ellipse nicht Ellipse nennen, weil es diesen Namen schon gibt. Ich nehme desshalb den Namen myEllipse.
  3. Die Klasse Linie ist aber noch immer von CObject abgeleitet. Sie soll aber genauso wie die Klassen myEllipse und Rechteck von ZeichenObjekt abgeleitet werden. Änderungen in der Datei Linie.h
    // Linie.h: Schnittstelle für die Klasse Linie.
    //
    //////////////////////////////////////////////////////////////////////
    
    #if !defined(AFX_LINIE_H__1D1B215B_847B_41F4_B837_877ABE780E17__INCLUDED_)
    #define AFX_LINIE_H__1D1B215B_847B_41F4_B837_877ABE780E17__INCLUDED_
    
    #if _MSC_VER > 1000
    #pragma once
    #endif // _MSC_VER > 1000
    
    #include "ZeichenObjekt.h"
    
    class Linie : public ZeichenObjekt
    {
    public:
    	void Serialize(CArchive &ar);
    	DECLARE_SERIAL(Linie)
    	int dicke;
    	COLORREF farbe;
    	CPoint endpunkt;
    	CPoint startpunkt;
    	Linie();
    	virtual ~Linie();
    
    };
    
    #endif // !defined(AFX_LINIE_H__1D1B215B_847B_41F4_B837_877ABE780E17__INCLUDED_)
    
  4. Die Klassen Rechteck und myEllipse benötigen nun noch Variable, um sich selber beschreiben zu können.
    Gebrauchte Variable:
    -> Rechteck : int x1,y1,x2,y2 oder ein CRect (hab ich gewählt, da es weniger zu schreiben ist einer statt vier Variablen jeweils einen Wert zuzuweisen)
    -> myEllipse : int x1,y1,x2,y2 oder ein CRect (hab ich gewählt)
  5. Jetzt brauchen wir in der Basisklasse noch die Methode zeichne.
    Da die Methode nur da ist, um von den abgeleiteten Klassen überschrieben zu werden, brauchen wir keinen Code für sie zu implementieren.
  6. Die Methode (muss) nun in allen von der Klasse abgeleiteten Klassen überschrieben werden:
    Sie braucht dort nicht virtual sein.
  7. In diese Methoden muss nun der Code zum Zeichnen verlagert werden.
    Beispiel für die Linie
    void Linie::zeichne(CDC *pDC)
    {
    	CPen *oldpen,*pen;
    
    	pen = new CPen(PS_SOLID,dicke,farbe);
    	oldpen = pDC->SelectObject(pen);
    
    	pDC->MoveTo(startpunkt);
    	pDC->LineTo(endpunkt);
    
    	pDC->SelectObject(oldpen);
    	delete pen;
    }
    
  8. Um später die neuen Objekte auch serialisieren zu können, muss in den 3 Klassen, die neu hinzugekommen sind, noch eine Implementierung der Methode Serialize hinzugefügt werden.
    Beispiel für die Klasse myEllipse:
    void myEllipse::Serialize(CArchive &ar)
    {
    	if (ar.IsStoring())
    	{
    		ar << rect;
    	}else
    	{
    		ar >> rect;
    	}
    }
    

    Es müssen auch in allen der Klassen ZeichenObjekt, Rechteck und myEllipse die Makros DECLARE_SERIAL und IMPLEMENT_SERIAL eingefügt werden. In der Klasse Linie muss IMPLEMENT_SERIAL abgeändert werden, weil sie nun nicht mehr von CObject sondern von ZeichenObjekt abgeleitet ist.
  9. Nun möchte sich der spätere Benutzer sicher auch aussuchen wollen, was er jetzt zeichnen möchte. Dazu erstelle ich einfach 3 Buttons in der Symbolleiste. Jeweils einen für Linie, Rechteck, und Ellipse.
  10. Jetzt wird noch eine Variable gebraucht, in der man sich merkt, was der Benutzer gerade zeichnen will. Am Besten ist es sie in der Klasse CZeichnerView zu erstellen.
  11. Es fehlen noch Handler, die auf die Buttons reagieren.
    Definitionen in CZeichnerView.h
    #define Z_LINIE		1
    #define Z_RECHTECK	2
    #define Z_ELLIPSE	3
    
    Beispiel für Handler:
    void CZeichnerView::OnLinie() 
    {
    	// Z_LINIE defininiert in CZeichnerView.h
    	aktuellesObjekt = Z_LINIE;
    }
    
    void CZeichnerView::OnRechteck() 
    {
    	aktuellesObjekt = Z_RECHTECK;
    }
    
    void CZeichnerView::OnEllipse() 
    {
    	aktuellesObjekt = Z_ELLIPSE;
    }
    
  12. Nun muss nur noch OnDraw geändert werden.
    Es müssen noch einige Dateien in CZeichnerView.cpp includiert werden:
    #include "myEllipse.h"
    #include "Rechteck.h"
    #include "ZeichenObjekt.h"
    
    Änderung des Konstruktors für CZeichnerView:
    CZeichnerView::CZeichnerView()
    {
    	// ZU ERLEDIGEN: Hier Code zur Konstruktion einfügen,
    	startpunkt = CPoint(0,0);
    	endpunkt = CPoint(0,0);
    
    	strichdicke = 1;
    	// auf Schwarz setzen
    	strichfarbe = RGB(0,0,0);
    	aktuellesObjekt = Z_LINIE;
    }
    
    Neue OnDraw:
    void CZeichnerView::OnDraw(CDC* pDC)
    {
    	CZeichnerDoc* pDoc = GetDocument();
    	ASSERT_VALID(pDoc);
    
    	POSITION pos;
    	ZeichenObjekt *z;
    	
    	pos = pDoc->ObjektListe.GetHeadPosition();
    
    	while (pos != NULL)
    	{
    		// ZeichenObjekt holen und in Zeiger auf ZeichenObjekt konvertieren
    		z = (ZeichenObjekt*) pDoc->ObjektListe.GetNext(pos);
    		
    		// ZeichenObjekt sich selber zeichnen lassen
    		z->zeichne(pDC);
    	}
    	// ZU ERLEDIGEN: Hier Code zum Zeichnen der ursprünglichen Daten hinzufügen
    }
    
  13. Fertig