Mqtt added, changed MVC to MVVM hybrid architecture

This commit is contained in:
naumueller 2025-11-02 13:39:07 +01:00
parent b777e4c817
commit 411033e6b8
17 changed files with 354 additions and 97 deletions

View File

@ -26,6 +26,11 @@
<version>2.1.0</version> <version>2.1.0</version>
<type>jar</type> <type>jar</type>
</dependency> </dependency>
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>1.2.5</version>
</dependency>
</dependencies> </dependencies>
<build> <build>
<plugins> <plugins>

View File

@ -1,13 +1,13 @@
package efi.projekt.gesundheitsassistent; package efi.projekt.gesundheitsassistent;
import atlantafx.base.theme.NordDark; import atlantafx.base.theme.Dracula;
import efi.projekt.gesundheitsassistent.controller.MetadataDialogController; import efi.projekt.gesundheitsassistent.model.FxModel;
import efi.projekt.gesundheitsassistent.model.Metadata; import efi.projekt.gesundheitsassistent.controller.FxViewController;
import efi.projekt.gesundheitsassistent.viewmodel.FxViewModel;
import javafx.application.Application; import javafx.application.Application;
import javafx.fxml.FXMLLoader; import javafx.fxml.FXMLLoader;
import javafx.scene.Parent; import javafx.scene.Parent;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.stage.Modality;
import javafx.stage.Stage; import javafx.stage.Stage;
import java.io.IOException; import java.io.IOException;
@ -18,49 +18,30 @@ import java.io.IOException;
* @author naumueller * @author naumueller
*/ */
public class FxStart extends Application { public class FxStart extends Application {
private static Scene scene;
private Metadata metadata; // das zentrale Model
@Override @Override
public void start(Stage primaryStage) throws IOException { public void start(Stage primaryStage) throws IOException {
// Lade FXML
FXMLLoader loader = new FXMLLoader(getClass().getResource("/efi/projekt/gesundheitsassistent/view/FxView.fxml"));
Parent root = loader.load();
FxViewController controller = loader.getController();
// Erzeuge Model & ViewModel
FxModel model = new FxModel();
FxViewModel viewModel = new FxViewModel(model);
// Verbinde Controller mit ViewModel
controller.setViewModel(viewModel);
// 1 Metadata Model erzeugen Scene scene = new Scene(root, 1280, 720);
metadata = new Metadata(); Application.setUserAgentStylesheet(new Dracula().getUserAgentStylesheet());
// 2 Metadata Dialog laden
FXMLLoader dialogLoader = new FXMLLoader(getClass()
.getResource("/efi/projekt/gesundheitsassistent/view/MetadataDialog.fxml"));
Parent dialogRoot = dialogLoader.load();
// Controller bekommen und Model übergeben
MetadataDialogController dialogController = dialogLoader.getController();
dialogController.setMetadata(metadata);
// 3 Dialog Stage erzeugen
Stage dialogStage = new Stage();
dialogStage.setTitle("Metadaten eingeben");
dialogStage.initModality(Modality.APPLICATION_MODAL); // blockiert Hauptfenster
dialogStage.setScene(new Scene(dialogRoot));
dialogStage.showAndWait(); // wartet bis Dialog geschlossen wird
// 4 Hauptfenster laden
FXMLLoader mainLoader = new FXMLLoader(getClass()
.getResource("/efi/projekt/gesundheitsassistent/view/FxView.fxml"));
Parent mainRoot = mainLoader.load();
scene = new Scene(mainRoot, 1280, 720);
Application.setUserAgentStylesheet(new NordDark().getUserAgentStylesheet());
primaryStage.setTitle("Virtueller Gesundheitsassistent"); primaryStage.setTitle("Virtueller Gesundheitsassistent");
primaryStage.setScene(scene); primaryStage.setScene(scene);
primaryStage.show(); primaryStage.show();
} }
static void setRoot(String fxml) throws IOException {
scene.setRoot(loadFXML(fxml));
}
private static Parent loadFXML(String fxml) throws IOException { private static Parent loadFXML(String fxml) throws IOException {
FXMLLoader fxmlLoader = new FXMLLoader( FXMLLoader fxmlLoader = new FXMLLoader(
FxStart.class.getResource("/efi/projekt/gesundheitsassistent/view/" + fxml + ".fxml") FxStart.class.getResource("/efi/projekt/gesundheitsassistent/view/" + fxml + ".fxml")
@ -71,8 +52,4 @@ public class FxStart extends Application {
public static void main(String[] args) { public static void main(String[] args) {
launch(); launch();
} }
public Metadata getMetadata() {
return metadata;
}
} }

View File

@ -5,13 +5,23 @@
package efi.projekt.gesundheitsassistent.controller; package efi.projekt.gesundheitsassistent.controller;
import efi.projekt.gesundheitsassistent.model.FxModel; import efi.projekt.gesundheitsassistent.model.FxModel;
import efi.projekt.gesundheitsassistent.model.Metadata;
import efi.projekt.gesundheitsassistent.viewmodel.FxViewModel;
import efi.projekt.gesundheitsassistent.viewmodel.MetadataDialogViewModel;
import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.util.ResourceBundle; import java.util.ResourceBundle;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable; import javafx.fxml.Initializable;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Alert; import javafx.scene.control.Alert;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.ButtonType; import javafx.scene.control.ButtonType;
import javafx.scene.control.TextField;
import javafx.stage.Modality;
import javafx.stage.Stage;
/** /**
* FXML Controller class * FXML Controller class
@ -20,17 +30,27 @@ import javafx.scene.control.ButtonType;
*/ */
public class FxViewController implements Initializable { public class FxViewController implements Initializable {
private FxModel model; private FxViewModel viewModel;
@FXML @FXML private Button exitButton;
private Button exitButton; @FXML private TextField messageInputField;
@FXML private TextField messageOutputField;
@FXML private Button sendButton;
/**
* Initializes the controller class.
*/
@Override @Override
public void initialize(URL url, ResourceBundle rb) { public void initialize(URL url, ResourceBundle rb) {
exitButton.setOnAction(e -> handleExit());
}
public void setViewModel(FxViewModel viewModel) {
this.viewModel = viewModel;
initBindings();
}
public void initBindings() {
//Bindings: View <-> ViewModel
messageInputField.textProperty().bindBidirectional(viewModel.outgoingMessageProperty());
messageOutputField.textProperty().bindBidirectional(viewModel.incomingMessageProperty());
} }
@FXML @FXML
@ -38,11 +58,31 @@ public class FxViewController implements Initializable {
Alert alert = new Alert(Alert.AlertType.CONFIRMATION, "Programm wirklich beenden?"); Alert alert = new Alert(Alert.AlertType.CONFIRMATION, "Programm wirklich beenden?");
alert.showAndWait() alert.showAndWait()
.filter(response -> response == ButtonType.OK) .filter(response -> response == ButtonType.OK)
.ifPresent(response -> System.exit(0)); .ifPresent(response -> System.exit(0));
} }
@FXML
private void handleSendMessage() {
viewModel.sendMessage(messageInputField.getText());
}
@FXML
private void handleOptions() throws IOException {
// Dialog
FXMLLoader loader = new FXMLLoader(getClass()
.getResource("/efi/projekt/gesundheitsassistent/view/MetadataDialog.fxml"));
Parent dialogRoot = loader.load();
public void setModel(FxModel model) { // ViewModel erzeugen
this.model = model; Metadata metadata = new Metadata();
loader.<MetadataDialogController>getController().setViewModel(
new MetadataDialogViewModel(metadata)
);
// Dialog Stage erzeugen
Stage dialogStage = new Stage();
dialogStage.initModality(Modality.APPLICATION_MODAL);
dialogStage.setScene(new Scene(dialogRoot));
dialogStage.showAndWait();
} }
} }

View File

@ -5,12 +5,14 @@
package efi.projekt.gesundheitsassistent.controller; package efi.projekt.gesundheitsassistent.controller;
import efi.projekt.gesundheitsassistent.model.Metadata; import efi.projekt.gesundheitsassistent.model.Metadata;
import efi.projekt.gesundheitsassistent.viewmodel.MetadataDialogViewModel;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.Alert; import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType; import javafx.scene.control.ButtonType;
import javafx.scene.control.ChoiceBox; import javafx.scene.control.ChoiceBox;
import javafx.scene.control.TextField; import javafx.scene.control.TextField;
import javafx.stage.Stage; import javafx.stage.Stage;
import javafx.util.converter.NumberStringConverter;
/** /**
* *
@ -18,59 +20,39 @@ import javafx.stage.Stage;
*/ */
public class MetadataDialogController { public class MetadataDialogController {
@FXML @FXML private TextField nameField;
private TextField nameField; @FXML private TextField ageField;
@FXML private ChoiceBox<String> genderChoiceBox;
@FXML private TextField heightField;
@FXML private TextField weightField;
@FXML private MetadataDialogViewModel viewModel;
private TextField ageField;
@FXML public void setViewModel(MetadataDialogViewModel viewModel) {
private ChoiceBox<String> genderChoiceBox; this.viewModel = viewModel;
@FXML
private TextField heightField;
@FXML
private TextField weightField;
private Metadata metadata;
public void initialize() {
// ChoiceBox füllen
genderChoiceBox.getItems().addAll("Männlich", "Weiblich", "Divers"); genderChoiceBox.getItems().addAll("Männlich", "Weiblich", "Divers");
genderChoiceBox.setValue("Männlich"); // default
} // Bindings
nameField.textProperty().bindBidirectional(viewModel.nameProperty());
public void setMetadata(Metadata metadata) { ageField.textProperty().bindBidirectional(viewModel.ageProperty(), new NumberStringConverter());
this.metadata = metadata; genderChoiceBox.valueProperty().bindBidirectional(viewModel.genderProperty());
bindFields(); heightField.textProperty().bindBidirectional(viewModel.heightProperty(), new NumberStringConverter());
} weightField.textProperty().bindBidirectional(viewModel.weightProperty(), new NumberStringConverter());
private void bindFields() {
// Bidirektionales Binding zwischen Model und UI
nameField.textProperty().bindBidirectional(metadata.nameProperty());
ageField.textProperty().bindBidirectional(metadata.ageProperty(), new javafx.util.converter.NumberStringConverter());
genderChoiceBox.valueProperty().bindBidirectional(metadata.genderProperty());
heightField.textProperty().bindBidirectional(metadata.heightProperty(), new javafx.util.converter.NumberStringConverter());
weightField.textProperty().bindBidirectional(metadata.weightProperty(), new javafx.util.converter.NumberStringConverter());
} }
@FXML @FXML
private void handleOk() { private void handleOk() {
try { try {
metadata.validate(); viewModel.save();
// Dialog schließen ((Stage) nameField.getScene().getWindow()).close();
Stage stage = (Stage) nameField.getScene().getWindow();
stage.close();
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
Alert alert = new Alert(Alert.AlertType.ERROR, e.getMessage(), ButtonType.OK); new Alert(Alert.AlertType.ERROR, e.getMessage(), ButtonType.OK).showAndWait();
alert.showAndWait();
} }
} }
@FXML @FXML
private void handleCancel() { private void handleCancel() {
Stage stage = (Stage) nameField.getScene().getWindow(); ((Stage) nameField.getScene().getWindow()).close();
stage.close();
} }
} }

View File

@ -19,6 +19,7 @@ import javafx.beans.property.StringProperty;
public class FxModel implements Serializable { public class FxModel implements Serializable {
private final StringProperty id = new SimpleStringProperty(this, "id"); private final StringProperty id = new SimpleStringProperty(this, "id");
private MqttJavaClient mqttClient = new MqttJavaClient();
public FxModel() {} public FxModel() {}
@ -33,5 +34,16 @@ public class FxModel implements Serializable {
public void validate() throws IllegalArgumentException { public void validate() throws IllegalArgumentException {
// Subklassen können überschreiben // Subklassen können überschreiben
} }
public void publishMessage(String topic, String messageContent, int qos) {
getMqttClient().publishMessage(topic, messageContent, qos);
}
/**
* @return the mqttClient
*/
public MqttJavaClient getMqttClient() {
return mqttClient;
}
} }

View File

@ -0,0 +1,102 @@
/*
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
*/
package efi.projekt.gesundheitsassistent.model;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
/**
*
* @author aumni
*/
public class MqttJavaClient implements MqttCallback {
private Map<String, Consumer<String>> topicListeners = new HashMap<>();
private static final String BROKER_URL = "tcp://localhost:1883";
private static final String CLIENT_ID = "JavaClientPublisherSubscriber";
private static final String PUBLISH_TOPIC = "temperatur";
private static final String SUBSCRIBE_TOPIC = "Text";
private MqttClient client;
public MqttJavaClient() {
try {
client = new MqttClient(BROKER_URL, CLIENT_ID, new MemoryPersistence());
client.setCallback(this);
MqttConnectOptions options = new MqttConnectOptions();
options.setCleanSession(true);
options.setAutomaticReconnect(true);
System.out.println("Verbinde mit Broker..." + BROKER_URL);
client.connect(options);
System.out.println("Verbunden");
System.out.println("Abonniere Topic: " + SUBSCRIBE_TOPIC);
client.subscribe(SUBSCRIBE_TOPIC);
System.out.println("Topic abonniert!");
} catch (MqttException ex) {
System.err.println("Fehler beim Initialisieren oder Verbinden: " + ex.getMessage());
ex.printStackTrace();
}
}
public void addTopicListeners(String topic, Consumer<String> listener) {
topicListeners.put(topic, listener);
try {
client.subscribe(topic);
System.out.println("Abonniert: " + topic);
} catch (MqttException e) {
e.printStackTrace();
}
}
public void publishMessage(String topic, String message, int qos) {
try {
MqttMessage msg = new MqttMessage(message.getBytes());
msg.setQos(qos);
client.publish(topic, msg);
System.out.println("Nachricht veröffentlicht: Topic = " + topic + ", Inhalt = " + message + ", QoS = " + qos);
} catch (MqttException ex) {
System.err.println("Fehler beim Veröffentlicher der Nachricht: " + ex.getMessage());
ex.printStackTrace();
}
}
@Override
public void connectionLost(Throwable cause) {
System.out.println("Verbindung zum Broker verloren! Ursache: " + cause.getMessage());
}
@Override
public void messageArrived(String topic, MqttMessage message) throws Exception {
String payload = new String(message.getPayload());
System.out.println("Nachricht emmpfangen:");
System.out.println(" Topic: " + topic);
System.out.println(" Inhalt: " + new String(message.getPayload()));
System.out.println(" QoS: " + message.getQos());
Consumer<String> listener = topicListeners.get(topic);
if (listener != null) {
listener.accept(payload);
}
}
@Override
public void deliveryComplete(IMqttDeliveryToken token) {
System.out.println("Nachricht erfolgreich zugestellt (QoS > 0). Token: " + token.getMessageId());
}
}

View File

@ -0,0 +1,50 @@
/*
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
*/
package efi.projekt.gesundheitsassistent.viewmodel;
import efi.projekt.gesundheitsassistent.model.FxModel;
import java.util.HashMap;
import java.util.Map;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
/**
*
* @author naumueller
*/
public class FxViewModel {
private final FxModel model;
private Map<String, StringProperty> topicMessages = new HashMap<>();
private final StringProperty incomingMessage = new SimpleStringProperty("");
private final StringProperty outgoingMessage = new SimpleStringProperty("");
private final StringProperty messageToSend = new SimpleStringProperty("");
public FxViewModel(FxModel model) {
this.model = model;
subscribeTopic("Text");
subscribeTopic("Temperatur");
}
public void subscribeTopic(String topic) {
model.getMqttClient().addTopicListeners(topic, msg -> {
Platform.runLater(() -> getTopicProperty(topic).set(msg));
});
}
public void sendMessage(String msg) {
if (msg != null && !msg.isBlank()) {
model.getMqttClient().publishMessage("Text", msg, 1);
outgoingMessage.set("");
}
}
public StringProperty incomingMessageProperty() { return incomingMessage; }
public StringProperty outgoingMessageProperty() { return outgoingMessage; }
public StringProperty getTopicProperty(String topic) { return topicMessages.computeIfAbsent(topic, t -> new SimpleStringProperty()); }
}

View File

@ -0,0 +1,33 @@
/*
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
*/
package efi.projekt.gesundheitsassistent.viewmodel;
import efi.projekt.gesundheitsassistent.model.Metadata;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.StringProperty;
/**
*
* @author naumueller
*/
public class MetadataDialogViewModel {
private final Metadata metadata;
public MetadataDialogViewModel(Metadata metadata) {
this.metadata = metadata;
}
public StringProperty nameProperty() { return metadata.nameProperty(); }
public IntegerProperty ageProperty() { return metadata.ageProperty(); }
public StringProperty genderProperty() { return metadata.genderProperty(); }
public DoubleProperty heightProperty() { return metadata.heightProperty(); }
public DoubleProperty weightProperty() { return metadata.weightProperty(); }
public void save() {
metadata.validate();
}
}

View File

@ -3,6 +3,7 @@ module efi.projekt.gesundheitsassistent {
requires javafx.fxml; requires javafx.fxml;
requires java.base; requires java.base;
requires atlantafx.base; requires atlantafx.base;
requires org.eclipse.paho.client.mqttv3;
opens efi.projekt.gesundheitsassistent to javafx.fxml; opens efi.projekt.gesundheitsassistent to javafx.fxml;
opens efi.projekt.gesundheitsassistent.controller to javafx.fxml; opens efi.projekt.gesundheitsassistent.controller to javafx.fxml;

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 467 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 633 KiB

View File

@ -3,7 +3,10 @@
<?import javafx.geometry.Insets?> <?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?> <?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?> <?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.control.TitledPane?> <?import javafx.scene.control.TitledPane?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.BorderPane?> <?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?> <?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?> <?import javafx.scene.layout.StackPane?>
@ -18,7 +21,7 @@
<Insets bottom="15" left="15" right="15" top="15" /> <Insets bottom="15" left="15" right="15" top="15" />
</padding> </padding>
<Label text="Virtueller Gesundheitsassistent" /> <Label text="Virtueller Gesundheitsassistent" />
<Button text="Einstellungen" /> <Button onMouseClicked="#handleOptions" text="Einstellungen" />
</HBox> </HBox>
</top> </top>
@ -29,7 +32,7 @@
<Insets bottom="15" left="15" right="15" top="15" /> <Insets bottom="15" left="15" right="15" top="15" />
</padding> </padding>
<!-- Linke Seite: Gesundheitsdaten --> <!-- Linke Seite: Gesundheitsdaten -->
<VBox alignment="CENTER" spacing="15" HBox.hgrow="ALWAYS"> <!--<VBox alignment="CENTER" spacing="15" HBox.hgrow="ALWAYS">
<TitledPane text="Gesundheitsdaten"> <TitledPane text="Gesundheitsdaten">
<content> <content>
<VBox spacing="10"> <VBox spacing="10">
@ -42,6 +45,46 @@
</VBox> </VBox>
</content> </content>
</TitledPane> </TitledPane>
</VBox>-->
<VBox alignment="CENTER" spacing="15" HBox.hgrow="ALWAYS">
<TitledPane text="Gesundheitsdaten">
<content>
<VBox spacing="10">
<padding>
<Insets bottom="10" left="10" right="10" top="10" />
</padding>
<!-- Herzfrequenz -->
<HBox alignment="CENTER_LEFT" spacing="10">
<ImageView fitHeight="24" fitWidth="24" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@../icons/icons8-health-48.png" />
</image></ImageView>
<Label text="Herzfrequenz: 72 bpm" />
</HBox>
<!-- Temperatur -->
<HBox alignment="CENTER_LEFT" spacing="10">
<ImageView fitHeight="24" fitWidth="24" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@../icons/icons8-temperature-48.png" />
</image></ImageView>
<Label text="Temperatur: 36.8 °C" />
</HBox>
<!-- Stress-Level -->
<HBox alignment="CENTER_LEFT" spacing="10">
<ImageView fitHeight="24" fitWidth="24" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@../icons/icons8-stress-64.png" />
</image></ImageView>
<Label text="Stress-Level: Niedrig" />
</HBox>
</VBox>
</content>
</TitledPane>
</VBox> </VBox>
<!-- Rechte Seite: Avatar --> <!-- Rechte Seite: Avatar -->
@ -49,9 +92,18 @@
<TitledPane text="Avatar"> <TitledPane text="Avatar">
<content> <content>
<StackPane fx:id="avatarContainer" alignment="CENTER" prefHeight="400" prefWidth="400"> <StackPane fx:id="avatarContainer" alignment="CENTER" prefHeight="400" prefWidth="400">
<Label text="3D-Avatar wird geladen..." textFill="white" /> <HBox alignment="CENTER" spacing="10">
<ImageView fitHeight="300.0" fitWidth="300.0" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@../icons/icons8-account-male.gif" />
</image>
</ImageView>
</HBox>
</StackPane> </StackPane>
</content> </content>
<graphic>
<Label text="3D-Avatar wird geladen..." textFill="white" />
</graphic>
</TitledPane> </TitledPane>
</VBox> </VBox>
</HBox> </HBox>
@ -63,7 +115,10 @@
<padding> <padding>
<Insets bottom="15" left="15" right="15" top="15" /> <Insets bottom="15" left="15" right="15" top="15" />
</padding> </padding>
<Button onMouseClicked="#handleExit" text="Beenden" /> <Button fx:id="sendButton" mnemonicParsing="false" onMouseClicked="#handleSendMessage" text="Send message" />
<TextField fx:id="messageInputField" />
<TextField fx:id="messageOutputField" editable="false" />
<Button fx:id="exitButton" onMouseClicked="#handleExit" text="Beenden" />
</HBox> </HBox>
</bottom> </bottom>