Story Transcript
USOS INTERESANTES DE JFormattedTextField v. 1.1 Francesc Rosés i Albiol (03/2005) Todos los que en alguna ocasión nos hemos tenido que enfrentar al desarrollo de interficies de usuario en Swing nos hemos encontrado con algunas deficiencias en cuanto a los componentes disponibles. Una de ellas, y creo que importante por su uso masivo en aplicaciones de este tipo, ha sido la de los campos de entrada. Hasta la aparición de la versión 1.4 del JDK, sólo disponíamos de JTextField. Un componente bastante limitado que ponía de manifiesto las deficiencias de Swing respecto a otras aproximaciones como, por ejemplo, Delphi. Siempre he defendido a Swing respecto a las típicas aproximaciones de Windows haciendo hincapié en la bondad de su arquitectura y las posibilidades que ésta ofrece al desarrollador para extender de manera relativamente fácil (o, al menos, ortogonal) los componentes que nos ofrece. Fruto de ello fue la pequeña colección de widgets que desarrollé para JDK 1.2 que incluía, entre otros, un campo con máscara o campos especializados para números.1 Pero la verdad es que, a pesar de dichas posibilidades de expansión, si querías algo tan simple como un spinner, te lo tenías que fabricar y eso siempre da pereza y está sujeto a errores difíciles de controlar. Cuando apareció la versión 1.4 del JDK, tuve la impresión de que Sun había, de alguna manera, recogido las quejas de los usuarios y se había decidido a cubrir algunas de las necesidades básicas del desarrollador de interficies de usuario proporcionando, por primera vez en bastante tiempo, dos componentes básicos: JSpinner y JFormattedTextField. Este artículo, como su título indica, pretende profundizar en las posibilidades que nos ofrece JFormattedTextField. JFormattedTextField no es más que una extensión de JTextField que viene a cubrir algunas de las deficiencias que todos encontrábamos en él. Un campo de entrada es un componente que cumple una doble función. Por una parte, permite que el usuario entre el texto que se corresponde con un dato solicitado por la aplicación; y por otra, permite a la aplicación mostrar datos al usuario. La principal limitación que le encuentro a JTextField es que permite entrar cualquier texto y que no hay manera fácil de mostrar un texto con un formato concreto. No hay posibilidad de establecer un control sobre lo que el usuario entra ni manera sencilla de mostrar convenientemente datos de tipos tan habituales como fechas o números. Lo primero que uno piensa cuando se acerca por primera vez a JFormattedTextField es “finalmente tenemos un campo que soporta máscaras”. Todos esperábamos este componente. Sin embargo, su nombre no es algo así como JMaskedTextField. Su nombre nos insinúa que va más allá de un campo con máscaras. Se trata de un campo de entrada de texto que nos permite especificar formatos. Ciertamente, una manera de especificar un formato de entrada es mediante una máscara, pero no es el único. Además, como acabo de comentar, un campo de entrada no sólo sirve para 1 Algunos de estos widgets se pueden encontrar en mi Web personal (http://www.froses.com/ES/Descarregues/Widgets.htm).
1 de 24
entrar texto. También muestra lo que hemos entrado. Volveremos más tarde sobre este punto. Resumiendo un poco las características de este componente podemos decir que: • Distingue entre el valor del campo y el texto que lo representa • Permite especificar formatos fijos de entrada de datos, por ejemplo, mediante máscaras • Sabe aprovechar el resto de especificaciones de formato disponibles en Java para números, fechas, horas, etc. • Permite decidir si se admiten caracteres incorrectos en la entrada o no. • Permite distinguir entre modalidad de edición y modalidad de visualización. • Permite establecer dos modalidades de escritura: inserción y sobrescritura. • Permite que decidamos qué hacer con el foco si lo que el usuario ha entrado no es correcto, al beneficiarse de las nuevas capacidades del JDK 1.4 para la comprobación de valores en Swing. Bueno, creo que las posibilidades que nos ofrece JFormattedTextField son esperanzadoras e intentaré mostrar hasta qué punto. Para ello, he pensado dividir el artículo en tres partes diferenciadas con objetivos distintos. La primera, muestra los usos más habituales de JFormattedTextField y comenta cómo usar la clase javax.swing.InputVerifier para controlar el foco en base al valor entrado en el campo. El objetivo, en esta primera parte, es conseguir que el lector pueda utilizar fácilmente JFormattedTextField en sus aplicaciones. La segunda discute más a fondo la arquitectura y el funcionamiento de JFormattedTextField, y tiene como objetivo preparar al lector para poder extender las posibilidades de JFormattedTextField, extendiéndolo y creando nuevos componentes especializados. En la tercera, presento algunos widgets derivados de JFormattedTextField, y desarrollados específicamente para este artículo, que tienen un doble objetivo: mostrar al lector una posible vía de expansión del componente que nos ocupa y proporcionarle unos componentes listos para ser usados en sus aplicaciones. También proporciono algunas aplicaciones que ejemplifican tanto el uso de estos widgets como de algunos aspectos de JFormattedTexField y de las clases derivadas de JFormattedTextField.AbstractFormatter.
USOS GENERALES DE JFormattedTextField Como he comentado más arriba, el objetivo de este capítulo es proporcionar la información suficiente al lector para que pueda hacer uso de las principales funcionalidades de JFormattedTextField.
¿Qué es JFormattedTextField? JFormattedTextField es un componente derivado de JTextField que, como éste, sirve para entrar y mostrar datos. Una de las características principales de JFormattedTextField es la de permitir dar formato a los datos, tanto en el momento de entrarlos como en el de visualizarlos. Para ello, y a diferencia de JTextField, JFormattedTextField distingue entre el valor almacenado (una subclase de Object accesible mediante el método getValue()) y el texto que muestra (una java.lang.String accesible mediante getText()). En el siguiente apartado, veremos cómo podemos especificar los distintos formatos.
2 de 24
Especificación de formato El componente JFormattedTextField nos permite especificar el formato de diversas maneras:
De manera automática: Asignando un valor al campo Simplemente asignando un valor al campo, éste nos asigna un formato. Así, por ejemplo, si le asignamos una fecha, él nos la permitirá editar. El siguiente código crea un campo de entrada para fechas con el formato por defecto: JFormattedTextField efFecha = new JFormattedTextField(new Date());
El campo mostrará la fecha actual con el siguiente formato: 19-ago-2002
Pero no sólo nos presenta la fecha. Nos permite editarla de una manera sencilla y sin error posible. Si colocamos, por ejemplo, el cursor sobre el mes y pulsamos la flecha hacia arriba, el mes cambia y pasa a ser sept. Si pulsamos la flecha hacia abajo, el mes será jul. El mismo comportamiento se da para el día y el año. Además, el comportamiento es inteligente. Supongamos que la fecha sea 28 de febrero de 2002 y que aumentemos el día. La nueva fecha sería 1 de marzo de 2002. Lógicamente, si el año fuera el 2000 (bisiesto) la fecha propuesta sería el 29 de febrero de 2000.
Mediante una máscara Podemos utilizar una máscara para determinar el formato. Por ejemplo, si quisiéramos crear un campo para entrar códigos de cuenta corriente, podríamos hacerlo así de fácil: MaskFormatter mfCC = new MaskFormatter(“####-####-##-##########”); mfCC.setPlaceholderCharacter('_'); JFormattedTextField efCC = new JformattedTextField(mfCC);
El campo tendría el siguiente aspecto: ____-____-__-__________
Fijémonos que las partes escribibles se representan con el carácter de subrayado que hemos especificado con setPlaceholderCharacter(). La siguiente tabla resume los caracteres utilizables en una máscara:
Carácter
Descripción
#
Un número
?
Una letra
A
Una letra o un número
*
Cualquier cosa
U
Una letra que será pasada a mayúscula
L
Una letra que será pasada a minúscula
H
Un dígito hexadecimal (A-F, a-f, 0-9)
'
Carácter de escape para otro carácter de máscara
Mediante descriptores de formato ya existentes Java nos ofrece una amplia gama de especificaciones de formato para fechas, horas, números y monedas. Todos ellos pueden ser utilizados, directa o indirectamente, para especificar el formato 3 de 24
usado por el campo. Ejemplificaremos algunos de ellos. Más arriba, hemos mostrado cómo especificar un formato de fecha simplemente pasando una al constructor del campo. El resultado es vistoso, pero cualquier persona que entre datos nos dirá que es poco práctico. Sería más interesante algo del estilo de dd/mm/aa. El siguiente código nos muestra cómo hacerlo:2 JFormattedTextField efFecha = new JFormattedTextField(new SimpleDateFormat(“dd/MM/yy”));
El resultado obtenido sería: 19/08/02. El comportamiento de las flechas sería el ya descrito. Si lo que pretendemos es entrar un número con un máximo de dos decimales: JFormattedTextField efNum = new JformattedTextField(new DecimalFormat(“#,###.00”));
Si nos interesa que el usuario entre importes en euros en nuestro campo: JFormattedTextField efMon = new JformattedTextField(NumberFormat.getCurrencyInstance()); efMon.setValue(new Integer(1000));
Lo que veríamos sería: 1.000,00 €
Admitir o no caracteres incorrectos A veces, nos puede interesar permitir que el usuario pueda entrar caracteres incorrectos. Para hacerlo, usaremos formateadores propios de JFormattedTextField. Veamos el siguiente ejemplo:3 JFormattedTextField efNum = new JformattedTextField(new DecimalFormat(“#,###.00”)); NumberFormatter nf = (NumberFormatter)efNum.getFormatter(); nf.setAllowsInvalid(true);
Disponemos de tres formateadores especiales derivados todos ellos de la clase javax.swing.JFormattedTextField.AbstractFormatter: • MaskFormatter – Utilizado para máscaras y derivado directamente de AbstractFormatter. • NumberFormatter – Utilizado para números y derivado de una subclase de AbstractFormatter: InternationalFormatter. • DateFormatter – Utilizado para fechas y horas y derivado, como el anterior, de InternationalFormatter.
Insertar o sobrescribir Algo de sumo interés es poder especificar si insertamos o sobrescribimos caracteres. Lo ideal sería que se pudiera decidir pulsando la tecla , pero esto no es inmediato. El siguiente ejemplo nos indica cómo permitir la sobrescritura:4 2 Los componentes DateField, DecimalField, DecimalFieldScrollable, IntegerField, MoneyField y PercentField, comentadas más abajo, ilustran la asignación de diversos formatos y se incluyen en el código fuente de este artículo. 3 La aplicación de ejemplo IntegerFieldTest, comentada más abajo, nos permite comprobar el funcionamiento de setAllowsInvalid(boolean). 4 El componente DefaultJFormattedTextField, comentado más abajo, implementa los métodos de inserción y sobrescritura y se incluye en el código fuente de este artículo.
4 de 24
JFormattedTextField efNum = new JformattedTextField(new DecimalFormat(“#,###.00”)); NumberFormatter nf = (NumberFormatter)efNum.getFormatter(); nf.setOverrideMode(true);
Modalidad de edición y de visualización Imaginemos que tenemos un campo para entrar importes en euros como el que hemos descrito más arriba. El resultado obtenido es interesante; se ve bien, con el símbolo de euro al final y los separadores de millares y la coma decimal siguiendo las directrices de nuestro país. Pero una vez más, un usuario que se dedique a entrar datos nos diría que es incómodo. Normalmente, se usa el teclado numérico y uno no tiene que ir a buscar la coma al teclado alfanumérico. Usa un punto para indicar la coma. Claro que si bien es práctico entrar 1234.35, queda mal cuando se visualiza. Tenemos, pues, un conflicto: lo que es práctico para la entrada de datos no es claro en la visualización. JFormattedTextField nos permite resolver este conflicto especificando un formato para la edición y otro para la visualización. Cuando el foco esté en el campo, usará el de edición y cuando éste pierda el foco, usará el de visualización.5 Veamos cómo hacerlo para nuestro campo de importes en euros: // Creamos el campo JFormattedTextField efDecimal = new JformattedTextField(); // Formato de visualización NumberFormat dispFormat = NumberFormat.getCurrencyInstance(); // Formato de edición: inglés (separador decimal: el punto) NumberFormat editFormat = NumberFormat.getNumberInstance(Locale.ENGLISH); // Para la edición, no queremos separadores de millares editFormat.setGroupingUsed(false); // Creamos los formateadores de números NumberFormatter dnFormat = new NumberFormatter(dispFormat); NumberFormatter enFormat = new NumberFormatter(editFormat); // Creamos la factoría de formateadores especificando los // formateadores por defecto, de visualización y de edición DefaultFormatterFactory currFactory = new DefaultFormatterFactory(dnFormat, dnFormat, enFormat); // El formateador de edición admite caracteres incorrectos enFormat.setAllowsInvalid(true); // Asignamos la factoría al campo efDecimal.setFormatterFactory(currFactory);
Editamos en formato inglés (usamos el punto como separador decimal) y sin separadores de millares. Visualizamos lo que hemos entrado en el formato de moneda y numérico de nuestro país. En este caso, el euro como símbolo de moneda, el punto como separador de millares y la coma como separador decimal, pero si el programa se ejecutara en Inglaterra, verían el símbolo de la Libra, la coma sería el separador de millares y el punto, el separador decimal. 5 Los componentes DecimalField, DecimalFieldScrollable, IntegerField, PercentField y MoneyField, comentadas más abajo e incluidas en el código fuente de este artículo, ilustran el uso de formatos distintos para la edición y la visualización.
5 de 24
La siguiente imagen muestra nuestro campo en modalidad de edición:
La siguiente imagen muestra el mismo campo en modalidad de visualización:
Si observamos el código, veremos que opto por admitir caracteres incorrectos en edición. El motivo es que NumberFormatter define el comportamiento de las teclas más (+) y menos (-) haciendo que sean las responsables del cambio de signo. No escriben el signo, sino que lo cambian. Por ello, y para hacer que la escritura se parezca más a la que solemos utilizar, he decidido permitir el uso de caracteres “incorrectos” en el ejemplo.
Control del foco: InputVerifier Uno de los problemas típicos en el desarrollo de interficies gráficas para la entrada de datos es decidir qué se hace cuando el usuario que ha rellenado incorrectamente un campo quiere pasar al siguiente. En algunos casos nos puede interesar que lo pueda hacer, pero en otros no. Imaginemos que necesitamos un campo para entrar números de DNI, con su letra. Este campo, en nuestra aplicación, es clave ya que a partir de él, se obtiene el resto de la información. Así, pues, si el número entrado fuera incorrecto, no debiéramos permitir que el usuario saltara al campo siguiente. Anteriormente, había que utilizar algún truco (que no viene al caso) para evitar que el foco se fuera al siguiente componente. En la versión 1.3 del JDK, se nos facilita bastante el trabajo. Podemos asignar al campo una clase que extienda InputVerifier mediante el método setInputVerifier() de manera que sea esta clase la que controle si el usuario podrá salir del campo (pasar el foco a otro componente) o no. Para seguir con el ejemplo del DNI, proponemos un pequeño ejemplo que ilustre el procedimiento.6 Vamos a definir una máscara que facilite la entrada de DNIs en nuestro campo: // Definición de la máscara de DNI MaskFormatter maskDNI = null; try { maskDNI = new MaskFormatter("##.###.###-U"); } catch (ParseException e) { // De momento, no hacemos nada } // El caràcter a mostrar en las posiciones escribibles es el // subrayado. maskDNI.setPlaceholderCharacter('_');
La máscara obligará al usuario a entrar ocho dígitos y una letra que será pasada a mayúsculas. Además, mediante el método setPlaceholderCharacter(), asignamos un carácter de subrayado para que sirva de pauta al usuario, indicándole las posiciones editables del campo. El carácter U que vemos en la máscara obligará al usuario a escribir la letra del DNI y pasará dicha letra a mayúsculas. La máscara se encargará, pues, de que el usuario escriba dígitos y letras donde corresponda, pero el valor entrado no será entregado al campo directamente hasta que pulsemos la tecla Intro. Al 6 El componente DNIField, cuyo código fuente se incluye, es un ejemplo completo de campo destinado a la entrada de DNIs.
6 de 24
cambiar de foco, el MaskFormatter no entrega el valor. Hay que decirle explícitamente que si lo editado es válido, pase el valor al campo. Para ello, utilizaremos el método setCommitsOnValidEdit(boolean). maskDNI.setCommitsOnValidEdit(true);
Si comentamos esta línea, veremos que al entrar un DNI incorrecto nos deja cambiar el foco debido a que el valor no se ha entregado al campo para que determine si debe permitir el cambio de foco o no. Finalmente, creamos el campo: JFormattedTextField efDNI = new JformattedTextField(maskDNI);
En este momento, ya hemos dotado a nuestro campo de un cierto control para entrar DNIs: •
Nos fuerza a escribir los números y la letra en los lugares que corresponde
•
Pasa automáticamente la letra final de control a mayúsculas
Sin embargo, la máscara no nos proporciona todo el control que necesitamos. Si la persona que entra los datos se equivoca en la letra de control, el error queda registrado. Necesitamos, pues, impedir que la persona que entra los datos entre un DNI erróneo (aunque, y de eso se encarga la máscara, bien formado). La versión 1.3 del JDK incorpora un nuevo método a la clase javax.swing.JComponent: setInputVerifier(InputVerifier v). Este método nos permite asignar a un JFormattedTextField un algoritmo de control del contenido entrado. Este algoritmo de control se hallará embebido en una subclase de InputVerifier. La clase InputVerifier es abstracta y obliga a sus subclases a implementar el método public boolean verify(JComponent input). Este método devuelve true, si la comprobación es correcta, o false, si no lo es. Veamos ahora la clase derivada de InputVerifier que se encarga de verificar si el DNI entrado es correcto y permite al campo decidir si autoriza, o no, el cambio de foco. Disponemos de la clase CIF_NIF, con el método estático boolean isNIFOK(String DNI) que nos devuelve true o false en función del DNI pasado como parámetro.7 Creamos, por ejemplo, la clase ValidateDNI que extiende InputVerifier: class ValidateDNI extends InputVerifier { /** * Sobrescribimos el método del padre para realizar la * comprobación del DNI entrado. */ public boolean verify(JComponent input) { if (input instanceof JFormattedTextField) { Object o = ((JFormattedTextField)input).getValue(); if (o == null) return true; String value = o.toString(); return CIF_NIF.isNIFOK(value);
}
} return false;
El método verify() se encarga de llamar al método CIG_NIF.isNIFOK() que contiene el algoritmo de verificación de DNIs. Si este método da el DNI por bueno, el usuario podrá cambiar el 7 La clase CIF_NIF, comentada más abajo, se incluye en el código fuente de este artículo.
7 de 24
foco. Si no es así, el foco permanecerá en el campo de DNI.8
¿CÓMO FUNCIONA EN REALIDAD? En la primera parte de este artículo he intentado que el lector disponga de la información suficiente sobre JFormattedTextField para que se anime a usarlo en sus aplicaciones. Buena parte de la información que el lector ha encontrado en la primera parte puede, también, encontrarla en diversos tutoriales. El de Sun, por ejemplo, nos muestra algunos ejemplos parecidos a los que he descrito. Sin embargo, ni sus ejemplos, ni los que yo he presentado son realmente serios. Simplemente pretenden ilustrar algunas de las funcionalidades básicas de los distintos elementos implicados en JFormattedTextField. Para esta segunda parte, me he propuesto intentar proporcionar a lector la información necesaria para poder usar aquellos aspectos poco evidentes de JFormattedTextField. La información que aquí presento es fruto del estudio directo de las distintas APIs implicadas en JFormattedTextField y del código fuente de las distintas clases. Espero que le ahorre trabajo al lector.
Observaciones generales JFormattedTextField siempre almacena como valor un objeto (una subclase de Object). Este valor, sin embargo, debe ser representado como una tira de caracteres (una String) ya que JFormattedTextField no es sino una subclase de JTextField, quien, como su propio nombre indica, es un campo de texto. Esto no es problema para JTextField, ya que siempre almacena objetos de tipo String, pero para JFormattedTextField no es tan evidente. Alguien tiene que transformar este objeto en una String para que pueda ser representado. Volviendo al nombre de nuestro componente, JFormattedTextField, observamos que contiene el adjetivo formatted (formateado, con formato). Este es un detalle importante. La transformación del objeto almacenado a String comporta un proceso de aplicación de formato. Resumiendo, JFormattedTextField : 1. Toma el valor que le asignamos, 2. crea una tira de caracteres convenientemente formateada según algún criterio y 3. la muestra en el campo Pero hemos comentado más arriba que el valor no sólo se asigna y se ve, sino que se edita. Es decir, que hay un formato de edición y que quien controla este formato se encarga de decidir, por ejemplo, si en tal posición podemos escribir un número o una letra o si podemos escribir o no en una posición concreta.
El formato Los responsables del formato, tanto del de edición como del de visualización, son los formateadores. Un formateador es, en realidad, una clase derivada de JFormattedTextField.AbstractFormatter y cumple diversas funciones. En modo de edición, decide qué se puede escribir y dónde y cómo y cuándo pasa el valor editado al 8 El componente de ejemplo DNIField, descrito más abajo, se incluye en el código fuente de este artículo.
8 de 24
campo. Por ejemplo, si usamos un formato numérico, y no permitimos la inserción de caracteres incorrectos, no podremos teclear ninguna letra. En el campo de DNI que he puesto de ejemplo, nunca podremos escribir sobre el guión de separación de la letra de control, a pesar de que MaskFormatter use el modo de sobrescritura por defecto. En algunos casos, el formateador también define el comportamiento del teclado. Por ejemplo, DateFormatter permite el incremento o decremento de los distintos campos de una fecha (día, mes, etc.) mediante las flechas del teclado. En modo de visualización, decide cómo se muestra el valor almacenado en el campo. Así, en la aplicación de ejemplo MoneyFieldTest, podremos comprobar que el valor almacenado como BigDecimal en un campo MoneyField, en modo de visualización, se muestra como un número y un carácter de moneda. Un formateador transforma un valor (esto es, un Object) a una tira de caracteres usando el método valueToString(Object) y una tira de caracteres a un valor (esto es, un Object) usando el método stringToValue(String). Estos son los métodos que le permiten almacenar lo editado como una subclase de Object y mostrar este valor como una tira de caracteres en el campo. La siguiente figura muestra la jerarquía de clases de los distintos formateadores:
Figura 1: Jerarquía de formateadores
El más sencillo es DefaultFormatter que se utiliza para formatear objetos arbitrarios. El método valueToString(), simplemente, devuelve el resultado del método toString() del valor almacenado. Y para almacenar una tira como valor, usa un constructor de la clase del objeto que tenga como parámetro una tira de caracteres. InternationalFormatter es una subclase de DefaultFormatter que usa java.text.Format para pasar de String a Object y viceversa. Por defecto, sólo admite caracteres correctos (setAllowsInvalid(false)), por lo que no es conveniente modificar esta propiedad si no queremos tener problemas. 9 de 24
También se encarga de ajustar la posición del cursor, situándolo sobre aquellas posiciones en las que se puede escribir. NumberFormatter es una subclase de InternationalFormatter diseñada especialmente para la entrada de números. Entre otras cosas, establece el comportamiento de la tecla de manera que, estemos donde estemos del campo, convierte el número entrado en negativo.9 La tecla pasa el número a positivo. Esto es, desaparece el signo menos. DateFormatter es una subclase de InternationalFormatter diseñada especialmente para la entrada de fechas. Como hemos comentado más arriba, define el comportamiento de las flechas del teclado para aumentar o disminuir días, meses, años, etc. La clase MaskFormatter extiende DefaultFormatter y está pensada especialmente para la edición de tiras de caracteres con formatos específicos. Como hemos visto más arriba, se basa en una máscara que indica qué caracteres se pueden escribir en una posición determinada.
Personalización de los formateadores Todos los formateadores tienen comportamientos por defecto. Por ejemplo, DateFormatter no admite caracteres incorrectos en la edición y MaskFormatter está siempre en modo de sobrescritura. Pero podemos cambiar alguno de estos comportamientos o acceder a alguna de sus características ya que nos proporcionan métodos de acceso. Así, DefaultFormatter nos proporciona los siguientes métodos:10 •
setAllowsInvalid(boolean): Nos permite decidir si aceptamos caracteres incorrectos o no.
•
setCommitsOnValidEdit(boolean): Nos permite, en modo de edición, decidir cuándo se libra el valor de lo que estamos escribiendo al campo. Si usamos true como parámetro, cada vez que escribamos algo se validará y, si es correcto, se asignará como valor del campo.
•
setOverwriteMode(boolean): Nos permite decidir si la modalidad de edición es sobrescritura (true) o inserción (false).
InternationalFormatter nos ofrece tres posibilidades interesantes como: •
setMaximum(Comparable): Para establecer el valor máximo admisible por el campo.
•
setMinimum(Comparable): Para establecer el valor mínimo admisible por el campo.11
•
getFields(int offset): Para determinar qué campo (entero, decimal, signo, etc.) se corresponde con una posición determinada.12
9 Curiosamente, si no hemos entrado ningún texto o valor y no permitimos la entrada de caracteres incorrectos, pulsar la tecla no sirve de nada. 10 En los componentes que he desarrollado de ejemplo, he considerado necesario facilitar el acceso a alguno de los métodos de personalización de formateadores desde el propio componente. Así, por ejemplo, los componentes ponen a disposición del programador métodos como setOverwriteMode(boolean) o setAllowInvalidCharacters(boolean). 11 Los métodos setMaximum(Comparable) y setMinimum(Comparable) se han implementado en los componentes de ejemplo DefaultNumberField, IntegerField, DecimalField. Esto permite establecer rangos de valores aceptables para instancias de dichos componentes y de sus subclases PercentField y MoneyField. 12 Véase el método sum(int sign) del componente de ejemplo DefaultNumberField.
10 de 24
A falta de pruebas intensivas, observo que si, habiendo definido un rango de valores aceptable para un InternationalFormater, asignamos un valor fuera del rango definido mediante el método setValue(object), no se tiene en cuenta el rango y el valor se asigna sin problemas al campo.13 MaskFormatter nos obsequia con algunos métodos realmente útiles: •
setInvalidCharacters(String): Nos permite especificar una lista de caracteres que no serán aceptados por el campo. Supongamos, por ejemplo, que tenemos un campo con una máscara para entrar códigos de producto. La máscara podría ser parecida a ésta: “U###”. Es decir, un carácter alfabético que será pasado a mayúsculas, seguido de tres dígitos. Es de esperar que el usuario no entre una letra acentuada en la primera posición de la máscara, pero los usuarios son muy listos y seguro que más de uno lo intentará. Si recordamos la sintaxis para la especificación de máscaras, observaremos que no hay ninguna manera de especificar que no admitiremos caracteres acentuados. Por lo tanto, el usuario astuto puede entrar una Á y fastidiarnos la aplicación. La manera de impedirlo es, pues, usando el método setInvalidCharacters() al que se le pasará como parámetro una String con todos los caracteres acentuados (en mayúsculas).
•
setValidCharacters(String): Nos permite especificar la lista de caracteres que serán aceptados. Se trata del método complementario del anterior. Siguiendo con el mismo ejemplo, podríamos especificar que los caracteres aceptables son “ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789”.
•
setPlaceHolder(String) y setPlaceHolderChracter(char): permiten, como hemos visto más arriba, especificar los caracteres que no se mostrarán en las posiciones escribibles. En el ejemplo del campo de DNI, el carácter utilizado era el de subrayado.
•
setValueContainsLitteralCharacters(boolean): Nos permite decidir si los valores entrados, con setValue(), o recuperados, con getValue(), contienen también los literales incluidos en la máscara (true) o no (false). Así, si nuestra máscara para DNI es “##.###.###-U” y hemos especificado true, getValue(), nos devolverá 12.345.678-A. Si, por el contrario, especificamos false, nos devolverá 12345678A. De igual manera, si hemos especificado true, el parámetro para setValue() tendrá que ser “12.345.678-A” y si hemos especificado false, “12345678A”.
Asignación de formatos La única manera que tenemos de asignar formatos a un JFormattedTextField es mediante una subclase de JformattedTextField.AbstractFormatterFactory. Normalmente, pues, usaremos la única existente: DefaultFormatterFactory. DefaultFormatterFactory permite especificar cuatro AbstractFormatters para tres situaciones distintas: 1. Formato por defecto: Se usa si no hay otro definido para una situación concreta, lo cual implica que se usará tanto como formato de edición como de visualización y como nulo, si 13 Esto es debido a las diferencias de comportamiento entre setValue() y commitEdit() que se comentan más abajo. Este comportamiento ha sido “corregido” en los componentes numéricos de ejemplo. Si se intenta asignar un valor fuera de rango, se lanza una IllegalArgumentException.
11 de 24
sólo especificamos éste. 2. Formato de edición: Se usa cuando el campo tiene el foco y sirve para facilitar la edición 3. Formato de visualización: Se usa cuando el campo no tiene el foco y se utiliza para mostrar el valor 4. Formato nulo: Se usa cuando el campo tiene un valor nulo (null)
Los procesos En el apartado Observaciones generales, he presentado un breve esquema de los principales procesos que realiza JFormattedTextField para almacenar un valor y mostrarlo. Vamos ahora a entrar con un poco más de detalle en estos procesos.
Asignación del valor Mientras estamos escribiendo, JFormattedTextField no asume, en principio, ningún valor. Lo que escribimos está, por así decirlo, en el aire y sólo se puede obtener con el método getText() común a todos los JtextComponents. La excepción a dicho comportamiento está en el hecho de especificar setCommitsOnValidEdit(true) para el formateador correspondiente, como hemos comentado más arriba. Hay dos maneras de asignar un valor al campo: de manera automática y por programa.
Asignación de valor automática Cuando pulsamos o cuando cambiamos el foco, empieza el proceso de asignación automática de valor. A grandes rasgos, lo que sucede al pulsar o cambiar el foco es lo siguiente: 1. Ejecuta el método commitEdit(). Este mira si hay un AbstractFormatter definido que se encargue de transformar el texto en un objeto. Si no lo encuentra, no asigna valor. Es decir, si usamos el método getValue() de JFormattedTextField, nos devolverá null. 2. Si encuentra el AbstractFormatter, obtendrá el texto entrado mediante getText() y usará el método stringToValue() para obtener el objeto correspondiente y asignarlo. El proceso de transformación de una tira a un objeto, comporta un análisis de la tira y es posible que no funcione. En este caso, el AbstractFormatter generará una ParseException y no se asignará ningún valor. Un ejemplo típico sería el intento de asignación de la tira 1OOO (obsérvese que en vez de ceros hay os mayúsculas) mediante un NumberFormatter. Éste intentará convertir esta tira en un Long y se producirá una excepción. El resultado es que getValue() nos devolverá null o el último valor correcto entrado. Pero JFormattedTextField nos permite afinar un poco más en el caso de la asignación de valor por cambio de foco y nos permite establecer políticas a seguir. Para ello usaremos el método JFormattedTextField.setFocusLostBehavior(int behavior). La siguiente tabla las resume:
12 de 24
Valor JFormattedTextField.REVERT
Descripción Revierte lo editado al último valor almacenado en el campo. Es decir, al valor devuelto por el método getValue(). Si no hay ningún valor almacenado previamente o no forzamos un setValue(), por ejemplo, pulsando , el contenido de la edición actual se perderá.
JFormattedTextField.COMMIT
Entrega el valor actual al perder el foco. Si el valor editado no es considerado un valor legal por el AbstractFormatter (esto es, se lanza una ParseException), entonces el valor editado no cambiará pero no se asignará como valor. Resumiendo, se comporta como COMMIT, pero si el valor editado no es correcto, no limpia el campo.
JFormattedTextField.COMMIT_OR_REVERT Es similar a COMMIT, pero si el valor que escribimos no es legal (esto es, el AbstractFormatter lanza una ParseException), se comporta como REVERT y limpia el contenido del campo. JFormattedTextField.PERSIST
No hace nada, no obtiene un nuevo AbstractFormatter y no actualiza el valor. Sea correcto o incorrecto lo que entremos, mantiene el valor correcto anterior pero no limpia el campo.
Tabla 1: Políticas de asignación de valor en la pérdida de foco
La política por defecto es COMMIT_OR_REVERT.14
Asignación de valor por programa A grandes rasgos, el proceso de asignación funciona de manera similar a la asignación automática. Disponemos de dos métodos para asignar un valor: setValue(Object valor) y commitEdit(), descrito más arriba. El más más usado es setValue(Object). El proceso, resumido, es el siguiente: 1. Mira si dispone de un formateador (en este caso, mira si hay definida una FormatterFactory). 2. Si no hay ninguna definida, la crea basándose en la clase del objeto pasado como parámetro. Este objeto debe ser una instancia de DateFormat, NumberFormat, Format, Date o Number. JFormattedTextField creará una DefaultFormatterFactory basada en el tipo del valor que intentamos asignar. Si se trata de una instancia de DateFormat, 14 La aplicación de ejemplo IntegerFieldTest permite hacer pruebas con las distintas políticas de pérdida de foco y con el método setCommitsOnValidEdit(boolean).
13 de 24
creará una DefaultFormatterFactory basada en un DateFormatter, si es una instancia de NumberFormat, usará un NumberFormatter, si es una Date, usará DateFormatter, si es un Format, usará InternationalFormatter, si se trata de un Number, usará un NumberFormatter como formateador por defecto y para visualización y un NumberFormatter especial que usa un DecimalFormat con el patrón “#.#” para la edición. 3. Asigna el valor. Observamos una diferencia notable entre commitEdit() y setValue(). El primero usa el método stringToValue() para obtener el valor a pasar como parámetro de setValue(), mientras que éste asigna el valor directamente. Esto es, commitEdit() puede recibir una ParseException como resultado de invocar stringToValue() si el formateador detecta un error de formato. Hemos de hablar, pues, de un comportamiento asimétrico entre commitEdit() y setValue() que no suele tener consecuencias. Sin embargo, hay algún caso en que esta asimetría comporta algún que otro problema. Veamos un ejemplo: Nosotros construimos un campo para entrada de DNIs con un MaskFormatter, tal y como hemos visto más arriba. Pero ahora decidimos que el valor del campo no debe contener literales y lo especificamos usando el método valueContainsLitteralCharacters(false). Si asignamos como valor, por ejemplo, “12.345.678-Z”, usando setValue(), no se produce ningún error ya que no interviene para nada el formateador. Es más, getValue() devuelve “12.345.678-Z” en vez de “12345678Z”.15
15 Esta asimetría ha sido corregida para el componente de ejemplo DNIField. El método setValue() llama siempre al formateador para comprobar errores de formato.
14 de 24
APLICACIONES DE EJEMPLO Para ilustrar los conceptos tratados en este artículo, he desarrollado una serie de aplicaciones. Dichas aplicaciones se incluyen con el código fuente correspondiente para que el lector pueda estudiarlas y modificarlas a su conveniencia. He procurado que los ejemplos no sean abstractos sino que sean prácticos y usables para cualquier desarrollador. He clasificado los ejemplos en tres categorías: 1. Componentes auxiliares: se usan en las aplicaciones de demostración y tienen poco que ver con JFormattedTextField. A pesar de ello, creo que algunas de ellas pueden ser bastante interesantes para los desarrolladores. 2. Componentes derivados de JFormattedTextField: son un conjunto de subclases de JFormattedTextField que, a mi entender, cumplen un doble objetivo. Por una parte ilustran la mayor parte de conceptos relacionados con JFormattedTextField y con los distintos formateadores y por otra, constituyen un conjunto de componentes (en inglés, los llamarían widgets) especializados en distintas tareas (entrada de números, porcentajes, fechas y DNIs) listos para ser utilizados por cualquier desarrollador. La estrategia seguida para el desarrollo de los componentes ha sido doble. Por una parte, he añadido funcionalidades que no están directamente relacionadas con JFormattedTextField, como la aritmética de fechas, la asignación de escala a un valor decimal o la autocompleción de un DNI y, por otro, he hecho emerger, a nivel de componente, propiedades del formateador, como la asignación dinámica del formato de representación de fechas o la especificación de rango para los campos numéricos. 3. Aplicaciones de demostración: se trata de pequeñas aplicaciones que ilustran tanto el funcionamiento de los distintos componentes descritos en el punto anterior, como el de algunos aspectos de JFormattedTextField y de los formateadores. A continuación, paso a describir brevemente cada una de ellas.
Componentes auxiliares CIF_NIF Es una clase que proporciona una serie de métodos estáticos para la verificación de CIFs y NIFs españoles. Contiene una amplia documentación sobre las fuentes en las que me he basado para escribirla y la casuística que se trata. Destacaría las siguientes funcionalidades: •
Determina si una tira se corresponde con un NIF o con un CIF
•
Determina si un NIF o un CIF son correctos
•
Dado un NIF sin letra de control, calcula y devuelve dicha letra
•
Trata NIEs (NIFs para extranjeros)
Esta clase se utiliza en el componente DNIField.
BoundJSpinner Es una subclase de JSpinner que tiene la propiedad value bound. Esto es, cada vez que el valor 15 de 24
de BoundJSpinner cambia, se genera un PropertyChangeEvent. Se usa en las demostraciones de algunos de los componentes.
ButtonGroupJPanel Es una subclase de JPanel que facilita el uso de JRadioButons desde un editor visual. Si queremos un comportamiento normal de mutua exclusión de JRadioButons (esto es, que cuando se pulse en uno el que estaba seleccionado deje de estarlo), es necesario añadir todos los JRadioButons a un ButtonGroup. ButtonGroupPanel, se encarga de ello por nosotros. ButtonGroupPanel está basado en un ejemplo de Scott Stanchfield (http://www.javadude.com) y se usa en las demostraciones de algunos componentes.
OverwriteCaret Es una subclase de DefaultCaret que dibuja un cursor horizontal. Se utiliza para indicar que estamos en modalidad de sobrescritura.
Pair Un simple bean no visual que mantiene una pareja de tipo clave/descripción. Se usa en los JCombobox de algunas de las demostraciones de los componentes.
Componentes derivados de JFormattedTextField Son componentes de ejemplo, usables,16 que llevan a la práctica todo lo que he intentado explicar sobre JFormattedTextField y su entorno. Recomiendo vivamente al lector que estudie con cariño el código fuente y lea los javadocs de las distintas clases. Creo que esto va a ser más práctico (y más corto) que entrar en los detalles de diseño de cada una de los componentes.
EnhancedJFormattedTextField Es una interface que establece los métodos, y por ende las funcionalidades, generales del conjunto de componentes.
DefaultJFormattedTextField Es una subclase abstracta de JFormattedTextField que implementa la interface EnhancedJFormattedTextField y que contiene código para funcionalidades comunes al resto de los componentes. Destaco las siguientes: •
Gestión y creación de la AbstractFormatterFactory usada por los distintos subcomponentes.
•
Posibilidad de establecer la modalidad de escritura y mostrar un cursor diferente para cada modalidad.
•
La asignación de dicha funcionalidad a la tecla , que intercambia las dos modalidades de escritura.
16 A pesar de haber realizado una infinitud de pruebas y de diseñar y ejecutar pruebas unitarias, puede que los componentes no se comporten como debieran. Los proporciono a guisa de ejemplos y no me responsabilizo de los efectos colaterales que se deriven de su uso en producción.
16 de 24
•
Implementa el método clear() que permite borrar el contenido de un campo.
•
Hace accesibles, a nivel de componente, el uso de los métodos setCommitsOnValidEdit(boolean) y getCommitsOnValidEdit() de DefaultFormatter.
•
Implementa, a nivel de componente, la política de aceptación de caracteres incorrectos del formateador.
•
Implementa, a nivel de componente, la política de establecimiento y gestión de rangos de valores aceptables.
•
Implementa el método isEmpty() que nos indica si el campo está vacío..
DefaultNumberField Es una subclase abstracta de DefaultJFormattedTextField que contiene código para las funcionalidades comunes de los campos numéricos (DecimalField, IntegerField, MoneyField y PercentField). Por ejemplo, define el comportamiento del teclado para que las flechas sirvan para incrementar o decrementar el valor almacenado en el campo o permite establecer un rango de valores para los componentes numéricos.
DecimalField Extiende DefaultNumberField adaptándolo a la presentación y edición de números decimales. Destacaré las siguientes funcionalidades: •
Distingue el modo de presentación, en el que muestra separadores de millares y un carácter de separación decimal acorde con el Locale, del de edición, en el que se facilita el uso del teclado numérico
•
Permite establecer diversas políticas de redondeo
•
Permite determinar la escala
•
Los valores entrados se almacenan siempre como BigDecimals
•
Se le puede asignar cualquier valor derivado de Number, pero también valores de tipos nativos (int, long, byte, short, float, double, etc.). Internamente, se almacenan como BigDecimals
IntegerField Es una subclase de DefaultNumberField que facilita la edición y presentación de números enteros. Se le puede asignar cualquier valor derivado de Number (si el valor tiene decimales, sólo se toma la parte entera) pero también valores de tipos nativos (int, long, byte, short, float, double, etc.). Internamente, se almacenan como BigInteger.
MoneyField Extiende DecimalField asignando como formato de visualización el de moneda.
PercentField Extiende DecimalField asignando como formato de visualización el de porcentaje. 17 de 24
DNIField Es una subclase de DefaultJFormattedTextField que facilita la entrada y validación de DNIs españoles mediante una máscara de entrada. Funcionalidades destacables: •
Permite determinar si el DNI entrado es correcto (boolean DNIField.isOK())
•
Permite la activación y desactivación del proceso de verificación. Si está activada la verificación y el valor entrado no es correcto, se deshabilita el cambio de foco.
•
Permite completar el DNI con la letra de control correcta pulsando o, también, cuando el campo pierde el foco.
•
Evita la entrada de letras de control no admisibles para DNIs.
DateField Es una subclase de DefaultFormattedTextField que facilita la entrada, visualización y manejo de fechas y horas. Destaco las siguientes funcionalidades: •
Admite valores asignables de tipo Date y Calendar
•
Permite cambiar dinámicamente el formato de visualización (el de edición es el mismo que el de visualización) especificando patrones con sintaxis de SimpleDateFormat
•
Implementa una aritmética simple de fechas. Permite añadir o quitar días, semanas, meses, años, horas, minutos o segundos a la fecha almacenada como valor en el campo de manera sencilla (p.e. addMonths(3), añadiría tres meses, addWeeks(-3) restaría tres semanas a la fecha).
StringField Extiende DefaultFormattedTextField para facilitar la escritura de texto. Si bien Swing nos proporciona ya un campo de texto, JTextField, considero que no es suficiente para cubir algunas de las necesidades más importantes de un campo de este estilo. Así, por ejemplo, JTextField sólo nos proporciona un método de escritura, la inserción, y no nos permite determinar la longitud máxima del texto a escribir. Este segundo aspecto es importante si tenemos ligado el texto a alguna columna de una tabla en una base de datos. Si usamos JTextField, no podemos asegurar que el texto entrado por el usuario tenga una longitud inferior o igual a la definida para la columna de la tabla, por lo que nos veremos obligados ha controlar este hecho por programa. StringField nos permite delegar en la interficie dicho control al permitirnos determinar la longitud máxima admisible para el texto. StringField nos permite, también, establecer una política de recorte para la asignación de valor. Si la activamos, usando el método setStripOn(boolean), al intentar asignar un valor con una longitud superior a la permitida, éste será recortado convenientemente antes de ser asignado. Si no la tenemos activada, consecuentemente, lanzará una IllegalArgumentException al intentar asignar un valor con una longitud superior a la permitida.
18 de 24
Aplicaciones de demostración Siempre se ha dicho que una imagen (en nuestro caso, una aplicación visual) vale más que mil palabras. Es por este motivo que he creado una serie de aplicaciones de escritorio que pretenden ejemplificar las funcionalidades de cada uno de los componentes comentados en el apartado anterior. Así, pues, el lector dispone de un ejemplo de uso para cada uno de los componentes derivados de JFormattedTextField: •
DecimalFieldTest – Ilustra las posibilidades de DecimalField
•
IntegerFieldTest – Ilustra las posibilidades de IntegerField y las distintas políticas de comportamiento de JFormattedTextField con la pérdida de foco. También muestra las posibilidades de uso de setAllowsInvalid(). Permite, a su vez, verificar las asignaciones de valor cuando hay pérdida de foco en función de la política definida.
•
MoneyFieldTest – Ilustra las posibilidades de MoneyField
•
PercentFieldTest – Ilustra las posibilidades de PercentField
•
DNIFieldTest – Ilustra las posibilidades de DNIField
•
DateFieldTest – Ilustra las posibilidades de DateField
•
StringFieldTest – Ilustra las posibilidades de StringField
Estas aplicaciones se pueden ejecutar por separado o bien a través de la clase Pruebas que nos permite decidir qué aplicación ejecutar. Los ejemplos de uso son, creo, bastante intuitivos, sin embargo, hay algunos trucos poco evidentes: •
En la aplicación DNIFieldTest, no hace falta escribir siempre la letra del DNI, si se han entrado todos los números del DNI, basta con pulsar Ctrl-Espacio y la letra correcta aparecerá por arte de magia. Si lo que sucede es que la letra entrada es incorrecta, también se puede recurrir a Ctrl-Espacio para que se cambie por la correcta.
•
En la aplicación DateFieldTest, después de especificar un nuevo patrón, podemos activarlo pulsando el botón OK o bien pulsando intro en el campo de patrón. También es importante tener en cuenta que en el campo Añadir, podemos especificar cantidades negativas para que reste.
•
En los ejemplos IntegerFieldTest y DNIFieldTest, la combinación de teclas , cuando el foco está en el campo, abren un diálogo que muestra el valor del campo.
19 de 24
Arquitectura de clases La siguiente figura nos muestra el diagrama de clases de los componentes de ejemplo derivados de JformattedTextField. Téngase en cuenta que el diagrama no incluye todos los métodos.
Figura 2: Diagrama de clases de los componentes de ejemplo
20 de 24
21 de 24
EPÍLOGO Es notable el aparente cambio de estrategia de Sun proporcionando en la versión 1.4 de Java dos nuevos componentes Swing que intentan cubrir vacíos importantes en lo que al desarrollo de interficies gráficas se refiere. Lamentablemente, la versión 5.0 no incluye ningún componente nuevo ni mejora los anteriores. JFormattedTextField nos permite desarrollar aplicaciones más profesionales y mejora la imagen de Swing. Tenemos máscaras, campos de fecha con un comportamiento razonable, podemos usar diversos formateadores para personalizar nuestros campos, etc. Sin embargo, durante el tiempo que me ha llevado construir este artículo, he encontrado algunos obstáculos que me han dificultado la labor de escribir tanto el texto del artículo como los componentes de ejemplo. He encontrado, y es una opinión personal, problemas de ortogonalidad, algunos de los cuales ya he expuesto, como la diferencia de comportamiento de commitEdit() y setValue() que, combinados con la admisión o no de literales en el valor del MaskFormatter, me han dado algún que otro quebradero de cabeza. En el mismo orden de cosas estaría el establecimiento de rangos de InternationalFormatter. Siguiendo con la ortogonalidad, me pregunto ¿dónde está el model de JformattedTextField? Ciertamente, sigue siendo el mismo que el de su padre, JTextField, un PlainDocument. Sin embargo, las relaciones entre vista/controlador (delegate) y modelo, ni son tan claras como en JTextField ni se cuentan en parte alguna. Me he encontrado también con problemas de visibilidad (scope) cuando he intentado extender, por ejemplo, DefaultFormatter. Hay métodos importantes de DefaultFormatter y de JTextComponent que sólo están visibles a nivel de package y que dificultan extender tanto DefaultFormatter como AbstractFormatter. Es realmente complejo extender los formateadores que nos vienen dados. En el proceso de creación de este artículo, me propuse reproducir un componente que había desarrollado hace tiempo como una extensión de JTextField y que ofrecía una funcionalidad sencilla pero práctica: determinar el número máximo de caracteres que podía aceptar un campo de entrada. Ante la dificultad de extender DefaultFormatter, decidí ir directamente a PlainDocument y atacar el modelo como hice anteriormente. Bien, no acabó de funcionar. El método insertString() de AbstractDocument no se invoca al insertar una tira (por teclado o desde el clipboard) como en JTextField, sino cuando el campo cambia de foco. La falta de tiempo y las dificultades han hecho que abandonara esta línea. Finalmente, y para la versión 1.1 de este artículo, he desarrollado StringField, pero recurriendo al control de la propiedad value, ya que las otras vías, a mi entender más coherentes, han resultado imposibles de seguir (posiblemente por mis limitaciones personales). A pesar de los pesares, creo que JFormattedTextField es un componente importante que debe formar parte, de manera habitual, en nuestras aplicaciones. Deseo que el lector pueda, con la ayuda de este artículo, sortear mejor que yo las dificultades de creación de componentes derivados de JFormattedTextField y que este artículo contribuya a hacer un mejor y mayor uso de Swing en sus aplicaciones de escritorio.
22 de 24
¿QUÉ HE USADO? He usado Eclipse 3.0.1 (http://www.eclipse.org) para el desarrollo, la generación de Javadocs y las pruebas unitarias con JUnit 3.8.1 (http://www.junit.org). Para el desarrollo de las interficies de usuario en Swing, he usado el Visual Editor de Eclipse (http://www.eclipse.org/vep/) en su versión 1.0.2.1RC2. Para la generación del diagrama de clases de los componentes, he usado la última versión del plugin de Eclipse Omondo EclipseUML (http://www.omondo.com/index.html). Este artículo ha sido escrito con OpenOffice 2.0 beta (http://www.openoffice.org/) y éste también se ha usado para la generación del PDF.
¿QUÉ HE LEÍDO? Realmente, hay poca literatura que haga referencia a JformattedTextField. Yo sólo he encontrado un par de tutoriales que cubren los aspectos más básicos. El primero, siempre es una referencia, es el capítulo How to Use Formatted Text Fields (http://java.sun.com/docs/books/tutorial/uiswing/components/formattedtextfield.html) del tutorial oficial de Java. Es correcto, pero creo que insuficiente si quieres trabajar a fondo con las posibilidades de JformattedTextField. Expone con claridad algunos conceptos básicos. El segundo, es el artículo de John Zukowski Swing's new JFormattedTextField component (http://www-106.ibm.com/developerworks/java/library/j-mer0625/) de junio de 2002 dentro de la interesante serie de artículos sobre novedades de la versión 1.4 de Java Magic with Merlin que el autor ha publicado en developerWorks. Bien escrito pero muy básico. Como he comentado más arriba, para entender el funcionamiento de JFormattedTextField, he tenido que leer mucha API y bastante código fuente.
INSTALACIÓN, CONTENIDO Y EJECUCIÓN Este tutorial y los archivos relacionados están empaquetados en un archivo ZIP. Al descomprimirlo (respetando la estructura de directorios) se creará el subdirectorio JFTF que contiene: •
El archivo LEEME.TXT que explica, como aquí, qué contiene el directorio y cómo se usa.
•
Este artículo en formato PDF.
•
El archivo jftf.jar que contiene los componentes de ejemplo, las aplicaciones de demostración y los tests unitarios.
•
El subdirectorio src que contiene el código fuente.
•
El subdirectorio bin con las classes compiladas.
•
El subdirectorio doc que contiene los javadoc de todas las clases.
Ejecución de las aplicaciones de ejemplo Las aplicaciones de ejemplo se pueden ejecutarse por separado o bien, yo lo recomiendo, ejecutando la clase com.froses.jftf.widgets.demo.Pruebas: 23 de 24
java -classpath jftf.jar com.froses.jftf.widgets.demo.Pruebas
o bien java -jar jftf.jar
desde el directorio [...]\JFTF creado al descomprimir el archivo ZIP del artículo. Si el lector está usando un entorno Windows, puede, simplemente, hacer doble clic sobre el archivo jftf.jar.
Estructura del código fuente El código está organizado en cinco packages: 1. com.froses.jftf.tools: Contiene los componentes auxiliares CIF_NIF y Pair. 2. com.froses.jftf.widgets: Contiene los componentes de ejemplo derivados de JFormattedTextField. 3. com.froses.jftf.widgets.demo: Contiene las aplicaciones de demostración. 4. com.froses.jftf.widgets.tools: Contiene los componentes auxiliares gráficos: •
BoundJSpinner
•
ButtonGroupJPanel
•
OverwriteCaret
24 de 24