MokaByte Numero  46  - Novembre 2000
Corso di Swing
III parte
di 
Andrea Gini
La gestione degli eventi

L'uso di interfacce grafiche ha introdotto un grosso cambiamento nella programmazione. Prima dell'avvento della grafica, i programmi interattivi erano organizzati a menu, e l'utente era costretto a seguire una interazione dall'inizio alla fine, per poter segnalare ad ogni interruzione quale fosse la strada da seguire. L'uso di finestre, mouse e pulsanti ha introdotto un modello di programmazione "Event Driven", in cui il programma principale resta in uno stato di attesa, fino a quando l'utente, utilizzando il mouse o la tastiera, genera un evento che provoca un determinato effetto. In questo articolo vedremo come implementare in java il controllo degli eventi.

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.

 

Chi volesse mettersi in contatto con la redazione può farlo scrivendo a mokainfo@mokabyte.it