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

Counter-app - enkel variant

Image Added

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

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-filerFXML-editor med ny FXML-fil
Image Added

Image Added

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 hva innholdet i den aktive FXML-editoren betyr, altså slik det vil se ut hvis FXML-koden kjøres. 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

Image Added

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 FXML som lagringsformat, men lar deg redigere innholdet vha. direkte manipulasjon, tekstfelt, menyer og dialoger.

Image Added

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

Image Added

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

Enkel programmering med Python bygger gjerne på at programmet utføres linje for linje fra topp til bunn før det avslutter. Underveis ber det kanskje om tekstinput fra brukeren og beregner et resultat som vises som tekst. Dette er ikke veldig objektorientert og kanskje heller ikke så spennende. Et alternativ er noe mer app-aktig, som både er morsommere å kunne og passer bedre til objektorientert tenkning.

Karakteristisk for en app er at den først starter opp (åpner vinder og viser frem innhold) og så bare venter på (å reagere på) input fra brukeren. Dette er nokså likt hvordan et objekt fungerer: først opprettes og initialiseres det, og siden så venter det på at metodene skal bli kalt.