Form studente con JavaFX

Vediamo un nuovo esempio dopo quello iniziale (vedi qui) e cerchiamo di prendere confidenza con SceneBuilder e tutte le attività encessarie a realizzare il nostro software. Questa volta vogliamo far interagire una delle classi come le abbiamo realizzatee negli esempi precedenti (es qui), con tutto il sisetma grafico. Ancora non siamo di fronte ad un esempio completo e funzionante in modo realistio, ma otteremo un risultato interessante. Sotto riportiamo l’esempio che vogliamo realizzare.

Il nuovo progetto

Partiamo sempre da un nuovo progetto con IntelliJ. Poichè stiamo diventando via via più professionali, no naccontentiamoci dei nomi standard delle clasi Hello* ma procediamo a selezionarle e col tasto destro “Refactor->Rename”, forniamogli un nome più coerente. Ad esempio StudenteController, StudenteApplication e studente.fxml. Possiamo lasciare anche semplicemente Application e Controller ma in alcuni ambienti potrebbe dare errore di conflitto nomi. Creiamoci la classe Studente con attributi corrispondenti a quelli che vogliamo disegnare nella nostra finestra. Metodi set/get e costruttori standard per questo nostro esercizio andranno benissimo. Sempre con Generate, creiamo anche il metodo toString() che ci servirà per stampare i valori dell’oggetto nello spazio bianco della nostra form. Creiamo anche due enum Provincia e Sesso, o inserendoli via codice dentro la classe studente o nell’albero del progetto. Nel nostro caso vogliamo ottenere un albero di progetto simile a quello in figura qui sotto.

package com.itt.studentefx;

public class Studente {
    private String nome;
    private String cognome;
    private int anno;
    private Sesso genere;
    private Provincia citta;


    public Studente(String nome, String cognome, int anno, Sesso genere, Provincia citta) {
        this.nome = nome;
        this.cognome = cognome;
        this.anno = anno;
        this.genere = genere;
        this.citta = citta;
    }

    public Sesso getGenere() {
        return genere;
    }

    public void setGenere(Sesso genere) {
        this.genere = genere;
    }

    public Provincia getCitta() {
        return citta;
    }

    public void setCitta(Provincia citta) {
        this.citta = citta;
    }

    public String getNome() {
        return nome;
    }

    public void setNome(String nome) {
        this.nome = nome;
    }

    public String getCognome() {
        return cognome;
    }

    public void setCognome(String cognome) {
        this.cognome = cognome;
    }

    public int getAnno() {
        return anno;
    }

    public void setAnno(int anno) {
        this.anno = anno;
    }

    @Override
    public String toString() {
        return "Studente{" +
                "nome='" + nome + '\'' +
                ", cognome='" + cognome + '\'' +
                ", anno=" + anno +
                ", genere=" + genere +
                ", citta=" + citta +
                '}';
    }
}

Usiamo Scene Builder

Se abbiamo salvato il lavoro fatto in SceneBuilder, il file FXML del nostro progetto si autoaggiorna. Possiamo verificare la coerenza della versione andando a dare uno sguardo al codice e individuando nomi e widget vari che abbiamo creato precedentemente nello Scene Builder. Tolto questo, l’unica criticità di questo file potrebbe essere la mancanza del riferimento alla classe controller. Qui nel nostro file è la porzione presente nel tag Pane -> fx:controller=”com.example.helloworldfx.HelloController”. Cosa fare se non compare fx:controller? Abbiamo due alternatove: o apriamo ScenBuilder, ci posizioniamo col mouse sul Pane principale (o equivalente) e nel menù di sinistra alla voce Semplicemente digitiamo fx, in automatico scegliamo il suggeromento controller e nelle virgolette cominciamo a scrivere controller finchè non arriva il suggerimento giusto completo col nome della nostra classe.

Per quanto riguarda i vari nodi scelti, non c’è nulla di sorprendente. Come base siamo partiti da un pannello “preconfezionato” come HBox. Al suo interno abbiamo posizionato una Pane standard a mano libera e una TextArea che verranno messe in automatico una di fianco all’altra orizzontali. Un piccolo elemento interessante è quello di ancorare l’area di stampa alla finestra principale in modo che si ridimensioni con questa. Nel tab Layout possiamo sperimentare un po’ con le varie opzioni di dimensioni tra MAX_VALUE e PREFERRED_SIZE ma l’opzione fondamentale è HGrow impostata ad ALWAYS poichè questo parametro è vincolato al pannello HBox (HBox Constraints).

Gli altri elementi non presentano particolari anomalie. Prendete confidenza con il Pane e la disposizione a mano libera. Ricordiamo sempre di scegliere a destra nel tab Code un nome/id nel campo fx:id per gli oggetti grafici che devono interagire col codice. Qui conviene sempre inserire un prefisso come btb per i bottoni o txt per i campi di input in modo da distinguerli con i ccampi delle rispettive classi.

Posizioniamo infine i due bottoni in basso a destra e ricordiamoci di inserire non solo fx:id ma anche un nome nel campo On Action

Riportiamo anche il file fxml, anche se in questo caso no ndobbiamo eseguire particolari attività. Ricordiamoci di salvare sempre dallo scenebuilder per aggiornare in automatico questo file

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

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ChoiceBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.RadioButton?>
<?import javafx.scene.control.TextArea?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Pane?>

<HBox maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/19" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.itt.studentefx.Controller">
   <children>
      <Pane prefHeight="400.0" prefWidth="332.0">
         <children>
            <Label layoutX="15.0" layoutY="23.0" prefHeight="20.0" prefWidth="90.0" text="Nome" />
            <TextField fx:id="txtNome" layoutX="108.0" layoutY="20.0" />
            <Label layoutX="15.0" layoutY="59.0" prefHeight="20.0" prefWidth="85.0" text="Cognome" />
            <TextField fx:id="txtCognome" layoutX="108.0" layoutY="56.0" />
            <Label layoutX="15.0" layoutY="95.0" prefHeight="20.0" prefWidth="90.0" text="Anno" />
            <TextField fx:id="txtAnno" layoutX="108.0" layoutY="92.0" prefHeight="26.0" prefWidth="161.0" />
            <Button fx:id="btbInserisci" layoutX="172.0" layoutY="360.0" mnemonicParsing="false" onAction="#inserisci" text="Inserisci" />
            <Button fx:id="btbSbianca" layoutX="252.0" layoutY="360.0" mnemonicParsing="false" onAction="#sbianca" text="Sbianca" />
            <Label layoutX="15.0" layoutY="129.0" text="Sesso" />
            <RadioButton fx:id="radioM" layoutX="108.0" layoutY="129.0" mnemonicParsing="false" text="M" />
            <RadioButton fx:id="radioF" layoutX="157.0" layoutY="129.0" mnemonicParsing="false" text="F" />
            <ChoiceBox fx:id="listaCitta" layoutX="105.0" layoutY="165.0" prefWidth="150.0" />
            <Label layoutX="14.0" layoutY="169.0" text="Città" />
         </children>
      </Pane>
      <TextArea fx:id="stampa" editable="false" maxWidth="1.7976931348623157E308" prefHeight="400.0" prefWidth="277.0" HBox.hgrow="ALWAYS" />
   </children>
</HBox>

La classe Controller

Una volta terminato il design, dato quindi il nome a tutti i componenti, possiamo passare a creare il controller con tutte le attività che deve effettuare. Abbiamo un esempio già predisposoto come sempre andando a cliccare tr i menù di SceneBuilder alla voce View->Show Sample Controller Skeleton. Se abbiamo inserito già il controller nel pannello alla voce Controller, avremo già codice coerente col nostro progetto e ci basterà sostituirlo al controller di esempio creato dal progetto.

Procediamo quindi a personalizzarlo. La prima cosa da fare è valorizzare la tendina Città. Creiamoci una New JavaClass->Enum posizionanci nell’albero di sisnistra del progetto. Nell’enum inseriamo i nomi delle province, ad esempio solo abruzzesi. Il nome che scegliamo per l’enum, posizioniamolo nella voce del ChoiceBox, qui a ? abbiamo quindi sostituito Provincia.

private ChoiceBox<Provincia> listaCitta;

Seconda attività da fare è inserire i bottoni radio M/F in un gruppo detto ToggleGroup. Questo permetetrà di creare l’effetto seleziona uno/deseleziona gli altri automatico, classico dei radio button. Posizioniamoci nel metodo initialize e digitiamo in fondo un codice tipo questo che segue

@FXML
void initialize() {        
...
ToggleGroup radioGenere= new ToggleGroup();
radioF.setToggleGroup(radioGenere);
radioM.setToggleGroup(radioGenere);

Sempre nello stesso spazio, andiamo ad inizializzare la tendina per inserirci i valori dell’enum creato. Su cosa sia un observableArrayList ci torneremo più avanti per comprendere alcuni concetti più raffinati.

@FXML
void initialize() {        
...
listaCitta.setItems(FXCollections.observableArrayList( Provincia.values()));

Non ci rimangono che crere i metodi dietro le azioni dei due bottoni inserisci e sbianca. Sbianca è molto semplice: resetta e svuota i campi della form, magari inseriti erroneamente.

    @FXML
    void sbianca(ActionEvent event) {
        this.txtNome.setText("");
        this.txtCognome.setText("");
        this.txtAnno.setText("");
        this.listaCitta.setValue(null);
        this.radioF.setSelected(false);
        this.radioM.setSelected(false);
    }

Il metodo inserisci vale la pena commentarlo un po’. Vediamo il codice.

    @FXML
    void inserisci(ActionEvent event) {
        String n = txtNome.getText();
        String c = txtCognome.getText();
        
        int a;
        try { a = Integer.valueOf(txtAnno.getText());}
        catch(Exception e){a = 0; }
        
        Sesso g = Sesso.M; 
        if(radioF.isSelected())
            g = Sesso.F;
         
        Provincia p = listaCitta.getValue();

        Studente s = new Studente(n,c,a,g,p);
        stampa.appendText(stampa.getText() + s.toString()+ "\n");

    }

Prima cosa da fare nel metodo è prelevare i valori dai campi della form. Se nome e cognome possiamo inserirli facilmente in altrettante variabili String, non è prorpio così per gli altri campi.

Il campo anno è infatti un numero e come tale va trattato per creare un variabile intera. Si deve effettuare una conversione da string a int, ma ci viene incontro una funzione statica del tipo Integer ovvero valueOf della stringa. Basterebbe già questo se non altro che se malaguratamente non viene inserito il numero dell’anno, si genera una fastidiosa eccezione che vale la pena bypassare imemrgendio il nostrometodo in un try/catch generico.

Per la gesione dell’enum, siamo obbligati a valorizzare l’enum con un valore di default per poi controllare se sia selezionato il valore femminile del radio button con il metodo isSelected dedicato. Per la provincia invece è tutto più semplice, basta prelevare il valore

Il listato completo del nostro controlelr è qundi il seguente.

package com.itt.studentefx;

import java.net.URL;
import java.util.ResourceBundle;

import javafx.collections.FXCollections;
import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.fxml.FXML;
import javafx.scene.control.*;

public class Controller {

    @FXML
    private ResourceBundle resources;

    @FXML
    private URL location;

    @FXML
    private Button btbInserisci;

    @FXML
    private Button btbSbianca;

    @FXML
    private ChoiceBox<Provincia> listaCitta;

    @FXML
    private RadioButton radioF;

    @FXML
    private RadioButton radioM;

    @FXML
    private TextField txtAnno;

    @FXML
    private TextField txtCognome;

    @FXML
    private TextField txtNome;

    @FXML
    private TextArea stampa;
    @FXML
    void inserisci(ActionEvent event) {
        String n = txtNome.getText();
        String c = txtCognome.getText();

        int a;
        try { a = Integer.valueOf(txtAnno.getText());}
        catch(Exception e){a = 0; }

        Sesso g = Sesso.M;
        if(radioF.isSelected())
            g = Sesso.F;
         Provincia p = listaCitta.getValue();

        Studente s = new Studente(n,c,a,g,p);
        stampa.appendText(stampa.getText() + s.toString()+ "\n");

    }

    @FXML
    void sbianca(ActionEvent event) {
        this.txtNome.setText("");
        this.txtCognome.setText("");
        this.txtAnno.setText("");
        this.listaCitta.setValue(null);
        this.radioF.setSelected(false);
        this.radioM.setSelected(false);
    }

    @FXML
    void initialize() {
        assert btbInserisci != null : "fx:id=\"btbInserisci\" was not injected: check your FXML file 'studente.fxml'.";
        assert btbSbianca != null : "fx:id=\"btbSbianca\" was not injected: check your FXML file 'studente.fxml'.";
        assert listaCitta != null : "fx:id=\"listaCitta\" was not injected: check your FXML file 'studente.fxml'.";
        assert radioF != null : "fx:id=\"radioF\" was not injected: check your FXML file 'studente.fxml'.";
        assert radioM != null : "fx:id=\"radioM\" was not injected: check your FXML file 'studente.fxml'.";
        assert stampa != null : "fx:id=\"stampa\" was not injected: check your FXML file 'studente.fxml'.";
        assert txtAnno != null : "fx:id=\"txtAnno\" was not injected: check your FXML file 'studente.fxml'.";
        assert txtCognome != null : "fx:id=\"txtCognome\" was not injected: check your FXML file 'studente.fxml'.";
        assert txtNome != null : "fx:id=\"txtNome\" was not injected: check your FXML file 'studente.fxml'.";

        ToggleGroup radioGenere= new ToggleGroup();
        radioF.setToggleGroup(radioGenere);
        radioM.setToggleGroup(radioGenere);

        listaCitta.setItems(FXCollections.observableArrayList( Provincia.values()));

    }

}

La classe Application

Alla classe application non dedichiamo al momento particolari attenzioni. Vale la pena modificare però il valore della dimensione della Scena, qui ad esempio mettiamo 600×400 e il titolo “Esempio JavaFX!” Il resto lasciamo come si trova.

package com.itt.studentefx;

import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.io.IOException;

public class Application extends javafx.application.Application {
    @Override
    public void start(Stage stage) throws IOException {
        FXMLLoader fxmlLoader = new FXMLLoader(Application.class.getResource("studente.fxml"));
        Scene scene = new Scene(fxmlLoader.load(), 600, 400);
        stage.setTitle("Esempio JavaFX!");
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch();
    }
}

Ultima modifica 26 Maggio 2023