|
|||||||||||||||||||||||||||||||||
L’obiettivo del presente articolo è illustrare le tecniche che permettono di risolvere il problema del controllo interattivo dell’input utente estendendo opportunamente le funzionalità della classe “JTextField” (API Swing). Si tratta di un problema “atavico”, ma tuttora ricorrente, presente pressoché in ogni applicativo provvisto di un’interfaccia utente, per così dire, non di “servizio” |
|||||||||||||||||||||||||||||||||
Introduzione
La questione del controllo interattivo potrebbe essere considerata piuttosto “decrepita”; lo stesso autore ci si è imbattuto per la prima volta oltre un decennio orsono, realizzando un apposito modulo in linguaggio C (i tempi d’oro del Turbo C 2.0 della Borland). Da allora molte cose sono cambiate, ma il problema sembrerebbe essere di attualità, sebbene si ripresenti in forme diverse: chissà che ne direbbe Gianbattista Vico! E’ possibile misurare l’incidenza del problema mediante l’analisi dell’ingente quantitativo di quesiti a riguardo presenti nel sito “www.javasoft.com”. Il modo migliore per risolvere il caso, e qui il concetto della riusabilità trova la sua esaltazione, è quello di progettare una classe (meglio un bean), più generale possibile che, opportunamente parametrizzata, sia in grado di controllare e gestire tutte le diverse casistiche di campi dati (solo testo, numerici, monetari, data, …). Il dominio del metodo di controllo dell’input è quindi costituito dall’insieme delle stringhe di caratteri (inserite per mezzo della funzione di “Paste”) aventi come caso più frequente, il sottoinsieme delle stringhe di lunghezza unitaria (analisi del singolo carattere premuto). In sostanza si tratta di realizzare un’estensione della classe “JTextField” che svolga il compito di “filtro-interattivo”. La classe così ottenuta, una volta ricevuta l’attenzione (“focus”) dovrebbe occuparsi di monitorare tutto ciò che le viene fornito dalla tastiera (compresi i risultati dei comandi “Copy & Paste”) al fine di accettare i dati validi e di scartare quelli non previsti dal tipo di campo. Il criterio di selezione (quali caratteri considerare validi) dovrebbe essere fornito per mezzo di un’apposita stringa di “mascheramento”. Per esempio, se si volesse demandare alla classe il compito di acquisire i valori di un campo monetario, la si dovrebbe parametrizzare al fine di farle accettare i soli caratteri numerici, eventualmente corredati da opportuni separatori. Per poter affrontare il problema è necessario però possedere una certa dimestichezza con il famossissimo modello “M. V. C.” (ampiamente illustrato nei numeri precedenti di MokaByte), o meglio, della versione denominata “model-delegate”, su cui si basa l’architettura Swing. Da notare che, volenti o nolenti, è necessario affrontare il problema, dal momento che la classe “JTextField”, di per sé, non dispone di alcun metodo per limitare il numero di caratteri impostabili dall’utente. Poiché l’esercizio, come si vedrà, non è banalissimo, ci si chiede come mai non sia stato realizzato un opportuno metodo (“setMaxLength()”) direttamente dai progettisti delle Swing. Per il lettore meno attento si vuole sottolineare che si tratta di un’interrogazione retorica. Il problema potrebbe sembrerebbe abbastanza banale, il timore è tuttora grande, se non fosse per il fatto che anche il relativo esempio fornito nella sezione “Tips & Tricks” del sito Sun presenti un’imprecisione abbastanza rilevante: si è “semplicemente” trascurata l’architettura MVC (o meglio la relativa versione Swing)! Ci si rende conto che l’articolo voleva essere solo un esempio, ma, proprio per il suo carattere didattico e per la sua funzione di riferimento (faro per milioni di programmatori), forse sarebbe stato più opportuno porvi maggiore attenzione. L’articolo “incriminato”, attualmente, è stato rimpiazzato, ne sono rimaste tracce nell’archivio utilizzato per le ricerche (provare ad eseguire un reperimento specificando il titolo “Constructing a Masked JTextField”) e, casualmente (touché), l’autore del presente articolo (come si vedrà) ne conserva una copia in formato digitale. AWT e SWING: Architetture
differenti….
public class MaskedField extends TextFielded andare a sovrascrivere (“override”) il metodo “handleEvent”, al fine di gestire, uno per uno, i tasti premuti dall’utente. Tali tasti, in funzione di un’opportuna stringa di mascheramento, potevano essere ignorati (valore di ritorno “false”) o accettati (valore di ritorno “true”). public boolean handleEvent(Event oEvnt) {Tipicamente all’interno di tale metodo si inseriva un apposito costrutto “switch” al fine di poter gestire gli eventi della tastiera desiderati, ed il gioco era pressoché fatto. switch (oEvnt.id) {Con l’introduzione delle API Swing tale approccio non è più utilizzabile, sia perché non è possibile evitare che un tasto venga rifiutato, sia (ancor più importante) perché si contravverrebbe (almeno in linea di principio) l’architettura MVC. Se si accede alle “FAQ Swing”, si legge che il consiglio (o meglio l’ordine) fornito è quello di agire (override) sui metodi “remove()” e “insertString()” appartenenti alla classe “PlainDocument” da associare alla classe che estende la “JTextField()” (come avviene del resto per ogni altro componente dell’”API Swing”). protected class NewDocument extends PlainDocument {Per associare una classe estensione di una “PlainDocuement”, è necessario inserire, all’interno della classe che specializza la “JTextField”, il seguente frammento di codice: protected Document createDefaultModel() {Da qui si dovrebbe intuire, tra l’altro, perché si parla di modello per delegazione Fin qui sembrerebbe tutto logico e lineare. Poiché si vuole specializzare il comportamento di un “oggetto” grafico e non il suo “look” è necessario estendere opportunamente la classe “PlainDocument”. La nuova tecnica risulta per molti versi semplificare il lavoro. In primo luogo, per capire se l’utente intenda cancellare o inserire una sotto-stringa, non è necessario andare a “leggere” tutti i caratteri premuti da tastiera ed intercettare la pressione del tasto “CANC” o “BACK SPACE”: è sufficiente estendere il relativo metodo; il quale, attraverso i parametri, fornisce gli indici del testo da eliminare. Il lavoro tedioso spicca quando, invece, si deve gestire l’inserimento di un’intera stringa precedentemente memorizzata (“Copy&Paste”). La questione è così delicata che taluni programmatori, spaventati dal problema, ahimè, si riducono ad inibirne la funzionalità. Purtroppo oggigiorno non è infrequente il caso di molti programmatori che, trovandosi di fronte ad un algoritmo da realizzare, la cui complessità supera un “epsilon” piccolo a piacere, invece di affrontare il problema, decidano semplicemente di evitarlo. VB docet! Prima di proseguire, si analizzi l’esempio fornito nell’articolo “pubblicato” nel sito “JavaSoft”. L’obiettivo era realizzare un componente, denominato IPField, dedicato all’acquisizione di indirizzi IP di quattro byte; quartetto di sotto-stringhe numeriche di tre caratteri, separate dal carattere punto, secondo il formato visualizzato in figura 1. La
soluzione proposta è rappresentata, fedelmente, dal codice riportato
di seguito.
import javax.swing.*;
public class IPAddressDemo extends Jframe {
public IPAddressDemo() {
Classe estendente la “JTextField”. class
IPField extends JTextField {
// Disable paste operations
protected Document createDefaultModel() {
Classe estendente la il “plainDocument”.
protected
class IPDocument extends PlainDocument {
Sebbene
la soluzione proposta funzioni egregiamente, il problema è che si
sono trascurate le direttive dei progettisti delle Swing e forse anche
un po’ l’architettura: si agisce fortemente sulle componenti “view” e “Controller”
(che rappresentano l’”UI Delegate”) anziché sul modello.
Esempio di un
oggetto dedicato all’acquisizione della data
E’ ormai chiaro che la parte di codice di interesse è quella relativa alla classe che estende la “PlainDocument”, che, visto l’argomento, è stata “battezzata” “DateDocument”. protected class DateDocument extends PlainDocument {
/** Array con i giorni di cui è composto ciascun mese*/
Il metodo “remove” (o meglio la relativa sovrascrittura) è abbastanza semplice, le uniche cose da tenere in considerazione sono:
if (iLen > 0) { // E’ un’operazione di eliminazione reale? String sInsertedDate = super.getText(0,10); //legge la data visualizzata // determina la stringa corredata dalla maschera String sNewDate = replaceStr(sInsertedDate, iOffs, getSignChar(iOffs,iLen)); super.remove(0,10); //..elimina tutto il testo visualizzato super.insertString(0,sNewDate,null); //..Inserisce quello risultante
// Determina la nuova posizione
if ((iOffs == 3) || (iOffs == 6)) // Ci si trova su uno slash?
setCaretPosition(iOffs); // posizione il cursore
Si noti che si sono invocati metodi “remove()” e “InsertString” della classe padre al fine di evitare chiamate ricorsive che, tra l’altro, genererebbero un “dead lock”. Il metodo “insertString()” è decisamente più complesso di quello precedente, e svolge i seguenti di compiti:
throws BadLocationException {
// Si tratta di un reale nuovo inserimento?
String sTxtInserted = this.getText(0,10); // preleva la stringa video
//--------------------------------------------------
if (iOffs > iDEF_LENG_INT+1) { //Si sta cercando di andare oltre la fine? int iFirstEmpty = sTxtInserted.indexOf(getMaskChar());
if (iFirstEmpty > -1) //E’ presente almeno uno spazio vuoto?
beep();
return;
// -----------------------------------------------------------------
int iIntStrLen = sTxt.length(); // lunghezza della stringa da inserire sTxt = getOnlyDigit(sTxt); // Elimina eventuali caratteri non validi
if (iIntStrLen != sTxt.length()) { // Eliminati caratteri non validi?
// --------------------------------------------------------------
iIntStrLen = getOnlyDigit(sTxtInserted).length(); iIntStrLen += sTxt.length(); //Lunghezza della data inclusa la nuova stringa
// troppi caratteri?
// --------------------------------------------------------------------
if (sTxt.length() > 0) {
//Cio’ che si vuole inserire “sovrascrive” i caratteri slash?
sTxt = insertSubstr(sTxt, (2-iOffs), getSeparChar()+"" ); }
if(iOffs == 2) //Il nuovo inserimento è sul primo slash?
if ((iOffs < 5) && (sTxt.length()+iOffs > 5)) { //comprende
il 2 slash?
if(iOffs == 5) //Si vuole inserire sopra il secondo slash?
// --------------------------------------------------------
// prepara la nuova stringa da inserire
sTxtInserted = getValidCharacters(sTxtInserted); // Verifica se la data è valida
// --------------------------------------------------------
super.remove(0,10); //Elimina la stringa visualizzata
// ----------------------------------------------------------
setCaretPosition(getNextAvailablePos(iOffs,sTxtInserted,sTxt.length()>0)); } else { //Operazione fittizia?
super.insertString(0,getSignChar(0,10),oAttSet);//Visualizza la formattazione
/** Determina la posizione del primo “carattere” vuoto (‘_’) nella stringa
private int getNextAvailablePos(int iCurrPos, String sDate, boolean boInsStr) { int iNewPos = sDate.indexOf(getMaskChar()); if (iNewPos == -1) { // Nessun spazio vutoto? if (boInsStr) { // Si sta eseguendo un inserimento? iNewPos = 10; // Posiziona il cursore oltre la fine della stringa } else { iNewPos = iCurrPos;
if (iNewPos < 10) {// Non si è ancora raggiunta la fine della
stringa?
if ( (iNewPos == 2) || (iNewPos == 5)) // Ci si trova su uno slash?
return iNewPos;
/** Verifica che il carattere fornito sia compreso nei limiti specificati
private boolean verCharValidity(char cToVer, char cValMin, char cValMax) { boolean boRet = true; if (cToVer != getMaskChar()) { //Non si tratta di un carattere di formattazione?
if ((cToVer < cValMin) || (cToVer > cValMax)) {
}
return boRet;
/** Elimina i caratteri non validi presenti nella stringa fornita
private String getValidCharacters(String sText) {
String sValidate = "";
// -------------------------------------------- Carattere in pos. 0
// -------------------------------------------- Carattere in pos. 1
if(!verCharValidity(sText.charAt(1), '0','1')){
} else {
// ------------------------------------------- Carattere in pos. 2 ==>
slash
// ------------------------------------------- Carattere in pos. 3
// ------------------------------------------- Carattere in pos. 4
// ------------------------------------------- Carattere in pos. 5 ==>
slash
// -------------------------------------------- Carattere in pos. 6
// ----------------------------------- Caratteri dalla pos. 7 a fine stringa
// --------------------------------------------------------------------
String sDays = sValidate.substring(0, 2); // Giorno
int iMonthId = -1;
// Presenti sia il mese sia l’anno?
iMonthId = Integer.parseInt(sMonth)-1;
if (iNumOfDays < Integer.parseInt(sDays)) { // giorno errato?
// Verifica dell’anno bisestile
// Si sta considerando il giorno 29 del mese di Febbraio?
// E’ presente il campo anno?
int iYear = Integer.parseInt(sYear); GregorianCalendar oCal = new GregorianCalendar();
if (!oCal.isLeapYear(iYear)) {
if (boErr) { // Riscontrato un errore?
return sValidate;
/** Sostituisce una sottostringa in un dato oggetto stringa
private String replaceStr(String sSource, int iSrtPos, String sToRep) {
String sBefore = sSource.substring(0,iSrtPos);// Sottostringa che precede
il punto di sostituzione
String sRet = sBefore+sToRep+sAfter;// Nuova stringa
return sRet;
/** Inserisce una sottostringa in un dato oggetto stringa
private String insertSubstr(String sSource, int iSrtPos, String sToIns) {
String sBefore = sSource.substring(0,iSrtPos); // Sottostringa che precede
il punto di inserimento
return sRet;
/** It reurns a string of significant characters
private String getSignChar(int iOffs,int iLen){
for (int iIncr = 0; iIncr< iLen; iIncr ++){ //let's work on this string
if (iOffs == 2 || iOffs == 5) //are we on the separator position
return sSignChar; //the right separator and masked
/** Preleva dalla stringa data i soli campi numerici (elimina i caratteri
di formattazione)
private String getOnlyDigit(String sText){
int iInd = 0; // Contatore di ciclo
while (iInd < sText.length()) {
}
/**
Determina il numero di caratteri slash che precedono la posizione specificata
private int getSlashNumber (int iOffs){
if (iOffs > 2) //Passata la seconda posizione?
Conclusioni
|
|||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||
|