Test IMC in JavaFX

Vogliamo realizzare un piccolo software di utilità che calcola l’Indice di Massa Corporea (IMC). La formuletta è molto semplice ma è il pretesto per fare una piccola applicazione di utilità realistica. Il software si compone di una piccola form per l’inserimento dei dati necessari, viene creata una classe IMC e viene quindi fatta la visualizzazione del calcolo sia sotto forma di testo, che con una immagine colorata di blu, verde, giallo, rosso a seconda della fascia numerica del calcolo stesso per indicare sottopeso, peso forma, sovrappeso, obesità.

Test IMC in JavaFX

Il progetto

Consiglio sempre di effettuare il refactor dei file delle classi e del file FXML

Creiamo a mano nella cartella nella sottocartella resources del progetto complessivo appena creato, una cartella img e inseriamo 4 immagini esplicative per i diversi stati del nostro calcolo. Nel nostro progetto 4 pollici in su/giù con altrettanti colori di sfondo.

SceneBuilder

In SceneBuilder possiamo realizzare il nostro display in molti modi. Suggerisco di cominciare da un Pane vuoto portato a 600×400, quindi inseriamo una VBox con due elementi: una barra menu e un altro Pane vuoto per disporre a mano libera. Cercando gli allineamenti migliori, inseriamo i textfield con le loro etichette, il choiceox con etichetta, un ImageView con sotto una label e gli ultimi due bottoni per lanciare il calcolo o il reset. Una idea di realizzazione qui in basso con l’albero dei vari nodi/elementi UI.

MI RACCOMANDO: ad ogni oggetto che deve interagire col codice dobbiamo assegnare un fx:id dalla tendina di destra Code. Stessa cosa, nei bottoni, menù o ovunque c’è un click bisogna aggiungere l’evento sempre nella tendina code. Qui abbiamo aggiunto un onAction per i bottoni e i tre menù presenti in alto nella barra. Per simulare una validazione dei dati inseriti nei campi, abbiamo annche aggiunto una funzione sull’evento On Key Typed per resettare eventuali segnalzione di errore di digitazione.

Una volta che tutti gli id ed eventi validi sono stati assegnati, andiamo nel menu View->Show Sample Controller Skeleton. Copiamo tutto nella nostra classe Controller sovrascrivendo il testo preesistente.

SceneBuilder in azione

Application

Qui facciamo poco. Assi curiamoci di inserire il nome del file fxml che vogliamo rinominare , impostare la dimensione della scena a piacimento, ad esempio 600 x 400px, il titolo magari.

package com.centinaro.convertitoreimc;

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

import java.io.IOException;

public class IMCApplication extends Application {
    @Override
    public void start(Stage stage) throws IOException {
        FXMLLoader fxmlLoader = new FXMLLoader(IMCApplication.class.getResource("IMC-view.fxml"));
        Scene scene = new Scene(fxmlLoader.load(), 600, 400);
        stage.setTitle("IMC Test!");
        stage.setScene(scene);
        stage.show();
    }

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

La classe IMC

Produciamo prima due enum che ci occorrono per gestire in modo più semplice il sesso e lo stato/risposta del calcolo. Per creare un enum possiamo semplicemente usare nell’albero di progetto il tasto destro per creare una nuova classe->enum. Per Sesso inseriamo M, S, mentre per Stato inseriamo Sottopeso, Normopeso, Sovrappeso, Obeso.

package com.centinaro.convertitoreimc;

public class IMC
{
    private double peso;
    private double altezza;
    private Sesso sesso;

    public IMC( double altezza, double peso,Sesso sesso) {
        this.peso = peso;
        this.altezza = altezza;
        this.sesso = sesso;
    }

    public double getPeso() {
        return peso;
    }

    public void setPeso(double peso) {
        this.peso = peso;
    }

    public double getAltezza() {
        return altezza;
    }

    public void setAltezza(double altezza) {
        this.altezza = altezza;
    }

    public Sesso getSesso() {
        return sesso;
    }

    public void setSesso(Sesso sesso) {
        this.sesso = sesso;
    }

    /**
     * Formula: Peso (kg) / ( Altezza (m) * Altezza (m) )
     */
    double calcolaIMC()
    {
        double imc = 0;

        imc = this.peso / (this.altezza * this.altezza);

        return imc;
    }

    /**
     * sottopeso 	<18,5
     * normopeso 	18,5-24,9
     * sovrappeso 	25-29,9
     * obesità 	>30
     */

    Stato statoIMC()
    {
        double imc = this.calcolaIMC();
        Stato  stato = Stato.Sottopeso;

        //if (this.sesso == Sesso.M){} cerca la formula uomo/donna

        if (imc < 18.5)
        {
            stato = Stato.Sottopeso;
        }

        if (imc >= 18.5 && imc <= 24.9)
        {
            stato =  Stato.Normopeso;
        }

        if (imc >= 25 && imc <= 29.9)
        {
            stato =  Stato.Sovrappeso;
        }

        if (imc >29.9) {
            stato =  Stato.Obeso;
        }

        return stato;

    }
}

Controller

Una volta disegnato con SceneBuilder il nostro design, passiamo al codice copiato col tasto View->Show Sample Controller Skeleton.

La ChoiceBox

Prima cosa da fare: inizializziamo la tendina di scelta del Sesso. Dobbiamo fare due cose: nel campo di dichiarazione:

    @FXML     private ChoiceBox<?> chcSesso;  

Sostituiamo il punto interrogativo con l’enum che abbiamo creao prima ovvero

    @FXML
    private ChoiceBox<Sesso> chcSesso;

Sempre per il nostro choice, impostamo un frammento di codice che inizilizza la tendina con i valori reali dell’enum.

void initialize() {
     
    //....

    this.chcSesso.setItems(FXCollections.observableArrayList(Sesso.values()));

}

Passiamo ai metodi associati ai singoli eventi. Tralasciamo il chiudi e cancella che sono intuitivi, concentriamoci sul bottone calcola.

Qui abbiamo due livelli di difficoltà di implementazione. Nella versione più semplice preleviamo i valori dai singoli campi, convertiamo con Double.valueOf i valori di testo in numeri e costruimo un oggetto IMC. Questo ci permette di effettuare i calcoli sia per trovare il valore numerico del IMC sia capire in che range ricade. Il valore numerico lo scriviamo banalmente con un messaggio di accompagnamento nella etichettina a destra sotto l’ImageView.

La gestione delle immagini

Prepariamo invece una serie di if di selezione per stampare l’immagine associata alla situazione sottopeso, pesoforma…. Dentro ogni if dobbiamo fare tre operazioni aprire il file che contiene l’immagine, piazzare l’immagine in una classe di gestione opportuna, inserire l’immagine nell’ImageView. Il percorso dell’immagine lo possiamo ottenere cliccndo col tasto destro sull’immagine stessa nell’albero di sinistra e prelevando la voce: “Path from Content Root“. Procedura un po’ farragginosa. Probabilmete il codice vi darà errore e il suggerimento vi richiederà o di inserire un throw dopo il nome del metodo o un blocco try/catch.

      void calcola(ActionEvent event) throws FileNotFoundException

[]

        if (stato == Stato.Sovrappeso)
        {
            FileInputStream input = new FileInputStream("src/main/resources/img/pollice-giallo.png");
            Image image = new Image(input);
            this.immagine.setImage(image);
        }

Validare gli input

Più complicato è invece il fattore validazione. Nei nostrio esercizi più semplici non teniamo mai conto del fatto che l’utente potrebbe inserire un valore sbagliato numerico per testo o viceversa ad edempio, o dimenticare proprio di inserire un valore nelle form. Dovremmo prevdere un sistema che impedisca ai calcol idi andare in errore e generare problemi col software. Qui abbiamo deciso di implementare un controllo quando premiamo il bottone Calcola che richiama un controllo sui campi atezza e peso. Se il valore è vuoto o contiene una lettera alfabetica, il campo si colora di rosso con bordo rosso. isEmpty() è intuitivo matches() invece prevede una RegEx, una espressione regolare come filtro da controlalre. Vi rimandiamo ad altri articoli per megli ocomprendere quest’ultimo argomento

Interessante è la possibilità di cambiare stile agli oggetti JavaFX, proprio come un fogli odi stile CSS con classi su misura da impostare puntualmente o caricando un vero CSS. Il controllo di validità viene alternato con la funzione valida e reset che permettono di resettare i valori delle caselle e ricontrollare lo stato al nuovo click di Controlla, Cancella o quando si posiziona il mouse nella casella e si digita una carattere da tastiera con la funzione validaPeso(KeyEvent event) e validaAltezza(KeyEvent event) resettando lo stile di default. La parte dei controlli non è complessa ma richiede sempre una certo ordine nel controllorare i valori nel modo e tempistiche giuste. Fa la differenza però tra un software che funziona ed un odi qualità.

Il metodo valida poi raccoglie tutti i controlli che vogliamo fare: campo vuoto o, se numerico, con lettere. Ovviamente si può complicare e raffinare ulteriormente con regex più complesse.

        if(txtAltezza.getText().isEmpty() || txtAltezza.getText().matches(".*[a-zA-Z].*"))
        {
            txtAltezza.setStyle("-fx-background-color:#9c2b2e; -fx-border-color:#e84e4f");
            errore = false;
        }

Aprire un’altra finestra/scena

Ultimo elemento interessante è il popoup per le informazioni sul software. Qui Il pannello è banale ma ci apre spazi infiniti per personalizzare i nostri software dove vengon oaperte più finestre in modo analogo. Coem fare ad aprire quindi una finestra oltre quella iniziale? Copiamo il codice della classe Application. Abbiamo bisogno di un nuovo file FXML da personalizzare ed aggiungere al progetto, una Scene ed uno Stage. Più facile di quell oche si possa pensare! Qui non abbiamo inserito un Controller per il nuovo FXML/Scene per semplicità anche della stessa finestrella ma lasciamo al lettore il modo intuitivo di farlo.

    @FXML
    void popInfo(ActionEvent event) throws IOException {

        //ObservableList<Window> windows = Window.getWindows();

        FXMLLoader fxmlLoader = new FXMLLoader(IMCApplication.class.getResource("PannelloInfo.fxml"));
        Scene scene = new Scene(fxmlLoader.load(), 500, 200);
        Stage stage = new Stage();
        stage.setScene(scene);
        stage.show();
    }

Il listato completo

package com.centinaro.testimc;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import java.util.ResourceBundle;

import javafx.collections.FXCollections;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyEvent;
import javafx.stage.Stage;

public class IMCController {

    @FXML
    private ResourceBundle resources;

    @FXML
    private URL location;

    @FXML
    private Button btnCalcola;

    @FXML
    private Button btnCancella;

    @FXML
    private ChoiceBox<Sesso> chcSesso;

    @FXML
    private MenuItem cancellaMenu;

    @FXML
    private MenuItem chiudi;

    @FXML
    private ImageView immagine;

    @FXML
    private MenuItem info;
    @FXML
    private TitledPane pannello;

    @FXML
    private Label risultato;

    @FXML
    private TextField txtAltezza;

    @FXML
    private TextField txtPeso;

    @FXML
    void chiudi(ActionEvent event) {
        System.exit(0);
    }

    @FXML
    void popInfo(ActionEvent event) throws IOException {

        //ObservableList<Window> windows = Window.getWindows();

        FXMLLoader fxmlLoader = new FXMLLoader(IMCApplication.class.getResource("PannelloInfo.fxml"));
        Scene scene = new Scene(fxmlLoader.load(), 500, 200);
        Stage stage = new Stage();
        stage.setScene(scene);
        stage.show();
    }

    @FXML
    void calcola(ActionEvent event) throws FileNotFoundException
    {

        if (!this.valida())
            return;

        double altezza = Double.valueOf(this.txtAltezza.getText());
        double peso = Double.valueOf(this.txtPeso.getText());
        Sesso s = this.chcSesso.getValue();


        IMC  imc = new IMC(altezza, peso, s);
        Stato stato = imc.statoIMC();
        if (stato == Stato.Sottopeso)
        {
            FileInputStream input = new FileInputStream("src/main/resources/img/pollice-blu.png");
            Image image = new Image(input);
            this.immagine.setImage(image);
        }


        if (stato == Stato.Normopeso)
        {
            FileInputStream input = new FileInputStream("src/main/resources/img/pollice-verde.png");
            Image image = new Image(input);
            this.immagine.setImage(image);
        }

        if (stato == Stato.Sovrappeso)
        {
            FileInputStream input = new FileInputStream("src/main/resources/img/pollice-giallo.png");
            Image image = new Image(input);
            this.immagine.setImage(image);
        }

        if (stato == Stato.Obeso)
        {
            FileInputStream input = new FileInputStream("src/main/resources/img/pollice-verde.png");
            Image image = new Image(input);
            this.immagine.setImage(image);
        }

        this.risultato.setText("Il tuo IMC è: " + String.valueOf(Math.floor(imc.calcolaIMC())));
    }

    @FXML
    void cancella(ActionEvent event) {
        this.txtPeso.setText("");
        this.txtAltezza.setText("");
        this.chcSesso.getValue();

        this.reset();
    }

    @FXML
    void cambia(KeyEvent event) { this.reset(); this.valida();}

    @FXML
    void validaAltezza(KeyEvent event) {
        txtAltezza.setStyle(null);
    }

    @FXML
    void validaPeso(KeyEvent event) {
        txtPeso.setStyle(null);
    }



    void reset()
    {
        txtAltezza.setStyle(null);
        txtPeso.setStyle(null);
    }

    boolean valida()
    {
        boolean errore = true;

        if(txtAltezza.getText().isEmpty() || txtAltezza.getText().matches(".*[a-zA-Z].*"))
        {
            txtAltezza.setStyle("-fx-background-color:#9c2b2e; -fx-border-color:#e84e4f");
            errore = false;
        }

        if(txtPeso.getText().isEmpty() || txtPeso.getText().matches(".*[a-zA-Z].*"))
        {
            txtPeso.setStyle("-fx-background-color:#9c2b2e; -fx-border-color:#e84e4f");
            errore = false;
        }

        return errore;
    }

    @FXML
    void initialize() {
        assert btnCalcola != null : "fx:id=\"btnCalcola\" was not injected: check your FXML file 'IMC-view.fxml'.";
        assert btnCancella != null : "fx:id=\"btnCancella\" was not injected: check your FXML file 'IMC-view.fxml'.";
        assert cancellaMenu != null : "fx:id=\"cancellaMenu\" was not injected: check your FXML file 'IMC-view.fxml'.";
        assert chcSesso != null : "fx:id=\"chcSesso\" was not injected: check your FXML file 'IMC-view.fxml'.";
        assert chiudi != null : "fx:id=\"chiudi\" was not injected: check your FXML file 'IMC-view.fxml'.";
        assert immagine != null : "fx:id=\"immagine\" was not injected: check your FXML file 'IMC-view.fxml'.";
        assert info != null : "fx:id=\"info\" was not injected: check your FXML file 'IMC-view.fxml'.";
        assert risultato != null : "fx:id=\"risultato\" was not injected: check your FXML file 'IMC-view.fxml'.";
        assert txtAltezza != null : "fx:id=\"txtAltezza\" was not injected: check your FXML file 'IMC-view.fxml'.";
        assert txtPeso != null : "fx:id=\"txtPeso\" was not injected: check your FXML file 'IMC-view.fxml'.";


        this.chcSesso.setItems(FXCollections.observableArrayList(Sesso.values()));

    }

}

File di Progetto IntelliJ

Il codice completo su GitHub -> qui

Eseguibile JAR -> qui

Ultima modifica 9 Novembre 2023