Kürzlich habe ich mit der Installation eines JDK experimentiert, das JavaFX enthält. Das vereinfacht die Ausführung von JavaFX-Anwendungen, da man die JavaFX-Laufzeitumgebung nicht separat herunterladen muss, beispielsweise von der Gluon-Website [1]. Ich habe diese Experimente auf einem Raspberry Pi durchgeführt und eine kleine Testanwendung verwendet, die eine Menge sich bewegender Punkte auf den Bildschirm bringt. Dabei habe ich bemerkt, dass die Leistung bei vielen dieser Kreisobjekte langsamer wird.
Ich habe schon mehrmals gelesen, dass ein Canvas für diese Art von Anwendungsfall viel effizienter sein kann, und das hat mich dazu veranlasst, eine Testanwendung mit „Bouncing Balls“ (Abb. 1) zu erstellen, die es einfach macht, Nodes und Canvas zu vergleichen.
Abb. 1: Meine Testanwendung
Node versus Canvas
In JavaFX sind sowohl Nodes als auch Canvas Teil des Scene Graphs, aber sie haben unterschiedliche Use Cases. Die Wahl zwischen den beiden hängt oft von den spezifischen Anforderungen Ihrer Anwendung ab. Sie verwenden Nodes für statische Inhalte wie Eingabeformulare, Datentabellen, Dashboards mit Diagrammen … Das ist in der Regel bequemer und effizienter. Das Canvas bietet Ihnen mehr Flexibilität, wenn Sie dynamische oder benutzerdefinierte Inhalte erstellen müssen.
JavaFX Node
javafx.scene.Node ist die Basisklasse und alle visuellen JavaFX-Komponenten erweitern sie. Das geht mehrere „Schichten“ tief. Zum Beispiel Button > ButtonBase > Labeled > Control > Region > Parent > Node.
Zusammengefasst:
- Ein Node in JavaFX repräsentiert ein Element des Scene Graph.
- Dazu gehören UI-Steuerelemente wie Buttons, Labels, Text Fields, Shapes, Images, Media, Embedded Web Browser usw.
- Jeder Node kann im 3D-Raum positioniert und transformiert werden, er kann Events handlen und es können Effekte auf ihn angewendet werden.
- Node ist eine Basisklasse für alle visuellen Elemente.
- Die Verwendung von Nodes wird als „Retained Mode Rendering“ bezeichnet.
SIE LIEBEN JAVA?
Den Core-Java-Track entdecken
Das sind einige typische Komponenten, die von Node abgeleitet sind:
Label label = new Label("Hello World!");
Button button = new Button("Click Me!");
JavaFX Canvas
javafx.scene.canvas erweitert ebenfalls Node, fügt aber spezielle Funktionen hinzu. Sie können Ihren eigenen Inhalt auf dem Canvas zeichnen, indem Sie eine Reihe von Grafikbefehlen verwenden, die von einem GraphicsContext bereitgestellt werden.
Zusammengefasst:
- Sie zeichnen auf einem Canvas mit einem GraphicsContext.
- Das direkte Zeichnen auf einem Canvas wird als „Immediate Mode Rendering“ bezeichnet.
- Das gibt Ihnen mehr Flexibilität, ist aber weniger effizient, wenn sich der Inhalt nicht oft ändert.
In diesem Beispiel wird ein Rechteck gezeichnet:
Canvas canvas = new Canvas(400, 300); GraphicsContext gc = canvas.getGraphicsContext2D(); gc.setFill(Color.BLUE); gc.fillRect(50, 50, 100, 70);
Demoanwendung
Die Demoanwendung kann im GitHub Gist unter [2] gefunden werden. Sie enthält Code, um eine Menge sich bewegender Kreise zu erzeugen – sowohl als Nodes als auch gezeichnet auf einem Canvas. Der Wert am Anfang des Codes definiert, welcher Ansatz verwendet wird:
private static int TYPE_OF_TEST = 1; // 1 = Nodes, 2 = Canvas
Nodes verwenden
Wenn Sie Nodes verwenden, wird dem Bildschirm ein Bereich hinzugefügt, in dem Bälle eingefügt werden. Bei jedem Ball handelt es sich um einen Circle Node mit einer Bewegungsmethode (Listing 1).
class BallNode extends Circle { private final Color randomColor = Color.color(Math.random(), Math.random(), Math.random()); private final int size = r.nextInt(1, 10); private double dx = r.nextInt(1, 5); private double dy = r.nextInt(1, 5); public BallNode() { this.setRadius(size / 2); this.setFill(randomColor); relocate(r.nextInt(380), r.nextInt(620)); } public void move() { if (hitRightOrLeftEdge()) { dx *= -1; // Ball hit right or left } if (hitTopOrBottom()) { dy *= -1; // Ball hit top or bottom } setLayoutX(getLayoutX() + dx); setLayoutY(getLayoutY() + dy); } ... }
Canvas verwenden
Wenn Sie das Canvas verwenden, ist jeder Ball ein Datenobjekt, und alle Bälle werden bei jedem Tick auf das Canvas gezeichnet (Listing 2).
class BallDrawing { private final Color fill = Color.color(Math.random(), Math.random(), Math.random()); private final int size = r.nextInt(1, 10); private double x = r.nextInt(APP_WIDTH); private double y = r.nextInt(APP_HEIGHT - TOP_OFFSET); private double dx = r.nextInt(1, 5); private double dy = r.nextInt(1, 5); public void move() { if (hitRightOrLeftEdge()) { dx *= -1; // Ball hit right or left } if (hitTopOrBottom()) { dy *= -1; // Ball hit top or bottom } x += dx; y += dy; } ... }
Verschieben der Objekte
Die Anwendung verwendet eine Timeline, um alle fünf Millisekunden weitere Objekte hinzuzufügen und sie zu verschieben (Listing 3).
Timeline timeline = new Timeline(new KeyFrame(Duration.millis(5), t -> onTick())); timeline.setCycleCount(Timeline.INDEFINITE); timeline.play(); private void onTick() { if (TYPE_OF_TEST == 1) { // Add ball nodes to the pane for (var i = 0; i < ADD_BALLS_PER_TICK; i++) { paneBalls.getChildren().add(new BallNode()); } // Move all the balls in the pane for (Node ballNode : paneBalls.getChildren()) { ((BallNode) ballNode).move(); } } else if (TYPE_OF_TEST == 2) { // Add balls to the list of balls to be drawn for (var i = 0; i < ADD_BALLS_PER_TICK; i++) { ballDrawings.add(new BallDrawing()); } // Clear the canvas (remove all the previously balls that were drawn) context.clearRect(0.0, 0.0, canvas.getWidth(), canvas.getHeight()); // Move all the balls in the list, and draw them on the Canvas for (BallDrawing ballDrawing : ballDrawings) { ballDrawing.move(); context.setFill(ballDrawing.getFill()); context.fillOval(ballDrawing.getX(), ballDrawing.getY(), ballDrawing.getSize(), ballDrawing.getSize()); } } }
Ausführen der Anwendung
Zum Ausführen der Anwendung habe ich folgenden Ansatz gewählt:
- den Code in einer Datei FxNodesVersusCanvas.java speichern
- eine Java-Laufzeitumgebung mit JavaFX installieren, z. B. von Azul Zulu [3] oder mit SDKMAN [4]: sdk install java 22.0.1.fx-zulu
- JBang installieren, entweder von [5] oder mit SDKMAN: sdk install jbang
- die Anwendung starten mit: jbang FxNodesVersusCanvas.java
Leistung im Vergleich
Natürlich hängt die Leistung vom System ab, auf dem Sie die Anwendung ausführen. Wie Sie im Video unter [6] und in Abbildung 2 sehen können, habe ich es sowohl auf einem Apple Mac Studio als auch auf einem Raspberry Pi 5 ausgeführt. Das Ergebnis ist konsistent, da man ungefähr zehnmal mehr Objekte zum Canvas verglichen mit der Anzahl der Nodes hinzufügen kann, bevor die Framerate einbricht. Das ist kein „wissenschaftliches Ergebnis“, aber es vermittelt einen guten Eindruck davon, was mit Canvas erreicht werden kann.
- Raspberry Pi wird bei 3k Nodes deutlich langsamer als bei 30k Nodes auf Canvas
- Mac wird bei 15k Nodes langsamer als bei 150k auf Canvas
Abb. 2: Das laufende Experiment
Fazit
Eine große Anzahl visueller Komponenten in einer typischen JavaFX-Benutzeroberfläche würde eine schlecht gestaltete Anwendung darstellen. Stellen Sie sich ein langes Registrierungsformular mit Hunderten von Eingabefeldern und Beschriftungen vor … Das würde Ihre Benutzer in den Wahnsinn treiben. Aber in anderen Fällen, in denen Sie eine komplexe Animation oder eine fortgeschrittene Benutzerschnittstellenkomponente erzeugen wollen, ist die Möglichkeit, auf dem Canvas zu zeichnen, ein idealer Ansatz.
Links & Literatur
[1] https://gluonhq.com/products/javafx/
[2] https://gist.github.com/FDelporte/c74cdf59ecd9ef1b14df86e08faa0c56
[3] https://www.azul.com/downloads/?package=jdk-fx#zulu
[6] https://www.youtube.com/watch?v=nJGRW5xP_AE
[7] https://leanpub.com/gettingstartedwithjavaontheraspberrypi/
[8] https://www.elektor.com/getting-started-with-java-on-the-raspberry-pi