Skip to content

10. [Compito]: Implementazione del livello [ui] utilizzando un'interfaccia Swing -

Parole chiave: architettura multistrato, Spring, iniezione di dipendenze, libreria di componenti Swing.

10.1. Assistenza

Nel livello [UI], vogliamo creare un'interfaccia grafica Swing. NetBeans dispone di uno strumento [Matisse] per la creazione di queste interfacce Swing che è superiore a quello offerto da Eclipse. Le interfacce Swing vengono sempre più sostituite da quelle JavaFX. NetBeans ed Eclipse utilizzano lo stesso strumento per creare queste ultime. Pertanto, se creiamo interfacce JavaFX, possiamo utilizzare Eclipse nell'intera architettura a livelli.

NetBeans può aprire qualsiasi progetto Maven. Utilizzeremo quindi il precedente progetto Maven e vi aggiungeremo un'interfaccia Swing. In [2], carichiamo (File / Apri progetto) i progetti Maven per i tre livelli che abbiamo creato con Eclipse. Quindi, creiamo i loro binari [3]. Le opzioni [Build] e [Clean and Build] creano il binario per il progetto a cui sono applicate. Questi binari vengono inseriti nella cartella [target] del progetto [4-5]:

L'opzione [Clean] elimina questa cartella [target]. L'opzione [Build] la ricompila. L'esperienza dimostra che quando sorgono problemi imprevisti, la prima cosa da fare è eseguire un [Clean and Build] sul progetto per assicurarsi di lavorare con l'ultima versione. Ciò è particolarmente necessario quando si hanno file di configurazione che, se modificati, non attivano una ricompilazione automatica all'esecuzione del progetto. È quindi necessario forzare questa ricompilazione con un [Clean and Build] in modo che le loro nuove versioni vengano installate nella cartella [target].

10.2. Come funziona l'applicazione

Torniamo all'architettura generale dell'applicazione [Elections]:

Ci stiamo ora concentrando su una nuova implementazione del livello [ui]. L'unica implementazione attualmente in uso è un'interfaccia da console. Stiamo ora creando un'interfaccia utente grafica.

L'utente disporrà della seguente interfaccia per interagire con l'applicazione [Elections]:

L'interfaccia utente grafica si trova nel livello [ui]. È questo livello che interagisce con l'utente.

  • All'avvio, l'applicazione console [main] istanzia i tre livelli dell'applicazione utilizzando Spring. Ciò avviene prima ancora che l'interfaccia grafica utente sia visibile. Inoltre, durante questa fase di inizializzazione, le informazioni che caratterizzano l’elezione (numero di seggi da assegnare, soglia elettorale, liste in competizione) vengono richieste al livello [dao]. Se questa fase di inizializzazione fallisce (ad es. impossibilità di accedere ai dati), viene visualizzato un messaggio di errore sulla console e l’interfaccia grafica utente non viene visualizzata.
  • Se i dati sono stati letti correttamente, l'interfaccia grafica viene visualizzata con le seguenti informazioni (vedi screenshot sopra):
    • il numero di seggi da assegnare (2)
    • la soglia elettorale di cui al punto (3)
    • i codici identificativi e i nomi delle liste dei candidati di cui al punto (4)
  • L'utente assegna quindi il numero di voti a ciascuna lista di candidati utilizzando i campi 4 (ID - Nome), 5 (Voti) e 6 (Aggiungi).

Image

  • È quindi possibile utilizzare il link (10) per calcolare i seggi:

Image

  • Il link [Salva] (12) consente di salvare i risultati nell'origine dati.

10.3. La classe [ElectionsSwing] che implementa il livello [ui]

10.3.1. Il progetto NetBeans

Nota: la Sezione 22.4 spiega come procurarsi NetBeans.

Il progetto NetBeans finale per l'applicazione avrà questo aspetto [1]. Compilatelo seguendo i passaggi [2–5]:

Assicurarsi che il progetto sia configurato per essere compilato con un JDK 1.8 [1-6]:

10.3.2. Configurazione Maven

Il nuovo progetto [elections-swing-business-dao-jdbc] si baserà sul progetto precedente [elections-console-business-dao-jdbc]. A tal fine, aggiungere una dipendenza Maven come segue [1-3]:

10.3.3. Creazione dell'interfaccia grafica

Per creare l'interfaccia grafica, possiamo procedere come segue:

  • [1]: Aggiungi un oggetto al pacchetto [elections.ui.service]
  • [2]: Selezionare l'opzione [JFrame Form] nella categoria [Swing GUI Forms]
  • [4]: Assegnare un nome alla classe
  • [5]: Il pacchetto della classe.
  • Completa la procedura guidata.
  • [6]: La classe generata
  • [7]: la classe [AbstractElectionsSwing] in modalità [Design]
  • [8]: la scheda [Navigator] che mostra l'albero [9] dei componenti della finestra
  • [10]: la scheda [Properties], che mostra le proprietà del componente [JFrame] selezionato in [9]
  • [11]: [JFrame] è un contenitore di componenti. I componenti possono essere disposti all'interno del contenitore secondo varie regole di posizionamento chiamate layout. In questo caso, scegliamo il layout [Free Design] [14], che consente di posizionare liberamente i componenti all'interno del contenitore.

Troviamo i componenti nella barra degli strumenti chiamata Palette:

  • [1]: la palette
  • [2]: un componente JLabel viene trascinato nel contenitore dei componenti
  • Facendo clic con il tasto destro del mouse su di esso, possiamo accedere a varie proprietà: il suo nome [4], il suo testo [3] o i suoi gestori di eventi [5]. Utilizziamo [3] per impostare il testo [6].
  • [1]: La scheda [Proprietà] del componente [JLabel] consente di accedere alle sue proprietà: la posizione orizzontale [2], la posizione verticale [3], il carattere del testo [4] e il testo [5].

Quando un componente viene trascinato e configurato sull'interfaccia grafica e si salva (Ctrl-S) il lavoro, viene generato del codice nella classe [AbstractElectionsSwing]:

 
 

Non modificare questo codice in grigio, poiché verrà eliminato e rigenerato al prossimo salvataggio. Qualsiasi modifica apportata andrebbe quindi persa.

Un tutorial sulla creazione di un'interfaccia utente grafica con NetBeans è disponibile all'indirizzo [https://netbeans.org/kb/docs/java/quickstart-gui.html?print=yes#design] (novembre 2015).

Ora creeremo la seguente interfaccia:

I componenti dell'interfaccia sono i seguenti:

N.
tipo
nome
ruolo
1
JMenuBar
jMenuBar1
un menu
2
JLabel
jLabelSAP
il numero di posti disponibili
3
JLabel
jLabelSE
soglia elettorale
4
JComboBox
jComboBoxListNames
elenco dei nomi delle liste in lizza
5
JTextField
jTextFieldVotesList
il numero di voti per una lista
6
JLabel
jLabelAdd
per aggiungere una lista a (8)
7,8
(JScrollPane, JList)
jListNamesVoices
i nomi e le voci delle liste
9
JLabel
jLabelDelete
per rimuovere da (8) l'elenco selezionato in (8)
10
JLabel
jLabelCalculate
per calcolare i risultati elettorali
11
JLabel
jLabelClear
per cancellare i risultati delle elezioni
12
JLabel
jLabelSave
per salvare i risultati delle elezioni
13,14
(JScrollPane, JList)
jListResults
per visualizzare i risultati elettorali
15,16
(JScrollPane,
JTextPane)
jTextPaneMessages
per visualizzare i messaggi di follow-up

L'annotazione (JScrollPane, JList) [13-14] serve a indicare che, quando un componente [JList] viene trascinato nella finestra, viene automaticamente inserito in un componente [JScrollPane] che consente di scorrere l'elenco. È il componente [JScrollPane] che consente di visualizzare tutti gli elementi dell'elenco, anche se in un dato momento ne è visibile solo un numero limitato. Lo stesso vale per il componente [JTextPane] [15-16].

Il menu può essere creato come segue:

  • [1, 2]: un componente [Barra dei menu] viene posizionato sulla finestra
  • [3]: il menu predefinito come mostrato nella scheda [Navigatore]
  • [4,5,6]: facendo clic con il tasto destro su un'opzione del menu, è possibile:
    • modificarne il testo [4], il nome [5]
    • gestirne gli eventi [6]
  • [7]: Il menu desiderato

Il menu desiderato è il seguente:

Livello 1
Livello 2
Elezioni
 
 
Uscita
Liste
 
 
Aggiungi
 
Elimina
Risultati
 
 
Calcola
 
Cancella
 
Salva
Informazioni
 

Puoi provare l'interfaccia grafica in qualsiasi momento:

 

Durante la creazione dell'interfaccia, è necessario associare un gestore di eventi a determinate etichette e menu [Aggiungi, Elimina, ...]. Ecco come procedere:

  • [1]: Fare clic con il tasto destro del mouse sul componente per il quale si desidera gestire un evento
  • [2]: Selezionare l'opzione [Eventi]
  • [3]: Seleziona una categoria di evento
  • [4]: Seleziona l'evento che desideri gestire

Il codice Java generato da questa operazione è il seguente:


    jLabelCalculer.addMouseListener(new java.awt.event.MouseAdapter() {
      public void mouseClicked(java.awt.event.MouseEvent evt) {
        jLabelCalculerMouseClicked(evt);
      }
    });
...
 
  private void jLabelCalculerMouseClicked(java.awt.event.MouseEvent evt) {
    // TODO add your handling code here:
}
  • Righe 1–5: viene aggiunto un gestore di eventi al componente jLabelCalculer. Il metodo addMouseListener richiede come parametro una classe che implementi la seguente interfaccia MouseListener:
 

L'interfaccia MouseListener è implementata da diverse classi, tra cui la classe MouseAdapter. Questa classe implementa i cinque metodi dell'interfaccia MouseListener, ma tali metodi non eseguono alcuna operazione. Pertanto, è necessario creare una sottoclasse di questa classe per implementare i metodi desiderati. Ciò è stato fatto nel codice sopra riportato e illustrato di seguito:


    jLabelCalculer.addMouseListener(new java.awt.event.MouseAdapter() {
      public void mouseClicked(java.awt.event.MouseEvent evt) {
        jLabelCalculerMouseClicked(evt);
      }
});

Il codice sopra riportato utilizza la tecnica delle classi anonime descritta nella Sezione 2.5 del corso [rif. 1].

Nella riga 1, il parametro del metodo addMouseListener è una classe anonima, definita al volo. Si tratta di un'istanza di una classe derivata dalla classe MouseAdapter (riga 1), il cui metodo mouseClicked viene sovrascritto (righe 2–4) in modo da eseguire un'azione specifica.

Il metodo jLabelCalculerMouseClicked chiamato alla riga 3 è definito come segue:


  private void jLabelCalculerMouseClicked(java.awt.event.MouseEvent evt) {
    // TODO add your handling code here:
}

Lo sviluppatore gestisce l'evento "MouseClicked" inserendo il codice in questo metodo.

Tutti i gestori di eventi vengono generati da NetBeans in questo modo. Lo sviluppatore può ignorare le righe di codice generate da NetBeans per associare un metodo all'evento di un componente. Può semplicemente inserire il proprio codice nella riga 2 sopra. Ecco un esempio:


  private void jLabelCalculerMouseClicked(java.awt.event.MouseEvent evt) {
    System.out.println("Mouse Clicked");
}

Se si avvia l'interfaccia grafica e si fa clic sul pulsante [Calcola], sulla console viene visualizzato un messaggio:

  • [1]: Fare doppio clic sull'etichetta [Calcola]
  • [2]: Il gestore di eventi è stato eseguito e ha generato i messaggi mouseClicked nella console di NetBeans.

I componenti [jComboBoxNomsListes, jListNomsVoix, jListResultats] sono dichiarati come segue:


protected javax.swing.JComboBox jComboBoxNomsListes;
protected javax.swing.JList jListNomsVoix;
protected javax.swing.JList jListResultats;

Questi componenti sono elenchi che normalmente sono configurati con un tipo T: il tipo degli elementi nel modello visualizzato dai componenti. Questo tipo T può essere qualsiasi tipo. Il valore visualizzato nel componente elenco è di tipo [String]. Per impostazione predefinita, per la visualizzazione viene utilizzato il metodo [T.toString()]. Per controllare meglio ciò che viene visualizzato, il tipo T sarà qui il tipo String. Pertanto, la dichiarazione corretta dei nostri elenchi è la seguente:


protected javax.swing.JComboBox<String> jComboBoxNomsListes;
protected javax.swing.JList<String> jListNomsVoix;
protected javax.swing.JList<String> jListResultats;

Otteniamo questo risultato modificando una delle proprietà del componente:

10.3.4. Separazione del codice

Torniamo alla struttura della nostra applicazione:

La classe [AbstractElectionsSwing] deve implementare il livello [ui] sopra indicato. Il suo codice, generato da NetBeans, attualmente contiene solo codice di gestione delle finestre e gestori di eventi che, a questo punto, non fanno nulla. Come visto in precedenza, la classe [AbstractElectionsSwing] dovrà gestire le interazioni con il livello [business]. Tale gestione avverrà all'interno dei gestori di eventi. Per chiarire la struttura del codice, abbiamo deciso di suddividerlo in due classi:

  • [AbstractElectionsSwing], che rimarrà come generata da NetBeans con alcune modifiche minori. Questa classe non gestirà direttamente alcun evento. I gestori di eventi saranno vuoti e dichiarati astratti. Saranno implementati da una classe derivata da [AbstractElectionsSwing].
  • [ElectionsSwing], una classe derivata da [AbstractElectionsSwing] che implementerà tutti i gestori di eventi.

Questo tipo di separazione non è insolito. Si può trovare, ad esempio, nelle pagine web ASP.NET (versione non MVC). Il progetto NetBeans si evolve come segue:

 

Il codice della classe [AbstractElectionsSwing] si evolve come segue:


public abstract class AbstractElectionsSwing {
....
    private void jMenuItemCalculerActionPerformed(java.awt.event.ActionEvent evt) {
        doCalculer();
    }
 
...
 
    private void jLabelCalculerMouseClicked(java.awt.event.MouseEvent evt) {
        if (jLabelCalculer.isEnabled()) {
            doCalculer();
        }
    }
 
....
    // event managers
    abstract protected void doSupprimer();
 
    abstract protected void doCalculer();
 
    abstract protected void doQuitter();
 
    abstract protected void doEffacer();
 
    abstract protected void doEnregistrer();
 
    abstract protected void doAjouter();
 
    abstract protected void doInformer();

    abstract protected void doMajLabelAjouter();
 
    abstract protected void doMajLabelSupprimer();
...
}
  • riga 1: la classe è dichiarata abstract
  • righe 3–5: gestione del clic sull'opzione di menu [jMenuItemCalculer]. Si nota che la gestione dell'evento è delegata al metodo doCalculer alla riga 19. Questo metodo non è implementato ed è dichiarato astratto. Sarà implementato dalla classe derivata [ElectionsSwing];
  • righe 9–13: il gestore per l'evento clic sull'etichetta [jLabelCalculer]. Un clic attiva sempre un evento, indipendentemente dal fatto che il componente [jLabel] sia attivo (enabled=true) o inattivo (enabled=false). Qui ci assicuriamo che sia effettivamente attivo per gestire l'evento;
  • righe 15 e seguenti: questa tecnica di delegare la gestione degli eventi a un metodo astratto viene applicata a tutti i gestori di eventi.

La classe [ElectionsSwing], derivata da [AbstractElectionsSwing], implementa tutti i gestori di eventi non implementati da [AbstractElectionsSwing]:


package elections.ui.service;;
...
public class ElectionsSwing extends AbstractElectionsSwing {
 
    // event managers
 
    @Override
    protected void doInformer() {
...
    }
 
    @Override
    protected void doAjouter() {
    ...
    }
 
    @Override
    protected void doCalculer() {
    ...
    }
 
    @Override
    protected void doEffacer() {
    ...
    }
 
    @Override
    protected void doEnregistrer() {
...
    }
 
    @Override
    protected void doQuitter() {
        System.exit(0);
    }
 
    @Override
    protected void doSupprimer() {
...
    }
 
    @Override
    protected void doMajLabelAjouter() {
...
    }
 
    @Override
    protected void doMajLabelSupprimer() {
...
    }
 
}
  • riga 3: [ElectionsSwing] estende [AbstractElectionsSwing]
  • righe 7–50: i gestori di eventi per la finestra grafica

I metodi della classe derivata [ElectionsSwing] manipoleranno i componenti della classe padre [AbstractElectionsSwing]. Attualmente, questi componenti hanno un ambito privato, impedendo alla classe figlia [ElectionsSwing] di accedervi:


private JMenuItem jMenuItemAPropos = null;
 
private JLabel jLabelAjouter = null;

Per risolvere questo problema, ci assicureremo che l'ambito dei componenti GUI sia [protected]:

  • impostare l'attributo [protected] in [3];

10.3.5. Implementazione dell'interfaccia [IElectionsUI]

Torniamo alla struttura della nostra applicazione:

Come illustrato sopra, il livello [ui] deve presentare l'interfaccia [IElectionsUI] all'oggetto [main]:


package elections.ui.service;
 
public interface IElectionsUI {
    /**
     * lance le dialogue avec l'utilisateur
     */
    public void run();
}

Questa interfaccia è stata definita nel progetto [elections-console-metier-dao-jdbc] e descritta nella Sezione 9.4. Poiché questo progetto è una dipendenza del progetto [swing], questa interfaccia è nota.

Poiché la classe [AbstractElectionsSwing] è diventata astratta, non può più essere istanziata da Spring. Ora è necessario istanziare invece la classe [ElectionsSwing]. La classe [ElectionsSwing] deve implementare l'interfaccia [IElectionsUI]. Il suo codice cambia quindi come segue:


public class ElectionsSwing extends AbstractElectionsSwing implements IElectionsUI {
 
    // interface [ElectionsUI] run method
    public void run() {
...
    }
 
  • riga 1: la classe [ElectionsSwing] implementa l'interfaccia [IElectionsUI]
  • righe 4–6: il metodo [run] di questa interfaccia

Cosa dovrebbe fare il metodo run? Visualizzare la finestra GUI. Come si fa? Possiamo usare come guida il metodo [main] generato da NetBeans nella classe [AbstractElectionsSwing], che fa esattamente ciò che vogliamo:


public static void main(String args[]) {
    /* Set the Nimbus look and feel */
    //<editor-fold defaultstate="collapsed" desc=" Look and feel setting code (optional) ">
    /* If Nimbus (introduced in Java SE 6) is not available, stay with the default look and feel.
         * For details see http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html 
     */
    try {
      for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) {
        if ("Nimbus".equals(info.getName())) {
          javax.swing.UIManager.setLookAndFeel(info.getClassName());
          break;
        }
      }
    } catch (ClassNotFoundException ex) {
      java.util.logging.Logger.getLogger(NewJFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
    } catch (InstantiationException ex) {
      java.util.logging.Logger.getLogger(NewJFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
    } catch (IllegalAccessException ex) {
      java.util.logging.Logger.getLogger(NewJFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
    } catch (javax.swing.UnsupportedLookAndFeelException ex) {
      java.util.logging.Logger.getLogger(NewJFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
    }
    //</editor-fold>
 
    /* Create and display the form */
    java.awt.EventQueue.invokeLater(new Runnable() {
      public void run() {
        new NewJFrame().setVisible(true);
      }
    });
  }

Il costruttore [AbstractElectionsSwing] utilizzato alla riga 28 è il seguente:


  public AbstractElectionsSwing() {
    initComponents();
}
  • Riga 2: Il metodo [initComponents] è un metodo privato generato dal generatore di GUI. Il suo codice non può essere modificato.

Il metodo [run] della classe [ElectionsSwing] potrebbe quindi essere il seguente:


  @Override
  public void run() {
    // on affiche l'interface graphique
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        init();
        setVisible(true);
      }
    });
}
  • Riga 6: L'interfaccia grafica viene inizializzata utilizzando il metodo [init]. Qui vorremmo chiamare il metodo [initComponents] della classe padre, ma è privato. Aggiungiamo quindi il seguente metodo [init] alla classe padre [AbstractElectionsSwing]:

  protected void init(){
    initComponents();
}
  • (continua)
    • Poiché si trova nella classe [AbstractElectionsSwing], il metodo [init] ha accesso al metodo privato [initComponents] della stessa classe;
    • poiché ha l'attributo [protected], è visibile nella classe figlia [ElectionsSwing];
  • riga 7: la GUI viene resa visibile;

Nota: una volta scritto il metodo [run] nella classe [ElectionsSwing], il metodo [main] della classe astratta [AbstractElectionsSwing] può essere rimosso.

10.3.6. La classe eseguibile

Torniamo alla struttura della nostra applicazione:

Vorremmo che Spring istanziasse il livello [ui] come avveniva quando era implementato da un'applicazione console. A tal fine, la classe di implementazione [ElectionsSwing] deve disporre di un riferimento al livello [business]:


@Component
public class ElectionsSwing extends AbstractElectionsSwing implements IElectionsUI{
 
  // reference on the [business] layer
  @Autowired
  private IElectionsMetier metier;
...
  • riga 1: la classe [ElectionsSwing] è un componente Spring;
  • righe 5–6: Spring inietta un riferimento nel livello [business];

L'interfaccia utente grafica viene avviata eseguendo la seguente classe [BootElectionsSwing]:

  

package elections.ui.boot;
 
import elections.ui.service.IElectionsUI;
 
public class BootElectionsSwing extends AbstractBootElections {
    public static void main(String[] arguments) {
        new BootElectionsSwing().run();
    }
 
    @Override
    protected IElectionsUI getUI() {
        return ctx.getBean("electionsSwing", IElectionsUI.class);
    }
}

Abbiamo spiegato un codice simile nella Sezione 9.5 quando abbiamo discusso il codice delle classi [AbstractBootElections] e [BootElectionsConsole]. Alla riga 12, recuperiamo il bean denominato [electionsSwing], che corrisponde al nome Spring standard per la classe [ElectionsSwing].

10.3.7. Inizializzazione dell'interfaccia utente grafica

Quando viene visualizzata la GUI, alcuni dei suoi componenti sono già stati inizializzati:

Image

Come mostrato sopra:

  • che la casella combinata è stata popolata con i nomi delle liste;
  • che il numero di seggi da assegnare e la soglia elettorale sono visualizzati;
  • che alcuni collegamenti sono stati disabilitati;
  • che nella parte inferiore della finestra viene visualizzato un messaggio di conferma;

Quando avranno luogo queste inizializzazioni? Possono avvenire solo dopo che il campo [electionsMetier] della classe [ElectionsSwing] è stato inizializzato. Questo perché i nomi delle liste saranno richiesti dal livello [metier]. Spring inizializzerà questo campo nel seguente ordine:

  • utilizzando il costruttore senza parametri della classe [ElectionsSwing];
  • iniezione delle dipendenze, in questo caso il riferimento al livello [business];
  • esecuzione del metodo [run] della classe [ElectionsSwing]:

    @Override
    public void run() {
        // on affiche l'interface graphique
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                init();
                setVisible(true);
            }
        });
}
  • Alla riga 12 abbiamo specificato che avremmo chiamato il metodo [init] della classe padre, che disegnerà i componenti della GUI. Sovrascriveremo questo metodo localmente nella classe [ElectionsSwing]. È all'interno di questo metodo che inizializzeremo i componenti della finestra (caselle combinate, etichette) con i dati questa volta:

Il metodo [init] locale potrebbe avere la seguente struttura:


public class ElectionsSwing extends AbstractElectionsSwing implements IElectionsUI {
 
    ...
  // reference on the [business] layer
  @Autowired
  private IElectionsMetier metier;
 
    // initializations
    public void init() {
      // generation of components by the parent class
    super.init();
 
    // lists are requested from the [metier] layer
        ...
        // associate list names with the jComboBoxNomsListes combo
        ...
        // and election parameters
        ...
        // we initialize the labels linked to these two pieces of information
        ...
        // message of success
        ...
        // initialization status of certain components form
...
}

Si noti, alla riga 11, la chiamata al metodo [init] della classe padre.

10.3.8. La classe [Utilities]

Nella classe [Utilities] sono stati raggruppati diversi metodi di utilità statici:

 

La classe [Utilities] è la seguente:


package istia.st.elections.ui;
 
import javax.swing.JLabel;
import javax.swing.JMenuItem;
 
//utility class
class Utilitaires {
    // manage label array status
    public static void setEnabled(JLabel[] labels, boolean value) {
        for (int i = 0; i < labels.length; i++) {
            labels[i].setEnabled(value);
        }
    }
 
    // manage the status of a table of menu options
    public static void setEnabled(JMenuItem[] menuItems, boolean value) {
        ...
    }
 
}
  • Riga 9: Il metodo setEnabled imposta lo stato dei componenti JLabel definiti in un array. Il metodo setEnabled di un componente JLabel consente di abilitare o disabilitare il JLabel.

Compito: seguendo l'esempio del metodo setEnabled alla riga 9, scrivi il metodo setEnabled alla riga 16 che esegue la stessa operazione con i componenti *JMenuItem*.


10.3.9. Il codice della classe [ElectionsSwing]

Rivediamo la struttura generale della classe [ElectionsSwing]:


package istia.st.elections.ui;
 
...
public class ElectionsSwing extends AbstractElectionsSwing implements IElectionsUI {
 
  // reference on the [business] layer
  @Autowired
  private IElectionsMetier metier;
 
 
    // initializations
    public void init() {
    ...
    }
 
    // event managers
 
    @Override
    protected void doInformer() {
...
    }
 
    @Override
    protected void doAjouter() {
...
    }
 
    @Override
    protected void doCalculer() {
    ...
    }
 
    @Override
    protected void doEffacer() {
...
    }
 
    @Override
    protected void doEnregistrer() {
...
    }
 
    @Override
    protected void doQuitter() {
        System.exit(0);
    }
 
    @Override
    protected void doSupprimer() {
...
    }
 
    @Override
    protected void doMajLabelAjouter() {
    ...
    }
 
    @Override
    protected void doMajLabelSupprimer() {
...
    }
 
}

Esamineremo i metodi della classe uno per uno.

10.3.9.1. Il metodo [init]

Torniamo all'interfaccia grafica:

Il metodo [init] ha i seguenti obiettivi:

  • poppolare la casella combinata [4] con gli ID e i nomi delle liste nel formato [ID - nome]
  • visualizzare un messaggio di conferma in [15]
  • inizializzare le etichette [2] e [3]
  • disabilitare determinati link

Lo scheletro del metodo [init] potrebbe essere il seguente:


@Override
    protected void init() {
        // génération des composants par la classe parent
        super.init();
        // initialisations locales
        modèleNomsVoix = new DefaultListModel<>();
        jListNomsVoix.setModel(modèleNomsVoix);
        modèleRésultats = new DefaultListModel<>();
        jListResultats.setModel(modèleRésultats);
        String info;
        try {
            // on demande les listes à la couche [métier]
            listes = ...
            // on associe les noms des listes au combo jComboBoxNomsListes
            ...
            // ainsi que les paramètres de l'election
            int nbSiegesAPourvoir = ...
            double seuilElectoral = ...
            // on initialise les labels liés à ces deux informations
            ...
            // message de succès
            info = "Source de données lue avec succès";
        } catch (ElectionsException ex1) {
            // on note l'error
            info = getInfoForException("Les erreurs suivantes se sont produites :", ex1);
        } catch (RuntimeException ex2) {
            // on note l'error
            info = getInfoForException("Les erreurs suivantes se sont produites :", ex2);
        }
        // on affiche l'info
        jTextPaneMessages.setText(info);
        jTextPaneMessages.setCaretPosition(0);
        // état formulaire
        Utilitaires.setEnabled(new JLabel[] { jLabelAjouter, jLabelCalculer, jLabelEnregistrer, jLabelSupprimer }, false);
        Utilitaires.setEnabled(
                new JMenuItem[] { jMenuItemAjouter, jMenuItemCalculer, jMenuItemEnregistrer, jMenuItemSupprimer }, false);
        // centrer la fenêtre
        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
        Dimension frameSize = getSize();
        if (frameSize.height > screenSize.height) {
            frameSize.height = screenSize.height;
        }
        if (frameSize.width > screenSize.width) {
            frameSize.width = screenSize.width;
        }
        setLocation((screenSize.width - frameSize.width) / 2, (screenSize.height - frameSize.height) / 2);
    }
 
    private String getInfoForException(String message, ElectionsException ex) {
        // on affiche le message
        StringBuffer info = new StringBuffer(String.format("%s -------------\n", message));
        info.append(String.format("Code erreur : %d\n", ex.getCode()));
        // on affiche les erreurs
        for (String erreur : ex.getErreurs()) {
            info.append(String.format("-- %s\n", erreur));
        }
        return info.toString();
    }
 
    private String getInfoForException(String message, RuntimeException ex) {
        // on affiche le message
        StringBuffer info = new StringBuffer(String.format("%s -------------\n", message));
        // on affiche la pile des exceptions
        Throwable cause = ex;
        while (cause != null) {
            info.append(String.format("-- %s\n", cause.getMessage()));
            cause = cause.getCause();
        }
        return info.toString();
    }

Compito: Completa il codice per il metodo [init].


Da leggere nel corso: componenti JTextField e JLabel

Nota:

Un componente JList visualizza i dati contenuti in un modello. Per impostazione predefinita, questo modello è di tipo DefaultListModel (righe 2 e 3). Un oggetto DefaultListModel si comporta in modo simile a un ArrayList:

  • Per aggiungere un oggetto o al modello:

[DefaultListModel].addElement(Object o);

In questa applicazione, l'oggetto o sarà sempre di tipo String.

  • Per rimuovere l'elemento i dal modello:

[DefaultListModel].remove(int i);
  • Per recuperare l'elemento i dal modello:

[DefaultListModel].elementAt(int i);

Per aggiungere un elemento alla casella combinata [jComboBoxNomsListes], utilizzare il metodo [addItem]:


jComboBoxNomsListes.addItem(chaîne de caractères)

Un componente JTextPane dispone dei metodi getText() e setText() per leggere/scrivere il testo visualizzato.

Il pulsante [Aggiungi] [6] è attivo solo quando il campo [5] per i voti non è vuoto. Nella classe [AbstractElectionsSwing], il gestore che tiene traccia dei movimenti del cursore nel campo [5] è il seguente:


  private void jTextFieldVoixListeCaretUpdate(javax.swing.event.CaretEvent evt) {
    doMajLabelAjouter()
}

La riga 2 chiama il metodo [doMajLabelAjouter] della classe [ElectionsSwing].


    protected void doMajLabelAjouter() {
        // on fixe l'état du label [jLabelAjouter]
        ...
        // on fixe l'état du menu [jMenuItemAjouter]
        ...
}

Compito: completare il codice per il metodo [doMajLabelAjouter].


10.3.9.3. Assegnare i voti a ciascuna lista

Per ogni lista di candidati di cui al punto (4), procedere come segue:

  • selezionare una lista da (4)
  • inserire il numero di voti in (5)
  • confermare cliccando sul link [Aggiungi]

Gli errori di inserimento vengono segnalati come mostrato nell'esempio seguente:

Image

Se il numero di voti è corretto, l'elenco viene aggiunto al componente (8), il numero di voti viene azzerato e il link [Aggiungi] viene disabilitato:

Nella classe [AbstractElectionsSwing], il gestore che gestisce il clic sul link [Aggiungi] è il seguente:


    private void jLabelAjouterMouseClicked(java.awt.event.MouseEvent evt) {
        if (jLabelAjouter.isEnabled()) {
            doAjouter();
        }
}

La riga 3 chiama il metodo [doAjouter] della classe [ElectionsSwing]:


  // modèles des listes JList
  private DefaultListModel<String> modèleNomsVoix = null;
  private DefaultListModel<String> modèleRésultats = null;
 
  // les listes en compétition
  private ListeElectorale[] listes;
 
  // listes saisies par l'user
  private final List<ListeElectorale> listesSaisies = new ArrayList<>();
  private ListeElectorale[] tListesSaisies;
...
  @Override
  protected void doAjouter() {
    // le nombre de voix est-il correct ?
    ...
    // si erreur, alors on la signale
    if (erreur) {
      JOptionPane.showMessageDialog(null, "Nombre de voix incorrect", "Elections : erreur",
              JOptionPane.INFORMATION_MESSAGE);
      jTextFieldVoixListe.requestFocus();
      // retour à l'graphic interface
      return;
    }
    // pas d'error - save the list
    listesSaisies.add(...);
    modèleNomsVoix.addElement(...);
    // on nettoie le nombre de voix
    jTextFieldVoixListe.setText("");
    // état formulaire (menus, labels)
...
}
  • Riga 25: Ogni volta che l'utente aggiunge voci a un elenco e conferma la selezione, tale elenco viene memorizzato nel campo [listesSaisies] alla riga 9. L'elenco viene salvato lì con le informazioni [id, versione, nome, voce]. Le prime tre informazioni provengono dagli elenchi inizialmente memorizzati nell'array alla riga 6. Il metodo [getSelectedIndex] della casella combinata restituisce l'indice dell'elenco selezionato;

Compito: completare il codice del metodo [doAjouter].


Il link [Delete] [9] è attivo solo quando un elemento è selezionato in [8].

Nella classe [AbstractElectionsSwing], il gestore che risponde al clic su un elemento nell'elenco [8] è il seguente:


  private void jListNomsVoixValueChanged(javax.swing.event.ListSelectionEvent evt) {
    doMajLabelSupprimer();
}

La riga 2 chiama il metodo [doMajLabelSupprimer] della classe [ElectionsSwing].


    @Override
    protected void doMajLabelSupprimer() {
            // on allume le label [jLabelSupprimer] et l'corresponding menu option
            ...
}

Compito: completare il codice per il metodo [doMajLabelSupprimer].


10.3.9.5. Elimina un elenco di candidati

Il link [Elimina] [9] consente di eliminare la coppia (nome, voto) selezionata in (8). Una volta completata l'eliminazione, il link [Elimina] viene disabilitato. Verrà riabilitato solo quando verrà selezionato un nuovo elenco in (8).

Nella classe [AbstractElectionsSwing], il gestore che risponde al clic sul link [Elimina] è il seguente:


  private void jLabelSupprimerMouseClicked(java.awt.event.MouseEvent evt) {
    if(jLabelSupprimer.isEnabled()){
      doSupprimer();
    }
}

La riga 3 chiama il metodo [doSupprimer] della classe [ElectionsSwing].


@Override
    protected void doSupprimer() {
        // suppression de la liste sélectionnée, du modèle modèleNomsVoix et des listes saisies
        ...
        // maj de l'status of labels and form menu options
        Utilitaires.setEnabled(...);
    ...
}

Lavoro da svolgere: completare il codice per il metodo [doSupprimer].


Il link [Calculate] [10] è attivo solo quando c'è almeno un elemento in [8].


Compito: aggiungere il codice necessario per gestire questo link nei metodi [doAdd] e [doDelete] scritti in precedenza. Verrà gestita anche la corrispondente opzione di menu.


Nota: il numero di elementi in un DefaultListModel si ottiene utilizzando il metodo size().

10.3.9.7. Calcola i posti

Il link [Calcola] [10] consente di avviare il calcolo dei posti e di visualizzarne i risultati in (14). Se il calcolo non va a buon fine (tutte le liste sono state eliminate), viene visualizzato un messaggio di errore in [15]. In ogni caso, al termine del calcolo, il link [Calcola] [10] viene disabilitato.

Nella classe [AbstractElectionsSwing], il gestore che risponde al clic sul link [Calcola] è il seguente:


  private void jLabelCalculerMouseClicked(java.awt.event.MouseEvent evt) {
    if(jLabelCalculer.isEnabled()){
      doCalculer();
    }
}

La riga 3 chiama il metodo [doCalculer] della classe [ElectionsSwing].


  // listes saisies par l'utilisateur
  private final List<ListeElectorale> listesSaisies = new ArrayList<>();
  private ListeElectorale[] tListesSaisies;
 
...
  @Override
  protected void doCalculer() {
    tListesSaisies = listesSaisies.toArray(new ListeElectorale[0]);
    // calcul des sièges
    try {
      ...
    } catch (ElectionsException ex) {
      // on affiche l'exception
      ...
      return;
    }
    // affichage des résultats
    ...
    // maj état formulaire
    Utilitaires.setEnabled(...);
}

Compito: Completa il codice per il metodo [doCalculer].


10.3.9.8. Salvare i risultati nell'origine dati

Il link [Salva] (12) consente di salvare i risultati del calcolo dei posti nell'origine dati. Una volta completato il salvataggio, il link [Salva] viene disabilitato. Se il salvataggio non va a buon fine, viene visualizzato un messaggio di errore in [15]. In entrambi i casi, il link [Salva] viene quindi disabilitato.

Nella classe [AbstractElectionsSwing], il gestore che gestisce il clic sull'etichetta [Save] è il seguente:


  private void jLabelEnregistrerMouseClicked(java.awt.event.MouseEvent evt) {
    if(jLabelEnregistrer.isEnabled()){
      doEnregistrer();
    }
}

La riga 3 chiama il metodo [doEnregistrer] della classe [ElectionsSwing]:


@Override
    protected void doEnregistrer() {
        // on demande l'enregistrement à la couche métier
        try {
            ...
        } catch (ElectionsException ex) {
            // on affiche l'exception
            ...
            // retour à l'interface graphique
            return;
        }
        // maj du formulaire
        Utilitaires.setEnabled(...);
...
}

Compito: Completa il codice per il metodo [doEnregistrer].


10.3.9.9. Cancella risultati

Il link [Cancella] (11) cancella i risultati visualizzati in (14).

Nella classe [AbstractElectionsSwing], il gestore che gestisce il clic sull'etichetta [Clear] è il seguente:


  private void jLabelEffacerMouseClicked(java.awt.event.MouseEvent evt) {
    if(jLabelEffacer.isEnabled()){
      doEffacer();
    }
}

La riga 3 chiama il metodo [doEffacer] della classe [ElectionsSwing]:


    @Override
    protected void doEffacer() {
        // on vide la liste des résultats
        ....
        // maj du formulaire
        Utilitaires.setEnabled(...);
}

Compito: Completa il codice per il metodo [doEffacer].


Nota: la classe DefaultListModel dispone di un metodo clear() che rimuove tutti i suoi elementi.

10.3.10. Miglioramenti

L'interfaccia grafica precedente può essere migliorata in vari modi: l'utente potrebbe dimenticare di inserire i voti per tutte le liste presenti nella casella combinata e potrebbe anche inserire accidentalmente più volte i voti per la stessa lista.


Compito: Migliorare l'algoritmo in modo che nessuno di questi casi possa verificarsi. Una soluzione semplice consiste nel mantenere un dizionario delle liste inserite, dove le chiavi sono le voci della casella combinata. Ci assicureremo inoltre che il pulsante [Calcola] sia abilitato solo quando tutte le liste sono state inserite.


Vedi il corso [ref1]: la classe HashTable nella sezione 3.8.