Cómo usar JTable

Con la clase JTable se pueden mostrar tablas de datos, permitiendo opcionalmente que el usuario edite los datos. JTable ni contiene ni cachea el contenido; es simplemente una vista de los datos. A continuación mostramos una ilustración con una tabla típica mostrada dentro de una panel de desplazamiento (ScrollPane):

El resto de este documento muestra como afrontar algunas tareas comúnmente relacionadas con las tablas.

  1. Creando una tabla simple.
  2. Añadiendo una tabla al contenedor.
  3. Fijar y cambiar los anchos de columna.
  4. Selecciones de usuario.
  5. Creando un modelo de tabla.
  6. A la escucha de cambios en los datos.
  7. Lanzando eventos de cambio de datos.

Creando una tabla simple

La tabla en SimpleTableDemo.java declara los nombre de columna en un vector de String:

String[] columnNames = {"First Name",
                        "Last Name"
                        "Sport",
                        "# of Years",
                        "Vegetarian"};

Sus datos son inicializados y almacenados en una matriz bidimensional de objetos:

Object[][] data = {
    {"Kathy", "Smith",
     "Snowboarding", new Integer(5), new Boolean(false) },
    {"John", "Doe",
     "Rowing", new Integer(3), new Boolean(true) },
    {"Sue", "Black",
     "Knitting", new Integer(2), new Boolean(false) },
    {"Jane", "White",
     "Speed reading", new Integer(20), new Boolean(true) },
    {"Joe", "Brown",
     "Pool", new Integer(10), new Boolean(false) }
};

La tabla se construye usando data y columnNames:

JTable table = new JTable(data, columnNames);

Hay dos constructores de JTable que aceptan directamente data (SimpleTableDemo usa el primero):

La ventaja de estos constructores es que son fáciles de usar. Sin embargo, estos constructores también tienen inconvenientes:

Si se quiere pasar por encima de estas restricciones, es necesario implementar un modelo de tabla, como se describe en Creando un modelo de tabla.

Añadiendo una tabla al contenedor

Éste es el código típico para crear un panel de desplazamiento que sirva como contenedor a una tabla:

JScrollPane scrollPane = new JScrollPane (table);
table.setFillsViewportHeight (true);

Las dos líneas de este fragmento hacen lo siguiente:

El panel de desplazamiento coloca automáticamente la cabecera de la tabla en la parte superior de la vista. Los nombre de columna se mantienen visibles en la parte superior cuando se desplazan los datos de la tabla.

Si se usa la tabla sin panel de desplazamiento, se debe mantener la cabecera de la tabla en su sitio manualmente. Por ejemplo:

container.setLayout (new BorderLayout());
container.add (table.getTableHeader(), BorderLayout.PAGE_START);
container.add (table, BorderLayout.CENTER);

Fijar y cambiar los anchos de columna

Por defecto, todas las columnas en una tabla tienen la misma anchura, y las columnas llenan automáticamente el ancho total de la tabla. Entonces la tabla se ensancha o se estrecha (lo que puede ocurrir cuando el usuario dimensione la ventana contiene a la tabla), todos los anchos de columna cambian de forma consecuente.

Cuando un usuario dimensiona una columna arrastrando su borde derecho, entonces o bien el resto columna cambian también su tamaño, o bien debe cambiar el ancho de la tabla. Por defecto el ancho de la tabla se conserva, y todas las columnas a la derecha del punto de arrastre se acomodan al espacio añadido o quitado a la columna a la izquierda del punto de arrastre.

Para personalizar el ancho inicial de las columnas, se puede invocar al método setPreferredWidth en cada una de las columnas de la tabla. Esto configura tanto el ancho preferido de las columnas como sus anchos relativos aproximados. Por ejemplo, añadiendo el siguiente código a SimpleTableDemo se consigue que su tercera columna sea más grande que las demás:

TableColumn column = null;
for (int i = 0; i < 5; i++) {
    column = table.getColumnModel().getColumn(i);
    if (i == 2)
        //third column is bigger
        column.setPreferredWidth (100); 
    else
        column.setPreferredWidth (50);
    }

Como muestra el código anterior, cada columna en una tabla se representa mediante un objecto TableColumn. TableColumn proporciona los métodos getter y setter para los anchos mínimos, preferidos y máximo, así como el método para fijar el ancho actual. En el método initColumnSizes() de TableRenderDemo.java se encuentra un ejemplo de cómo establecer los anchos de celda basándose en una aproximación del espacio necesario para dibujar el contenido de las celdas.

Cuando el usuario dimensiona explícitamente las columnas, los anchos de columna preferidos se fijan de tal manera que los tamaños especificados por el usuario se convierten en los nuevos anchos actuales. Sin embargo, cuando se dimensiona la propia tabla -típicamente porque el tamaño de la ventana ha cambiado-; los anchos de columna preferidos no cambian. En vez de eso, los anchos de banda preferidos se usan para calcular los nuevos anchos de columna para llenar el espacio disponible.

Se puede cambiar el comportamiento de dimensionamiento de la tabla invocando setAutoResizeMode().

Selecciones de usuario

En su configuración por defecto, una tabla soporta una selección que consista en una o más filas. El usuario puede seleccionar un rango contiguo o un conjunto arbitrario de filas. La última celda que marque el usuario tiene una indicación especial: en el estilo Metal, la celda aparece recuadrada. Se dice que la celda es el líder de la selección; a veces se la llama la “celda con el foco” o la “celda actual”.

El usuario utiliza el ratón y/o el teclado para hacer las seleccioner, como se describe en la siguiente tabla:

Operación

Acción de ratón

Acción de teclado

Seleccionar una sola fila.

Click.

Flecha arriba o Flecha abajo.

Extender una selección contigua

Mayús-Click o Arrastra sobre filas.

Mayús+Flecha arriba o Mayús+Flecha abajo.

Añadir/quitar una fila a la selección

Control-Click

Mover la selección líder con with Control+Flecha Arriba o Control+Flecha Abajo, entonces use la barra de espacio para añadir a la selección o Control-Barra de Espacio para cambiar el estado de selección.



El método JTable::setSelectionMode() toma un solo argumento, que debe ser una de las siguientes constantes definidas en javax.swing.ListSelectionModel: MULTIPLE_INTERVAL_SELECTION, SINGLE_INTERVAL_SELECTION, y SINGLE_SELECTION.

Los objetos JTable tiene tres variables boolean que controla tres opciones del comportamiento de la selección:

NOTA: JTable usa un concepto de selección muy sencillo, caracterizado como una intersección de filas y columnas. No ha sido diseñado para manejar una selección de celdas completamente independientes.

Si se deshabilitan las tres opciones (poniendo las tres propiedades a false), no hay selección, únicamente se muestra al líder de la selección.

La selección de celdas no está soportada en el modo de selección de intervalos. Se puede pedir la selección de celdas, pero el resultado es una tabla que no produce selecciones útiles.

Cambiar una de los tres opciones de selección puede afectar a los otros dos. Esto es debido a que permitir tanto la selección de filas como de columnas es exactamente lo mismo que habilitar la selección de celdas. JTable automáticamente actualiza el valor de las tres propiedades como se necesite para mantener la consistencia.

NOTA: Fijando cellSelectionEnabled a un valor tiene el efecto colateral de fijar tanto rowSelectionEnabled como columnSelectionEnabled a ese valor. Fijar el mismo valor en rowSelectionEnabled y columnSelectionEnabled tiene el efecto colateral de fijar ese valor en cellSelectionEnabled. Fijar un valor distinto en las propiedades rowSelectionEnabled y columnSelectionEnabled tiene el efecto colateral de fijar el valor de cellSelectionEnabled a false.

Para recuperar la selección actual, debe usarse JTable::getSelectedRows() que devuelve un vector de índices de fila, y JTable::getSelectedColumns() que devuelve un vector de índices de columna. Para obtener las coordenadas del líder de la selección, debe referenciarse el modelo de selección de la propia tabla y del modelo del columna de la tabla. El siguiente ejemplo da formato a una cadena de texto que contiene la fila y columna del líder de la selección:

String.format("Lead Selection:
%d, %d. ",
   
table.getSelectionModel().getLeadSelectionIndex(),
table.getColumnModel().getSelectionModel().getLeadSelectionIndex());

Las selecciones de usuario generan eventos. Para más información sobre éstos, consulte How to Write a List Selection Listener en la lección Writing Event Listeners.

NOTA: Los datos de selección describen las celdas seleccionadas en la “vista” (los datos de la tabla como aparecen después de ordenarse o filtrarse) en vez de en modelo de tabla. Esta diferencia no tiene importancia a menos que los datos mostrados han sido recolocados mediante ordenación, filtrado o manipulación de columnas por el usuario. En ese caso, se debe convertir las coordenadas de selección usando los métodos de conversión descritos en Sorting and Filtering.

Creando un modelo de tabla

Cada objeto de tabla usa un modelo de tabla para gestionar los datos actuales de la tabla. Un modelo de tabla debe implementar una interfaz TableModel. Si el programador no proporciona un modelo de tabla, JTable automáticamente crea una instancia de DefaultTableModel. Esta relación se muestra a continuación.



El constructor JTable usado en el ejemplo del principio utiliza un código similar al siguiente:

new AbstractTableModel() {
    public String getColumnName (int
col) {
        return
columnNames[col].toString();
        }
    public int getRowCount() {
        return rowData.length;
        }
    public int getColumnCount() {
        return columnNames.length;
        }
    public Object getValueAt (int
row, int col) {
        return rowData[row][col];
        }
    public boolean isCellEditable
(int row, int col) {
        return true;
        }
    public void setValueAt (Object
value, int row, int col) {
        rowData[row][col] = value;
        fireTableCellUpdated (row,
col);
        }
    }

Como muestra el código anterior, implementar un modelo de tabla puede ser sencillo. Generalmente, se implementa el modelo de tabla como una subclase de la clase AbstractTableModel.

Su modelo puede almacenar sus datos en una matriz, en un vector, o en un mapa de códigos hash, o coger los datos de una fuente externa tal y como puede ser una base de datos. Incluso puede generar los datos en tiempo de ejecución.

Por contra, este otro ejemplo crea un modelo de tabla más complejo. El código en negrita indica el código que hace que el comportamiento de este modelo de tabla sea diferente del definido automáticamente por JTable.

public TableDemo() {
    ...
    JTable table = new JTable (new
MyTableModel());
    ...
    }

class MyTableModel extends
AbstractTableModel {
    private String[] columnNames =
...//igual
que antes
    private Object[][] data =
...//igual
que antes

    public int getColumnCount() {
        return columnNames.length;
        }
    public int getRowCount() {
        return data.length;
        }
    public String getColumnName (int
col) {
        return columnNames[col];
        }
    public Object getValueAt (int
row, int col) {
        return data[row][col];
        }
    public Class getColumnClass
(int c) {
        return getValueAt(0,
c).getClass();
        }
    /* No
se necesita implementar este método a menos que 
     * la tabla
sea editable.
     */
    public boolean isCellEditable
(int row, int col) {
        /*
Observa que las columnas 0 y 1 no serán
editables
         *
independientemente de que redistribuyan las columnas
         */
        return
(col >= 2);
        }
    /* No
se necesita implementar este método a menos que el
     *
contenido de la tabla pueda cambiar.
     */
    public void setValueAt (Object
value, int row, int col) {
        data[row][col] = value;
        fireTableCellUpdated (row,
col);
        }
    }

El comportamiento de las tablas difiere de la siguiente manera:

A la escucha de cambios en los datos

Un modelo de tabla puede tener un conjunto de escuchadores a los que se notifique de cualquier cambio en los datos de la tabla. Los escuchadores son instancias de TableModelListener. En el siguiente ejemplo, se extiende SimpleTableDemo para que incluya dichos escuchadores. El código nuevo está en negrita.

import javax.swing.event.*;
import
javax.swing.table.TableModel;

public class SimpleTableDemo ...
implements TableModelListener {
    ...
    public SimpleTableDemo() {
        ...
       
table.getModel().addTableModelListener (this);
        ...
        }
    public void tableChanged
(TableModelEvent e) {
        int row = e.getFirstRow();
        int column =
e.getColumn();
        TableModel model =
(TableModel) e.getSource();
        String columnName =
model.getColumnName (column);
        Object data =
model.getValueAt (row, column);

        ...//
Do something with the data...
        }
    ...
    }

Lanzando eventos de cambio de datos

Con el objeto de disparar los eventos de cambio de datos, el modelo de tabla debe saber cómo construir un objeto TableModelEvent. Este procedimiento puede ser complicado, pero ya está implementado en DefaultTableModel. Se puede permitir a JTable que su instancia por defecto de DefaultTableModel, o crear una subclase propia personalizada de DefaultTableModel.

Si DefaultTableModel no se adapta como clase base para su propia clase de modelo de tabla, considere subclasificar AbstractTableModel. Esta clase implementa un marco sencillo para construir objetos TableModelEvent. Su clase personalizada sólo necesita invocar uno de los siguientes métodos de AbstractTableModel cada vez que los datos de la tabla sean cambiados por una fuente externa.

Método

Cambio

fireTableCellUpdated

Actualizada una celda específica.

fireTableRowsUpdated

Actualizada una fila específica.

fireTableDataChanged

Actualizada la tabla entera (sólo los datos).

fireTableRowsInserted

Se han insertado más filas.

fireTableRowsDeleted

Se han borrado filas.

fireTableStructureChanged

Ha cambiado tanto la estructura como los datos de la tabla.