|
Java Tutorial - Parte 2 0.1
Un tutorial per esempi
|
La applicazione MasterMind comincia ad avere una certa funzionalità e può essere eseguita con diverse opzioni di gioco: è possibile cambiare il tipo di simboli, la lunghezza della sequenza, il nome ed il tipo dei giocatori, etc...
java -ea mastermind.game.Main playerName=Lukas opponentType=AI k=4 codex=COLORS
Tuttavia, dover digitare sulla riga di comando una serie di opzioni delle quali dobbiamo anche ricordarci la sintassi è davvero noioso, poco professionale e, sopratutto, error-prone (=incline agli errori). Se fosse una applicazione CLI (=a riga di comando) allora non abbiamo molte altre possibilità ma in una applicazione GUI dobbiamo fare di meglio.
La soluzione è quella di scrivere un pannello delle proprietà da visualizzare prima che la partita cominci e che può essere attivato dallo user clikkando il bottone "Options" dello splash-screen. Nella figura seguente potete vedere come si presenterà il pannello in argomento :
I sorgenti di questa versione sono riepilogati nella tabella seguente:
| nome file | package | descrizione |
|---|---|---|
| PropertyPanel.java | mastermind.gui | il pannello delle proprietà |
| TestPropertyPanel.java | mastermind.test.gui | app di test del pannello proprietà |
| Main07.java | mastermind.app | classe principale versione 0.7 |
Il pannello delle proprietà è diviso in tre "schede", racchiuse in un bordo nero con titolo ed ognuna delle tre schede si riferisce ad una specifica categoria di proprietà:
Params permette di selezionare i parametri di gioco e altre opzioni di visualizzazione Players permette di selezionare i giocatori Remote permette di selezionare i parametri della connessione remotaNoterete che non tutte le proprietà della applicazione possono essere selezionate dal pannello omonimo:
Il pannello proprietà viene disposto in un BoxLayout e diviso in quattro pannelli disposti verticalmente: i tre pannelli da cui si selezionano i valori delle proprietà più un pannello per i bottoni di comando "Play" ed "Exit":
----BoxLayout.Y_AXIS ----------- | - Params ----------------- | | | | | | | | | | -------------------------- | | -Players ----------------- | | | | | | | | | | -------------------------- | | -Remote ------------------ | | | | | | | | | | -------------------------- | |--------------------------------| | vertical_glue | |--------------------------------| | PLAY EXIT | --------------------------------
Il sottopannello dei parametri di gioco contiene i tre parametri strettamente collegati al gioco del Mastermind ed altre proprietà che riguardano comunque il gioco. Nella seguente tabella si riepilogano le etichette della proprietà, il componente Swing usato per modificare la proprietà ed una breve descrizione:
| Etichetta | Componente | Descrizione |
|---|---|---|
| Number of Symbols | JList | il numero di simboli a disposizione |
| Sequence length | JRadioButton | la lunghezza della sequenza |
| Symbols Type | JComboBox | il tipo di simboli (NUMBER,LETTERS,COLORS) |
| Allow repetition | JCheckBox | consenti la ripetizione dei simboli |
| Swap first turn | JCheckBox | scambia il primo turno nelle partite multiple |
Usiamo il componente Swing JList per il parametro di gioco "numero di simboli
a disposizione". Una JList (il nome del componente è list-box) presenta all'utente un gruppo di elementi, visualizzati in una o più colonne, tra cui scegliere. La JList può contenere molti elementi, quindi essa viene spesso visualizzata in uno JScrollPane (= pannello scorrevole). Il codice per visualizzare la lista è il seguente:
La JList viene costruita passando come argomento una array di stringhe che rappresenta i valori che l'user può selezionare dalla lista; per il parametro di gioco "n" i valori selezionabili sono i numeri da "6" a "10" ma ho previsto anche il valore ZERO che ha un significato particolare: se il giocatore seleziona "0" come parametro "n" allora la applicazione sceglierà un valore casuale tra quelli ammissibili. Il componente JList ha diverse caratteristiche impostabili dal programmatore:
setLayoutOrientation; nel nostro caso l'orientamento è HORIZONTAL_WRAP (=orizzontale con eventuale ritorno a capo) setSelectionMode nel nostro caso impostiamo SINGLE_SELECTION che significa che un solo elemento della lista può essere selezionato in ogni momento setVisibleRowCount si può impostare il numero di righe che si possono visualizzare: poichè la nostra lista ha un orientamento orizzontale e tutti gli elementi dovrebbero essere contenuti in una unica riga, impostiamo questo valore a UNO.Infine, impostiamo come "già selezionato" il valore attuale di "n" usando il metodo setSelectedValue. Altri metodi molto utili del componente JList sono:
getSelectedIndex che ritorna l'indice nella array dell'elemento selezionato getSelectedValue che ritorna il valore dell'elemento selezionato clearSelection che cancella la selezione isSelectionEmpty che ritorna TRUE se nessun elemento è selezionatoPer maggiori informazioni sull'uso del componente vedi il tutorial ufficiale al seguente link: How to Use Lists
Usiamo il componente Swing JRadioButton per la proprietà "sequence length" (=lunghezza della sequenza) I compomenti radio-button (=pulsanti di opzione) sono gruppi di pulsanti in cui, per convenzione, è possibile selezionare un solo pulsante alla volta. La libreria Swing supporta i pulsanti di opzione con le classi JRadioButton e ButtonGroup. In un certo senso, i radio-button assomigliano ad una list-box ma i primi rinforzano il concetto di selezione singola: se viene selezionato un elemento tutti gli altri che fanno parte dello stesso gruppo sono automaticamente deselezionati.
Ogni radio-button è un componente a se stante quindi per inserirli tutti nella scheda dei parametri di gioco dobbiamo creare un sotto-pannello ed iterare su tutti i valori possibili creando, per ognuno di essi, un componente JRadioButton specifico:
Anche per il parametro di gioco "k" ho previsto, oltre ai due valori ammissibili ("3" e "4") lo ZERO: questo ha lo stesso significato già visto per il parametro "n" e cioè, se il giocatore seleziona lo "0" come parametro di gioco "k" allora la applicazione ne scelgierà uno a caso tra quelli ammissibili.
I metodi più usati del componente JRadioButton sono:
setText: imposta il testo del bottone getText: ritorna il testo del bottone setSelected: imposta lo stato del bottone (selezionato o meno) isSelected: ritorna TRUE se il bottone è selezionatoPer maggiori informazioni sull'uso del componente vedi il tutorial ufficiale al seguente link: How to Use Radio Buttons
Il parametro "symbol type" (=tipo di simbolo) non è un vero parametro di gioco ma, piuttosto, una preferenza del giocatore che può scegliere in che modo visualizzare i codici della sequenza (NUMBERS, LETTERS, COLORS). Per questa proprietà ho scelto il componente Swing JComboBox (una combo-box = casella combinata). Una combo-box consente all'utente di scegliere tra diverse opzioni e può avere due forme molto diverse: la forma predefinita è la casella combinata non modificabile, che presenta un pulsante ed una drop-down list (= un elenco a discesa di valori). La seconda forma, chiamata casella combinata modificabile, presenta un campo di testo con un piccolo pulsante adiacente. L'utente può digitare un valore nel campo di testo o fare clic sul pulsante per visualizzare un elenco a discesa.
Le combo-box occupano poco spazio sullo schermo e il loro formato modificabile (campo di testo) è utile per consentire all'utente di scegliere rapidamente un valore ma senza limitarsi ai valori visualizzati. Poichè la proprietà "symbol type" prevede solo tre valori possibili, la combo-box di questo parametro ha il campo editabile disabilitato.
La combo-box viene costruita con una array che rappresenta i possibili valori selezionabili dalla lista a discesa. Dopo aver disabilitato il campo editabile, il codice imposta nella combo l'elemento già selezionato in base alle proprietà della applicazione. Infine, aggiunge il componente al sotto-pannello.
I metodi più usati del componente JComboBox sono:
addItem inserisce un nuovo elemento alla lista drop-down insertItemAt inserisce un nuovo elemento alla lista drop-down ad uno specifico indice removeItem rimuove un elemento dalla lista drop-down getSelectedItem ritorna l'elemento selezionato oppure editatoPer maggiori informazioni sull'uso del componente vedi il tutorial ufficiale al seguente link: How to Use Combo Boxes
Le proprietà "allow repetition" (=consenti ripetizione dei simboli) e "swap first turn" (=scambia il primo turno di gioco) sono rappresentate dal componente Swing JCheckBox. La classe JCheckBox supporta i pulsanti di controllo. I pulsanti di controllo sono simili ai pulsanti di opzione (JRadioButton) ma il loro modello di selezione è diverso, per convenzione: è possibile selezionare un numero qualsiasi di pulsanti di controllo in un gruppo: nessuno, alcuni o tutti. Un gruppo di pulsanti di opzione, invece, può avere un solo pulsante selezionato.
I metodi più usati del componente JCheckBoxBox sono:
setText: imposta il testo del bottone getText: ritorna il testo del bottone setSelected: imposta lo stato del bottone (selezionato o meno) isSelected: ritorna REUW se il bottone è selezionatoPer maggiori informazioni sull'uso del componente vedi il tutorial ufficiale al seguente link: How to Use CheckBoxes Boxes
Il sottopannello dei giocatori permette di selezionare il nome del giocatore umano ed il tipo di giocatore dell'avversario che può anche essere un altro umano.
Benchè piuttosto improbabile che accada, questa implementazione di Mastermind offre la possibilità per un umano di giocatore contro un umano anche sulla stessa macchina come abbiamo visto in Umano contro umano; Nella seguente tabella vengono riespilogati, per ogni proprietà, la etichetta che la descrive, il componente GUI utilizzato e una breve descrizione:
| etichetta | componente | descrizione |
|---|---|---|
| Nickname | JTextField | un semplice campo di test editabile |
| Opponent | JComboBox | il tipo di avversario (anche HUMAN) |
Il sottopannello dei giocatori viene creato dal metodo createPlayerPanel il quale istanzia i due componenti descritti nella tabella precedente e li aggiunge al sottopannello.
La proprietà "Player name" permette di specificare il nome del giocatore umano che, per impostazione di default è null. Per specificare questa proprietà si è scelto il componente Swing JTextField: che è un controllo di testo di base che consente all'utente di digitare una piccola quantità di testo. Quando l'utente indica che l'inserimento del testo è completo (solitamente premendo nvio), il campo di testo attiva un ActionEvent.
Il componente JTextField non consente di digitare il testo su più di una riga ma questo non è un problema: il nome del giocatore non dovebbe essere composto di due o più righe.
Per creare il componente JTextField è sufficente istanziare la classe passando il testo che deve essere visualizzato all'inizio come argomento al costruttore. Successivamente è sufficiente aggiungere il componente ad un pannello per renderlo visibile ed il lavoro termina qui: è davvero uno dei componenti più semplici da usare, al pari delle etichette (le JLabel). I metodi più usati del componente JTextField sono:
setText imposta il testo nel componente getText ottiene il testo digitato dallo user setFont imposta il font del componentePer quanto riguarda il font usato nel componente JTextField vi è da osservare che il font impostato si applica a tutto il testo nel componente e non solo ad una parte di esso.
La libreria Swing di mette a disposizione due varianti specializzate di caselle di testo:
JFormattedTextField: è uguale alla sua classe base ma il prorgammatore può specificare dettagliatamente i caratteri che possono esservi digitati JPasswordField: è uguale alla sua classe base ma i caratteri digitati non sono visibili dallo userPer maggiori informazioni sull'uso del componente vedi il tutorial ufficiale al seguente link: How to Use Text Fields
La proprietà "Opponent type" viene rappresentata da un componente JComboBox con campo editabile disabilitato esattamente come la proprietà "Symbols type" che abbiamo già analizzato in Il componente JComboBox.
Anche se al momento la funzionalità di connessione remota non è ancora stata implementata, disegnamo comunque la interfaccia grafica che permette al giocatore di specificare i parametri di rete per la connessione remota. Poichè al momento sarà implementata la sola connessione TCP/IP, le proprietà che l'user deve indicare nel sotto-pannello della connessione riguardano i seguenti parameti:
Nella seguente tabella vengono riespilogati, per ogni proprietà, la etichetta che la descrive, il componente GUI utilizzato e una breve descrizione:
| etichetta | componente | descrizione |
|---|---|---|
| server | JRadioButton | selezionato per il server di rete |
| client | JRadioButton | selezionato per il client di rete |
| server hostname | JTextField | il nome host o indrizzo IP del server |
| server port | JSpinner | la porta di ascolto o di connessioen |
Per le proprietà "server" e "client" si usano due JRadioButton inseriti in un ButtonGroup che ne garantisce la selezione esclusiva. Il componente radio-button è già stato descritto in Il componente JRadioButton.
La proprietà "Server hostname" permette al giocatore dal lato client della connessione remota di specificare il nome host o l'indirizzo IP del lato server del gioco. Si tratta di un componente JTextField in cui l'user può inserire una piccola porzione di testo ed è già stato descritto in Il componente JTextField.
Il nome del server da indicare dipende dalla connessione: potrebbe essere un server centralizzato oppure un host o indirizzo IP sulla rete locale (LAN). Esempi di host da inserire in questo campo possono essere:
mastermind.galdom.net // server centralizzato
topolino // host sulla LAN, presuppone la presenza i un server DNS
192.168.1.40 // indirizzo IP sulla LAN
Il codice per creare il sottopannello dei parametri di rete è molto simile a quello degli altri due sotto-pannelli. Riporto solo l'estratto che riguardo lo spinner per la selezione della porta TCP/IP:
Gli spinner sono simili alle caselle combinate (JComboBox) ed agli elenchi (JList) in quanto consentono all'utente di scegliere tra un intervallo di valori. Come le caselle combinate modificabili, gli spinner consentono all'utente di digitare un valore. A differenza delle caselle combinate, gli spinner non hanno un elenco a discesa. Poiché gli spinner non visualizzano i valori possibili (è visibile solo il valore corrente), vengono spesso utilizzati al posto di caselle combinate o elenchi quando l'insieme di valori possibili è estremamente ampio. Tuttavia, gli spinner dovrebbero essere utilizzati solo quando i valori possibili e la loro sequenza sono evidenti.
Uno spinner è un componente composto con tre sottocomponenti: due piccoli pulsanti e un editor. L'editor può essere qualsiasi JComponent, ma per impostazione predefinita è implementato come un pannello che contiene un campo di testo formattato.
Lo spinner è il componente ideale per specificare il numero di porta TCP dal momento che consente di dare un forte feedback allo user su quali sono i valori ammessi:
SpinnerNumberModel possono essere inseriti solo valori numerici: la porta TCP è un numero, infatti Riepilogando, lo spinner per la porta TCP è costruito in questo modo:
I metodi più usati del componente JSpinner sono:
setValue imposta il valore dello spinner getValue ottiene il valore dello spinner setModel imposta il data-model dello spinner getModel ottiene il data-model dello spinnerPer maggiori informazioni sull'uso del componente vedi il tutorial ufficiale al seguente link: How to Use Spinners
Dopo che l'user ha scelto i parametri di gioco e le altre proprietà della applicazione, dobbiamo leggere le scelte del giocatore dai vari componenti. Per ottenere questo ci sono sostanzialmente due strategie:
Quando il valore di un componente Swing viene modificatoo viene accodato nella coda degli eventi un oggetto derivato da EventObject il quale descrive l'evento occorso. Abbiamo già affrontato questo argomento nella sezione Il gestore degli eventi nella quale abbiamo imparato che per intercettare un qualsiasi evento è sufficente registrarsi come listener di quel particolare evento per quello specifico componente.
In sostanza, se vogliamo essere notificati quando l'user seleziona un elemento dalla lista dei "numero di simboli a disposizione" dobbiamo creare la JList e successivamente aggiungere la classe che implementa il listener all'elenco dei listeners:
Nella tabella seguente riepilogo il componente, la interfaccia da implementare per il listener e l'evento da gestire:
| Componente | Evento | Listener |
|---|---|---|
| JList | ListSelectionEvent | ListSelectionListener |
| JRadioButton | ActionEvent | ActionListener |
| JComboBox | ActionEvent | ActionListener |
| JCheckBox | ActionEvent | ActionListener |
| JTextField | ActionEvent | ActionListener |
| JSpinner | ChangeEvent | ChangeListener |
Il vantaggio di questo approccio consiste nel fatto che non devo tenere traccia dei riferimenti ai componenti istanziati ed aggiunti al pannello: posso ottenere il valore selezionato dallo user nell'evento che ho intercettato nel listener. Lo svantaggio è che per ogni componente devo registrare un listener ma, come potete vedere dalla tabella, i listener sono solo di tre tipi diversi.
Con questo approccio posso disinteressarmi dei listeners: in fondo, cosa mi importa delle singole selezioni del giocatore? Posso lasciare che l'user selezioni, deselezioni e modifichi i parametri anche cento volte senza doverne registrare i valori: ciò che conta è che io ottenga i valori selezionati quando il giocatore clikka il bottone "Play", cioè alla fine delle operazioni.
Accanto al vantaggio della assenza dei listeners, con questo approccio ho lo svantaggio di dover tenere traccia di tutti i componenti istanziati nei metodi createXxxxPanel dal momento che, alla fine, dovrò usare quei riferimenti per ottenere i valori:
Quale dei due approcci è stato scelto? Il secondo, quello in cui tutte le proprietà vengono lette alla fine delle operazioni di selezione da parte dello user e non perchè sia il più efficente o il più comodo, anzi. Il motivo di questa scelta è più sfuggente, più subdolo: supponiamo che l'user modifichi diversi parametri e proprietà ma che alla fine decida di non giocare più e chiude la applicazione. Egli si aspetta che le proprietà originali, quelle in essere prima della visualizzazione del pannello delle proprietà siano ancora valide ma poichè ad ogni selezione le abbiamo modificate ciò che si aspetta l'user non è più vero.
Si potrebbe ovviare a questo inconveniente creando una copia delle proprietà originali nel costruttore del pannello delle proprietà da ripristinare nel caso l'user non intenda più giocare: a parte il fatto che dobbiamo intercettare la chiusura della applicazione abbiamo anche un altro problema. Saremo tentati di creare la copia in questo modo:
ma non funziona! Non abbiamo affatto creato una copia dell'oggetto MMProperties ma un semplice secondo riferimento ALLO STESSO OGGETTO. Non ha importanza se apportiamo le modifiche a properties oppure a oldProperties perchè entrambi si riferiscono alla unica istanza esistente. Non possiamo creare copie di oggetti, quindi? Certo che possiamo creare copie di qualsiasi oggetto ma con il codice dedicato, non certo con l'operatore di uguaglianza:
Il metodo getCopy crea una NUOVA ISTANZA della classe MyObject con l'operatore new: è questa istruzione che crea la copia effettiva; ora ci sono davvero due istanze separate della classe MyObject che contengono lo stesso dato. La modifica di una delle due istanze non ha alcun effetto sull'altra.
Questa sezione vi sembrerà inutile dal momento che non siamo interessati ai valori selezionati in ogni momento dallo user avendo scelto di ottenere tutte le proprietà alla fine delle scelte, quando il giocatore clikka il bottone "Play". E' vero, non siamo interessati ai valori scelti dal giocatore in ogni momento ma siamo interessati ad alcuni di questi valori per dare allo user un forte feedback nelle scelte:
Ecco perchè dobbiamo intercettare gli eventi di questi tre componenti e scrivere i metodi che gestiscono la abilitazione 7 disabilitazione dei componenti a seconda della scelta dello user:
La classe principale Main07, derivata da Main06 in modo da mantenerne le funzionalità preesistenti, sovrascrive i metodi che implementano le nuove funzionalità:
actionPerformed: è necessario gestire l'evento button-clikked del bottone "Options" newGame: è necessario leggere le proprietà prima di creare una nuova istanza dell'oggetto game terminate: prima di chiudere la applicazione, viene richiamato il metodo Propertypanel.close il quale è deputato a liberare le risorse eventualmente allocate.Al momento attuale, il pannello delle proprietà non alloca alcuna risorsa che deve essere liberata o chiusa ma in futuro questo metodo dovrà essere implementato davvero. Ne riparliamo nel capitolo Version 0.9: la ricerca dei giocatori in rete.
La documentazione completa del package descritto in questo capitolo può essere visualizzata clikkando il seguente link che riporta alla documentazione Javadoc della versione 0.7 del progetto: The MasterMind Project Version 0.7
Argomento precedente - Argomento successivo - Indice Generale