MokaByte 55 - 7mbre 2001
Foto dell'autore non disponibile
di
Andrea Gini
Sviluppo della GUI in applicazioni 
Java stand alone 
Parte VI
Aggiunta di nuove funzionalità: 
la pratica sistematica del riuso
Prosegue questo mese il lavoro sul Text Editor. Dopo aver aggiunto una funzionalità di ricerca, è il momento di procedere allo sviluppo del Replace, uno strumento estremamente utile in qualunque Editor di testo. Rispetto al lavoro presentato il mese scorso, questo esempio introduce un ulteriore grado di complessità nel dialogo con l'utente: la realizzazione di questo nuovo strumento permetterà di illustrare nuovi aspetti delle tecniche di sviluppo trattate fino ad ora

Un'altra funzionalità essenziale per un editor di testo è il Replace, che offre la possibilità di sostituire, all'interno di un testo, tutte le occorrenze di una certa parola con un'altra, scelta dall'utente. Questa funzionalità può essere sviluppata rapidamente a partire dalle classi realizzate il mese scorso. L'esistenza di numerosi punti di contatto tra i due esempi offre il pretesto per illustrare come una certa disciplina nella stesura del codice favorisca ampliamente diverse forme di riuso.
 
 
 

Find & Replace: modalità di interazione con l'utente
Per offrire una funzionalità tipo Find & Replace, è necessario instaurare un'interazione User Driven di una certa complessità; il normale svolgimento dell'operazione richiede ben quattro passaggi:
 

  • l'inserimento, tramite un campo di testo, di una stringa da ricercare e di una con cui sostituirla
  • l'individuazione, all'interno del testo, della prima occorrenza della stringa da sostituire
  • la richiesta all'utente di scegliere una delle seguenti possibilità:
    • rimpiazza l'occorrenza attuale
    • rimpiazza tutte le occorrenze
    • vai alla successiva
    • termina l'operazione
  •  l'esecuzione dell'operazione scelta


Come si può notare, questa situazione prevede due passaggi di interrogazione utente, visibili al primo ed al terzo punto. Anche in questo esempio si vuole modellare l'interazione in un modo da rispettare la divisione tra lo strato di presentazione e quello di gestione dei dati, secondo la modalità illustrata nel precedente articolo. 
 
 


Figura 1 - In un'architettura a strati esiste una netta separazione tra la 
logica di sistema e la gestione della presentazione




Strato di presentazione: creazione del Frame di Dialogo principale
Per prima cosa procederemo a realizzare la finestra di dialogo con l'utente. Tale finestra deve contenere due campi di testo, uno per inserire la parola da ricercare ed uno per inserire quella da sostituire, e una coppia di pulsanti per attivare le due funzionalità.
 
 


Figura 2 - La finestra di dialogo per il Find & Replace può essere 
realizzata come estensione della finestra del Find 

Una simile finestra di dialogo può essere realizzata per specializzazione a partire da quella del Find, realizzata il mese scorso. Avendo adottato un'architettura modulare, e' possibile realizzarla con poche righe di codice, limitandosi a sovrascrivere i metodi che devono implementare un comportamento differente rispetto alla superclasse:

public class FindAndReplaceDialog extends FindDialog {

  private JTextField replaceTextField;
  private JButton replaceButton;

  public FindAndReplaceDialog(TextEditor owner) {
    super(owner);
  }
  protected void setupFrame() {
    setTitle("Find And Replace");
    setResizable(false);
  }
  protected Border createTitledBorder() {
    return BorderFactory.createTitledBorder("Find And Replace"); 
  }
  protected void setupComponents() { 
    super.setupComponents();
    replaceTextField = new JTextField(15); 
    replaceButton = new JButton(new ImageIcon("Replace24.gif")); 
  }
  protected Box createMainBox() {
    Box box = super.createMainBox();
      Box replaceBox = Box.createHorizontalBox();
      replaceBox.add(new JLabel("Replace with:"));
      replaceBox.add(replaceTextField);
      replaceBox.add(replaceButton);

    box.add(replaceBox);

    return box;
  }
  protected void registerListeners() {
    super.registerListeners();
    ActionListener replaceListener = new ReplaceButtonListener();
    replaceButton.addActionListener(replaceListener);
    replaceTextField.addActionListener(replaceListener);
  }
  protected void enableFind() {
    super.enableFind();
    replaceButton.setEnabled(true);
    replaceTextField.setEnabled(true);
  }
  protected void disableFind() {
    super.disableFind();
    replaceButton.setEnabled(false);
    replaceTextField.setEnabled(false); 
  }

  [......]

  class ReplaceButtonListener implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      replace();
    }
  }

  [....]
}

Nel sorgente di esempio, il costruttore si limita a richiamare quello della superclasse, che definisce un ordine preciso di chiamata dei metodi di set up. I metodi setupFrame() e createTitledBorder() sovrascrivono i corrispondenti metodi nella superclasse, in modo da impostare correttamente il nome del pannello. Nelle righe sucessive, vengono estesi alcuni metodi presenti nella superclasse, in modo da adattarli al nuovo ambiente: il metodo setupComponents() crea un nuovo campo di testo ed un nuovo pulsante; il metodo createMainBox() posiziona i nuovi componenti all'interno della finestra; registerListener() aggiunge un ascoltatore ai due componenti e cosi' via, il tutto senza essere costretti a duplicare il codice che imposta le dimensioni ed il layout della finestra.
 
 
 

La logica del Replace
Per implementare la logica della funzione Replace, è necessario definire alcuni metodi nuovi nella classe TextEditor. Tali metodi devono lavorare su una coppia di stringhe:  quella da ricercare, e quella da sostituire. Come nell'esempio del mese scorso, anche in questo caso è utile definire un insieme di metodi che offrano accesso alla stessa funzionalità, utilizzando nei diversi casi la possibilità di fornire un insieme di parametri diverso. Vediamo anzitutto come implementare un metodo "replace" che effettui la ricerca a partire dalla posizione attuale del cursore:

  public void replaceStringFromActualOffset(String toFind,
                               String toReplace)
                               throws NotFoundException {
    try {
      replaceStringFromOffset(toFind,
                               toReplace,
                               getEditor().getSelectionStart());
    }
    catch(BadLocationException e) {} // Non può capitare
  }

La richiesta specificata da questo metodo viene inoltrata al metodo replaceStringFromOffset, che come parametri riceve, oltre alle stringhe, un intero che specifica la posizione da cui iniziare la ricerca. Qualora la posizione specificata fosse illegale, viene generata una BadLocationException.

  public void replaceStringFromOffset(String toFind,
                                      String toReplace,
                                      int startOffset)
                                      throws NotFoundException,
                                      BadLocationException{

    goToString(toFind,startOffset);  // throws NotFoundException
    getEditor().replaceSelection(toReplace);
  }

Un'altra modalità di fruizione della funzionalità "Replace" è il "Replace All", che permette di sostituire tutte le occorrenze si una certa stringa con un'altra. Per implementare questa funzionalità è sufficiente spostare il cursore all'inizio del testo, e quindi avviare un ciclo che prosegue fino a quando tutte le occorrenze della stringa da cercare non siano state sostituite.

  public void replaceAll(String toFind,String toReplace) {
    getEditor().setCaretPosition(0);

    while(true) {
      try {
      replaceStringInRange(toFind,
                           toReplace,
                           getEditor().getCaretPosition(),
                           getEditor().getDocument().getLength());
      }
      catch(BadLocationException ble) {
        // non puo' capitare
      }
      catch(NotFoundException nfe) {
        // fine ricerca
       return; 
      }
    }

Questo metodo si appoggia su replaceStringInRange, che effettua la ricerca ed il rimpiazzo solamente nel range specificato dai parametri "startOffset" ed "endOffset":

  public void replaceStringInRange(String toFind,
                                   String toReplace,
                                   int startOffset,
                                   int endOffset)
                                   throws NotFoundException,
                                   BadLocationException  {
    goToString(toFind,startOffset,endOffset); 
               // throws NotFoundException
    getEditor().replaceSelection(toReplace); 
  }

E' interessante notare come l'implementazione di questi metodi ricorra ai metodi "goToString" definiti il mese scorso: questa forma di riutilizzo è resa possibile dal fatto di aver operato una valida separazione tra sintassi e semantica delle operazioni che concorrono a realizzare la funzionalità Find.
 
 
 

Mettiamo tutto assieme
Dopo aver illustrato la realizzazione del lato presentazione (la finestra di dialogo) e del livello logico (i comandi replaceXxx() di TextEditor), è arrivato il momento di mettere tutto insieme. Per concretizzare l'operazione di Replace, bisogna definire il metodo replace(), che viene chiamato alla pressione del pulsante "replace" del pannello di dialogo:

  private void replace() {
    while(true)
      try {
         String ft=getFindTextField().getText();
         getTarget().goToStringFromActualOffset(ft);
         int returnVal = promptReplaceAll();

         if(returnVal==REPLACE){
          String ft=getFindTextField().getText();
          String rt=replaceTextField.getText();
          getTarget().replaceStringFromActualOffset(ft,rt);
        }
        else if(returnVal==REPLACE_ALL) { 
        // Replace All
          String ft=getFindTextField().getText();
          String rt=replaceTextField.getText();
          getTarget().replaceAll(ft,rt);
          return;
        }
        else if(returnVal==NEXT){
           String ft=getFindTextField().getText();
           getTarget().goToStringFromNextOffset(ft);
         }
        else if(returnVal==CANCEL)
          return;
      }
      catch(NotFoundException ex) {
        JOptionPane.showMessageDialog(this,
          "Finished!",
          "Finished!",
          JOptionPane.WARNING_MESSAGE);
        return; 
      }
  }
  private int promptReplaceAll() {
    String ft=getFindTextField().getText();
    return JOptionPane.showOptionDialog(this, 
                   "Replace this occourrence of "+ft,
                   "Replace?", 
                   JOptionPane.DEFAULT_OPTION, 
                   JOptionPane.WARNING_MESSAGE,
                   null, options, options[0]); 
  }
 

Questo metodo avvia un ciclo, durante il quale vengono eseguite le seguenti operazioni:

  1. Il cursore viene portato sulla prima occorrenza della parola da cercare
  2. All'utente vene richiesto di scegliere tra le seguenti opzioni
    •  - rimpiazza l'occorrenza attuale della parola
    •  - rimpiazza tutte le occorrenze
    •  - vai alla successiva occorrenza
    •  - termina l'operazione

Figura 3 - Il Find & Replace richiede un secondo passaggio di interrogazione utente

Il ciclo termina in tre casi:

  1. Non ci sono più occorrenze disponibili, evento segnalato da una NotFoundException
  2. Viene eseguito un ReplaceAll
  3. L'utente preme il tasto CANCEL, ed interrompe esplicitamente l'interazione.



Figura 4 - Diagramma di interazione tra FindDialog e 
TextEditor durante un'operazione di Replace




Integrazione del comando all'interno dell'editor 
Ancora una volta, l'integrazione della nuova funzionalità richiede alcune modifiche ai metodi che effettuano il set up dell'applicazione, ossia ai metodi: 

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

Anche in questo caso, il progetto di base dell'applicazione ha mantenuto l'integrità originale, restando comunque aperto a nuove modifiche. In figura 5 possiamo avere una veduta d'insieme sulla struttura attuale del progetto.


Figura 5 - Il diagramma di classe completo dell'applicazione all'attuale stadio di sviluppo




Conclusioni
Questo mese abbiamo visto altre pratiche di riuso, utili durante lo sviluppo di programmi grafici. L'uso di queste tecniche può facilmente essere esteso a progetti su vasta scala, come vedremo a partire dal mese prossimo: è in arrivo una grossa novità per tutti i lettori di MokaByte, che risulterà particolarmente interessante a tutti quelli che hanno seguito con interesse questa rubrica.
 

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