12.5Geometrische Objekte
Die JavaFX-Bibliothek repräsentiert geometrische Formen wie Linien, Polygone oder Kurven durch Objekte. Diese zweidimensionalen Formenobjekte sind abgeleitet von einer abstrakten Basisklasse javafx.scene.shape.Shape, die dreidimensionalen von javafx.scene.shape.Shape3D. Konkrete Formen realisieren Unterklassen, von Shape sind das Line, Rectangle, Circle, Ellipse, Arc, Polygon, Polyline, Path, CubicCurve, QuadCurve, SVGPath und Text. Bis auf Text liegen alle Shape-Unterklassen im Paket javafx.scene.shape.
Erstes Beispiel
Die Oberklasse Shape erweitert javafx.scene.Node, und somit sind die konkreten Formen allesamt gültige Objekte im Szenegraphen. Fünf konkrete Shape-Objekte kommen in unserem Beispiel in eine Group und dann auf den Bildschirm:
Listing 12.5com/tutego/insel/javafx/ShapeDemo.java, start()
Shape arc = new Arc( 100, 100, 20, 40, 0, 180 );
Shape circle = new Ellipse( 100, 200, 20, 20 );
OfInt rnd = new Random().ints( 10, 780 ).iterator();
Shape poly1 = new Polyline( rnd.nextInt(), rnd.nextInt(), rnd.nextInt(),
rnd.nextInt(), rnd.nextInt(), rnd.nextInt() );
Shape poly2 = new Polygon( rnd.nextInt(), rnd.nextInt(), rnd.nextInt(),
rnd.nextInt(), rnd.nextInt(), rnd.nextInt() );
SVGPath svg = new SVGPath();
svg.setContent( "M300 50 L175 200 L425 200 Z" );
Group group = new Group( text, arc, circle, poly1, poly2, svg );
stage.setScene( new Scene( group, 800, 800 ) );
stage.show();
[»]Hinweis
Um Formen zu zeichnen, gibt es in JavaFX zwei Möglichkeiten: Die eine ist – wie im Beispiel gesehen –, Shape-Objekte zu nutzen und diese in den Szenegraphen zu hängen. Die andere ist, eine Zeichenfläche (Canvas) zu erzeugen, sich darüber einen GraphicsContext zu besorgen und Methoden wie fillOval(…) oder drawImage(…) einzusetzen, um damit auf einer Zeichenfläche zu zeichnen, die dann selbst als Objekt in den Szenegraphen gehängt werden kann. Was es in JavaFX nicht gibt, ist eine Zeichenmethode wie draw(Shape).
12.5.1Linien und Rechtecke
Bei Linien müssen wir uns von der Vorstellung trennen, die uns die analytische Geometrie nahelegt. Laut Euklid ist eine Linie als kürzeste Verbindung zwischen zwei Punkten definiert. Da Linien eindimensional sind, besitzen sie eine Länge aus unendlich vielen Punkten, doch keine wirkliche Breite. Auf dem Bildschirm besteht eine Linie nur aus endlich vielen Punkten, und wenn eine Linie gezeichnet wird, werden Pixel gesetzt, die nahe an der wirklichen Linie sind. Die Punkte müssen passend in ein Raster gesetzt werden, und so kommt es vor, dass die Linie in Stücke zerbrochen wird. Dieses Problem gibt es bei allen grafischen Operationen, da von Fließkommawerten eine Abbildung auf Ganzzahlen, in unserem Fall absolute Koordinaten des Bildschirms, durchgeführt werden muss. Eine bessere Darstellung der Linien und Kurven ist durch Antialiasing zu erreichen. Dies ist eine Art Weichzeichnung mit nicht nur einer Farbe, sondern mit Abstufungen, sodass die Qualität auf dem Bildschirm wesentlich besser ist. Auch bei Zeichensätzen ist dadurch eine merkliche Verbesserung der Lesbarkeit auf dem Bildschirm zu erzielen.
Linien
Im ersten Beispiel haben wir schon javafx.scene.shape.Line verwendet, das Objekt verwaltet Start- und Endkoordinate:
Property | Property-Typ | Funktion |
---|---|---|
startX, startY | DoubleProperty | Start-Koordinate x/y |
endX, endY | DoubleProperty | End-Koordinate x/y |
Tabelle 12.1Properties von Line
Neben den Settern/Gettern, einem Standard-Konstruktor und dem Konstruktor Line(double startX, double startY, double endX, double endY) hat die Klasse nichts zu bieten.
Rechtecke
Rechtecke sind im Grunde nichts anderes als vier Linien. Doch Rechtecke kann JavaFX auch füllen, und zudem können sie abgerundet sein, wofür die Klasse die Angabe eines Bogen-Durchmessers für die Ecken erlaubt – alle Ecken können nur die gleiche Rundung haben.
Property | Property-Typ | Funktion |
---|---|---|
X, y | DoubleProperty | Start-Koordinate x/y |
width, height | DoubleProperty | Breite/Höhe des Rechtecks |
arcHeight | DoubleProperty | Durchmesser für Eckenbogen |
Tabelle 12.2Properties von Rectangle
Die Klasse hat die zu erwartenden Setter/Getter und vier Konstruktoren:
extends Shape
Rectangle()
Rectangle(double width, double height)
Rectangle(double x, double y, double width, double height)
Rectangle(double width, double height, Paint fill)
12.5.2Kreise, Ellipsen, Kreisförmiges
Ein Kreis wird in JavaFX von der Klasse Circle repräsentiert, eine Ellipse durch Ellipse. Beide verfügen über einen Mittelpunkt. Während der Kreis nur einen Radius hat, hat die Ellipse zwei Radien, die Breite und Höhe bestimmen:
Klasse | Property | Property-Typ | Funktion |
---|---|---|---|
Circle, Ellipse | centerX | DoubleProperty | Mittelpunkt, x-Achse |
Circle, Ellipse | centerY | DoubleProperty | Mittelpunkt, y-Achse |
Circle | radius | DoubleProperty | Radius des Kreises |
Ellipse | radiusX | DoubleProperty | x-Radius der Ellipse |
Ellipse | radiusY | DoubleProperty | y-Radius der Ellipse |
Tabelle 12.3Properties von Circle und Ellipse
Beide Klassen haben einen Standard-Konstruktor und dazu folgende weitere parametrisierte Konstruktoren:
extends Shape
Circle(double radius)
Circle(double centerX, double centerY, double radius)
Circle(double centerX, double centerY, double radius, Paint fill)
Circle(double radius, Paint fill)
extends Shape
Ellipse(double radiusX, double radiusY)
Ellipse(double centerX, double centerY, double radiusX, double radiusY)
Die Ellipse kann also bisher nicht direkt mit Füllfarbe initialisiert werden.
Kreisbogen
Die Klasse javafx.scene.shape.Arc repräsentiert einen Kreisbogen. Diese Bögen ähneln Ellipsen, nur dass sie einen Startwinkel (in Grad) und relativ von dort einen Bogenwinkel haben. Und da ein Kreisbogen an sich »offen« ist, gibt es einige Optionen, ob das so bleibt:
ArcType.OPEN: Kreisbogen bildet eine einfache Kreislinie, der verbleibende Teil ist offen.
ArcType.CHORD: Start- und Endpunkt des Bogens werden durch eine Linie verbunden.
ArcType.ROUND: Start- und Endpunkt des Bogens werden mit dem Mittelpunkt des Kreises verbunden, was dann so aussieht wie ein Kuchen (unter Java 2D noch »Pie« genannt).
Abbildung 12.4Die drei Bogentypen
Damit ist jeder Kreisbogen durch folgende Properties bestimmt:
Property | Property-Typ | Funktion |
---|---|---|
centerX, centerY | DoubleProperty | x-/y-Achse vom Mittelpunkt des Kreisbogens |
radiusX, radiusY | DoubleProperty | horizontaler/vertikaler Radius |
startAngle | DoubleProperty | Startwinkel |
length | DoubleProperty | Winkel des Bogens |
type | ObjectProperty<ArcType> | ArcType.OPEN, ArcType.CHORD oder -ArcType.ROUND |
Tabelle 12.4Properties von Arc
Neben den Settern/Gettern für die Properties hat die Klasse Arc nichts zu bieten. Zwei Konstruktoren gibt es:
extends Shape
Arc()
Baut einen Kreisbogen ohne irgendwelche gesetzten Properties auf.Arc(double centerX, double centerY, double radiusX,
double radiusY, double startAngle, double length)
Baut einen Kreisbogen mit Zustand auf.
12.5.3Es werde kurvig – quadratische und kubische Splines
Die Klasse QuadCurve beschreibt quadratische Kurvensegmente. Dies sind Kurven, die durch zwei Endpunkte und einen dazwischenliegenden Kontrollpunkt definiert sind. CublicCurve beschreibt kubische Kurvensegmente, die durch zwei Endpunkte und zwei Kontrollpunkte definiert sind. Kubische Kurvensegmente werden auch Bézier-Kurven genannt.
Abbildung 12.5Kontrollpunkte der Kurven
Listing 12.6com/tutego/insel/javafx/CubicCurveDemo.java
public void start( Stage stage ) {
double startX = 200, startY = 200;
DoubleProperty controlX1 = new SimpleDoubleProperty( 20 );
DoubleProperty controlY1 = new SimpleDoubleProperty( 20 );
DoubleProperty controlX2 = new SimpleDoubleProperty( 400 );
DoubleProperty controlY2 = new SimpleDoubleProperty( 20 );
double endX = 300, endY = 200;
// Linie von [controlX1, controlY1] nach [startX, startY]
Line line1 = new Line( 0, 0, startX, startY );
line1.startXProperty().bind( controlX1 );
line1.startYProperty().bind( controlY1 );
line1.setStrokeWidth( 2 );
// Linie von [controlX2, controlY2] nach [endX, endY]
Line line2 = new Line( 0, 0, endX, endY );
line2.startXProperty().bind( controlX2 );
line2.startYProperty().bind( controlY2 );
line2.setStrokeWidth( 2 );
// Animierte Kontrollpunkte
Timeline timeline = new Timeline( new KeyFrame( Duration.millis( 1000 ),
new KeyValue( controlX1, 300 ),
new KeyValue( controlY2, 300 ) ) );
timeline.setCycleCount( Timeline.INDEFINITE );
timeline.setAutoReverse( true );
timeline.play();
CubicCurve curve = new CubicCurve( startX, startY, 0, 0, 0, 0, endX, endY );
curve.controlX1Property().bind( controlX1 );
curve.controlY1Property().bind( controlY1 );
curve.controlX2Property().bind( controlX2 );
curve.controlY2Property().bind( controlY2 );
curve.setFill( null );
curve.setStroke( Color.BLUEVIOLET );
curve.setStrokeWidth( 3 );
stage.setScene( new Scene( new Group( line1, line2, curve ), 450, 300 ) );
stage.show();
}
Abbildung 12.6Screenshot der Anwendung CubicCurveDemo
12.5.4Pfade *
Ein Pfad besteht aus zusammengesetzten Segmenten, die miteinander verbunden sind. Es ist ein wenig wie Malen nach Zahlen, bei dem der Stift von einem Punkt zum anderen gezogen wird, aber auch abgesetzt und neu positioniert werden darf. Die Segmente bestehen nicht wie bei Polygonen ausschließlich aus Linien, sondern können auch quadratische oder kubische Kurven sein.
Die Klasse javafx.scene.shape.Path erbt von Shape und repräsentiert eine Liste dieser Pfadsegmente, die in JavaFX vom Typ PathElement sind. Die Pfadsegmente sind also Objekte und werden dem Path entweder am Anfang über den Konstruktor übergeben oder später der Liste angehängt. Von der abstrakten Basisklasse PathElement gibt es folgende Unterklassen: LineTo, HLineTo, VLineTo, ArcTo, CubicCurveTo, QuadCurveTo, ClosePath und MoveTo. Die letzten beiden Typen nehmen eine gewisse Sonderstellung ein, da ClosePath den Pfad abschließt und ein MoveTo-Objekt den Zeichenstift absetzt und neu positioniert; da der Startpunkt automatisch bei (0,0) liegt, kann MoveTo ihn auch zu Anfang gut setzen.
[zB]Beispiel
Zeichnen einer Linie als Pfad, Variante 1:
Listing 12.7com/tutego/insel/javafx/PathDemo.java, Ausschnitt
Alternativ dazu Variante 2: erst das Path-Objekt aufbauen, dann die Liste der Segmente erfragen und schließlich anhängen:
path.getElements().add( new MoveTo( 10, 10 ) );
path.getElements().add( new LineTo( 100, 20 ) );
Natürlich hätten wir in diesem Fall auch gleich ein Polyline- oder ein Line-Objekt nehmen können. Doch dieses Beispiel zeigt einfach, wie ein Pfad aufgebaut ist. Zunächst bewegen wir den Zeichenstift mit einem über den MoveTo(double, double)-Konstruktor erzeugten Objekt auf die Startposition, und anschließend zeichnen wir eine Linie mit einem über den Konstruktor LineTo(double, double) erzeugten Objekt.
Die Eigenschaften der Klasse Path sind daher auch übersichtlich:
extends Shape
Path()
Path(Collection<? extends PathElement> elements)
Path(PathElement... elements)
ObservableList<PathElement> getElements()
Des Weiteren gibt es eine Property fillRule für die Windungsregel und daher Setter/Getter.
Windungsregel *
Eine wichtige Eigenschaft der Pfade für gefüllte Objekte ist die Windungsregel (engl. winding rule). Diese Regel beschreibt eine Aufzählung javafx.scene.shape.FillRule und kann entweder NON_ZERO oder EVEN_ODD sein. Wenn Zeichenoperationen aus einer Form herausführen und wir uns dann wieder in der Figur befinden, sagt EVEN_ODD aus, dass dann innen und außen umgedreht wird. Wenn wir also zwei Rechtecke durch einen Pfad ineinander positionieren und der Pfad gefüllt wird, bekommt die Form in der Mitte ein Loch.
Das folgende Programm zeichnet ein blaues Rechteck mit NON_ZERO und ein rotes Rechteck mit EVEN_ODD. Mit der Konstanten NON_ZERO bei setFillRule(FileRule) wird das innere Rechteck mit ausgefüllt. Ausschlaggebend dafür, ob das innere Rechteck gezeichnet wird, ist die Anzahl der Schnittpunkte nach außen – »außen« heißt in diesem Fall, dass es unendlich viele Schnittpunkte gibt. Diese Regel wird aber nur dann wichtig, wenn wir mit nichtkonvexen Formen arbeiten. Solange sich die Linien nicht schneiden, ist dies kein Problem:
Listing 12.8com/tutego/insel/javafx/FillRuleDemo.java, start(…)
public void start( Stage stage ) {
Rectangle rect1 = new Rectangle( 70, 70, 130, 50 );
rect1.setFill( Color.YELLOW );
Path rect2 = makeRect( 100, 80, 50, 50 );
rect2.setFill( Color.BLUE );
rect2.setFillRule( FillRule.NON_ZERO );
Path rect3 = makeRect( 200, 80, 50, 50 );
rect3.setFill( Color.RED );
rect3.setFillRule( FillRule.EVEN_ODD );
Group group = new Group( rect1, rect2, rect3 );
stage.setScene( new Scene( group, 300, 200 ) );
stage.show();
}
Die eigene statische Methode makeRect(int, int, int, int) definiert den Pfad für die Rechtecke mit den Mittelpunktkoordinaten x und y. Das erste Rechteck besitzt die Breite width sowie die Höhe height, und das innere Rechteck ist halb so groß:
Listing 12.9com/tutego/insel/javafx/FillRuleDemo.java, makeRect(…)
Path p = new Path(
new MoveTo( x + width/2, y - height/2 ), new VLineTo( y + height/2 ),
new HLineTo( x - width/2 ), new VLineTo( y - height/2 ),
new ClosePath(),
new MoveTo( x + width/4, y - height/4 ), new VLineTo( y + height/4 ),
new HLineTo( x - width/4 ), new VLineTo( y - height/4 ),
new ClosePath() );
return p;
}
Mit MoveTo(double, double) bewegen wir uns zum ersten Punkt. Die weiteren Linien-Direktiven formen das Rechteck. Die Form muss geschlossen werden, doch dieses Beispiel macht durch das innere Rechteck anschaulich, dass die Figuren eines Path-Objekts nicht zusammenhängend sein müssen. Das innere Rechteck wird genauso gezeichnet wie das äußere.
Abbildung 12.7Die Windungsregeln NO_ZERO und EVEN_ODD
12.5.5Polygone und Polylines
Eine Polyline besteht aus einer Menge von Linien, die einen Linienzug beschreiben. Dieser Linienzug muss nicht geschlossen sein. Ist er es dennoch, sprechen wir von einem Polygon. Für beides gibt es in JavaFX Klassen: Polygon und Polyline. Aufgebaut werden sie über den Standard-Konstruktor oder über einen parametrisierten Konstruktor, der mit (double... points) eine Liste von x-/y-Koordinaten annimmt. Die Klassen konvertieren die Punkte in eine interne ObservableList<Double>, die mit getPoints() zugänglich ist. Die Liste aus der Rückgabe kann problemlos verändert werden. Beide Objekte haben selbst keine Properties, daher gibt es neben getPoints() keine Setter/Getter in beiden Klassen.
12.5.6Beschriftungen, Texte, Fonts
Eine einfach formatierte Beschriftung realisiert die Klasse Text. Gesetzt wird ein String an eine Position, wobei der String mit »\n« einen Umbruch enthalten kann, was JavaFX respektiert. Die Koordinaten bestimmen die Position der Schriftlinie – auch Grundlinie genannt (engl. baseline) –, auf der die Buchstaben stehen.
Damit ergeben sich die ersten drei Properties:
Property | Property-Typ | Funktion |
---|---|---|
x und y | DoubleProperty | x-/y-Koordinate |
Text | StringProperty | Beschriftung |
Tabelle 12.5Properties von Text
Da die Eigenschaften gerne am Anfang gesetzt werden, initialisiert sie zwei Konstruktoren gleich:
Text(String text)
Text(double x, double y, String text)
Einen Standard-Konstruktor hat die Klasse natürlich auch. Die Klasse hat weitere Properties, die die Javadoc aufzeigt, etwa ob der Text durch-/unterstrichen ist oder nicht, die Ausrichtung, wo umbrochen werden soll, oder den FontSmoothingType.
Zeichensätze/Fonts
JavaFX bringt die Unicode-Zeichenketten auf den Bildschirm, die aus so genannten Glyphen bestehen, das sind konkrete grafische Darstellungen eines Zeichens. Die Darstellung von Zeichen übernimmt in Java ein Font-Renderer, der in JavaFX fest verdrahtet ist.
Mit jeder Beschriftung ist standardmäßig ein Zeichensatz verbunden, der sich ändern lässt, üblicherweise mit setFont(Font). Argument der Methode ist ein javafx.scene.text.Font-Objekt, das im Wesentlichen den Zeichensatz, die Größe und Ausrichtung repräsentiert. Aufbauen lässt sich das Font-Objekt über zwei Konstruktoren oder über fünf Fabrikmethoden:
Font(double size)
Font(String name, double size)
static Font font(String family, double size)
static Font font(String family, FontPosture posture, double size)
static Font font(String family, FontWeight weight, double size)
static Font font(String family, FontWeight weight, FontPosture posture, double size)
Bis auf FondPosture (was eine Aufzählung mit ITALIC und REGULAR ist) und FontWeight (ebenfalls eine Aufzählung aus THIN, EXTRA_LIGHT, LIGHT, NORMAL, MEDIUM, SEMI_BOLD, BOLD, EXTRA_BOLD, BLACK) sind die Parameternamen selbsterklärend: Wir können den Namen des Zeichensatzes angeben und die Größe. Die Größe ist in Punkt angegeben, wobei 1 Punkt 1/72 Zoll (in etwa 0,376 mm) entspricht. Wird ein Font einem Text zugewiesen, der ja ein Node ist, kann der Knoten natürlich noch transformiert, etwa skaliert werden, sodass sich die Größe ändert.
Ist der Font einmal aufgebaut, gibt es die vier Getter, die von dem immutable Objekt die Zustände erfragen: String getName(), double getSize(), String getStyle() und String getFamily() – über JavaFX-Properties verfügt die Klasse nicht.
[»]Hinweis
Die Dokumentation spricht zwar von »Punkt«, in Java sind aber Punkt und Pixel bisher identisch. Würde das Grafiksystem wirklich in Punkt arbeiten, müsste es die Bildschirmauflösung und den Monitor mit berücksichtigen.
Neue TrueType-Fonts in Java nutzen
Die auf allen Systemen vordefinierten Standardzeichensätze sind etwas dürftig, obwohl die Font-Klasse selbst jeden installierten Zeichensatz mit getFontNames() und getFamilies() einlesen kann. Da ein Java-Programm aber nicht von der Existenz eines bestimmten Zeichensatzes ausgehen kann, ist es praktisch, einen Zeichensatz mit der Installation auszuliefern und dann diesen zu laden; das kann die Font-Klasse mit loadFont(…) machen. Die beiden statischen Methoden loadFont(InputStream in, double size), loadFont(String urlStr, double size) lesen einen Zeichensatz (in der Regel TrueType) ein und erstellen das entsprechende Font-Objekt. Es gibt die Rückgabe null, wenn JavaFX den Zeichensatz nicht einlesen kann, eine Ausnahme sollte es bei einem falschen Format nicht sein.
12.5.7Die Oberklasse Shape
Die Klassen Shape und Shape3D erben von Node, was jeder Form eine Reihe von Fähigkeiten gibt, etwa:
Methoden zur Transformation und zum Setzen von Effekten
Abfangen von diversen Events
Punkt-in-Form-Test
Abfragen der umgebenden Box
Setzen der Transparenz (engl. opacity)
Nehmen eines Bildschirmabzugs (Screenshot) von der Form mit snapshot(…)
Shape ist selbst abstrakt, doch gibt es keine abstrakte Methode, die eine Unterklasse implementieren muss – die Klasse ist nur daher abstrakt, weil es keinen Sinn ergibt, von diesem speziellen Knotentyp ein Exemplar zu bilden.
Fähigkeiten jeder Form
Die Unterklassen von Shape bekommen Funktionalität nicht nur von Shapes Oberklasse Node, sondern Shape liefert den Formen selbst auch noch eine Reihe von Möglichkeiten, wie das Setzen von Muster oder Farbe. Insgesamt deklariert Shape folgende Properties:
Property | Property-Typ | Funktion |
---|---|---|
smooth | BooleanProperty | Antialiasing ein/aus |
fill | ObjectProperty<Paint> | wie das Innere der Form zu füllen ist, in der Regel mit Farbe |
stroke | ObjectProperty<Paint> | wie Außenlinien der Form zu zeichnen sind, in der Regel Linienfarbe |
strokeDashOffset | DoubleProperty | Abstand im Linienmuster |
strokeLineCap | ObjectProperty<StrokeLineCap> | Abschluss einer einzelnen Linie, zum Beispiel abgerundet |
strokeLineJoin | ObjectProperty<StrokeLineJoin> | Form von Linienzusammenschlüssen |
strokeMiterLimit | DoubleProperty | Präzisiert strokeLineJoin, veranschaulicht wie »spitz« zwei Linien sich treffen. |
strokeWidth | DoubleProperty | Stiftbreite |
strokeType | ObjectProperty<StrokeType> | von wo die Stiftbreite eigentlich gibt, etwa außen um die Form |
Tabelle 12.6Properties eines Shape
Konstruktive Flächengeometrie *
Die Klasse Shape bietet statische Methoden, mit der sich zwei Formen zu neuen Formen verknüpfen lassen. Die Verknüpfungen sind Addition (Vereinigung), Subtraktion, und Schnitt (ein XOR gibt es nicht).
Die Signaturen der Methoden sind:
implements EventTarget
static Shape union(Shape shape1, Shape shape2)
Bildet eine Vereinigung zweier Formen. Ein Haus lässt sich zum Beispiel auf diese Weise durch ein Polygon mit dreieckiger Form, vereinigt mit einem Rectangle, darstellen.static Shape subtract(Shape shape1, Shape shape2)
Schneidet die Form shape2 aus shape1 aus, sozusagen shape1 minus shape2.static Shape intersect(Shape shape1, Shape shape2)
Bildet den Schnitt von zwei Formen, also alles, wo beide Formen Pixel haben, bleibt im Ergebnis.
Abbildung 12.8Beispiele für konstruktive Flächengeometrie mit Rechteck und Kreis
Die Verknüpfungen werden auch CAG (Constructive Area Geometry), zu Deutsch konstruktive Flächengeometrie, genannt.