Programación Avanzada

Interfaces gráficas de usuario

Swing: Componentes sofisticados

Introducción

En este capítulo vamos a ver cómo dibujar directamente sobre el área de los componentes utilizando el API 2D.

También veremos cómo escuchar los eventos de ratón que generan los componentes cuando el cursos interacciona con ellos.

Finalmente, veremos algunos componentes sofisticados que nos permitirán visualizar varios paneles si están contenidos en solapas, ventanas de diálogo para seleccionar ficheros, y cómo visualizar datos en formato de tabla.

Bibliografía

Contenidos

  1. JPanel y el API 2D.
  2. Detección de eventos del ratón.
  3. Paneles con solapas.
  4. Ventanas de diálogo para selección de ficheros.
  5. Tablas.

JPanel y el API 2D

Todos los componentes de Swing son hijos de JComponent, clase que contiene el método public void paint(Graphics g), llamado por Swing cada vez que se dibuja un componente de la interfaz gráfica de usuario.

IMPORTANTE
Nunca se llama directamente al método paint(Graphics g). Si necesitamos redibujar un componente, llamaremos al método repaint() del componente.

JPanel y el API 2D

El método repaint(), llama internamente a paint(Graphics g) proporcionándole en contexto gráfico necesario para dibujar. A su vez, paint(Graphics g) llama, en orden, a los siguientes métodos:

  1. paintComponent(Graphics g): Dibuja el interior del componente.
  2. paintBorder(Graphics g): Dibuja el borde del componente
  3. paintChildren(Graphics g): Llama al método paint(Graphics g) de cada uno de sus hijos.

JPanel y el API 2D

El origen del sistema de coordenadas para el API 2D es:

Fuente de la imagen: http://docs.oracle.com/javase/tutorial/figures/2d/2D-11.gif

JPanel y el API 2D

Si quiero cambiar el aspecto del componente, puedo hacerlo sobrescribiendo los dos primeros métodos:

public class MiBoton extends JButton{
    private MiBoton(String text) {
        super(text);
    }
    @Override
    public void paintComponent(Graphics g) {
        Graphics2D g2d = (Graphics2D)g;
        g2d.setBackground(Color.CYAN);
        g2d.clearRect(0, 0, getWidth(), getHeight());
        g2d.drawString(getText(), 0, getHeight()-2);
    }
    @Override
    protected void paintBorder(Graphics g) {
        Graphics2D g2d = (Graphics2D)g;
        g2d.setColor(Color.RED);
        g2d.drawRect(0, 0, getWidth()-1, getHeight()-1);
    }...

JPanel y el API 2D

El botón que acabamos de crear tendrá el siguiente aspecto:

Sigue siendo un botón, ya que extiende a JButton, le puedo añadir escuchadores, y funcionarán, lo único que he modificado es cómo se dibuja.

JPanel y el API 2D

A partir de ahora, lo que vamos a hacer es utilizar un JPanel como nuestro lienzo de dibujo.

Como sabes, un JPanel no tiene ningún aspecto visual, pero eso no quiere decir que no le podamos dar uno, reescribiendo su método paintComponent(Graphics g).

public class MiLienzo extends JPanel {
    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2d = (Graphics2D)g;
        // Aquí empiezo a dibujar
    }
}

JPanel y el API 2D

El API 2D nos proporciona algunos métodos para dibujar primitivas sencillas:

  • Líneas.
  • Curvas cuadráticas y de Bezier.
  • Rectángulos.
  • Elipses.
  • Arcos.
  • Curvas arbitrarias.
  • Texto.

También podemos dibujar mapas de bits, pero es algo que no cubriremos en este curso.

JPanel y el API 2D

La clase Graphics2D nos ofrece métodos para dibujar cada una de las primitivas anteriores.

Para dibujar una línea drawLine(xIni, yIni, xFin, yFin)

JPanel y el API 2D

Podemos cambiar el color y el estilo de la línea:

Stroke estilo = new BasicStroke(3.0f);
g2d.setStroke(estilo);
g2d.setColor(Color.RED);
g2d.drawLine(0, getWidth(), getHeight(), 0);

JPanel y el API 2D

Podemos dibujar rectángulos, y rectángulos con los bordes redondeados:

g2d.drawRect(10, 10, 100, 200);
g2d.drawRoundRect(150, 50, 100, 200, 40, 40);

JPanel y el API 2D

Óvalos:

g2d.drawOval(10, 10, 100, 200);
g2d.drawOval(150, 50, 100, 100);

JPanel y el API 2D

Y también tenemos las versiones sólidas de los rectángulos:

g2d.fillRect(10, 10, 100, 200);
g2d.fillRoundRect(150, 50, 100, 200, 40, 40);

JPanel y el API 2D

Y las versiones sólidas de los óvalos:

g2d.fillOval(10, 10, 100, 200);
g2d.fillOval(150, 50, 100, 100);

Detección de eventos del ratón

Todos los componentes generan eventos provocados por la interacción del ratón. Eventos tales como que el cursor del ratón ha entrado en el area que define un componente, o que el cursor ha abandonado el area que define un componente.

Este tipo de situaciones las podemos escuchar si implementamos interface MouseListener que es capaz de escuchar MouseEvent.

Detección de eventos del ratón

MouseListener define 5 métodos:

lienzo.addMouseListener(new MouseListener() {
    @Override
    public void mouseReleased(MouseEvent e) {
    }
    @Override
    public void mousePressed(MouseEvent e) {
    }
    @Override
    public void mouseExited(MouseEvent e) {
    }
    @Override
    public void mouseEntered(MouseEvent e) {
    }
    @Override
    public void mouseClicked(MouseEvent e) {
      System.out.println("["+ e.getX() + ", " + e.getY() + "]");
    }
});

Detección de eventos del ratón

En este caso, tenemos una clase adaptadora, como en el caso de WindowListener para no tener que sobreescribir tantos métodos y dejarlos vacíos.

lienzo.addMouseListener(new MouseAdapter() {
    @Override
    public void mouseClicked(MouseEvent e) {
        System.out.println("["+ e.getX() + ", " + e.getY() + "]");
    }
});
[224, 24]
[65, 75]
[202, 145]
[101, 111]
[206, 136]
[137, 166]

Detección de eventos del ratón

Podemos escuchar otros eventos del ratón, por ejemplo, los relacionados con el movimiento del cursor con MouseMotionListener:

  • mouseDragged(MouseEvent)
  • mouseMoved(MouseEvent)

Y los relacionados con el desplazamiento de la rueda del ratón MouseWheelListener:

  • mouseWheelMoved(MouseWheelEvent)

Afortunadamente, MouseAdapter da una implementación por defecto vacía de todos ellos.

Paneles con solapas

Los paneles con solapas son un modo compacto de tener más de una zona de visualización en la misma ventana:

Paneles con solapas

La idea básica para programarlos, es que, en cada solapa puedo visualizar el contenido de un panel de modo independiente.

Cuando creo una nueva solapa, indico cual es el panel que va en su interior: public void insertTab(String title, Icon icon, Component component, String tip, int index).

Existe una familia de métodos de comodidad add(...) o addTab(...), donde puedo dejar por especificar alguno de los parámetros.

Paneles con solapas

El ejemplo mostrado lo conseguimos con:

JTabbedPane pestanyas = new JTabbedPane();
pestanyas.add("Rectangulos", new PanelRectangulos());
pestanyas.add("Ovalos", new PanelOvalos());
pestanyas.add("Rectangulos solidos", new PanelRectangulosSolidos());
pestanyas.add("Ovalos solidos", new PanelOvalosSolidos());
...
private class PanelRectangulos extends JPanel {
    @Override
    protected void paintComponent(Graphics g) {
        Graphics2D g2d = (Graphics2D)g;
        g2d.setColor(Color.BLACK);
        g2d.drawRect(10, 10, 100, 200);
        g2d.drawRoundRect(150, 50, 100, 200, 40, 40);
    }
}

Diálogo para selección de ficheros

Swing proporciona una clase muy sofisticada para mostrar una ventana de diálogo en la que el usuario puede elegir un fichero, JFileChooser.

Diálogo para selección de ficheros

Para mostrar una ventana de diálogo que permita abrir un fichero para lectura, debes pasar una referencia al JFrame que retomará el foco cuando se cierre la ventana del selector:

JFileChooser selector = new JFileChooser();
selector.showOpenDialog(ventana);

Una vez que cerrada la ventana de diálogo, el método devuelve una constante que indica lo sucedido:

  • CANCEL_OPTION
  • APPROVE_OPTION
  • ERROR_OPTION

El fichero seleccionado, después que la ventana se haya cerrado, lo puedes recuperar con File getSelectedFile()

Diálogo para selección de ficheros

Si lo quieres es mostrar una ventana de diálogo para grabar un fichero:

JFileChooser selector = new JFileChooser();
selector.showSaveDialog(ventana);

Que también devolverá un entero indicando el resultado de la interacción.

Si quieres personalizar un poco la ventana de diálogo, cambiando el texto de la barra de título y el botón Aceptar puedes utilizar:

JFileChooser selector = new JFileChooser();
selector.showDialog(ventana, "Elige un fichero");

Diálogo para selección de ficheros

Si quieres filtrar por extensión, puedes usar la clase FileNameExtensionFilter, de este modo:

JFileChooser selector = new JFileChooser();
FileNameExtensionFilter filtro = new FileNameExtensionFilter(
        "Fichero texto", "txt", "rtf");
selector.setFileFilter(filtro);
selector.showDialog(ventana, "Elige un fichero");

El constructor de FileNameExtensionFilter es:

FileNameExtensionFilter(String descripcion, String... extensiones);

No lo hagas, usa un array de String en su lugar.

Diálogo para selección de ficheros

Aquí tienes un ejemplo de ventana personalizada y con filtro para el formato de los ficheros que se pueden abrir.

Tablas

Otro componente muy sofisticado es JTable que nos permite visualizar datos en forma de tabla:

Las tablas no tienes barras de desplazamiento, hay que añadirlas con JScrollPane.

Tablas

De nuevo, nos encontramos con el patrón de diseño MVC para la programación de este componente gráfico. JTable visualiza los datos que le proporciona un modelo.

Un modelo para JTable es una clase que implementa la interface TableModel, o más cómodo, que extienda la clase AbstractTableModel quien ya implementa algunos métodos de la interface TableModel.

El modelo, a su vez, debe proporcionar, entre otras cosas, los datos:

Fuente de la imagen: http://docs.oracle.com/javase/tutorial/figures/uiswing/components/model.png

Tablas

public class Modelo extends AbstractTableModel {
    private final String nombreColumnas[] = {"DNI", "Nombre", "e-mail", 
                                            "Grupo Laboratorio"};
    private Object datos[][];

    public int getColumnCount() {
        return nombreColumnas.length;
    }
    public int getRowCount() {
        return datos.length;
    }
    public Object getValueAt(int row, int column) {
        return datos[row][column];
    }
    @Override
    public String getColumnName(int column) {
        return nombreColumnas[column];
    }...

Tablas

Podemos ordenar los elementos de la tabla por columnas simplemente pulsando sobre la cabecera de la columna, si hemos indicado ordenación automática con:

tabla.setAutoCreateRowSorter(true);

También podemos elegir si queremos selección única, múltiple o con espacios intercalados, igual que con JList.

tabla.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);

Tablas

A JTable la podemos escuchar para que nos notifique cuando se ha seleccionado una de sus filas.

El escuchador debe implementar interface ListSelectionListener que es el mismo que usábamos con JList.

public void valueChanged(ListSelectionEvent e) {
    if(e.getValueIsAdjusting() != true) {
        int fila = tabla.convertRowIndexToModel(tabla.getSelectedRow());
        ...

Tablas

Fíjate que si ordenamos los elementos de la tabla, puede ocurrir que el índice de los elementos en la tabla no coincida con los índices de los datos originales.

El método convertRowIndexToModel(int) nos devuelve el índice sobre el modelo a partir de índice sobre la tabla.

Resumen

En este capítulo hemos visto cómo acceder al API 2D para dibujar en el interior de los componentes.

El API 2D nos ofrece métodos para dibujar primitivas sencillas.

También hemos visto cómo detectar los eventos de ratón que todo componente genera cuando el usuario mueve el cursor del ratón sobre él.

Los paneles con solapas, las ventanas de diálogo para apertura de ficheros y las tablas, son algunos de los componentes sofisticados que puedes encontrar el Swing.