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

29
.editorconfig Normal file
View File

@ -0,0 +1,29 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
# Default formatting Unix-style newlines with a newline ending every file
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 4
# do not trim trailing whitespace in markdown files
[*.md]
trim_trailing_whitespace = false
# explicit 4 space indentation
[*.py]
indent_size = 4
# explicit 2 space indentation
[*.{json, yml, yaml, xml, ddl, sql}]
indent_size = 2
# windows specific files
[*.{bat, cmd}]
end_of_line = crlf

5
.gitattributes vendored Normal file
View File

@ -0,0 +1,5 @@
#
# https://help.github.com/articles/dealing-with-line-endings/
#
# These are explicitly windows files and should use crlf
*.bat text eol=crlf

70
.gitignore vendored Normal file
View File

@ -0,0 +1,70 @@
# InelliJ IDEA files
*.iml
*.ipr
*.ids
*.iws
.idea/
# Eclipse files
.project
.metadata
.classpath
.settings/
.loadpath
bin/
# Netbeans
nbactions.xml
# Visual Studio Code
.vscode
# Maven
target/
# gradle files
.gradle
build/
# ignore logfiles
*.log*
# OS dependant files
.DS_Store
.Spotlight-V100
.Trashes
Thumbs.db
Desktop.ini
*~
# Thumbnails
._*
# compiled files
*.com
*.class
*.dll
*.exe
*.o
*.so
# packages
*.7z
#*.jar
*.rar
*.zip
*.gz
*.bzip
*.xz
*.lzma
*~$*
# package managment formats
*.dmg
*.xpi
*.gem
*.egg
*.deb
*.rpm
# databases
*.sqlite

345
README.adoc Normal file
View File

@ -0,0 +1,345 @@
:source-highlighter: coderay
:icons: font
:experimental:
:!sectnums:
:imagesdir: ./images/
: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
:AppGross: image:VorgabeApp_Gross.png[APPGROSS,300,role=left,fit=none,position=top left]
:AppKlein: image:VorgabeApp_Klein.png[APPKLEIN,220,role=left,fit=none,position=top left]
:AppSchmal: image:VorgabeApp_Schmal.png[APPSCHMAL,220,role=left,fit=none,position=top left]
:AppHilfe: image:AnzeigenHilfeBlau.png[APPHILFE,220,role=top,fit=none,position=top left]
:AppFehlend: image:FehlenderEintragRot.png[APPFEHLEND,220,role=top,fit=none,position=top left]
:AppKorrekt: image:KorrekteAusfuehrungGruen.png[APPKORREKT,220,role=top,fit=none,position=top left]
:MenuLoeschen: image:MenuLoeschen.png[MENULOESCHEN,175,role=top,fit=none,position=top left]
= {logo} Praktikum GUI
== Einleitung
_Das folgende Praktikum ist in zwei Teile unterteilt. +
Im ersten Teil ergänzen Sie das Resultat des Workshops um ein Model. +
Im zweiten Teil wird eine Applikation erstellt. Diese Applikation stellt keine Ansprüche an ein in der Praxis taugliches Tool. Bestimmt gibt es bessere Tools, die in den entsprechenden Bereichen eingesetzt werden. +
Es geht darum, dass Sie die in der Vorlesung vermittelten Inhalte ausprobieren und umsetzten können._
=== Ziele
* Sie können ein einfaches JavaFX GUI nach Vorgabe erstellen.
* Sie setzen die zur Verfügung stehenden Container (Panes) korrekt ein.
* Sie finden die entsprechenden Einstellungen in der Doku.
* Sie testen verschiedene Möglichkeiten für die Darstellung und das Event-Handling aus.
* Sie wenden das Model-View-Control Pattern korrekt an.
* Sie können ein GUI mit dem link:{url-scene-builder}[JavaFX Scene Builder] erstellen und FXML Dateien in die Anwendung integrieren.
=== Voraussetzungen
* Vorlesung: GUI Foundations, GUI Toolbox
=== Tooling
* Installiertes JDK 17+
* Gradle 7.3+
* JavaFX Scene Builder
Der link:{url-scene-builder}[JavaFX Scene Builder] wird im zweiten Teil des Praktikums verwendet. Hinweise zur Installation und finden sie dort.
=== Struktur
Ein Praktikum kann verschiedene Arten von Aufgaben enthalten, die wie folgt gekennzeichnet sind:
[TU] Theoretische Übung::
Dient der Repetition bzw. Vertiefung des Stoffes aus der Vorlesung und als Vorbereitung für die nachfolgenden Übungen.
[PU] Praktische Übung::
Übungsaufgaben zur praktischen Vertiefung von Teilaspekten des behandelten Themas.
[PA] Pflichtaufgabe::
Übergreifende Aufgabe zum Abschluss. Das Lösen dieser Aufgaben ist Pflicht. Sie muss bis zum definierten Zeitpunkt abgegeben werden, wird bewertet und ist Teil der Vornote.
=== Zeit und Bewertung
Für dieses Praktikum stehen 2 Wochen in den Praktikumslektionen und im Selbststudium zur Verfügung. +
Je nach Kenntniss- und Erfahrungsstufe benötigen Sie mehr oder weniger Zeit.
Nutzen Sie die Gelegenheit den Stoff zu vertiefen, Auszuprobieren, Fragen zu stellen und Lösungen zu diskutieren (Intensive-Track). +
Falls Sie das Thema schon beherrschen, müssen Sie nur die Pflichtaufgaben lösen und bis zum angegebenen Zeitpunkt abgeben (Fast-Track).
Die Pflichtaufgabe wird mit 0 bis 2 Punkten bewertet (siehe _Leistungsnachweise_ auf Moodle).
=== Referenzen
* link:{handout}[Praktikumsverzeichnis Quellcode, Projektstruktur]
* link:{url-openjfx}[OpenJFX Webseite]
* link:{url-openjfx-gradle}[OpenJFX Gradle Konfiguration]
* link:{url-openjfx-gradle-plugin}[OpenJFX Gradle Plugin]
* link:{url-openjfx-javadoc}[JavaFX 17 Javadoc Dokumentation]
* link:{url-scene-builder}[JavaFX Scene Builder Website]
:sectnums:
:sectnumlevels: 2
// Beginn des Aufgabenblocks
== Erweitern des Workshops um ein Model [PU]
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.
* 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.
** `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.
* Ändern Sie den Controller so ab, dass die Vorgaben im Ziel erreicht sind.
[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
Die Aufgaben in diesem Teil basieren auf der nachfolgend beschriebenen Übungsapplikation, die sie auf verschiedene Arten umsetzen.
=== Kurzbeschrieb
Der Capital Assets Calculator soll auf Basis von 4 Eingabewerten das voraussichtliche Kontovermögen für die kommenden Jahre berechnen. Die Eingabewerte sind:
* Kontovermögen beim Start (Initial amount)
* Jahreszins, der erwartet wird (Return in %)
* Kontoverwaltungskosten pro Jahr (Annual cost)
* Anzahl der Jahre, die berechnet werden sollen (Number of years)
Die daraus errechnete Vermögensentwicklung (pro Jahr eine Zeile) soll im Resultatbereich ausgegeben werden.
* Die Applikation soll die Eingabewerte prüfen und im Falle von ungültigen Werten eine Fehlermeldung im Resultatbereich ausgeben.
* Die Applikation soll ein Menü zur Verfügung stellen, welches ermöglicht:
** die eingegebenen Werte zu löschen
** den Resultatbereich zu löschen
** einen Hilfetext im Resultatbereich anzuzeigen.
=== Anforderungen an die Applikation
NOTE: Bitte beachten Sie die weiter unten folgenden Bilder der Applikation in den verschiedenen Zuständen.
* Nach dem Start der Anwendung wird das Hauptfenster angezeigt, in welchem der Benutzer folgende Aktionen auführen kann:
** Die 4 Werte eingeben.
** Mit btn:[Calculate] die Berechnung starten
** Nach der Berechnung wird das Resultat im Resultatbereich ausgegeben
** Der Rahmen des Resultatbereichs wird grün, wenn alles i.O. war
** Ist ein Eingabewert falsch oder unvollständig, so wird eine Meldung ausgegeben (roter Rahmen)
*** Initial amount: > 0
*** Return in %: Darf nicht leer sein
*** Annual cost: > 0 und darf nicht leer sein
*** Number of years: >0 und < 99, nur ganze Zahlen
** Das Menü `menu:Clear[]` enthält 6 Einträge:
*** Je einen Eintrag pro Eingabewert (diese können gewählt oder abgewählt werden), z.B. `menu:Clear[&check; Initial amount]`
*** Einen Eintrag `menu:Clear[Clear values]`, der alle Eingabewerte, die im Menu gewählt sind, wieder auf leer setzt
*** Einen Eintrag `menu:Clear[Clear results]`, der den Resultatbereich wieder zurücksetzt.
** Das Menü `menu:&quest;[]` enthält einen Eintrag:
*** `menu:&quest;[Show help]` zeigt einen Hilfetext im Resultatfenster an (blauer Rahmen).
*** Dies soll auch über die Taste kbd:[F1] ausgelöst werden können.
** Mit btn:[Close] wird die Applikation geschlossen.
TIP: Berechnen Sie den Wert des Vermögens für jedes Jahr mit: `Amount = Amount * (100% + ReturnIn%) - AnnualCost` oder verwenden Sie die bereitgestellte Klasse `ValueHandler`.
==== Bilder als Vorlage
[.float-group]
--
{AppKorrekt} {AppFehlend} {AppHilfe} {MenuLoeschen}
--
// Beginn des Aufgabenblocks
=== Überlegungen zum Einsatz von Containern (Panes)
In der Vorlesung wurden verschiedene Container vorgestellt, die Controls oder weitere Container enthalten können.
Die Kombination dieser Container trägt massgeblich zum Verhalten und zur Gestaltung eine Applikation bei. Vorgestellt wurden z.B.:
****
[horizontal]
GridPane:: Ordnet die Inhalte in Zeilen und Spalten an
HBox:: Ordnet die Inhalte in einer Zeile
VBox:: Ordnet die Inhalte in einer Spalte
BorderPane:: Ordnet die Inhalte in 5 Bereichen: Left, Right, Top, Bottom und Center
...:: weitere können Sie im Manual oder im Unterrichtsstoff nachschlagen...
****
Für die Benutzeroberfläche der zu erstellende Applikation sind die folgenden Anforderungen bekannt:
[cols="50,~",frame=none,grid=none]
|===
| {AppGross}
| Der Resultatbereich füllt den Platz des Fensters aus. +
Die Eingabefelder und die Beschriftungen sind links oben angeordnet und bleiben in der Grösse konstant. +
Das Menü ist oben-links angeordnet, die Buttons bleiben zentriert.
|===
[cols="40,~",frame=none,grid=none]
|===
| {AppKlein}
| Beim Verkleinern des Fensters verkleinert sich der Resultatbereich. +
Die Breite des Fensters soll limitiert werden, so dass die Beschriftungen lesbar bleiben.
|===
[cols="40,~",frame=none, grid=none]
|===
| {AppSchmal}
| Wird die Höhe des Fensters vergrössert, wächst der Resultatbereich mit.
|===
== 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.
=== Erstellen der Applikation [PU]
==== Basis
Als Basis finden Sie im link:{handout}[Praktikumsverzeichnis] das Projekt *Calculator* mit zwei bereitgestellten Klassen.
Sie sind nicht verpflichtet, diese Klassen zu verwenden.
Es steht Ihnen frei, eine eigene Struktur aufzubauen oder die Struktur zu erweitern.
`Main`::
Diese Klasse enthält das Grundgerüst der Hauptanwendung und muss erweitert werden.
`ValueHandler`::
Diese Klasse bietet Ihnen Hilfsfunktionen für die Prüfung der Eingabewerte und die Berechnung der Resultate.
==== Umsetzung
[loweralpha]
. Ergänzen Sie die Projektkonfiguration (Gradle) für den Einsatz von link:{url-openjfx}[JavaFX]
+
[NOTE]
====
Seit Java 11 ist JavaFX nicht mehr Teil der Java Standard Edition und wird unabhängig davon im link:{url-openjfx}[OpenJFX-Projekt] weiterentwickelt.
Für Projekte die Java 11 und neuer verwenden, muss es deshalb explizit zum Projekt hinzugefügt werden.
Da JavaFX neben generischen Libraries (API, Controls, FXML) auch Plattform-/Betriebssystemabhängige Komponenten benötigt, wird für Gradle (und Maven) ein PlugIn zur Verfügung gestellt, welches die notwendigen Komponenten (Dependencies) lädt und in den Prozess einbindet.
====
** Deklarieren und konfigurieren Sie das JavaFX-Plugin gemäss der link:{url-openjfx-gradle}[OpenJFX Anleitung für Gradle].
** Verwenden Sie die JavaFX-Version `'17'` (oder neuer)
. Erstellen Sie das Layout für die Applikation
** Bauen Sie den SceneGraph für die Anwendung gemäss Ihren Überlegungen aus der theoretischen Aufgabe zusammen und binden Sie diesen ins Hauptfenster ein.
. Fügen Sie die Handler für die benötigten Events hinzu.
** Entscheiden Sie, ob Sie innere Klassen verwenden wollen oder ob Sie mit anonymen Klassen arbeiten.
** Auch eine Kombination ist möglich (und eventuell sinnvoll?).
. 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]
Nachdem Sie sich bis hierher einige Gedanken zur Verwendung der Container (Panes) gemacht haben und praktische Erfahrung mit dem Aufbau des User Interfaces gesammelt haben, sind Sie nun bereit für die Umsetzung der Benutzeroberfläche mit FXML unter Verwendung des link:{url-scene-builder}[Scene Builder].
TIP: FXML unterstützt die Trennung von View und Controller.
Die Behandlung von Aktionen sollen deshalb konsequent in Methoden des Controllers ausgelagert werden und die Aktualisierung der GUI-Komponenten mittels JavaFX-Properties erfolgen.
****
SceneBuilder
SceneBuilder ist ein Werkzeug zur Bearbeitung von FXML-Dateien und ermöglicht somit die Erstellung und Bearbeitung eines SceneGraphs und die Verknüpfung mit einer zugehörigen Controller-Klasse.
SceneBuilder muss als eigenständiges Werkzeug link:{url-scene-builder}[heruntergeladen und installiert] werden.
[NOTE]
IDE's können den SceneBuilder einbinden (FXML-Code-Ansicht & Scene Builder-Ansicht), bzw. liefern bereits eine Version von SceneBuilder mit (IntelliJ).
Diese bietet jedoch meist nur einen eingeschränkten Funktionsumfang, weshalb es bei intensiver Arbeit trotzdem Sinn macht, die unabhängige Version zu installieren und aus der IDE aufzurufen (Dateipfad in Einstellungen konfigurieren und mit "Open In SceneBuilder" öffnen)
****
=== Basis
Als Basis finden Sie im link:{handout}[Praktikumsverzeichnis] das Projekt *FXML-Calculator* mit zwei bereitgestellten Klassen und einer leeren FXML-Datei `src/main/resources/ch/zhaw/prog2/fxmlcalculator/MainWindow.fxml`.
[IMPORTANT]
FXML-Dateien werden nicht kompiliert, müssen jedoch zur Laufzeit im Klassenpfad zur Verfügung stehen.
Gradle (und auch Maven) erwartet deshalb, dass Sie im Ressourcen-Ordner (default: `src/main/resources/`) abgelegt werden (am besten im gleichen Package bzw. Unterverzeichnis wie die zugehörigen Klassen `ch/zhaw/prog2/fxmlcalculator/`). Beim Erstellen der Anwendung werden Sie automatisch kopiert und beim Start in den Klassenpfad integriert.
Sie sind nicht verpflichtet, diese Klassen zu verwenden.
Es steht Ihnen frei, eine eigene Struktur aufzubauen oder die Struktur zu erweitern.
`Main`::
Diese Klasse enthält das Grundgerüst der Hauptanwendung und muss erweitert werden.
`MainWindowController`::
Diese Klasse ist fast leer und muss erweitert werden. Sie ist insbesondere noch nicht mit dem Layout (`MainWindow.fxml`) verbunden.
`ValueHandler`::
Diese Klasse enthält Hilfsfunktionen für die Prüfung der Eingabewerte und die Berechnung der Resultate.
TIP: Falls Ihre IDE bereits ein Start-Projekt für FXML anbietet, können Sie auch dieses verwenden. Nehmen Sie sich die Zeit, die beiden Ansätze zu vergleichen und zu verstehen.
=== Umsetzung
[loweralpha]
. Verifizieren Sie die Projektkonfiguration in `build.gradle` für FXML. +
Da für FXML ein zusätzliches JavaFX-Modul (`javafx.fxml`) benötigt wird, muss dieses in in der Konfiguration des Plugins hinzugefügt werden.
. Erstellen Sie den SceneGraph mit Hilfe des SceneBuilder als FXML-Spezifikation, laden diesen und binden ihn im Hauptfenster ein.
** Als Vorlage können Sie die vorhandene `MainWindow.fxml` im Ressourcen-Ordner und die Hauptanwendungsklasse verwenden.
. Erstellen Sie die Controllerklasse und verknüpfen Sie die Controls und Actions mit dem FXML-SceneGraph
** Als Vorlage finden Sie im Praktiumsvezeichnis bereits eine Klasse für den Controller.
. Überlegen Sie sich, wie das vorhandene Datenfeld `resultBound` der Hilfsklasse `ValueHandler` verwenden werden kann, um die View (z.B. Textfeld im MainWindow) mittels Observer-Pattern zu aktualisieren, damit `ValueHandler` selbst keine Referenz auf die View oder den Controller benötigt.
** Welche Hilfsmittel bietet JavaFX dazu an?
** Passen Sie ihre Anwendung entsprechend an.
. Fügen Sie im MainWindow einen weiteren Button und zugehörige Aktion (e.g. `openResultWindow`) ein. Diese soll ein zusätzliches einfaches Resultatfenster öffnen.
** Als Vorlage finden Sie im Praktikumsverzeichnis bereits eine FXML-Datei (`ResultWindow.fxml`) und einen leeren Controller.
** Erweitern Sie den Controller von ResultWindow so, dass auch hier das Resultat angezeigt wird, sobald es in `ValueHandler` neu gerechnet wird.
** Verwenden Sie in beiden Views das gleiche `ValueHandler` Objekt.
=== Hinweise
Die Layout-Einstellungen für die verschiedenen Panes sind grundsätzlich so vorkonfiguriert, dass sich die Grösse der Panes nach dem vorhandenen und nach dem verwendeten Platz richtet.
Für die meisten Fälle ist das ok und Sie müssen nur an wenigen Stellen eingreifen.
Zum Beispiel um die Mindestbreite festzulegen.
WARNING: Die Anzeige des Previews stimmt nicht genau mit der echten Anzeige überein. Starten Sie zwischendurch das Programm neu, um die echte Ansicht zu sehen.
// Ende des Aufgabenblocks
:!sectnums:
== Abschluss
Stellen Sie sicher, dass die Pflichtaufgabe mittels `gradle run` gestartet werden kann und pushen Sie die Lösung vor der Deadline in Ihr Abgaberepository.

View File

@ -0,0 +1,45 @@
/*
* 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'
}
// Project/Module information
description = 'Lab02 Calculator'
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'
}
// 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
}
}

View File

@ -0,0 +1,60 @@
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;
public class Main extends Application {
private Stage primaryStage;
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();
// Create scene with root node with size
Scene scene = new Scene(rootPane, 600, 400);
// scene.getStylesheets().add(getClass().getResource("MyLabel.css").toExternalForm());
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();
} catch(Exception e) {
e.printStackTrace();
}
}
}

View File

@ -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;
}
}

View File

@ -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 Calculator'
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.fxmlcalculator.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
}
}

View File

@ -0,0 +1,32 @@
package ch.zhaw.prog2.fxmlcalculator;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
/**
* Main-Application. Opens the first window (MainWindow) and the common ValueHandler
* @author
* @version 1.0
*/
public class Main extends Application {
private ValueHandler valueHandler;
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
valueHandler = new ValueHandler();
mainWindow(primaryStage);
}
private void mainWindow(Stage primaryStage) {
//load main window
}
}

View File

@ -0,0 +1,22 @@
package ch.zhaw.prog2.fxmlcalculator;
import javafx.scene.Scene;
import javafx.scene.control.CheckMenuItem;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
/**
* Controller for the MainWindow. One controller per mask (or FXML file)
* Contains everything the controller has to reach in the view (controls)
* and all methods the view calls based on events.
* @author
* @version 1.0
*/
public class MainWindowController {
// please complete
}

View File

@ -0,0 +1,25 @@
package ch.zhaw.prog2.fxmlcalculator;
import javafx.scene.control.TextArea;
import javafx.stage.Stage;
/**
* Controller for the MainWindow. One controller per mask (or FXML file)
* Contains everything the controller has to reach in the view (controls)
* and all methods the view calls based on events.
* @author
* @version 1.0
*/
public class ResultWindowController {
// add datafields
//@FXML
private TextArea results;
//@FXML
private void closeWindow() {
Stage stage = (Stage) results.getScene().getWindow();
stage.close();
}
}

View File

@ -0,0 +1,139 @@
package ch.zhaw.prog2.fxmlcalculator;
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;
}
}

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane xmlns:fx="http://javafx.com/fxml/1">
<!-- TODO Add Nodes -->
</AnchorPane>

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextArea?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.Font?>
<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="250.0" minWidth="400.0" prefHeight="250.0" prefWidth="400.0"
xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="ch.zhaw.prog2.fxmlcalculator.ResultWindowController">
<center>
<VBox minWidth="300.0" BorderPane.alignment="TOP_CENTER">
<children>
<VBox spacing="5.0" VBox.vgrow="ALWAYS">
<VBox.margin>
<Insets top="10.0" />
</VBox.margin>
<children>
<Label text="Results">
<font>
<Font name="System Bold" size="12.0" />
</font>
</Label>
<TextArea fx:id="results" editable="false" minHeight="100.0" VBox.vgrow="ALWAYS" />
</children>
</VBox>
</children>
<padding>
<Insets left="10.0" right="10.0" />
</padding>
</VBox>
</center>
<bottom>
<HBox alignment="BOTTOM_RIGHT" BorderPane.alignment="CENTER">
<children>
<Button fx:id="closeForm" mnemonicParsing="false" onMouseClicked="#closeWindow" text="Close">
<HBox.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</HBox.margin>
</Button>
</children>
<BorderPane.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</BorderPane.margin>
</HBox>
</bottom>
</BorderPane>

View File

@ -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'
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.WordModel'
}
// 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
}
}

View File

@ -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);
}

View File

@ -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();
}

View File

@ -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();
}
}

View File

@ -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();
}
}
}

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane xmlns:fx="http://javafx.com/fxml/1">
<!-- TODO Add Nodes -->
</AnchorPane>

7
gradle.properties Normal file
View File

@ -0,0 +1,7 @@
# Used to set properties for gradle builds
# (see https://dev.to/jmfayard/configuring-gradle-with-gradle-properties-211k)
# gradle configuration
# (https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties)
#org.gradle.warning.mode=(all,fail,summary,none) default: summary
org.gradle.warning.mode=all

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

234
gradlew vendored Executable file
View File

@ -0,0 +1,234 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

89
gradlew.bat vendored Normal file
View File

@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

BIN
images/MenuLoeschen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
images/PROG2-300x300.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

BIN
images/VorgabeApp_Gross.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

BIN
images/VorgabeApp_Klein.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

45
settings.gradle Normal file
View File

@ -0,0 +1,45 @@
/*
* Dynamic Multi-Module project structure
* automatically adds each exercise as a sub-project (module)
*/
// use current directory name as root project name
rootProject.name = file('.').name
// dynamically add sub-projects in handout folder
File handoutDir = file('code')
if (handoutDir.isDirectory()) {
handoutDir.eachDir { dir ->
String subProjectName = ":${dir.name}"
include(subProjectName)
project(subProjectName).projectDir = dir
}
}
// dynamically add sub-projects in solutions* folders
//List<File> solutionDirs = List.of(file('.').listFiles((File dir, String name) -> name.startsWith("solutions")))
file('.').eachDirMatch( name -> name.startsWith('solutions')) { solutionDir ->
if (solutionDir.isDirectory()) {
solutionDir.eachDir { dir ->
if (!dir.name.equals('images')) {
String subProjectName = ":${dir.name}-sol"
include(subProjectName)
project(subProjectName).projectDir = dir
}
}
}
}
// lab preparation tasks
File classroomDir = file('classroom')
if (classroomDir.isDirectory()) {
String subProjectName = ":${classroomDir.name}"
include(subProjectName)
}
// Example: manually adding sub-project with name == folder
//include 'module1'
// Example: manually adding sub-projects with different name & folder
//include(':lab00-module1')
//project(':lab00-module1').projectDir = file('handout/module1')

View File

@ -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
}
}

View File

@ -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();
}
}
}
}

View File

@ -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;
}
}

View File

@ -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
}
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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();
}

View File

@ -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();
}
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}
}

View File

@ -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

View File

@ -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