Basis-App für weitere Themen
1. Überblick
in Arbeit ...
Folgende verwendeten Konzepte sind "erwähnenswert":
-
Alert-Box (Klasse Alert)
-
Nutzung des Rückgabewerts einer Zuweisung
-
Trennung GUI von GUI-unabhängigen Funktionalitäten (Klasse
Engine
) -
Properties
-
assertions (Behauptungen): assert (Behauptungs-Aussage) : "Fehlermeldung" (erfordert VM-Param -ea oder -enableassertions, sonst nicht ausgeführt!)
-
Menüs (MenuBar, Menu, MenuItem)
-
TabPane, Tab
-
TextArea
-
UserData (für vermutlich jedes GUI-Element von JavaFX) –
aGuiElem.setUserData(Object obj); (TypeOfObj)aGuiElem.getUserData()
. Damit können mit dem jeweiligen GUI-Objekt in Zusammenhang stehende Objekte für dieses leicht erreichbar gemacht werden -
Button, MenuItem – setOnAction((AtionEvent event) → { … });
-
File
-
FileReader, FileWriter, BufferedReader, BufferedWriter
-
FileChooser
-
Image, ImageView
-
Hyperlink
-
App-Icon
-
Einstellungen bei Programm-Ende "merken", bei Start wiederherstellen
-
Text Blocks mit Newlines etc., eingeschlossen in """ … """, kleinste Einrückung wird überall "abgezogen"
-
Methoden "compose…(…)" oder "setup…(…)" zum Konfigurieren logischer GUI-Einheiten
-
…
2. Applikations-Klasse 'JfxApp04b'
package my.pkg;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import javafx.application.Application;
import javafx.collections.ListChangeListener;
import javafx.collections.ListChangeListener.Change;
import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import my.pkg.duty.QuickNote;
import my.pkg.engine.Engine;
/**
* App besteht aus mehreren eigenen Klassen.
* Beginn der Trennung von UI und Nicht-UI-Funktionalitäten.
*
* @author renkin@spengergasse.at
* @version 2022-03-02
*
*/
public class JfxApp04b extends Application {
private static final File CONF_FILE = new File("./app-conf.properties"); // in project basedir
private static final String CONF_WINPOS_X = "winPos_X"; // Property key
private static final String CONF_WINPOS_Y = "winPos_Y"; // --''--
private static final String DEFAULT_WINPOS_X_TXT = "60"; // Default Property value (text!!)
private static final String DEFAULT_WINPOS_Y_TXT = "120"; // --''--
public static final String MENU_FILE = "menu-file";
public static final String MENU_TABS = "menu-tabs";
public static final String MENU_HELP = "menu-help";
private final Engine engine;
private final Properties confProps = new Properties();
private final List<QuickNote> quickNotes = new ArrayList<>();
private Stage primaryStage;
private TabPane tabPane;
private MenuBar menuBar;
private Menu tabMenu;
private int nextQuickNoteId = 1; // Wird nach Erzeugung einer QuickNote erhöht.
public JfxApp04b() {
engine = new Engine();
}
public static void main(String[] args) {
launch(args);
}
public Engine getEngine() {
return engine;
}
public void init() throws Exception {
super.init();
System.out.println("init() - initialization (BEFORE access to GUI) called");
loadConfig();
}
@Override
public void stop() throws Exception {
// Aufraeumen - MIT Zugriff auf GUI
System.out.println("stop() called");
storeConfig();
super.stop();
}
@Override
public void start(Stage primaryStage) {
this.primaryStage = primaryStage;
configWindow();
BorderPane root = new BorderPane();
Scene scene = new Scene(root /*, 400, 400*/);
primaryStage.setScene(scene);
// Stylesheet einbinden:
String cssUrl = getClass().getResource("jfx-app-04.css").toExternalForm();
scene.getStylesheets().add(cssUrl);
System.out.format("CSS-File URL is '%s'%n", cssUrl);
// MenuBar in Top-Bereich setzen:
root.setTop(this.menuBar = composeMenuBar());
// tabPane als BasisContainer des Center setzen:
root.setCenter(this.tabPane = composeTabPane(1)); // Tab-Nr 1, 2 oder 3 aktivieren
// Anzeige des Fensters:
primaryStage.show();
}
public void activateTab(Tab tab) {
assert (tabPane.getTabs().contains(tab)) : "TabPane does not contain %s".formatted(tab);
System.out.format("Aktiviere Tab '%s'%n", tab.getText());
tabPane.getSelectionModel().select(tab);
}
public Stage getPrimaryStage() {
return primaryStage;
}
public boolean quickNotesRemove(QuickNote qNote) {
boolean done = quickNotes.remove(qNote);
System.out.format("Nach Entfernen: Anzahl QuickNotes=%d%n", quickNotes.size());
return done;
}
public int quickNotesSize() {
return quickNotes.size();
}
public QuickNote quickNote4Tab(Tab tab) { // not used at the moment
return (QuickNote)tab.getUserData(); // viel besser! Erfordert Setzen nach Tab-Erzeugen
// for (QuickNote qNote : quickNotes) {
// if (tab.equals(qNote.getMyTab())) {
// return qNote;
// }
// }
// return null;
}
//////// private methods from here: ////////
private MenuBar composeMenuBar() {
MenuBar menuBar = new MenuBar();
//
Menu fileMenu = new Menu("Datei");
MenuItem mniCreateFile = new MenuItem("Neue Datei");
fileMenu.getItems().add(mniCreateFile);
//
tabMenu = new Menu("Tabs"); // InstanceVar, needed to change dynamically!
MenuItem mniCreateNoteTab = new MenuItem("Neuer Notiz-Tab");
tabMenu.getItems().add(mniCreateNoteTab);
mniCreateNoteTab.setOnAction((ActionEvent event) -> {
System.out.println("MenuItem 'mniCreateNoteTab' aktiviert");
QuickNote qNote = new QuickNote(this, nextQuickNoteId++, tabPane, tabMenu);
quickNotes.add(qNote);
System.out.format(
"Nach Hinzufügen: Anzahl QuickNotes=%d%n".formatted(this.quickNotesSize()));
qNote.setupMyTab(-1);
});
//
Menu helpMenu = new Menu("Hilfe");
MenuItem mniAboutBox = new MenuItem("Über diese App ...");
helpMenu.getItems().add(mniAboutBox);
mniAboutBox.setOnAction(event -> {
System.out.println("MenuItem 'aboutBox' aktiviert");
Alert alert = new Alert(AlertType.INFORMATION);
alert.setTitle("App-Info");
alert.setHeaderText("Was ist diese App");
alert.setContentText("Das ist die weitaus faszinierendste App");
alert.initOwner(primaryStage);
alert.showAndWait();
});
//
menuBar.getMenus().addAll(fileMenu, tabMenu, helpMenu);
return menuBar;
}
private TabPane composeTabPane(int activeTabNr) {
assert (activeTabNr >= 1) : "invalid activeTabNr (1..n): %d".formatted(activeTabNr);
tabPane = new TabPane();
tabPane.setPrefSize(800.0, 500.0);
// Change-Handling auch von TabPane-Tabs-Liste aus möglich:
tabPane.getTabs().addListener((Change<? extends Tab> chg) -> {
while (chg.next()) { // nur das Entfernen wird hier beachtet:
if (chg.wasRemoved()) {
Tab removed = chg.getRemoved().get(0);
System.out.format("TabPane: Tab '%s' wird entfernt%n".formatted(removed.getText()));
}
}
});
Tab tab1 = new Tab("Tab-" + nextQuickNoteId++);
tab1.setClosable(false);
tab1.setContent(new VBox());
VBox tab1Vbox = (VBox) tab1.getContent();
tab1Vbox.getChildren().add(setupHyperlinkHbox( //
"Link zu", "", "RX Java-Unterlagen",
"https://htlw5.renkin.webspace.spengergasse.at/2xhif/"));
tab1Vbox.getChildren()
.add(setupHyperlinkHbox("Video-Link-1", "",
"Fraktal-Zoom: Mandelbrot-Menge (YouTube)",
"https://www.youtube.com/watch?v=01kUkiVsuV8"));
tab1Vbox.getChildren()
.add(setupHyperlinkHbox("Video-Link-2", "",
"A Cycling Journey - Mandelbrot Fractal Zoom (YouTube)",
"https://www.youtube.com/watch?v=a3XDry3EwiU"));
tab1Vbox.getChildren()
.add(setupHyperlinkHbox("Video-Link-3", "",
"Mandelbrot-Menge Erklärung+Impl. (YouTube)",
"https://www.youtube.com/watch?v=LaHSbUWAQUE"));
// Seit Java 15 verfügbar: "Text Blocks mit Newlines etc.", eingeschlossen in """ ... """
// https://www.baeldung.com/java-multiline-string [Java Multi-line String | Baeldung]
// https://www.baeldung.com/java-text-blocks [Java Text Blocks | Baeldung]
String imgDescr = """
Die Mandelbrot-Menge ist durch eine äußerst simple Formel aus komplexen Zahlen definiert.
Die Pixel der Fläche mit ihrer jeweiligen Farbe werden durch Einsetzen jedes enthaltenen
Pixels mit Koordinaten
x: Realteil, y: Imaginärteil
in die Formel und Berechnung des "Konvergenzradius"
des jeweiligen Punktes berechnet.
Das existenziell Wichtige dieser Struktur ist, dass aus einer sehr kompakten, endlichen
algorithmischen Struktur eine UNENDLICHE GEORDNETE KOMLEXITÄT entsteht.
Der einzig potentiell unendliche Input daran ist der Rechenaufwand - quasi die "Beschäftigung" damit.
Diese Unendlichkeit ist in keiner Weise "fad" wie eine Gerade,
sondern erzeugt ein beim Hineinzoomen NIE ENDENDES, geordnetes und schönes Gebilde.
* Die ersten beiden Video-Links oben zeigen das Hineinzoomen - quasi eine "Reise" in dieses Gebilde.
* Video-Link-2 davon enthält eine sehr lange "Reise" (3 Stunden bei Normal-Geschwindigkeit)
* Der Video-Link-3 enthält eine Erläuterung und Programmier-Anleitung (in C++, Idee leicht übertragbar).
"""; // Kleinste Einrückung wird überall "abgezogen"
tab1Vbox.getChildren().add(setupImageWithDescrHbox( //
"file:resources/images/wikimedia-1280px-mandel-zoom07-satellite.jpg", 300.0,
imgDescr, //
(MouseEvent event) -> {
System.out.format("Mausklick auf Bild (neu)%n");
}));
tabPane.getTabs().addAll(tab1);
activateTab(activeTabNr);
tab1.setOnSelectionChanged((Event event) -> {
if (tab1.isSelected()) {
System.out.println("Tab 1 wurde jetzt aktiviert");
} else {
System.out.println("Tab 1 ist jetzt nicht mehr selektiert");
}
});
tab1.setOnClosed((Event event) -> { // wird nie aktiv wegen tab1.setClosable(false)
System.out.println("Tab 1 wurde jetzt geschlossen");
});
return tabPane;
}
private HBox setupImageWithDescrHbox(String imgUrl, double width, String description,
EventHandler<MouseEvent> imgClickEvtHdlr) {
TextArea textArea = new TextArea();
textArea.setEditable(false);
textArea.setWrapText(true);
textArea.setText(description);
HBox hbox = new HBox(setupImageView(imgUrl, width, imgClickEvtHdlr), textArea);
return hbox;
}
private ImageView setupImageView(String imgUrl, double width,
EventHandler<MouseEvent> imgClickEvtHdlr) {
URL imgURL = null;
try {
imgURL = new URL(imgUrl); //TODO: nach Test korrigieren!
} catch (MalformedURLException e) {
throw new IllegalArgumentException("Fraktal-Bild-URL ungültig!", e);
}
String imgUrlTxt = imgURL.getPath(); // für Test ungültige URL: 2 + "--";
System.out.format("ImgUrl Fraktal: '%s'%n", imgUrlTxt);
Image img = null;
try {
img = new Image(new FileInputStream(imgUrlTxt));
} catch (FileNotFoundException e) {
throw new IllegalArgumentException(
"Fraktal-Bild-URL zeigt nicht auf existierende Datei", e);
}
//Kompakte Alternative (aber BUG: fehlendes Bild nicht erkannt!!): img = new Image(imgUrl);
// assert (img != null) : "Img für '%s' ist null".formatted(imgUrl);
// assert (new File(imgUrl).exists()) : "Img für '%s' existiert nicht".formatted(imgUrl);
ImageView imgView = new ImageView(img);
imgView.setFitWidth(width);
imgView.setPreserveRatio(true);
imgView.setOnMouseClicked(imgClickEvtHdlr);
return imgView;
}
private HBox setupHyperlinkHbox(String preText, String endText, String linkText, String url) {
Label lblPre = new Label(preText);
Label lblEnd = new Label(endText);
Hyperlink hylUrl = new Hyperlink(linkText);
HBox hbox = new HBox(lblPre, hylUrl, lblEnd);
hbox.setStyle("-fx-alignment: baseline-left"); // damit Texte auf gleicher Linie sitzen
hylUrl.setOnAction((ActionEvent event) -> {
System.out.println("hyperlink clicked");
this.getHostServices().showDocument(url);
});
return hbox;
}
private void activateTab(int tabNr) { // tabNr 1, 2, 3, 4, ...
int maxTabNr = tabPane.getTabs().size();
assert (tabNr >= 1 && tabNr <= maxTabNr) : "ungültige TabNr: %d".formatted(tabNr); //TODO: rethink!
tabPane.getSelectionModel().select(tabNr - 1); // tab mit tabNr aktiv machen
}
private void configWindow() {
assert (this.primaryStage != null) : "needed field 'primaryStage' not set yet";
System.out.println("configWindow() called");
// der Fenstertitel wird gesetzt (wie HTML-<title>):
primaryStage.setTitle("Mein erstes JavaFX-Programm");
// Das App-Icon wird gesetzt:
String iconUrl = getClass().getResource("htlw5-logo1.png").toExternalForm();
primaryStage.getIcons().add(new Image(iconUrl));
// Die beim letzten Programmlauf gespeicherte Fenster-Position wird wieder verwendet:
String winPosX_txt = confProps.getProperty(CONF_WINPOS_X, DEFAULT_WINPOS_X_TXT);
primaryStage.setX(Double.parseDouble(winPosX_txt));
String winPosY_txt = confProps.getProperty(CONF_WINPOS_Y, DEFAULT_WINPOS_Y_TXT);
primaryStage.setY(Double.parseDouble(winPosY_txt));
}
private void loadConfig() {
System.out.println("loadConfig() called");
assert (confProps != null) : "no confProps set"; // compact way to check preconditions
if (CONF_FILE.exists()) {
try (FileReader fr = new FileReader(CONF_FILE)) {
confProps.load(fr);
} catch (IOException e) {
System.out.format("Problem reading config file: '%s'%n", e.getMessage());
}
}
}
private void storeConfig() {
System.out.println("storeConfig() called");
assert (confProps != null) : "no confProps set"; // compact way to check preconditions
// collecting all current settings within confProps:
confProps.setProperty(CONF_WINPOS_X, "" + primaryStage.getX());
confProps.setProperty(CONF_WINPOS_Y, "" + primaryStage.getY());
//
try (FileWriter fw = new FileWriter(CONF_FILE)) {
confProps.store(fw, "JfxApp Config");
} catch (IOException e) {
System.out.format("Problem beim Schreiben von %s: %s%n", //
CONF_FILE.getAbsolutePath(), e.getMessage());
}
}
}
3. Klasse 'QuickNote'
package my.pkg.duty;
import java.io.File;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.List;
import javafx.collections.ListChangeListener;
import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.scene.control.Alert;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextArea;
import javafx.scene.control.Alert.AlertType;
import javafx.stage.FileChooser;
import javafx.stage.FileChooser.ExtensionFilter;
import my.pkg.JfxApp04b;
/**
* Klasse zum Verwalten einer Notiz als Tab incl. nötiger Menü-Einträge.
*
* @author renkin@spengergasse.at
* @version 2022-03-02
*/
public class QuickNote {
private JfxApp04b app;
private int tabId;
private TabPane containingTabPane;
private Menu containingMenu;
private Tab myTab;
private MenuItem mniSaveNote;
private MenuItem mniLoadNote;
private TextArea noteTextArea;
public QuickNote(JfxApp04b app, int tabId, TabPane containingTabPane, Menu containingMenu) {
this.app = app;
this.tabId = tabId;
this.containingTabPane = containingTabPane;
this.containingMenu = containingMenu;
this.mniSaveNote = new MenuItem();
this.mniLoadNote = new MenuItem();
this.noteTextArea = new TextArea();
this.noteTextArea.setEditable(true);
this.noteTextArea.setWrapText(true);
}
public void setupMyTab(int tabIdx) {
assert (myTab == null) : "myTab already instantiated";
System.out.format("'setupMyTab(%d)' aktiv%n", tabIdx);
myTab = new Tab();
myTab.setUserData(this); // Speichere Referenz auf die "Besitzer-QuickNote"
List<Tab> tabs = containingTabPane.getTabs();
if (tabIdx < 0) {
tabs.add(myTab);
myTab.setText("Tab " + tabId);
} else {
//TODO: check if valid position
tabs.set(tabIdx, myTab);
// assuming no individual/semantic names are set:
for (int i = tabIdx; i < tabs.size(); i++) {
tabs.get(i).setText("" + (i + 1));
}
}
myTab.setContent(noteTextArea);
myTab.setOnSelectionChanged((Event event) -> {
if (myTab.isSelected()) {
System.out.format("'%s' wurde selektiert%n", myTab.getText());
addMenuItems(myTab.getText());
} else {
System.out.format("'%s' wurde inaktiv%n", myTab.getText());
removeMenuItems();
}
});
myTab.setOnClosed((Event event) -> {
//assert (false) : "RX-Probe-Assertion";
System.out.format("'%s' wird jetzt geschlossen%n", myTab.getText());
boolean done = app.quickNotesRemove(this);
assert (done) : "QuickNote zu Tab '%s' nicht entfernbar".formatted(myTab.getText());
//System.out.format("Nun Anzahl QuickNotes=%d%n".formatted(app.quickNotesSize()));
});
app.activateTab(myTab);
}
public Tab getMyTab() {
return myTab;
}
///////////////// private methods from here ////////////////////
/**
* Adds its MenuItems as soon as the tab gets active.
* @param containingMenu the Menu where the menuItems should be added
*/
private void addMenuItems(String tabName) {
assert (!containingMenu.getItems().contains(mniLoadNote))
: "MenuItem 'mniLoadNote' already there";
containingMenu.getItems().addAll(mniLoadNote, mniSaveNote);
mniLoadNote.setText("'%s' Notiz laden".formatted(tabName));
mniSaveNote.setText("'%s' Notiz speichern".formatted(tabName));
mniLoadNote.setOnAction((ActionEvent event) -> {
String txt = loadNoteFromFile();
noteTextArea.setText(txt);
});
mniSaveNote.setOnAction((ActionEvent event) -> {
saveNoteToFile();
});
}
/**
* Removes this QuickNotes MenuItems as soon as tab gets inactive.
* @param containingMenu the Menu fron which the menuItems should be removed
*/
private void removeMenuItems() {
assert (containingMenu.getItems().contains(mniLoadNote))
: "MenuItem 'mniLoadNote' is not there";
containingMenu.getItems().removeAll(mniLoadNote, mniSaveNote);
}
private void saveNoteToFile() {
FileChooser fileChooser = new FileChooser();
fileChooser.setInitialDirectory(new File(System.getProperty("user.home")));
fileChooser.setInitialFileName(
"note_%1$tY-%1$tm-%1$td_%1$tH-%1$tM.txt".formatted(LocalDateTime.now()));
fileChooser.setTitle("Notiz speichern");
fileChooser.getExtensionFilters().addAll(new ExtensionFilter("Text Files", "*.txt"),
new ExtensionFilter("All Files", "*.*"));
File selectedFile = fileChooser.showSaveDialog(app.getPrimaryStage());
if (selectedFile != null) {
app.getEngine().saveNote(selectedFile, noteTextArea.getText());
}
}
private String loadNoteFromFile() {
String txt = "";
FileChooser fileChooser = new FileChooser();
fileChooser.setInitialDirectory(new File(System.getProperty("user.home")));
fileChooser.setTitle("Notiz laden");
fileChooser.getExtensionFilters().addAll(new ExtensionFilter("Text Files", "*.txt"),
new ExtensionFilter("All Files", "*.*"));
File selectedFile = fileChooser.showOpenDialog(app.getPrimaryStage());
if (selectedFile != null) {
try {
txt = app.getEngine().loadNote(selectedFile);
} catch (IOException e) {
Alert alert = new Alert(AlertType.ERROR);
alert.setTitle("Laden einer Notiz fehlgeschlagen");
alert.setHeaderText("Laden in Notiz %s nicht gelungen"
.formatted(selectedFile.getAbsolutePath()));
alert.setContentText(e.getMessage());
alert.initOwner(app.getPrimaryStage());
alert.showAndWait();
}
}
return txt;
}
}
4. Klasse 'Engine'
package my.pkg.engine;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* Zuständig für alles, was unabhängig vom GUI getan wird.
* @author renkin@spengergasse.at
* @version 2022-03-02
*/
public class Engine {
public Engine() {
// dzt. ohne Inhalt
}
public void saveNote(File file, String txt) {
System.out.println("engine-saveNote called - TODO: implement!");
//TODO: implement
}
public String loadNote(File file) throws FileNotFoundException, IOException {
System.out.println("engine-loadNote called");
String txt = "";
try (FileReader fr = new FileReader(file);
BufferedReader br = new BufferedReader(fr);) {
String line;
int idx = 0;
while ((line = br.readLine()) != null) {
txt += line;
}
}
return txt;
}
}
5. App-Icon und CSS-Datei 'jfx-app-04.css'
Hier das verwendete App-Icon:
.root { /*Basis-CSS-Klasse - dem root-Element der Scene zugeordnet*/
-fx-font-size: 12pt;
-fx-font-family: "Sans Serif";
/* -fx-text-fill: blue; */
/* -fx-background: rgb(32, 128, 128); */
}
#lblHello {
-fx-font-family: Serif;
}
.lblHelloClass {
-fx-font-weight: bold;
}
#btn2 {
-fx-font-family: Helvetica, Arial, Sans-Serif;
}
/*
VBox {
-fx-padding: 10 10 10 10;
-fx-background: green;
}
*/
Tab {
/*
-fx-padding: 10 10 10 10;
-fx-background: green;
*/
}
/* .tab-content-area > * */
.tab-content-area {
-fx-padding: 10 10 10 10;
/* -fx-background: green; */
}
6. Mandelbrotmengen-Bild
Im Projekt verwendeter Pfad:
PROJ-BASIS-DIR/resources/images/wikimedia-1280px-mandel-zoom07-satellite.jpg
.
Downloadbar (muss umbenannt werden) von upload.wikimedia.org | 1280px-Mandel_zoom_07_satellite.jpg (1280×960)
Hier die verwendete Bild-Datei (Kopie in der Unterlage, mit angepasstem Namen):
