CORSO DI INTERFACCE UTENTE - INTERFACCE GRAFICHE ================================================ LABORATORIO DEI GIORNI 11-12 APRILE 2002 ================================================ COMMENTI GENERALI Per usare le componenti AWT occorre importare il package AWT scrivendo in testa al sorgente java: import java.awt.*; Per usare anche gli eventi occorre importare anche il sotto-package degli eventi scrivendo: import java.awt.event.*; COMMENTI ALL'ESERCIZIO 1 ===Posizionamento delle componenti nell'interfaccia=== L'idea e' di NON collocarle in una posizione specifica ma lasciare fare ai layout manager. Bisogna: - progettare l'organizzazione dell'interfaccia secondo un sistema di contenitori concentrici - ciascun contenitore e' dotato di un certo layout manager, che stabilisce quanti oggetti ci posso mettere dentro e in che modo saranno dislocati - il contenitore esterno e' un Frame - ogni volta che in una posizione di un contenitore voglio mettere piu' di un oggetto, devo usare un altro contenitore per raggrupparli - gli altri contenitori diversi da quello esterno sono Panel Per esempio per la calcolatrice l'organizzazione poteva essere questa: - frame dotato di un layout manager che mi consenta di gestirlo come tre aree sovrapposte: va bene un BorderLayout di cui useremo solo le posizioni NORTH, CENTER e SOUTH. non e' indicato un GridLayout con 3 righe e 1 colonna perche' costringerebbe le tre aree ad avere dimensioni uguali fra loro (mentre per es. l'area che contiene la tastiera la vogliamo piu' grande) - nella parte alta del frame mettiamo un panel con dentro i due bottoni ok e cancel. questo panel lo posso dotare semplicemente di un FlowLayout (che mette le cose in fila) oppure di un GridLayout con 1 riga e 2 colonne. Notare che col flow layout i due bottoni hanno dimensioni diverse (calcolate in base alla lunghezza delle loro etichette), mentre col grid layout i due bottoni hanno dimensioni uguali fra loro. - nella parte centrale mettiamo il display, che puo' essere per esempio un TextField - nella parte bassa mettiamo un pannello con GridLayout contenente tutti i bottoni che servono ===Dimensioni del frame e del suo contenuto=== Nell'esempio da cui sono partiti quasi tutti c'era un frame vuoto che veniva esplicitamente forzato ad avere una certa dimensione tramite il metodo setSize. In realta' esistono due modi per dimensionare un frame e il suo contenuto: (1) SCONSIGLIATA Partendo dall'esterno (dal frame) e propagando le dimensioni verso l'interno (il suo contenuto): - si chiama setSize(,) sul frame - java provvedera' a dimensionare il contenuto in modo da farlo stare dentro quelle dimensioni SENZA avanzare spazio vuoto Per es. si vede che se metto UN bottone in un frame dimensionato grande, viene un bottone grande come tutto il frame, TROPPO grande. (2) PREFERIBILE Partendo dall'interno (dal contenuto) e propagando le dimensioni verso l'esterno (il frame): - NON si chiama setSize sul frame - PRIMA si aggiungono tutti i sotto-componenti - POI si chiama sul frame pack() - java provvedera' a impacchettare il contenuto del frame alle dimensioni piu' piccole possibili sufficienti a farci stare tutto (per es. i bottoni sono dimensionati in base alla loro etichetta, i contenitori in base a quello che devono contenere...) - i vincoli imposti dai layout manager possono influire sulle dimensioni (per es. con una griglia tutti gli elementi vengono dimensionati come il piu' grande) In genere pack() si chiama per ultimo, subito prima di setVisible(true). Nota: nell'esempio che avevate (frame vuoto) usando pack la dimensione del frame sarebbe stata zero x zero pixel, per questo gliene e' stata attribuita forzatamente una con setSize. ===Come mai non si chiude la finestra=== Avrete notato che cliccando sul dispositivo di chiusura della finestra (il simbolo "x" sul bordo a destra) non succede nulla. Per far si' che la finestra si chiuda bisogna far gestire al frame l'evento che si scatena con il click sul dispositivo di chiusura della finestra, in particolare gestirlo terminando il programma con la chiamata a System.exit(). Tale evento appartiene alla classe java WindowEvent (altri window event sono per es. iconficicazione, deiconificazione...). Occorre pertanto definire un WindowListener e associarlo al frame. WindowListener e' un'interfaccia, non una classe. Per definire un nostro window listener (che agisca come vogliamo) occorre implementare TUTTI i metodi previsti da WindowListener, anche se a noi interessa implementarne solo uno, cioe' windowClosing (che e' chiamato quando la finestra si deve chiudere). In alternativa posso specializzare la classe WindowAdapter, implementazione predefinita di WindowListener che gestisce ogni window event non facendo nulla. In questo modo possiamo ridefinire l'implementazione solo di windowClosing. Posso farlo in vari modi: (1) Definire una classe a parte per gestire l'evento: class CloseWindowListener extends WindowAdapter { public void windowClosing(WindowEvent e) { System.exit(0); } } e poi chiamare sul frame: addWindowListener (new CloseWindowListener()); La classe ausiliaria CloseWindowListener puo' essere definita o nel corpo della classe principale, oppure fuori ma nello stesso file, oppure in un altro file. Se nello stesso file non deve essere dichiarata "public". Se in un altro file java lo trova da se' (purche' sia nella stessa directory), non occorre fare linking come in C. (2) Fare tutto in un colpo, chiamando sul frame: addWindowListener (new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } }); questo associa al frame un'istanza di WindowAdapter dove pero' il metodo windowClosing e' ridefinito nel modo indicato. (3) Fare si' che la classe principale che sto definedo estenda WindowAdapter, definendo nel suo corpo il metodo windowClosing. Poi si chiama: addWindowListener(this); Questo non sempre e' possibile. Per es. se gia' la mia classe estende Frame, non puo' estendere anche WindowAdapter (in java una classe non puo' estenderne due, si dice con termine object oriented che java ha ereditarieta' singola). Una soluzione e' questa: fare si' che estenda Frame e implementi WindowListener (una classe puo' estendere una sola classe ma non c'e' limite a quante interfacce puo' implementare). Pero' in questo caso devo ridefinire non solo windowClosing, ma anche tutti gli altri metodi di WindowListener (semplicemente come {}, cioe' che non facciano nulla). COMMENTI ALL'ESERCIZIO 2 ===Progettazione del controllo=== La reazione agli eventi determina il comportamento dell'interfaccia. Per dare all'interfaccia un comportamento coerente bisogna prima definire bene: - lo stato interno: quali variabili lo descrivono, quali sono le configurazioni e i cambiamenti di stato legali - le azioni (sarebbero gli eventi) che cambiano lo stato: in quale stato un'azione e' legale, come modifica lo stato Nel caso della calcolatrice, lo stato interno puo' essere descritto da: - il numero che compare sul display: se e' appena stata fatta un'operazione, questo e' il risultato dell'operazione appena eseguita e diventera' il primo operando della prossima operazione; altrimenti e' il secondo operando per la prossima operazione, mentre il primo operando deve essere stato salvato da quanche parte - il numero corrispondente al primo operando che e' stato salvato - il tipo della prossima operazione da eseguire, se l'utente l'ha gia' specificato - la fase di esecuzione, che puo' essere una delle seguenti: fase1 = non ho ne' operandi ne' operazione fase2 = ho il primo operando fase3 = ho il primo operando e l'operazione fase4 = ho il primo operando, l'operazione e il secondo operando Esempio: 0- inizio (il display e' vuoto) A- l'utente immette 23 (il sistema mostra 23) B- l'utente preme + (il sistema cancella il display) C- l'utente immette 10 (il sistema mostra 10) D- l'utente preme = (il sistema mostra 33) E- l'utente preme + (il sistema cancella il display) F- l'utente immette 7 (il sistema mostra 7) G- l'utente preme = (il sistema mostra 40) Se chiamiamo numero_mostrato, numero_precedente e prossima_operazione le tre variabili di stato descritte sopra, abbiamo: 0- numero_mostrato=0, numero_precedente=0, prossima_operazione=nessuna (fase1) A- numero_mostrato=23, numero_precedente=0, prossima_operazione=nessuna (fase2) B- numero_mostrato=0, numero_precedente=23, prossima_operazione=+ (fase3) C- numero_mostrato=10, numero_precedente=23, prossima_operazione=+ (fase4) D- numero_mostrato=33, numero_precedente=0, prossima_operazione=nessuna (fase2) E- numero_mostrato=0, numero_precedente=33, prossima_operazione=+ (fase3) F- numero_mostrato=7, numero_precedente=33, prossima_operazione=+ (fase4) G- numero_mostrato=40, numero_precedente=0, prossima_operazione=nessuna (fase2) Le azioni previste nella calcolatrice sono: 1- immissione di una cifra (aggiunge la cifra al display) 2- pressione di un tasto operazione +,-,*,/ (cancella il display, se avevo un'operazione pendente la esegue) 3- pressione del tasto = (esegue l'operazione pendente e mostra il risultato) L'azione 1 e' possibile in ogni fase (se in fase1 porta in fase2, se in fase3 porta in fase4, altrimenti lascia la fase invariata): aggiorna numero_mostrato concatenando la cifra. L'azione 2 e' possibile solo in fase2 e in fase4 (e porta in fase3): - in fase2: salva numero_mostrato in numero_precedente, azzera numero_mostrato, registra l'operazione in prossima_operazione. - in fase4: applica l'operazione memorizzata in prossima_operazione agli operandi numero_precedente e numero_mostrato, salva il risultato in numero_precedente, azzera numero_mostrato, registra l'operazione in prossima_operazione. L'azione 3 e' possibile solo in fase4 (e porta in fase2): applica l'operazione memorizzata in prossima_operazione agli operandi numero_precedente e numero_mostrato, salva il risultato in numero_precedente, aggiorna numero_mostrato ponendolo uguale al risultato. Abbiamo visto che ci sono configurazioni dello stato (fasi) in cui certe azioni non sono legali (es. premere = in una fase in cui non ho l'operazione oppure non ho entrambi gli operandi). Abbiamo varie scelte per questi casi: - disabilitare il tasto con setEnabled(false), e poi riabilitarlo con setEnabled(true) quando lo stato cambia e l'azione diviene legale. - lasciarlo abilitato e reagire mostrando sul display un codice di errore se viene premuto. - lasciarlo abilitato ma non eseguire l'azione corrispondente (pratica sconsigliata, non da' feedback e confonde all'utente). In modo analogo si gestiscono i tasti Cancel (corrisponde ad azzeramento del display e ritorno allo stato iniziale) e Ok (che puo' essere usato per far sparire dal display i messaggi di errore, oppure puo' essere reciclato trasformandolo nel tasto Exit per terminare il programma. Questa progettazione a tavolino del controllo del programma e' necessaria e indispensabile! Solo dopo si puo' passare a scrivere codice. ===Definizione delle azioni=== Per associare un'azione ai bottoni occorre catturare l'evento ActionEvent (evento azionamento del bottone) associando un ActionListener a ciascun bottone. Anche qui (come visto per il WindowEvent) abbiamo varie possibilita': (1) definire una classe a parte che implementi ActionListener, creare un oggetto di quella classe e associarlo al bottone (2) definire una classe che implementi ActionListener e un nuovo oggetto di quella classe associandolo al bottone in un colpo solo (3) fare si' che la mia classe principale implementi ActionListener, poi associare this al bottone Notare che ActionListener e' un'interfaccia con un solo metodo, actionPerformed, quindi (a differenza di WindowListener) non ha un ActionAdapter (perche' non serve). Si puo' definire un unico ActionListener per tutti i bottoni. Dentro actionPerformed si risale a quale bottone e' stato premuto applicando all'evento il metodo: - getSource() che ritorna l'oggetto su cui e' avventuo l'evento, siccome sappiamo che e' un bottone possiamo fare un cast per convertirlo in bottone: Button b = ev.getSource(); dove ev e' l'evento, e poi confrontarlo con i bottoni - oppure getActionCommand() che ritorna la stringa etichetta del bottone premuto, poi bisogna confrontarla con le etichette dei bottoni A seconda del bottone premuto e si eseguono poi le operazioni opportune. Vi forniamo come esempio una versione semplificata di calcolatrice con SOLO L'OPERAZIONE SOMMA, SENZA I TASTI OK E CANCEL e SENZA IL NOME DEL GRUPPO.