MokaByte 54 - Luglio/Agosto 2001
Foto dell'autore non disponibile
di
Andrea Gini
Sviluppo della GUI in applicazioni 
Java stand alone 
Parte V: aggiunta di nuove funzioni
Nel mondo frenetico dell'informatica personale chi si ferma è perduto. Il progresso incalza, e anche per il piccolo editor di questa rubrica e' giunto il momento di una seconda release. Dopo una serie di interventi tesi unicamente a migliorare l'intergrità strutturale dell'applicazione, questo mese procederemo finalmente all'aggiunta di una nuova funzionalità, cogliendo in tal modo il pretesto per ripassare tutti gli argomenti affrontati fino ad ora

Negli articoli precedenti, abbiamo introdotto alcune tecniche di programmazione e di ingegnerizzazione del software che permettono di raggiungere una maggiore efficienza nelle fasi della manutenzione del codice. Per confermare l'importanza dei concetti esposti precedentemente, andremo ora a sviluppare una funzionalita' estremamente utile nell'elaborazione dei testi: il Find.

La sintassi del Find e' abbastanza diversa rispetto a quella dei comandi trattati fino ad ora, dal momento che richiede una modalita' di interazione con l'utente di gran lunga piu' complessa. Malgrado tali differenze, non sarà necessario stravolgere l'architettura dell'applicazione per aggiungere questa funzionalità.
 
 

La Sintassi del Find
Durante la stesura di un testo, torna sempre utile una funzione di ricerca. La classica modalita' di interazione avviene attraverso una finestra di dialogo non modale, vale a dire una finestra che non blocca il frame principale, lasciando libero l'utente di alternare le operazioni di ricerca con le  normali operazioni di editing. La finestra di dialogo deve contenere un campo di testo, un pulsante "Find Next" associato al Text Field ed un pulsante "Cancel" per la chiusura. Mentre si inserisce il testo nell'apposita casella, il cursore si sposta sulla prima occorrenza della parola cercata (ricerca incrementale). Premendo il pulsante "Find Next" (o premendo invio nel campo di testo), il cursore viene posizionato sull'occorrenza successiva. La ricerca deve avvenire in modo circolare a partire dalla posizione corrente del cursore: prima si esplora il testo che segue il cursore, quindi quello che lo precede.
 


Figura 1 - La finestra di dialogo del Find

La realizzazione di pannelli di dialogo solitamente non richiede le stesse attenzioni necessarie per sviluppare l'applicazione principale. Nella maggior parte dei casi i pannelli di dialogo rientrano nella casistica di "applicazioni mono-uso di piccole dimensioni", nelle quali una codifica sintetica puo' risultare preferibile ad una modulare. Tuttavia la realizzazione di un simile pannello offre il pretesto per osservare una variante del Framework descritto negli articoli precedenti, cosa che si rivelerà molto utile in seguito.
 
 
 

Costruiamo la finestra di dialogo
Per costruire una finestra di dialogo occorre definire un'opportuna sottoclasse di JDialog, che nel nostro caso chiameremo FindDialog. Una finestra di dialogo deve essere associata ad un JFrame, che deve essere fornito al momento della costruzione. Il costruttore di FindDialog assomiglia molto a quello del TextEditor, dal momento che le fasi di costruzione individuate sembrano coprire una casistica piuttosto ampia:

  public FindDialog(TextEditor owner) {
    super(owner);
    this.target = owner;
    setupFrame();
    setupFrameBehaviour();
    setupComponents();
    buildGui();
    registerListeners(); 
    disableFind();
  }

Il codice dei primi tre metodi non presenta particolari difficolta', e per questa ragione non verrà trattato. La parte più interessante si trova senza dubbio negli ultimi due metodi, che si occupano di impostare gli ascoltatori di questa interfaccia utente.

  protected void registerListeners() {
    ActionListener nextListener = new NextButtonListener();
    nextButton.addActionListener(nextListener);
    findTextField.addActionListener(nextListener);

    cancelButton.addActionListener(new CancelButtonListener());

    findTextField.getDocument().addDocumentListener(
     new TextFieldDocumentListener());
  }

Nelle prime tre righe viene creato un ActionListener che si occupa di associare una funzione al pulsante "Find Next" (quello con il canocchiale con sopra una freccia): lo stesso ascoltatore viene impostato come ActionListener per il campo di testo: in questo modo la pressione del tasto "Invio" svolge la stessa funzione del tasto "Find Next". L'ascoltatore si limita a chiamare il metodo findNext() che verrà descritto in seguito:

  class NextButtonListener implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      findNext();
    }
  }

Nella riga successiva viene associato un ActionListener al pulsante "Cancel": tale ascoltatore, che si limita a rendere invisibile la finestra di dialogo, non necessita di ulteriori approfondimenti.

L'ultima riga associa al campo di testo (o, per meglio dire, al suo Document) un ascoltatore che, ad ogni inserimento o rimozione di un carattere, chiama la funzione findCurrent(): questo ascoltatore rende possibile la ricerca incrementale.

  class TextFieldDocumentListener implements DocumentListener {
    public void changedUpdate(DocumentEvent e) {}
    public void insertUpdate(DocumentEvent e) {
      findCurrent();
    }
    public void removeUpdate(DocumentEvent e) {
      findCurrent();
    }
  } 

L'ultima riga del costruttore effettua una chiamata al metodo disableFind(): tale metodo disabilita il pulsante "Find Next" e colora di rosso il testo all'interno del TextField, per segnalare all'utente che la stringa inserita non e' presente nel testo

  protected void disableFind() {
    findTextField.setForeground(Color.red);
    nextButton.setEnabled(false);
  }


Figura 2 - Un uso corretto della programmazione ad eventi permette di 
fornire un utile feedback all'utente: il testo si colora di rosso quando 
non esistono occorrenze nel testo esaminato

Questo metodo, in seguito, verra' chiamato dal metodo findCurrent() ogni volta che la ricerca non ha dato un buon esito: in questa maniera viene offerto all'utente un utile feedback operativo. Ogni volta che la ricerca va a buon fine, viene chiamato il metodo speculare, enableFind(), che esegue l'operazione inversa:

  protected void enableFind() {
    findTextField.setForeground(Color.black);
    nextButton.setEnabled(true);
  }
 

La semantica del Find
La sintassi del Find cosi' come l'abbiamo descritta e' abbastanza familiare; resta ora da individuarne la semantica funzionale: ancora una volta ci proponiamo di ottenere dei metodi che svolgano il compito desiderato a partire dai parametri necessari, e che segnalino con delle eccezioni gli eventuali malfunzionamenti.

Per individuare con chiarezza una funzione che descriva il Find, e' utile ribaltare il punto di vista: dal momento che il TextEditor incapsula un testo, la funzione Find deve a sua volta essere incapsulata. La maniera piu' naturale di esprimere la semantica del Find è attraverso una funzione del tipo:

"Vai alla prima occorrenza di della parola che ti indico a partire dall'attuale posizione del cursore, e avvertimi se non la trovi"

public void goToStringFromActualOffset(String toFind)
      throws NotFoundException

NotFoundException e' una eccezione sviluppata apposta per l'occasione. Le eccezioni sono tra le classi piu' semplici da creare: nella maggior parte dei casi e' sufficiente definire una sottoclasse vuota di Exception, il cui nome rifletta una particolare condizione di errore:

public class NotFoundException extends Exception {
}

Il Find Next e il Find Incrementale hanno una semantica differente: il primo puo' essere espresso da una funzione del tipo:

"Vai alla prima occorrenza della parola che ti indico a partire dalla posizione successiva a quella in cui ora si trova il cursore, e avvertimi se non la trovi"

public void goToStringFromNextOffset(String toFind)
      throws NotFoundException{}

Il pannello di dialogo si comportera' come un front end a queste funzioni, come si vede nel diagramma successivo.


Figura 3 - Interazione tra FindDialog e TextEditor durante una ricerca

La tipica chiamata a goToStringFromNextOffset() si svolgerà nel modo seguente:

  public void findNext() {
    try {
      target.goToStringFromNextOffset(findTextField.getText());
    }
    catch(NotFoundException ex) {
      JOptionPane.showMessageDialog(this,
        "Not Found",
        "String Not Found",
        JOptionPane.WARNING_MESSAGE); 
    } 
  }

come negli esempi della volta scorsa, si inserisce la chiamata in un blocco try, mentre il blocco catch gestisce l'eventuale situazione di eccezione.
 
 
 

Un occhio all'implementazione
L'implementazione della funzione Find offre il pretesto di osservare come una funzionalità complessa possa essere sviluppata grazie ad un insieme di metodi piccoli e specializzati, piuttosto che attraverso un unico metodo lungo di uso generale.

I metodi goToStringFromActualOffset() e goToStringFromNextOffset() possono essere considerati due casi particolari del metodo

  public void goToString(String toFind,int startOffset)
   throws NotFoundException,BadLocationException

che posiziona il cursore sulla prima occorenza della stringa specificata dal primo parametro, effettuando una ricerca circolare a partire dall'offset specificato dal secondo parametro. Se l'indice è troppo grosso o negativo, viene lanciata una BadLocationException. Ecco di seguito l'implementazione dei due metodi goToStringFromActualOffset() e goToStringFromNextOffset() in funzione di goToString(String toFind,int startOffset).

  public void goToStringFromActualOffset(String toFind)
      throws NotFoundException {
    int fromIndex = getEditor().getSelectionStart();
    try {
      goToString(toFind,fromIndex);
    }
    catch(BadLocationException ble) {}
  }

Nella prima riga viene interrogato il TextComponent per conoscere la posizione di inizio della ricerca, quindi viene effettuata la chiamata a goToString() a partire da quell'indice. Il blocco catch è vuoto, dal momento che la posizione specificata è sicuramente legale, essendo stata ottenuta attraverso un'interrogazione al TextComponent stesso. 

  public void goToStringFromNextOffset(String toFind)
      throws NotFoundException {
    int fromIndex = getEditor().getSelectionStart();
    int length = getEditor().getDocument().getLength();
    try { 
      goToString(toFind,(fromIndex+1)%length);
    }
    catch(BadLocationException ble) {} 
  }

Questo metodo e' simile al precedente, tranne che in questo caso l'indice viene ricavato con un'operazione di modulo sulla lunghezza del testo. L'operazione di modulo, caratterizzata in Java dal simbolo "%", garantisce che, una volta raggiunta l'ultima posizione sul testo, la ricerca riparta da zero.

Il metodo goToString(String toFind,int startOffset) puo' a sua volta essere visto come un caso particolare di un metodo 

public void goToString(String toFind,int startOffset,int endOffset)
    throws NotFoundException,BadLocationException 

che limita la ricerca al range specificato dai parametri startOffset e endOffset.

Disponendo di un simile metodo, possiamo descrivere il precedente in questa maniera:

  public void goToString(String toFind,int startOffset)
   throws NotFoundException,BadLocationException {
    int endOffset = getEditor().getDocument().getLength();
    try {
      goToString(toFind,startOffset,endOffset); 
    }
    catch(NotFoundException nfe) {
      goToString(toFind,0,startOffset);
   // throws NotFoundException
    }
  }

Il blocco try tenta di posizionare il cursore sulla prima occorrenza della stringa analizzando il testo che segue il cursore; se questa ricerca fallisce, il blocco catch tenta con la meta' superiore del testo. Se anche questa ricerca fallisce, la NotFoundException viene fatta rimbalzare fuori dal metodo.

Come si puo' implementare il metodo goToString(String toFind,int startOffset,int endOffset)? Lo scaricabarile finisce qui: all'interno di questo metodo viene praticata la ricerca vera e propria e il corrispondente riposizionamento del cursore

  public void goToString(String toFind,int startOffset,int endOffset)
    throws NotFoundException,BadLocationException {
    if(toFind.length()==0)
      throw new NotFoundException();

    int length = endOffset-startOffset;
    String text = 
 getEditor().getDocument().getText(startOffset,length);
      // throws BadLocationException

    int index = text.indexOf(toFind);
    if(index!=-1) {
      getEditor().requestFocus();
      getEditor().select(startOffset+index,
   startOffset+index+toFind.length());
      return;
    }
    else 
      throw new NotFoundException(); 
  } 


Figura 4 - La parola cercata viene evidenziata all'interno dell'Editor 

Le prime due righe verificano che la stringa da cercare non sia vuota: in un simile caso la ricerca viene interrotta e viene sollevata una NotFoundException. Nelle righe successive viene prelevato il testo compreso tra gli indici specificati, ricorrendo al metodo getText() di JTextComponent. Questo metodo, nel caso si specifichino degli indici illegali, genera una BadLocationException: in questo caso l'esecuzione del metodo goToString() viene interrotta, e l'eccezione viene fatta rimbalzare al chiamante. Infine viene praticata la ricerca vera e propria, ricorrendo ai metodi di ricerca su stirnghe: se la ricerca va a buon fine, la parola cercata viene evidenziata; in caso contrario viene sollevata una NotFoundException.
 
 
 

Integrazione del comando all'interno dell'editor
Una volta definita la sintassi e la semantica del comando e' un gioco da ragazzi integrarlo nell'editor preesistente. Per aggiornare l'interfaccia grafica e' sufficiente modificare i metodi

  setupComponents()
  buildMenuItems()
  buildButtons()
  createMenuBar()
  createToolBar()

L'aggiunta non ha creato disordine nel sorgente, che rimane equivalente a quello presentato il mese scorso.

Per rendere attivo il pulsante "Find" bisogna agire sui metodi

  registerListeners()
  actionPerformed(ActionEvent ae)

facendo in modo che, alla pressione del pulsante venga chiamato il seguente metodo:

  public void promptFind() {
    findDialog.setVisible(true);
  }
 

Conclusioni
In questo articolo abbiamo studiato l'integrazione di una nuova funzionalità nel nostro editor, assecondando il design preesistente. Questo lavoro ha permesso di mettere in luce come alcune precise scelte di design possano condizionare la manutenzione e la rielaborazione del codice. Il mese prossimo proseguiremo con l'integrazione di una nuova funzionalità, che ci permetterà di mettere in luce un'altro importantissimo aspetto della Programmazione ad Oggetti: il riuso.

L'esempio con i sorgenti può essere trovato qui

Vai alla Home Page di MokaByte
Vai alla prima pagina di questo mese


MokaByte®  è un marchio registrato da MokaByte s.r.l.
Java®, Jini®  e tutti i nomi derivati sono marchi registrati da Sun Microsystems; tutti i diritti riservati
E' vietata la riproduzione anche parziale
Per comunicazioni inviare una mail a
mokainfo@mokabyte.it