Geometrie II: benutzerdefinierte Umrisse



XML und Java ... Die Java 2D API ... Next: Painting und Stroking

[nach oben]


Das Shape-Interface

Die Graphics2D-Klasse weiß grundsätzlich nichts über Geometrie. Dennoch muß es einen Weg geben, um elementare geometrische Objekte wie Rechtecke oder Ellipsen darzustellen, wie das letzte Kapitel gezeigt hat. Die Entwickler der Java 2D API haben sich dabei - vom logischen Ansatz her ähnlich wie das Paint-Interface - eine Lösung ausgedacht, die eine Vereinheitlichung verschiedenster geometrischer Formen ermöglicht: Das Interface jawa.awt.Shape.

Das Shape-Interface schreibt Methoden vor, durch die man die meisten geometrische Objekte - hier auch Umrisse genannt - beschreiben kann. Durch diese Methoden wird Graphics2D die Darstellung dieser Objekte ermöglicht.

Graphics2D kann also im Prinzip lediglich ein geometrisches Objekt zeichnen oder füllen, welches das Shape-Interface implementiert.

Alle Klassen, die geometrische Objekte repräsentieren - hierzu zählen die Klassen aus dem Geometrie-Paket jawa.awt.geom wie z.B. Rectangle2D, Ellipse2D oder Line2D, implementieren das Shape-Interface.

Komplexe geometrische Formen haben gemeinsam, daß sie sich aus mehreren einfacheren Formen zusammensetzen lassen. Diese Tatsache macht sich das Shape-Interface zu Nutze. Ein Shape-Objekt beinhaltet unter anderem ein PathIterator-Objekt, welches zur geometrischen Beschreibung eines Umrisses dient. jawa.awt.geom.PathIterator ist selbst auch ein Interface.

Jedes Shape-Objekt besitzt ein Begrenzungsrechteck, welches das kleinstmögliche Rechteck darstellt, daß den gesamten Umriß umschließt (zugreifbar mit Rectangle2D getBounds2D()).

Das Shape-Interface schreibt folgende Methoden vor:

Methode Beschreibung
boolean contains(double x, double y) gibt zurück, ob der Punkt mit den Koordinaten x und y innerhalb des Begrenzungsrechtecks des Umrisses liegt.
boolean contains(double x, double y, double w, double h) gibt zurück, ob das Rechteck mit x,y,w,h innerhalb des Begrenzungsrechtecks des Umrisses liegt.
boolean contains(Point2D p) gibt zurück, ob der Punkt p im Umriß enthalten ist.
boolean contains(Rectangle2D r) gibt zurück, ob das Rechteck r vollständig im Umriß enthalten ist.
Rectangle getBounds() gibt das Begrenzungsrechteck für den Umriß als Rectangle zurück.
Rectangle2D getBounds2D() gibt das Begrenzungsrechteck für den Umriß als Rectangle2D zurück.
PathIterator getPathIterator(AffineTransform at) gibt ein PathIterator-Objekt zurück, daß den Umriß nach Ausführung der affinen Transformation at beschreibt.
PathIterator getPathIterator(AffineTransform at, double flatness) gibt ein PathIterator-Objekt zurück, daß den Umriß nach Ausführung der affinen Transformation at beschreibt. Die Kurvensegmente werden durch gerade Segmente angenähert. flatness gibt an, um wieviele Punkte ein solches Näherungssegment maximal von dem originalen Kurvensegment abweichen darf. Diese Methode ist wichtig für Graphics2D-Implementationen, die keine Kurvensegmente darstellen können.
boolean intersects(double x, double y, double w, double h) gibt zurück, ob das Rechteck x,y,w,h den Umriß schneidet.
boolean intersects(Rectangle2D r) gibt zurück, ob der Umriß das Rechteck r schneidet.

Das PathIterator-Objekt stellt eine iterative Beschreibung eines Umrisses durch Geraden und Kurven (2. und 3. Ordnung) dar. Es gibt jeweils ein aktuelles Segment, dessen Lage und Typ durch die currentSegment(...)-Methoden ermittelt werden können (s. Tabellen zu PathIterator). Die Gesamtheit aller Segmente bezeichnet man als Pfad. Ein einzelnes Segment besitzt einen der folgenden in PathIterator definierten, als int codierten, Typen:

Segmenttyp Bedeutung
SEG_CLOSE schließt den Pfad durch Ziehen einer Linie zu dem Punkt, der durch das letzte SEG_MOVETO angesprungen wurde.
SEG_CUBICTO beschreibt ein Segment, das durch eine kubische Kurve, die durch 3 Punkte defniert wird, vom vorherigen Punkt aus gezeichnet wird.
SEG_LINETO beschreibt ein Segment, das durch eine Gerade, die durch einen Punkt defniert wird, vom vorherigen Punkt aus gezeichnet wird.
SEG_MOVETO unterbricht den Pfad und setzt ihn an einem beliebigen Punkt wieder fort.
SEG_QUADTO beschreibt ein Segment, daß durch eine quadratische Kurve, die durch 2 Punkte defniert wird, vom vorherigen Punkt aus gezeichnet wird.

Des weiteren bietet PathIterator Konstanten an, die die Winding Rule des gesamten Pfades beschreiben. Die Winding Rule gibt bei Pfaden mit sich schneidenden Segmenten an, welche Teile des durch den Pfad definierten Umrisses innerhalb des Umrisses liegen und deshalb bei einem Aufruf von Graphics2D.fill(Shape s) gefüllt würden. Die Winding Rules sind gerade bei komplexen Pfaden nicht leicht nachzuvollziehen und somit ist es schwierig, manuell herauszufinden, welche Flächen des Pfades nun gefüllt werden und welche nicht.
Folgende Winding Rules werden von PathIterator in Form von Konstanten angeboten. Die mathematisch-abstrakt gehaltenen Erläuterungen entnehmen Sie bitte der Spezifikation von PathIterator in der Java 2D API-Spezifikation:

Das PathIterator-Interface schreibt folgende Methoden vor, die im Wesentlichen zur Abfrage der einzelnen Segmente und zum Voranschreiten innerhalb des Pfades dienen:

Methode Beschreibung
int currentSegment(double[] coords) liefert den Typ des aktuellen Segments (s. Tabelle oben) sowie dessen Koordinaten in einem double-Array.
coords enthält...
  • ...3*2=6 Koordinaten (jew. x und y) für kubische Segmente
  • ...2*2=4 Koordinaten (jew. x und y) für quadratische Segmente
  • ...1*2=2 Koordinaten (jew. x und y) für Geradensegmente
int currentSegment(float[] coords) wie vorherige Methode, aber liefert ein float-Array
int getWindingRule() gibt die Winding-Rule (s. Tabelle oben) für den Pfad zurück

Wie wir sehen, ist die Implementierung aller Shape- und PathIterator-Methoden eine mühsame Angelegenheit. Deswegen gibt es zur Vereinfachung eine Klasse jawa.awt.geom.GeneralPath, die das Shape-Interface bereits implementiert und die somit auch das von Shape geforderte PathIterator-Objekt zurückgeben kann. Bei der Erstellung eigener geometrischer Objekte ist aus Gründen der Einfachheit also der Einsatz der GeneralPath-Klasse sehr empfehlenswert.

[nach oben]


GeneralPath

Die Klasse java.awt.GeneralPath repräsentiert einen geometrischen Pfad, der aus einzelnen Segementen besteht. Die Segmente können Linien, quadratische Kurven oder kubische Kurven sein und werden nacheinander dem GeneralPath-Objekt hinzugefügt, bis der gewünschte Umriß erstellt ist. Da GeneralPath das Shape-Interface implementiert, kann der erstellte Umriß dann sofort und ohne vorherige Umwandlungen in das Graphics2D-Objekt gezeichnet werden.

Die Konstruktoren für GeneralPath entnehmen Sie bitte folgender Tabelle:

Konstruktor Beschreibung
GeneralPath() konstruiert einen leeren Pfad mit der non-zero Winding Rule.
GeneralPath(int rule) konstruiert einen neuen Pfad mit einer Winding Rule (s. letztes Kapitel), die angibt, auf welche Weise berechnet werden soll, ob ein Punkt in der Fläche des Pfades liegt. Hier muß GeneralPath.WIND_EVEN_ODD oder GeneralPath.WIND_NON_ZERO angegeben werden.
GeneralPath(Shape s) erzeugt einen neuen Pfad, der den Umriß s beschreibt.

Wenn man ein GeneralPath-Objekt erstellt hat, möchte man dem noch leeren Pfad Segemente hinzufügen. Der Startpunkt des Pfades liegt immer bei (0,0). Im schrittweisen Aufbauprozeß eines Pfades ist es wichtig, zu wissen, daß man auch diskontinuierliche Pfade erstellen kann, indem man dem Pfad einen neuen Punkt (mit moveTo() hinzufügt, der aber nicht mit dem letzten Punkt durch eine der drei Kurvenarten verbunden wird.

Das Hinzufügen von Segementen geschieht mit den folgenden Methoden:

Methode Beschreibung
void curveTo(float x1, float y1, float x2, float y2, float x3, float y3) fügt dem Pfad ein kubisches Kurvensegment hinzu. Die Punkte (x1,y1) und (x2,y2) dienen dabei als Kontrollpunkte, der letzte hinzugefügte Punkt und (x3,y3) dienen als Endpunkte.
void lineTo(float x, float y) fügt dem Pfad ein Liniensegment hinzu, daß vom letzten hinzugefügten Punkt zum Punkt (x,y) reicht.
void quadTo(float x1, float y1, float x2, float y2) fügt dem Pfad ein quadratisches Kurvensegment hinzu, daß vom letzten hinzugefügten Punkt zum Punkt (x2,y2) reicht. Der Kontrollpunkt ist (x1,y1).
void append(Shape s, boolean connect) fügt den Umriß s dem Pfad hinzu. Ist connect true, so wird vom letzten hinzugefügten Punkt ein Liniensegment zum Anfangspunkt des Pfades von s gezogen.
void append(PathIterator pi, boolean connect) wie vorherige Methode, aber der hinzuzufügende Pfad wird aus einem Objekt bezogen, welches das PathIterator-Interface implementiert.

Die folgende paint-Methode erstellt einen neuen Pfad und demonstriert den Einsatz der verschiedenen Segementtypen. Der Pfad wird in ein Graphics2D-Objekt gezeichnet (Zugriff auf die Quelle):

public void paint(Graphics g) {
				//Upcast --> mehr Funktionen in Graphics2D
				Graphics2D g2d=(Graphics2D)g;
				
				//Pfad erzeugen:
				GeneralPath gp = new GeneralPath();
				
				//Punkte hinzufügen
				gp.moveTo( 16, 12 );
				gp.lineTo( 41, 81 );
				gp.quadTo( 43, 86 ,44, 81 );
				gp.lineTo( 53, 60 );
				gp.quadTo( 54, 58, 53, 57  );
				gp.lineTo( 47, 44 );
				gp.lineTo( 48, 41 );
				gp.quadTo( 53, 36, 51, 36 );
				gp.lineTo( 45, 35 );
				gp.lineTo( 37, 17 );
				gp.quadTo( 36, 12, 43, 12 );
				gp.quadTo( 49, 12, 50, 15 );
				gp.lineTo( 75, 81 );
				gp.quadTo( 76, 83, 78, 81 );
				gp.lineTo( 87, 58 );
				gp.lineTo( 82, 44 );
				gp.quadTo( 77, 41, 84, 39 );
				gp.lineTo( 91, 39 );
				gp.quadTo( 93,39, 93, 41 );
				gp.quadTo( 94, 45, 95, 41 );
				gp.lineTo( 106, 10 );
				gp.lineTo( 83, 10 );
				gp.lineTo( 91, 32 );
				gp.quadTo( 91, 34, 89, 35 );
				gp.lineTo( 80, 35 );
				gp.lineTo( 69, 12 );
				gp.quadTo( 68, 9, 66, 10 );
				gp.lineTo( 18, 10 );
				gp.quadTo( 13, 9, 16, 12 );
				
				//Pfad unterbrechen und kubische Kurve hinzufügen
				gp.moveTo( 12, 100 );
				gp.curveTo( 0,90, 115, 105, 110, 95 );
				
				//Pfad skalieren (="größer machen")
				g2d.scale( 5, 5 );
							
				// Verlaufsfarbe erzeugen
				g2d.setPaint( new GradientPaint( 10,10, new Color(0,0,255), 
								 80,80, Color.white ));
				// Pfad füllen
				g2d.fill( gp );
				
				// schware Farbe setzen
				g2d.setPaint( Color.black );
				
				// Umriß des Pfades malen
				g2d.draw( gp );
			}
Das Ergebnis sieht wie folgt aus:

[nach oben]


konstruktive Flächengeometrie

Unter konstruktiver Flächengeometrie versteht man in der Java 2D API die Verknüpfung der Flächen von Shape-Objekten. Die Verknüpfung erfolgt analog zu Mengenverknüfpungen aus der Mathematik. Die Elemente der zu verknüpfenden Mengen sind hier die Punkte zweier Flächen.

Die Klasse java.awt.geom.Area ermöglicht solche Verknüpfungen. Mit Hilfe des Konstruktors

erstellt man ein neues Area-Objekt, daß die Fläche eines Shape-Objektes darstellt. Die folgenden Methoden aus Area stehen zur Verknüpfung von Flächen zur Verfügung:

Die folgende paint-Methode (Zugriff auf die Quelle) führt zwei Flächenadditionen, Flächensubtraktionen, und eine xor-Verknüpfung aus:


public void paint(Graphics g) {
				
				//Upcast --> mehr Funktionen in Graphics2D
				Graphics2D g2d=(Graphics2D)g;
				
				//Geometrische Formen erstellen
				Ellipse2D e = new Ellipse2D.Float( 20,20, 400,200 );
				Rectangle2D r = new Rectangle2D.Float( 300,80, 190,180 );
				Rectangle2D r2 = new Rectangle2D.Float( 80, 50, 50, 50 );
				Rectangle2D r3 = new Rectangle2D.Float( 90,150, 100, 100 );
				
				//Area-Objekte erstellen
				Area a_e = new Area( e );
				Area a_r = new Area( r );
				Area a_r2 = new Area ( r2 );
				Area a_r3 = new Area( r3 );
				
				//Flächen verknüpfen, Ergebnis in a:
				Area a = new Area();
				a.add( a_e ); //addieren der Ellipse
				a.add( a_r ); //addieren Rechteck r
				a.subtract( a_r2 ); //subtrahieren Rechteck r2
				a.exclusiveOr( a_r3 ); //exklusives Oder mit Rechteck r3
				
				//eine Verlaufsfarbe setzen
				g2d.setPaint( new GradientPaint( 0,0, Color.blue,
							500, 500, Color.green )
					);
					
				//Ergebnis füllen (dies ist möglich, weil auch Area Shape implementiert)
				g2d.fill( a );
			}
Das Ergebnis sieht wie folgt aus:


XML und Java ... Die Java 2D API ... Zum Seitenanfang