Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Comment: Migrated to Confluence 5.3

Introduksjon

Excerpt

I denne leksjonen vil du lære hvordan du kan koble FXML-koden til Java-koden og gjøre app-en levende (interaktiv)!

Steg 1: Opprette app-pakke, app-klasse, FXML-fil og controller-klasse.

Lag først en ny app-pakke og app-klasse som forklart i Hello world-leksjonen. Bruk kalkulator som navn på pakken og KalkulatorApp som navn på app-klassen.

...

Det vesentlig her er fx:controller="kalkulator.KalkulatorController", som forteller at KalkulatorController er klassen som skal reagere på og styre knappene og tekstfeltet i KalkulatorApp.fxml.

Steg 2: Koble FXML-fil og Controller sammen

Hvis du har lagt inn fx:controller="kalkulator.KalkulatorController"KalkulatorApp.fxml, så vil det automagisk bli laget en KalkulatorController når FXML-en lastes inn (av FXMLLoader-en i KalkulatorApp sin start-metode). Det som da gjenstår er å koble KalkulatorController-en til knappene og tekstfeltet, og tenke ut hva som er riktig kalkulatoren-logikk. Kalkulator-logikken er ikke så enkel som en kan tro, så vi tar sammenkoblingen først.

...

I begge tilfeller må en legge til kode i både Kalkulator.fxml og KalkulatorController, men på ulike måter.

Fra FXML til Controller

Alle elementene i FXML-fila kan si fra om hva brukeren gjør med dem. Knapper kan f.eks. si fra når de trykkes på, og tekstfelt kan si fra når teksten inn endres. Dette kalles hendelser (eng: events) og det vi må gjøre er å legge inn koder i FXML-fila som angir hvilken metode i Controller-en som skal kalles for hver type hendelse på de ulike elementene. Hendelsen "knappetrykk" kalles action, og derfor legger en inn onAction="#handleButton" i FXML-koden for en knapp for å kalle handleButton-metoden når akkurat den knappen trykkes. Hvis forskjellige knapper skal ha omtrent samme logikk, så kan en bruke samme metode. F.eks. så skal kalkulatoren gjøre omtrent det samme for alle tall-knappene, så derfor kan en legge onAction="#handleDigitButton" på alle disse. En må selvsagt huske på å lage handleDigitButton-metoden også. Her er eksempelkode for tall-knappene:

I KalkulatorController.java:

Code Block
languagejava
public class KalkulatorController {
 
	... annen kode ...
 
	@FXML 
	void handleDigitButton(ActionEvent event) {
		// denne setningen skriver ut hvilken knapp som kalte metoden
		System.out.println(event.getSource());
		... her er kode for tall-knappene ...
	}
}

@FXML brukes for å gi FXML-en lov til å kalle metoden, mens void betyr at metoden ikke skal returnere noen verdi.

event-parameteret (av typen ActionEvent) inneholder informasjon om hendelsen. F.eks. kan en hente ut hvilken knapp som ble trykket med event.getSource(). Her skriver vi ut denne verdien, for å sjekke at det virker som det skal. Hvis det ikke er viktig å vite hvilken knapp som ble trykket (f.eks. hvis den bare kalles av én knapp), så trenger du ikke ha med event-parameteret i metoden.

Hvis du senere ønsker å håndtere andre typer hendelser, f.eks. museklikk, så må du bruke en annen argument-type enn ActionEvent. For museklikk (og andre muse-hendelser) må du f.eks. bruke MouseEvent.

I KalkulatorApp.fxml

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

<AnchorPane ...	fx:controller="kalkulator.KalkulatorController">
	<children>
		<Button text="1" onAction="#handleDigitButton"    ... />
		<Button text="12" onAction="#handleDigitButton" ... />
 
	... de andre knappene kommer her ...
	</children>
</AnchorPane>

Både 1- og 2-knappen har onAction="#handleDigitButton", så for begge disse vil handleDigitButton blir kalt når de trykkes på.

Prøv å gjøre endringene vist over, kjør koden og se om du får skrevet ut hvilken knapp som trykkes. Husk at det bare er knappene som har onAction="#handleDigitButton" som vil kalle handleDigitButton-metoden.

Fra Controller til FXML

For at KalkulatorController-koden skal ha en synlig effekt, så må den kunne endre på elementene som vises i app-vinduet. F.eks. så ønsker jo vi å endre på teksten som vises i tekstfeltet. For å få det til, så må en ha én eller flere variabler som peker på elementer i KalkulatorApp.fxml-fila og få satt disse automagisk når FXML-fila lastes inn. Trikset er igjen å legge inn kode både i KalkulatorController og KalkulatorApp.fxml. I dette tilfellet deklarerer man en variabel i KalkulatorController og legger in et fx:id-attributt i KalkulatorApp.fxml. Typen og navn til variabelen må stemme med typen FXML-element og fx:id-en. 

I KalkulatorController.java:

Code Block
languagejava
public class KalkulatorController {
 
	... annen kode ...
 
	@FXML
	TextField valueTextField; // denne vil bli satt til å peke på tekstfeltet med fx:id="valueTextField"
	@FXML
	void handleDigitButton(ActionEvent event) {
		Button button = (Button) event.getSource();
		valueTextField.setText(button.getText());
	}
}

@FXML brukes her for å gi FXML-en lov til å sette variabelen. Typen TextField stemmer med FXML-elementet og navnet "valueTextField" stemmer med fx:id-verdien.

Koden i handleDigitButton er endret, slik at den først henter ut knappen med event.getSource()og så setter teksten til tekstfeltet til knappens tekst med valueTextField.setText(button.getText()).

I KalkulatorApp.fxml

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

<AnchorPane ...	fx:controller="kalkulator.KalkulatorController">
	<children>
		<TextField fx:id="valueTextField" ... />
 
		<Button text="1" onAction="#handleDigitButton" ... />
		<Button text="2" onAction="#handleDigitButton" ... />
 
	... de andre knappene kommer her ...
 
</AnchorPane>

 

Fra Controller til FXML

Fra Kalkulator.fxml til KalkulatorController

	</children>
</AnchorPane>

Tekstfeltet har fått en fx:id som stemmer med variabel-navnet. Også typen TextField stemmer med FXML-koden for tekstfeltet.


Prøv å gjøre endringene vist over, kjør koden og se om teksten i tekstfeltet endres når du trykker på tall-knappene.

Steg 3: Kalkulator-logikk

Så langt har vi fokusert på å få teknikken for å koble FXML-fil og Controller-klasse sammen til å virke. Nå kan vi begynne å se på kalkulator-logikken. Det vanskeligste er i grunnen å finne ut hvilke variabler som trengs for å holde rede på kalkulator-tilstanden, og så endre variablene på riktig måte når knappene trykkes på. Det kan også være vanskelig å organisere koden så det ikke bare blir rot når en legger til logikk. Vi skal starte med logikken for å bygge opp et tall ved å legge nye sifre til bakerst, altså det som gjør at en får 2469 hvis en trykker knappene 2, 4, 6 og 9 i rekkefølge.

Den mest direkte måten å gjøre det på er å endre teksten i tekstfeltet ved å kalle valueTextField.setText(valueTextField.getText() +  button.getText()) i handleDigitButton-metoden. En henter altså ut teksten slik den er i tekstfeltet, legger teksten på knappen bak denne og putter resultatet tilbake. Koden er vist under til venstre. Vi skal imidlertid gjøre det litt mer indirekte, fordi det i lengden gjør koden ryddigere. Trikset er å ha en egen variabel (valueText) for tallet som bygges opp, og ha to metoder, én for å legge til nye tall (appendDigit) og én for å oppdatere teksten i tekstfeltet (updateValueTextField). Koden er vist under til høyre.

Direkte variant, som endrer teksten i tekstfeltet direkte.

Code Block
languagejava
public class KalkulatorController {
 
	... annen kode ...
 
	@FXML
	TextField valueTextField;
 
	@FXML
	void handleDigitButton(ActionEvent event) {
		Button button = (Button) event.getSource();
		String currentText = valueTextField.getText();
		String newText = currentText + button.getText();
		valueTextField.setText(newText);
	}
}
Code Block
languagejava
public class KalkulatorController {
 
	String valueText = "";
 
	void appendDigit(String digit) {
		valueText = valueText + digit;
	}

	@FXML
	TextField valueTextField;
 
	void updateValueTextField() {
		valueTextField.setText(valueText);
	}
 
 @FXML
	void handleDigitButton(ActionEvent event) {
		Button button = (Button) event.getSource();
		appendDigit(button.getText());
		updateValueTextField();
	}
}

Vi ser at det er litt mer kode, men i praksis skjer det samme: Teksten i valueTextField blir lengre og lengre. Fordelen med den høyre varianten er at vi deler koden opp i mer håndterlige deler (metoder) med logiske navn. Etterhvert som vi legger til mer logikk, så utvider vi kanskje med flere variabler og metoder og/eller vi utvider metodene vi allerede har laget. Et viktig prinsipp å skille mellom metoder som endrer på variabler og metoder som oppdaterer det som vises i app-vinduet. Siden vi bare har ett tekstfelt, så trenger vi faktisk bare én metode for å oppdatere app-vinduet, nemlig updateValueTextField.

For å få oversikt over all logikken som trengs, så er det lurt å prøve å tenke på hva som skal skje for ulike rekkefølger av tastetrykk. Vi har allerede sett hva som skjer hvis en trykker på tall-knappene: valueText blir lengre og lengre. Men hva skal skje når du så trykker på +-knappen? Det er klart at KalkulatorController-en på en eller annen måte må huske det, så den vet at den må utføre +-operasjonen når en senere trykker på =-knappen. Hvis en tenker på mange nok ulike tilfeller, så vil en til slutt ha oversikt over alt kalkulatoren må kunne huske, dvs. lagre i variabler og hvordan disse endre for hvert tilfelle. Det kan være greit å sette opp hvert tilfelle i en tabell, som en så bruker som en slags oppskrift når en begynner på koden.

I tabellen under så har vi laget fem kolonner. Den første, Tastetrykk, viser hva som er tastet så langt. Den andre, Logikk, forklarer med tekst hva som gjøres. De tre neste er variablene som trengs: valueText er tallet en er i ferd med å legge inn, memory er tallet en allerede har skrevet inn eller regnet ut, og operator er den siste operasjonen en trykket på. Verdien i hver variabel-kolonne er resultatet av å utføre det som er beskrevet i Logikk-kolonnen. Den siste er teksten som vises i tekstfeltet, som om updateValueTextField() kalles automatisk etter at Logikk-en er utført. Merk at en for String-variabler skiller mellom null, som du kan tenke på som ingenting og "", som er en tom tekst

TastetrykkLogikkString valueTextdouble memoryString operatorvalueTextField.text
(før noen knapper er trykket)memory vises i tekstfeltetnull0.0null"0.0"
1Bygger opp ny valueText, som vises i tekstfeltet"1"0.0null"1"
1, 2Legger nytt siffer bakerst i valueText"12"0.0null"12"
1, 2, +

Gjør om valueText til et tall og legger det i memory.
Blanker valueText og lagrer +-operasjonen i operator

null12.0"+""12.0"
1, 2, +, 3Bygger opp ny valueText"3"12.0"+""3"
1, 2, +, 3, 4Legger nytt siffer bakerst i valueText"34"12.0"+""34"
1, 2, +, 3, 4, =Gjør om valueText til et tall og utfører operator-operasjonen på memory og det nye tallet.
Resultatet legges i memory, og så blankes operator og valueText
null46.0null"46.0"

Tilfellet over var et nokså vanlig og enkelt tilfelle. La oss prøve et litt mer komplisert et. Vi hopper over første rad, siden den er som over. I tillegg så bytter vi ut Logikk-teksten med noe som ligner på metode-kall. Det vil hjelpe oss når vi siden skal skrive koden for Logikk-en. 

TastetrykkLogikkString valueTextdouble memoryString operatorvalueTextField.text
1append2ValueText("1")"1"0.0null"1"
1, .append2ValueText(".")"1."0.0null"1."
1, ., +

convertAndStoreValueText(), clearValueText(), storeOperator("+")

null1.0"+""1.0"
1, ., +, 3append2ValueText("3")"3"1.0"+""3"
1, ., +, 3, -convertValueTextComputeAndStoreValue(), clearValueText(), storeOperator("-")null4.0"-""4.0"
1, ., +, 3, -, 2append2ValueText("4")"2"4.0"-""2"
1, ., +, 3, -, 2, =convertValueTextComputeAndStoreValue(), clearValueText(), storeOperator(null)null2.0null"2.0"

Nå kan du prøve å lage din egen tabell med tilfellet hvor Tastetrykk er sekvensen 1, +, 2, =, +, 3, =.

 

Steg 4: Skriv koden!

Det er din oppgave! For å gjøre det litt lettere så viser vi her noen kode-snutter for småproblemer du sikkert vil støte på underveis.

ProblemKodeForklaring
Gjøre om fra tekst (String) to (desimal)tall (double)Double.valueOf(text)Du kaller Double-klassen sin valueOf-metode, for å konvertere en tekst til en double.
Gjøre om fra (desimal)tall til tekstString.valueOf(num) eller "" + numDu kaller enten String.valueOf, eller legger num til "", som konverterer num i samme slengen
Sjekke om en String-variabel er ingentingif (text != null) { ... }En bruker != null for å sjekke om er variabel er ingenting
Sjekke om en String-variabel er tomif (text.length() == 0) { ... }text.length() gir deg antall bokstaver i en String, så her sjekker vi om antall bokstaver er 0

Hva har du lært?

  • å koble FXML-koden til Java-koden med fx:id- og onAction-attributter
  • å organisere koden i flere mindre metoder
  • å skille mellom metoder som endrer interne variabler og de som oppdaterer app-vinduet 

P.S.

Her er én av manger løsninger som virker omtrent som foreslått over:

Code Block
languagejava
collapsetrue
package kalkulator;

import javafx.beans.property.StringProperty;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;

public class KalkulatorController {
	private double memory = 0.0;
	private String valueText = null;
	private String operator = null;

	@FXML
	TextField valueTextField;

	@FXML
	void initialize() {
		updateValueField();
	}

	private void updateValueField() {
		if (valueText != null) {
			valueTextField.setText(valueText);
			validateValueTextField();
		} else {
			valueTextField.setText(String.valueOf(memory));
		}
	}

	private void validateValueTextField() {
		try {
			// prøv å konvertere teksten i tekstfeltet
			Double.valueOf(valueTextField.getText());
			valueTextField.setStyle(null);
		} catch (Exception e) {
			valueTextField.setStyle("-fx-border-color: red;");
		}
	}

	private void append2ValueText(String s) {
		if (valueText == null) {
			valueText = "";
		}
		valueText = valueText + s;
	}

	private void clearValueText() {
		valueText = null;
	}

	private void convertAndStoreValueText() {
		memory = Double.valueOf(valueText);
	}

	private void convertValueTextComputeAndStoreValue() {
		double value = Double.valueOf(valueText);
		switch (operator) {
		case "/": memory = memory / value; break;
		case "*": memory = memory * value; break;
		case "+": memory = memory + value; break;
		case "-": memory = memory - value; break;
		}
	}

	private void storeOperator(String op) {
		operator = op;
	}

	@FXML
	void handleDigitButton(ActionEvent event) {
		Button button = (Button) event.getSource();
		append2ValueText(button.getText());
		updateValueField();
	}

	@FXML
	void handleOperatorButton(ActionEvent event) {
		if (valueText != null) {
			if (operator != null) {
				convertValueTextComputeAndStoreValue();
			} else {
				convertAndStoreValueText();
			}
		}
		clearValueText();
		Button button = (Button) event.getSource();
		storeOperator(button.getText());
		updateValueField();
	}

	@FXML
	void handleDesimalPoint() {
		append2ValueText(".");
		updateValueField();
	}

	@FXML
	void handleComputeButton() {
		if (valueText != null && operator != null) {
			convertValueTextComputeAndStoreValue();
			clearValueText();
			storeOperator(null);
		}
		updateValueField();
	}
	// manuell redigering av valueTextField

	@FXML
	void handleValueTextFieldChange(StringProperty prop, String oldValue, String newValue) {
		if (valueText != null && (! newValue.equals(valueText))) {
			valueText = newValue;
			validateValueTextField();
		}
	}
}