MokaByte 50 - Marzo 2001
Foto dell'autore non disponibile
di
Andrea Gini
Sviluppo della GUI in applicazioni 
Java stand alone 
Parte I: uno sguardo agli errori piu' comuni
Il progetto dell'interfaccia grafica e' una fase cruciale nello sviluppo di un'applicazione Stand Alone per almeno una buona ragione: e' l'unica parte del programma visibile all'utente. Una simile realta' puo' provocare grosse frustrazioni agli sviluppatori: per quanto complesso sia il dominio applicativo di un programma, l'unico aspetto che la maggior parte degli utenti sara' in grado di apprezzare e' l'interfaccia grafica. Se forniamo ad un gruppo di persone una coppia di programmi simili dal punto di vista funzionale, vincera' quello dotato dell'interfaccia grafica migliore: basti pensare al successo di WinZip rispetto alle decine di programmi che svolgono esattamente la stessa funzione.

L'interfaccia grafica, al di la' dell'aspetto estetico, e' un elemento estremamente critico dal punto di vista strutturale: essa infatti rappresenta il punto di ingresso pubblico al codice applicativo, con tutte le implicazioni di sicurezza e di prestazioni che un simile fatto comporta. Capita infatti che un bug nell'interfaccia grafica permetta di violare, in una maniera non prevista, la sicurezza di un'applicazione, e capita anche piu' spesso che un programma grafico presenti un livello di performance inferiore ad un analogo strumento a console.

Negli ultimi dieci anni si e' progressivamente affermato un approccio visuale allo sviluppo delle interfacce grafiche: attraverso appositi tool di sviluppo, il programmatore puo' disegnare un'interfaccia, e abbinare delle operazioni ai controlli grafici. Il paradigma visuale ha il pregio di ridurre i costi di formazione e i tempi di sviluppo di semplici applicazioni a finestre; purtroppo ha anche il grandissimo difetto di spostare il centro di attenzione del programmatore dal "linguaggio di programmazione" al cosiddetto "ambiente di sviluppo", con la paradossale conseguenza di indurre migliaia di programmatori a confondere il concetto di "sviluppo di programmi grafici" con quello di "sviluppo visuale". In verita', se da una parte lo sviluppo visuale presenta una sintassi generalmente piu' agevole, dall'altra esso fornisce accesso ad un modesto sottoinsieme delle possibilita' offerte dalle librerie di programmazione grafica. In secondo luogo, tali strumenti tendono a legare il programmatore ad un'architettura software inefficiente dal punto di vista strutturale.

Questa serie di articoli vuole illustrare le basi dello sviluppo programmatico di applicazioni a finestre, descrivendo la nascita e l'evoluzione di una applicazione tipica. Attraverso tale sviluppo, sara' possibile individuare gli errori piu' comuni e individuare processi di ristrutturazione (Refactoring) che possono aiutare ad irrobustire il design di programmi gia' esistenti. Verranno inoltre illustrati alcuni esempi su come integrare i componenti Swing con il dominio applicativo del programma principale, e in una fase piu' avanzata verra' illustrato lo sviluppo di componenti grafici personalizzati. Gli esempi sono stati realizzati senza ricorrere a strumenti visuali; in ogni caso le tecniche di sviluppo presentate hanno una validita' abbastanza ampia, e pertanto potranno tornare utili anche agli utenti di simili ambienti di programmazione. 
 
 
 

Definiamo il problema
Vogliamo creare un editor di testo tipo Notepad, ossia un programma che permetta di caricare un file di testo, editarlo e salvarlo su disco. Un simile programma presenta un dominio applicativo molto semplice e familiare a chiunque utilizzi un computer; in secondo luogo e' un programma facile da realizzare in poche righe, cosa che ci permettera' di illustrarne diverse implementazioni senza complicazioni che distraggano dallo studio degli aspetti strutturali. Il ricorso ai componenti di testo Swing ci permette di trascurare le complesse problematiche legate all'editing vero e proprio; le operazioni sulle quali ci concentreremo inizialmente sono solamente cinque:

  • Open
  • Save
  • Cut
  • Copy
  • Paste


L'immagine seguente mostra uno screenshot di quanto vogliamo ottenere: scopriremo tuttavia che esiste piu' di un modo di ottenere lo stesso risultato, e che la scelta di un modello o di un altro puo' avere importanti conseguenze.
 
 


Figura 1 - Uno screenshot del nostro editor di testo 








Costruiamo l'interfaccia grafica
Innanzitutto cominciamo con il definire una sottoclasse di JFrame. Dal momento che lavoreremo con pulsanti, abbiamo bisogno di un ActionListener. In questo primo esempio definiamo la classe stessa come ascoltatore dei propri pulsanti

public class BadTextEditor extends JFrame implements ActionListener {

  public BadTextEditor() {

  }
  public void actionPerformed(ActionEvent e) {

  }
}

I macro-elementi che costituiscono la nostra interfaccia grafica sono tre: un'area di testo, una pulsantiera ed un menu a tendina. La pulsantiera e il menu a tendina devono contenere tanti pulsanti quanti sono le funzionalita' del programma: dal momento che ci torneranno utili in futuro creiamo un attributo per ogni pulsante o menuitem

....
  private JTextComponent editor;
  private JFileChooser fileChooser;

  private JMenuItem OpenMenuItem;
  private JMenuItem SaveMenuItem;
  private JMenuItem CutMenuItem;
  private JMenuItem CopyMenuItem;
  private JMenuItem PasteMenuItem; 

  private JButton OpenButton; 
  private JButton SaveButton;
  private JButton CutButton;
  private JButton CopyButton;
  private JButton PasteButton;
....
 

La classe BadTextEditor presenta solo due metodi: un costruttore e un metodo ascoltatore; analizziamo per prima cosa il costruttore. La prima cosa che il costruttore definisce sono alcune proprieta' della finestra, come le dimensioni e il titolo, quindi viene impostata l'operazione standard di chiusura:

setTitle("TextEditor");
setSize(300,300);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Quindi procediamo con la creazione dei vari componenti grafici: una TextArea, un FileChooser, un gruppo di pulsanti e uno di MenuItem. Come area di testo scegliamo senza indugio JTextArea, un componente di testo versatile e di semplice utilizzo; come pulsanti creiamo un gruppo di pulsanti decorati con icone.
 
editor = new JTextArea();
fileChooser = new JFileChooser();

OpenMenuItem = new JMenuItem("Open",new 
                             ImageIcon("Open24.gif"));
SaveMenuItem = new JMenuItem("Save",new 
                             ImageIcon("Save24.gif"));
CutMenuItem = new JMenuItem("Cut",new 
                             ImageIcon("Cut24.gif"));
CopyMenuItem = new JMenuItem("Copy",new 
                             ImageIcon("Copy24.gif"));
PasteMenuItem = new JMenuItem("Paste",new 
                             ImageIcon("Paste24.gif"));
OpenButton = new JButton(new ImageIcon("Open24.gif"));
SaveButton = new JButton(new ImageIcon("Save24.gif"));
CutButton = new JButton(new ImageIcon("Cut24.gif"));
CopyButton = new JButton(new ImageIcon("Copy24.gif"));
PasteButton = new JButton(new ImageIcon("Paste24.gif"));

A questo punto passiamo a costruire il JMenu e la JToolBar:
JMenu menu = new JMenu("Menu");
menu.add(OpenMenuItem);
menu.add(SaveMenuItem);
menu.addSeparator();
menu.add(CutMenuItem);
menu.add(CopyMenuItem);
menu.add(PasteMenuItem);
JMenuBar menuBar = new JMenuBar();
menuBar.add(menu);
JToolBar toolbar = new JToolBar();
toolbar.add(OpenButton);
toolbar.add(SaveButton);
toolbar.addSeparator();
toolbar.add(CutButton);
toolbar.add(CopyButton);
toolbar.add(PasteButton);


Le righe seguenti servono ad associare un ascoltatore ai vari pulsanti; come abbiamo gia' detto, la classe BadTextEditor funziona anche da ascoltatore:

OpenMenuItem.addActionListener(this);
SaveMenuItem.addActionListener(this);
CutMenuItem.addActionListener(this);
CopyMenuItem.addActionListener(this);
PasteMenuItem.addActionListener(this);
OpenButton.addActionListener(this);
SaveButton.addActionListener(this);
CutButton.addActionListener(this);
CopyButton.addActionListener(this);
PasteButton.addActionListener(this);


Per concludere, impostiamo il Layout Manager e assembliamo l'interfaccia. Come Layout manager scegliamo BorderLayout, dal momento che si presta molto bene a definire lo scheletro di un'applicazione di questo tipo, con un componente grosso al centro e una toolbar in alto: 

getContentPane().add(BorderLayout.NORTH,toolbar)
getContentPane().add(BorderLayout.CENTER,new JScrollPane(editor));
setJMenuBar(menuBar);
setVisible(true); 


Implementiamo le operazioni
Per associare le funzionalita' ai pulsanti dobbiamo costruire un opportuno ActionListener: nel nostro caso dobbiamo sviluppare un metodo actionPerformed(ActionEvent e) che verifichi la provenienza dell'evento e produca l'azione richiesta. Il codice che svilupperemo avra' questa struttura:

  public void actionPerformed(ActionEvent ae) {
    if(ae.getSource().equals(OpenButton)) {
       ....
    }
    else if(ae.getSource().equals(SaveButton)) {
       ....
    }
    ....
  }

L'operazione Open puo' essere implementata con poche righe di codice: dapprima viene aperto il JFileChooser, quindi, se l'utente ha selezionato un file, tale file viene aperto e caricato nella JTextArea, utilizzando il metodo read(Reader in, Object desc) presente in qualunque componente di testo:

if(ae.getSource().equals(OpenButton) || 
                 ae.getSource().equals(OpenMenuItem)) {
  int response = fileChooser.showOpenDialog(this);
  if(response==JFileChooser.APPROVE_OPTION) {
    try {
      File f = fileChooser.getSelectedFile();
      Reader in = new FileReader(f);
      editor.read(in,null);
    }
    catch(Exception e) {}
  } 
}


In modo del tutto simile definiamo l'operazione Save: 

    ....
    else if(ae.getSource().equals(SaveButton) ||
        ae.getSource().equals(SaveMenuItem)) {
      int response = fileChooser.showSaveDialog(this);
      if(response==JFileChooser.APPROVE_OPTION) {
        try {
          File f = fileChooser.getSelectedFile();
          Writer out = new FileWriter(f);
          editor.write(out);
        }
        catch(Exception e) {}
      } 
    }
    ....

JTextArea fornisce le operazioni sulla clipboard: questo permette di implementare in modo molto semplice le funzioni Cut Copy e Paste:

....
    else if(ae.getSource().equals(CutButton) ||
             ae.getSource().equals(CutMenuItem)) {
      editor.cut();
    }
    else if(ae.getSource().equals(CopyButton) || 
             ae.getSource().equals(CopyMenuItem)) {
      editor.copy();
    }
    else if(ae.getSource().equals(PasteButton) ||
            ae.getSource().equals(PasteMenuItem)) {
      editor.paste();
    }
....

Nelle righe seguenti possiamo vedere tutto il sorgente in un colpo solo:

import javax.swing.*;
import javax.swing.text.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;

public class BadTextEditor extends JFrame implements ActionListener {

  private JTextComponent editor;
  private JFileChooser fileChooser;

  private JMenuItem OpenMenuItem;
  private JMenuItem SaveMenuItem;
  private JMenuItem CutMenuItem;
  private JMenuItem CopyMenuItem;
  private JMenuItem PasteMenuItem; 

  private JButton OpenButton; 
  private JButton SaveButton;
  private JButton CutButton;
  private JButton CopyButton;
  private JButton PasteButton;

  public BadTextEditor() {
    setTitle("TextEditor");
    setSize(300,300);
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    editor = new JTextArea();
    fileChooser = new JFileChooser();

    OpenMenuItem = new JMenuItem("Open",new ImageIcon("Open24.gif"));
    SaveMenuItem = new JMenuItem("Save",new ImageIcon("Save24.gif"));
    CutMenuItem = new JMenuItem("Cut",new ImageIcon("Cut24.gif"));
    CopyMenuItem = new JMenuItem("Copy",new ImageIcon("Copy24.gif"));
    PasteMenuItem = new JMenuItem("Paste",new ImageIcon("Paste24.gif"));

    OpenButton = new JButton(new ImageIcon("Open24.gif"));
    SaveButton = new JButton(new ImageIcon("Save24.gif"));
    CutButton = new JButton(new ImageIcon("Cut24.gif"));
    CopyButton = new JButton(new ImageIcon("Copy24.gif"));
    PasteButton = new JButton(new ImageIcon("Paste24.gif"));

    JMenu menu = new JMenu("Menu");
      menu.add(OpenMenuItem);
      menu.add(SaveMenuItem);
      menu.addSeparator();
      menu.add(CutMenuItem);
      menu.add(CopyMenuItem);
      menu.add(PasteMenuItem);
    JMenuBar menuBar = new JMenuBar();
    menuBar.add(menu);

    JToolBar toolbar = new JToolBar();
    toolbar.add(OpenButton);
    toolbar.add(SaveButton);
    toolbar.addSeparator();
    toolbar.add(CutButton);
    toolbar.add(CopyButton);
    toolbar.add(PasteButton);

    OpenMenuItem.addActionListener(this);
    SaveMenuItem.addActionListener(this);
    CutMenuItem.addActionListener(this);
    CopyMenuItem.addActionListener(this);
    PasteMenuItem.addActionListener(this);

    OpenButton.addActionListener(this);
    SaveButton.addActionListener(this);
    CutButton.addActionListener(this);
    CopyButton.addActionListener(this);
    PasteButton.addActionListener(this);

    getContentPane().setLayout(new BorderLayout());
    getContentPane().add(BorderLayout.NORTH,toolbar);
    getContentPane().add(BorderLayout.CENTER,
                         new JScrollPane(editor));
    setJMenuBar(menuBar);
 }
  public void actionPerformed(ActionEvent ae) {
    if(ae.getSource().equals(OpenButton) || 
       ae.getSource().equals(OpenMenuItem)){
     int response = fileChooser.showOpenDialog(this);
     if(response==JFileChooser.APPROVE_OPTION) {
       try {
        File f = fileChooser.getSelectedFile();
        Reader in = new FileReader(f);
        editor.read(in,null);
       }
       catch(Exception e) {}
      } 
    }
    else if(ae.getSource().equals(SaveButton) || 
            ae.getSource().equals(SaveMenuItem)) {
      int response = fileChooser.showSaveDialog(this);
      if(response==JFileChooser.APPROVE_OPTION) {
        try {
          File f = fileChooser.getSelectedFile();
          Writer out = new FileWriter(f);
          editor.write(out);
        }
        catch(Exception e) {}
      } 
    }
    else if(ae.getSource().equals(CutButton) || 
            ae.getSource().equals(CutMenuItem)) {
      editor.cut();
    }
    else if(ae.getSource().equals(CopyButton) || 
            ae.getSource().equals(CopyMenuItem)) {
      editor.copy();
    }
    else if(ae.getSource().equals(PasteButton) || 
            ae.getSource().equals(PasteMenuItem)) {
      editor.paste();
    } 
  }
  public static void main(String argv[]) {
    BadTextEditor b = new BadTextEditor();
    b.setVisible(true); 
  }
}
 
 

Uno sguardo critico a BadTextEditor
La prima versione del nostro editor vuole fornire un valido esempio di come non vada sviluppato un programma a finestre. Esso soffre almeno tre grossi difetti: in primo luogo presenta metodi troppo lunghi; in secondo luogo non fornisce alcun tipo di modularita' strutturale; per finire non opera nessuna distinzione tra sintassi e semantica. Questi tre difetti tendono a ricorrere con una certa frequenza nei programmi grafici: nei prossimi paragrafi vedremo di analizzare questi aspetti in modo piu' dettagliato

Metodi Troppo Lunghi
Come abbiamo gia' fatto notare, BadTextEditor e' formato da due soli metodi di circa cinquanta righe ciascuno. Una simile dimensione puo' anche essere considerata accettabile, ma in questo caso specifico genera perplessita' il fatto che ciascuno di questi metodi svolga un gran numero di compiti: nel costruttore si possono individuare cinque fasi distinte, evidenziate durante l'esposizione, mentre il metodo actionPerformed(), oltre a svolgere la normale funzione di ascolto, contiene, in reazione ai comandi "Open" e "Save",  due grossi blocchi di codice che svolgono operazioni di dialogo con l'utente e di manipolazione di files. La programmazione ad oggetti tende ad incoraggiare la definizione di metodi piccoli (5-10 righe) ed estremamente specializzati; una tale impostazione puo' essere adottata a priori in fase di progettazione, oppure, come vedremo nel prossimo articolo, puo' essere realizzata a posteriori, ricorrendo a semplici tecniche di Refactoring

Modularita'
Un'interfaccia grafica e' formata da una collezione di controlli disposti all'interno di contenitori. Controlli e contenitori possono essere combinati a piacere come i mattoni di un gioco ad incastro: prendo un gruppo di pulsanti e li incastro all'interno di un contenitore, quindi incastro il tutto dentro un contenitore piu' grosso assieme ad altri controlli. I componenti Swing sono un esempio di oggetti modulari: derivano tutti da una medesima matrice e offrono delle modalita' standard di interazione, cosa che permette di combinarli a piacere.

Quando realizziamo un programma grafico, solitamente raggruppiamo i controlli grafici in macro elementi, come la ToolBar o la MenuBar, ognuno dei quali puo' essere visto come un macro-mattone del nostro gioco ad incastro, sostituibile a piacere con un altro equivalente.

Nel programma di esempio l'interfaccia grafica viene costruita con un procedimento non modulare, ossia con un blocco monolitico di istruzioni: seguendo la metafora del Lego, e' come se avessimo realizzato una costruzione saldando i componenti con la colla, anziche' ricorrendo al piu' flessibile metodo ad incastro.

Un programma grafico modulare dovrebbe permettere all'utente di sostituire un elemento (ad esempio la ToolBar) con un altro equivalente (una ToolBar in radica con pulsanti in marmo), attraverso una modalita' semplice ed intuitiva come quella dei giochi ad incastro.

Nel prossimo articolo studieremo in maniera approfondita alcune tecniche di programmazione che aiutano a modellare un programma in moduli funzionali ben definiti, sostituibili all'occorrenza con altri equivalenti.
 
 
 

Sintassi e Semantica
Abbiamo detto che il nostro editor deve mettere a disposizione cinque funzionalita': Load, Save, Cut, Copy e Paste. Queste cinque operazioni costituiscono la semantica del programma, ovvero cio' che esso e' in grado di fare. Ovviamente il nostro programma esibisce una semantica molto piu' estesa: esso infatti ingloba tutte le funzionalita' gia' presenti nel suo componente principale, la JTextArea; in questa prima analisi, in ogni caso, ci limiteremo a considerare le operazioni definite ex novo.

Ogni funzionalita' e' accessibile attraverso due controlli: un pulsante ed un elemento di menu. Pulsanti e menu costituiscono la sintassi del nostro editor, ovvero le modalita' che esso ci offre per accedere alle operazioni.

Nel sorgente incriminato le operazioni vengono definite all'interno del metodo actionPerformed(), in corrispondenza di una clausola condizionale. In un certo senso e' come se avessimo incastonato le operazioni all'interno dei pulsanti, dal momento che l'unica maniera per accedere ad una determinata funzione e' quella di clickare il pulsante corrispondente. Questa modalita' di sviluppo mostra i suoi limiti non appena si prova a definire un'operazione composita, ossia una macro-operazione che combini operazioni gia' definite. Se volessimo introdurre una funzionalita' tipo auto-save, ossia un salvataggio automatico del testo ogni 10 minuti, ci farebbe comodo riutilizzare la funzione "Save": l'attuale implementazione della gestione delle azioni dell'utente non ci permette di farlo.

Se vogliamo realizzare una buona separazione tra sintassi e semantica, dobbiamo anzitutto ridefinire la modalita' di interazione con l'utente. In primo luogo dobbiamo formulare un'interfaccia di programmazione abbastanza evoluta da riuscire ad esprimere tutte gli aspetti di una interazione complessa; questo ci permettera' in seguito di associare alla sintassi di qualunque tipo di controllo un'opportuna semantica. Affronteremo in modo piu' approfondito queste tematiche nel terzo articolo.
 
 
 

Conclusioni
In questo primo articolo abbiamo introdotto un esempio di programma grafico sviluppato secondo un modello di programmazione abbastanza comune; una prima analisi ne ha rivelato i principali limiti. Nel prossimo articolo analizzeremo alcune tecniche di programmazione che permettono di superare agevolmente tali limiti e di realizzare applicazioni piu' flessibili e solide dal punto di vista strutturale.

L'esempio descritto in questo articolo 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