MokaByte
Numero 19 - Maggio 1998
|
|||
|
III parte |
||
Michele Sciabarrà |
In questo numero finalmente completiamo il sistema. Una applet permetterà di fornire agli utenti del sito un semplice sistema di chat | ||
Interfacce
grafiche in Java
Il programma
server gira in background senza interagire direttamente con gli utenti
e non ha quindi bisogno di una interfaccia grafica (anche se potrebbe eventualmente
creare una finestra di log). Il client è un caso diverso: vogliamo
che l’utente interagisca comodamente. L’uso di una GUI è quindi
obbligatorio. In Java per costruire interfacce grafiche occorre utilizzare
la libreria AWT. In realtà oggi questa non è più l’unica
scelta. Per esempio Netscape ha sviluppato le IFC (Internet Foundation
Classes), mentre Microsoft sta sviluppando le AFC (Application Foundation
Classes). L’ultima novità è che le IFC e la AWT convergeranno
nelle JFC (Java Foundation Classes) grazie alla collaborazione di
Sun, Netscape e IBM. Mentre scrivo comunque i browser che supportano le
nuove librerie di classi sono ancora in beta. Moltissimi browser installati
non sono in grado di supportare né IFC né AFC né JFC,
ma solo la AWT 1.0. Ragion per cui da oggi e per un certo periodo a questa
parte (un paio di anni al massimo) per compatibilità con l’installato
dovremo continuare a scrivere applet che utilizzino la prima versione della
AWT. Abbiamo deciso quindi di rispettare questo vincolo per garantirci
la compatibilità con il maggior numero di browser possibile.
La AWT 1.0 in
verità non è particolarmente ricca né molto ben progettata.
Programmare con questa libreria non è molto comodo e le interfacce
prodotte sono abbastanza povere. In principio occorreva sviluppare a mano
il codice che genera l’interfaccia grafica. Un programmatore Visual Basic
può quindi pensare che Java sia un ritorno ai primi tempi di Windows,
non l’ultima novità della tecnologia. Sono presto comparsi in commercio
vari strumenti che consentono di disegnare l’interfaccia utente invece
di codificarla. In pratica nessuno di essi svolge un buon lavoro. Il problema
è che la AWT è stata chiaramente progettata pensando alla
codifica manuale delle GUI. L’impostazione porta a mescolare il codice
che descrive l’interfaccia con quello che gestisce gli eventi. Questo è
abbastanza comodo quando il programmatore deve fare tutto a mano: non deve
continuamente saltare da un file all’altro. Utilizzando invece uno strumento
visuale, il risultato diviene contorto e poco chiaro: le parti codificate
manualmente si confondono con codice generato automaticamente, che è
generalmente complesso e poco leggibile.
Per fortuna,
la AWT 1.1 pone rimedio a tutti i problemi che rendono difficoltoso usare
ambienti di sviluppo visuali con la 1.0. Con la nuova AWT finalmente è
efficace utilizzare gli strumenti visuali (infatti gestione eventi e codifica
dell’interfaccia sono distinti), mentre la codifica manuale diviene più
complessa.
La AWT ed
il disegno dell’interfaccia...
Vediamo come
si programma utilizzando la AWT 1.0. In realtà l’obiettivo è
scrivere una applet con interfaccia grafica. Poiché utilizzeremo
la AWT per la GUI delle applet, lo studio di questa libreria è un
passaggio obbligato. Coprire tutti gli aspetti sarebbe abbastanza lungo,
quindi ci limiteremo a trattare gli argomenti principali (quanto basta
per capire il codice del client).
Trattandosi
di un linguaggio OOP, la libreria AWT è una gerarchia di classi.
Alcune classi sono più importanti di altre perché sono le
radici di grossi sottoalberi.
Da
Object
è derivato Component
e da questo Container. Queste
ultime due classi sono fondamentali: servono per suddividere le classi
della AWT in due importanti categorie: i componenti di interfaccia veri
e propri (per esempio i bottoni) e i contenitori di componenti (per esempio
le finestre). In realtà la distinzione tra componenti e contenitori
non è così netta: un Container
e tutti i componenti
contenuti, può infatti essere trattato come se fosse un singolo
componente. Questo comunque non è vero sempre: per esempio una Window
non può contenere altre Window. Pensando alla MDI di Windows
si potrebbe forse pensare che sia lecito. In realtà la MDI è
specifica di Windows e un programma Java che ne faccia uso non potrebbe
girare, per esempio, sul Mac. Comunque ci sono dei contenitori (in particolare
i Panel ) che non generano una finestra separata e possono effettivamente
essere utilizzati come componenti (quindi composti da sottocomponenti).
I Panel possono effettivamente essere aggiunti ad altri container
come se fossero singoli componenti. In Tabella 1 sono riassunti
i componenti disponibili, mentre nella Tabella 2 sono riassunti i contenitori.
Tabella 1
Button | Bottone |
Canvas | Compoonente per disegnare |
Checkbox | Selezione on/off |
CheckboxMenuItem | MenuItem a selezione on/off |
Choice | Lista di selezione a discesa |
Label | Etichetta |
List | Lista di selezione |
Menu | Un menù contenente MenuItem |
MenuBar | La barra dei menù |
MenuItem | Una voce di menù |
Scrollbar | Barre di scorrimento |
TextArea | Campo di testo multilinea |
TextField | Campo di testo a linea singola |
Tabella 2
Dialog | Finestra di Dialogo |
FileDialog | Seleziona un file da aprire |
Frame | Finestra con menù |
Panel | Contenitore non finestra |
Window | Finestra semplice |
L’uso è
semplice: si crea un contenitore e si aggiungono dei componenti al contenitore.
Il problema si pone nel posizionamento degli elementi nel contenitore.
Si potrebbe pensare che quando si aggiunge un componente occorra anche
specificare le sue coordinate nel container. Questo approccio effettivamente
è previsto ed è utilizzabile. Comunque c’è un problema:
codificando in questo modo si ha una dipendenza dell’interfaccia dalla
risoluzione. Infatti, data la varietà delle macchine che accedono
ad Internet, quello che si vede bene a 640x480 sembrerebbe un francobollo
a 1024x768. La soluzione usata dalla AWT (e ripresa dal linguaggio Tcl/Tk)
è utilizzare un posizionamento "logico" nel container: non si specificano
le coordinate, bensì si dà una indicazione di come debbano
essere posizionati gli elementi: per esempio Nord, Centro, Est. Il meccanismo
si basa sull’uso dei cosiddetti LayoutManager. In Tabella 3 sono
elencati i layout manager disponibili nella AWT 1.0, ma è possibile
definirne di nuovi. Innanzitutto ogni contenitore può impostare
il layout manager corrente utilizzando setLayout. Quando verrà
aggiunto un nuovo elemento, il contenitore affiderà il posizionamento
dell’elemento al layout manager.
Tabella 3
BorderLayout | A croce |
CardLayout | A "tabbed dialog" |
FlowLayout | A flusso orizzontale |
GridBagLayout | A griglia con vincoli |
GridLayout | A griglia |
È importante
notare che il posizionamento è dinamico e dipendente sia dalla dimensione
del contenitore che dalla dimensione dei componenti. Utilizzando questo
meccanismo si creano interfacce dinamiche facilmente ridimensionabili che
mantengono la loro struttura.
Per esempio
consideriamo il caso di una finestra che contiene una Label
in alto
(la stringa "Hello") e il bottone "OK" in basso.
class MioFrame
extends Frame {
Button b = new Button("OK");
MioFrame() {
setLayout(new BorderLayout());
add("North",new Label("Hello"));
add("South", b);
}
}
...e la gestione
degli eventi
Una volta disegnata
l’interfaccia utente, si pone il problema della gestione degli eventi:
ovvero vorremo fare in modo che l’interfaccia risponda al click di un bottone,
alla selezione di una voce di menu o altro. Ogni componente in Java è
sensibile agli eventi tramite il metodo handleEvent.
Quest’ultimo
metodo ereditato "così com’è" non fa nulla: per gestire gli
eventi occorre ereditare da un componente una nuova classe che lo ridefinisca
opportunamente. Per esempio, supponiamo di voler dotare la classe MioFrame
di
un gestore di eventi che nasconda la finestra quando si preme un bottone.
Occorre aggiungere il seguente metodo:
public boolean handleEvent(Event evt) {
if(evt.target==b) {
hide();
return true;
}
return
super.handleEvent(evt);
}
Cliccando su
OK innanzitutto il sistema tenterà di smistare l’evento alla
handleEvent del bottone. Se il bottone sarà in grado di gestire
l’evento, la handleEvent
ritornerà true e la gestione
terminerà.
Nel nostro caso
non andrà così perché la Button.handleEvent
non
farà nulla e ritornerà
false. L’evento verrà
allora smistato al contenitore dei componenti, nel nostro caso alla finestra
MioFrame.
Questa volta l’evento verrà effettivamente gestito e la gestione
terminerà grazie al "return true". È da notare l’uso
della super.handleEvent(): è utilizzata per smistare l’evento
ad una eventuale superclasse qualora l’evento non dovesse essere gestito
localmente.
Infine ricordiamo
che non sempre è necessario ridefinire la handleEvent, ma
si possono utilizzare degli altri metodi più specifici. Ridefinire
la handleEvent comporta che vengano intercettati tutti gli eventi.
Tuttavia la finestra Component.handleEvent
(la finestra principale)
chiama alcuni metodi in corrispondenza a particolari eventi: per esempio
mouseDown
o keyUp. Se si vogliono soltanto gestire i click del mouse o
la tastiera è sufficiente ridefinire questi metodi invece di ridefinire
completamente la handleEvent. Infatti, quest’ultima potrebbe essere
una scelta inefficiente: handleEvent
si occupa di gestire anche
eventi di sistema come i repaint. Un buon compromesso è ridefinire
action che intercetta tutte le azioni dell’utente, tralasciando
quelle di sistema.
La classe
Applet
Le applet delle
pagine Web devono essere definite estendendo la classe java.applet.Applet.
Questa mette a disposizione alcuni metodi necessari per interagire con
il browser. Normalmente l’esecuzione di un programma in Java parte dal
metodo main statico della classe indicata come la classe principale
del programma. Le applet invece vengono invocate direttamene dal browser
che ha una sua main. In pratica quello che si deve fare è
ridefinire alcuni o tutti dei seguenti metodi:
Modifiche
al server
Abbiamo innanzitutto
previsto lo switch -w che consente di aprire esplicitamente una
finestra di logging. Senza questo switch, il log va nello standard error.
Il motivo di questa modifica è contingente: infatti dispongo di
un server Linux senza interfaccia grafica. Si tratta di un vecchio 486
con soli 8 mega di RAM e far girare XWindows è "pesante". Ciò
nonostante, il JDK per Linux gira senza problemi, a patto ovviamente di
non usare la GUI. Ragion per cui ho previsto la possibilità di logging
nello standard error.
La seconda modifica
è l’aggiunta del comando
WHO per "listare" chi c’è.
Questa modifica serve per consentire di scegliere il destinatario dei messaggi.
Senza questo comando non è possibile tenere traccia di chi è
in chat e quindi scegliere il destinatario di una comunicazione riservata.
Come funziona
il client
Il client è
una applet che viene caricata dinamicamente quando viene consultata la
pagina Web che la contiene. Il JRC è utile per offrire un servizio
di chat, seppur molto semplice agli utenti del sito. All’avvio appare una
finestra che consente di entrare (Figura 1).
Viene richiesto
il nostro nickname: se è ammissibile (ovvero non ci sono altri utenti
con lo stesso nickname), viene attivata la connessione. Appare quindi la
finestra di chat vera e propria, visibile in Figura 2.
Si tratta di
una finestra indipendente, aperta e gestita dalla JVM. Si possono distinguere,
in alto, la parte dove appaiono gli interventi; al centro abbiamo un campo
di testo per digitare il proprio intervento. In basso ci sono dei bottoni:
Talk per inviare l’intervento a tutti, Who
per listare chi
è correntemente presente nella stanza di conversazione e infine
Send per inviare una comunicazione riservata ad un solo utente.
Segue infatti un altro campo di testo per scrivere il nickname di destinatari
di conversazioni riservate. Infine abbiamo un bottone Clear
per
svuotare il buffer qualora si riempisse troppo e infine il bottone
Quit,
che chiude la finestra di chat. Si ritorna così alla finestra principale
e volendo si può rientrare nuovamente in chat digitando un nuovo
nickname.
Classe AppClient
La programmazione
orientata agli oggetti è nata allo scopo di rinforzare la riusabilità
del codice. Nonostante questa semplice verità, vedendo come programmano
alcuni sviluppatori C++ ci si rende conto che forse lo scopo non è
stato raggiunto. È vero, la OOP offre altri vantaggi come la possibilità
di ottenere una struttura del codice più ordinata che semplifica
la manutenzione. Comunque il vero salto di qualità si ha quando
si riesce a riutilizzare del codice. Il problema è che avendo a
disposizione delle completissime librerie come le MFC per il Visual C++
o anche la stessa libreria di Java, il programmatore si sente scoraggiato
a farsi una nuova libreria, perché gli sembra di "reinventare l’acqua
calda". Il fatto è che le librerie di classi sono di solito generaliste
e non offrono soluzioni pronte all’uso per il particolare dominio applicativo
del programmatore. Di solito invece i programmatori tendono a fare programmi
abbastanza simili tra di loro (anche per motivi commerciali o aziendali)
legati ad un particolare dominio applicativo. Quindi in ogni caso vale
la pena crearsi una piccola libreria. Non si tratta di "perdere tempo",
ma di prendere l’abitudine di codificare scrivendo piccole e semplici classi
riutilizzabili. C’è comunque un prezzo da pagare: bisogna imparare
a separare i concetti generali da quelli specifici delle applicazioni.
Consideriamo per esempio la classe
JRCClient
che scriveremo più
avanti e che implementa il protocollo JRC. Se scriviamo una unica classe
monolitica le possibilità di riuso, a meno di non scrivere altri
client JRC, sono abbastanza basse. Tuttavia c’è un aspetto di questa
classe abbastanza generale: il fatto che si tratta di un client di protocollo.
Alcuni elementi dunque sono comuni a vari client. Per esempio, possiamo
immaginare facilmente la seguente classe:
class AppClient {
void open(String host, int port);
void close();
void putLine();
void getLine();
String[] split(String s);
}
Questa è
una classe estremamente semplice, come vedete. Eppure il suo uso è
chiaramente frequente. Il trucco è quindi imparare a separare
il caso specifico dell’applicazione dai casi più generali. Scrivere
le due classi JRCClient e
AppClient può prendere pochi
minuti in più rispetto alla sola classe JRCClient. Però
il codice di AppClient può essere facilmente riutilizzato. Ogni
riuso è tempo risparmiato, non tanto forse in termini di codifica,
quanto soprattutto di testing, di analisi del problema e di consultazione
di manuali ed help on-line. Quella classe rappresenta un problema risolto
che non deve essere rianalizzato, per di più già testata
nel contesto di un progetto concreto. La classe AppClient può
valere un’oretta di tempo. Saper rifare la stessa cosa sistematicamente
significa produttività. Tanto per fare un esempio dell’importanza
di questo approccio, almeno metà del codice del server JRC è
codice riutilizzato da altri progetti.
Classe JRCClient
Questa classe
è al centro del client: infatti si tratta della classe che interagisce
col server utilizzando il protocollo. Per aumentare l’astrazione, incapsula
completamente la gestione del server, l’apertura e la chiusura di connessioni
di rete, l’invio e la ricezione di stringhe dai Socket. La definizione
"astratta" di questa classe (senza l’implementazione dei metodi e senza
altri dettagli) è la seguente:
class JRCClient extends AppClient {
JRCClient();
void nick();
void send();
void talk();
void list();
void who();
}
Questa classe
incapsula completamente la gestione del protocollo. L’utilizzatore crea
un nuovo JRCClient e interagisce con esso come se si trattasse di
una classe locale, senza sapere nulla del server e del protocollo. Per
l’implementazione sfruttiamo i metodi ereditati da AppClient, per
aprire e chiudere connessioni e leggere e scrivere nei socket. Abbiamo
anche definito due metodi privati che rendono semplice l’implementazione
dei metodi pubblici corrispondenti ai comandi:
check() e data().
Nella interazione
con il server sono infatti possibili due casi: comandi che ritornano solo
codici di successo o di errore e comandi che ritornano dati. La check()
semplicemente controlla se l’ultimo comando sia stato correttamente
inserito. Invece data()
si aspetta dei dati nel formato fornito
dal protocollo e li ritorna come un array di stringhe (oppure ritorna
null
se ci sono errori). In questo modo l’implementazione ad esempio del
metodo nick (che usa check) è semplicemente:
public synchronized boolean nick(String name) throws
IOException {
putLine("nick "+name+"\n");
return check();
}
mentre l’implementazione
di read (che usa
data) è
public synchronized String[] read() throws IOException {
putLine("read\n");
return data();
}
Classe JRCApplet
Questa classe
mostra l’interfaccia che si presenta all’utente prima di entrare in chat.
Il metodo init costruisce l’interfaccia (vedere Figura 1), che presenta
un campo di input per il nickname, un bottone e una label che serve da
barra di stato che mostra messaggi di errore. Per esempio tentando di entrare
con un nickname già esistente oppure tentando di entrare in chat
quando la finestra è già aperta si ottengono corrispondenti
messaggi di errore in basso. La classe JRCApplet
mette a disposizione
anche due metodi, lock e unlock, che consentono alla finestra
di chat di bloccare l’apertura di nuove finestre e di sbloccarla. Per il
resto non ci sono altri particolari elementi di rilievo.
Classe JRCChat
Questa classe
costruisce e gestisce l’interfaccia che si presenta all’utente quando si
apre la finestra di chat. Si tratta della classe che implementa la parte
grafica del client (la classe JRCClient implementava la parte non
grafica). Mostriamo, nel Listato 1, il costruttore di JRCChat
in modo da esemplificare i concetti di AWT di cui abbiamo parlato prima.
Listato 1Notiamo come il costruttore utilizzi le tecniche dei LayoutManager: il posizionamento non è basato su coordinate assolute ma relative. Riferendosi alla Figura 2, vediamo le tecniche usate per costruire questa interfaccia. Innanzitutto l’intera finestra utilizza un BorderLayout: al nord abbiamo posto la TextArea che mostra le conversazioni, mentre al centro e al sud abbiamo posto due pannelli. Abbiamo quindi utilizzato un contenitore come componente che contiene al suo interno altri componenti. Questa tecnica di nidificazione dei componenti si rivela molto utile in pratica, come si può vedere in questo caso. I pannelli utilizzano il FlowLayout per posizionare i loro elementi orizzontalmente e contengono i bottoni e i campi di testo che completano l’interfaccia utente. Tra le altre azioni svolte dal costruttore, abbiamo il lock della applet (per non consentire l’apertura di altre finestre di chat finché la presente è attiva), la creazione di una nuova istanza di JRCClient che gestirà l’interazione con il server, ed infine l’avvio di un thread autonomo per l’aggiornamento automatico della finestra di chat. Infatti nella finestra di chat devono essere mostrati anche gli interventi degli altri utenti. Poiché questa operazione non è automatica ma occorre esplicitamente eseguire una read, abbiamo risolto il problema creando un thread che ogni mezzo secondo controlla se ci sono stati altri interventi. L’aggiornamento vero e proprio è effettuato dal metodo update, invocato periodicamente dal thread che esegue il metodo run.
class JRCChat extends Frame implements Runnable {
public JRCChat(JRCApplet applet, String nick) throws Exception {
applet.lock();
this.applet = applet;
if(!client.open(applet.getDocumentBase().getHost(), 6789))
throw new Exception("Cannot open the JRC server");
area.setEditable(false);
setTitle("JRCClient - ["+nick+"]");
setResizable(false);
Panel pn = new Panel();
pn.setLayout(new BorderLayout());
Panel ps = new Panel();
ps.setLayout(new BorderLayout());
Panel psn = new Panel();
psn.setLayout(new FlowLayout());
Panel pss = new Panel();
pss.setLayout(new FlowLayout());
psn.add(new Label("Input:"));
psn.add(text);
pss.add(talk);
pss.add(who);
pss.add(send);
pss.add(new Label("To:"));
pss.add(choice);
pss.add(new Label(" "));
pss.add(clear);
pss.add(quit);
pn.add("Center", area);
ps.add("North", psn);
ps.add("South", pss);
add("North", pn);
add("South", ps);
pack();
thread = new Thread(this);
thread.start();
if(!client.nick(nick)) {
exit();
throw new Exception("Nickname already exist");
}
}
}
Conclusioni
Con questa puntata
abbiamo completato una prima implementazione del sistema. È ora
disponibile per i lettori un completo chat system che, volendo, può
essere ampliato e modificato. Il chat gira sia utilizzando il Netscape
Navigator 3.0 che Microsoft Internet Explorer 3.0. In Figura 2 infatti
sono visibili le finestre di chat aperte da due browser in esecuzione sulla
stessa macchina. Questo è già un buon risultato: Java mette
d’accordo due acerrimi rivali come Netscape e Microsoft. In realtà
c’è da ricordare che il server, pur essendo stato sviluppato sotto
Windows 95, in realtà gira su una macchina Linux. Questo è
un risultato ancora più eclatante, che mostra chiaramente come Java
consenta di raggiungere concretamente quel sogno di interoperabilità
multipiattaforma vagheggiato per decenni e che solo ora, finalmente, è
realtà concreta e tangibile.
Bibliografia
Michele Sciabarrà è consulente informatico e scrittore tecnico, specializzato in sistemi e applicazioni Internet ed Intranet. Si occupa di consulenza, amministrazione e sviluppo software per ambienti di rete e siti Web.
|
||
|
||
MokaByte ricerca
nuovi collaboratori
|
||
|