Zeichnen in JavaFX-Komponenten
JavaFX bietet eine Vielzahl an Bedienelementen, sowie grafische Effekte und die Gestaltung mithilfe von CSS. In diesem Artikel möchte ich auf einige Gestaltungsmittel eingehen, die darüber hinausgehen 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 [3].
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 z. B. 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 Abb.1. Dort werden Figuren mitsamt der zugeordneten Effekte im gewählten Layout der FlowPane ausgerichtet. Häufig möchte man die grafischen Figuren zu neuen, komplexeren Gestalten flexibel kombinieren. Das ist mithilfe von Layouts nur schwer zu realisieren. Schauen wir daher auf eine Alternative.
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 getGraphicsContext2D() verschaffen. Daraufhin stehen vielfältige Möglichkeiten zum Zeichnen zur Verfügung, wobei dies an die Zeichenoperationen in Swing mithilfe der Klasse Graphics2D und die 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 Abb.2 dargestellt.
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 pixelbasierten 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 werden 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:
Ausschnitt aus dem Programm ’SecondCanvasExample’
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 (s. Abb.3).
Fazit
Neben den durchaus schon sehr reichhaltigen Möglichkeiten zur Gestaltug 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.
Möchten Sie mehr zu JavaFX erfahren, werfen Sie doch einen Blick in folgende Bücher:
- A. Epple, 2015: JavaFX 8: Grundlagen und fortgeschrittene Techniken, dpunkt.verlag
- M. Inden, 2015: Der Weg zum Java-Profi: Konzepte und Techniken für die professionelle Java-Entwicklung, dpunkt.verlag
- M. Inden, 2015: Java 8 - Die Neuerungen: Lambdas, Streams, Date and Time API und JavaFX 8 im Überblick, dpunkt.verlag