JDC Tech Tips (20 de Noviembre del 2001)

Bienvenido a los Consejos Técnicos de la Conexión del Desarrollador Java (JDC), del 20 de Noviembre de 2001. Esta edición cubre:

  • Validar Entradas Numéricas en un JTextField.
  • Trabajar con Fuentes.

Estos consejos fueron desarrollados usando Java(tm) 2 SDK, Standard Edition v 1.3

Se puede ver esta edición (en su original en inglés) de los Consejos Técnicos en formato html en http://java.sun.com/jdc/JDCTechTips/2001/tt1120.html

Validar Entradas Numéricas en un JTextField

El Java 2 SDK, Standard Edition, v 1.4 que está disponible actualmente en versión beta, añade un componente JFormattedTextField para entrada de texto formateado. Entre otras cosas, éste nos ofrece la habilidad de validar la entrada del campo. Pero ¿Pero que pasa si necesitamos validar ahora la entrada de un campo y no podemos esperar una nueva versión? Hay al menos tres formas diferentes en las que podemos validar un campo de texto hoy en día: nivel de pulsación, nivel de foco, y nivel de modelo de datos. Este truco nos muestra cómo usar estas técnicas para crear un campo de entrada que sólo acepte datos numéricos.

Como en el caso del componente TextField del AWT, el componente JTextField de Swing, soporta registrar un KeyListener con el componente. Cuando se registra un oyente, podemos vigilar las pulsaciones de teclas. Si la tecla pulsada no es una tecla numérica, podemos rechazarla, es decir, con una excepción: tenemos que permitir backspace y delete para corregir errores. El rechazo se maneja llamando al método consume() del KeyEvent, que le dice al componente que la tecla fue pulsada en uno de sus oyentes de entrada y que no debería ser mostrada.

Aquí podemos ver una verificación de entrada usando un oyente:

  keyText.addKeyListener(new KeyAdapter() {
    public void keyTyped(KeyEvent e) {
      char c = e.getKeyChar();      
      if (!((Character.isDigit(c) ||
         (c == KeyEvent.VK_BACK_SPACE) ||
         (c == KeyEvent.VK_DELETE))) {
        getToolkit().beep();
        e.consume();
      }
    }
  });

Hay una consideración especial en el uso del keylistener si estámos trabajando en entornos donde necesitamos instalar un método oyente de entrada. En ese caso, el método oyente de input desactivará la habilidad de capturar pulsaciones con un keylistener. Esto sucede normalmente cuando no hay suficientes teclas en el teclado para mapear los caracteres de entrada. Un ejemplo de este es aceptar caracteres Chinos o Japoneses como entrada.

Usar un FocusListener en lugar de un KeyListener proporciona un comportamiento ligeramente diferente. Donde el KeyListener verifica cada pulsación, el FocusListener valida la entrada cuando el campo pierde el foco. Como verifica el campo completo, esta técnica simple implica el análisis de la entrada con el método parseInt() de Integer. De hecho, el valor de la entrada no importa. Lo que importa es que podemos analizarla.

Aquí podemos ver como sería una versión de verificación de entrada usando FocusListner:

  focusText.addFocusListener(new FocusAdapter() {
    public void focusLost(FocusEvent e) {
      JTextField textField = 
        (JTextField)e.getSource();
      String content = textField.getText();
      if (content.length() != 0) {
        try {
          Integer.parseInt(content);
        } catch (NumberFormatException nfe) {
          getToolkit().beep();
          textField.requestFocus();
        }
      }
    }
  });

Desafortunadamente, hay un problema al usar oyentes a nivel de foco cuando usamos pantallas de entrada que tienen un menú o diálogos desplegables. Todos estos eventos lanzarían una llamada al oyente de foco. Con los menús, el oyente es realmente llamado cuando el menú de más alto nivel obtiene el foco de entrada, es decir, cuando nos movemos para encontrar el ítem de menú a seleccionar. El oyente mostrado arriba hace un beep cuando hay una entrada inválida, imagina lo que sucedería si se desplegara un diálogo para mostrar un mensaje de error.

Hay una segunda forma de hacer la verificación a nivel de foco de los componentes Swing. Podemos adjuntar un InputVerifier al componente. La clase abstracta tiene un sólo método, boolean verify(JComponent), que nosotros implementamos para realizar la validación de la entrada. Este método necesita devolver true si la entrada es válida y false si no lo es. Como en el técnica del FocusListener, podemos usar parseInt() para chequear true o false. Para adjuntar el verificador, llamamos al método setInputVerifier(). Cuando un usuario intenta mover el foco de entrada más allá del campo asociado, el verificador realiza la acción de validar la entrada.

Como con el FocusListener, el InputVerifier permite la validación de todo el campo, contra la intención de determinar qué parte de la entrada es válida. Esto es importante, por ejemplo, si queremos que la entrada esté dentro de un cierto rango.

Hay un segundo método en InputVerifier para manejar cómo responder a una entrada inválida: boolean shouldYieldFocus(JComponent). La implementación por defecto de este método devuelve el valor devuelto por verify(). Si queremos hacer un beep ante una entrada inválida, tenemos que chequear el valor antes de volver.

Aquí tenemos un ejemplo de verificación de entrada que hace un beep en las entradas inválidas usando un InputVerifier:

  inputText.setInputVerifier(new InputVerifier() {
    public boolean verify(JComponent comp) {
      boolean returnValue = true;
      JTextField textField = (JTextField)comp;
      String content = textField.getText();
      if (content.length() != 0) {
        try {
          Integer.parseInt(textField.getText());
        } catch (NumberFormatException e) {
          returnValue = false;
        }
      }
      return returnValue;
    }
    public boolean shouldYieldFocus(JComponent input) {
      boolean valid = super.shouldYieldFocus(input);
      if (!valid) {
        getToolkit().beep();
      }
      return valid;
    }
  });

Aunque esta tercera forma podría parecer un poco más clara, en que no requiere que proporcionemos una llamada a requestFocus() para devolver el foco a la entrada, también sufre de los mismos problemas que el FocusListener.

La forma final para validar entradas cubierta en este truco implica entender la arquitectura Modelo-Vista-Controlador (MVC) de Swing. Detrás de cada JTextComponent (como un JTextField), hay un modelo que contiene los datos del componente de texto. El JTextField es sólo una vista dentro de ese modelo. Pero limitando lo que puede entrar en el modelo, podemos limitar lo que se puede mostrar en el JTextField.

Añadiendo la validación de la entrada en el modelo de datos, evitamos los problemas mencionados anteriormente cuando se selecciona un menú o cómo validar la entrada cuando se adjunta un método oyente. A pesar de que este último modelo de validación es el más complejo, funciona bien.

El modelo por defecto para el JTextField es la clase javax.swing.text.PlainDocument. Esta clase proporciona los métodos insertString() y remove() que son llamados cuando un usuario introduce o elimina texto del componente. Normalmente, esto se haría un caracter cada vez. Sin embargo, debemos tener en cuenta cuando ocurre una operación "copy&paste". Lo que cada método hace es asegurarse de que el modelo será válido si los nuevos datos fueron añadidos o eliminados desde el modelo. Esta tarea suena más complicada de lo que en realidad es. Sólo tenemos que determinar manualmente si el nuevo contenido debería ir con (o sin) los nuevos datos. Asumiendo que se pasa la validación, pasamos los datos a la superclase llamando a super.insertString() o a super.remove().

Aquí está la parte principal de lo que sería el método insertString(). Para validar la entrada, determinamos lo que será el nuevo string. Si el modelo estaba originalmente vacío, el nuevo valor es la entrada. De otro modo, insertamos el nuevo valor en el medio de los contenidos existentes. Después de tener el nuevo valor, lo validamos con el método parseInt() de Integer. Si la validación tiene éxito, llamamos a super.insertString(). Observa que el rechazo está indicado simplemente con no llamar a super.insertString(). Si no insertamos el string, no tenemos que hacer nada. Sin embargo, este código hace beep si la entrada falla.

 String newValue;
  int length = getLength();
  if (length == 0) {
    newValue = string;
  } else {
    String currentContent = 
      getText(0, length);
    StringBuffer currentBuffer = 
      new StringBuffer(currentContent);
    currentBuffer.insert(offset, string);
    newValue = currentBuffer.toString();
  }
  try {
    Integer.parseInt(newValue);
    super.insertString(offset, string, 
      attributes);
  } catch (NumberFormatException exception) {
    Toolkit.getDefaultToolkit().beep();
  }

Para el caso de un modelo que sólo acepta entradas enteras, no es necesario sobreescribir el comportamiento por defecto del método remove(). Es imposible eliminar datos de string de texto que no sea un entero y devolver un no-entero.

Después de definir el modelo completo, usamos el método setDocument() para asociar el modelo con el campo de texto:

  modelText.setDocument(new IntegerDocument());

Aquí hay un ejemplo completo que demuestra las cuatro opciones. En él también encontramos la definición de la clase IntegerDocument:

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

   public class TextInput extends JFrame {
     JPanel contentPane;
     JPanel jPanel1 = new JPanel();
     FlowLayout flowLayout1 = new FlowLayout();
     GridLayout gridLayout1 = new GridLayout();
     JLabel keyLabel = new JLabel();
     JTextField keyText = new JTextField();
     JLabel focusLabel = new JLabel();
     JTextField focusText = new JTextField();
     JLabel inputLabel = new JLabel();
     JTextField inputText = new JTextField();
     JLabel modelLabel = new JLabel();
     JTextField modelText = new JTextField();
     IntegerDocument integerDocument1 =
       new IntegerDocument();

     public TextInput() {
       this.setDefaultCloseOperation(
         JFrame.EXIT_ON_CLOSE);
       contentPane = (JPanel)getContentPane();
       contentPane.setLayout(flowLayout1);
       this.setSize(new Dimension(400, 300));
       this.setTitle("Input Validation");
       jPanel1.setLayout(gridLayout1);
       gridLayout1.setRows(4);
       gridLayout1.setColumns(2);
       gridLayout1.setHgap(20);
       keyLabel.setText("Key Listener");
       modelLabel.setText("Model");
       focusLabel.setText("Focus Listener");
       inputLabel.setText("Input Verifier");

       keyText.addKeyListener(new KeyAdapter() {
         public void keyTyped(KeyEvent e) {
           char c = e.getKeyChar();
           if (!(Character.isDigit(c) ||
              (c == KeyEvent.VK_BACK_SPACE) ||
              (c == KeyEvent.VK_DELETE))) {
             getToolkit().beep();
             e.consume();
           }
         }
       });

       focusText.addFocusListener(new FocusAdapter() {
         public void focusLost(FocusEvent e) {
           JTextField textField =
             (JTextField)e.getSource();
           String content = textField.getText();
           if (content.length() != 0) {
             try {
               Integer.parseInt(content);
             } catch (NumberFormatException nfe) {
               getToolkit().beep();
               textField.requestFocus();
             }
           }
         }
       });

       inputText.setInputVerifier(new InputVerifier() {
         public boolean verify(JComponent comp) {
           boolean returnValue = true;
           JTextField textField = (JTextField)comp;
           String content = textField.getText();
           if (content.length() != 0) {
             try {
               Integer.parseInt(textField.getText());
             } catch (NumberFormatException e) {
               getToolkit().beep();
               returnValue = false;
             }
           }
           return returnValue;
         }
       });

       modelText.setDocument(integerDocument1);

       contentPane.add(jPanel1);
       jPanel1.add(keyLabel);
       jPanel1.add(keyText);
       jPanel1.add(focusLabel);
       jPanel1.add(focusText);
       jPanel1.add(inputLabel);
       jPanel1.add(inputText);
       jPanel1.add(modelLabel);
       jPanel1.add(modelText);
     }

     public static void main(String args[]) {
       TextInput frame = new TextInput();
       frame.pack();
       frame.show();
     }

     static class IntegerDocument
         extends PlainDocument {

       public void insertString(int offset,
           String string, AttributeSet attributes)
           throws BadLocationException {

         if (string == null) {
           return;
         } else {
           String newValue;
           int length = getLength();
           if (length == 0) {
             newValue = string;
           } else {
             String currentContent =
               getText(0, length);
             StringBuffer currentBuffer =
               new StringBuffer(currentContent);
             currentBuffer.insert(offset, string);
             newValue = currentBuffer.toString();
           }
           try {
             Integer.parseInt(newValue);
             super.insertString(offset, string,
               attributes);
           } catch (NumberFormatException exception) {
             Toolkit.getDefaultToolkit().beep();
           }
         }
       }
     }
   }

Debemos asegurarnos de probar los cuatro campos de texto con "cut&paste" para ver que sucede. Por ejemplo, el campo de texto validado usando la técnica KeyListener nos permite pegar datos no numéricos en el campo. Para corregir este comportamiento, tendríamos que desactivar el pegado. Por comparación, el campo de texto que se valida con IntegerDocument, es decir, el etiquetado "Model," funciona apropiadamente cuando se pegan datos de texto.

Si estámos pasando un programa con un componente TextField de AWT a un JTextField de Swing, debemos observar que un comportamiento de TextField no está soportado en el JTextField es la habilidad de adjuntar un TextListener al control. Sin embargo, podemos fácilmente reemplazar este comportamiento usando la aproximación del modelo de datos de adjutnar un Document personalizado. El uso de Document se mapea directamente para ser notificaco cuando el valor del texto ha cambiado (o, al menos, quiere cambiar).

Para aprender más sobre los componentes de texto Swing, puedes ver la lección Using Swing Components en la sección "Creating a GUI with JFC/Swing" del "The Java Tutorial".

Trabajar con Fuentes

El dibujo de texto en los programas Java no ha cambiado mucho desde el nacimiento de la plataforma Java. Sólo tenemos que seleccionar la fuente del tipo apropiado y el tamaño y luego dibujar el string, como se muestra en el siguiente programa:

  import java.awt.*;
  import javax.swing.*;

  public class Text1 extends JFrame {
    public void paint(Graphics g) {
      g.drawString("Hello, JDC", 50, 100);
    }
    public static void main(String args[]) {
      JFrame frame = new Text1();
      frame.setDefaultCloseOperation(
                                 JFrame.EXIT_ON_CLOSE);
      frame.setSize(300, 150);
      Font f = new Font("Serif", Font.BOLD, 48);
      frame.setFont(f);
      frame.show();
    }
  }

El tipo Serif es uno de los cinco nombres lógicos de fuentes soportados por la plataforma Java, los otros cuatro son Dialog, DialogInput, Monospaced, y SansSerif. La plataforma de ejecución mapea estos nombres lógicos a los nombres de fuentes específicos de la platafora, como Times Roman.

Si queremos usar una fuente específica, como Helvetica, podemos pasar el nombre al constructor de Font. Sin embargo, no está garantizado que la fuente esté instalada en el sistema. En vez de eso, lo que necesitamos hacer es preguntarle al sistema que fuentes tiene realmente instaladas, y encontrar una apropiada para usarla.

La clase GraphicsEnvironment del paquete AWT proporciona dos formas de obtener el conjunto de fuentes instaladas en el entorno gráfico local. Podemos solicitar todos los nombres de familias de fuentes con el método getAvailableFontFamilyNames(), o podemos solicitar objetos Font específicos con el método getAllFonts().

Para demostrarlo, el siguiente prorama solicita los nombres de todas las fuentes instaladas, y luego muestra diez nombres, cada nombre escrito en el estilo de su fuente:

  import java.awt.*;
  import javax.swing.*;
  
  public class Fonts extends JFrame {
  
    Insets insets;
    GraphicsEnvironment ge = 
      GraphicsEnvironment.getLocalGraphicsEnvironment();
    String fontList[] = 
      ge.getAvailableFontFamilyNames();
  
    Fonts() {
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setSize(435, 150);
      show();
    }
  
    public void paint(Graphics g) {
      super.paint(g);
      if (insets == null) {
        insets = getInsets();
      }
      g.translate(insets.left, insets.top);
      Font theFont;
      FontMetrics fm;
      int fontHeight = 0;
      int count=Math.min(10, fontList.length);
      for (int i = 0; i < count; i+=2) {
        theFont = new Font(fontList[i], Font.PLAIN, 11);
        g.setFont(theFont);
        fm = g.getFontMetrics(theFont);
        fontHeight += fm.getHeight();
        g.drawString(fontList[i], 10, fontHeight);
  
        if (i+1 != fontList.length) {
          theFont = new Font(fontList[i+1], Font.PLAIN, 11);
          g.setFont(theFont);
          g.drawString(fontList[i+1], 200, fontHeight);
        }
      }
    }
  
    public static void main(String args[]) {
      new Fonts();
    }
  }

Si estamos desarrollando un programa, la única forma segura de conocer la fuente específica utilizada por el programa que se está ejecutando es cargar la fuente durante la ejecución. De hecho, podemos cargar dinámicamente una fuente TrueType. La habilidad de cargar dinámicamente una fuente TrueType se presentó en el Java 2 SDK, Standard Edition versión 1.3. Para cargar la fuente, obtenemos los datos de la fuente en un InputStream y luego llamamos al método createFont() de Font. Y entonces podemos utilizar esa Font para derivar otros tamaños de fuentes a través del método deriveFont().

Aquí tenemos el programa básico anterior, reescrito para usar un nombre de fichero de fuente TrueType pasado como un argumento de la línea de comandos.

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

  public class Text2 extends JFrame {
    public void paint(Graphics g) {
      g.drawString("Hello, JDC", 50, 100);
    }
    public static void main(String args[]) throws 
                                            Exception {
      if (args.length != 0) {
        JFrame frame = new Text2();
        frame.setDefaultCloseOperation(
                                 JFrame.EXIT_ON_CLOSE);
        InputStream is = new FileInputStream(args[0]);
        Font font = Font.createFont(
                               Font.TRUETYPE_FONT, is);
        frame.setFont(font.deriveFont(24f));
        frame.setSize(400, 150);
        frame.show();
      } else {
        System.err.println(
                          "Pass in the .TTF filename");
      }
    }
  }

Después de compilar el programa, poemos ejecutarlo con un comando similar a este:

  java Text2 font.ttf

Reemplazamos font.ttf con el nombre de un fichero de fuente TrueType real. Si no tenemos ningún fichero de fuente TrueType disponible, podemos buscar uno en la Web. Por ejemplo, un buen lugar para buscar es FontParty.com. Muchas de las fuentes listadas allí son gratuitas para uso personal. Algunas son incluso gratis para uso comercial. Tendremos que ver el contrato de licencia para las fuentes específicas que nos interesen.

Para información adicional sobre cómo dibujar texto y trabajar con fuentes, puedes ver El tutorial sobre 2D.

Copyright y notas de la traducción

Nota respecto a la traducción

El original en inglés de la presente edición de los JDC Tech Tips fue escrita por Glen McCluskey, la traducción no oficial fue hecha por Juan A. Palos (Ozito), cualquier sugerencia o corrección hágala al correo [email protected] , sugerencia respecto a la edición original a mailto:[email protected]

Nota (Respecto a la edición via email)

Sun respeta su tiempo y su privacidad. La lista de correo de la Conexión del desarrollador Java se usa sólo para propósitos internos de Sun Microsystems(tm). Usted ha recibido este email porque se ha suscrito a la lista. Para desuscribirse vaya a la página de suscripciones, desmarque casilla apropiada y haga clic en el botón Update.

Suscripciones

Para suscribirse a la lista de correo de noticias de la JDC vaya a la página de suscripciones, elija los boletines a los que quiera suscribirse, y haga clic en Update.

Realimentación

¿Comentarios?, envie su sugerencias a los Consejos Técnicos de la JDC a mailto:[email protected]

Archivos

Usted encontrará las ediciones de los Consejos Técnicos de la JDC (en su original en inglés) en http://java.sun.com/jdc/TechTips/index.html

Copyright

Copyright 2001 Sun Microsystems, Inc. All rights reserved. 901 San Antonio Road, Palo Alto, California 94303 USA.

Este documento esta protegido por las leyes de autor. Para mayor información vea http://java.sun.com/jdc/copyright.html

Enlaces a sitios fuera de Sun

Los Consejos Técnicos de la JDC pueden dar enlaces a otros sitios y recursos. Ya que Sun no tiene control sobre esos sitios o recursos usted reconoce y acepta que Sun no es responsable por la disponibilidad de tales sitios o recursos, y no se responsabiliza por cualquier contenido, anuncios , productos u otros materiales disponibles en tales sitios o recursos. Sun no será responsable, directa o indirectamente, por cualquier daño o pérdida causada o supuestamente causada por o en relación con el uso de o seguridad sobre cualquier tal contenido, bienes o servicios disponibles en o através de cualquier sitio o recurso.

El original en Ingles de esta edición de los Consejos técnicos fue escrita por Glen McCluskey.

JDC Tech Tips November 20, 2001

Sun, Sun Microsystems, Java y Java Developer Connection (JDC) son marcas registradas de Sun Microsystems Incs. en los Estados Unidos y cualquier otro país.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP
ARTÍCULO ANTERIOR

SIGUIENTE ARTÍCULO