Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

Excerpt

JavaFX er Java sitt nye rammeverk for å lage og beskrive grafiske brukergrensesnitt (GUI). Her gir vi en kort introduksjon til å lage enkle GUI med JavaFX vha. Eclipse med e(fx)clipse-tillegget.

JavaFX og det tilhørende filformatet FXML og verktøyet SceneBuilder, gjør det relativt enkelt å lage fasaden til app-er. Med Eclipse-tillegget e(fx)clipse så kan du faktisk lage fasaden uten programmering, og så trinnvis gjøre app-en funksjonell ved å koble elementene i fasaden, så som knapper og tekstfelt, til selve (koden for) app-logikken. Her viser vi hvordan dette gjøres for en enkel app basert på Counter-logikken i Tilstand og oppførsel.

Counter-

...

Counter-klassen implementerer en enkel teller, som får fra 1 til en angitt øvre grense. Koden og en enkel forklaring er gitt under:

Counter-klassenForklaring
Code Block
languagejava
public class Counter {
	int end;
	int counter = 0;


	Counter(int end) {
		this.end = end;
	}

	int getCounter() {
		return counter;
	}

	void count() {
		if (counter < end) {
			counter = counter + 1;
		}
	}
 }

Counter-klassen deklarerer felt for den øvre grensa og telleren.

Den øvre grensa initialiseres av konstruktøren.

Telleren kan lese (utenifra) med getCounter()-metoden.

count()-metoden brukes til å telle opp ett trinn, men dette skjer bare hvis den øvre grensa ikke alllerede er nådd.

Figuren under viser hva som skjer når en teller fra 0 og oppover og den øvre grensa er 3.

PlantUML Macro
object "~#1: Counter" as counter0 {
	end = 3
	counter = 0
}
object "~#1: Counter" as counter1 {
	end = 3
	counter = 1
}
counter0 -> counter1: count()
 
object "~#1: Counter" as counter2 {
	end = 3
	counter = 2
}
counter1 -> counter2: count()

object "~#1: Counter" as counter3 {
	end = 3
	counter = 3
}
counter2 -> counter3: count()
counter3 -> counter3: count()

Vi ser at det samme objektet (samme #id) endrer tilstand ved at telleren øker, inntil telleren er lik den øvre grensa.

Counter-appen

Tanken er nå å lage en app som lar oss opprette og initialisere, lese og endre tilstanden til et Counter-objekt. Vi skal starte med en enkel variant, som lar oss lese og endre tilstanden til et forhåndsopprettet Counter-objekt, og så utvider vi den til å støtte å lage nye Counter-objekter.

...

Enkel app for å telle opp telleren til et Counter-objekt, som er laget på forhånd med en grense på 35.

Trykk Count-knappen for å telle opp.

Med JavaFX kan en velge å bruke kode for å bygge opp GUI-et eller beskrive GUI-et med FXML-filer og så lese inn og vise frem innholdet. Vi velger å bruke FXML, siden det er mye enklere. Prosessen er grovt sett delt i to:

  • En lager en FXML-fil i Eclipse, som enten kan redigeres som tekst i Eclipse eller og åpnes i SceneBuilder for mer grafisk redigering av GUI-fasaden.
  • En lager en Java-klasse for styring av app-en og kobler denne til FXML-koden vha. spesielle Java- og FXML-elementer.

Oppretting av FXML-fil

En lager en FXML-fil med Eclipse sin New FXML Document veiviser: høyreklikk på pakka (kode-mappa) hvor Counter-klassen ligger, og velg New > Other... > New FXML Document. Fyll så ut navnet, f.eks. Counter1 og velg HBox (for horisontal box) som såkalt "Root Element" (panel-type). Avslutt og du vil få opp en FXML-editor med kode for et tomt HBox-panel. For å vise frem innholdet, altså "kjøre" FXML-koden, så kan du høyre-klikke på fila og velge Run As > FXML Application (dette forutsetter et spesielt Eclipse-tillegg). Du vil da få opp et tomt vindu, siden du ikke har redigert FXML-koden og lagt inn noe synlig innhold.

Veiviser for FXML-dokumenterfilerFXML-editor med nytt ny FXML-dokumentfil

FXML-kode for tomt HBox-panel. Den første linja er obligatorisk for alle XML-dokumenter (utelates i eksemplene under).

<?import ...>-elementet lar oss bruke HBox som navn på det som egentlig er Java-klassen javafx.scene.layout.HBox.

<HBox ...>-elementet angir at app-vinduet skal inneholdet et objekt av typen javafx.scene.layout.HBox, som gir en horisontal layout på panel-innholdet. Her er elementet imidlertid tomt, så ingenting vises ved kjøring.

Merk attributtet xmlns:fx="...", som angir at elementene skal tolkes som FXML-kode, altså referanser til JavaFX-klasser. Uten dette attributtet på det ytterste elementet vil ingenting virke.

Redigering av FXML-koden

FXML-filer kan redigeres som tekst i Eclipse. Prøv f.eks. å erstatte <!-- ... --> med <Button text="Count"/> (du trenger også et import-element for javafx.scene.control.Button). Ved kjøring vil du da få opp et vindu med en Count-knapp i (inni et HBox-panel). Hvis en redigerer FXML som tekst på denne måten, kan det være nyttig å åpne JavaFX Preview-panelet med Window > Show View > Other... > JavaFX Preview. Dette panelet viser hele tiden frem hva innholdet i den aktive FXML-editoren betyr, altså slik det vil se ut hvis FXML-koden kjøres. Under vises Figuren under viser koden for HBox-panelet med en Count-knapp i og JavaFX Preview-panel. NB: JavaFX Preview-panelet fungerer ikke i 2019-utgaven av faget og Eclipse


For en så liten app er det greit å redigere FXML-koden som tekst, men generelt er det greiere å bruke applikasjonen SceneBuilder. Høyreklikk på fila (installer først ved å følge lenka og instruksjonene) og velg Open with SceneBuilder, og du vil kunne redigere FXML-koden grafisk. SceneBuilder bruker altså FXML som lagringsformat, men lar deg redigere innholdet vha. direkte manipulasjon, tekstfelt, menyer og dialoger.

SceneBuilder-applikasjonen lar deg redigere FXML-koden grafisk.

Området i midten viser GUI-et.

Øverst til venstre er det en palett med elementer en kan dra og slippe i GUI-området i midten.

Nederst til venstre vises den hierarkiske strukturen av paneler og elementer, her med en HBox med en Button inni.

Til høyre er det et tredelt detalj-panel. Properties-seksjonen viser generelle grafiske egenskaper og egenskaper som er mer spesifikke for det valgte elementet. Her er knappen valgt, og vi ser at knappeteksten styres av Text-egenskapen, som har verdien Count. Layout-seksjonen har med plassering av elementet å gjøre og Code-seksjonen med koblingen til GUI-logikken.

 

 

...

I app-en trenger vi en tekst til venstre for knappen. Det kan en få til ved å legge til et Text-objekt slik at det havner før Butten-objektet inni HBox-objektet. Finn først frem til Text-objektet i paletten (til venstre) ved å velge Shapes-seksjonen og bla nedover. Så kan du enten dra og slippe Text-objektet til venstre for knappen i GUI-området (i midten), eller rett over Button-objektet i hierarkiet (nede til venstre). Rediger så teksten så den blir Current counter value: 0, enten ved å dobbelt-klikke på teksten i GUI-området og redigere, eller ved å redigere Text-egenskapen i Properties-seksjonen (oppe til høyre). Resultatet er vist under til venstre. Lagre, gå tilbake til Eclipse og klikk på eller i FXML-editoren for å sikre at Eclipse viser den nylig lagrede versjonen. Koden skal være omtrent som vist under til høyre.

Image Added


Code Block
languagejavafx
<?xml version="1.0" encoding="UTF-8"?>

<?import java.lang.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.control.Button?>

<HBox xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/8">
   <children>
      <Text strokeType="OUTSIDE" strokeWidth="0.0" text="Current counter value: 0" />
   	  <Button text="Count" />
   </children>
</HBox>

 


App-logikk

Så langt har vi et GUI uten oppførsel, for hvis du kjører og trykker på knappen så skjer det ingenting. Det vi mangler er app-logikken som styrer koblingen mellom den indre tilstanden til applikasjonen og GUI-fasaden. I vårt tilfelle utgjøres den indre tilstanden av Counter-objektet, mens GUI-fasaden består av Text- og Button-objektene, eller generelt av hierarkiet av JavaFX-objekter. Koblingslogikken må sikre at Text-objektet viser counter-verdien til Counter-objektet og at trykking på Count-knappen trigger count-metoden. Koblingen er altså toveis: (deler av) den indre tilstanden vises i GUI-et og GUI-et styrer (deler av) den indre tilstanden.

App-logikken ligger i en egen klasse, som gjerne kalles en kontroller-klasse, og når appen kjøres vil det finnes ett objekt av denne klassen, som er koblet sammen med de Text- og Button-objektene i GUI-et og Count-objektet. Sammenkoblingen av alle disse objektene krever spesiell kode i både FXML-koden og i kontroller-klassen. Under til venstre ser du den relevante FXML- og kontroller-koden, med en forklaring på sammenhengene til høyre.

FXML- og Java-kodeForklaring


Code Block
languagejavafx
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.text.Text?>

<HBox xmlns:fx="http://javafx.com/fxml/1" fx:controller="counter.CounterController">
	<Text fx:id="counterOutput" text="Current counter value: 0"/>
	<Button text="Count" onAction="#handleCountAction"/>
</HBox>


Code Block
import javafx.fxml.FXML;
 
public class CounterController {

	Counter counter;

	@FXML
	Text counterOutput;

	@FXML
	void initialize() {
		counter = new Counter(5);
	}

	@FXML
	void handleCountAction() {
		counter.count();
		counterOutput.setText("Current counter value: " + counter.getCounter());
	}
}


FXML-koden inneholder et fx:controller-attributt, som angir (det fulle) navnet til kontroller-klassen. Når FXML-koden kjøres (lastes inn og vises), så vil det automatisk lages et objekt av denne klassen. Her angis at kontroller-klassen er counter.CounterController-objekt og dermed vil et objekt av denne klassen styre appen.

CounterController-objektet gis sjansen til å utføre initialiseringskode, ved at en metode med signaturen void initialize() og annotert med @FXML (egentlig javafx.fxml.FXML) automatisk kalles (hvis den finnes). Annotasjonen står foran metoden og en import-setningen gjør at vi slipper å bruke det fulle navnet. Her opprettes det et Counter-objekt med 5 som øvre grense for telleren, og en får koblingen som vist under.

PlantUML Macro
object "counter:Counter" as counter {
	end = 5
	counter = 0
}
object "kontroller:CounterController" as controller
controller -l> counter: counter
 

CounterController-objektet skal bl.a. oppdatere Text-objektet som viser counter-verdien. For å kunne gjøre det, så må CounterController-objektet ha en referanse til Text-objektet, altså et felt av typen Text (egentlig javafx.scene.text.Text), slik at en får koblingene vist under.

PlantUML Macro
object "counter:Counter" as counter {
	end = 5
	counter = 0
}
object "kontroller:CounterController" as controller
controller -l> counter: counter
object "text:Text" as text {
	text = "Current counter value: 0"
}
controller -r> text: counterOutput
 

Selve koblingen opprettes automatisk av JavaFX ved kjøring av FXML-en vha. to elementer i koden:

  1. Java: counterOutput-feltet i CounterController, med Text som type og annotert med @FXML
  2. FXML: fx:id-attributtet på Text-elementet i FXML-koden, med navnet på feltet som verdi

Merk at navnet på feltet kan være hva som helst, bare det stemmer med fx:id-attributtet og felt-typen er riktig. Dersom noe ikke stemmer, så vil det bli markert som en feil i FXML-koden av editoren.

Det siste som trengs er kode som håndterer Count-knappen og koblingen mellom knappen og koden. Dette håndteres med to elementer i koden:

  1. Java: handleCountAction-metoden annotert med @FXML
  2. FXML: onAction-attributtet med #handleCountAction som verdi

handleCountAction-metoden sørger for å øke telleren, med counter.count(), og oppdatere teksten som vises av Text-objektet, med counterOutput.setText(...). Som over så spiller ikke navnet (på metoden) noen rolle, bare det stemmer med verdien til attributtet som bruker navnet.

onAction er attributtet som brukes av Button-klassen når knappen trykkes, og hver type interaktive JavaFX-objekt har sine on-attributter for de ulike typene interaksjon den støtter. Hvis kontroller-objektet skal reagere på en annen type input, så brukes tilsvarende on-attributt.

 

De spesielle attributtene som trengs i FXML-koden, altså fx:controller, fx:id og on..., kan legges inn i FXML-editoren i Eclipse eller i tilsvarende tekstfelt i SceneBuilder. fx:controller legges inn Controller-seksjonen nederst til venstre, mens fx:id og on...-attributtene legges inn i Code-seksjonen ned til høyre.

Counter-app - komplett variant

SceneBuilder-applikasjonen lar deg redigere grafisk.

Området i midten viser GUI-et.

Øverst til venstre er det en palett med elementer en kan dra og slippe i GUI-området i midten.

Nederst til venstre vises den hierarkiske strukturen av paneler og elementer, her med en HBox med en Button inni.

Til høyre er det et tredelt detalj-panel. Properties-seksjonen viser generelle grafiske egenskaper og egenskaper som er mer spesifikke for det valgte elementet. Her er knappen valgt, og vi ser at knappeteksten styres av Text-egenskapen, som har verdien Count. Layout-seksjonen har med plassering av elementet å gjøre og Code-seksjonen med koblingen til GUI-logikken.

 

 

 

 

En mer komplett app for å lage nye Counter-objekter og telle dem opp.

I starten har en ikke noe Counter-objekt. En ny lages ved å fylle inn den øvre grensa i tekstfeltet og trykke New Counter-knappen. Tilstanden til dette Counter-objektet vises på linja under og en kan telle opp telleren ved å trykke Count-knappen.

Som over så starter vi med å redigere FXML-koden, slik at vi får en linje med et tekstfelt, av typen TextField, og en knapp til. For å få ønsket layout, med to rader, så må vi i tillegg ha en HBox rundt tekstfeltet og knappen, og en VBox (vertical box) utenpå den eksisterende HBox-en og den nye. Under til venstre vises ønsket struktur, og til høyre en kort forklaring på hvordan få det til.

Image Added

En kan pakke (eng: wrap) et VBox-panel rundt eksisterende elementer ved å velge disse, høyreklikke og velge Wrap In > VBox.

Et nytt HBox-panel legges inn ved å velge Container-seksjonen i paletten og dra og slippe et HBox-objekt inn VBox-objektet, men over det eksisterende HBox-panelet. Markøren gir tilbakemelding om hvor objektet havner når du slipper, men det er lett å bomme. Da må en i så fall dra og slippe objektet innen hierarkiet til det havner på rett plass.

TextField- og Button-objektene finner du begge i Controls-seksjonen, som generelt har interaktive elementer. (Text-objektet var i Shapes-seksjonen, siden det er passiv grafikk.)

I panelet til venstre er forøvrig visning av fx:id slått på, ved å velge Hierarchy displays > fx:id i nedtrekksmenyen markert med tannhjul. Vi ser det allerede er lagt inn en fx:id for tekstfeltet med navn endInput, som betyr at vi må ha et tilsvarende felt i CounterController-klassen. Dette trengs fordi vi må kunne lese teksten ut av feltet fra CounterController-objektet, når vi skal lage et nytt Counter-objekt med en spesifikk øvre grense.

Under er FXML- og Java-koden for den utvidete varianten vist, med forklaring til høyre.

FXML- og Java-kodeForklaring


Code Block
languagejavafx
<VBox xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="counter.CounterController">
   <children>
      <HBox>
         <children>
            <TextField fx:id="endInput" />
            <Button mnemonicParsing="false" onAction="#handleNewCounterAction" text="New Counter" />
         </children>
      </HBox>
      <HBox>
         <children>
            <Text fx:id="counterOutput" strokeType="OUTSIDE" strokeWidth="0.0" text="Current counter value: 0" />
       	    <Button onAction="#handleCountAction" text="Count" />
         </children>
      </HBox>
   </children>
</VBox>


Code Block
languagejava
 public class CounterController {

	Counter counter = null;

	@FXML
	TextField endInput;

	@FXML
	Label counterOutput;

	@FXML
	void initialize() {
		counterOutput.setText("No counter");
	}

	void updateCounterOutput() {
		counterOutput.setText("Current counter value: " + counter.getCounter());
	}

	@FXML
	void handleNewCounterAction() {
		int end = Integer.valueOf(endInput.getText());
		counter = new Counter(end);
		updateCounterOutput();
	}

	@FXML
	void handleCountAction() {
		counter.count();
		updateCounterOutput();
	}
}


FXML-koden har blitt litt større, dels fordi vi har utvidet hierarkiet med et nivå til og lagt til ekstra elementer, og dels fordi SceneBuilder har lagt til ekstra <children>-elementer og noen attributter. <children>-elementene er egentlig implisitt inni panel-elementer og dermed overflødige her, men det er greit å la dem stå.

Det viktigste er at det nye TextField-elementet har et fx:id-attributt og det nye Button-elementet et onAction-attributt.

Java-koden har tilsvarende et nytt felt av typen TextField med navn tilsvarende fx:id-attrbibuttet og og en metode med navn tilsvarende onAction-attributtet. Og begge har med @FXML-annotasjonen, som angir at de brukes av JavaFX og FXML-mekanismen.

Java-koden har forøvrig fått følgende endringer:

  • initialize-metoden lager ikke lenger et Counter-objekt, som dermed starter som null, og setter i stedet counterOutput-teksten til "No counter".
  • Vi har en hjelpemetode for å oppdatere counterOutput-teksten med teller-verdien. Dette er praktisk fordi vi må oppdatere to steder, når vi i handleNewCounterAction lager et nytt Counter-objekt og når vi i handleCountAction teller opp.
  • handleNewCounterAction-metoden henter ut teksten fra endInput-tekstfeltet og konverterer til en int vha. Integer.valueOf-metoden, og lager et nytt Counter-objekt. Det nye Counter-objektet erstatter det gamle (uavhengig av om det er telt opp til grensen eller ikke).

 

Når du kjører FXML-koden, så kan du forresten legge merke til to feil/mangler:

  • Det sjekkes ikke om den øvre grensa i tekstfeltet faktisk er et gyldig tall. Hvis en f.eks. skriver fem i stedet for 5 og trykker på New Counter-knappen så skjer det tilsynelatende ingenting. Det er fordi konverteringskoden i Integer.valueOf-metoden kræsjer og det derfor ikke legges inn noe nytt Counter-objekt. Det hadde vært bedre om en hele tiden sjekket om teksten var gyldig og evt. markerte det med farge. New Counter-knappen kunne dessuten blir deaktivert, hvis teksten var ugyldig.
  • En kan trykke på Count-knappen selv om det ikke er laget noe Counter-objekt ennå. counter.count()-kallet vil kræsje fordi counter-feltet er null. Også her vil det være bedre om knappen var deaktivert, inntil et Counter-objekt faktisk var laget.

Prøv gjerne å fikse disse problemene, men merk at løsningen på det første problemet er mer komplisert enn en skulle tro. Det andre problemer er enklere å løse... hint: gjør det mulig å nå knappen vha. et fx:id og et felt, og bruk Button sin setDisabled-metode.