Is this your first time here? SwingWiki is a Java Swing Developer community site with an big archive of Swing-related usenet groups and mailing lists, but also tips, tricks and articles and book reviews written by your colleagues from around the world. If you came here through a search engine and did not find what you were looking for, make sure to check the wiki table of contents.

Column spanning with JTable

In an equaly distributed table, a single cell is an intersection of a single row and a single column. However, for design purposes, it is often convenient to use group table column headers (above a sequence of cells), and sometimes several vertical cells are containing the same data, so they can be merged into one single large cell. Word processing and spreadsheet software usually uses terms like “merged cells” for joined cells, and in HTML terminology, merged cells are often denoted by column-spanning and row-spanning. We will use HTML terminology here, since it is more precise and takes into account a difference betweek horizontal (column) and vertical (row) merging.

Column spanning is a HTML cell property that defines how much “logical” columns a table cell contains. Row spanning is a similar property that defines how much rows a table cell is taking. Throughout this article, term “visible cell” will describe a cell shown on the screen (the merged cell), “logical cell” will describe a cell in an equaly distributed table, and “hidden” cell will describe a “logical” cell not shown on the screen becase it is hidden behing the visible cell. In a group of merged logical cells, only one will be visible, the rest are hidden.

Cell rendering in practice

JTable does not natively support cells spanning accros multiple columns or rows, but it seems that the class was designed to allow non-even distribution of cells. Three specially interesting methods for this goal are getCellRect(), columnAtPoint() and rowAtPoint(). The first returns a bounding Rect of a cell, and the other two return the row and column of the cell containing a specific point on the screen. We will have to override these methods to accomplish column spanning.

When our new table returns the correct information about cell positions and dimensions, all that is left is correcting cell display (rendering). Contrary to most university text-books on Java I have seen, and even most Java GUI books, most components in the Swing library do not render directly from the paint method, but delegate that work to an utility ComponentUI object . This is not because the folk at Sun would like to make your life harder (though it sometimes looks that way), but to allow flexible look and feel manipulation.

Why is this important in this case? For start, we will do nothing useful by directly overriding paint, since we would have to draw the entire table by hand – this makes subclassing JTable preety useless. So, our goal in this case is to find a ComponentUI object that renders the table, and to modify it.

Basic setup

Now that all that is cleared, lets start by solving the problem. First, for simplicity, this article will only deal with column spanning – row spanning can be achieved in the same way.

Since there is currently no data model in Swing that could describe spanning data for cells, we will need a new class that publishes cell dimensions. To draw only parts of the table easier, we will need a way to find out which cell covers some other cell. These two methods will be in the interface CMap.

package com.neuri.ctable;
public interface CMap
{
 /** 
* @param row logical cell row
* @param column logical cell column
* @return number of columns spanned a cell 
*/
int span (int row, int column);
/**
* @param row logical cell row
* @param column logical cell column
* @return the index of a visible cell covering a logical cell 
*/
int visibleCell(int row, int column);
}

Now we can override three required methods of JTable. Since we are currently concerned only with cell spanning, rowAtPoint will not be overriden. However, columnAtPoint() must be modified. We will use the original method to read the logicall cell coordinates, and then calculate the visual cell coordinates.

Fixing getCellRect() will require a bot more work. It is used in the constructor1) of JTable, we must take that also into account execution while the screen cell dimensions and subclass-specific fields are not yet defined. Each of the hidden cells should have the same position and dimensions on the screen as the visible cell that covers it – so other JTable methods do not get invalid information. So, the width of all cells is calculated by adding widths of all spanned logical cells.

Notice the UI property modification in the constructor – that is the object that will render the table on the screen.

package com.neuri.ctable;
import javax.swing.*;
import javax.swing.table.*;
import java.awt.*;
public class CTable extends JTable {
  public CMap map;
  public CTable(CMap cmp, TableModel tbl) {
    super(tbl);
    map=cmp;
    setUI(new CTUI());
  }
  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;
  }
  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);
  }
}
 

Table renderer

ALl that remains is creating a modified version of the table rendering utility object. Various user interface managers use different classes for drawing tables. We will subclass javax.swing.plaf.basic.BasicTableUI and modify it a bit2). The method we must override is paintComponent.

Before the component is drawn on the screen, it is already intialized and set up for work – we can use its internal fields table and rendererPane. Field table is a reference to the table being drawn, and rendererPane is a special object that draws cells on the table. The purpose of that object is to break the direct dependency between cells and the table, and to prevent repainting entire table when only a single cell is modified.

Method getClipBounds is used to find out which part of the table should be drawn, so the first step is to find a range of visible rows - for this, we can use the rowAtPoint method3). We can then examine all cells in those rows, by using the intersects method of the Rectangle class, and find which cells should be drawn on the screen. Before drawing any single cell, we will check if it is visible, and if needed, draw a visible cell instead.

Depending on whether the cell is being edited or not, the cell will be drawn by object returned by getCellEditor or getCellRenderer. If you look at the BasicTableUI source code, you will see that cells are drawn first by calling table.prepareRenderer, and then painted by calling rendererPane.paintComponent. We will apply the same method.

package com.neuri.ctable;
import javax.swing.table.*;
import javax.swing.plaf.basic.*;
import java.awt.*;
import javax.swing.*;
public class CTUI extends BasicTableUI
{
  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=((CTable)table).map.visibleCell(red,i);
        paintCell(row,sk,g,r1);
        // increment the column counter
        i+=((CTable)table).map.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);
    }
  }
}

Examples

Here are an example implementation of CMap and a test program, that were used to generate the following image:

Example implementation of CMAP

package com.neuri.ctable;
import javax.swing.*;
import javax.swing.table.*;
class CMap1 implements CMap
{
  public int span(int row, int column)  {
    if ((row==3) &&(column==3)) return 6;
    if ((row==4)&& (column==7)) return 2;
    if ((row==9)&&(column==5)) return 3;
    return 1;
  }
  public int visibleCell(int row, int column)  {
    if ((row==3)&& (column>=3)&&(column<9)) 
      return 3;
    if ((row==4)&&(column>=7)&&(column <9)) 
    	return 7;
    if ((row==9)&&(column>=5)&&(column<8))
      return 5;
    return column;
  }
}
 

Test program

package com.neuri.ctable;
import javax.swing.*;
import javax.swing.table.*;
public class CTest 
{
  public static void main(String args[])
  {
    JFrame jf=new JFrame("Table with cell spanning");

    CMap m=new CMap1();
    TableModel tm=new DefaultTableModel(15,20);
    jf.getContentPane().add(new JScrollPane(new CTable(m,tm)));
    jf.setDefaultCloseOperation(jf.EXIT_ON_CLOSE);
    jf.setSize(500,500);
    jf.show();
  }
}

What next?

First, implement a more sensible version of CMap, with dynamic column spanning and cell dimension loading. You can also extend CMap to return row-spanning information.

1) look at the JTable source
2) alternatively, modify this class to wrap arround an enclosed BasicTableUI and delegate the work to the enclosed table renderer, this will make your code more flexible
3) if you are implementing row-spanning, you will have to do this differently
 

Comments? Corrections? Contact us or Login to edit pages directly (registration is free and takes less than displaying a JLabel)
  howto/column_spanning.txt · Last modified: 2005/02/21 10:42 by 195.137.75.14 (gojko)
 
Recent changes | RSS changes | Table of contents | News Archive | Terms And Conditions | Register | The Quest For Software++| Ruby Resources
FitNesse Resources