Et GUI-rammeverk er en samling klasser som gjør det mulig å bygge rike, funksjonelle grafiske brukergrensenitt uten alt for mye arbeid. Brukergrensesnitt bygget med slike rammeverk har noenlunde samme struktur, og her skal vi gi en liten introduksjon med utgangspunkt i JavaFX-rammeverket. Vi har valgt JavaFX, fordi det er mer fleksibelt og moderne enn Java Swing, som det er ment å ta over etter og det ligner mer på andre "rammeverk" som HTML5 og Flash.
JavaFX er en standard del av både Java 7 og Java 8, men merk at om du bruker Java 7, så må du selv legge til en JavaFX-jar i Build Path for prosjektet ditt, for å kunne bruke JavaFX-klassene. Denne heter jfxrt.jar og finnes i jre/lib-mappen i Java-installasjonen din. Et alternativ er å installere e(fx)clipse-tillegget til Eclipse, som bidrar med en veiviser for å opprette JavaFX-prosjekter.
Vi deler gjerne rammeverket opp i håndtering av
- kjøring/oppstart av programmer
- strukturer av grafiske og interaktive elementer, altså det rent visuelle
- interaktivitet, altså brukerinput og dynamikk
- GUI-definisjonsfiler
Et "vanlig" program starter naturlig nok med det første. Så bygges det opp en visuell struktur, med en blanding av rent grafiske elementer og interaktive komponenter (ofte kalt widgets). De interaktive elementene knyttes deretter til programlogikken som skal reagere på og håndtere input fra brukeren. De fleste rammeverk gjør det mulig å beskrive det andre punktet i separate XML-filer (litt som HTML brukes for web-sider), slik at programmet blir mindre og ryddigere.
Vi skal ta disse i tur og orden.
Kjøring/oppstart av programmer
JavaFX har en egen programklasse kalt Application (i pakken javafx.application) som en må arve fra. Denne definerer en del metoder som kalles når programmet initialiseres (init), starter (start) opp og stopper (stop). Av disse er det bare start-metoden som må defineres i din egen hovedprogramklasse. Men merk at det ikke er din egen kode som oppretter instansen av hovedprogramklassen og kaller disse oppstartsmetodene, det er det rammeverket selv som gjør. Og for at det skal skje må du i main-metoden kalle den statiske metoden launch i Application-klassen med navnet til din egen klasse som første argument og programargumentene til din main-metode som launch-metoden sitt andre argument. En minimalt hovedprogramklasse er vist under til venstre, med applikasjonsvinduet som da dukker opp til høyre.
public class MinimalApplication extends Application { @Override public void start(Stage stage) throws Exception { Pane root = new Pane(); // Root of the scene graph Scene scene = new Scene(root, 500, 500); stage.setScene(scene); stage.setTitle("MinimalApplication"); stage.show(); } public static void main(String[] args) { launch(MinimalApplication.class, args); } } |
start-metoden tar inn et argument av typen Stage (i javafx.stage-pakken), som tilsvarer applikasjonsvinduet. Denne må knyttes til innhold ved at en oppretter en instans av Scene (javafx.scene) med selve innholdet i. I denne minimale applikasjonen så nøyer vi oss med en instans av Pane (javafx.scene.layout), som gir oss t tomt panel. I tillegg til Pane-instansen så oppgir vi her også bredden og høyden Til slutt settes tittelen til applikasjonsvinduet og den vises frem. Merk at launch-metoden som ble kalt i main-metoden ikke returnerer (mens vinduet er oppe), så det er poengløst å legge annen koden etter launch-kallet.
Strukturer av grafiske og interaktive elementer
Innholdet i applikasjonen utgjøres av en såkalt scene graph, som er en hierarkisk struktur av såkalte noder. Node kan være beholdere (eng: container) for andre noder, rene grafiske elementer som streker, rektangler, ellipser polygoner osv. eller interaktive komponenter som knapper, tekstfelt, menyer, lister osv. Nodene har informasjon om plassering og beholderne kan i tillegg ha egen logikk for hvordan innholdet fordeles utover i bredden og høyden. Denne strukturen er illustrert i figurene under (hentet fra Oracle sine nettsider).
Nodetype | Klasser som kan opptre som tilsvarende nodetype |
---|
Det er få om ingen begrensninger på hva som kan være inni hva, og sammen så forteller denne strukturen akkurat hva som skal vises på skjermen for brukeren. Dersom en underveis i kjøringen endrer på noen av objektene, f.eks. endrer posisjoner, farger, fonter eller legger til og/eller fjerner elementer, så oppdateres skjermen tilsvarende. Man slipper altså å forholde seg til primitive tegnemetoder, men manipulerer istedet på objektstrukturer og lar systemet ta seg av detaljer omkring hvordan disse tegnes (såkalt rendering).
Container-klasser og layout
Når en bygger opp dette objekt-hierarkiet så tenker en gjerne først på den overordnede layout-strukturen, dvs. utlegget av hovedelementer og velger container-typer som gir ønskede muligheter. Merk at det er mulig å unnta en node fra den vanlige layout-logikken til en container vet at den markeres som ikke managed, ved å kalle setManaged(false).
Her er en liste av de vanligste container-typene (nærmere beskrevet i http://docs.oracle.com/javafx/2/layout/builtin_layouts.htm):
- HBox og VBox - legger ut elementene horisontalt (x-dimensjonen, dvs. bortover) eller vertikalt (y-dimensjonen, dvs. nedover) og gjør dem like store i den andre dimensjonen.
- GridPane og TilePane - plasserer elementene i et rutenett, TilePane gjør alle ruten like store, mens GridPane er mer fleksibel.
- BorderPane - fordeler elementene i regioner, midten (for hovedinnholdet), top, bunn, venstre og høyre, og passer for hovedinnholdet i applikasjonen.
- AnchorPane - henger elementene fast i punkter knyttet til en eller flere av sidene
- Pane - generell container uten spesifikk layout, så posisjonen på elementer må settes manuelt.
Alle container-objektene har en liste med elementer i sin children-liste (hentes ut med getChildren()), som igjen kan være container-objekter, om en ønsker, f.eks. en GridPane inni en AnchorPane inni en BorderPane. Hele denne strukturen legges så inn i en Scene i en Stage.
Det er generelt to metoder for å legge til barn:
- container.getChildren().add(node); // legger til én node
- container.getChildren.addAll(node1, node2, node3,....); // legger til flere noder
Noen container-klasser har egne metoder for å legge inn noder, f.eks. har BorderPane én metode for hver av de fem regionene som den deler panelet inn i. Dette er vist i eksemplet under, som utvider MinimalApplication med en BorderPane og ett tekst-objekt for hver region.
public class BorderPaneApplication extends Application { @Override public void start(Stage stage) throws Exception { BorderPane root = new BorderPane(); // Root of the scene graph // Add one Text node in each region root.setTop(new Text("top")); root.setBottom(new Text("bottom")); root.setLeft(new Text("left")); root.setRight(new Text("right")); root.setCenter(new Text("center")); Scene scene = new Scene(root, 500, 500); stage.setScene(scene); stage.setTitle("BorderPaneApplication"); stage.show(); } public static void main(String[] args) { launch(BorderPaneApplication.class, args); } } |
Noder
Det finnes noen fellestrekk som gjelder for alle nodene man bruker i JavaFX. Alle noder, utenom rotnoden, legges til i grafen ved å legge dem inn i children-lista til en Parent-instans. En node kan bare være ett sted i hierarkiet og dersom man forsøker å legge til et objekt som allerede er i hierarkiet til en annen/ny container, så vil objektet fjernes fra dens forrige, før den legges til den nye.
Til hver node kan det knyttes en id, altså et unikt navn (må sikres av utvikleren) tilsvarende id-tag'en i HTML. En nyttig funksjon er lookup(String id) som kan brukes til å finne en node med en unik id i en del av hierarkiet. Ellers kan også id'en brukes til å identifisere noder når man bruker css-stiler (mer om css lenger ned).
Alle noder har en x,y-posisjon og størrelse (desimaltall) i et koordinatsystem med økende verdier mot høyre på x-aksen, og økende verdier nedover på y-aksen. Med dette er altså (0,0) øverst til venstre. Denne x,y-posisjonen angis imidlertid ikke direkte, men er kombinasjonen av ulike typer transformasjoner:
- translateX og translateY - forskyver noden i x- og y-retningen
- rotate - roterer noden et antall grader rundt et gitt referansepunkt.
- scaleX og scaleY - skalerer størrelen med en x- og y-faktor og settes med scale-metoden
- layoutX og layoutY - gir en ekstra forskyving i x- og y-retning og brukes gjerne i forbindelse med midlertidige justeringer, f.eks. drag'n drop
Det går også an å lage egne transformasjonsobjekter, hvor disse effektene kan kombineres, og som kan knyttes til noder.
Figurer
Det finnes en rekke standard-figurer som kan brukes for rent grafisk innhold. Disse er alle subklasser av Shape-klassen (javafx.scene.shape), som får dermed en del felles egenskaper:
- fyll (fill) - fargen eller effekten (av typen Paint) som fyller figuren
- strek (stroke) - fargen eller effekten som brukes for å tegne figuren
- strek-tykkelse (strokeWidth), plassering (strokeType), stipling (strokeDashOffset) og hjørne/endehåndtering (strokeLineJoin og strokeLineCap)
Nyttige figurer er strek (Line), rektangel (Rectangle), sirkel og ellipse (Circle og Ellipse), buesegment (Arc), polygon (Polygon), segmentert figur (Path) og tekst (Text). I tillegg er det en egen klasse for bilder (ImageView som viser et Image, som ikke er en Shape). Noen av disse er vist i eksemplet under, hvor center-regionen er fylt med et Pane-objekt med noen figurer i, med ulike grafiske effekter.
@Override public void start(Stage stage) throws Exception { BorderPane root = new BorderPane(); // Root of the scene graph // Add one Text node in each surrounding region root.setTop(new Text("top")); root.setBottom(new Text("bottom")); root.setLeft(new Text("left")); root.setRight(new Text("right")); Pane shapesPane = new Pane(); shapesPane.setPrefSize(300, 300); Line line = new Line(10, 10, 100, 100); // x1, y1, x2, y2 line.getStrokeDashArray().setAll(10.0d, 10.0d); // dashes Rectangle rect = new Rectangle(150, 10, 30, 40); // x, y, w, h rect.setFill(Color.BLUE); Ellipse ell = new Ellipse(40, 180, 40, 30); // cx, cy, rx, ry ell.setStroke(Color.RED); ell.setStrokeWidth(5); ell.setFill(Color.GREEN); Text text = new Text(180, 180, "center"); List<String> fonts = Font.getFamilies(); text.setFont(new Font(fonts.get((int) (Math.random() * fonts.size())), 32)); shapesPane.getChildren().addAll(line, rect, ell, text); root.setCenter(shapesPane); Scene scene = new Scene(root, 500, 500); stage.setScene(scene); stage.setTitle("BorderPaneApplication"); stage.show(); } |
Les mer om enkel 2d-grafikk her: Enkel 2D-grafikk med JavaFX og FXML
Grafisk stil med CSS (Cascading Style Sheets)
En node har feltene id, styleClass og style. Disse brukes til å endre nodens grafiske stil med CSS. Feltene id og styleClass brukes i CSS for å avgjøre hvilke noder som skal ha de forskjellige stilene. Feltet style brukes til å legge på en CSS stil til en node direkte i koden. For en basic forståelse av dette konseptet, ta en titt på denne videoen. Oracle har også en god tutorial for å få et lite innblikk i mulighetene CSS gir oss.
Interaktivitet
De mest interessante nodene er de som er interaktive, altså reagerer på input fra brukeren og brukes til å styre applikasjonen og redigere applikasjonsdata. Her er en oversikt over standard-komponentene: http://docs.oracle.com/javafx/2/ui_controls/jfxpub-ui_controls.htm Standard-utvalget er foreløpig ikke så godt som i Java Swing, men det begynner å komme en del gode tredjeparts klassebiblioteker, f.eks. http://fxexperience.com/controlsfx/
De interaktive komponentene har en felles måte å rapportere hva brukeren gjør, gjennom såkalte hendelser (eng: event). Hendelsene kan fortelle om både elementære ting (såkalte leksikalske hendelser) som at muspekeren flyttes eller at en tast trykkes ned, eller mer høynivå ting (såkalte syntaktiske hendelser) som at en knapp trykkes, meny- eller liste-element velges, tekst redigeres osv. For å motta en hendelse, så må applikasjonen registrere såkalte lyttere, dvs. objekter som implementerer spesifikke lytter-grensesnitt og som får beskjed om hendelsene ved at spesifikke metoder kalles.
Det er to hovedkategorier hendelser, interaksjonshendelser og dataendringshendelser, og disse bruker hver sine lyttergrensesnitt og metoder for å registrere lyttere. La oss ta et tekstfelt som eksempel, siden rapporterer hendelser av begge typer:
Interaksjonshendelsen "action"
Når brukeren trykker return (for å angi at teksten er ferdigredigert), så genereres en interaksjonshendelser kalt "action" av typen ActionEvent. Denne rapporteres til grensesnittet EventHandler<ActionEvent> og metoden handle(ActionEvent). I praksis betyr dette at grensesnittet med tilhørende metode må implementeres av et objekt, og dette objektet må registreres som lytter. Det vanlige er at lytteren implementeres vha. en såkalt anonym indreklasse, som enkelt sagt er et enkeltobjekt som implementerer grensesnittet uten at en eksplisitt må opprette og navngi en ny klasse. I dette tilfellet gjøres det med følgende kode:
new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent actionEvent) { ... her er selve koden ... } }
Denne lytter(grensesnitt-instans)en oppgis direkte som argument til metoden som registrerer lytteren. Det finnes to (typer) metoder som brukes til dette:
- en generell addEventHandler-metode, som tar inn en hendelsestype og lyttereren, i dette tilfellet addEventHandler(ActionEvent.ACTION, new EventHandler<ActionEvent>() {... });
- spesielle setOnXXX-metoder, som brukes for spesifikke hendelsestyper og tar inn en lytter, i dette tilfellet setOnAction(, new EventHandler<ActionEvent>() {... })
I koden nedenfor har vi brukt setOn-varianten, siden den er enklest å lese og skrive.
Dataendringshendelse for "text"-egenskapen
Mens brukeren redigerer teksten i et tekstfelt, så genereres det kontinuerlig (for hver enkelt-endring) en dataendringshendelser for "text"-egenskapen (eng: property) til tekstfeltet. En slik egenskap kan ses på som en verdi (i dette tilfellet en String) som er innkapslet av et objekt som lar en lese, endre og lytte til endringer. Dette er en generell teknikk som brukes av alle dataorienterte komponenter, for å bruke og redigere data. Denne typen hendelser rapporteres til grensesnittet ChangeListener spesialisert til typen data, i dette tilfellet ChangeListener<String> og metoden changed. Også her er det praktisk å bruke enkeltobjekter som implementerer grensesnittet, i dette tilfellet følgende kode:
new ChangeListener<String>() { @Override public void changed(ObservableValue<? extends String> property, String oldValue, String newValue) { ... her er selve koden ... } }
Denne lytter(grensesnitt-instans)en oppgis (som over) direkte som argument til metoden som registrerer lytteren. Metoden er en del av objektet som innkapsler String-verdien, så følgende kode brukes: textProperty().addListener(new ChangeListener<String>() {... }); textProperty()-kallet returnerer text-egenskapen og det er denne som registrerer lytteren med addListener.
I eksemplet under har vi kombinert både interaksjonshendelser og dataendringshendelser for et tekstfelt i topp-regionen. Hvor hver redigering så kopieres teksten i tekstfeltet til Text-objektet i midt-regionen, og når brukeren trykker return, så gjøres denne teksten om til store bokstaver.
@Override public void start(Stage stage) throws Exception { BorderPane root = new BorderPane(); // Root of the scene graph textField = new TextField("center"); textField.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent arg0) { centerText.setText(textField.getText().toUpperCase()); } }); textField.textProperty().addListener(new ChangeListener<String>() { @Override public void changed(ObservableValue<? extends String> value, String oldText, String newText) { centerText.setText(textField.getText()); } }); centerText = new Text(180, 180, textField.getText()); // x, y, text root.setTop(textField); root.setBottom(new Text("bottom")); root.setLeft(new Text("left")); root.setRight(new Text("right")); Pane shapesPane = new Pane(); shapesPane.setPrefSize(300, 300); Line line = new Line(10, 10, 100, 100); // x1, y1, x2, y2 line.getStrokeDashArray().setAll(10.0d, 10.0d); // dashes Rectangle rect = new Rectangle(150, 10, 30, 40); // x, y, w, h rect.setFill(Color.BLUE); Ellipse ell = new Ellipse(40, 180, 40, 30); // cx, cy, rx, ry ell.setStroke(Color.RED); ell.setStrokeWidth(5); ell.setFill(Color.GREEN); centerText = new Text(180, 180, "center"); List<String> fonts = Font.getFamilies(); centerText.setFont(new Font(fonts.get((int) (Math.random() * fonts.size())), 32)); shapesPane.getChildren().addAll(line, rect, ell, centerText); root.setCenter(shapesPane); Scene scene = new Scene(root, 500, 500); stage.setScene(scene); stage.setTitle("BorderPaneApplication"); stage.show(); } |
GUI-definisjonsfiler
For større GUI, så blir koden for å bygge opp hierarkiet av grafiske og interaktive elementer fort nokså stor, selv om den ikke er så kompleks. Spesielt når en begynner å endre mange egenskaper, f.eks. farge, strek, font osv. så blir det mye kode og mye "støy". Helt analogt med hvordan HTML brukes til å beskrive strukturen og innholdet til en nettside, så tilbyr JavaFX et XML-format kalt FXML for å beskrive strukturen og innholdet til et GUI. Hierarkiet av XML-elementer (tags) tilsvarer hierarkiet av grafiske og interaktive elementer, og formatet støtter "alle" klasser og typer som er presentert over. En kan også utvide med egne elementer, hvis en lager egne klasser som passer inn i hierarkiet. Den viktigste ulikheten med HTML er at FXML-koden ikke inkluderer programlogikk, det er Java-koden som laster inn FXML-fila. Vi skal ikke gå i dybden på FXML-språket her (se heller http://docs.oracle.com/javafx/2/api/javafx/fxml/doc-files/introduction_to_fxml.html), men komme med en generell forklaring.
Det viktigste å vite om FXML er at det bygger direkte på klassene, metodene og attributtene som finnes i JavaFX-rammeverket og at disse brukes som navn på XML-elementer og attributter. Hvis et element heter javafx.scene.layout.Pane, så er det en direkte referanse til JavaFX-klassen Pane-klassen, og som i Java-kode så er det støtte for å importere navn, så en kan bruke kortformen Pane. Tilsvarende så kan en sette teksten i et Text-objekt med XML-attributtet text i (Text-elementet) fordi Text-klassen har en setText-metode. Sammenhengen mellom hva som er gyldig XML og hvilke klasser, metoder og attributter som finnes i JavaFX-rammeverket er sammenfattet i en del relativt enkle regler, så hvis en kjenner JavaFX-API'et, så er det ikke så vanskelig å bli vant med FXML-formatet.
Under ser vi det siste eksemplet kodet som FXML.
<?xml version="1.0" encoding="UTF-8"?> <?import java.lang.*?> <?import javafx.scene.*?> <?import javafx.scene.control.*?> <?import javafx.scene.layout.*?> <?import javafx.scene.text.*?> <?import javafx.scene.textfield.*?> <?import javafx.scene.shape.*?> <?import java.lang.Double?> <BorderPane xmlns:fx="http://javafx.com/fxml" prefHeight="500" prefWidth="500"> <top> <TextField id="textField" text="center"/> </top> <center> <Pane> <Line startX="10" startY="10" endX="100" endY="100"> <strokeDashArray> <Double fx:value="10"/> <Double fx:value="10"/> </strokeDashArray> </Line> <Rectangle x="150" y="10" width="30" height="40" fill="blue"/> <Ellipse centerX="40" centerY="180" radiusX="40" radiusY="30" strokeWidth="5" fill="green" stroke="red"/> <Text id="centerText" x="180" y="180" text="center"> <font> <Font name="Times" size="32"/> </font> </Text> </Pane> </center> <left> <Text text="left"/> </left> <right> <Text text="right"/> </right> <bottom> <Text text="bottom"/> </bottom> </BorderPane>