ui-javafx.html 40.2 KB
Newer Older
1

Blanke, Daniela's avatar
Blanke, Daniela committed
2
3
<p><a href="https://openjfx.io/">JavaFX</a> ist ein auf Java basierendes UI-Framework insbesondere für Desktop-Anwendungen. 
Es kann auch zur Entwicklung mobiler Anwendungen für spezifische eingebettete Systeme und für die sehr verbreiteten Zielplattformen Android und iOS eingesetzt werden. 
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Als Laufzeitumgebung bietet sich für diesen Fall die <a href="https://gluonhq.com/products/mobile/vm/">Gluon VM</a> an.</p>

<p>JavaFX war seit Version 7 (2012) bis inkl. Version 10 (2018) Teil der <a href="https://www.oracle.com/technetwork/java/javase/downloads/">Java SE</a> und damit das standardmäßig empfohlene UI-Framework. 
Im Zuge der Modularisierung und Verschlankung der Java SE ist auch JavaFX seit Java 11 nicht mehr Teil des JDK und muss als separate Bibliothek eingebunden werden.
Oracle erläutert diese Entscheidung in einem <a href="https://blogs.oracle.com/java-platform-group/the-future-of-javafx-and-other-java-client-roadmap-updates">Blog-Artikel</a>.
Die Weiterentwicklung von JavaFX erfolgt heute über die Open Source-Community <a href="https://openjfx.io/">OpenJFX</a>.
Da OpenJFX unter der <a href="https://de.wikipedia.org/wiki/GNU_General_Public_License">GPL</a> veröffentlicht ist, lassen sich mittels OpenJFX auf Basis des <a href="https://openjdk.java.net/">OpenJDK</a> nun freie, nicht durch Lizenzen eingeschränkte Desktop-Anwendungen mit Java konstruieren, was nicht nur für freie Linux-Distributionen erfreulich ist.
In der Dokumentation zu OpenJFX finden sich ausführliche und bebilderte <a href="https://openjfx.io/openjfx-docs/">Installationsanleitungen</a> für verschiedene Entwicklungsumgebungen und Build-Management-Tools.</p>

<p>JavaFX stellt eine Vielzahl von grundlegenden Steuerungselementen (sogenannte <a href="https://docs.oracle.com/javase/8/javafx/user-interface-tutorial/ui_controls.htm"><i>UI Controls</i></a>) zur Interaktion mit dem Anwender bereit.
Dazu zählen <i>Button</i>, <i>Text Field</i>, <i>Radio Button</i>, <i>Checkbox</i>, <i>List View</i>, <i>Table View</i>, <i>Slider</i>, <i>Progress Bar</i> usw.
Die Darstellung der Steuerungselemente kann individuell über ein <i>Look and Feel-Theme</i> angepasst werden – z.B. mittels <a href="https://github.com/jfoenixadmin/JFoenix">JFoenix</a> an das <a href="https://material.io/design/">Material Design</a> von Google.
Zum Einstieg bietet sich der grafische Editor <a href="https://gluonhq.com/products/scene-builder/">SceneBuilder</a> an, der es erlaubt das UI per Drag & Drop zusammenzustellen.
SceneBuilder lässt sich in geläufige Java-Entwicklungsumgebungen wie IntelliJ oder Eclipse integrieren, um schnell zwischen der grafischen Editoransicht und dem generierten deklarativen Code wechseln zu können.
Für JavaFX-Anfänger bietet der SceneBuilder-Editor die Möglichkeit die vorhandenen Steuerungselemente und ihre anpassbaren Eigenschaften explorativ zu erkunden und zu beobachten, welcher Code durch die Zusammenstellung per Drag & Drop entsteht. Weiter unten folgt ein Video, das eine Einführung in den SceneBuilder-Editor bietet.
</p>

<h4>Trennung von Präsentation und Steuerung einer View</h4>

<p>Bevor wir die Motivation hinter der Trennung von Präsentation und Steuerungslogik in einem UI erläutern, klären wir kurz den allgemeinen Begriff "View":
Als eine View bezeichnen wir hier den Ausschnitt des gesamten UI, der dem Anwender aktuell angezeigt wird.
Blanke, Daniela's avatar
Blanke, Daniela committed
25
Das gesamte UI besteht also aus mehreren Views, zwischen deren Anzeige der Anwender i.d.R. durch eigene Interaktion, wie das Anklicken eines Buttons, navigiert.
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
Die möglichen Transitionen zwischen den Views einer Anwendung können als Zustandsautomat modelliert werden.
Je nach UI-Framework werden die Views unterschiedlich bezeichnet: In der Web-Entwicklung wird klassischerweise von Seiten (engl. <i>Pages</i>) oder in neueren Web-Frameworks wie <a href="https://angular.io/">Angular</a>, <a href="https://vuejs.org/">Vue.js</a> oder <a href="https://reactjs.org/">React</a> generisch von Komponenten (engl. <i>Components</i>) gesprochen. Im Android SDK navigiert der Anwender zwischen unterschiedlichen <i><a href="https://developer.android.com/guide/components/activities/activity-lifecycle">Activities</a></i> (oder ihren Fragmenten). 
In JavaFX wurde zur Bezeichnung der Views eine Analogie zur Theaterbühne (engl. <i>Stage</i>) gewählt, auf der mehrere Szenen (engl. <i>Scenes</i>) mit unterschiedlichem Bühnenbild ablaufen.
Das Fenster, in dem eine Anwendung dargestellt wird, ist dabei die <code><a href="https://openjfx.io/javadoc/12/javafx.graphics/javafx/stage/Stage.html">Stage</a></code>. Der Anwender kann durch Interaktion zwischen unterschiedlichen <code><a href="https://openjfx.io/javadoc/12/javafx.graphics/javafx/scene/Scene.html">Scenes</a></code> wechseln.</p>

<pre><code class="language-java line-numbers">import javafx.application.Application;
import javafx.stage.Stage;
    
public class HelloWorldApp extends Application {

    public static void main(String[] args) {
        launch(args);
    }
    
    public void start(Stage stage) {
        stage.setTitle("Hello World!");
        stage.show();
    }
}</code></pre>

<img src="media/javafx_helloworld.png" style="width:460px">
<label>Minimale JavaFX-Anwendung mit leerer Stage</label>

Blanke, Daniela's avatar
Blanke, Daniela committed
49
<p>Der Inhalt einer <code>Scene</code> besteht in JavaFX aus mehreren Knoten (die entsprechende Klasse heißt <code><a href="https://openjfx.io/javadoc/12/javafx.graphics/javafx/scene/Node.html">Node</a></code>), die in einer Baumstruktur ineinander verschachtelt sind. Diese Baumstruktur wird in JavaFX als <i>Scene Graph</i> bezeichnet und entspricht konzeptionell dem <i>Document Object Model (DOM)</i> in der Web-Entwicklung. Container-Knoten, die mehrere andere Knoten als Kindelemente aufnehmen können, werden als <code><a href="https://openjfx.io/javadoc/12/javafx.graphics/javafx/scene/Parent.html">Parent</a></code> bezeichnet. Jede <code>Scene</code> hat einen Wurzelknoten, den <i>Root Parent Node</i>. Eine beliebig tief geschachtelte Baumstruktur entsteht dadurch, dass ein Container-Knoten auch andere Container-Knoten als Kindelemente aufnehmen kann. Grafische Animationen, wie z.B. Verschiebungen, Rotationen und Skalierungen oder andere Effekte, die auf einen <code>Parent</code> angewendet werden, gelten auch für ihre untergeordneten Kindelemente. Konkrete Knoten, d.h. die Unterklassen der allgemeinen Klasse <code>Node</code> sind sämtliche Klassen für Steuerungselemente, Bilder oder Texte, die im UI angezeigt werden. Die folgende Abbildung und das folgende Code-Beispiel verdeutlichen den Aufbau eines <i>Scene Graph</i> in JavaFX.</p>
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165

<img src="media/javafx_nodes.png" style="width:400px">
<label>Scene Graph in JavaFX</label>

<pre><code class="language-java line-numbers">HBox rootNode = new HBox(); // HBox is a concrete Parent subclass
Scene scene = new Scene(rootNode);

Node node = new Button("Button 1"); // Button is a concrete Node subclass
rootNode.getChildren().add(node);

stage.setScene(scene);
stage.show();</code></pre>

<p>Wie bei den anderen oben genannten UI-Frameworks aus dem Web- und Mobile-Bereich hat es sich auch in JavaFX bewährt, die Präsentation einer View von ihrer Steuerungslogik zu trennen. In nahezu alle modernen UI-Frameworks wird aktuell der Ansatz verfolgt, die grundlegende Präsentation einer View als Baumstruktur deklarativ in XML oder HTML zu formulieren und die zugehörige Steuerungslogik in einer imperativen Programmiersprache wie Java. Vor- und Nachteile dieses Ansatzes sollen kurz zusammengefasst werden:</p>
<ul>
    <li><b>Deklarative Programmierung der Präsentation:</b> In einer separaten Ressourcen-Datei wird die grundlegende Struktur einer View in einer deklarativen Auszeichnungssprache wie XML oder HTML gespeichert. Bei JavaFX ist dies <a href="https://docs.oracle.com/javase/8/javafx/fxml-tutorial/why_use_fxml.htm">FXML</a>. Derartige Auszeichnungssprachen sind sehr gut geeignet, um Baumstrukturen übersichtlich abzubilden. Das Framework ist in der Lage eine valide Ressourcen-Datei zu verarbeiten und für alle Knoten der Baumstruktur ein entsprechendes <code>Node</code>-Objekt zu erzeugen. Die einzelnen Einträge in Listen und Tabellen können zur Entwurfszeit natürlich nicht deklarativ angelegt werden, da sie erst dynamisch zur Laufzeit der Anwendung über einen Webservice o.ä. geladen werden.</li>    
    <li><b>Imperative Programmierung der Steuerungslogik:</b> Die Steuerung der View wird in einer imperativen Programmiersprache wie Java implementiert und beinhaltet insbesondere die Verarbeitung von sämtlichen Ereignissen, die zur Laufzeit auftreten können – wie z.B. die Navigation zur nächsten View nach einem Klickereignis und das damit verbundene dynamische Nachladen von Daten. Würde zusätzlich die grundlegende Struktur beim initialen Laden der Views imperativ zusammengestellt werden, entsteht i.d.R. sehr viel unleserlicher Code zum Aufbau der UI-Elemente, zum Setzen ihrer Eigenschaften und zum Positionieren im jeweiligen Container-Element (= Vaterknoten in XML). Änderungen an diesem Code sind aufwändig, da auch für kleine Verschiebungen von UI-Elementen viel Code bewegt wird. In der Versionsverwaltung sind die Effekte der Änderungen schwer nachzuvollziehen. Wer einen Blick auf diesen Code wirft, wird schnell erkennen, dass er sich sehr gut zur automatisierten Generierung durch ein Framework anbietet.</li>
</ul>

<p>Ein weiterer Vorteil der klaren Trennung von Präsentation und Steuerung einer View in der Anwendungsentwicklung ist, dass die Rollenaufteilung zwischen spezialisierten Teammitgliedern vereinfacht wird: UI-Designer sind i.d.R. den Umgang mit deklarativen Auszeichnungssprachen wie HTML gewohnt und müssen sich nicht mehr mit der imperativen Programmiersprache auseinandersetzen. 
Im <a href="#unit-mvc" class="navigate">MVC-Kapitel</a> ist bereits vorgestellt worden, wie das MVC-Muster zum MVVM-Muster ausgebaut werden kann, um die Steuerungslogik der View (= <i>ViewModel</i> in MVVM) unabhängig von einer konkreten Präsentation (= <i>View</i> in MVVM) zu gestalten. Die Präsentation kann dann relativ einfach ausgetauscht werden, ohne die Steuerungslogik der View anpassen zu müssen – z.B. von FXML zu HTML oder umgekehrt. Im Kapitel <a href="#unit-mvc-mvvm-javafx" class="navigate">MVC und MVVM in JavaFX</a> ist bereits dargestellt worden, wie eine einfache View inkl. Controller bzw. ViewModel in JavaFX aufgebaut werden kann.</p>

<h4>Layout Panes</h4>
<p>Ein <a href="https://openjfx.io/javadoc/12/javafx.graphics/javafx/scene/layout/Pane.html"><code>Pane</code></a> ist in JavaFX ein bestimmter Container-Knoten, in dem mehrere andere Knoten vereint und nach einem bestimmten Layout-Prinzip im UI angeordnet werden. Die wichtigsten der vorgefertigten <i><a href="https://docs.oracle.com/javase/8/javafx/layout-tutorial/builtin_layouts.htm">Layout Panes</a></i> sollen im Folgenden kurz erläutert werden.</p>
<ul>
    <li><b>HBox: </b>Die Kindelemente werden horizontal nebeneinander dargestellt. Es kann ein Abstand zwischen den Kindelementen angegeben werden.</li>
    <li><b>VBox: </b>Die Kindelemente werden entsprechend vertikal untereinander dargestellt.</li>
    <li><b>TilePane: </b>Die Kindelemente werden auf gleich großen Kacheln dargestellt. Die Anzahl der gewünschten Zeilen und Spalten kann angegeben werden.</li>
    <li><b>GridPane: </b>Die Kindelemente werden auf einem Gitter dargestellt, das aus einer definierten Anzahl an Zeilen und Spalten besteht. Kindelemente können sich über mehrere Zeilen und/oder Spalten des Gitters erstrecken.</li>
    <li><b>FlowPane: </b>Die Kindelemente werden horizontal oder vertikal hintereinander dargestellt und können unterschiedlich breit und hoch sein. Wenn die Breite des Containers (bei horizontaler Flussrichtung) bzw. die Höhe des Containers (bei vertikaler Flussrichtung) ausgeschöpft ist, wird der Fluss der Kindelemente in der nächsten Zeile bzw. Spalte fortgesetzt.</li>
    <li><b>BorderPane: </b>Die Kindelemente können in 5 definierten Regionen (<i>Center</i>, <i>Top</i>, <i>Left</i>, <i>Bottom</i>, <i>Right</i>) des Containers positioniert werden. Die Regionen dürfen auch leer bleiben.</li>
    <li><b>AnchorPane: </b>Die Kindelemente können an den Kanten und den Ecken des Containers mit fixen Koordinaten ausgerichtet werden.</li>
    <li><b>StackPane: </b>Die Kindelemente können gezielt übereinander gestapelt werden, wenn sie sich überlappen sollen, um Tiefe als dritte Dimension anzudeuten.</li>
    <li><b>TitledPane: </b>Der Container besteht aus einem Titel- und einem Inhaltsbereich. Der Inhaltsbereich kann mit einem Klick auf den Titel zugeklappt werden.</li>
    <li><b>TabPane: </b>Der Container besteht aus mehreren Tabs, von denen jeweils nur eines sichtbar ist.</li>
    <li><b>ScrollPane: </b>Der Container ist horizontal oder vertikal scrollbar und kapselt i.d.R. einen weiteren Container.</li>
    <li><b>SplitPane: </b>Der Container ist horizontal oder vertikal in mehrere Bereiche unterteilt, deren Breite bzw. Höhe der Anwender ggf. interaktiv verändern kann.</li>
</ul>
<img src="media/fx_layout_panes.png" style="width:700px">
<label>Layout Panes in JavaFX</label>

<h4>Navigation zwischen Views</h4>
<p>Die verschiedenen Views einer Anwendung teilen sich häufig bestimmte inhaltliche Bereiche wie einen Header, einen Footer oder eine Sidebar. Das Navigationsmenu selbst ist i.d.R. in einem dieser Bereiche untergebracht. Es ergeben sich zwei alternative Ansätze, um die Navigation zwischen den Views einer Anwendung zu realisieren, ohne den Code für die geteilten Bereiche zu duplizieren:</p>
<ul>
    <li><b><i>Multiple scenes</i>:</b> Alle Views sind strukturell gleich aufgebaut und inkludieren die geteilten Bereiche. Im folgenden Code-Beispiel werden der Header und die Sidebar aus separaten Dateien eingebunden. Weitere Views der Anwendung sind gleich aufgebaut wie die folgend dargestellte FXML-Datei <code>myFirstView.fxml</code> und unterscheiden sich nur im inhaltlichen Bereich. Nachteil: Wenn die allgemeine Struktur angepasst werden muss (z.B. Verschieben der Sidebar von links nach rechts), müssen alle Views einzeln editiert werden. 
        <script type="text/plain" class="language-xml line-numbers"><BorderPane fx:id="myFirstView">
    <top>
        <fx:include source="header.fxml"/>
    </top>
    <left>
        <fx:include source="sidebar.fxml"/>
    </left>
    <center>
        <!-- specific content of this view  -->
    </center>
</BorderPane></script>    
    Vorteil in JavaFX: Jede View kann über einen eindeutigen Dateinamen in einer neuen Szene geladen werden. 
        <pre><code class="language-java line-numbers">Scene scene = new Scene(FXMLLoader.load(getClass().getResource("myFirstView.fxml")));
stage.setScene(scene);</code></pre>    
    </li>
    <li><b><i>Single scene, dynamic content</i>:</b> Es wird eine FXML-Datei (z.B. namens <code>main.fxml</code>) angelegt, die den strukturellen Rahmen inkl. Header und Sidebar für alle Views definiert.
        <script type="text/plain" class="language-xml line-numbers"><BorderPane fx:id="main" fx:controller="MainController">
    <top>
        <fx:include source="header.fxml"/>
    </top>
    <left>
        <fx:include source="sidebar.fxml"/>
    </left>
    <center>
        <StackPane fx:id="viewHolder">
            <!-- specific content of the active view is loaded here -->
        </StackPane>
    </center>
</BorderPane></script> 
    Diese Datei wird initial in einer Szene geladen. Anstatt zur Laufzeit zu neuen Szenen zu wechseln, wird dynamisch der spezifische Inhalt einer View in ein dafür vorgesehenes <i>Layout Pane</i> (hier <code>StackPane viewHolder</code>) geladen.    
    <pre><code class="language-java line-numbers">public class MainController {

    @FXML
    private StackPane viewHolder;

    public void changeView(String fxmlFilename) {
        Node view = FXMLLoader.load(getClass().getResource(fxmlFilename));
        viewHolder.getChildren().setAll(view); // clears the list of child elements and adds the view as a new child element
    }
}</code></pre>       
    </li>
</ul>

<h4>Beispielanwendung "Yacht Shop"</h4>
<p>In folgendem Video wird eine JavaFX-Beispielanwendung vorgestellt, die mit der im <a href="#unit-spring" class="navigate">Spring-Kapitel</a> erstellten REST-API kommuniziert. Spezifische JavaFX-UI-Elemente werden im Video hervorgehoben. Der vollständige Code der Beispielanwendung findet sich im Verzeichnis <a href="/ui/javafx-shop" class="repo-link">/ui/javafx-shop</a> des Modul-Repository.</p>

<video controls><source src="media/JavaFX_Yacht_Shop.mp4" type="video/mp4"></video>
<label>JavaFX-Beispielanwendung "Yacht Shop"</label>

Die Anwendung folgt dem Ansatz <i>"Single scene, dynamic content"</i> und lädt dynamisch 3 unterschiedliche Views in Form von FXML-Dateien in den Inhaltsbereich eines <i>ViewHolder-Pane</i>. Die 3 Views bilden folgende Anwendungsfälle ab:
<ul>
    <li><code>login.fxml</code>
        <ul>
            <li>Anmelden als registrierter User</li>
        </ul>
    </li>
    <li><code>catalog.fxml</code>
        <ul>
            <li>Einsehen des Produktkatalogs</li>
            <li>Buchen eines Produkts</li>
        </ul>
    </li>
    <li><code>bookings.fxml</code>
        <ul>
            <li>Einsehen aller Buchungen des angemeldeten Users in einer Tabelle</li>
            <li>Löschen einer Buchung des angemeldeten Users</li>
        </ul>
    </li>        
</ul>
Zusätzlich besteht die Anwendung noch aus 2 weiteren FXML-Dateien:
<ul>
Blanke, Daniela's avatar
Blanke, Daniela committed
166
    <li><code>main.fxml</code>: Enthält die gemeinsame Rahmenstruktur aller inhaltlichen Views, insbesondere Header und Navigationsmenu.</li>
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
    <li><code>menu.fxml</code>: Enthält das Navigationsmenu, das in einem <i>MenuDrawer</i> dargestellt wird. Über das Menu kann zwischen den Views gewechselt werden.</li>
</ul> 
Im Folgenden wird der Code auszugsweise und teilweise vereinfacht anhand ausgewählter Aspekte erläutert.

<h4>FXML, Controller und CSS in JavaFX</h4>
<p>Das folgende Video zeigt, wie das Anmeldeformular einer Anwendung mittels des SceneBuilder-Editors ohne Programmierkenntnisse entworfen werden kann. Als Container werden im Video exemplarisch nur eine <code>VBox</code> und eine <code>HBox</code> verwendet, die verschiedene UI-Steuerungselemente aufnehmen und ineinander geschachtelt werden.</p>

<video controls><source src="media/SceneBuilder.mp4" type="video/mp4"></video>
<label>FXML-Entwurf im SceneBuilder-Editor</label>

<p>Die Ausgabe des Entwurfsvorgangs im SceneBuilder ist eine FXML-Datei, hier namens <code>login.fxml</code>, die noch nicht an einen Controller gebunden ist. Der Login-Button hat dadurch noch keine Funktionalität. In dem folgenden Code-Beispiel wird dem Anmeldeformular durch die Referenz auf die Klasse <code>LoginController</code> in Zeile 1 ein Controller zugeordnet, der automatisch durch das JavaFX-Framework instantiiert wird. Die Methode <code>initialize</code> versehen mit der Annotation <code>@FXML</code> (Zeilen 12-15 in <code>LoginController</code>) wird beim Erzeugen des Controllers durch das Framework ausgeführt. In Zeile 1 der FXML-Datei wird zusätzlich die CSS-Datei <code>app.css</code> eingebunden, so dass alle UI-Elemente die dort definierten Stylesheet-Klassen über das Attributs <code>styleClass</code> adressieren können.</p>

<ul class="nav nav-tabs" id="javafx1-tabs" role="tablist">
    <li class="nav-item"><a href="#javafx1-tabs-fxml" class="nav-link active" data-toggle="tab" role="tab">FXML (login.fxml)</a></li>
    <li class="nav-item"><a href="#javafx1-tabs-java" class="nav-link" data-toggle="tab" role="tab">Controller (LoginController.java)</a></li>
    <li class="nav-item"><a href="#javafx1-tabs-css" class="nav-link" data-toggle="tab" role="tab">CSS (app.css)</a></li>
</ul>
<div class="tab-content" id="javafx1-tabs-content">
    <div class="tab-pane show active" id="javafx1-tabs-fxml" role="tabpanel">
        <pre><code class="language-xml line-numbers"><!-- <VBox xmlns="http://javafx.com/javafx/10.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="shop.controller.LoginController" stylesheets="@app.css">
    <HBox>
        <MaterialIconView glyphName="LOCK"></MaterialIconView>
        <Label>Anmelden</Label>
    </HBox>
    <JFXTextField fx:id="username" promptText="Username" styleClass="input"/>
    <JFXPasswordField fx:id="password" promptText="Passwort" styleClass="input"/>
    <JFXButton onAction="#loginAction" text="LOGIN" styleClass="button-raised"></JFXButton>
    <Label fx:id="loginFailure" visible="false">Login fehlgeschlagen!</Label>
</VBox> --></code></pre> 
    </div>
    <div class="tab-pane" id="javafx1-tabs-java" role="tabpanel">
        <pre><code class="language-java line-numbers">public class LoginController {

    @FXML
    private TextField username;

    @FXML
    private PasswordField password;

    @FXML
    private Label loginFailure;
    
    @FXML
    public void initialize() {
        // executed when the controller object is created by the framework
    }
    
    public void loginAction(ActionEvent e1) {
        // execute login request in async task
        PostLoginTask loginTask = new PostLoginTask(username.getText(), password.getText());
        new Thread(loginTask).start();

        // login task response handler
        loginTask.setOnSucceeded((WorkerStateEvent e2) -> {
            // login is successfull if the return value is not null
            if (loginTask.getValue() == null) {
                loginFailure.setVisible(true);
                return;
            }
            // navigate to catalog view
            MainController.getInstance().changeView("catalog");
        });
    }
}</code></pre> 
    </div>
    <div class="tab-pane" id="javafx1-tabs-css" role="tabpanel">
        <pre><code class="language-css line-numbers">.button-raised {
    -jfx-button-type: RAISED;
    -fx-background-color: deeppink;
    -fx-text-fill: white;
    -fx-font-size: 14; 
    -fx-padding: 8 20;       
}

.input {
    -fx-max-width: 200;
}

#loginFailure {
    -fx-text-fill: red;
    -fx-font-size: 16;
}</code></pre> 
    </div>
</div>

<span>Die UI-Elemente aus der FXML-Datei können an Attribute in der Controller-Klasse gebunden werden (<i>Two-Way-Binding</i>). Auch diese Attribute werden per Dependency Injection durch das Framework instantiiert. Dazu muss ...</span>
<ul>
Blanke, Daniela's avatar
Blanke, Daniela committed
254
    <li>der Bezeichner des Attributs in der Java-Klasse gleich dem Wert des Attributs <code>fx:id</code> in der FXML-Datei sein,</li>
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
    <li>der Datentyp des Attributs in der Java-Klasse gleich dem Datentyp in der FXML-Datei sein</li>
    <li>und das Attribut in der Java-Klasse mit der Annotation <code>@FXML</code> versehen sein.</li>
</ul>

<p>Die Methode <code>loginAction</code> (Zeilen 17-32 in <code>LoginController</code>) ist durch das Attribut <code>onAction="#loginAction"</code> in der FXML-Deklaration des Login-Buttons als <i>EventHandler</i> an diesen Button gebunden. Die Implementierung dieser Methode, inbesondere die Klasse <code>PostLoginTask</code>, wird weiter unten genauer erläutert.</p>

<p>Das Styling der UI-Elemente erfolgt in JavaFX wie aus der Web-Entwicklung bekannt über <a href="https://de.wikipedia.org/wiki/Cascading_Style_Sheets"><i>Cascading Style Sheets (CSS)</i></a>. Es ist demzufolge zu empfehlen, dass eine FXML-Datei nur die strukturelle Anordnung der UI-Elemente definiert und deren Styling über wiederverwendbare Stylesheet-Klassen in eine separate CSS-Datei ausgelagert wird. Eine Styling-Klasse kann über das Attribut <code>styleClass</code> adressiert werden. Über das Attribut <code>style</code> können die CSS-Properties eines UI-Elements genau wie in HTML direkt verändert werden. Eine Referenz über alle verfügbaren <a href="https://docs.oracle.com/javase/8/javafx/api/javafx/scene/doc-files/cssref.html">CSS-Properties in JavaFX</a> findet sich in der Dokumentation von Oracle.</p>

<p>Das folgende Code-Beispiel zeigt exemplarisch, wie ein Button alternativ aus dem imperativen Java-Code heraus einer View hinzugefügt werden kann. Das ist insbesondere für die Kindelemente von Listen (<a href="https://openjfx.io/javadoc/12/javafx.controls/javafx/scene/control/ListView.html"><code>ListView</code></a>) und Tabellen (<a href="https://openjfx.io/javadoc/12/javafx.controls/javafx/scene/control/TableView.html"><code>TableView</code></a>) sinnvoll, die zur Laufzeit dynamisch angelegt werden müssen, weil ihre Ausprägungen zur Entwurfszeit noch nicht bekannt sind.</p>

<pre><code class="language-java line-numbers">class LoginController {

    @FXML
    private Pane container;

    @FXML
    protected void initialize() {
        Button loginButton = new Button("LOGIN");
        container.getChildren().add(loginButton); // add button to a parent container

        // register button event handler
        loginButton.setOnAction((e) -> {
            // process login action here ...
        });

        // button styling
        loginButton.getStyleClass().add("button-raised");
        loginButton.setStyle("-fx-background-color: deeppink");
    }
}</code></pre>
<ul>
    <li>In Zeile 9 wird der neue Button dem in der zugehörigen FXML-Datei deklarierten <i>UI-Pane</i> <code>container</code> hinzugefügt.</li>
    <li>In den Zeilen 12-14 wird ein anonymer EventHandler für den Button registriert.</li>
    <li>In den Zeilen 17-18 wird das Styling des Buttons mittels CSS angepasst.</li>
</ul>

<h4>Asynchrone Tasks in einer JavaFX-Anwendung</h4>
<p>In einer JavaFX-Anwendung wird das UI aus dem sogenannten <i>JavaFX Application Thread</i> heraus gesteuert. Nur aus diesem Thread heraus darf das UI zur Laufzeit manipuliert werden, z.B. durch das Hinzufügen von Knoten zum <i>Scene Graph</i>. Es können in diesem Thread also nur nicht-blockierende EventHandler ausgeführt werden, damit sichergestellt ist, dass das UI stets auf Eingaben des Anwenders reagieren kann. Blockierende Aufrufe (wie z.B. das Senden eines HTTP-Request an eine REST-API) hingegen sind in nebenläufige Threads zu verschieben. Aus Sicht des <i>JavaFX Application Thread</i> wird in diesem Fall eine asynchrone Aufgabe (engl. <i>Task</i>) gestartet, deren Ergebnis – sobald es verfügbar ist – über eine Callback-Funktion verarbeitet wird, um das UI ggf. zu aktualisieren. Es bietet sich für derartige asynchrone Aufgaben in Java an, die Klasse <a href="https://openjfx.io/javadoc/12/javafx.graphics/javafx/concurrent/Task.html"><code>Task</code></a> zu erweitern. Diese Klasse implementiert die Interfaces <code>Future</code> und <code>Runnable</code> (s. Kapitel <a href="#unit-streams" class="navigate">Futures und parallele Streams</a>). Das folgende Code-Beispiel zeigt, wie das Senden eines HTTP-Request an eine REST-API und die Verarbeitung der zugehörigen HTTP-Response in eine spezielle, nebenläufig ausgeführte Task ausgelagert wird. In diesem Fall wird der HTTP-Request durch das Klicken auf den Login-Button im <i>JavaFX Application Thread</i> ausgelöst. Serverseitig soll geprüft werden, ob der Anwender eine gültige Kombination aus Username und Passwort eingegeben hat, und der Loginversuch damit erfolgreich war oder eben nicht.</p>

<ul class="nav nav-tabs" id="javafx2-tabs" role="tablist">
    <li class="nav-item"><a href="#javafx2-tabs-fxml" class="nav-link active" data-toggle="tab" role="tab">JavaFX Application Thread</a></li>
    <li class="nav-item"><a href="#javafx2-tabs-java" class="nav-link" data-toggle="tab" role="tab">Nebenläufiger Task</a></li>
</ul>
<div class="tab-content" id="javafx2-tabs-content">
    <div class="tab-pane show active" id="javafx2-tabs-fxml" role="tabpanel">
        <pre><code class="language-java line-numbers">// the following code is executed in the event handler of the login button

PostLoginTask loginTask = new PostLoginTask(username, password);
new Thread(loginTask).start();

// login task response handler
loginTask.setOnSucceeded((WorkerStateEvent e) -> {
    User user = loginTask.getValue();
    if (user != null) {
        // login is successfull
    }
    else {
        // login is not successfull
    }
});</code></pre> 
    </div>
    <div class="tab-pane" id="javafx2-tabs-java" role="tabpanel">
        <pre><code class="language-java line-numbers">import javafx.concurrent.Task;
import com.mashape.unirest.http.Unirest;
import com.mashape.unirest.http.HttpResponse;
// ...

public class PostLoginTask extends Task&lt;User> {

    private String username;
    private String password;

    public PostLoginTask(String username, String password) { this.username = username; this.password = password; }

	@Override
	protected User call() {
		String url = "https://example.com/login";
		String json = "{\"name\": \"" + username + "\", \"passwordHash\": \"" + password + "\"}";

		try {
			HttpResponse&lt;String> res = Unirest.post(url).header("Content-Type", "application/json").body(json).asString();
			if (res.getStatus() != 403) {
				String jsonWebToken = res.getHeaders().getFirst("Authorization");
				return new User(username, jsonWebToken);
			}
		} catch (UnirestException e) {
			e.printStackTrace();
		}
		return null;
	}
}</code></pre> 
    </div>
</div>

<ul>
    <li>In Zeile 4 des <i>JavaFX Application Thread</i> wird der Task nebenläufig gestartet. In Zeile 7 wird mittels der Methode <code>setOnSucceeded</code> ein EventHandler registriert, der ausgeführt wird, sobald der Task abgeschlossen ist. In diesem EventHandler kann über die Methode <code>getValue</code> auf das Ergebnis des Task zugegriffen werden.</li>
    <li>Die Klasse <code>Task&lt;V></code> ist eine generische Klasse, für die ein bestimmter Rückgabetyp <code>V</code> für die Methode <code>call</code> spezifiziert werden kann. Für die Klasse <code>PostLoginTask</code> ist dies im vorliegenden Beispiel die Datenstruktur-Klasse <code>User</code> (vgl. Zeilen 6 und 14).</li>
    <li>In Zeile 19 des Task wird ein blockierender HTTP-Request gesendet, der ggf. eine Exception wirft, wenn z.B. der Server nicht zu erreichen ist. Insbesondere wegen dieser Zeile wird überhaupt ein asynchroner Task angelegt. In den Zeilen 20-23 wird die HTTP-Response verarbeitet.</li>
    <li>Zum Senden des HTTP-Request wird hier exemplarisch die Bibliothek <a href="http://unirest.io/java.html">Unirest</a> verwendet. Diese würde alternativ auch das Senden eines nicht-blockierenden HTTP-Request ermöglichen, wenn am Ende von Zeile 19 anstatt der Methode <code>asString</code> die Methode <code>asStringAsync</code> aufgerufen wird. Die Rückgabe wäre dann vom Typ <code>Future&lt;HttpResponse&lt;String>></code>. Dadurch würde sich das Auslagern des blockierenden Aufrufs aus dem eigenen Code in den internen Code der Bibliothek verlagern. Ein Task wäre nicht mehr unbedingt erforderlich, falls die Vorbereitung des HTTP-Request und die Verarbeitung der HTTP-Response als nicht sonderlich aufwändig eingestuft werden und es tolerierbar ist, diese im <i>JavaFX Application Thread</i> auszuführen.</li>
Jens Ehlers's avatar
Jens Ehlers committed
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
</ul>

<h4>Animationen in JavaFX</h4>

<p>In einem Spiel bewegen sich häufig unterschiedliche Objekte über den Bildschirm, die miteinander im Rahmen der Spielregeln interagieren. In JavaFX wird zur Animation von Objekten die abstrakte Klasse <code><a href="https://openjfx.io/javadoc/11/javafx.graphics/javafx/animation/AnimationTimer.html">AnimationTimer</a></code> bereitgestellt. Mit dieser Klasse kann ein Timer erstellt werden, der in jedem Frame aufgerufen wird, während er aktiv ist. Die Frame-Rate ist variabel und hängt von der zugrundeliegenden Ausführungsumgebung und Hardware ab. Grundsätzlich ist es zu empfehlen, die Frame-Rate, in der das UI aktualisiert wird, und die Spielphysik voneinander zu entkoppeln (siehe Diskussion <a href="https://gamedev.stackexchange.com/questions/97933/framerate-is-affecting-speed-of-object">hier</a>). Zur Verwendung eines konkreten <code>AnimationTimer</code> muss die Methode <code>handle</code> sinnvoll überschrieben werden. Die Methoden <code>start</code> und <code>stop</code> erlauben das Starten bzw. Stoppen des Timers. Das folgende Code-Beispiel erzeugt eine Anwendung, in der ein animierter Ball mit fixer Geschwindigkeit an den Kanten des Anwendungsfensters abprallt (siehe <a href="/ui/javafx-animation/src/main/java/animation/SingleBouncingBall.java" class="repo-link">SingleBouncingBall.java</a>).</p>

<ul class="nav nav-tabs" id="javafx3-tabs" role="tablist">
    <li class="nav-item"><a href="#javafx3-tabs-bouncingball" class="nav-link active" data-toggle="tab" role="tab">SingleBouncingBallApp</a></li>
    <li class="nav-item"><a href="#javafx3-tabs-ball" class="nav-link" data-toggle="tab" role="tab">Ball</a></li>
</ul>
<div class="tab-content" id="javafx3-tabs-content">
    <div class="tab-pane show active" id="javafx3-tabs-bouncingball" role="tabpanel">
        <pre><code class="language-java line-numbers">import javafx.application.Application;
import javafx.animation.AnimationTimer;            
// ...

public class SingleBouncingBall extends Application {

    @Override
    public void start(Stage stage) {

        final Pane ballContainer = new Pane();
        final Scene scene = new Scene(ballContainer, 600, 400);
        final Ball ball = new Ball(300, 200, 25, Color.DARKGRAY, 300,  Math.toRadians(45));
        ballContainer.getChildren().add(ball);

        stage.setScene(scene);
        stage.show();

        final LongProperty lastUpdateTime = new SimpleLongProperty(0);
        final AnimationTimer timer = new AnimationTimer() {
            @Override
            public void handle(long now) {
                if (lastUpdateTime.get() > 0) {

                    // change the ball position
                    double elapsedSeconds = (now - lastUpdateTime.get()) / 1_000_000_000.;
                    ball.setCenterX(ball.getCenterX() + elapsedSeconds * ball.velocity * Math.cos(ball.angle));
                    ball.setCenterY(ball.getCenterY() + elapsedSeconds * ball.velocity * Math.sin(ball.angle));

                    // bounce against a vertical wall
                    if (ball.getCenterX() &lt;= ball.getRadius() 
                            || ball.getCenterX() >= (ballContainer.getWidth() - ball.getRadius())) {
                        ball.angle = Math.PI - ball.angle;
                    }
                    // bounce against a horizontal wall
                    if (ball.getCenterY() &lt;= ball.getRadius() 
                            || ball.getCenterY() >= (ballContainer.getHeight() - ball.getRadius())) {{
                        ball.angle = 2 * Math.PI - ball.angle;
                    }
                }
                lastUpdateTime.set(now);
            }
        };
        timer.start();
    }

    public static void main(String[] args) { launch(); }
}</code></pre> 
    </div>
    <div class="tab-pane" id="javafx3-tabs-ball" role="tabpanel">
        <pre><code class="language-java line-numbers">import javafx.scene.shape.Circle;

class Ball extends Circle {
    double velocity;
    double angle;

    Ball(double centerX, double centerY, double radius, Paint fill, double velocity, double angle) {
        super(centerX, centerY, radius, fill);
        this.velocity = velocity;
        this.angle = angle;
    }
}</code></pre> 
    </div>
</div>

<p>Die Klasse <code>Ball</code> erweitert die Klasse <a href="https://openjfx.io/javadoc/11/javafx.graphics/javafx/scene/shape/Circle.html"><code>javafx.scene.shape.Circle</code></a>. Ein <code>Circle</code> ist ein spezieller <a href="https://openjfx.io/javadoc/11/javafx.graphics/javafx/scene/shape/Shape.html"><code>Shape</code></a>, der als <code>Node</code> dem <i>Scene Graph</i> hinzugefügt werden kann. Weitere Shapes sind <code>Rectangle</code>, <code>Polygon</code>, <code>Text</code>, <code>SVGPath</code>, uvm. 
Alternativ zur Verwendung von Shapes können animierte Objekte in JavaFX auch auf einer <a href="https://openjfx.io/javadoc/11/javafx.graphics/javafx/scene/canvas/Canvas.html">Canvas</a> über deren <a href="https://openjfx.io/javadoc/11/javafx.graphics/javafx/scene/canvas/GraphicsContext.html"><code>GraphicsContext</code></a> gezeichnet werden. Die beiden folgenden Code-Beispiele visualisieren jeweils den Text "Hello World!" und einen roten Kreis – im ersten Code-Beispiel werden dazu Shapes angelegt, im zweiten Code-Beispiel wird auf eine Canvas gezeichnet.</p>

<pre><code class="language-java line-numbers">// display text and circle as shapes
Pane shapesContainer = new Pane();

Shape text = new Text(20, 20, "Hello World!"); // x = 20, y = 20
shapesContainer.getChildren().add(text);

Shape circle = new Circle(100, 100, 30, Color.RED); // centerX = 100, centerY = 100, radius = 30
shapesContainer.getChildren().add(circle);</code></pre>

<pre><code class="language-java line-numbers">// draw text and circle on canvas
Canvas canvas = new Canvas(300, 200); // canvas is 300 x 200 pixels
GraphicsContext gc = canvas.getGraphicsContext2D();

gc.fillText("Hello World!", 20, 20); // x = 20, y = 20

gc.setFill(Color.RED);
gc.fillOval(100, 100, 60, 60); // x = 100, y = 100, width = 60, height = 60 => radius = 30</code></pre>

<p>Die alternativen Implementierungen in den Klassen <a href="/ui/javafx-animation/src/main/java/animation/BouncingBallsShapes.java" class="repo-link">BouncingBallsShapes.java</a> und <a href="/ui/javafx-animation/src/main/java/animation/BouncingBallsCanvas.java" class="repo-link">BouncingBallsCanvas.java</a> lassen erkennen, dass im Vergleich von Shapes- und Canvas-Animationen sich keine signifikanten Unterschiede hinsichtlich der Frame-Rate und des Hauptspeicherbedarfs ergeben, aber die Shapes-Animationen eine etwas höhere CPU-Auslastung verursachen. Das folgende Video demonstriert diese Beobachtungen.</p>

<video controls><source src="media/Bouncing_Balls.mp4" type="video/mp4"></video>
<label>Animationen in JavaFX mittels Shapes und Canvas</label>

<h4>Simulation der Spielphysik</h4>

<p>Die Spielphysik kann – wie in den obigen Beispielen – durch stark vereinfachte Annahmen bei der <a href="https://de.wikipedia.org/wiki/Sto%C3%9F_(Physik)">Kollision von Objekten</a> eigenständig implementiert werden. Wenn in der Spielphysik allerdings auch spezielle Effekte und Kräfte wie Reibung, Dichte oder Rotationsgeschwindigkeit berücksichtigt werden sollen, bietet sich die Verwendung einer Physik-Engine wie z.B. <a href="http://www.dyn4j.org/">dyn4j</a> an. Die Physik-Engine ist unabhängig von JavaFX und kann gleichermaßen mit jedem anderen UI-Framework verwendet werden.</p>

<p>In dyn4j wird eine 2D-Welt simuliert. Die Welt (Klasse <a href="http://docs.dyn4j.org/v3.3.0/org/dyn4j/dynamics/World.html"><code>World</code></a>) kann diverse Körper (Klasse <a href="http://docs.dyn4j.org/v3.3.0/org/dyn4j/dynamics/Body.html"><code>Body</code></a>) enthalten, die ihrerseits aus mehreren miteinander verbundenen Körperteilen (Klasse <code>BodyFixture</code>) bestehen (s. <a href="http://www.dyn4j.org/documentation/getting-started/">Getting Started zu dyn4j</a>). Auf die Körper können Kräfte und Impulse wirken. Das folgende Code-Beispiel zeigt wie einer dyn4j-Welt ein rechteckiger und ein runder Körper hinzugefügt werden.</p>

<pre><code class="language-java line-numbers">World world = new World();

Body rect = new Body();
rect.addFixture(Geometry.createRectangle(width, height)); // 1st body fixture is a rectangle
rect.setMass(MassType.INFINITY);
body.translate(0, 0); // sets the rectangle's center (x, y)
world.addBody(rectangle);

Body circle = new Body();
circle.addFixture(Geometry.createCircle(radius), density, friction, restitution);
circle.setMass(MassType.NORMAL);
circle.translate(0, 10); // sets the circle's center (x, y)
world.addBody(circle);

world.setGravity(World.EARTH_GRAVITY); // the circle will fall down, the rectangle is fixed due to an infinite mass

circle.applyImpulse(new Vector2(x, y)); // applies an impulse on the circle in the direction of (x, y)</code></pre>

<p>Mittels der Methode <code>update</code> auf der Klasse <code>World</code> kann die Simulation der Welt in flexiblen zeitlichen Intervallen fortgeführt werden.</p>

<pre><code class="language-java line-numbers">ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(1);
scheduler.scheduleAtFixedRate(() -> {
    world.update(interval); // simulation of the world model is updated here
}, 0, interval, TimeUnit.MILLISECONDS);</code></pre>

<p>Das folgende Video zeigt eine Beispielanwendung, in der dyn4j zur Kollisionserkennung der animierten Objekte eingesetzt wird. Der Ball kann über die Pfeiltasten gesteuert werden. Durch Mausklicks können herabfallende Rechtecke erzeugt werden. Der vollständige Code der Beispielanwendung findet sich in der Klasse <a href="/ui/javafx-animation/src/main/java/animation/BouncingBallDyn4j.java" class="repo-link">BouncingBallDyn4j</a> im Verzeichnis <a href="/ui/javafx-animation" class="repo-link">/ui/javafx-animation</a> des Modul-Repository.</p>

<video controls><source src="media/Bouncing_Ball_dyn4j.mp4" type="video/mp4"></video>
<label>Spielphysik in JavaFX mittels dyn4j</label>