3 Geometrische Objekte

3.1 Entwurf einer Klassenhierarchie
3.2 Implementierung
3.3 Die polymorphe Liste
3.4 Kann die Maus "sehen"?
3.5 Objekte interaktiv manipulieren
3.6 Die Krönung: Abhängige Objekte
3.7 Warnende Worte für Weiterwollende

Zum vorigen Kapitel Zum Inhaltsverzeichnis Zum nächsten Kapitel


In diesem Kapitel wird ein Beispiel für eine Klassenhierarchie entwickelt, wie sie in sogenannten "Dynamischen Geometrie-Programmen" verwendet wird. In solchen Programmen erzeugt der Benutzer "Objekte", die er dann mit der Maus nachträglich noch manipulieren kann. Dabei ist das Wort "Objekte" durchaus im doppelten Sinn gemeint:



3.1 Entwurf einer Klassenhierarchie

Zunächst müssen wir klären, welche Eigenschaften und Fähigkeiten unsere Objekte haben sollen. Punkte, Kreise, Geraden, Strecken - egal welche konkrete geometrische Objektsorte wir betrachten, zunächst muss jedes Objekt über die Fähigkeit verfügen, sich selbst auf dem Bildschirm zeichnen zu können. Dazu muss festgelegt werden, wo und wie das Objekt gezeichnet werden soll. Jedes geometrische Objekt hat also einen Ort und eine Farbe. Zumindest "am Ende seines Lebens" wird es nötig sein, das Objekt auch wieder vom Bildschirm zu löschen. Außerdem brauchen wir sicher auch jeweils einen Konstruktor und einen Destruktor für jede Objektsorte. Damit sind schon einmal diejenigen Eigenschaften und Methoden, die sicher für alle Klassen geometrischer Objekte zur Verfügung gestellt werden müssen. Wir fassen diese in einer Klasse TGeoObj zusammen:

Klasse TGeoObj

Diese Darstellung unserer Klasse lehnt sich an die Konventionen der "Universal Modelling Language" (kurz: UML) an: unter der Titelzeile mit dem Klassennamen werden zunächst in einem Kasten die Eigenschaften aufgelistet und in einem zweiten Kasten darunter die Fähigkeiten. Technisch gesehen werden Eigenschaften durch Variablen repräsentiert, Fähigkeiten hingegen durch Prozeduren und Funktionen. Vor den Einträgen in den beiden Kästen stehen noch einige "Modifizierer". Die erste Sorte spezifiziert die Sichtbarkeit der einzelnen Eigenschaften und Fähigkeiten:

Zeichen Bedeutung Beschreibung
# protected nur innerhalb der Klasse selbst oder innerhalb davon abgeleiteter Klassen sichtbar
+ public "von außen" sichtbar, gehört also zur öffentlichen Schnittstelle

Eine zweite Sorte von Modifizierern spezifiziert die Fähigkeiten unseres Objekts. Man unterscheidet dabei zwischen zwei verschiedenen Arten von "Fähigkeiten":

Zeichen Bedeutung Beschreibung
! Auftrag eine Prozedur, mit der ein Objekt einen Auftrag erledigt
? Anfrage eine Funktion, deren Ergebnis die Antwort auf eine Anfrage liefert

In der obigen Definition unserer Klasse TGeoObj kommen bisher gar keine Anfragen vor, aber das wird nicht lange so bleiben. Außerdem fällt auf, dass alle Eigenschaften protected sind und alle Fähigkeiten public, aber das ist Zufall: in dem TBruch-Beispiel aus dem vorigen Kapitel gibt es z.B. die nur intern verfügbare Funktion ggT, die damit also eine Anfrage mit dem Sichtbarkeitsbereich protected darstellt.

Die Klasse TGeoObj modelliert nun das, was wir bisher als Gemeinsamkeiten aller möglichen geometrischen Objekte erkannt haben. Allerdings können wir manche der Methoden dieser Klasse noch gar nicht implementieren: was sollte denn in DrawIt gezeichnet werden? Das können wir erst entscheiden, wenn wir nun zu einer konkreten Sorte von geometrischen Objekten übergehen. Man nennt eine solche "Platzhalter-Methode" eine abstrakte Methode: sie muss in jeder abgeleiteten Klasse implementiert (also "konkretisiert") werden, bevor von dieser neuen Klasse Objekt-Instanzen hergestellt werden können. Und natürlich ist auch HideIt eine abstrakte Methode. Solche Methoden werden im Klassendiagramm in kursiver Schrift dargestellt. Beachten Sie aber, dass z.B. der Konstruktor Create durchaus schon implementiert werden kann: er ist nicht abstrakt, ebensowenig wie der Destruktor Destroy!

Wenn wir nun zur Modellierung konkreter geometrischer Objektklassen übergehen, dann können außerdem entsprechende spezifische Eigenschaften und Fähigkeiten zu den in TGeoObj schon vorhandenen hinzukommen. Betrachten wir zum Beispiel die Klasse TGCircle aller Kreise: jeder Kreis ist durch einen Mittelpunkt und einen Radius festgelegt. Wenn wir TGCircle von TGeoObj ableiten, dann können wir für die Speicherung der Mittelpunkt-Koordinaten die schon vorhandenen Felder x und y benutzen. Für den Radius müssen wir jedoch eine neue Eigenschaft einrichten. Und außerdem müssen die abstrakten Methoden des Vorgängertyps TGeoObj implementiert werden. Wir erhalten damit also zum Beispiel das folgende Klassen-Diagramm:

Klasse TGCircle


Beachten Sie, dass dieses Diagramm nur die Änderungen gegenüber der Vorgänger-Klasse enthält! Um zu verstehen, wie TGCircle-Objekte gebaut sind, muss man das Diagramm von TGeoObj hinzuziehen. Die Tatache, dass TGCircle von TGeoObj abgeleitet ist, wird durch einen Pfeil dargestellt:

Klassen TGeoObj und TGCircle


Der Pfeil wird dabei "vom Kind zum Elter" gezeichnet; er kann damit gelesen werden als "erbt von": TGCircle erbt seine Strukturen von TGeoObj. In UML wird die Bedeutung dieses Pfeils sogar kurz mit "ist ein" angegeben, was allerdings nicht im Sinne einer Identität gemeint ist, sondern als Inklusion, nämlich als "ist eine spezielle Art von": ein TGCircle-Objekt ist also eine spezielle Art von TGeoObj-Objekt.

Damit unsere Klassendiagramme uns später noch einen größeren Nutzen bringen können, empfiehlt es sich, sie gleich mit einem passenden Werkzeug herzustellen. Das Programm "UMLed" ist ein komfortabler Editor für solche Klassendiagramme. In einem "Objektinspektor" können Sie die Eigenschaften und Methoden der aktuellen Klasse editieren bzw. ergänzen, wobei im Hauptfenster ein entsprechendes Diagramm dargestellt wird. Diese Grafik lässt sich bequem als BMP-Datei exportieren. So sind auch die obigen Bilder entstanden.



Aufgabe:


  1. Weitere Objekt-Klassen

    Falls Sie das Programm "UMLed" noch nicht auf Ihrem Rechner verfügbar haben, installieren Sie es. Stellen Sie dann zunächst die obigen Klassendiagramme her.

    Ergänzen Sie das Klassen-Diagramm um die Klasse TGLine aller Strecken und die Klasse TGQuad aller Quadrate, deren Seiten parallel zu den Fensterkanten laufen. Überlegen Sie in jedem Fall, welche Eigenschaften neu hinzukommen und welche Fähigkeiten neu implementiert werden müssen.
    [Lösungsvorschlag]



Einen speziellen Fall der Vererbung stellt die Konstruktion der Klasse aller Punkte dar. Die Klasse TGeoObj enthält nämlich schon alle Eigenschaften, die wir benötigen, um einen geometrischen Punkt vollständig zu beschreiben. Damit braucht eine von TGeoObj abgeleitete Klasse TGPoint keine zusätzlichen eigenen Eigenschaften; sie muss lediglich die abstrakten Methoden von TGeoObj implementieren. Wenn wir nun alle bisher modellierten Klassen zusammenfassen, erhalten wir die folgende Hierarchie:

TGeoObj, TGCircle, TGQuadrat, TGLine und TGPoint




3.2 Implementierung

Was haben wir nun von diesen Klassendiagrammen? Wir haben damit sozusagen den Bauplan für die Klassen, die unsere geometrischen Objekte darstellen sollen. Nun müssen wir noch den Schritt zum lauffähigen Programm zurücklegen. Gar so weit ist dieses Ziel aber nun nicht mehr entfernt, zumindest wenn Sie Ihre Diagramme mit UMLed erstellt haben. Dieses Programm kann nämlich entprechende Delphi-Objekt-Deklarationen erzeugen: zu jedem Klassendiagramm erzeugt es eine Unit mit einem vollständigen Interface! Im Implementation-Teil sind schon die Köpfe aller noch zu erstellenden Methoden aufgelistet. Wir müssen also nur noch die Prozedur- und Funktionsrümpfe einfügen.


Aufgabe:


  1. UMLed als "Frame-Worker"...

    Legen Sie ein neues Verzeichnis für unser GeoObj-Projekt an, z.B. "GeoObj_1". Kopieren Sie in dieses Verzeichnis die UMLed-Datei mit den oben entwickelten Klassendiagrammen. Öffnen Sie diese Datei in UMLed. Fokussieren Sie zunächst die "Mutterklasse" TGeoObj, indem Sie sie mit der Maus anklicken. Wählen Sie dann aus dem Menü den Punkt "Klassen | Code anzeigen / speichern". Das daraufhin erscheinende Fenster zeigt den Code-Rahmen für eine entsprechende Delphi-Unit, in der diese Klasse implemntiert wird. Mit dem Button "Als Unit speichern" schreiben Sie nun eine entsprechende Unit-Datei in Ihr Projekt-Verzeichnis. Dabei können Sie den von UMLed vorgegebenen Namen der Unit übernehmen: er besteht aus dem Klassennamen mit einem vorangestellten "m" für "Modul". Verfahren Sie entsprechend mit allen von TGeoObj abgeleiteten Klassen.
    [Lösungsvorschlag]


  2. ...und Sie als "Content-Worker"!

    Starten Sie Delphi und erzeugen Sie ein neues Projekt. Speichern Sie es im selben GeoObj-Projekt-Verzeichnis ab, in dem auch die UMLed-Datei mit dem Entwurf der Klassenhierarchie steht. Fügen Sie von UMLed erzeugten Units zu Ihrem Projekt hinzu (Menüpunkt "Projekt | Dem Projekt hinzufügen"). Erzeugen Sie im Formular ein Panel für die Aufnahme von Knöpfen; füllen Sie den restlichen Fensterplatz mit einer Paintbox aus, auf der unsere Objekte erscheinen sollen.

    Implementieren Sie nun die Klassen der geometrischen Objekte. Stellen Sie dazu für jede Klasse eine passende Variable in Ihrem Formular bereit, und nutzen Sie das Programm während der Entwicklung als Testumgebung.

    Sie werden dabei schnell feststellen, dass die von UMLed erzeugten Unit-Dateien gelegentlich noch etwas Feinarbeit benötigen. So müssen Sie zum Beispiel in allen uses-Listen noch die Standard-Unit graphics hinzufügen, damit Sie überhaupt eine Chance haben, dass der Compiler die Dateien übersetzen kann. Lesen Sie daher die Fehlermeldungen des Compilers stets aufmerksam und genau durch!
    [Lösungsvorschlag]




3.3 Die polymorphe Liste

Wenn Sie das Programm aus dem letzten Abschnitt kritisch anschauen, wird schnell klar, dass es höchstens ein Testprogramm ist, das bei der Entwicklung der Klassenhierarchie helfen kann. Es ist keinesfalls ein "ordentliche Anwendung". Einige der möglichen Kritikpunkte sind z.B.:
Interessanterweise lassen sich diese drei Probleme alle gemeinsam lösen. Wie wir dabei vorzugehen haben, ahnen Sie vielleicht schon angesichts des zuletzt angeführten Kritikpunktes: eine ähnliche Situation haben wir schon früher einmal durch Einführung einer "Paint-Methode" für das Formular gelöst. Wir müssen also eine Prozedur implementieren, die immer wenn es nötig wird, die gesamte Zeichnung neu zeichnet. Das Problem dabei ist, dass wir nicht voraussehen können, wieviele und welche Objekte der Benutzer bis zum Zeitpunkt des Aufrufs der Methode erzeugt hat - und beim nächsten Aufruf können die Verhältnisse schon wieder ganz anders sein! Mit Sicherheit reicht hier die bisher angewandte Methode nicht, jedes Objekt in einer zur Designzeit definierten Variablen zu speichern.

Die Lösung bringt eine Datenstruktur namens "Liste". Delphi stellt dazu eine vordefinierte Klasse TList zur Verfügung, die zur Speicherung einer zur Programmierzeit noch nicht festgelegten Anzahl beliebiger Objekte dienen kann. Um ein weiteres Objekt am Ende der Liste hinzuzufügen, verfügt die Liste über die Methode Add(item : TObject). Die Anzahl der aktuell in der Liste enthaltenen Objekte ist in der Variablen Count gespeichert, der Zugriff auf die einzelnen Objekte in der Liste ist über die Eigenschaft items[index: Integer] möglich, die wie ein Array zu benutzen ist; gültige Indizes liegen im Intervall [0, (Count-1)]. Weitere Methoden von TList finden Sie in der Online-Hilfe von Delphi.

Der Clou an Listen vom Typ TList ist, dass sie zur gleichzeitigen Speicherung verschiedenster Objektsorten dienen können, solange diese nur alle von TObject abgeleitet sind. Dies trifft für unsere Objekte ja sicher zu. Ein Nachteil ist aber, dass die Liste Ihnen keine Auskunft darüber geben kann, welchen genauen Typ ein bestimmmtes ihrer Elemente hat: für die Liste ist es eben immer ein TObject, mehr muss (und will!) sie nicht wissen. Wenn Sie ein TGeoObj in der Liste abgelegt haben und später wieder auf dieses Objekt zugreifen wollen, müssen Sie das gelieferte TObject eigenhändig in ein TGeoObj "umwandeln": wenn die Liste Ihnen als items[i] ein TObject liefert, dann stellt TGeoObj(items[i]) das ursprünglich in die Liste geschobene "GeoObjekt" dar - und erlaubt auch den Zugriff auf die TGeoObj-typischen Methoden und Eigenschaften, also z.B. TGeoObj(items[i]).DrawIt. Da DrawIt als virtuelle Methode deklariert ist, wird durch diesen Befehl stets die zum aktuellen Objekttyp passende Implementierungsvariante aufgerufen: falls z.B. Items[i] einen Kreis enthält, wird automatisch TGCircle.DrawIt aufgerufen, bei einer Strecke hingegen TGLine.DrawIt. Wir haben es hier also mit einer polymorphen Liste zu tun.

Wie setzen wir nun eine solche polymorphe Liste konkret ein? Zunächst brauchen wir in unserem Programm eine Variable vom Typ TList. Dazu können wir im private-Abschnitt unserer Formulardefinition z.B. die Deklaration
     GeoListe: TList;
einfügen. Da diese Liste ein Objekt ist, muss sie beim Programmstart erzeugt und zum Programmende wieder abgebaut werden. Passende Orte dafür sind die OnCreate- und die OnClose-Methode des Formulars. Erzeugt wird die Liste einfach mit
     GeoListe := TList.Create;
Wird zur Laufzeit ein TGeoObj-Objekt neu erzeugt, dann wird es einfach mit der Methode Add() an die Liste angefügt. Z.B. kann ein neuer roter Punkt an der Stelle (100;50) mit
     GeoListe.Add(TGPoint.Create(100, 50, clRed));
erzeugt und in der Liste abgelegt werden: der Konstruktor-Aufruf liefert die Objektinstanz zurück, und diese wird in die Liste eingetragen. Damit können zunächst einmal beliebig viele geometrische Objekte der verschiedenen Sorten in beliebiger Reihenfolge erzeugt werden.

Am Programmende muss die Liste wieder "entsorgt" werden. Dies geschieht am einfachsten durch einen Aufruf ihrer Free-Methode. Zuvor aber müssen alle in der Liste gespeicherten Objekte freigegeben werden:
     For i := GeoListe.Count - 1 downto 0 do
       TGeoObj(GeoListe.Items[i]).Free;
     GeoListe.Free;  
Dass die Schleife dabei abwärts durchlaufen wird, hat nur kosmetische Gründe: es soll Sie lediglich mal wieder daran erinnern, dass das auch so geht ;-)
Um auch gleich das Problem des flüchtigen Fensterinhaltes zu lösen, erzeugen wir noch eine OnPaint-Methode für das Formular, in der wir für jedes in der GeoListe gespeicherte Objekt seine Methode DrawIt aufrufen:
     For i := 0 to GeoListe.Count - 1 do
       TGeoObj(GeoListe.Items[i]).DrawIt(target); 
wobei Sie für target das TCanvas-Objekt einsetzen müssen, das Ihre Zeichenfläche repräsentiert: wenn Sie Ihre Zeichnung in einer Paintbox namens "Zeichenflaeche" unterbringen, dann ist hier also Zeichenflaeche.Canvas einzusetzen.




Aufgabe:


  1. Viele Objekte stabil darstellen

    Legen Sie eine Kopie unseres GeoObj-Projekts in einem neuen Verzeichnis "GeoObj_2" an. Stellen Sie auf dem Panel für jede Objektsorte passende Eingabe-Felder für die Objekt-Daten sowie einen ".... erzeugen"-Knopf zur Verfügung, also z.B. für einen Punkt zwei IntEdit-Felder für die Punktkoordinaten sowie einen Knopf "Punkt erzeugen". In der Klick-Prozedur des Knopfes soll dann ein entsprechendes Objekt mit diesen Daten erzeugt, auf der Zeichenfläche ausgegeben und in der Liste abgespeichert werden.
    Überzeugen Sie sich davon, dass in diesem Zustand die Darstellung der Objekte instabil ist (z.B. bei Überlappung durch andere Fenster). Implementieren Sie dann die oben beschriebene OnPaint-Methode für das Formular (bzw. die PaintBox).
    [Lösungsvorschlag]






3.4 Kann die Maus sehen?

In einem dynamischen Geometrieprogramm können wir mit der Maus auf ein "gemeintes" Objekt zeigen, und das Programm "weiß" dann, welches der verfügbaren Objekte wir meinen. Woher "weiß" das Programm das? Kann die Maus das Objekt "sehen"? Das erscheint doch recht unwahrscheinlich.

Tatsächlich ist es eher so, dass das Objekt selbst "bemerkt", dass es "gemeint" ist, indem es wahrnimmt, wie weit die Maus von ihm entfernt ist. Um eine solche Funktionalität zu implementieren, müssen wir unsere Objekte also mit der Fähigkeit ausstatten, den Abstand zwischen einer Position (mx,my) und sich selbst zu messen. Dazu erweitern wir unsere Objektdefinitionen um eine öffentliche Funktion Dist(mx, my: Integer): Double, die den Abstand der übergebenen Position (mx,my) vom Objekt berechnet und zurückliefert. Diese Funktion soll schon in TGeoObj als abstrakte Funktion deklariert werden, damit sie sicher in allen geometrischen Objekten implementiert werden wird.

Wenn wir diese Funktion zur Verfügung haben, lässt sich der "Gesichtssinn" der Maus realisieren. Als äußeres Zeichen, dass "die Maus das Objekt erkannt hat", wollen wir den Cursor auf eine andere Form umschalten. Der folgende Codeabschnitt zeigt, wie dies mit einer passenden Ereignisprozedur für das Ereignis "OnMouseMove" erreicht werden kann:
     procedure TForm1.PaintBox1MouseMove(Sender: TObject; Shift: TShiftState;
                                         X, Y: Integer);
       var d, dmin : Double;
           i       : Integer;
       begin
       dmin := 10000;
       For i := 0 to GeoListe.Count - 1 do begin
         d := TGeoObj(GeoListe.Items[i]).Dist(X, Y);
         If d < dmin then
           dmin := d;
         end;
       If dmin < 5 then
         PaintBox1.Cursor := crHandPoint
       else
         PaintBox1.Cursor := crDefault;
       end;
Wenn die Grafik in einer Paintbox ausgegeben wird, sollten Sie das MouseMove-Ereignis der Paintbox benutzen, nicht das des Formulars, weil die übergebenen Mauskoordinaten X und Y nämlich im lokalen Koordinatensystem der zugehörigen Komponente zu interpretieren sind. In der Schleife wird jedes der in der GeoListe gespeicherten Objekte gefragt, wie weit die Maus von ihm entfernt ist; nach dem Durchlaufen der Schleife enthält dmin den kleinsten der dabei auftretenden Abstände. Ist dieser kleiner als 5, wird der Cursor (der Paintbox!) als zeigende Hand dargestellt, andernfalls wird der normale Mauspfeil benutzt.

Damit dies aber auch wirklich wirksam werden kann, muss jede von TGeoObj abgeleitete Klasse eine korrekte Variante der Dist-Funktion implementieren. Das ist gelegentlich nicht ganz einfach, aber es führt kein Weg daran vorbei. Für den Punkt und den Kreis sollten Sie das Problem selbst lösen können. Für die anderen beiden Fälle gibt's hier ein wenig Mathe-Nachhilfe:




Aufgaben:


  1. Unsere Objekte lernen messen!

    Legen Sie eine Kopie unseres GeoObj-Projekts in einem neuen Verzeichnis "GeoObj_3" an. Ergänzen Sie alle Klassen jeweils um eine passende Dist-Funktion. Vergessen Sie nicht die Deklaration der abstrakten Variante in TGeoObj! (Was passiert eigentlich, wenn Sie darauf verzichten?) Implementieren Sie dann die oben beschriebene MouseMove-Methode, und testen Sie damit Ihre Dist-Funktionen.
    [Lösungsvorschlag]


  2. Objekt-Erkennung

    Ergänzen Sie das Programm durch ein Edit-Feld, das stets eine Beschreibung des aktuell durch die Maus selektierten Objekts beinhalten soll. Ist kein Objekt selektiert, dann soll das Edit-Feld leer sein. Sicher müssen Sie dazu die Klassendefinitionen für Ihre geometrischen Objekte geeignet erweitern, aber das sollte Sie nun nicht mehr vor unlösbare Probleme stellen.
    [Lösungsvorschlag]





3.5 Objekte interaktiv manipulieren

Nun wollen wir die neuen Kenntnisse unserer Maus sinnvoll nutzen, um ein typisches Kennzeichen dynamischer Geometrieprogramme nachzubauen, nämlich den Zugmodus: wird ein Objekt mit der Maus angeklickt und dann die Maus mit gedrückter Maustaste verschoben, dann folgt das Objekt auf dem Bildschirm der Mausbewegung.

Dazu müssen unserer Objekte lernen, wie sie sich über den Bildschirm bewegen können. Bisher bekommt das Objekt seinen Ort bei der Erzeugung zugewiesen, und dieser Ort ändert sich während der gesamten Lebenszeit des Objekts nicht mehr. Nun brauchen wir also eine zusätzliche Funktionalität: das Objekt muss sich an einen anderen Ort verschieben können. Dazu implementieren wir in der Klasse TGeoObj ein Methode Move(dx, dy : Integer), die das Objekt um den übergebenen Vektor (dx,dy) verschiebt:
     procedure TGeoObj.Move(dx, dy: Integer; target: TCanvas);
       begin
       HideIt(target);
       x := x + dx;
       y := y + dy;
       DrawIt(target);
       end;
Beachten Sie, dass diese Prozedur dann automatisch auch in allen abgeleiteten Klassen zur Verfügung steht, und - aufgrund der Polymorphie! - stets die passenden Versionen von DrawIt und HideIt aufgerufen werden. Damit können sich also alle unsere Objekte um den jeweils übergebenen Vektor an eine neue Stelle des Bildschirms verschieben, ohne dass wir dies extra jedem Objekt einzeln bebringen müssen.

Die Konstruktion des Verschiebungsvektors muss im aufrufenden Programm stattfinden, und diese Aufgabe ist nicht so einfach. Betrachten wir den Vorgang genauer:
Neben der bisher schon implementierten MouseMove-Methode müssen wir also noch ein MouseDown- und ein MouseUp-Ereignis verarbeiten. Damit wir in den zugehörigen Methoden die Information zur Verfügung haben, ob ein Objekt selektiert ist (und gegebenenfalls welches), fügen wir dem Formular eine Variable SelectedObj vom Typ TGeoObj zu. Diese Variable soll stets das aktuell selektierte Objekt enthalten; ist keines selektiert, dann soll sie "Nil" enthalten, ein "Nichts" also. Diese Variable erhält Ihre Werte in der schon existierenden MouseMove-Methode des Formulars, die Sie dazu entsprechend erweitern müssen.

In der MouseDown-Methode soll dann der Zugmodus "eingeschaltet" werden. Dazu können Sie z.B. eine bool'sche Variable isDragging deklarieren, die in der MouseDown-Methode auf "TRUE" gesetzt wird, wenn aktuell ein Objekt selektiert ist. Ist kein Objekt selektiert, dann behält sie ihren Standardwert "FALSE", den sie auch am Ende des Zugvorganges, also in der MouseUp-Methode, wieder erhalten muss. In allen dazwischen liegenden Aufrufen der MouseMove-Methode kann dann der eigentliche Zugmodus realisiert werden:
So weit, so gut, aber wie aber kommen wir zu dem jeweils an die Move-Prozedur zu übergebenden Verschiebungsvektor (dx, dy)? Dieser muss direkt vor dem Move-Aufruf berechnet werden! Zunächst deklarieren wir in unserem Formular eine Variable LastMousePos vom Typ TPoint, in der die jeweils letzte verarbeitete Mausposition gespeichert wird. Diese Variable erhält ihren ersten Wert in der MouseDown-Methode, direkt nachdem dort isDragging auf "TRUE" gesetzt wurde. In nachfolgenden Aufrufen der MouseMove-Methode kann dann der Verschiebungsvektor aus der jetzt aktuellen Mausposition (X,Y) und der zuvor in LastMousePos abgelegten berechnet werden:
     dx := X - LastMousePos.X;
     dy := Y - LastMousePos.Y;
Mit diesem Verschiebungsvektor rufen wir nun die Move-Prozedur des aktuell selektierten Objekts auf:
     SelectedObj.Move(dx, dy, PaintBox1.Canvas);
Danach ist natürlich die nun aktuelle Mausposition in LastMousePos zu speichern - für den nächsten MouseMove-Aufruf:
     LastMousePos.X := X;
     LastMousePos.Y := Y;
Zugegeben: nicht gerade trivial! Es lohnt sich aber, dass Sie sich durch dieses Problem durchbeißen, handelt es sich hier doch um ein recht typisches Beispiel für die Organisation einer Interaktion zwischen Benutzer und Programm.




Aufgaben:


  1. Bewegliche Objekte!

    Legen Sie eine Kopie unseres GeoObj-Projekts in einem neuen Verzeichnis "GeoObj_5" an. Ergänzen Sie die Klasse TGeoObj um die oben gegebene Move()-Prozedur. Realisieren Sie dann den Zugmodus für die Objekte, indem Sie die beschriebenen zusätzlichen Variablen und Methoden für das Formular implementieren. (Noch ein kleiner Tipp: Es ist günstig, beim Beenden des Zugmodus ein komplettes Neuzeichnen aller Objekte zu veranlassen.)

    Testen Sie sodann Ihr Programm! Dabei wird Ihnen (hoffentlich!) auffallen, dass die obige Move-Methode nicht für alle von TGeoObj abgeleiteten Klassen zufriedenstellend funktioniert. Lokalisieren Sie den Fehler, und beheben Sie ihn!
    [Lösungsvorschlag]

  2. Geht's nicht einfacher?

    Der Aufwand zur Realisierung des Zugmodus ist doch recht erheblich. Fritzchen Pfiffig schlägt daher folgendes Verfahren vor, um dasselbe Ziel einfacher zu erreichen:
    Man übergibt der Move-Prozedur nicht den schwierig zu berechnenden Verschiebungsvektor, sondern einfach die aktuelle Mausposition. Diese muss dann direkt als der neue Ort des Objekts eingetragen werden. Eine Probe-Implementierung zeigt, dass das für Punkte auch gut funktioniert.
    Hat Fritzchen recht? Wo erwarten Sie Probleme?
    Möglicherweise müssen Sie's einfach mal ausprobieren... ;-)


  3. Direktes Löschen: Maus killt Objekt!

    Man kann das Programm aus Aufgabe 7 leicht so erweitern, dass ein Mausklick mit der rechten Maustaste zur Löschung des angeklickten Objekts führt. Dazu müssen Sie nur den Parameter Button der MouseDown-Ereignis-Prozedur auswerten. Die Details finden Sie in der Delphi-Hilfe.
    [Lösungsvorschlag]






3.6 Die Krönung: Abhängige Objekte

Bisher sind alle unsere Objekte ziemlich ungesellig: sie haben nichts miteinander zu tun. Als Beispiel für ein Objekt, das Beziehungen zu anderen Objekten hat, wollen wir nun eine neue Art von Kreisobjekten konstruieren, deren Lage und Größe durch jeweils zwei gegebene "Eltern-Punkte" bestimmt wird: diese Kreise sollen stets den ersten Punkt als Mittelpunkt haben, und die Kreislinie soll immer durch den zweiten Punkt verlaufen. Schwierig wird dies erst, wenn wir für die beiden Punkte die oben konstruierten "ziehbaren" Punkt-Objekte nehmen: beim Verziehen dieser Punkte muss dann nämlich der Kreis seine Lage und Größe so nachführen, dass er unter allen Umständen den ersten Punkt als Mittelpunkt und den zweiten Punkt als "Peripherie-Punkt" behält! Wir nennen die neu zu erstellende Klasse dieser "abhängigen Kreise" TGCircleWP, wobei "WP" für "With Parents" steht.

Ein solcher Kreis selbst kann dann zwar nicht mehr selbst gezogen werden, weil seine Position und Größe ja durch die Lage der beiden definierenden Eltern-Punkte eindeutig bestimmt ist. Allerdings muss er indirekt doch am Zugvorgang teilnehmen, und zwar immer dann, wenn einer seiner Eltern-Punkte gezogen wird.

Dazu brauchen wir eine Prozedur, die ähnlich aufgebaut ist wie die Move-Methode: sie muss zunächst das Objekt verbergen, dann seine neuen Koordinaten berechnen und es schließlich am neuen Ort wieder anzeigen. Der Unterschied ist nur, dass die neuen Koordinaten hier nicht von der Maus geliefert werden, sondern aus den Koordinaten der Eltern-Punkte errechnet werden müssen. Wir nennen die neue Methode z.B. Update, und übergeben als Parameter den Ziel-Canvas der Zeichenaktionen. Dieser wird für die DrawIt- und HideIt-Aufrufe benötigt.

Eine knifflige Frage ist, wo in unserer Klassen-Hierarchie wir die Update-Methode eigentlich einführen wollen. Im Grunde wird sie ja nur für die TGCircleWP-Objekte gebraucht. Andererseits sollen in der Hauptformular-Unit möglichst alle Objekte gleich behandelt werden; also sollten wir dort möglichst nur solche Methoden verwenden, die schon in TGeoObj zur Verfügung stehen. Wir können dies erreichen, indem wir Update schon in TGeoObj definieren, und zwar als zunächst funktionslose, leere Methode:
     procedure TGeoObj.Update(target: TCanvas);
       begin
       end;
Die Deklaration von Update muss diese Methode natürlich als virtuell ausweisen, damit sie später (also z.B. in TGCircleWP) überschrieben werden kann.
In der neuen Klasse TGCircleWP allerdings muss die Prozedur Update dann aber doch etwas tun, dafür haben wir sie ja schließlich eingeführt! Damit dass TGCircleWP-Objekt sein Position und Größe aktualisieren kann, muss es die Positionen seiner Elternpunkte abfragen können. Die entsprechenden Variablen x und y sind jedoch in TGeoObj als protected deklariert und damit auch in TGPoint unzugänglich! Als erstes müssen wir also TGPoint mit der Fähigkeit ausstatten, über seine Position Auskunft zu geben. Wir deklarieren dazu in TGPoint eine öffentliche Auskunfts-Funktion GetPos, die die aktuelle Bildschirmposition des Punktes in einer TPoint-Struktur zurückgibt.
Nach diesen Vorarbeiten können wir jetzt an die Konstruktion der Klasse TGCircleWP gehen. Das nebenstehende Diagramm zeigt, wie TGCircleWP von TGCircle und TGeoObj abgeleitet wird. Dazu einige Bemerkungen:
Es ist eine Menge Arbeit zu tun, bis der "abhängige Kreis" implementiert ist! Und es wird noch mehr Arbeit sein, bis wir er schließlich auf dem Bildschirm erscheinen wird. Also: frisch ans Werk!




Aufgaben:


  1. Abhängige Kreise erzeugen....

    Legen Sie eine Kopie unseres GeoObj-Projekts in einem neuen Verzeichnis "GeoObj_5" an. Ergänzen Sie die Klasse TGPoint um die oben beschriebene Auskunfts-Funktion GetPos und die Klasse TGeoObj um die virtuelle, aber zunächst leere Update-Prozedur.
    Erzeugen Sie dann eine neue Unit, in der Sie die oben dargestellte Klasse TGCircleWP implementieren.

    Damit Sie zur Laufzeit auch Instanzen der neuen Klasse herstellen können, brauchen Sie noch einen Knopf, in dessen Click-Prozedur Sie den folgenden Code einfügen sollen:
         var P1, P2 : TGPoint;
             i : Integer;
         begin
         P1 := Nil;
         P2 := Nil;
         i := Pred(GeoListe.Count);
         While (i >= 0) and (P1 = Nil) do begin
           If TGeoObj(GeoListe.Items[i]) is TGPoint then
             If P2 = Nil then
               P2 := GeoListe.Items[i]
             else
               P1 := GeoListe.Items[i];
           i := i - 1;
           end;
         If P1 = Nil then
           MessageDlg('Keine Eltern-Punkte verfügbar!', mtError, [mbOk], 0)
         else
           GeoListe.Add(TGCircleWP.Create(P2, P1, clGreen));
         Invalidate;
         end;
    Machen Sie sich genau klar, was hier passiert!
    Welche Vorgänge müssen der Erzeugung eines "abhängigen Kreises" vorausgehen? Wie kommt der neue Kreis zu seinen "Eltern"?
    Testen Sie sodann Ihr Programm! Erzeugt es die neuen Objekte richtig?
    Welche Funktionalität bietet es schon, welche vermissen Sie noch?
    [Lösungsvorschlag]


  2. ...und bewegen!

    Das Programm aus der vorigen Aufgabe kann zwar schon "abhängige Kreise" erzeugen, aber beim Verziehen der Eltern-Punkte folgen die Kreise noch nicht nach. Kein Wunder, denn wir haben unsere neue Update-Methode ja noch gar nicht eingesetzt! Sie können das Problem lösen, indem Sie die PaintBox1MouseMove-Methode des Formulars erweitern: bei eingeschaltetem Zugmodus muss einfach nach dem Aufruf von SelectedObj.Move() für alle Objekte der GeoListe die Update-Methode aufgerufen werden!

    Jetzt folgt der abhängige Kreis brav, wenn man seinen Mittelpunkt verzieht. Das Ziehen am Peripherie-Punkt scheint hingegen nicht möglich zu sein: statt des Peripherie-Punktes wird stets der Kreis selbst selektiert; der Peripherie-Punkt ist somit unzugänglich!

    Die erste Idee zur Behebung dieses Übels könnte sein, TGCircleWP.Dist zu überschreiben, so dass es immer 10000 liefert und deshalb ein abhängiger Kreis nie selektiert werden kann. Warum ist das eine dumme Idee?

    Schauen Sie sich stattdessen nochmals die PaintBox1MouseMove-Methode des Formulars im Falle des ausgeschalteten Zugmodus' an. Dort wird die gesamte GeoListe durchsucht und dasjenige Objekt selektiert, das am nächsten an der Maus ist. Stattdessen könnten Sie auch nach einem Objekt suchen, das hinreichend nahe bei der Maus ist, und die Suche abbrechen, sobald Sie ein solches Objekt gefunden haben. Schreiben Sie den Such-Algorithmus entsprechend um!
    [Lösungsvorschlag]




3.7 Warnende Worte für Weiterwollende

Wenn Sie jetzt die Versuchung verspüren, das bisher erarbeitete Programm zu einem ernsthaften dynamischen Geometrie-Programm weiterentwickeln zu wollen, dann sollten Sie sich zunächst die folgenden Anmerkungen aufmerksam durchlesen - ehe Sie in dieselben Fallen tappen wie ich damals im Jahr 1994, als ich mit der Programmierung des "EUKLID" begann!


Einer der größeren Konstruktionsfehler unseres obigen Programms ist, dass es in Bildschirm-Koordinaten rechnet. Das ist aus zweierlei Gründen unschön:
  1. Bildschirm-Koordinaten sind vom Typ Integer. Wenn Sie irgend etwas ernsthaftes vorhaben, müssen Sie unbedingt mit Fließkomma-Koordinaten (also z.B. Double) arbeiten, sonst scheitern Sie bei nächster Gelegenheit an Rechenungenauigkeiten.

  2. Über kurz oder lang wird Ihr Programm einen Ausdruck produzieren müssen. Angesichts der dann ohnehin notwendigen Umskalierung ist es klüger, alle geometrischen Rechnungen gleich in einem entsprechenden "User-Koordinatensystem" zu rechnen und nur direkt vor der Grafikausgabe auf Bildschirm-Koordinaten zu skalieren.
Die Technik, in User-Koordinaten zu rechnen und nur für die Ausgaben auf das Maschinen-Koordinatensystem des Ausgabegerätes umzurechnen, haben Sie schon im Kapitel "Was man schwarz auf weiß besitzt" kennengelernt. Die dort erarbeiteten Verfahren lassen sich ohne Probleme auf die hier gegebene Situation übertragen. Zwar entstehen nun zusätzliche Schwierigkeiten durch das Maus-Handling: die maus-relevanten Formularereignisse liefern die Mauskoordinaten natürlich im Bildschirm-Koordinatensystem, wohingegen Ihr Programm intern entsprechende User-Koordinaten brauchen wird. Aber dafür haben wir ja damals schon die passenden Umrechnungs-Funktionen erarbeitet!


Ein weiterer Konstruktionsfehler zeigt sich bei unserem Einsatz der Methode Update zur Realisierung des Zugmodus: wir haben sie einfach für alle Objekte der Liste aufgerufen, was bei größeren Zeichnungen sicher uneffektiv ist. Dies lässt sich vermeiden, wenn man dafür sorgt, dass nicht nur das abgeleitete Objekt seine "Eltern" kennt (was bei TGCircleWP ja schon der Fall ist), sondern auch jedes Eltern-Objekt seine "Kinder". Dann kann jedes Objekt, das seine Lage aktualisiert, sogleich die Aktualisierung aller seiner Kinder anstoßen. Insgesamt muss man sich also bei der Buchführung über die "Verwandtschaftsbeziehungen" der Objekte sehr viel mehr Mühe geben als wir das bisher getan haben.


Zum Schluss wollen wir einmal abschätzen, was eigentlich so alles zu einem "ordentlichen Geometrie-Programm" dazugehört. Nehmen wir mal an, dass Sie mit möglichst geringem Aufwand ein Programm schreiben wollen, mit dem man alles machen kann, was mit Zirkel und Lineal erreichbar ist. Um eine wohldefinierte Grenze für unser Projekt zu haben, lehnen wir den Gebrauch aller weiteren Werkzeuge (wie z.B. die "Parallelen-Striche" auf dem Geo-Dreieck) oder Messgeräte (wie z.B. Winkelmesser) ab. Auch soll unser Lineal nur aus einer geraden Kante bestehen, d.h. wir verzichten auf einen Maßstab zur Entfernungsmessung. Damit sind die zur Verfügung stehenden Basis-Objekte unseres Geometrie-Programms vorgegeben: Punkte, Geraden und Kreise. Die Punkte sind dabei einfach die Punkte in unserer Zeichenebene; der uns zugängliche Ausschnitt der Zeichenebene ist unser Konstruktionsfenster.

Um aus gegebenen Objekten neue zu konstruieren, gibt es die Operationen des Verbindens und des Schneidens. Mit Lineal und Zirkel sind dabei die folgenden Operationen durchführbar:

Welche Konstruktionsbefehle muss Ihr Programm also zur Verfügung stellen? Nach den obigen Überlegungen muss man...

  1. ...an beliebiger Stelle des Zeichenblattes einen Punkt setzen können.

  2. ...zwei beliebige Punkte durch eine Gerade verbinden können.

  3. ...den Schnittpunkt zweier beliebiger, nicht paralleler Geraden konstruieren können.

  4. ...zu zwei beliebigen Punkten P1 und P2 einen Kreis um P1 konstruieren können, der durch P2 geht.

  5. ...die Schnittpunkte eines Kreises k1 mit einer Geraden g oder einem anderen Kreis k2 konstruieren können (soweit sie existieren).

Man kommt also im Prinzip mit 5 Konstruktionsbefehlen aus! Was man zu deren Realisierung an Mathematik braucht, wird durch den Mathe-Unterricht der Oberstufe ohne Schwierigkeiten abgedeckt. Das so ausgestattete Programm ist bietet zwar noch keinen großen Komfort, weil es sich auf die elementaren Konstruktionen beschränkt. Aber im Prinzip kann der Benutzer mit ihm alle möglichen Zirkel- und Lineal-Konstruktionen durchführen - wenn er hinreichend fleißig ist und genug von Geometrie versteht!







Zum vorigen Kapitel Zum Inhaltsverzeichnis Zum nächsten Kapitel