jueves, 11 de agosto de 2011

Colspan JTable, expandir columnas en tablas de Swing

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


8 comentarios:

Jon Keatley dijo...

Gracias por su buen ejemplo.

Unknown dijo...

De nada @Jon, gracias por leerme y valorar mis aportes.

Jon Keatley dijo...

@Angelon, a partir de su código de ejemplo, he creado un código que le permite crear una JTable con un número arbitrario de rowspans o colspans. Echa un vistazo a esto:

https://code.google.com/p/spantable

Unknown dijo...

@Jon, me alegro de que le haya sido útil para crear su librería y espero que le ayude igualmente a otros desarrolladores.
BTW you have a really nice portfolio :)

Unknown dijo...
Este comentario ha sido eliminado por el autor.
Unknown dijo...
Este comentario ha sido eliminado por el autor.
Unknown dijo...

Como podria hacer lo mismo pero para expandir solo ciertas celdas pero tendiendo en cuenta la fila?

Anónimo dijo...

Muy bueno tu aporte! y como sería un rowspan??
Graciass

Publicar un comentario