Initial commit
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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[✓ 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:?[]` enthält einen Eintrag:
|
||||
*** `menu:?[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.
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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 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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
@ -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>
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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,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,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>
|
||||
|
|
@ -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
|
|
@ -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
|
|
@ -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" "$@"
|
|
@ -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
|
After Width: | Height: | Size: 104 KiB |
After Width: | Height: | Size: 78 KiB |
After Width: | Height: | Size: 102 KiB |
After Width: | Height: | Size: 55 KiB |
After Width: | Height: | Size: 121 KiB |
After Width: | Height: | Size: 77 KiB |
After Width: | Height: | Size: 57 KiB |
After Width: | Height: | Size: 64 KiB |
|
@ -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')
|
|
@ -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>
|
After Width: | Height: | Size: 81 KiB |
After Width: | Height: | Size: 65 KiB |
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.
|
||||
****
|