You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 14 Current »

Med JavaFX er det lett å lage enkel 2D-grafikk, og med FXML så blir det enda enklere!

 

Figurer i FXML

Med FXML er det nokså enkelt å lage større figurer, som en sammensetning av enkle figur-elementer som sirkler, rektangler og streker. En enkel strekmann kan se ut som følger med FXML:

    <Group layoutY="50">
        <Circle layoutX="100" layoutY="20" radius="20" stroke="black" fill="white"/>
        <Circle layoutX="112" layoutY="20" radius="2" stroke="black" fill="blue"/>
        <Line layoutX="120" layoutY="20" startX="0" startY="-4" endX="3" endY="0" stroke="black"/>
        <Line layoutX="120" layoutY="20" startX="3" startY="0" endX="0" endY="1" stroke="black"/>
        <Line layoutX="100" layoutY="40" endX="0" endY="30" stroke="black"/>
        <Group layoutX="100" layoutY="50">
            <Line layoutY="0"  endX="-10" endY="10" stroke="black"/>
            <Line layoutY="0"  endX= "10" endY="10" stroke="black"/>
            <Line layoutY="20" endX="-10" endY="15" stroke="black"/>
            <Line layoutY="20" endX= "10" endY="15" stroke="black"/>
        </Group>
    </Group>

Denne figuren er laget med FXML-koden til venstre.

Ved å se på figur-typen og koordinatene skal det være nokså lett å se hvilke kode-linjer som tilsvarer hodet, øyet, nesa, kropp, armer og bein. Posisjonen (koordinatene) styres på tre måter:

  • layoutX og layoutY i Group-nodene: Disse verdiene legges til koordinatene lenger ned i hierarkiet.
  • layoutX og layoutY i Circle og Line: Disse plasserer figurene i forhold til gruppen de er i.
  • startX, startY, endX og endY i Line: Disse verdiene angir start- og slutt-posisjonen til streken og er relativt til layout-verdiene.

Legg altså merke til hvordan koordinat-verdiene legges sammen, slik at den faktiske plasseringen bestemmes av verdier mange steder i koden. Gruppering med Group kan nettopp være lurt for å gjøre det enklere å plasserer figure-elementer riktig.

Visuelle effekter angis med ullike attributter, f.eks. brukes fill for å angi fargen som en (del)figur fylles med og stroke angir strek-fargen. Muligheten er bestemt av typen figur-element og egenskapene til den tilsvarende Java-klassen. F.eks. kan både Line og Circle ha et stroke-attributt, fordi begge arver en setStroke-metode fra deres felles superklasse Shape.

Merk at for å vise frem strekmann-figuren, så trengs det litt mer FXML-kode over/rundt og litt Java-kode for å laste inn FXML-koden og vise frem figuren. Den komplette FXML- og Java-koden er vist under.

<?xml version="1.0" encoding="UTF-8"?>
 
<?import javafx.scene.layout.Pane?>
<?import javafx.scene.Group?>
<?import javafx.scene.shape.Circle?>
<?import javafx.scene.shape.Line?>

<Pane xmlns:fx="http://javafx.com/fxml"
     minWidth="400" minHeight="600">
    <Group layoutY="50">
        <Circle layoutX="100" layoutY="20" radius="20" stroke="black" fill="white"/>
        <Circle layoutX="112" layoutY="20" radius="2" stroke="black" fill="blue"/>
        <Line layoutX="120" layoutY="20" startX="0" startY="-4" endX="3" endY="0" stroke="black"/>
        <Line layoutX="120" layoutY="20" startX="3" startY="0" endX="0" endY="1" stroke="black"/>
        <Line layoutX="100" layoutY="40" endX="0" endY="30" stroke="black"/>
        <Group layoutX="100" layoutY="50" visible="true">
            <Line layoutY="0"  endX="-10" endY="10" stroke="black"/>
            <Line layoutY="0"  endX= "10" endY="10" stroke="black"/>
            <Line layoutY="20" endX="-10" endY="15" stroke="black"/>
            <Line layoutY="20" endX= "10" endY="15" stroke="black"/>
        </Group>
    </Group>
</Pane>

package trinn2;

import java.io.IOException;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class StickMan1 extends Application {
    @Override
    public void start(Stage primaryStage) throws IOException {
        Parent root = FXMLLoader.load(this.getClass().getResource("StickMan1.fxml"));
        primaryStage.setScene(new Scene(root));
        primaryStage.show();
    }
    public static void main(String[] args) {
        launch(args);
    }
}

Den komplette FXML-koden er vist til venstre. Import-linjene øverst trengs for å kunne bruke navn tilsvarende Java-klasser lenger ned i fila. F.eks. vil Circle egentlig referere til java-klassen javafx.scene.shape.Circle. Java-koden antar her at FXML-fila ligger i samme mappe som java-fila.

Bevegelige figurer

Figurene i FXML-filer er i utgangspunktet statiske; filene lastes inn og innholdet vises frem. For å lage dynamisk grafikk, altså bevegelige figurer, så trenger en å koble sammen FXML-kode og Java-kode. For å få det til, så må koden kunne to ting:

  1. Referere til figur-elementer: Uten denne muligheten går det ikke an å få endre på figur-instansene, f.eks. Circle- og Line-objekter, som ble laget da FXML-fila ble lastet inn og vist.
  2. Reagere på input fra brukeren: Uten denne muligheten kan ikke brukeren påvirke eller styre hva som skjer.

Dette kan gjøres uten FXML, men det er litt enklere hvis en bruker mekanismer innebygget i FXML, som vi skal se.

Referanser til figur-objekter

Når en laster inn og viser frem (innholdet i) en FXML-fil, så blir det enkelt sagt, laget ett Java-objekt for hver FXML-tag tilvarende tag-navnet. F.eks. vil <Group> gi et Group-objekt og <Circle> gir et Circle-objekt. Objektene legges inni hverandre tilsvarende tag-strukturen. F.eks. vil sirkelen havne inni gruppa, siden <Circle> inni <Group>. Det første problemet en støter på når program-koden skal endre på sirkelen, er hvordan få tak i sirkel-objektet i første omgang, når det ligger dypt nedi en struktur som ble laget under FXML-innlasting. Heldigvis har FXML en todelt mekanisme for akkurat det: 1) en kan knytte id-er til tag-ene og 2) få FXML til å sette tilsvarende variabler ved innlasting. 

Til hver FXML-tag eller node kan det knyttes en id, altså et unikt navn (må sikres av utvikleren), helt analogt med id-tag'en i HTML. Dette angis i FXML-en med fx:id="...", f.eks. <circle fx:id="headCircle"/>, hvor alle id-er må være forskjellig i én fil. (fx-prefikset må være deklarert tidligere med xmlns:fx="http://javafx.com", typisk i ytterste tag, som vist i koden over.) Men merk at det kun er elementene en må ha tak i senere, som trenger id, så en trenger ikke finne på så mange id-er pr. fil.

Når en FMXL-fil først er lastet inn, så får en i utgangspunktet kun en referanse til det ytterste objektet, gjerne kalt rot-noden. For å få tak i et objekt med en bestemt id lenger ned i strukturen, så kan en kalle metoden lookup(String id) på rot-noden. F.eks. vil rootNode.lookup("#headCircle") returnere noden under rootNode med id-en headCircle. Det er også mulig å søke etter noder eller strukturer av bestemte typer, men det er mindre praktisk siden en ofte har mange objekter av samme type.

Den typiske kode-strukturen vil være:

  • Ett felt pr. JavaFX-objekt en vil ha tak i, gjerne med samme navn id-en, f.eks. Circle headCircle;
  • En initialiseringsmetode (enten start-metoden selv eller en egen initialize-metode) med én linje pr. felt/objekt, hvor en slår opp objektet med lookup-metoden og setter feltet, f.eks. headCircle = (Circle) rootNode.lookup("#headCircle").

Siden dette er et så vanlig mønster, så kan FXML-mekanismen automatisere oppslag og setting av feltene. Denne automatikken krever to ting:

  • FXML-fila må lastes inn av et eget FXMLLoader-objekt, som må få vite hvilket objekt som håndteres av automatikken. Dette objektet fungerer som såkalt kontroller og settes med setController-metoden. For små applikasjoner, så brukes gjerne applikasjonsobjektet selv, som i koden under hvor this brukes som argument.

  • Feltene som skal settes automatisk  annoteres med @FXML, så det er tydelig at feltet skal håndteres av automatikken. Merk at automatikken utføres som en del av kallet til FXMLLoader sin load-metode, så en kan ikke referere til de @FXML-annoterte feltene før load-kallet er utført.

Her er eksempel-kode som viser dette i praksis (komplett kode er vist lengre ned):

public class StickMan2 extends Application {
    @Override
    public void start(Stage primaryStage) throws IOException {
        FXMLLoader fxmlLoader = new FXMLLoader();
        fxmlLoader.setController(this);
        Parent root = (Parent) fxmlLoader.load(this.getClass().getResourceAsStream("StickMan2.fxml"));
        primaryStage.setScene(new Scene(root));
        primaryStage.show();
    }
	...
    @FXML Node stickMan, armsAndLegs1, armsAndLegs2;
    @FXML Shape eye;
	...
}

Reagere på input fra brukeren

Standard-teknikken for å håndtering av interaktive elementer er bruk av såkalte lyttere, som er (ofte egne) objekter med metoder som kalles når noe bestemt skjer, f.eks. at en bruker trykker en knapp. Denne teknikken er litt omstendelig (vi viser ikke eksempler her), så også her tilbyr FXML litt automatisering. Tanken er at det samme kontroller-objektet som settes med setController-metoden, også brukes som lytter, dvs. inneholder metoder som automatisk skal kalles ved gitte hendelser, som musklikk og tastetrykk. Denne automatikken ligner litt på teknikken beskrevet over, ved at den også er en kombinasjon av FXML-attributter og @FXML-annotering av Java-elementer:

  • I Java-koden lager en metoder for å håndtere bruker-input, og disse annoteres ed @FXML, f.eks. @FXML void handleButtonClick() { ... }. Disse metodene kan (men må ikke) deklarere et hendelse-argument for å ta imot mer detaljert informasjon om hva brukeren gjorde. Typen til argumentet er avhengig av typen hendelse, f.eks. vil et trykk på en Button ha typen ActionEvent.
  • I FXML-koden angis at disse metoden skal kalles ved å bruke on-attributter med verdien #metodenavn, f.eks. <Button onAction="#handleButtonClick"/>. Akkurat hvilke on-attributter en kan bruke, er avhengig av typen element, f.eks. støtter Button flere bl.a. onAction, onKeyPressed og onMouseClicked.

Her er eksempel-kode som viser dette i praksis, med Java-kode til venstre og FXML-kode til høyre (komplett kode er vist lengre ned):

    @FXML
    void walk() {
        step = ! step;
        stickMan.setLayoutX(stickMan.getLayoutX() + 5);
        update();
    }

@FXML angir at walk-metoden deltar i håndtering av hendelser vha. FXML-automatikken

 

        <HBox>
            <Button text="Walk!" onAction="#walk"/>
        </HBox> 

onAction="#walk" angir at knappens action-hendelse skal trigge walk-metoden

StickMan2

Teknikken beskrevet over er brukt i en utvidelse av eksemplet over, hvor en strekmann "spaserer" mot høyre og blunker når brukeren klikker på en knapp. Bevegelsen mot høyre håndteres ved å øke x-koordinaten for hvert steg (klikk på knappen), med følgende linje i walk-metoden: stickMan.setLayoutX(stickMan.getLayoutX() + 5); Skritt-effekten håndteres ved å ha to sett med armer og ben og veksle mellom dem ved å slå dem av og på med kall til setVisible-metoden. Blunke-effekten håndteres ved å veksle mellom to farger med kall til setFill-metoden. Steg-logikken er fordelt mellom walk-metoden, som bare lar step-feltet veksle mellom true og false, og update-metoden, som kaller setVisible- og setFill-metodene med verdier avhengig av step-feltet.

    void update() {
        if (step) {
            eye.setFill(Color.BLUE);
        } else {
            eye.setFill(Color.WHITE);
        }
        armsAndLegs1.setVisible(step);
        armsAndLegs2.setVisible(! step);
    }

Her er komplett Java- og FXML-kode:

package trinn2;

import java.io.IOException;
import javafx.application.Application;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.shape.Shape;
import javafx.stage.Stage;

public class StickMan2 extends Application {
    @Override
    public void start(Stage primaryStage) throws IOException {
        FXMLLoader fxmlLoader = new FXMLLoader();
        fxmlLoader.setController(this);
        Parent root = (Parent) fxmlLoader.load(this.getClass().getResourceAsStream("StickMan2.fxml"));
        primaryStage.setScene(new Scene(root));
        primaryStage.show();
    }
    @FXML Node stickMan, armsAndLegs1, armsAndLegs2;
    @FXML Shape eye;
    
    boolean step = true;
    @FXML
    void initialize() {
        update();
    }
    void update() {
        if (step) {
            eye.setFill(Color.BLUE);
        } else {
            eye.setFill(Color.WHITE);
        }
        armsAndLegs1.setVisible(step);
        armsAndLegs2.setVisible(! step);
    }
    @FXML
    void walk() {
        step = ! step;
        stickMan.setLayoutX(stickMan.getLayoutX() + 5);
        update();
    }
    public static void main(String[] args) {
        launch(args);
    }
}
<?xml version="1.0" encoding="UTF-8"?>
 
<?import javafx.scene.control.Button?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.Pane?>

<?import javafx.scene.Group?>
<?import javafx.scene.shape.Circle?>
<?import javafx.scene.shape.Line?>

<BorderPane xmlns:fx="http://javafx.com/fxml"
     minWidth="400">
    <top>
        <Button text="Walk!" onAction="#walk"/>
    </top>
    <center>
        <Pane minWidth="400" minHeight="600">
            <Group fx:id="stickMan">
                <Circle layoutX="100" layoutY="20" radius="20" stroke="black" fill="white"/>
                <Circle fx:id="eye" layoutX="112" layoutY="20" radius="2" stroke="black" fill="blue"/>
                <Line layoutX="120" layoutY="20" startX="0" startY="-4" endX="3" endY="0" stroke="black"/>
                <Line layoutX="120" layoutY="20" startX="3" startY="0" endX="0" endY="1" stroke="black"/>
                <Line layoutX="100" layoutY="40" endX="0" endY="30" stroke="black"/>
                <Group fx:id="armsAndLegs1" layoutX="100" layoutY="50" visible="true">
                    <Line layoutY="0"  endX= "-5" endY="10" stroke="black"/>
                    <Line layoutY="0"  endX= "10" endY="10" stroke="black"/>
                    <Line layoutY="20" endX="-10" endY="15" stroke="black"/>
                    <Line layoutY="20" endX=  "5" endY="15" stroke="black"/>
                </Group>
                <Group fx:id="armsAndLegs2" layoutX="100" layoutY="50" visible="false">
                    <Line layoutY="0"  endX="-10" endY="10" stroke="black"/>
                    <Line layoutY="0"  endX=  "5" endY="10" stroke="black"/>
                    <Line layoutY="20" endX= "-5" endY="15" stroke="black"/>
                    <Line layoutY="20" endX= "10" endY="15" stroke="black"/>
                </Group>
            </Group>
        </Pane>
    </center>
</BorderPane>

StickMan3

Dette er en annen variant som bruker de samme teknikkene, og lar brukeren bruke piltastene for å flytte strekmannen. Her er komplett Java- og FXML-kode:

package trinn2;

import java.io.IOException;
import javafx.application.Application;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Shape;
import javafx.stage.Stage;

public class StickMan3 extends Application {
    @Override
    public void start(Stage primaryStage) throws IOException {
        FXMLLoader fxmlLoader = new FXMLLoader();
        fxmlLoader.setController(this);
        Parent root = (Parent) fxmlLoader.load(this.getClass().getResourceAsStream("StickMan3.fxml"));
        primaryStage.setScene(new Scene(root));
        primaryStage.show();
    }
    @FXML Pane stickManPane;
    @FXML Node stickMan, armsAndLegs1, armsAndLegs2;
    @FXML Shape leftEye, rightEye;
    
    boolean step = true;
    KeyCode direction = KeyCode.DOWN;
    
    @FXML
    void initialize() {
        update();
    }
    void update() {
        if (direction == KeyCode.LEFT) {
            leftEye.setVisible(true);
            rightEye.setVisible(false);
        } else if (direction == KeyCode.RIGHT) {
            leftEye.setVisible(false);
            rightEye.setVisible(true);
        } else {
            leftEye.setVisible(true);
            rightEye.setVisible(true);
        }
        if (step) {
            leftEye.setFill(Color.BLUE);
            rightEye.setFill(Color.BLUE);
        } else {
            leftEye.setFill(Color.WHITE);
            rightEye.setFill(Color.WHITE);
        }
        armsAndLegs1.setVisible(step);
        armsAndLegs2.setVisible(! step);        
    }
    
    @FXML
    void keyPressed(KeyEvent keyEvent) {
        step = ! step;
        if (keyEvent.getCode() == KeyCode.LEFT) {
            stickMan.setLayoutX(stickMan.getLayoutX() - 5);
        } else if (keyEvent.getCode() == KeyCode.RIGHT) {
            stickMan.setLayoutX(stickMan.getLayoutX() + 5);
        } else if (keyEvent.getCode() == KeyCode.DOWN) {
            stickMan.setLayoutY(stickMan.getLayoutY() + 5);
        } else if (keyEvent.getCode() == KeyCode.UP) {
            stickMan.setLayoutY(stickMan.getLayoutY() - 5);
        } else {
            return;
        }
        direction = keyEvent.getCode();
        update();
    }
    public static void main(String[] args) {
        launch(args);
    }
}
<?xml version="1.0" encoding="UTF-8"?>
 
<?import javafx.scene.layout.Pane?>

<?import javafx.scene.Group?>
<?import javafx.scene.shape.Circle?>
<?import javafx.scene.shape.Line?>

<Pane xmlns:fx="http://javafx.com/fxml" minWidth="400">
    <Pane fx:id="stickManPane" minWidth="400" minHeight="600" focusTraversable="true" onKeyPressed="#keyPressed">
        <Group fx:id="stickMan">
            <Circle layoutX="100" layoutY="20" radius="20" stroke="black" fill="white"/>
            <Circle fx:id="leftEye" layoutX="92" layoutY="20" radius="2" stroke="black" fill="blue"/>
            <Circle fx:id="rightEye" layoutX="110" layoutY="20" radius="2" stroke="black" fill="blue"/>
            <Line layoutX="100" layoutY="40" endX="0" endY="30" stroke="black"/>
            <Group fx:id="armsAndLegs1" layoutX="100" layoutY="50" visible="true">
                <Line layoutY="0"  endX= "-5" endY="10" stroke="black"/>
                <Line layoutY="0"  endX= "10" endY="10" stroke="black"/>
                <Line layoutY="20" endX="-10" endY="15" stroke="black"/>
                <Line layoutY="20" endX=  "5" endY="15" stroke="black"/>
            </Group>
            <Group fx:id="armsAndLegs2" layoutX="100" layoutY="50" visible="false">
                <Line layoutY="0"  endX="-10" endY="10" stroke="black"/>
                <Line layoutY="0"  endX=  "5" endY="10" stroke="black"/>
                <Line layoutY="20" endX= "-5" endY="15" stroke="black"/>
                <Line layoutY="20" endX= "10" endY="15" stroke="black"/>
            </Group>
        </Group>
    </Pane>
</Pane>
  • No labels