Tabla de Contenidos

Usabilidad y accesibilidad

Los conceptos de usabilidad o experiencia de usuario y accesibilidad tienen una importante relacion con las interfaces de usuario. El estandar internacional ISO/IEC 25000 sobre los requisitos y la evaluación de calidad del software indica que la usabilidad se refiere a la capacidad de un software de ser comprendido, aprendido, usado y ser atractivo para el usuario, en condiciones específicas de uso.

La accesibilidad se refiere a que las aplicaciones software, las herramientas y las tecnologías estén diseñadas y desarrollados para que las personas con discapacidad puedan usarlas. Algunas características de los usuarios ante las que deben estar adaptadas las interfaces gráficas pueden ser:

Nuestras interfaces gráficas deben ser diseñadas atendiendo a cuqluier tipo de usuario.

Principios de usabilidad y accesibilidad

Aunque los dos conceptos no son idénticos, tienen ciertos aspectos comunes. A continuación se enumeran algunos:

En la guía de accesibilidad para desarrolladores de GNOME se indican muchas orientaciones sobre estos aspectos.

Aspecto Look&Feel

El look and feel de una interfaz gráfica en Java se encarga del aspecto de los componentes gráficos. Como Java es un lenguaje multiplataforma por defecto utiliza una estética independiente del sistema en el que se ejecute la aplicación.

Para modificar esta estética utilizamos la clase UiManager junto con su método setLookAndFeel(), que recibe un String en el que se indica el lookandfeel que se quiere emplear.

En caso de modificar el look and feel de una aplicación, se debe establecer al inicio del método main() que inicie la aplicación.

Diferentes aspectos look & feel

Look&Feel dependiente o independiente del sistema

La estética look and feel por defecto se conoce como cross-platform look and feel también llamada metal look and feel. Pero en algunos casos quizas nos interese utilizar el look and feel del sistema en el que se ejecuta la aplicación.

//Usar cross-platform L&F, ó metal L&F
UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
 
//Usar el look and feel del sistema anfitrión
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName();

*El método setLookAndFeel() lanza excepciones que debemos controlar con un try-catch.

Otros Look&Feel

También podemos modificar el look and feel mediante el pasando como parámetro el String que lo identifica. Los distintos aspectos que podemos usar son:

try {
   UIManager.setLookAndFeel("com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel");
   //UIManager.setLookAndFeel("javax.swing.plaf.metal.MetalLookAndFeel");
   //UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsLookAndFeel");
   //UIManager.setLookAndFeel("com.sun.java.swing.plaf.motif.MotifLookAndFeel");
   //UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsClassicLookAndFeel");
 
} catch (ClassNotFoundException e) {
   e.printStackTrace();
} catch (InstantiationException e) {
   e.printStackTrace();
} catch (IllegalAccessException e) {
   e.printStackTrace();
} catch (UnsupportedLookAndFeelException e) {
   e.printStackTrace();
}

Iconos intuitivos

Los botones de la mayoría de aplicaciones, no son botones que contengan texto, sino que contienen iconos. Del mismo modo los elementos de los menús, aunque si que contienen texto, también indican su función mediante un icono.

Debemos seleccionar las imágenes que contienen dichos botones de forma que representen las funciones que realizan.

Etiquetas ToolTips de ayuda

Un tooltip es una ayuda visual que nos permite insertar un texto a modo de explicación o ayuda sobre un componente o control de la aplicación. Este texto aparecerá automáticamente, al pasar el ratón por encima del componente.

JButton btnMensajeError = new JButton("Abrir fichero");
 
//Incluimos una ayuda tooltip sobre el boton
btnMensajeerror.setToolTipText("Opción de abrir fichero");

También podemos indicarlo desde el diseñador de WindowBuilder:

Control de errores de entrada de usuario

Podemos advertir al usuario cuando se produzcan errores mediante dialogos JOptionPane. Es muy útil poder notificar errores de entrada cuando se interactua con la aplicación:

Para que nuestra interfaz sea robusta, no podemos permitir que errores no controlados interrumptan la ejecución estable de nuestra aplicación.

Acceso por teclado

Como se ha comentado al inicio del bloque, una interfaz gráfica debe facilitar el manejo por teclado, por accesibilidad y ademas también por velocidad.

Accesos Mnemonicos

Puedo realizar acciones utilizando teclas mnemónicas. Estas teclas se usan manteniendo la tecla ALT + tecla mnemónica. Es la forma mas sencilla de acceso por teclado y se puede asociar a elementos de menú y a botones.

Para poder usar el acceso mnemónico es necesario ver el boton, o la opción del menú. Si está en otra sección de la aplicación y no está a la vista no puedo usar su acceso mnemónico.

Se pueden añadir mediante código o desde las propiedades de WindowBuilder:

JButton btnNuevoUsuario = new JButton("Nuevo Usuario");
 
btnNuevoUsuario.setMnemonic(KeyEvent.VK_N);

Ahora puedo realizar la accion de pulsar el botón, mediante las teclas ALT + N. Java subraya la letra del acceso mnemónico.

Aceleradores de Menu

En el caso de las opciones anidades dentro de un JMenu, tenemos otra opción mas práctica que las teclas mnemónicas: los aceleradores de menú. Aunque en la documentación se les da ese nombre, son básicamente atajos del teclado, pero solo se pueden aplicar a elementos del menú (JMenuItem, JMenuCheckBoxItem, etc).

La diferencia con las teclas mnemónicas es que permite ejecutar la acción dsede cualquier lugar de nuestra aplicación, independientemente de si el menú está desplegado o no.. Una vez configurada se muestra una indicación al lado del JMenuItem.

Se pueden añadir tanto desde WindowBuilder como mediante código:

//Pulsando CTRL + N
mntmNuevoUsuario.setAccelerator(
   KeyStroke.getKeyStroke(KeyEvent.VK_N, InputEvent.CTRL_DOWN_MASK));

Atajos del teclado para acciones

Si queremos tener la misma funcionalidad que los aceleradores, pero para los botones, la opción que tenemos es definir acciones mediante las clases Action y AbstractAction. Es habitual cuando desde nuestra aplicació podemos realizar las mismas acciones desde los elementos de un Menú y desde los botones de la barra de herramientas.

JButton boton= new JButton();
 
Action accionBoton = new AbstractAction("Refrescar") {
 
    @Override
    public void actionPerformed(ActionEvent evt) {
        System.out.println("Refrescando...");
    }
};
 
String key = "Refrescar";
 
button.setAction(buttonAction);
 
buttonAction.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_R);
 
button.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
        KeyStroke.getKeyStroke(KeyEvent.VK_F5, 0), key);
 
button.getActionMap().put(key, buttonAction);

Botones por defecto

Los botones por defecto son aquellos que están seleccionados cuando usamos una aplicación, y responden a una tecla (habitualmente Enter).

Solo puede haber un boton por defecto en cada contenedor de alto nivel.

JButton btnAdd = new JButton("Añadir");
 
//Indico el boton por defecto
getRootPane().setDefaultButton(btnAdd);

Seleccionar Texto

Es habitual que al pulsar sobre un campo de texto se seleccione el texto que contiene de modo que es más fácil modificarlo: el texto seleccionado se borra al escribir cualquier caracter. También ocurre cuando pulsamos la tecla “Tabulación” para pasar de un campo de texto a otro.

Para implementar esta funcionalidad sobre un campo de texto, debemos implementar un manejador de eventos de tipo Foco: FocusListener. El foco se denomina a la propiedad que hace que algo este seleccionado. Cuando seleccionamos un campo de texto con el ratón, este campo gana el foco, y cuando pulsamos sobre otro campos de texto, el campo anterior pierde el foco.

La interface FocusListener tiene dos métodos: focusGained() y focusLost(), que se ejecutan cuando un elemento gana o pierde el foco.

public class Controlador implements FocusListener{
 
 . . .
   miCampodeTexto.addFocusListener(this);
 . . .
 
 //Cuando mi componente gana el foco, realizo la operacion
 @Override
 public void focusGained(FocusEvent evt) {
    //Si el componente que gana el foco es mi campo de texto
    if(evt.getSource() == miCampoDeTexto){
       miCampoDeTexto.selectAll();
    }
 }
 
 @Override
 public void focusLost(FocusEvent evt) { }
}

Deshacer acciones

La opción de deshacer también es algo habitual en las aplicaciones que usamos. Podemos deshacer el último cambio realizado, o una lista de cambios.

Para ello debemos de crear una estructura de datos que almacene el cambio realizado. Puede ser la creación o la eliminación de un objeto, por ejemplo. En el momento de realizar la acción debemos guardar en esa estructura el tipo de cambio realizado y el objeto que hemos modificado, creado o borrado.

De este modo si queremos deshacer la acción, podemos recuperar el estado anterior de nuestra aplicación.

//Ejemplo de clase para almacenar modificaciones
public class Modificación{
 
private String accion;
private Object objetoModificado;
 
public Modificacion(String accion, Objecto objeto){
   this.accion = accion;
   this.objetoModificado = objeto;
}

Las modificaciones las podemos guardar en una lista o guardar solo la última modificación. De este modo podemos recuperar el estado anterior y saber qué se hizo.

Control de acceso de usuarios

Si queremos que nuestra aplicación controle el acceso de los usuarios, debemos crear una ventana de login. Suelen consistir en un pequeño cuadro de diálogo en el que se permite introducir un nombre usuario y una contraseña.

La lógica del control de usuarios es bastante sencilla, basta con controlar si el usuario coincide con la contraseña especificada. Podemos controlar un solo usuario, o varios usuarios dentro de una lista, por ejemplo.

Para mejorar la funcionalidad sería recomendable que nuestra aplicación permitiera crear usuarios y su contraseña. Incluso crear usuarios con diferente rol (administrador y usuario estándar), dejando ciertas operaciones solamente disponibles para ciertos roles.

Algunas indicaciones para la creación de ventana de login:

  1. Usar JDialog en lugar de JFrame para esta ventana
  2. Hacer un JDialog modal, ya que así impide el acceso al resto de la aplicación
  3. Usar un JTextField para el nombre y un JPasswordField para la contraseña
  4. Si el login es correcto se cierra (dispose())
  5. Si el login es incorrecto, se indica el error y la ventana se mantiene
  6. Si se presiona Cancelar, se termina la aplicacion (System.exit())
  7. Es conveniente quitar la barra de título y sus botones (undecorated)

Un tipo de implementación sencilla podría ser

public class VentanaLogin extends JDialog{
 
   //Declaro dos constantes con las credenciales
   private static final String USUARIO = "admin";
   private static final String PASS = "admin";
 
   public VentanaLogin(){
      //cuando es modal, la ejecución se detiene
      //cuando se cierra el dialogo, continua la ejecución del programa
      setModal(true);
 
      //inicialización de componentes gráficos
      . . .
 
      //elimino la barra de título
      setUndecorated(true);
 
      //Listener para botón de Login
      loginButton.addActionListener(new ActionListener() {
	public void actionPerformed(ActionEvent e) {
           //Si la contraseña coincide, cierro la ventana
	   if(comprobarUsuario(txtUser.getText(), passwordField.getPassword())) {
		dispose();
	   } else {
                //Si no coincide lo indico, pero dejo la ventana
		JOptionPane.showMessageDialog(ventana, "Login incorrecto", "Error", JOptionPane.ERROR_MESSAGE);
	   }
	}
      });
 
      //Listener boton cancelar
      cancelButton.addActionListener(new ActionListener() {
	public void actionPerformed(ActionEvent arg0) {
        //Salgo de la aplicacion
		System.exit(0);
	}
      });
   }
 
   . . .
 
   //Devuelvo true si la comprobación es correcta
   private boolean comprobarUsuario(String usuario, char[] pass){
      if(USUARIO.equals(usuario) && PASS.equals(pass.toString){
         return true;
      }
      return false;
   }
 
}

Internacionalización (I18n)

Internacionalizar (ó I18n) una aplicación consiste en hacerla accesible para distintas regiones. Se pueden internacionalizar las aplicaciones Swing aportando las traducciones de cadenas de caracteres, imágenes, etc., sin necesidad de tener que reconstruir el proyecto; sin tener que compilarlo según el país al que vaya dirigido.

Se puede utilizar esta características empleando ficheros de recursos (ResourceBundle files). En ellos, deberíamos indicar todos los textos visibles, como el texto de etiquetas, campos de texto, botones, etc. Posteriormente modificando el parametro Locale de nuestra aplicación, se utilizará un fichero de idioma concreto.

Clase Locale

La clase Locale se usa para representar la región geográfica y el lenguaje de la plataforma que ejecuta la aplicación. El objeto Locale suele ser creado a partir de los parámetros de nuestro sistema operativo. Si nuestro sistema indica que esta en España y que nuestro lenguaje es el español, las aplicaciones en Java utilizarán esos parámetros regionales.

Aun así, se puede modificar manualmente los valores del objeto Locale, siendo la forma más fácil de poder gestionar el idioma en el que se muestra nuestra aplicación.

public static void main(String[] args) {
 
   System.out.println(Locale.getDefault()); //Muestra es_ES
 
   Locale.setDefault(Locale.UK);
 
   System.out.println(Locale.getDefault()); //Muestra en_GB
 
 . . .
}
//Obtener Locale actual (obtenido de los parámetros del Sistema)
Locale locale = Locale.getDefault();
 
//Mediante constructor (español-España)
locale = new Locale("es", "ES");
 
//Mediante constantes (inglés-USA)
locale = Locale.US;
 
//Mediante Locale.Builder (francés-Canada)
locale = new Locale.Builder().setLanguage("fr").setRegion("CA").build();
 
//Modificar la localizacion actual
Locale.setDefault(locale);
 
//Para acceder a sus propiedades
String idioma = locale.getLanguage(); //fr
String pais = locale.getCountry();    //CA

Códigos de región e idiomas

Las regiones e idiomas se representan mediante un código de dos caracteres. Las regiones en MAYÚSCULAS y los idiomas en minúsculas. A continuación se enumeran algunos códigos:

Código de región Región
ES España
US Estados Unidos
UK Reino Unido
AU Australia
BR Brasil
CA Canada
CN China
DE Alemania
FR Francia
IN India
RU Rusia
Código de idioma Idioma
es Español
de Aleman
en Ingles
fr Frances
ru Ruso
ja Japones
jv Javanes
ko Koreano
zh Chino

Ficheros .properties

Los ficheros de propiedades son ficheros de texto plano. Su contenido está organizado mediante parejas clave-valor. En este tipo de ficheros es donde se guardarán todas las cadenas de texto que haya en la GUI de mi aplicacion: las etiquetas, el texto de los botones, el texto de los menus. Todo lo que sea susceptible de ser traducido en distintos idiomas Por cada linea hay una clave y un valor:

EtiquetasBundle.properties
# Fichero de idioma por defecto (español)
lblNombre = Nombre:
btnNuevoUsuario = Nuevo
menuItemGuardar = Guardar
 . . .
EtiquetasBundle_en.properties
# Fichero de idioma inglés
lblNombre = Name:
btnNuevoUsuario = New
menuItemGuardar = Save
 . . .

En cada fichero deben aparecer las mismas claves, pero traducidas el fichero de idioma concreto.

Estos ficheros de idiomas se deben llamar igual, y debe haber uno por defecto:

Si el idioma indicado en el objeto Locale no conincide con ningun fichero de idioma, se usará el base por defecto.

Clase ResourceBundle

Es la clase sobre la que basamos la internacionalización. Nos permite cargar propiedades de diferentes ficheros dependiendo de las propiedades del objeto Locale indicado.

Mediante el método estático getBundle() indicamos las propiedades de localización. Podemos indicarle un objeto Locale o que se utilice el actual:

//Nos carga el fichero EtiquetasBundle.properties (español)
Locale locale = new Locale("es", "ES")
ResourceBundle bundle = ResourceBundle.getBundle("EtiquetasBundle", locale);
 
String texto = bundle.getString("btnNuevoUsuario");   //Obtengo "Nuevo"
 
//Nos carga el fichero EtiquetasBundle_en.properties (inglés)
bundle = ResourceBundle.getBundle("EtiquetasBundle", Locale.US);
 
String texto = bundle.getString("btnNuevoUsuario");   //Obtengo "New"
 
 
//Si no pasamos un parametro Locale, usa el actual (Locale.getDefault())
Locale.setDefault(Locale.UK);
ResourceBundle bundle = ResourceBundle.getBundle("EtiquetasBundle");

Sustituir texto estático por texto localizado

Cuando utilizamos las clases Locale y ResourceBundle para internacionalizar, debemos tener en cuenta algo muy importante: No puede haber textos estáticos en las etiquetas de nuestros componentes gráficos.

Debemos revisar nuestro código fuente y sustituir toda aparición de un texto, susceptible de ser traducido, por una llamada al método getString() de la clase ResourceBundle:

//Debo sustitir estas cadenas
JButton btnGuardar = new JButton("Guardar");
JMenu menu = new JMenu("Archivo");
JLabel lbl1 = new JLabel("Nombre:");
 
//Por el método getString y la clave
 
ResourceBundle bundle = ResourceBundle.getBundle("EtiquetasBundle", locale);
 
JButton btnGuardar = new JButton(bundle.getString("textoGuardar"));
JMenu menu = new JMenu(bundle.getString("menuArchivo"));
JLabel lbl1 = new JLabel(bundle.getString("lblNombre"));

De este modo, el programa mostrará los textos utilizando los valores de cada fichero de ResourceBundle, en el idioma indicado por el objeto Locale actual.

En el siguiente video se plantea una forma de modificar el idioma desde la interfaz gráfica.

Opciones de Internacionalización en IntelliJ

El IDE IntelliJ, permite algunas facilidades a la hora de internacionalizar una aplicación:

Para poder añadir claves a nuestro ResourceBundle con los literales de String que tengamos en nuestro código fuente, es aconsejable habilitar una opcion de la configuracion: File → Settings → Editor → Inspections. Dentro del árbol buscar la sección Java → Internationalization Issues → Marcar el checkbox Hard coded strings

A partir de ahí puedo añadir literales de strings desde mi código fuente como claves a los ficheros .properties de los ResourceBundles.

En el siguiente video se explican las diferentes funcionalidades de IntelliJ respecto a la internacionalización:

Pantalla de carga (SplashScreen)

Tener una pantalla de carga es muy útil cuando nuestra aplicación no tiene solamente que levantar la GUI, sino que también realizar operaciones iniciales que conllevan algo de tiempo, establecer conexiones con una bbdd y obtener muchos datos, hacer un mapeo objeto-realacional, o cualquier operación que requiera algunos segundos de tiempo.

Podemos ocultar esas operaciones mediante una pantalla de carga que tarde un tiempo en desaparecer, momento en el cual mostraremos nuestra GUI principal después de haber realizado las operaciones más lentas.

El siguiente código se puede descargar y muestra un ejemplo de una ventana de carga con una imagen y una barra de carga:

SplashScreen.java
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.GridLayout;
import javax.swing.ImageIcon;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.SwingConstants;
 
public class SplashScreen2 extends JDialog{
 
   private JProgressBar barraProgreso;
 
   public SplashScreen2() {
      setBounds(100, 100, 637, 566);
      JPanel contentPane = new JPanel();
      contentPane.setLayout(new BorderLayout());
      setContentPane(contentPane);
 
      //Creo una etiqueta con la imagen en el centro
      JLabel lblImagen = new JLabel();
      //Indico la imagen que quiero mostrar en la label
      lblImagen.setIcon(new ImageIcon(SplashScreen.class.getResource("/gui/splash.jpg")));
      contentPane.add(lblImagen, BorderLayout.CENTER);
 
      //Creo un panel al sur con una barra de carga y una label para el autor
      JPanel panelInferior = new JPanel();
      panelInferior.setLayout(new GridLayout(2, 1, 0, 0));
      barraProgreso = new JProgressBar();
      //Muestra el % de carga
      barraProgreso.setStringPainted(true);
      panelInferior.add(barraProgreso);
 
      JLabel lblFersoft = new JLabel("FerSoft 2020");
      lblFersoft.setForeground(Color.BLUE);
      lblFersoft.setHorizontalAlignment(SwingConstants.CENTER);
      panelInferior.add(lblFersoft);
 
      //Anado el panel inferior al principal
      contentPane.add(panelInferior, BorderLayout.SOUTH);
 
      setResizable(false); //Impedir redimensionar la ventana
      setUndecorated(true); //Eliminar la barra de título y sus botones
      setLocationRelativeTo(null); //Mostrar en el centro
      setVisible(true);
 
      try {
         iniciarBarraCarga();
      } catch (InterruptedException e) {
         throw new RuntimeException(e);
      }
      //Al terminar la carga cierro la ventana
      dispose();
   }
 
   private void iniciarBarraCarga() throws InterruptedException {
      for(int i = 0; i <= 100; i++){
         Thread.sleep(20);
         actualizarBarraProgreso(i);
      }
   }
 
   private void actualizarBarraProgreso(int valor) {
      SwingUtilities.invokeLater(new Runnable() {
         @Override
         public void run() {
            progressBar1.setValue(valor);
         }
      });
   }
}

Para arrancar este hilo desde el método main:

public static void main(String[] args){
   Modelo modelo = new Modelo();
 
   //La vista se inicia pero no se muestra (visible = false)
   Vista vista = new Vista();
 
   //El controlador arranca 
   Controlador controlador = new Controlador(vista, modelo);
 
   SplashScreen splash = new SplashScreen();
 
   //Cuando ha terminado la ejecucion del SplashScreen
   // se muestra la ventana principal
   vista.setVisible(true);
}

Esta SplashScreen es simplemente decorativa, pero podría realizar un trabajo en algún otro hilo de la propia clase y que solo se cierre cuando se haya terminado, dando paso a la ventana principal.

Ayuda y manuales de usuario

Cuando trabajamos con alguna aplicación de software hay una parte común en todas ellas y es la sección de ayuda. Esta sección nos ofrece la documentación de usuario del software, y la forma de mostrarla y acceder a ella puede ser más o menos interactiva. En nuestro caso debemos crear una sección que permita al menos visualizar los manuales de usuario de nuestro software.

Las tres formas más sencillas son:

  1. Permitir que nuestra aplicación abra un sitio web desde el navegador del sistema
  2. Mostrar el manual en algún formato como .pdf desde nuestra aplicación
  3. Mostrar una página web en una ventana de nuestra aplicación

En el bloque 5 sobre distribución de aplicaciones, se trabaja la creación de manuales y veremos otras formas de mostrar la ayuda.

Por ahora, podemos hacer que nuestra aplicación use el navegador por defecto del sistema y redirija a una web, creando una JLabel con aspecto de link http:

// Creo una etiqueta de color azul
JLabel lblFersoft = new JLabel("FerSoft 2020");
lblFersoft.setForeground(Color.BLUE.darker());
 
// Modifico el puntero del ratón al pasar sobre ella (Hand_cursor)
lblFersoft.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
 
...
 
//Añadir un MouseListener a la JLabel para hacer click sobre ella
//Debemos controlar las excepciones de la siguiente instrucción
Desktop.getDesktop().browse(new URI("http://www.bitbucket.org/fvaldeon"));

© 2024 Fernando Valdeón