en
de

Zeichnen in JavaFX-Komponenten

2 Mai 2016
| |
Lesezeit: 5 Minutes

JavaFX bietet eine Vielzahl an Bedienelementen, sowie grafische Effekte und die Gestaltung mithilfe von CSS. In diesem Blog möchte ich auf einige Gestaltungsmittel eingehen, die darüber hinaus gehen und eventuell nicht jedem geläufig sind.

Nachfolgend schauen wir auf Varianten zum Zeichnen in JavaFX-Komponenten und im Speziellen auf die Klasse Canvas und Figuren mit Grafikprimitiven wie Linien, Kreisen, Rechtecken usw. Das kann man beispielsweise dazu nutzen, um Einträge in Listen, Comboboxen usw. mit einer ausgefallenen Darstellung zu versehen. Oder aber spezielle Problem angepasste Controls zu erstellen, etwa eine Tachometer-Anzeige. Beispiele finden Sie unter anderem in meinem Buch »Der Weg zum Java-Profi«.

Einstieg: Grafikprimitive als Nodes

In JavaFX werden grafische Figuren als Subklassen vom Typ Node bereitgestellt, etwa durch die Klassen Arc, Circle, Line und Rectangle – alle mit dem Basistyp Shape und aus dem Package javafx.scene.shape.

Diese lassen sich in Containerkomponenten, wie nachfolgend in einer FlowPane, anordnen – wie alle anderen Nodes auch. Wir schreiben folgendes Beispielprogramm:

Ausschnitt aus dem Programm ’FIRSTGRAPHICNODESEXAMPLE’

@Override
public void start(final Stage primaryStage) throws Exception
{
// Kreisbogen mit Beleuchtung
final Arc arc = new Arc(10, 10, 50, 50, 45, 270);
arc.setType(ArcType.ROUND);
arc.setFill(Color.GREENYELLOW);
arc.setEffect(new Lighting());
// Kreis mit Reflexion
final Circle circle = new Circle(10, 30, 30, Color.FIREBRICK);
circle.setEffect(new Reflection());
// Linie mit Schatten
final Line line = new Line(10, 10, 40, 10);
line.setEffect(new DropShadow());
// Rechteck mit Beleuchtung
Rectangle rectangle = new Rectangle(10, 10, 120, 120);
rectangle.setArcWidth(20);
rectangle.setArcHeight(20);
rectangle.setFill(Color.DODGERBLUE);
rectangle.setEffect(new Lighting());
final FlowPane flowPane = new FlowPane();
flowPane.getChildren().addAll(arc, circle, line, rectangle);
primaryStage.setScene(new Scene(flowPane, 300, 130));
primaryStage.setTitle(this.getClass().getSimpleName());
primaryStage.show();

Starten wir das Programm FIRSTGRAPHICNODESEXAMPLE so erhalten wir eine Ausgabe ähnlich zu der in Abbildung 1. Dort werden Figuren mitsamt der zugeordneten Effekten in dem gewählten Layout der FlowPane ausgerichtet. Häufiger möchte man die grafischen Figuren zu neuen, komplexeren Gestalten flexibel kombinieren. Das ist mithilfe von Layouts nur schwierig zu realisieren. Schauen wir daher auf eine Alternative.

Darstellung des Programms FirstGraphicNodesExample

Abb. 1: Darstellung des Programms FirstGraphicNodesExample

Zeichnen im Canvas

Alle bisher vorgestellten Komponenten bzw. Subtypen von Node beschreiben ihr Aussehen in Form von Vektorgrafiken und lassen sich daher ohne Qualitätsverlust skalieren, rotieren usw. Mitunter benötigt man mehr Flexibilität und feingranulare Kontrolle über die Darstellung. Dazu wurde in JavaFX 2.2 mit der Klasse javafx.scene.canvas.Canvas ein neues Element eingeführt, das ähnlich zu der Klasse java.awt.Graphics2D aus Swing ist und das Zeichnen von Bitmap-Grafiken mithilfe von einfachen Zeichenbefehlen erlaubt. Die Klasse Canvas besitzt als Besonderheit, dass hier auf Pixelebene gearbeitet wird.

Die Zeichenoperationen der Klasse Canvas werden wir uns nun einführend anschauen. Zunächst muss man sich Zugriff auf die Zeichenfläche per getGraphics-Context2D() verschaffen. Daraufhin stehen vielfältige Möglichkeiten zum Zeichnen zur Verfügung, wobei dies an die Zeichenoperationen in Swing mithilfe der Klasse Graphics2D und den dort definierten Methoden erinnert. Neben einfachen Formen wie Linien, Rechtecken, Ellipsen, Kreisbögen usw. kann man auch Polygone oder beliebige Pfade zeichnen oder füllen. Das geschieht mit Aufrufen wie clearRect(), strokeOval(), fillRoundRect() oder fillOval().

Darüber hinaus kann man komplexere Figuren durch sogenannte Pfade gestalten, die man mit beginPath() einleitet, dann den Zeichenstift per moveTo() bewegt und per lineTo() Linien bzw. mit bezierCurveTo() sogar Bezierkurven definieren kann. Durch einen Aufruf von closePath() wird die Figurbeschreibung abgeschlossen. Danach kann die so definierte Figur per stroke() oder fill() als Umriss oder gefüllt gezeichnet werden. Die eben beschriebenen Zeichenoperationen setzen wir im folgenden Listing ein:

Ausschnitt aus dem Programm ’FIRSTCANVASEXAMPLE’

@Override
public void start(final Stage primaryStage) throws Exception
{
// Canvas der Grösse 300 x 200 Pixel erzeugen
final Canvas canvas = new Canvas(300, 200);
drawOnCanvas(canvas);
final FlowPane flowPane = new FlowPane();
flowPane.getChildren().addAll(canvas);
primaryStage.setScene(new Scene(flowPane, 250, 100));
primaryStage.setTitle(this.getClass().getSimpleName());
primaryStage.show();
}
private void drawOnCanvas(final Canvas canvas)
{
final GraphicsContext gc = canvas.getGraphicsContext2D();
// Canvas-Hintergrund als Rechteck löschen
gc.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
// Oval zeichnen
gc.setStroke(Color.DARKGOLDENROD);
gc.setLineWidth(4);
gc.strokeOval(10, 20, 40, 40);
// Abgerundetes Rechteck füllen
gc.setFill(Color.BLUE);
gc.fillRoundRect(60, 20, 40, 40, 10, 10);
// Pfad definieren
gc.setStroke(Color.FIREBRICK);
gc.beginPath();
gc.moveTo(110, 30);
gc.lineTo(170, 20);
gc.bezierCurveTo(150, 110, 130, 30, 110, 40);
gc.closePath();
// Pfad malen
gc.stroke();
// Gefülltes Tortenstück darstellen
gc.setFill(Color.web("dodgerblue"));
gc.fillArc(180, 30, 30, 30, 45, 270, ArcType.ROUND);

Führt man das Programm FIRSTCANVASEXAMPLE aus, so werden verschiedene Figuren wie in Abbildung 2 dargestellt.

Darstellung des Programms FIRSTCANVASEXAMPLE

Abb. 2: Darstellung des Programms FIRSTCANVASEXAMPLE

Effekte im Canvas anwenden

Als Erweiterung zu den schon recht beachtlichen vorgestellten Möglichkeiten möchte ich noch auf ein paar Highlights eingehen: Selbst auf Pixel-basierten Grafiken lassen sich verschiedene Effekte anwenden. Dabei wirkt sich ein Effekt immer komplett auf den derzeitigen Inhalt im Canvas aus – kann demnach, im Gegensatz zu Nodes, nicht selektiv auf einzelne Figuren angewendet werden. Trotzdem lassen sich ansprechende Effekte erzielen, wie es das folgende Beispiel zeigt, etwa einen Schatten sowie einen Beleuchtungseffekt, der ein 3D-Aussehen verleiht und sich per Checkbox ein- und ausschalten lässt.

Die Figuren sowie Gradienten- und Schatteneffekte werden in der Methode createGraphics(Canvas) erstellt. Den Beleuchtungseffekt erzeugt die applyLighting(Canvas, boolean)-Methode. Allerdings lassen sich Reflexionen nicht direkt auf Canvas-Elemente anwenden. Reflexionen können nur auf den gesamten Canvas, der selbst vom Typ Node ist, angewendet werden:

Ausschnitt aus dem Programm ’SECONDCANVASEXAMPLE’

@Override
public void start(final Stage primaryStage) throws Exception
{
final Canvas canvas = new Canvas(550, 260);
createGraphics(canvas);
final CheckBox checkbox = new CheckBox("Apply Lighting");
// Beleuchtung
checkbox.setOnAction((event) -> applyLighting(canvas,
checkbox.isSelected()));
// Reflexionen gehen nur auf Ebene der Nodes
final Reflection reflection = new Reflection();
reflection.setFraction(0.7);
canvas.setEffect(reflection);
final FlowPane flowPane = new FlowPane();
flowPane.setPadding(new Insets(5));
flowPane.getChildren().addAll(checkbox, canvas);
primaryStage.setScene(new Scene(flowPane, 550, 500));
primaryStage.setTitle(this.getClass().getSimpleName());
primaryStage.show();

Nachfolgend sind die beiden Methoden createGraphics(Canvas) und applyLighting(Canvas, boolean) gezeigt. Beim Beleuchtungseffekt muss man zu einem kleinen Trick greifen: Weil hier auf Pixelebene gearbeitet wird, lässt sich ein einmal angewendeter Effekt – im Gegensatz zu den Effekten auf Nodes – nicht einfach rückgängig machen. Um den Eindruck des Ausschaltens zu erzielen, erzeugen wir die Grafik kurzerhand neu:

private GraphicsContext createGraphics(final Canvas canvas)
{
final GraphicsContext gc = canvas.getGraphicsContext2D();
gc.save();
gc.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
// Skalierung: x * 3 und y * 4
gc.scale(3, 4);
drawOval(gc);
fillRoundRectSpecialRadialGradient(gc);
fillPathWithLinearGradient(gc);
gc.restore();
return gc;
}
private void drawOval(final GraphicsContext gc)
{
gc.setStroke(Color.BLUEVIOLET);
gc.setLineWidth(7);
gc.strokeOval(10, 10, 40, 40);
}
private void fillRoundRectSpecialRadialGradient(final GraphicsContext gc)
{
gc.setFill(new RadialGradient(0, 0, 0.5, 0.5, 0.1, true,
CycleMethod.REFLECT,
new Stop(0.0, Color.LIGHTBLUE),
new Stop(0.5, Color.BLUE),
new Stop(1.0, Color.DODGERBLUE)));
gc.fillRoundRect(60, 10, 40, 40, 10, 10);
gc.applyEffect(new DropShadow(20, 5, 5, Color.BLACK));
}
private void fillPathWithLinearGradient(final GraphicsContext gc)
{
gc.beginPath();
gc.moveTo(110, 20);
gc.lineTo(170, 10);
gc.bezierCurveTo(150, 110, 130, 20, 110, 30);
gc.closePath();
// Pfad als Rahmen malen
gc.setStroke(Color.FIREBRICK);
gc.stroke();
// Pfad innen mit Gradient füllen
gc.setFill(new LinearGradient(0, 0, 1, 1, true, CycleMethod.NO_CYCLE,
new Stop(0, Color.GOLD),
new Stop(0.6, Color.RED),
new Stop(.85, Color.FIREBRICK)));
gc.fill();
}
private void applyLighting(final Canvas canvas,
final boolean applyLighting)
{
if (applyLighting)
{
// Reflexionen gehen nur auf Ebene der Nodes
final Light.Distant light = new Light.Distant();
final Lighting lighting = new Lighting();
lighting.setLight(light);
lighting.setSurfaceScale(10.0);
// Beleuchtungseffekt anwenden
final GraphicsContext gc = canvas.getGraphicsContext2D();
gc.applyEffect(lighting);
}
else
{
// Beleuchtungseffekt zurücksetzen => Grafik neu erzeugen
createGraphics(canvas);
}
}

Führt man das Programm SECONDCANVASEXAMPLE aus, so werden verschiedene Figuren dargestellt, die den Einfluss der Effekte zeigen (vgl. Abbildung 3).

Darstellung des Programms SECONDCANVASEXAMPLE

Abb. 3: Darstellung des Programms SECONDCANVASEXAMPLE

Fazit

Neben den durchaus schon sehr reichhaltigen Möglichkeiten zur Gestaltung eines ansprechenden GUIs mit gelungener Darstellung, Effekten, Animationen und CSS bietet JavaFX mit grafischen Nodes sowie vor allem dem Canvas viele Freiräume kreativ zu werden. Auf diese Weise können Sie das GUI bei Bedarf noch besser auf den jeweiligen Anwendungsfall abstimmen. Ein paar Ideen hat Ihnen dieser Blog vermittelt und Sie hoffentlich neugierig auf eigene Experimente gemacht.

Bedenken Sie bei Verschnörkelungen des GUIs aber bitte immer, dass wir Entwickler meistens nicht die besten Designer sind. Holen Sie sich daher gegebenenfalls externe Unterstützung.


Literatur & Weiterbildung

Für eine ausführliche Darstellung von Java im professionellen Einsatz sowie zu den Neuerungen in Java 8 verweise ich Sie auf meine Bücher:

Möchten Sie mehr zu JavaFX erfahren, werfen Sie doch einen Blick in das Buch von Anton Epple:

  • JavaFX, Anton Epple, 2015, dpunkt.verlag

Wenn Sie einen fundierten Überblick über die mit Java 8 eingeführten Neuerungen möchten, besuchen Sie unseren Java 8 Workshop.

Kommentare (0)

×

Updates

Schreiben Sie sich jetzt ein für unsere zwei-wöchentlichen Updates per E-Mail.

This field is required
This field is required
This field is required

Mich interessiert

Select at least one category
You were signed up successfully.

Erhalten Sie regelmäßige Updates zu neuen Blogartikeln

Jetzt anmelden

Oder möchten Sie eine Projektanfrage mit uns besprechen? Kontakt aufnehmen »