jueves, 11 de agosto de 2011

Colspan JTable, expandir columnas en tablas de Swing

8 comentarios
Hola de nuevo,

Hoy vamos a intentar explicar como expandir las columnas de un JTable, esto es muy común y fácil cuando hablamos de código HTML con las propiedades colspan y rowspan de las tablas. Pero en Swing esta característica no está contemplada, por lo que tendremos que darle una vuelta a la gestión de las celdas de un JTable.

En primer lugar necesitaremos un mapeo de las celdas que queremos expandir, para ello crearemos un mapa con las claves de fila y columna que indican la celda. Nos valdremos de la siguiente interfaz para gestionar nuestro mapa y la información que necesitamos.


package colspanTable;

import java.util.HashMap;

public interface ColSpanMap {

	/**
	 * Indica si la celda [row,column] esta expandida
	 * @param row celda logica de fila
	 * @param column celda logica de columna
	 * @return numero de columnas expandiodas por celda
	 */
	int span(int row, int column);

	/**
	 * Devuelve el indice de las celda visibles.
	 * @param row celda logica de fila
	 * @param column celda logica de columna
	 * @return indice de la celda visible que cubre la celda logica
	 */
	int visibleCell(int row, int column);

	/**
	 * Establece el mapeo de filas/columnas con celdas expandidas y su posicion.
	 * @param spanMap Mapa
	 */
	void setSpanMap(HashMap< Integer, Integer > spanMap);

}

La implementación debería seguir un modelo similar a este que os presento a continuación.


package colspanTable;

import java.util.HashMap;

class TableColSpanMap implements ColSpanMap {

	/** Columnas a expandir por celda. */
	private static final int CELLS_TO_SPAN = 5;

	/** Mapeo de las celdas a expandir. */
	private HashMap< Integer, Integer > spanMap;

	@Override
	public int span(int row, int column) {
		if (spanMap != null && spanMap.containsKey(row)) {
			if (spanMap.get(row) == column) {
				return CELLS_TO_SPAN;
			}
		}
		return 1;
	}

	@Override
	public int visibleCell(int row, int column) {
		if (spanMap != null && spanMap.containsKey(row)) {
			if (column >= spanMap.get(row) && column < spanMap.get(row) + CELLS_TO_SPAN) {
				return spanMap.get(row);
			}
		}
		return column;
	}

	@Override
	public void setSpanMap(HashMap< Integer, Integer > spanMap) {
		this.spanMap = spanMap;
	}
}

Ahora en nuestra tabla, a la hora de consultar la posición, índice y anchura de las celdas de la tabla deberemos de tener en cuenta nuestro mapeo especial, para ello sobrescribimos los métodos getCellRect para el tamaño de celda y getColumnAtPoint para buscar los índices correctos a la hora de editarla, además al contruir nuestra tabla le pasaremos en el constructor el mapeo de las celdas expandidas. Los cambios los muestro a continuación:

package colspanTable;

import java.awt.Point;
import java.awt.Rectangle;

import javax.swing.JTable;
import javax.swing.table.TableModel;

public class SpanTable extends JTable {

	/** Map with expanded columns mapped. */
	private ColSpanMap map;

	/**
	 * Constructor.
	 * @param csm Mapa de celdas
	 * @param tbl Modelo de la tabla
	 */
	public SpanTable(ColSpanMap csm, TableModel tbl) {
		super(tbl);
		map = csm;
		setUI(new ColSpanTableUI());
	}

	/**
	 * Nos devuelve el mapa de las celdas a expandir.
	 * @return map
	 */
	public ColSpanMap getSpanMap() {
		return map;
	}

	@Override
	public Rectangle getCellRect(int row, int column, boolean includeSpacing) {
		// required because getCellRect is used in JTable constructor
		if (map == null)
			return super.getCellRect(row, column, includeSpacing);
		// add widths of all spanned logical cells
		int sk = map.visibleCell(row, column);
		Rectangle r1 = super.getCellRect(row, sk, includeSpacing);
		if (map.span(row, sk) != 1)
			for (int i = 1; i < map.span(row, sk); i++) {
				r1.width += getColumnModel().getColumn(sk + i).getWidth();
			}
		return r1;
	}

	@Override
	public int columnAtPoint(Point p) {
		int x = super.columnAtPoint(p);
		// -1 is returned by columnAtPoint if the point is not in the table
		if (x < 0)
			return x;
		int y = super.rowAtPoint(p);
		return map.visibleCell(y, x);
	}
}
Por último solo nos quedaría cuidarnos de la parte gráfica, tendremos que establecerle un UI peculiar a la tabla que nos pinte la rejilla de las celdas especial que necesitamos donde tendremos en cuenta las partes visibles de cada celda.
package colspanTable;

import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;

import javax.swing.JComponent;
import javax.swing.plaf.basic.BasicTableUI;
import javax.swing.table.TableCellRenderer;

public class ColSpanTableUI extends BasicTableUI {

	@Override
	public void paint(Graphics g, JComponent c) {
		Rectangle r = g.getClipBounds();
		int firstRow = table.rowAtPoint(new Point(0, r.y));
		int lastRow = table.rowAtPoint(new Point(0, r.y + r.height));
		// -1 is a flag that the ending point is outside the table
		if (lastRow < 0)
			lastRow = table.getRowCount() - 1;
		for (int i = firstRow; i <= lastRow; i++)
			paintRow(i, g);
	}

	private void paintRow(int row, Graphics g) {
		Rectangle r = g.getClipBounds();
		for (int i = 0; i < table.getColumnCount(); i++) {
			Rectangle r1 = table.getCellRect(row, i, true);
			if (r1.intersects(r)) // at least a part is visible
			{
				int sk = ((SpanTable) table).getSpanMap().visibleCell(row, i);
				paintCell(row, sk, g, r1);
				// increment the column counter
				i += ((SpanTable) table).getSpanMap().span(row, sk) - 1;
			}
		}
	}

	private void paintCell(int row, int column, Graphics g, Rectangle area) {
		int verticalMargin = table.getRowMargin();
		int horizontalMargin = table.getColumnModel().getColumnMargin();

		Color c = g.getColor();
		g.setColor(table.getGridColor());
		g.drawRect(area.x, area.y, area.width - 1, area.height - 1);
		g.setColor(c);

		area.setBounds(area.x + horizontalMargin / 2, area.y + verticalMargin / 2, area.width - horizontalMargin, area.height - verticalMargin);

		if (table.isEditing() && table.getEditingRow() == row && table.getEditingColumn() == column) {
			Component component = table.getEditorComponent();
			component.setBounds(area);
			component.validate();
		} else {
			TableCellRenderer renderer = table.getCellRenderer(row, column);
			Component component = table.prepareRenderer(renderer, row, column);
			if (component.getParent() == null)
				rendererPane.add(component);
			rendererPane.paintComponent(g, component, table, area.x, area.y, area.width, area.height, true);
		}
	}
}
Bien, espero que todo este lo más claro posible. Como siempre os dejo un ejemplo funcionando :)
package colspanTable;

import java.util.HashMap;

import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableModel;

public class Test {

	private static void createAndShowGUI() {
		JFrame frame = new JFrame("Tabla con celdas expandidas");

	    ColSpanMap m = new TableColSpanMap();
	    HashMap< Integer, Integer > cellSpanMap = new HashMap();
	    // Aqui los indices [fila,columna] de las celdas que queremos expandir
	    cellSpanMap.put(3, 3);
	    cellSpanMap.put(4, 7);
	    cellSpanMap.put(9, 5);
	    m.setSpanMap(cellSpanMap);
	    TableModel tm=new DefaultTableModel(15,20);
	    SpanTable table = new SpanTable(m,tm);

	    frame.getContentPane().add(new JScrollPane(table));
	    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	    frame.setSize(500, 295);
	    frame.setVisible(true);
	}

	public static void main(String[] args) {

		javax.swing.SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				createAndShowGUI();
			}
		});
	}
}

Espero que os siga siendo útil todo lo que aquí se dice, vuestros comentarios y dudas siempre son bienvenidos.

Un saludo y hasta la próxima