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:
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 |