Il
modello ad Eventi
In
java, ogni oggetto grafico e' predisposto a ricevere un certo numero di
eventi dall'utente: un pulsante puo' essere premuto, una finestra puo'
essere chiusa, il mouse puo' essere spostato e cosi' via. La gestione di
questi eventi segue un modello definito in letteratura "Event Delegation",
in cui il componente grafico delega ad un ascoltatore l'azione da svolgere.
Un pulsante non sa cosa avverra' alla sua pressione: esso si limita a notificare
i propri ascoltatori che l'evento che essi attendevano e' avvenuto, e questi
provvederanno ad eseguire l'azione appropriata.
Si
noti che ogni componente grafico puo' avere piu' di un ascoltatore per
un determinato evento: in questo caso essi saranno chiamati uno per volta
secondo l'ordine in cui si sono registrati. Se l'utente cerca di scatenare
un nuovo evento prima che il precedente sia stato consumato (ad esempio
premendo ripetutamente un pulsante), ogni nuovo evento verra' bufferizzato,
e l'azione corrispondente sara' eseguita solo al termine della precedente.
Il Pattern Observer-Observable
Il
modello "Event Driven" e' una implementazione del pattern Observer-Observable,
lo schema di progettazione descritto dal diagramma in Figura 1. Abbiamo
un oggetto in grado di scatenare un determinato tipo di evento, a cui diamo
il nome Observable per sottolineare la sua caratteristica di poter essere
osservato.
Esso
mantiene una lista di ascoltatori, definiti dall'interfaccia Observer,
all'interno di un vettore listeners: un ascoltatore puo' registrarsi presso
Observable usando il metodo addListener(), mentre si esclude dalla lista
degli ascoltatori chiamando removeListener().
Ogni
volta che capita un evento, Observable chiama il metodo protetto processEvent(),
che esegue il metodo actionPerformed() su tutti gli ascoltatori presenti
nel Vector listeners; la notifica prevede lo scambio di oggetti Event,
che sono in grado di fornire importanti dettagli sull'evento generato.
L'ascoltatore
viene definito come un' interfaccia java; questa scelta pone al programmatore
un vincolo riguardo a come scrivere un ascoltatore, ma lascia la piu' completa
liberta' su cosa l'ascoltatore debba fare. Possiamo scrivere una classe
che stampa messaggi sullo schermo, un altra che faccia comparire una finestra
di dialogo, una che colora lo schermo di bianco e una che termina il programma;
possiamo persino scrivere una classe che non fa niente: se queste classi
implementano l'interfaccia Observer, e descrivono il proprio comportamento
all'interno del metodo actionPerformed(), esse risulteranno essere ascoltatori
validi per l'oggetto Observable . Nel diagramma vediamo la classe ConcreteObserver,
una possibile implementazione dell'interfaccia Observer.
|
Figura
1 - Il Pattern Observer Observable
Un esempio concreto
Come
abbiamo detto in precedenza, ogni Component e' in grado di notificare gli
eventi generati dal mouse; in particolare, e' predisposto per lavorare
con due tipi di ascoltatori: MouseListener e MouseMotionListener. Il primo
e' interessato principalmente alla pressione dei pulsanti del mouse, il
allo spostamento del puntatore: come si puo' vedere in Figura 2, la relazione
tra questi oggetti segue fedelmente il modello Observer-Observable, con
la variante che in questo caso sono presenti due ascoltatori, ognuno specializzato
in un evento (il fatto che un solo oggetto MouseEvent venga utilizzato
da entrambi gli ascoltatori non deve causare confusione, dal momento che
le informazioni che esso trasporta risultano utili in entrambi casi).
|
Figura
2 - La relazione tra Component e i suoi ascoltatori
ricalca
fedelmente il pattern Observer-Observable
Possiamo
scrivere un semplice programma di disegno usando tre classi:
-
una classe Painter, sottoclasse di JPanel, che costituisce la classe principale
del programma.
-
una classe mMotionListener che implementa l'interfaccia MouseMotionListener
-
una classe mListener che implementa l'intefaccia MouseListener.
Il
costruttore della classe principale crea un'istanza per ognuno dei due
ascoltatori, e li registra usando i metodi addMouseMotionListener(MouseMotionListener)
e addMouseListener(MouseListener).
MouseListener ml = new mListener();
addMouseListener(ml);
MouseMotionListener mml = new mMotionListener();
addMouseMotionListener(mml);
Dato
lo stretto legame che esiste tra la classe principale e i due ascoltatori,
essi sono stati definiti come classi interne, cioe' classi che vengono
dichiarate all'interno della classe principale, e che possono essere istanziate
direttamente solo dalla classe che funge da involucro. Il funzionamento
del programma e' abbastanza semplice: la classe mListener si occupa di
attivare o disattivare la modalita' di disegno, a seconda che si prema
o si rilasci il pulsante del mouse, mentre la classe mMotionListener mantiene
aggiornate due coppie di coordinate, una corrispondente alla posizione
attuale del mouse, l'altra alla posizione immediatamente precedente. Il
metodo paint(Graphics g), presente nella classe principale, disegna
una linea retta tra queste due coppie di coordinate nel caso ci si trovi
in modalita' di disegno.
import
javax.swing.*;
import
java.awt.*;
import
java.awt.Color.*;
import
java.awt.event.*;
public
class Painter extends JPanel {
private int startXPoint = 0; // coordinata x di partenza
private int endXPoint = 0; // coordinata x di arrivo
private int startYPoint = 0; // coordinata y di partenza
private int endYPoint = 0; // coordinata y di arrivo
private boolean paint = false;
// ascoltatore del moto del mouse
class mMotionListener implements MouseMotionListener {
public void mouseDragged(MouseEvent e) {
// aggiorna le coordinate
startXPoint = endXPoint;
startYPoint = endYPoint;
endXPoint = e.getX();
endYPoint = e.getY();
// disegna
Painter.this.repaint();
}
public void mouseMoved(MouseEvent e) {
// aggiorna le coordinate
startXPoint = endXPoint;
startYPoint = endYPoint;
endXPoint = e.getX();
endYPoint = e.getY();
}
}
// ascoltatore dei pulsanti del mouse
class mListener implements MouseListener {
// i seguenti metodi sono richiesti dalla
// interfaccia MouseListener anche se non
// vengono utilizzati
public void mouseClicked(MouseEvent e) {}
public void mouseEntered(MouseEvent e) {}
public void mouseExited(MouseEvent e) {}
// pulsante premuto
public void mousePressed(MouseEvent e) {
// attiva la modalita' di disegno
paint = true;
}
// pulsante rilasciato
public void mouseReleased(MouseEvent e) {
// disattiva la modalita' di disegno
paint = false;
}
}
// Costruttore della classe principale
public Painter() {
super();
MouseListener ml = new mListener();
addMouseListener(ml);
MouseMotionListener mml = new mMotionListener();
addMouseMotionListener(mml);
}
public void paint(Graphics g) {
// se e' attiva la modalita' di disegno, traccia una riga
if(paint)
g.drawLine(startXPoint,startYPoint,endXPoint,endYPoint);
}
public
static void main(String argv[]) {
Painter p = new Painter();
JFrame f = new JFrame("Painter");
f.getContentPane().add(p);
f.setSize(400,300);
f.setVisible(true);
}
}
|
Figura
3 - Con poche righe di codice possiamo dare sfogo al nostro estro
creativo
E'
importante notare che, all'interno del programma, non esistono chiamate
esplicite ai metodi delle classi mListener e mMotionLisener: questi metodi
vengono chiamati durante l'esecuzione del programma, nel momento in cui
l'utente agisce sul mouse. Quando il puntatore e' fermo, il programma non
fa niente: resta semplicemente in attesa che l'utente interagisca
con il mouse, scatenando gli eventi a cui gli ascoltatori sono interessati.
Uso di Adapter
nella definizione degli ascoltatori
Nel
programma di esempio abbiamo realizzato due ascoltatori, un MouseListener
e un MouseMotionListener creando due classi che forniscono l'implementazione
delle relative interfacce. Quando si cerca di compilare una classe che
implementa una determinata interfaccia, il compilatore controlla
che essa definisca tutti i metodi richiesti dall'interfacia, compresi quelli
che non vengono utilizzati. La classe mListener dell'esempio, sebbene utilizzi
solamente due dei cinque metodi forniti dall'interfaccia, deve per forza
dichiarare anche quelli che non usa, ricorrendo ad un'implementazione vuota,
vale a dire una coppia di parentesi graffe senza istruzioni all'interno.
public
void mouseClicked(MouseEvent e) {}
public void mouseEntered(MouseEvent e) {}
public void mouseExited(MouseEvent e) {}
Nei
casi come questo possiamo ricorrere agli Adapter, cioe' classi di libreria
che forniscono un'implementazione vuota un determinato ascoltatore. Se
definiamo mListener come sottoclasse di MouseAdapter, il compilatore accettera'
che si definisca un numero di metodi inferiore rispetto a quelli dell'interfaccia,
dal momento che la superclasse MouseAdapter fornisce un'implementazione
vuota dei metodi mancanti.
class mListener extends MouseAdapter {
public void mousePressed(MouseEvent e) {
// attiva la modalita' di disegno
paint = true;
}
public void mouseReleased(MouseEvent e) {
// disattiva la modalita' di disegno
paint = false;
}
}
|
Figura
4 - Gerarchia di MouseAdapter
Nei
package java.AWT.event e javax.swing.event troviamo Adapter per ogni ascoltatore
con piu' di un metodo: la classe MouseAdapter, ad esempio, fornisce un'implementazione
vuota dell'interfaccia MouseListemer, mentre la classe MouseMotionAdapter
fornisce un'implementazione vuota dell'interfaccia MouseMotionListemer.
L'uso di Adapter rende il codice degli ascoltatori piu' snello, e quindi
piu' facile da mantenere.
Uso di classi
anonime nella definizione degli ascoltatori
Esiste
un metodo molto sintetico per creare ascoltatori, che prevede il ricorso
alle classi anonime, cioe' classi prive di nome che vengono definite ed
usate nello stesso momento. Ecco un esempio di poche righe, che mostra
come creare un pulsante e aggiungervi un ActionListener
JButton
okButton = new JButton("OK");
okButton.addActionListener(new
MouseAdapter() {
public void actionPerformed(ActionEvent e) {
try {
System.exit(0);
}
catch (Exception ex) {}
}
});
Nella
seconda riga vediamo che, all'interno del metodo addActionListener viene
creata un'istanza di una classe di tipo ActionListener, il cui codice e'
contenuto tra le parentesi graffe che seguono; tale classe e' anonima,
in quanto non ne viene definito il nome ma solamente il tipo. Si noti che
nell'ultima riga troviamo prima la chiusura della parentesi graffa, quindi
la chiusura della tonda e infine il punto e virgola: questa apparente stranezza
si spiega col fatto che le righe tra la seconda e l'ottava sono interamente
comprese all'interno del metodo addActionListener().
L'uso
di classi anonime e' di grande aiuto quando si desidera sviluppare rapidamente
programmi brevi dotati di pochi controlli; nel caso di programmi di dimensioni
maggiori e' preferibile ricorrere alla definizione esplicita delle classi
destinate all'ascolto, per rendere il codece del programma piu' chiaro
e facile da mantenere.
Conclusioni
In
questo articolo abbiamo introdotto il modello ad eventi, un elemento indispensabile
per dar vita ai programmi grafici. Dal prossimo articolo affronteremo lo
studio dei controlli Swing e dei rispettivi ascoltatori.
Appendice: le
classi interne e le classi anonime
A
partire dal JDK1.1 nel linguaggio java sono state introdotte le Inner
Classes, o classi interne, con il preciso obiettivo di semplificare l'implementazione
del modello ad eventi di AWT. Una Inner Class viene dichiarata all'interno
di un'altra classe (addirittura all'interno di un metodo), e possiede una
visibilita' su tutti gli attributi e i metodi della classe ospite, compresi
quelli privati e protetti.
Le
classi anonime sono classi interne che vengono dichiarate ed utilizzate
nello stesso momento. Il loro uso principale e' in contesti "usa e getta"
in cui un oggetto e' talmente specializzato da non incoraggiare la creazione
di una classe.
Per
non alterare le specifiche della Virtual Machine, le Inner Classes vengono
compilate in file .class che hanno lo stesso nome della classe ospite con
una numerazione progressiva che usa il carattere "$" come separatore.
|