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:
Gracias por su buen ejemplo.
De nada @Jon, gracias por leerme y valorar mis aportes.
@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
@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 :)
Como podria hacer lo mismo pero para expandir solo ciertas celdas pero tendiendo en cuenta la fila?
Muy bueno tu aporte! y como sería un rowspan??
Graciass
Publicar un comentario