Initial commit

This commit is contained in:
github-classroom[bot]
2022-03-17 08:42:05 +00:00
commit c346486299
51 changed files with 9317 additions and 0 deletions
@@ -0,0 +1,53 @@
/*
* Gradle build configuration for specific lab module / exercise
*/
// the Java plugin is added by default in the main lab configuration
plugins {
id 'java'
// Apply the application plugin to add support for building a CLI application.
id 'application'
// Adding JavaFX support and dependencies
id 'org.openjfx.javafxplugin' version '0.0.12'
}
// Project/Module information
description = 'Lab02 Calculator solution'
group = 'ch.zhaw.prog2'
version = '2022.1'
// Dependency configuration
// repositories to download dependencies from
repositories {
mavenCentral()
}
// required dependencies
dependencies {
}
// Configuration for Application plugin
application {
// Define the main class for the application.
mainClass = 'ch.zhaw.prog2.calculator.Main'
}
// Configuration for JavaFX plugin
javafx {
version = '17'
modules = [ 'javafx.controls' ]
}
// Java plugin configuration
java {
// By default the Java version of the gradle process is used as source/target version.
// This can be overridden, to ensure a specific version. Enable only if required.
// sourceCompatibility = JavaVersion.VERSION_17 // ensure Java source code compatibility
// targetCompatibility = JavaVersion.VERSION_17 // version of the created byte-code
// Java compiler specific options
compileJava {
// source files should be UTF-8 encoded
options.encoding = 'UTF-8'
// for more options see https://docs.gradle.org/current/dsl/org.gradle.api.tasks.compile.CompileOptions.html
}
}
@@ -0,0 +1,242 @@
package ch.zhaw.prog2.calculator;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.input.KeyCombination;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
/**
* Main Application. Controller and View in the same class
* Other options:
* - Separating the start from the window classes
* - Separating view and Controller
* @author bles
*
*/
public class Main extends Application {
private static final int VERTICAL_GAP = 5;
private static final int HORIZONTAL_GAP = 10;
private static final String INFO = """
Enter valid values to
- Initial amount (> 0)
- Return in % (can be +/- or 0)
- Annual Costs (> 0)
- Number of years (> 0)
Calculate displays the annual balance development!";
""";
private Stage primaryStage;
private CheckMenuItem clearInitialAmount = new CheckMenuItem("Initial amount");
private CheckMenuItem clearReturnInPercent = new CheckMenuItem("Return in %");
private CheckMenuItem clearAnnualCosts = new CheckMenuItem("Annual Costs");
private CheckMenuItem clearNumberOfYears = new CheckMenuItem("Number of years");
private TextField initialAmount = new TextField();
private TextField returnInPercent = new TextField();
private TextField annualCost = new TextField();
private TextField numberOfYears = new TextField();
private TextArea results = new TextArea();
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) throws Exception {
this.primaryStage = primaryStage;
mainWindow();
}
/*
* Create the window, call methods to create the different parts of the
* scene graph. Put the parts together in appropriate panes.
*/
private void mainWindow() {
try {
BorderPane rootPane = new BorderPane();
// BorderPane top
MenuBar menuBar = new MenuBar();
createMenu(menuBar);
// BorderPane left
// two rows for grid (inputPanel) and other VBox (resultRows)
VBox inputOutputPanel = new VBox(VERTICAL_GAP);
GridPane inputPanel = new GridPane();
VBox resultRows = new VBox();
inputOutputPanel.getChildren().add(inputPanel);
inputOutputPanel.getChildren().add(resultRows);
createInputPanel(inputPanel);
createResultRows(resultRows);
// BorderPane bottom
HBox buttons = new HBox(HORIZONTAL_GAP);
buttons.setAlignment(Pos.BASELINE_CENTER);
createButtons(buttons);
// set up root border pane
rootPane.setTop(menuBar);
rootPane.setBottom(buttons);
rootPane.setCenter(inputOutputPanel);
// Create scene with root node with size
Scene scene = new Scene(rootPane, 600, 400);
primaryStage.setMinWidth(280);
// Set stage properties
primaryStage.setTitle("Return on Investment Calculator");
// Add scene to the stage and make it visible
primaryStage.setScene(scene);
primaryStage.show();
// Connect height of the result-area to the height of the scene
scene.heightProperty().addListener(new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
results.setPrefHeight((double) newValue);
}
});
} catch(Exception e) {
e.printStackTrace();
}
}
/*
* Add the title and the result TextArea to the VBox
*/
private void createResultRows(VBox resultRows) {
resultRows.getChildren().add(new Label("Results:"));
resultRows.getChildren().add(results);
resultRows.setPadding(new Insets(VERTICAL_GAP, HORIZONTAL_GAP, VERTICAL_GAP, HORIZONTAL_GAP));
}
/*
* 4 rows in a GridPane with row-title and input TextField
*/
private void createInputPanel(GridPane inputPanel) {
inputPanel.setVgap(5);
inputPanel.setHgap(5);
inputPanel.add(new Label("Initial amount"), 0, 1);
inputPanel.add(new Label("Return rate in %"), 0, 2);
inputPanel.add(new Label("Annual cost"), 0, 3);
inputPanel.add(new Label("Number of years"), 0, 4);
inputPanel.add(initialAmount, 1, 1);
inputPanel.add(returnInPercent, 1, 2);
inputPanel.add(annualCost, 1, 3);
inputPanel.add(numberOfYears, 1, 4);
inputPanel.setPadding(new Insets(VERTICAL_GAP, HORIZONTAL_GAP, VERTICAL_GAP, HORIZONTAL_GAP));
}
/*
* Create menu for the top-area of the BorderPane
*/
private void createMenu(MenuBar menu) {
Menu clearMenu = new Menu("Clear");
Menu helpMenu = new Menu("?");
// Create MenuItems
MenuItem clearValues = new MenuItem("Clear values");
clearValues.setId("clearValues");
MenuItem clearResults = new MenuItem("Clear results");
clearResults.setId("clearResults");
MenuItem helpShowText = new MenuItem("Show help");
clearMenu.getItems().addAll(clearInitialAmount, clearReturnInPercent, clearAnnualCosts, clearNumberOfYears);
clearMenu.getItems().addAll(new SeparatorMenuItem(), clearValues, new SeparatorMenuItem(), clearResults);
helpMenu.getItems().add(helpShowText);
menu.getMenus().addAll(clearMenu, helpMenu);
//using an inner class
ClearHandler clearHandler = new ClearHandler();
clearValues.addEventHandler(ActionEvent.ACTION, clearHandler);
clearResults.addEventHandler(ActionEvent.ACTION, clearHandler);
helpShowText.setAccelerator(KeyCombination.keyCombination("F1"));
helpShowText.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
showResult(INFO, true, Color.BLUE);
}
});
}
/*
* Create buttons in the HBox inside the bottom pane of the BorderPane
*/
private void createButtons(HBox buttons) {
Button closeButton = new Button("Close");
Button calculateButton = new Button("Calculate");
// Configure close event using lambda expressions
closeButton.setOnAction(e -> Platform.exit());
// Configure calculate event using anonymous inner class
calculateButton.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
ValueHandler valueHandler = new ValueHandler();
valueHandler.checkValuesAndCalculateResult(
initialAmount.getText(),
returnInPercent.getText(),
annualCost.getText(),
numberOfYears.getText());
String result = valueHandler.getResultBound();
if(valueHandler.areValuesOk()) {
showResult(result, true, Color.GREEN);
} else {
showResult(result, false, Color.RED);
}
}
});
buttons.getChildren().addAll(calculateButton, closeButton);
buttons.setPadding(new Insets(VERTICAL_GAP, HORIZONTAL_GAP, VERTICAL_GAP, HORIZONTAL_GAP));
}
/*
* Show text in the result box
*/
private void showResult(String text, boolean clearFirst, Color backColor) {
if(clearFirst) {
results.setText(text);
} else {
results.appendText("\n");
results.appendText(text);
}
results.setBorder(new Border(new BorderStroke(backColor, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, new BorderWidths(1))));
}
/*
* Handler to clear the controls
*/
private class ClearHandler implements EventHandler<ActionEvent> {
@Override
public void handle(ActionEvent event) {
switch (((MenuItem) event.getSource()).getId()) {
case "clearValues" -> {
if (clearInitialAmount.isSelected()) {
initialAmount.clear();
}
if (clearAnnualCosts.isSelected()) {
annualCost.clear();
}
if (clearNumberOfYears.isSelected()) {
numberOfYears.clear();
}
if (clearReturnInPercent.isSelected()) {
returnInPercent.clear();
}
}
case "clearResults" -> results.clear();
}
}
}
}
@@ -0,0 +1,139 @@
package ch.zhaw.prog2.calculator;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
/**
* Handles the values from the input form.
* Offers the {@link #resultBound} StringProperty to listen from a view (bind to a field in the view or add a listener)
* @author bles
* @version 1.1
*/
public class ValueHandler {
private double initialAmount;
private double returnInPercent;
private double annualCost;
private int numberOfYears;
private boolean valuesOk = false;
// Solution with bound properties
private StringProperty resultBound = new SimpleStringProperty();
/**
* Check the input values are valid (can be improved)
* If not ok, return an error message
* If ok, set the data fields and return an empty string
* @return empty string on success or error message on invalid value
*/
private String checkAndSetValues(String initialAmount, String returnInPercent, String annualCost, String numberOfYears) {
StringBuilder sb = new StringBuilder();
valuesOk = true;
if ("".equals(initialAmount) || Double.parseDouble(initialAmount)<=0) {
sb.append("Please specify a positive initial amount!\n");
valuesOk = false;
} else {
this.initialAmount = Double.parseDouble(initialAmount);
}
if ("".equals(returnInPercent)) {
sb.append("Please specify the annual return rate in %!\n");
valuesOk = false;
} else {
this.returnInPercent = Double.parseDouble(returnInPercent)/100;
}
if ("".equals(annualCost) || Double.parseDouble(annualCost)<0) {
sb.append("Please specify the annual cost!\n");
valuesOk = false;
} else {
this.annualCost = Double.parseDouble(annualCost);
}
if ("".equals(numberOfYears) ||
Double.parseDouble(numberOfYears) < 1 ||
Double.parseDouble(numberOfYears) > 99 ||
Math.round(Double.parseDouble(numberOfYears))!=Double.parseDouble(numberOfYears)) {
sb.append("Please enter a time period in years!");
valuesOk = false;
} else {
this.numberOfYears = Integer.parseInt(numberOfYears);
}
return sb.toString();
}
/**
* If the values checked by {@link #checkAndSetValues(String, String, String, String)} are ok, the return is true
* @return true, if values are ok
*/
public boolean areValuesOk() {
return valuesOk;
}
/**
* Calculates the result
* @return the result as a String
*/
private String calculateResult() {
StringBuilder resultSB = new StringBuilder();
double val = initialAmount;
for(int i = 1; i <= numberOfYears; i++) {
resultSB.append("After ");
resultSB.append(i).append(" year(s): ");
val = val * (1 + returnInPercent) - annualCost;
resultSB.append(Math.round(val)).append("\n");
}
return resultSB.toString();
}
// Solution with bound properties
/**
* String containing the result of the calculation
* Can be "", if no calculation or check is done or could contain the error message on invalid values
* @return String with the result of the value checking or the calculation
*/
public String getResultBound() {
return resultBound.get();
}
/**
* Sets the result of the calculation (or error message).
* @param infoText
*/
public void setResultBound(String infoText) {
resultBound.set(infoText);
}
/**
* Gives access to the StringProperty holding the result of the calculation
* @return result String property which can be bound to UI elements
*/
public StringProperty resultBoundProperty() {
return resultBound;
}
/**
* Checks the values and calculates the result. All values as String (from the Text-Fields)
* If the check fails, an error message is set to the bound result property
* @param initialAmount
* @param returnInPercent
* @param annualCost
* @param numberOfYears
*/
public void checkValuesAndCalculateResult(String initialAmount, String returnInPercent, String annualCost, String numberOfYears) {
setResultBound(checkAndSetValues(initialAmount, returnInPercent, annualCost, numberOfYears));
if(valuesOk) {
setResultBound(calculateResult());
}
}
/**
* clears result and resets all values
*/
public void clearResult() {
initialAmount = 0;
returnInPercent = 0;
annualCost = 0;
numberOfYears = 0;
setResultBound("");
valuesOk = false;
}
}
@@ -0,0 +1,53 @@
/*
* Gradle build configuration for specific lab module / exercise
*/
// the Java plugin is added by default in the main lab configuration
plugins {
id 'java'
// Apply the application plugin to add support for building a CLI application.
id 'application'
// Adding JavaFX support and dependencies
id 'org.openjfx.javafxplugin' version '0.0.12'
}
// Project/Module information
description = 'Lab02 FXML WordCloud solution'
group = 'ch.zhaw.prog2'
version = '2022.1'
// Dependency configuration
// repositories to download dependencies from
repositories {
mavenCentral()
}
// required dependencies
dependencies {
}
// Configuration for Application plugin
application {
// Define the main class for the application.
mainClass = 'ch.zhaw.prog2.application.App'
}
// Configuration for JavaFX plugin
javafx {
version = '17'
modules = [ 'javafx.controls', 'javafx.fxml' ]
}
// Java plugin configuration
java {
// By default the Java version of the gradle process is used as source/target version.
// This can be overridden, to ensure a specific version. Enable only if required.
// sourceCompatibility = JavaVersion.VERSION_17 // ensure Java source code compatibility
// targetCompatibility = JavaVersion.VERSION_17 // version of the created byte-code
// Java compiler specific options
compileJava {
// source files should be UTF-8 encoded
options.encoding = 'UTF-8'
// for more options see https://docs.gradle.org/current/dsl/org.gradle.api.tasks.compile.CompileOptions.html
}
}
@@ -0,0 +1,12 @@
/*
* This Java source file was generated by the Gradle 'init' task.
*/
package ch.zhaw.prog2.application;
import javafx.application.Application;
public class App {
public static void main(String[] args) {
Application.launch(MainWindow.class, args);
}
}
@@ -0,0 +1,18 @@
package ch.zhaw.prog2.application;
/**
* Most basic interface for observing an object
* @author bles
*
*/
public interface IsObservable {
/**
* Add an observer that listens for updates
* @param observer
*/
void addListener(IsObserver observer);
/**
* Remove an observer from the list
* @param observer
*/
void removeListener(IsObserver observer);
}
@@ -0,0 +1,13 @@
package ch.zhaw.prog2.application;
/**
* Most basic interface for beeing an observer
* @author bles
*
*/
public interface IsObserver {
/**
* This method is always called when an observed object
* changes
*/
void update();
}
@@ -0,0 +1,39 @@
package ch.zhaw.prog2.application;
import javafx.application.Application;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
public class MainWindow extends Application {
private WordModel wordModel = new WordModel();
@Override
public void start(Stage primaryStage) throws Exception {
openMainWindow(primaryStage);
}
private void openMainWindow(Stage stage) {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource("MainWindow.fxml"));
Pane rootNode = loader.load();
MainWindowController mainWindowController = loader.getController();
mainWindowController.connectProperties();
mainWindowController.setWordModel(wordModel);
Scene scene = new Scene(rootNode);
stage.setScene(scene);
stage.show();
} catch(Exception e) {
e.printStackTrace();
}
}
}
@@ -0,0 +1,50 @@
package ch.zhaw.prog2.application;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
public class MainWindowController {
private WordModelDecorator wordModelDecorator;
@FXML
private TextArea textHistory;
@FXML
private TextField textEingabe;
@FXML
private Label labelTitel;
public void connectProperties() {
//binding properties
labelTitel.textProperty().bind(textEingabe.textProperty());
}
public void setWordModel(WordModel wordModel) {
wordModelDecorator = new WordModelDecorator(wordModel);
wordModelDecorator.addListener(new IsObserver() {
@Override
public void update() {
textHistory.setText(wordModel.toString());
}
});
}
@FXML
private void hinzufuegenText(ActionEvent event) {
for(String word : textEingabe.getText().split(" ")) {
wordModelDecorator.addWord(word.toLowerCase());
}
}
@FXML
private void leerenTextEingabe(ActionEvent event) {
textEingabe.clear();
}
}
@@ -0,0 +1,36 @@
package ch.zhaw.prog2.application;
import java.util.HashMap;
import java.util.Map;
/**
* A WordModel object holds a map of words (String) with the actual count
* @author bles
*
*/
public class WordModel {
private Map<String, Integer> words = new HashMap<>();
public void addWord(String word) {
int count = words.getOrDefault(word, 0);
words.put(word, ++count);
}
public void removeWord(String word) {
int count = words.getOrDefault(word, 1);
if (count == 1) {
words.remove(word);
} else {
words.put(word, --count);
}
}
public String toString() {
StringBuilder sb = new StringBuilder();
for(Map.Entry<String, Integer> entry : words.entrySet()) {
sb.append(entry.getKey()).append(" - ").append(entry.getValue());
sb.append(System.lineSeparator());
}
return sb.toString();
}
}
@@ -0,0 +1,45 @@
package ch.zhaw.prog2.application;
import java.util.ArrayList;
import java.util.List;
/**
* Adds observable functionality to one WordModel-object.
* The decorator uses the original methods of the WordModel-object.
* @author bles
*
*/
public class WordModelDecorator implements IsObservable {
private final WordModel wordModel;
private List<IsObserver> listener = new ArrayList<>();
public WordModelDecorator(WordModel wordModel) {
this.wordModel = wordModel;
}
@Override
public void addListener(IsObserver observer) {
listener.add(observer);
}
@Override
public void removeListener(IsObserver observer) {
listener.remove(observer);
}
public void addWord(String word) {
wordModel.addWord(word);
informListener();
}
public void removeWord(String word) {
wordModel.removeWord(word);
informListener();
}
private void informListener() {
for(IsObserver observer : listener) {
observer.update();
}
}
}
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane prefHeight="480.0" prefWidth="640.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="ch.zhaw.prog2.application.MainWindowController">
<children>
<VBox alignment="CENTER" prefHeight="200.0" prefWidth="100.0" spacing="10.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<children>
<Label fx:id="labelTitel" text="Label" />
<TextField fx:id="textEingabe" maxWidth="300.0" />
<HBox alignment="CENTER" prefHeight="100.0" prefWidth="200.0" spacing="10.0">
<children>
<Button mnemonicParsing="false" onAction="#hinzufuegenText" text="Hinzufügen Text" />
<Button mnemonicParsing="false" onAction="#leerenTextEingabe" text="Löschen Eingabefeld" />
</children>
</HBox>
<TextArea fx:id="textHistory" editable="false" prefHeight="200.0" prefWidth="200.0" VBox.vgrow="ALWAYS" />
</children>
<padding>
<Insets top="10.0" />
</padding>
</VBox>
</children>
<padding>
<Insets bottom="20.0" />
</padding>
</AnchorPane>
Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

@@ -0,0 +1,216 @@
:source-highlighter: coderay
:icons: font
:experimental:
:!sectnums:
:imagesdir: ./images/
:solution: ./
:handout: ../code
:logo: IT.PROG2 -
ifdef::backend-html5[]
:logo: image:PROG2-300x300.png[IT.PROG2,100,100,role=right,fit=none,position=top right]
endif::[]
ifdef::backend-pdf[]
:logo:
endif::[]
ifdef::env-github[]
:tip-caption: :bulb:
:note-caption: :information_source:
:important-caption: :heavy_exclamation_mark:
:caution-caption: :fire:
:warning-caption: :warning:
endif::[]
// references
:url-openjfx: https://openjfx.io/
:url-openjfx-gradle: {url-openjfx}/openjfx-docs/#gradle
:url-openjfx-javadoc: {url-openjfx}/javadoc/17/index.html
:url-scene-builder: https://gluonhq.com/products/scene-builder
:url-openjfx-gradle-plugin: https://plugins.gradle.org/plugin/org.openjfx.javafxplugin
// images
:LayoutVboxHBox: image:LayoutNurVBoxHBox.png[LAYOUTVBOXHBOX,200,fit=none,role=left,position=top left]
:LayoutBorder: image:LayoutBorderPaneGridPane.png[LAYOUTBORDER,200,fit=none,role=left,position=top left]
= {logo} Lösungen zu den Übungen GUI
:sectnums:
:sectnumlevels: 2
// Beginn des Aufgabenblocks
== Erweitern des Workshops um ein Model [PU]
****
Eine Musterlösung für diese Aufgabe finden Sie im link:{solution}[Lösungsverzeichnis] unter `FXML-Wordcloud`.
****
Setzen Sie aufbauend auf der Applikation aus dem Workshop eine Modellklasse (Model) ein und verbinden Sie dieses gemäss den Vorgaben von MVC mit der Applikation.
=== Ziel
Die Applikation WordCloud soll, beim Eingeben von Texten, diese einzelnen Wörter nicht mehr direkt in der History anzeigen.
* In der View wird ein Button geklickt.
** Die Aktion am Controller setzt nicht mehr die View direkt, sondern ändert nur noch das Model.
* Gemäss MVC darf das Model nicht auf die View zugreifen:
** Wir brauchen eine Möglichkeit, den Controller zu informieren, wenn er den Inhalt der View auf den neuesten Stand bringen soll.
=== Basis
* Verwenden Sie als Model die im link:{handout}[Praktikumsverzeichnis] zur Verfügung gestellten Klassen `WordModel`, `WordModelDecorator` sowie die beiden Interfaces `IsObservable` und `IsObserver` aus dem Projekt FXML-WordCloud.
=== Aufgabenstellung
* Öffnen Sie das mit dem Workshop erstellte Projekt (oder arbeiten Sie den Workshop durch, um das Projekt zu erhalten).
* Ergänzen Sie das Projekt um die Model Klasse `WordModel`, den Decorator `WordModelDecorator` und die beiden Interfaces.
+
****
Bisher wurden die eingegebenen Worte nur im GUI verwendet.
Die Klasse `WordModel` soll die eingegebenen Worte halten und etwas damit tun (z.B. zählen der Worte).
****
* Studieren Sie die zur Verfügung gestellten Klassen und Interfaces. *Sie sollen nicht geändert werden!*
** `WordModel` enthält keine Hinweise auf das verwendete UI.
+
****
Dies ist eine wichtige Voraussetzung, damit verschiedene GUIs verwendet werden können.
****
** `WordModelDecorator` fügt die Möglichkeit zum Beobachten eines `WordModel` Objekts hinzu (Beobachter hinzufügen und entfernen).
** Überlegen Sie sich, wie hier das Observer-Pattern umgesetzt wurde.
+
****
Die Klasse `WordModelDecorator` implementiert das Interface `IsObervable`. Dies beinhaltet die Methoden `addListener` und `removeListener`.
Damit können Objekte vom Typ `IsObserver` hinzugefügt werden. Die Methode `informListener` (der Name spielt keine Rolle) ruft dann auf jedem "Zuhörer" die Methode `update` auf, welche durch das Interface `IsObserver` vorgegeben ist und vom "Zuhörer" implementiert werden muss.
****
* Ändern Sie den Controller so ab, dass die Vorgaben im Ziel erreicht sind.
+
****
Als erstes muss der Controller das Model kennen. Dies ist gemäss MVC in Ordnung, nur umgekehrt wäre falsch.
Das Model selbst wird in der Hauptapplikation gehalten (`MainWindow`). Die Instanz wird dann an den Controller durchgereicht, nachdem dieser beim Öffnen des Hauptfensters (MainWindow.fxml) erstellt wurde.
Im Controller (`MainWindowController`) verwenden Sie nur den Typ `WordModelDecorator`. Dieser hat die gleichen Methoden wie `WordModel` und einige zusätzliche (siehe oben).
Die Methode `hinzufuegenText` fügt die Worte dem Model hinzu. Das Model macht dann irgend etwas mit den Worten (für das GUI uninteressant) und informiert das GUI über den neuen Zustand.
Beim Instantiieren des `wordModelDecorator` fügen Sie auch gleich den Listener als anonyme Klasse vom Typ `IsObserver` hinzu.
Durch das Überschreiben der Methode `update` wird bestimmt, wie der geänderte Inhalt des Models angezeigt wird.
Aktuell wird einfach Text angezeigt (in der Lösung). Dies könnte aber auch graphisch umgesetzt werden, dem `wordModel` Objekt ist das egal.
****
[NOTE]
Um die einzelnen Worte ins WordModel zu schreiben, müssen Sie die Methode `addWord(String word)` für jedes eingegebene Wort einzeln aufrufen. Am einfachsten nehmen Sie die Worte in Kleinbuchstaben.
== Die Übungsapplikation ROI Calculator
****
Kurzbeschrieb und Anforderungen und Überlegungen zu den JavaFX-Container/Panes), finden Sie in der Aufgabenstellung
****
== Erstellen des ROI-Calculators mit Object-SceneGraph
=== Planung des Layouts [TU]
[loweralpha]
. Überlegen Sie sich, welche Container sie verwenden möchten, um die Anforderungen zu erfüllen
. Zeichnen Sie mindestens zwei Möglichkeiten auf
. Beschreiben Sie die Vor- und Nachteile, die sich aus Ihren Überlegungen ergeben könnten
TIP: Beachten Sie vor allem die Positionierung, die Abstände und die Ausrichtung der Controls. Vergessen Sie auch das Menü nicht.
****
Verschiedene Kombinationen sind möglich. Teilen Sie das ganze Layout in Bereiche auf, die vom Verhalten oder der Darstellung her gleich sind. Z.B. könnten die vier Labels mit den Textfeldern des Eingabebereichs so separiert werden.
Für diesen Bereich könnten Sie ein GridPane (2 Spalten mit 4 Zeilen) einsetzen. Oder 4 HBox Bereiche angeordnet in einer VBox. Oder 2 VBox Bereiche angeordnet in einer HBox.
Jeder Ansatz hat Vor- und Nachteile. Bei verschachtelten Containern ist es sinnvoll, eine Einstellung möglichst weit aussen einzustellen.
Gemäss obigem Beispiel: Wenn alle Textfelder sich gleich verhalten sollen, was die Grösse betrifft und den Abstand zwischen den Zeilen, so sollte das in der umgebenden VBox eingestellt werden (Padding für den Abstand zwischen den Textfeldern und die Breite der VBox an die Textfelder vererben). Erst wenn die einzelnen Textfelder ein unterschiedliches Verhalten bei der Grösse aufweisen sollen, nehmen Sie Einstellungen dort vor.
*Es gibt unzählige mögliche Kombinationen. Hier 2 davon:*
[cols="40,~",frame=none,grid=none]
|===
| {LayoutVBoxHBox}
| Es wurden zur Organisation der Controls nur VBox und HBox eingesetzt.
|===
[cols="40,~",frame=none, grid=none]
|===
| {LayoutBorder}
| Im BoderPane bleiben der Bereich LEFT und der Bereich RIGHT leer. Die Bereiche TOP, CENTER und BOTTOM wurden belegt. +
Die leeren Bereiche werden nicht angezeigt und belegen keinen Platz.
|===
Ob diese Layoutkombinationen im Code oder mit dem SceneBuilder erstellt werden, spielt keine Rolle. Die Verschachtelung bleibt die Gleiche.
****
=== Erstellen der Applikation [PU]
****
Eine Musterlösung für diese Aufgabe finden Sie im link:{solution}[Lösungsverzeichnis] unter `Calculator`.
****
[loweralpha]
. Ergänzen Sie die Projektkonfiguration (Gradle) für den Einsatz von link:{url-openjfx}[JavaFX]
+
****
Die Projektkonfiguration finden sie in der `build.gradle` Datei.
Wichtig ist, dass JavaFX ein link:{url-openjfx-gradle}[Plugin] benötigt, welche die Konfiguration unterstützt und insbesondere die platformspezifischen (Windows, macOS, Linux) Abhängigkeiten löst.
Es muss im Plugin-Teil der Konfiguration integriert werden:
[source, groovy]
----
plugins {
// ...
// Adding JavaFX support and dependencies
id 'org.openjfx.javafxplugin' version '0.0.12'
}
----
Die Konfiguration des Plugins erfolgt im entsprechenden Block:
[source, groovy]
----
// Configuration for JavaFX plugin
javafx {
version = '17'
modules = [ 'javafx.controls', 'javafx.fxml' ]
}
----
Zum einen können Sie die spezifische JavaFX-Version angeben, die verwendet werden soll.
Diese sollte natürlich zur verwendeten Java-Version kompatibel sein.
Zum anderen können sie spezifizieren, welche JavaFX-Module eingebunden werden sollen.
Zum Beispiel wird das `javafx.fxml` Modul nur benötigt, wenn sie auch mit FXML arbeiten,
also erst bei Aufgabe 4.
****
. Erstellen Sie das Layout für die Applikation
+
****
Das Zusammenbauen des Scene-Graph kann aufwändig werden.
Wenn Sie das Layout (Aufgabe 1) seriös gemacht haben, ist es jedoch reine Fleissarbeit die Objekte zu erstellen und richtig zusammenzubauen.
Sinnvollerweise unterteilen Sie diese Aufgabe in mehrere Methoden, zum Beispiel fürs Menu, das Hauptfenster, ggf. auch Teile des Hauptfensters (Eingabebereich, Resultat, Buttons).
Für immer wieder verwendete Angaben (Abstand zwischen Elementen, Farben, etc.), ergibt es Sinn Konstanten zu definieren oder Werte zu berechnen, damit bei einer Änderung nur an einer Stelle korrigiert werden muss.
****
. Fügen Sie die Handler für die benötigten Events hinzu.
+
****
In der Musterlösung finden sie verschiedene Varianten von Handlern:
* `ChangeListener` um bei einer Änderung der Fenstergrösse automatisch die Höhe des Resultatfensters anzupassen.
* Action-`EventHandler` welche auf Menu oder Button Betätigung reagieren.
* Mouse-`EventHandler` welche auf Maus-Clicked, Moved, ... über bestimmten Elementen reagieren.
Bei allen Event-Handlern sollte die Logik möglichst in separate Methoden ausgelagert werden, welche aus der (anonymen) inneren Klasse (oder einem Lambda-Ausdruck) aufgerufen werden kann. Damit wird der Event-Handler übersichtlichern und sie können die Menge an dupliziertem Code reduzieren.
****
. Probieren Sie verschiedene Lösungsansätze und hinterfragen Sie die Vor- und Nachteile der gewählten Lösung.
== Erstellen des ROI-Calculators mit FXML [PA]
****
Die Lösungen zu den bewerteten Pflichtaufgaben erhalten Sie nach der Abgabe und Bewertung aller Klassen.
****
File diff suppressed because one or more lines are too long