Des Kaisers neue Kleider

Migration von Swing auf JavaFX: ein Tutorial unter Einbezug des MVC-Patterns

Anatole Tresch

©Shutterstock / Billion Photos

Obwohl JavaFX 2.0 bereits 2011 [1] erschienen ist, verfügen etliche Java-Anwendungen noch über eine Java-Swing-Oberfläche, so auch die Back-up-Software Areca oder MediathekView, die Sendungen der öffentlich-rechtlichen Rundfunkanstalten zum Download anbietet. Da Oracle JavaFX als Nachfolger von Swing betrachtet, zeigen wir, wie ein Umstieg auf JavaFX erfolgen kann. Das Tutorial berücksichtigt das Model-View-Controller-Pattern (MVC), auf dem Anwendungen mit grafischer Oberfläche üblicherweise basieren.

2018 gab Facebook bekannt, den Datenzugriff auf Facebook einzuschränken. Anwendungen von Drittanbietern sollen nur noch auf das Facebook API zugreifen können, wenn Facebook und ein Administrator es erlauben [2]. Aktuell wird über die Einschränkung des Internets diskutiert, weil durch Ausgangsbeschränkungen Mediatheken Hochkonjunktur erleben und es zu sogenannten Datenstaus kommt. Nicht mehr alle Informationen sollen für die Massen zugänglich sein. Es lohnt sich, in Anwendungen für mobile Geräte und für Computer zu investieren, die nur nach Log-in funktionieren.

Android-Apps und etliche Computerprogramme werden in Java programmiert. Die meisten denken bei Java-Anwendungen nur an öde aussehende Programme, basierend auf Swing. Dabei lassen sich dank der Einbindung von CSS nicht nur Anwendungen mit einer umwerfenden grafischen Oberfläche erstellen, sondern sogar Spiele animieren. Weitere Features von JavaFX [3]:

  • JavaFX gibt es schon seit einiger Zeit als Add-on-Paket für die Java-Entwicklungsumgebung, es ist aber erst seit Java 8 fest inkludiert. Ihr könnt JavaFX also auf allen Geräten laufen lassen, die JDK 8 oder höher unterstützen: z. B. Smartphones, Tablets, Computer.

  • Oracle betrachtet JavaFX als den Nachfolger von Swing und implementiert daher neue Features ausschließlich für JavaFX.

  • JavaFX nutzt Begriffe aus der Welt des Theaters, um die Features zu beschreiben. Die Stage (Bühne) stellt den obersten Container dar und kann sich über die gesamte Anzeigenfläche erstrecken. Bei einem Theaterstück wechseln die Szenen, doch ist immer nur eine Szene gleichzeitig zu sehen. So ist es auch bei JavaFX: Die Stage beinhaltet mehrere Scenes, doch kann nur eine Scene zur selben Zeit aktiv sein.

  • Die Komponenten eines Anwendungsfensters wie Buttons, Checkboxen oder Labels werden in einer Scene gespeichert. Sie werden Nodes (Knoten) genannt. Auch Layoutmanager und geometrische Formen gehören zur Klasse der Nodes.

  • JavaFX unterstützt das Konzept von Properties. Eine Property ist eine Variable, deren Wert sich beobachten lässt. Ein Listener wird an eine Property gehängt, und sobald sich der Wert der Variable ändert, wird ein Ereignis ausgelöst. Mögliche Folgen können z. B. die Änderung der Farbe einer Form sein.

  • JavaFX-Anwendungen lassen sich mit CSS layouten. Genauso wie bei Webanwendungen werden die Stildefinitionen in einer Datei mit der Endung .css gespeichert. Darüber hinaus können CSS-Klassen oder -IDs in einer .java-Datei definiert werden.

  • JavaFX bietet Spezialeffekte an, die auf jedem Knoten innerhalb der Scene angewandt werden können. Mit visuellen Effekten wie Unschärfen, Schatten oder Reflexionen lässt sich das Aussehen von Benutzeroberflächen verändern.

  • Die integrierte Unterstützung von Animationen ermöglicht es, Effekte wie Überblendungen oder Rotationen an einem x-beliebigen Knoten durchzuführen. Mit den KeyFrame– und Timeline-Klassen lassen sich benutzerdefinierte Animationen einrichten.

  • Spezialklassen erlauben das Bedienen der JavaFX-Anwendung auf Geräten mit Touchdisplay.

Abstecher zu Swing

Da vielen Unternehmen eine Anpassung ihrer Anwendung an JavaFX als zu umständlich erscheint, ist Swing sehr verbreitet [4]. Unter Swing erstellt man zunächst ein leeres Fenster, genannt Frame, das durch die JFrame-Klasse definiert wird. Dem leeren Fenster kann ein Panel der Klasse JPanel, das alle Komponenten der Benutzeroberfläche enthält, hinzugefügt werden. Eine Swing-Anwendung ist im Grunde eine Klasse, die die JFrame-Klasse erweitert. Anders als in JavaFX, wo es mehrere Layoutmanager gibt, existiert unter Swing nur das JPanel, dessen Layout ihr festlegen könnt. Events in Swing sind weniger durchdacht und nicht so konsistent wie in JavaFX. Swing besitzt grundlegende Steuerelemente: Schaltflächen, Kontrollkästchen, Kombinationsfelder und Ähnliches. Das Einrichten von Animationen ist unter Swing weniger komfortabel. Sowohl der Timer als auch die Logik müssen selbst implementiert werden. Swing unterstützt keine Touchdisplays. Swings JOptionpane-Klasse erlaubt es, kurze Warnmeldungen anzuzeigen oder einfache Eingaben vom Benutzer zu erhalten. So etwas existiert bei JavaFX nicht.

Das MVC-Pattern

In der Softwareentwicklung wendet man Patterns an, um eine Anwendung zu bauen. Das MVC-Pattern (Abb. 1) unterteilt eine Anwendung in drei miteinander verbundene Teile. Das Hauptziel des Patterns ist es, die interne Darstellungen einer Anwendung von der Art und Weise zu trennen, wie dem Benutzer Informationen präsentiert werden. Diese drei Komponenten bestimmen das MVC-Pattern [5]:

  1. Das Model managt die Daten. Es verwaltet Listen, Zustände und gegebenenfalls die Datenbank. Obwohl das Model sich der View und des Controllers nicht bewusst ist, stellt es eine Schnittstelle zur Verfügung, um Zustände zu manipulieren und abzurufen [6].

  2. Die View repräsentiert das Model. Die erforderlichen Zustände und Daten werden vom Model übermittelt.

  3. Der Controller beinhaltet die Logik der Anwendung. Er ist für das Event Handling zuständig.

Sobald der Anwender Aktionen an der View vornimmt, verarbeitet der Controller die Events und aktualisiert die Zustände des Models. Daraufhin schickt das Model die neuen Daten zur View.

Abb. 1: MVC-Pattern

Abb. 1: MVC-Pattern

Richtige Version von JDK und JavaFX

Zum Zeitpunkt der Niederschrift des Artikels können Windows- und Linux-User Version 13 der Java-Entwicklungsumgebung herunterladen [7]. OpenJDK gibt es immerhin in der Version 14 [8]. Nutzer des Raspberry Pi brauchen die Edition für Java Embedded, die jedoch nur in Version 8 verfügbar ist [9]. Java Embedded beinhaltet allerdings keine Unterstützung für JavaFX, sodass man JavaFX manuell installieren muss [10].

JavaFX unter Java Embedded aktivieren

Nach dem Entpacken der .zip-Datei kopiert ihr die JavaFX-Bibliotheken in den Ordner der Java-Embedded-Installation (Tabelle 1). Außerdem fehlt in der Java Embedded Edition die Klasse SwingFXUtils, die jedoch wichtig ist, sofern Dateien in die JavaFX-Anwendung geladen werden. Unter [11] könnt ihr die fehlende Klasse herunterladen.

Pfad JavaFX ARM Pfad Java Embedded
~/armv6hf-sdk/rt/lib/ext/jfxrt.jar ~/jdk1.8.0_xyz/jre/lib/ext/
~/armv6hf-sdk/rt/lib/arm/* ~/jdk1.8.0_xyz/jre/lib/arm/
~/armv6hf-sdk/rt/lib/javafx.platform.properties ~/jdk1.8.0_xyz/jre/lib/
~/armv6hf-sdk/rt/lib/javafx.properties ~/jdk1.8.0_xyz/jre/lib/
~/armv6hf-sdk/rt/lib/jfxswt.jar ~/jdk1.8.0_xyz/jre/lib/

Tabelle 1: JavaFX unter Java Embedded aktivieren

Vorstellung der Beispielanwendung

Alle hier vorgestellten Java-Snippets wurden mit der Java Embedded Edition JDK 1.8.0_231 erstellt. Sie lassen sich auch mit den Java-Versionen für die x86-64-Prozessoren kompilieren. Die Anwendung, ein Programm mit einer grafischen Oberfläche, wurde für ein fiktives Burgerrestaurant programmiert. Das Hauptmenü wird in Form von vier Tabs dargestellt, die das Startfenster (Abb. 2), ein Fenster zur Darstellung der Bestellungen (Abb. 3), die Artikelansicht (Abb. 4) sowie die Warenkorbansicht (Abb. 5) beinhalten. Klickt der Benutzer auf den Orders-Tab, wird ein Ereignis ausgelöst, das die Bestellungen anzeigt. Diese werden in einer Liste festgehalten, die aus den Attributen Bestellnummer, Bestelldatum, den bestellten Menüs, Endpreis und Name des Bestellers besteht. In einer echten Anwendung stammte die Liste aus einer Datenbank, aber für dieses Beispiel wird eine Liste in der Testklasse TestLists angelegt. Der User kann im Drinks-Tab Getränke in den Warenkorb legen, der im Basket-Tab abgerufen wird.

Abb. 2: Startfenster der Burgeranwendung (Quelle: Pixabay/Niek Verlaan)

Abb. 2: Startfenster der Burgeranwendung (Quelle: Pixabay/Niek Verlaan

Abb. 3: Im Orders-Tab werden die Bestellungen angezeigt

Abb. 3: Im Orders-Tab werden die Bestellungen angezeigt

Abb. 4: Eine Liste von Getränken wird im Drinks-Tab zum Verkauf angeboten

Abb. 4: Eine Liste von Getränken wird im Drinks-Tab zum Verkauf angeboten

Abb. 5: Auf den Warenkorb kann über den Basket-Tab zugegriffen werden

Abb. 5: Auf den Warenkorb kann über den Basket-Tab zugegriffen werden

Das Modell setzt sich u. a. aus den Enums Hamburger und Drink zusammen (Abb. 6, Listing 1). Sie beinhalten nicht nur die Attribute wie Name und Preis, sondern auch die nötigen Setter- und Getter-Methoden. Ein Menü kann sowohl aus einem Hamburger und einem Getränk als auch lediglich aus einem Hamburger oder einem Getränk bestehen. In der Menu-Klasse befinden sich nicht nur die erforderlichen Attribute, sondern der Menüpreis, der extra berechnet wird. Eine Bestellung wird erzeugt, indem eine Instanz der Order-Klasse angelegt wird. Die Order-Klasse enthält die Attribute Person, Liste der Menüs, Bestelldatum, Bestellnummer sowie den Gesamtpreis. Eine Person kann mehrere Menüs bestellen, die in einer Liste vom Typ Menu gespeichert werden. Zum Berechnen des Gesamtpreises iteriert eine Setter-Methode durch die Menüliste und addiert die Preise der einzelnen Menüs. Vor- und Nachname des Kunden werden in der Person-Klasse festgehalten.

public class ModelServer {
  // Locale wird zum Ermitteln der richtigen Währung benötigt
  private Locale locale;
  private Currency c;
  private ObservableList<Order> data;
  private ObservableList<Person> persons;
  private ArrayList<Menu> menus;
  private Stage primaryStage = null;

  public ModelServer(Stage primaryStage,Locale locale) {
    this.primaryStage = primaryStage;
    this.c = Currency.getInstance(locale);
    this.data = FXCollections.observableArrayList();
    this.persons = FXCollections.observableArrayList();
    this.menus = new ArrayList<Menu>(); 
  }
  public Stage getPrimaryStage() {
    return primaryStage; 
  }
  public ObservableList<Order> loadData() {
    return this.data; }
  
  // formatiert das Datum
  public String getDateTime(String pattern) {
    DateTimeFormatter dtf = DateTimeFormatter.ofPattern(pattern);
    LocalDateTime now = LocalDateTime.now();
    return dtf.format(now); 
  }
  public Locale getLocale() {
    return this.locale;
  }
  public String getCurrency() {
    return this.c.getCurrencyCode(); 
  }

  public void addOrder(Order o) {
    if (!this.data.contains(o)) {
      this.data.add(o); 
    }
  }

  // Bestellung zur Liste hinzufügen
  public void addOrder(Person p, ArrayList<Menu> l) {
    String date = getDateTime(Constants.DATETIME2);
    Order o = new Order(p, l, date, getCurrency() );
    addOrder(o); 
  }
  // Menü zur Menü-Liste hinzufügen
  public void addMenu(List<Menu> l,Menu m) {
    if (!l.contains(m)) {
      l.add(m);
    } 
  }
  public void addMenu(List<Menu> l,Hamburger h, int ah, Drink d, int ad ) {
    Menu m = new Menu( h,ah, d, ad );
    addMenu( l,m);
  }
  // eine Person zur Liste hinzufügen
  public void addPerson(String f, String l) {
    Person p = new Person(f,l);
    if (!this.persons.contains(p)) {
      this.persons.add(p);
    } 
  }
[...]
}

Die Klasse ModelServer verwaltet die Listen für Bestellungen, Menüs und Personen und stellt dazu Getter- und Setter-Methoden bereit. Außerdem gibt es hier Attribute bzw. Methoden, die von anderen Klassen benutzt werden, um z. B. das aktuelle Datum auszugeben oder die Währung des jeweiligen Landes zu ermitteln. Die TestLists-Klasse beherbergt Instanzen der Person-Klasse, die zur Liste der Personen in der ModelServer-Klasse hinzugefügt werden. Die Liste der Menüs wird ausschließlich in der Testklasse erzeugt. Um eine Bestellung zu generieren, wird der Methode placeOrder eine Liste von Menüs übergeben. Die Bestellung wird von einer zufällig gewählten Person vorgenommen.

Abb. 6: Auf dieses Model greifen sowohl die Swing- als auch JavaFX-Varianten zu

Abb. 6: Auf dieses Model greifen sowohl die Swing- als auch JavaFX-Varianten zu

Die Swing-Variante

Die Swing-Variante ist eine vereinfachte Version der weiter oben vorgestellten Anwendung. Sie zeigt lediglich die Bestellungen in einer Tabelle an (Abb. 7).

Abb. 7: Die Swing-Variante gibt die Liste der Bestellungen aus

Abb. 7: Die Swing-Variante gibt die Liste der Bestellungen aus

Das Modell dieser Anwendung braucht eine Klasse, ModelSwing, die die Klasse DefaultTableModel erweitert. Diese Klasse liefert die Daten für die Tabelle, wobei zum Erzeugen der Tabelle zwei Argumente erforderlich sind: Die Liste der Tabellenüberschriften sowie die dazugehörigen Daten. Außerdem gibt es eine eigene View-Klasse, ViewSwing, weil die Klassen zum Erzeugen der View nicht mit denen der JavaFX-Klassen identisch sind (Abb. 8). Dem Konstruktor des Controllers ControllerSwing wird eine Instanz der Klasse DefaultTableModel übergeben, damit der Controller beim Auslösen von Ereignissen die Liste bearbeitet. Außerdem überschreibt der Controller die Methoden der Klasse ActionListener, um die Listener zu starten, die Ereignisse auslösen. Jedoch kommt die Swing-Anwendung ohne Event Handling aus.

Abb. 8: Swing-Variante mit View, Controller und Starterklasse BurgerAppSwing

Abb. 8: Swing-Variante mit View, Controller und Starterklasse BurgerAppSwing

Die ganze Welt ist eine Bühne

Die JavaFX-Anwendung entspricht der weiter oben erwähnten Beispielanwendung. Das Model muss eigentlich kaum geändert werden. Die Klasse ModelServer braucht lediglich einen weiteren Konstruktor, dem eine Instanz vom Typ Stage übergeben wird (Abb. 9). Die Controller, die in der Burgeranwendung auf VC enden, verfügen alle über eine show-Methode, die die show-Methode der dzugehörigen View aufruft. So ist es möglich, den einzelnen Views dieselbe Instanz vom Typ Stage zu übermitteln, mit der die Anwendung in der main-Methode gestartet wird. Durch den Aufruf der show-Methode in der jeweiligen View wird der Inhalt des Fensters gezeichnet. Außerdem ist es erforderlich, dass die Listen vom Typ ObservableList<T> sind. Dadurch ist es möglich, ihre Werte zu beobachten. Die ObservableList ist zusätzlich bei Komponenten wie der Tabelle, ComboBox oder ChoiceBox erforderlich, denn hier ändern sich die Werte laufend. Diese Komponenten verfügen alle über die Methode addListener. Jedoch ist es nicht ratsam, diese Methode direkt aufzurufen, da die Komponenten das bereits intern erledigen [12].

Abb. 9: Views und Controller in der JavaFX-Anwendung

Abb. 9: Views und Controller in der JavaFX-Anwendung

Eine ObservableList einzurichten ist nicht besonders aufwendig, da sie die Klasse List erweitert und beispielsweise wie eine ArrayList funktioniert. Das Grundgerüst unseres Models sieht aus wie in Listing 1. Ähnlich wie bei Swing erweitert JavaFX eine Klasse, um die Anwendung zu starten. Da es sich um eine Klasse des JavaFX-Pakets handelt, muss man eine neue Datei anlegen, die zusätzlich die main-Methode beinhaltet. Hier sollte nur das Nötigste gestartet werden, denn die Fenster werden in der jeweiligen View definiert. So sollte hier eine Instanz des Controllers für das erste Fenster erzeugt werden, dem das Model als Argument übergeben wird. In unserem Beispiel startet die Klasse BurgerAppServer die JavaFX-Anwendung (Listing 2).

import javafx.application.Application;
import javafx.stage.*;
import java.util.Locale;
public class BurgerAppServer extends Application {
  public void start(Stage primaryStage) {
    ModelServer data = new ModelServer(primaryStage,Locale.GERMANY);
    MainVC mainVC = new MainVC(data);
    mainVC.show();
  }
  public static void main (String[] args)
  { launch(args);
  }
}

Tabwechsel

Pro Fenster werden ein Controller sowie eine View benötigt. Controller haben zudem Zugriff auf das Model. Das ist wichtig, damit der Controller auf die Listen und andere Daten zugreifen kann, um die Kontrollelemente der View zu aktualisieren. Die View selbst wird im Konstruktor des jeweiligen Controllers gestartet. Einige Konstruktoren der Views brauchen zusätzliche Argumente. Das sind in der Regel eine Instanz der MainView sowie Instanzen anderer Views. Beim Wechsel von einem Tab zum nächsten ändert sich nur der Inhalt der Tabs, weshalb es hier noch nicht zum Szenenwechsel kommt. Aus diesem Grund bleiben die Buttons Cancel und Screenshot in jedem Tab erhalten. Das Event Handling dieser Buttons wird aus diesem Grund vom Controller MainVC geregelt. Der Inhalt der Tabs ändert sich, sobald der User auf einen Tab klickt. Dann wird im Controller MainVC die show-Methode des Controllers für die neue View aufgerufen, die den Inhalt der View in den Tab lädt. Das Grundgerüst eines Controllers könnte aussehen wie in Listing 3.

public class MainVC {
  // Model mit den Listen
  private ModelServer data;
  private MainView view;
  // Controller des Orders-Tabs
  private OrdersVC ordersVC;
  public MainVC(ModelServer data) {
    this.data = data;
    this.view = new MainView();
//Listener der Buttons definieren
    view.getCancelBtn().setOnAction(e -&gt; btnCancel_Click());
    view.getScreenshotBtn().setOnAction(e -&gt; btnScreenshot_Click());
    view.getOrdersTab().setOnSelectionChanged (e -&gt; tabOrders_Click()); 
  }
// diese show-Methode wird von der Klasse mit der main-Methode aufgerufen:
  public void show(){
  view.show(data.getPrimaryStage()); }
// schließt die Anwendung
public void btnCancel_Click() {
    data.getPrimaryStage().close(); 
}
// wechselt zum Orders-Tab
public void tabOrders_Click() {
  if ( view.getOrdersTab().isSelected() && OrdersVC.isActive() ) {
    this.ordersVC.show(view);
  } else if (view.getOrdersTab().isSelected()) {
  this.ordersVC = new OrdersVC(this.data);
  this.ordersVC.show(view); 
  }
... }

In der Burgeranwendung prüft eine if-Abfrage, ob eine Instanz des Controllers bereits erzeugt worden ist, damit es nicht zu einer NullPointerException kommt. Hierfür wird ein flag vom Typ boolean verwendet. Sobald eine Instanz existiert, wird das flag auf true gesetzt und lediglich die show-Methode des Controllers aufgerufen, um die View zu zeichnen (Listing 4).

// managt die Events der OrdersView
public class OrdersVC {
  private ModelServer model;
  private OrdersView view;
  private static boolean flag = false;
  public OrdersVC(ModelServer model ) {
    this.model = model;
    TestLists tl = new TestLists(this.model);
    this.view = new OrdersView();
    // lädt die Bestellliste in die Tabelle
    this.view.getTable().setItems(this.model.loadData() );
    // setzt das Flag auf true, wenn eine Instanz des Controllers vorliegt
    flag = true;}
  public static boolean isActive() { return flag; }
//ruft die show-Methode der OrdersView auf
public void show(MainView mView){
    this.view.show(model.getPrimaryStage(),mView); 
    }
  } 
}

Bühnenbild

Unter JavaFX gibt es keinen Skalierer, weshalb ihr euch selbst einen bauen müsst. Ihr könnt damit anfangen, die Bildschirmgröße in der MainView zu ermitteln [13]:

public void setScreenSize(){
  this.screenWidth = (int) Screen.getPrimary().getBounds().getWidth();
  this.screenHeight = (int) Screen.getPrimary().getBounds().getHeight(); }

Dann legt ihr Breite und Höhe des Fensters fest, ähnlich wie bei einer Webanwendung (Listing 5).

public void setSceneSize(){
  this.sceneWidth = 0;
  this.sceneHeight = 0;
  if (this.screenWidth <= 800 && this.screenHeight <= 600) {
    this.sceneWidth = 600;
    this.sceneHeight = 350;
  } else if (screenWidth <= 1280 && screenHeight <= 768) {
    this.sceneWidth = 800;
    this.sceneHeight = 500;
  } else if (this.screenWidth <= 1920 && this.screenHeight <= 1080) {
    this.sceneWidth = 1000;
    this.sceneHeight = 650; 
  }
}

Da in der MainView-Klasse das Tabmenü, das Startfenster und der Bereich außerhalb der Tabs gezeichnet wird, befinden sich die dazugehörigen Methoden in der MainView-Klasse. Die Steuerelemente wie Buttons, Checkboxen etc. sollten von einem der zahlreichen Layoutmanager verwaltet werden, so lassen sich die Steuerelemente leichter positionieren. Der oberste Layoutmanager der Burgeranwendung ist das GridPane der gleichnamigen Klasse. Es ordnet die Elemente anhand eines Rasters aus Spalten und Zeilen an. Der Layoutmanager und die Steuerelemente werden üblicherweise im Konstruktor initialisiert (Listing 6).

...
public class MainView {
// die erste Scene in unserem Beispiel
private Scene scene;
private GridPane grid;
  public MainView() {
  setScreenSize();
  setSceneSize();
  grid = new GridPane();
  grid.setPadding(new Insets(10));
  grid.setHgap(10);
  grid.setVgap(10);
// legt die Breite und Höhe der Scene fest
scene = new Scene(grid, this.sceneWidth, this.sceneHeight, Color.TRANSPARENT);
// CSS-Skript mit der Scene verknüpfen
scene.getStylesheets().add(getClass().getResource(Constants.CSS).toExternalForm());
  btnCancel = new Button("Cancel");
  btnScreenshot = new Button ("Screenshot");
  // HBox: weiterer Layoutmanager
    paneButtons = new HBox(10,btnCancel,btnScreenshot);
    // fügt die Buttons zum GridPane hinzu
    grid.add(paneButtons,2,4);
... }

Die Methode setHgap der Klasse GridPane definiert die Größe der Lücke zwischen den Zeilen, wobei setVgap auf Spalten angewandt wird. Mit setPadding wird den Innenkanten des GridPane jeweils ein Abstand von 10 Pixeln zugeordnet. Um die Buttons Cancel und Screenshot, die in allen Fenstern erscheinen, horizontal anzuordnen, werden sie vom Layoutmanager Hbox verwaltet. Der wird anschließend zum GridPane hinzugefügt. Mit der add-Methode von GridPane lässt sich außerdem die Position der Buttons bestimmen. So reichen die Buttons maximal bis zur zweiten Spalte und werden erst in der vierten Reihe eingeblendet. Jeder Szene kann mittels addStylesheets ein Stylesheet hinzugefügt werden. Außerdem sollten unbedingt Höhe und Breite beim Initialisieren der Szene angegeben werden.

public void setTab(Tab tab, String s){
  tab.setText(s);
  tabPane.getTabs().add(tab);
// verhindert das Wegklicken der Tabs
  tabPane.setTabClosingPolicy(TabClosingPolicy.UNAVAILABLE); 
}

public void setTabContent(Tab tab, Node l) {
  HBox hbox = new HBox();
  hbox.getChildren().add(l);
  hbox.setAlignment(Pos.CENTER);
  tab.setContent(hbox); 
}

public void setTabContent(Tab tab, Node node1, Node node2) {
  // erstellt einen Distanzhalter
  Region spacer = new Region();
  // passt immer die Breite des Knotens an
  VBox.setVgrow(spacer, Priority.ALWAYS);
  VBox vbox = new VBox();
    vbox.getChildren().addAll(node1,node2); 
  }
  // vertikale + horizontale Anordnung
  vbox.setAlignment(Pos.CENTER);
  vbox.setPadding(new Insets(10));
    tab.setContent(vbox); 
}
public void show(Stage stage, Scene s) {
  stage.setTitle("Hamburger App");
  stage.setScene(s);
  // User kann die Größe des Fensters anpassen
  stage.setResizable(true);
  // Anwendung startet maximiert
  stage.setMaximized(true);
  stage.initStyle(StageStyle.TRANSPARENT);
  // Anwendung im Vollbildschirmmodus starten
  stage.setFullScreen(true);
// dieser Aufruf ist Pflicht, um das Fenster auszugeben
  stage.show();
}

Das Tabmenü wird mit dem Layoutmanager TabPane erzeugt. Die Methode tab.setText legt die Beschriftung der Tabs fest. Anschließend muss der Tab noch dem TabPane hinzugefügt werden. Um die Tabs mit Inhalt zu füllen, wird die selbstdefinierte Methode setTabContent in den jeweiligen Views aufgerufen. Dabei sorgt der Layoutmanager VBox dafür, dass die einzelnen Elemente untereinander erscheinen. Darüber hinaus belegt VBox innerhalb des GridPane den maximalen Platz (Listing 7). Nur in der show-Methode der MainView-Klasse werden sowohl die Scene als auch die Stage weiter definiert und festgelegt (Listing 8). Die show-Methode der restlichen Views ändert lediglich den Inhalt des jeweiligen Tabs:

public void show(Stage stage,MainView mView) {
  this.lblTitle = new Label();
  mView.setTitle(this.lblTitle,"Incoming Orders!");
  mView.setTabContent(mView.getOrdersTab(), this.lblTitle,this.table);}

Bühnenbild 2.0

Anders als bei Swing gibt es unter JavaFX für jede Situation den passenden Layoutmanager. So lässt sich eine Bildergalerie, ein Brettspiel oder eine Artikelliste mit TilePane (Tile = Kachel) erzeugen. In der Burgeranwendung wird eine Getränkeliste durch ein TilePane realisiert. Als Grundlage dient eine Liste vom Typ Drink, die durch ein Enum erzeugt wird. Auch für die Hamburger wird ein Enum angelegt. Enums sind praktisch, weil sowohl die Getränke als auch Hamburger Konstanten beinhalten, deren Werte gleichbleiben. Pro Getränk und Hamburger werden vollständiger Name, Preis und Bildpfad gespeichert. Besser wäre es, eine Elternklasse namens Item zu erstellen, die Methoden und Attribute an die Kinderklassen Hamburger und Drink vererbt (Listing 9).

public enum Drink {
  CZEROSTEVIA("Coca Cola Zero Stevia", 2.5,Constants.IMGCAN),...,
    PLOSE ("Plose Water", 1.5,Constants.NOPICTURE);
    private final String drink;
    private final double price;
    private final String path;
  Drink(String key, double value,String value2) {
    this.drink = key;
    this.price = value;
    this.path = value2;
  }

  public String getKey() {
    return this.drink;
  }
  public double getValue() {
    return this.price;
  }
  public String getValue2() {
    return this.path;
  }

  public double price(int amount) {
    return amount*this.price ;
  }

  public static Drink getDrink(String name) {
    Drink drink = null;
    for (Drink d: Drink.values()) {
      if (name == d.getKey()) {
        drink = d;
        break; 
     }
    }
    return drink;
}

In jede Kachel kommen ein Bild des Artikels und der Button, der das Produkt in den Warenkorb legt. Der Layoutmanager TilePane wird in DrinksView wie in Listing 10 definiert. Da es sich bei den Artikeln um Enums handelt, lässt sich durch den Aufruf von Enum.values() eine Liste erzeugen, die es erlaubt, die Artikel zu iterieren. Davon wird von der Methode setPictures Gebrauch gemacht, die für jedes Getränk eine Kachel erzeugt (Listing 11). Bei kleineren Monitoren ist es zudem wichtig, dass man Fenster scrollen kann, um dessen gesamten Inhalt zu sehen. Daher wird die Instanz des Layoutmanagers TilePane zusätzlich mit dem Layoutmanager ScrollPane verschachtelt (Listing 12). Zur Ausgabe der Getränkeansicht im Tab muss man die Methode setTabContent mit den entsprechenden Argumenten in der show-Methode der DrinksView-Klasse aufrufen:

public void show(Stage stage) {
  this.mView.setTabContent(this.mView.getDrinksTab(),this.lblTitle,this.scroll);}

Javas Hunger auf Ressourcen, insbesondere den Arbeitsspeicher, ist bekanntlich während des Betriebs groß. Umso mehr trifft das auf aufwendig gestylte Anwendungen basierend auf JavaFX zu. Denn während des Betriebs der Burgeranwendung auf dem Raspberry Pi kam es zu einer unerwarteten NullPointerException, gefolgt von der Fehlermeldung glGetError 0x505. Dieser Fehler ereignete sich in der Bildergalerie. Er lässt sich jedoch beheben, indem der GPU mehr Arbeitsspeicher zugewiesen wird.

public void setTile() {
  this.tile = new TilePane();
  this.tile.setHgap(20);
  this.tile.setVgap(20);
  this.tile.setPadding(new Insets(20));
  // Anzahl der Spalten für die Bildergalerie
  this.tile.setPrefColumns(Constants.COLUMNS); 
}
public void setPictures() {
  for (Drink d : Drink.values()) {
    Button btnBasket = new Button("Add to Basket");
   // lädt den Pfad des Getränks
    File file = new File(d.getValue2());
    Image img = new Image(file.toURI().toString());
   // Bilder können in JavaFX nur mit ImageView bearbeitet werden
    ImageView iview = new ImageView(img);
    int sceneH = this.mView.getSceneHeight();
    double imgHeight = sceneH / Constants.COLUMNS;
    iview.setFitHeight(imgHeight);
   // Seitenverhältnis des Bildes soll erhalten bleiben
    iview.setPreserveRatio(true);
    Text txt = new Text(d.getKey());
    // setzt leeren Raum zwischen den Elementen einer VBox
    Region spacer = new Region();
   // Bild+Artikelname+Button kommen jeweils in eine VBox
    VBox box = new VBox(10, iview, spacer, txt,btnBasket);
    box.setVgrow(spacer, Priority.ALWAYS);
    box.setAlignment(Pos.CENTER);
   // fügt die Kachel zum Layoutmanager TilePane hinzu:
    this.tile.getChildren().add(box); 
  }
}
public void setScroll() {
  this.scroll = new ScrollPane(this.tile);
  int sWidth = this.mView.getSceneWidth();
  int sHeight = this.mView.getSceneHeight();
  double sHeightNew = sHeight / 2;
  this.scroll.setMinWidth(sWidth);
  this.scroll.setMinHeight(sHeightNew);
}

Pop-up-Fenster

Leider stellt JavaFX keine Klasse für Dialogboxen zur Verfügung, weshalb ihr eure eigenen entwickeln müsst. Unter Swing gibt es zwar die Klasse JOptionPane, für die es jedoch keine Entsprechung bei JavaFX gibt. Sofern eine klassische Dialogbox erforderlich ist, empfiehlt es sich, eine eigene Klasse zu erstellen, der man beim Anlegen einer Instanz die entsprechenden Argumente übergibt. So gelingt die klassische Dialogbox unter JavaFX:

  1. eine neue Stage erstellen

  2. eine neue Scene generieren

  3. eine Instanz vom Typ Label oder Text anlegen, die den Text in der Dialogbox ausgibt

  4. einen OK-Button erzeugen

  5. einen Layoutmanager wählen; bei klassischen Dialogboxen eignet sich insbesondere VBox gut.

  6. die Elemente zum Layoutmanager hinzufügen

  7. den Layoutmanager mit der Scene verknüpfen

  8. die Scene der Stage übergeben

  9. die Methode initModality der Klasse Stage überschreiben: stage.initModality(Modality.APPLICATION_MODAL); dadurch blockiert die Stage so lange alle anderen Events, bis der User die Dialogbox durch Drücken des OK-Buttons bestätigt

Eine andere Möglichkeit besteht darin, ein Pop-up-Fenster zu generieren. In der Burgeranwendung machen wir davon Gebrauch, indem beim Klicken auf den Button Add to Basket die Artikelansicht in Form eines Pop-up-Fensters geöffnet wird (Abb. 10). Zunächst werden im Controller der Bildergalerie, genannt DrinksVC, die Event-Handling-Methoden der einzelnen Add to Basket-Buttons festgelegt (Listing 13). Klickt der User auf Add to Basket, wird der Event im Controller DrinksVC aufgerufen (Listing 14). Die Getränkeansicht wird in der Klasse ItemView mittels VBox realisiert, um die Elemente vertikal anzuordnen (Listing 15). Um dieses täuschend echt aussehende Pop-up-Fenster zu erzeugen, wird der oberste Layoutmanager der Bildergalerie, in unserem Fall ScrollPane, in den Layoutmanager StackPane (Stack = Stapel) gespeichert, der zum Tab hinzugefügt wird:

minosi_javafx_10.tif_fmt1.jpgAbb. 10: Erstellen eines Pop–up-Fensters mit Layoutmanager
public void setEvents() {
  // Kindknoten des TilePanes bestehend aus lauter VBoxen
  ObservableList&lt;Node&gt; vboxes = this.view.getTiles();
    for(Node n: vboxes){
     // Cast von Node zu VBox
      VBox v = (VBox)n;
     // Kindknoten einer VBox laden
      ObservableList&lt;Node&gt; elems = v.getChildren();
      // Button des Artikels finden
      Button btn = getButton(elems);
      // Event-Methode für den Button festlegen
      btn.setOnAction(e -&gt; btnBasket_Click(elems));
  }
}

public Button getButton(ObservableList&lt;Node&gt; vbox) {
  Button b = null;
  // durch die Knoten einer einzelnen VBox iterieren
  for (Node e: vbox) {
    if(e instanceof Button){
      // gefundenen Knoten zu Button casten
      b = ((Button)e);
      break;
    }
  }
  return b;
}
public void btnBasket_Click(ObservableList<Node> vbox) {
  Text d = null;
  Drink dr = null;
// iteriert durch die Knoten der VBox, welche den Add-to-Basket-Button besitzt
  for (Node e: vbox) {
    if (e instanceof Text) {
      // Casten des Knotens zu Text
      d = (Text)e;
      // Getränk speichern
      dr = Drink.getDrink(d.getText());
      break;
    }
  }
  if (dr != null) {
    // erzeugt die Artikelansicht
    ItemVC itemVC = new ItemVC(this.model,this.mView,dr,this.view );
    itemVC.show();
  } else {
    ItemVC itemVC = new ItemVC(this.model,this.mView,this.view );
    itemVC.show();
  } 
}
public void setWindow(ImageView iview) {
// lädt den vollständigen Namen des Getränks
Label txt = new Label(this.drink.getKey());
// fügt dem Label die CSS-Klasse item hinzu
txt.getStyleClass().add("item");
// speichert die Elemente in der VBox ab
this.vbox.getChildren().addAll(this.lblTitle,iview,txt,this.cbox,this.paneButtons);
// vergibt der VBox die CSS-Klasse popup-window
this.vbox.getStyleClass().add("popup-window");
// wendet einen Schatten auf die VBox an
this.mview.createShadow(this.vbox);
}
public void show(Stage stage) {
  this.mView.setTabContent(this.mView.getDrinksTab(), this.stack);}

Die show-Methode der ItemView-Klasse fügt dann die Artikelansicht zu StackPane hinzu. StackPane sorgt dafür, dass die Views übereinandergestapelt werden. Da die Bildergalerie beim Aufruf des Artikels weiterhin angezeigt wird, bedarf es keiner weiteren Methoden:

public void show(Stage stage, DrinksView dview) {
  dview.getStack().getChildren().add(this.vbox);}

In der Getränkeansicht kann der User die Menge eines Getränks auswählen und es in den Warenkorb legen oder durch Drücken des X-Buttons das Fenster schließen. Beide Events werden vom zugehörigen Controller ItemVC verarbeitet. Beim Schließen des Fensters wird die VBox, die die Artikelansicht realisiert, aus StackPane entfernt:

public void btnX_Click() {
  this.dview.getStack().getChildren().remove(this.view.getVbox());
}

Der OK-Button verfügt über ein Event, das prüft, ob die angegebene Menge überhaupt in der Liste der ComboBox vorhanden ist, und sie dann samt dem Artikel der Menüliste hinzufügt. Die Menüliste ist in diesem Fall eine Art temporäre Liste, die nach einer erfolgten Bestellung wieder auf null gesetzt wird, denn die bestellten Artikel landen dann in der Bestellliste (Listing 16).

public void btnOK_Click() {
  // prüft, ob Menge in der Liste der ComboBox vorhanden ist
  if ( this.view.getCbox().getItems().contains(this.view.getCbox().getValue()) ) {
    // Wert der ComboBox entnehmen
    int amount = this.view.getCbox().getValue();
    // Artikel+Menge in der Menü-Liste abspeichern
    this.model.addMenu(this.model.getMenus(),this.drink , amount );
    // Fenster schließen
    btnX_Click();
  }else {
    btnX_Click();
  } 
}

Bühnenbild 3.0

Damit der User das Gefühl hat, die Artikelansicht sei ein neues Fenster, lässt sich das oberste Fenster mit Spezialeffekten versehen. Hierzu legt ihr für die VBox eine CSS-Klasse an (Listing 15) und verschönert das Pop-up-Fenster nach Belieben. Das Pop-up-Fenster der Burgeranwendung verfügt über eine Deckkraft (Opacity) von 80 Prozent, sodass das vorherige Fenster durchschimmert:

popup-window {
  -fx-background-color:myDarkGray;
  -fx-opacity: 0.8;
  -fx-border-color: myDarkOrange;
  -fx-border-width: 2; }

Zusätzlich existieren unter JavaFX Spezialeffekte, die sich vor allem für Überschriften eignen. Um den Transparenzeffekt hervorzuheben, könnte man die Überschrift des darunterliegenden Fensters mit einem Blur-Effekt versehen. Die Überschriften unserer Burgeranwendung hingegen besitzen einen Bloom-Effekt, der für den nötigen Glanz sorgt. Außerdem hat der User dadurch das Gefühl, dass die Überschrift im Hintergrund ist, sodass das Bild mehr in den Vordergrund rückt:

public void setTitle(Label l, String t) {
  Bloom e1 = new Bloom();
  e1.setThreshold(0.3);
  l.setText(t);
  l.getStyleClass().add("lbl-titel");

Das Pop-up-Fenster lässt sich weiter stylen, indem man beispielsweise einen Schatten hinzufügt (Listing 17).

public void createShadow(Node n) {
  DropShadow e = new DropShadow();
  e.setWidth(20);
  e.setHeight(20);
  e.setOffsetX(10);
  e.setOffsetY(10);
  e.setRadius(50);
  n.setEffect(e); 
}

Szenenwechsel

Die dritte Möglichkeit, eine Message-Box zu erstellen, besteht darin, als obersten Layoutmanager die VBox zu wählen und einen Szenenwechsel hervorzurufen (Abb. 11). Hierbei wird kein Inhalt mehr in die Tabs geladen, sondern stattdessen in der Klasse ItemView eine neue Szene erstellt. Die setWindow-Methode in der Klasse ItemView ändert sich dadurch (Listing 18). In der show-Methode der ItemView-Klasse wird anschließend die Scene neu gesetzt, wobei die Stage unverändert bleibt (Listing 19).

Abb. 11: Szenenwechsel: die Buttons des Hauptfensters sind nicht mehr vorhanden

Abb. 11: Szenenwechsel: die Buttons des Hauptfensters sind nicht mehr vorhanden

public void setWindow(ImageView iview) {
  // lädt den Namen des Getränks
  Label txt = new Label(this.drink.getKey());
  txt.getStyleClass().add("item"); this.vbox.getChildren().addAll(this.lblTitle,iview,txt,this.cbox,this.paneButtons);
  this.vbox.getStyleClass().add("popup-window");
  // neue Szene erstellen
  this.scene = new Scene(this.vbox, this.mview.getSceneWidth(),this.mview.getSceneHeight(), Color.TRANSPARENT);
  // CSS-Skript für die Szene festlegen
  this.mview.setCSS( this.scene);
  this.mview.createShadow(this.vbox);
}
// ItemView-Klasse
public void show(Stage stage) {
  this.mview.setMyScene(stage,this.scene); 
}

// MainView-Klasse
public void setMyScene(Stage stage, Scene s) {
  stage.setScene(s);
  stage.setResizable(true);
  stage.setMaximized(true);
  stage.setFullScreen(true);
}

Sobald der User die Artikelansicht schließt, sei es durch Drücken auf den OK– oder X-Button, wechselt der dazugehörige Controller ItemVC die Szene:

public void btnX_Click() {
  this.mView.setMyScene(this.model.getPrimaryStage(),this.mView.getMainScene());}

Event-Handling 2.0

Das Beobachten von Variablen funktioniert unter JavaFX definitiv leichter. In der Burgeranwendung werden zur Umsetzung des Warenkorbs Properties eingesetzt. Es handelt sich dabei um ein Label, dessen Wert aktualisiert wird, sobald der User einen Namen in der ComboBox auswählt (Abb. 12). ComboBox und Label werden in der Klasse BasketView initialisiert (Listing 20). Im dazugehörigen Controller BasketVC wird das Label mit der ComboBox verknüpft. Da der Controller Zugriff auf das Model beziehungsweise die Datenbank hat, wird das Binden der Property an die Elemente der View im Controller geregelt (Listing 21).

Abb. 12: Das Label weist eine Lücke auf, weil in der ComboBox nichts ausgewählt ist

Abb. 12: Das Label weist eine Lücke auf, weil in der ComboBox nichts ausgewählt ist

public BasketView(MainView m) {
  this.mview = m;
  this.lblName = new Label();
  setCbox();
}

public void setCbox() {
  this.cbox = new ComboBox&lt;Person&gt;();
  this.cbox.setPromptText("Select Name or enter new one!");
  this.cbox.setEditable(true);
  this.cbox.setVisibleRowCount(10);
}
public BasketVC(ModelServer model,MainView m ,DrinksView d) {
  this.view = new BasketView(m);
  initialize();
  setEvents();
}

// lädt die Personliste in die ComboBox
public void initialize() {
  this.view.getCbox().setItems(this.model.getPersons());
}

public void setEvents() {
// belauscht die ComboBox
  this.view.getCbox().setOnAction(e -&gt; cbox_changed());
}

// verknüpft die Label-Property mit der Name-Property
public void cbox_changed() {
  this.view.getName().textProperty().bind(this.view.getCbox().getValue().nameProperty());
}

Das Binden der Property findet in der Methode cbox_changed statt. Zwei Properties werden hierbei miteinander verbunden: die textProperty des Labels mit der selbstdefinierten nameProperty der Klasse Person. In der Person-Klasse muss jedoch die nameProperty erst definiert werden. Vor allem bei JavaFX Properties ist es erforderlich, sich an die Namenskonvention zu halten. Die Getter- und Setter-Methode des Attributs name wird mit dem Präfix get beziehungsweise set vorangestellt, wobei das Attribut nach dem Präfix großgeschrieben wird. Unsere nameProperty besitzt nicht das Java-Schlüsselwort final, weshalb es nicht nur lesbar, sondern auch veränderbar ist. So erstellt ihr die SimpleStringProperty in wenigen Schritten:

  1. ein privates Feld erstellen, das die Daten speichert: private String _name;

  2. eine private Variable deklarieren, die jedoch nicht instanziiert wird: private SimpleStringProperty name;

  3. die Getter-Methode erstellen: getName

  4. die Setter-Methode erstellen: setName

  5. eine Zugriffsmethode auf das SimpleStringProperty erstellen: nameProperty

Diese Methode prüft, ob die Property des Objekts name existiert. Falls nicht, wird SimpleStringProperty angelegt. Dann wird das Objekt name zurückgegeben (Listing 22).

import javafx.beans.property.*;
public class Person {
  //
  private String _firstName;
  private String _lastName;
  private String _name;
  private SimpleStringProperty firstName;
  private SimpleStringProperty lastName;
  // dieses Attribut wird für das Label verwendet
  private SimpleStringProperty name;

  public Person(String f, String l) {
    setFirstName(f);
    setLastName(l);
    setName();
  }

  public final String getFirstName() {
    if(firstName == null) {
      return _firstName;
    } else
    return firstName.get(); 
  }

  public final String getLastName() {
    if(lastName == null) {
      return _lastName;
    } else
    return lastName.get(); 
  }

  public final String getName() {
    if(name == null) {
      return _name;
    } else
    return name.get(); 
  }

  public final void setFirstName(String value) {
    if(firstName == null) {
      _firstName = value;
    } else {
    firstName.set(value);
    } 
  }

  public final void setLastName(String value) {
    if(lastName == null) {
      _lastName = value;
    } else
    lastName.set(value);
  }

  public final void setName() {
    if(firstName == null &amp;&amp; lastName == null) {
      _name = getFirstName()+" "+getLastName();
    } else
    name.set(getFirstName()+" "+getLastName());
  }

  public final StringProperty firstNameProperty() {
    if(firstName == null) {
      firstName = new SimpleStringProperty(this,"firstName",_firstName);
    }
    return firstName; 
  }

  public final StringProperty lastNameProperty() {
    if(lastName == null) {
      lastName = new SimpleStringProperty(this,"lastName",_lastName);
    }
    return lastName; 
  }

  // selbstdefinierte nameProperty
  public final StringProperty nameProperty() {
    if(name == null) {
      name = new SimpleStringProperty(this,"name",_name);	
  }
    return name; 
  }
}
Abb. 13: Das Label wird beim Auswählen der ComboBox synchronisiert

Abb. 13: Das Label wird beim Auswählen der ComboBox synchronisier

In Abbildung 13 wird in der ComboBox nur die Adresse der Person-Objekte angezeigt. Da sie Werte vom Typ String leserlich ausgibt, muss der Typ der ComboBox geändert werden, damit sie eine Liste mit Strings akzeptiert:

public BasketView(MainView m) {
  this.mview = m;
  this.cbox = new ComboBox<String>(); }

Da die ComboBox bereits Stringobjekte beinhaltet, könnt ihr die von der ComboBox-Klasse zur Verfügung gestellte valueProperty benutzen, um sie mit der textProperty des Labels zu verknüpfen:

public void cbox_changed() {
  this.view.getName().textProperty().bind(this.view.getCbox().valueProperty());
}

Fazit

Die Migration der Swing-Anwendung auf die JavaFX-Anwendung veranschaulicht, dass sich am Model am wenigsten ändert. JavaFX erwartet jedoch, dass Kontrollelemente wie TableView oder ComboBox Listen vom Typ ObservableList erhalten. Die Umstellung auf diese Listen sollte dennoch flott vonstattengehen. Die Starterklasse, die die main-Methode besitzt, muss definitiv neu geschrieben werden, denn sie überschreibt Methoden der Application-Klasse, die ausschließlich im JavaFX-Paket vorkommt. Der Controller einer View müsste von Grund auf neu definiert werden. Das Event Handling gestaltet sich unter JavaFX allerdings leichter, was auf Properties sowie ObservableLists zurückzuführen ist. Für eine erfolgreiche Umstellung der View ist es erforderlich, eine eigene Datei anzulegen und die Methoden und Klassen des JavaFX-Pakets zu benutzen. Andererseits stellt JavaFX gleich mehrere Layoutmanager bereit, was das Design der Fenster erleichtert. Die Tatsache, dass an die Scene ein CSS-Skript gehängt werden kann, macht das Styling flexibler.

Die Migration auf JavaFX würde sich sicherlich lohnen, weil die Synchronisation des Models mit der View vereinfacht wurde. Außerdem haben wir bei der Burgeranwendung festgestellt, dass sich der Touchscreen ohne zusätzliche Konfiguration bedienen ließ. Das heißt, dass wir das Klicken auf die Buttons oder das Scrollen mit dem Finger erledigen konnten. Das sollte man beim nächsten großen Release der eigenen Java-Anwendung bedenken.

 

Geschrieben von
Anatole Tresch
Anatole Tresch
A​natole Tresch studierte Wirtschaftsinformatik und war anschließend mehrere Jahre lang als Managing Partner und Berater aktiv. ​Die letzten Jahre war ​Anatole Tresch als technischer Architekt und Koordinator bei der Credit Suisse​ tätig. Aktuell arbeitet Anatole ​als Principal Consultant für die Trivadis AG, ist Specification Lead des JSR 354 (Java Money & Currency) und PPMC Member von Apache Tamaya. Twitter: @atsticks
Kommentare

Hinterlasse einen Kommentar

avatar
4000
  Subscribe  
Benachrichtige mich zu: