一、单元格合并。

(1)我们可以使用Jtable的三个方法:getCellRect(),columnAtPoint(),and rowAtPoint()。第一个方法返回一个单元格的边界(Rectangle类),第二、三个方法分别返回屏幕指定位置的列和行。为了实现单元格合并,我们需要重载(overwrite)这三个方法。 

(2)另外我们需要找出渲染Jtable的ComponentUI对象,并且修改它以达到我们的目的。 

(3)创建新的类记录单元格合并情况的数据模型,它要包涵一个方法来取得单元格的所跨越的列数。另外,为了使用Jtable画(paint)起来更容易些,我们需要一个方法来确定指定单元格是否被其它单元格所覆盖,被哪个单元格覆盖。我们将这两种方法都集成在接口Cmap里: 


代码:

package com.neuri.ctable; 
public interface CMap 

/** 
* @参数row:指定单元格所在的逻辑行 
* @参数column:指定单元格所在的逻辑列 
* @返回指定单元格所跨越的列数 
*/ 
int span (int row, int column); 
/** 
* @参数row:指定单元格所在的逻辑行 
* @参数column:指定单元格所在的逻辑列 
* @返回覆盖指定单元格的可视单元格的列值,如果单元格本来就是可视的话,返回自身的列值 
*/ 
int visibleCell(int row, int column); 



现在我们开始重载上面提及过的三个方法。由于我们目前只关注于跨列单元格的合并,方法rowAtPoint()就不用重载了。然而,方法columnAtPoint()就必须重载了,我们会使用Jtable自身的方法来取得指定单元格的列值,并且计算出覆盖该单元格的可视单元格列值(如果该单元格本来就是可视的,则返回自身列值)。在单元格合并后,在合并区域内只有一个跨越多列的可视单元格,其它被覆盖的单元格则不会再被渲染。当使用getCellRect()方法取得被覆盖的单元格的大小时,都返回覆盖该单元格的可视单元格的大小。 

代码:

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());//设置Jtable的渲染UI 
 } 
public Rectangle getCellRect(int row, int column, boolean includeSpacing){ 
// 该方法是Jtable构建时所必须的 
 if (map==null) return super.getCellRect(row,column, includeSpacing); 
// 指定单元格的可视单元格列值 
 int sk=map.visibleCell(row,column); 
 Rectangle r1=super.getCellRect(row,sk,includeSpacing); 
// 如果指定单元格列宽不为1,累计出跨列单元格的宽度 
 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); 
// 当指定位置不在Table内时,返回-1 
  if (x<0) return x; 
  int y=super.rowAtPoint(p); 
//获取指定位置可视单元格的列值 
  return map.visibleCell(y,x); 
  } 




现在剩下的就只有创建一个表格的渲染对象了。不同的用户接口管理器(user interface managers)使用不同的类来画表格。我们会继承子类 javax.swing.plaf.basic.BasicTableUI,并且重载其方法 paintComponent。 

在组件(component)画在屏幕上之前,它已经被初始化和设定好了,所以我们能使用其内部的属性 table 和 rendererPane。属性 table 就是将要被画在屏幕的表格,rendererPane 是用于将单元格画在表格中的特殊对象。使用RendererPane的目的是打破单元格和表格的直接依赖关系,并且防止当一个单元格被修改时重画整个表。 


BasicTableUI的方法getClipBounds是用于找出表格的哪一部分将会被画出来,所以我们首先要知道那些行是可视的,我们可以使用Jtable 的rowAtPoint方法。我们可以使用Rectangle类的intersects方法来确定这些行中的所有单元格是否将会被画在屏幕上。在我们画任何一个单元格前,我们必须检查一下当前单元格是否可视,如果该单元格是被其它单元格所覆盖的,就将覆盖它的单元格画出来。 


根据单元格是否正在被编辑,单元格将会被方法getCellEditor或getCellRenderer所返回的对象画出来如果你查看一下BasicTableUI的源代码,你就会发现所以单元格会先被BasicTableUI调用table.prepareRenderer画(drawn)出来,然后再被BasicTableUI调用rendererPane.paintComponent来渲染(paint)。我们会采用同样的方法。 
代码:

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); 
  } 
 } 


http://www.swingwiki.org/howto:column_spanning 

今天下载了关于java swing的一个开源项目包tame,不过由于完成年代久远(98年),很多类在新的jdk1.4或jdk1.5上已经会报错。例如AttributiveCellTableModel类的setDataVector方法便需要改为:
public void setDataVector(Vector newData, Vector columnNames)
{
super.setDataVector(newData, columnNames);
cellAtt = new DefaultCellAttribute(dataVector.size(),columnIdentifiers.size());
}
有心重整tame,先记一笔。

①java swing基于MVC架构,或者说是Model-driven结构。以jtable为例,它的特有GUI-State Model是 TableColumnModel(JTable是面向列的,它基于每一列进行绘制和编辑。分别是列绘制器TableCellRenderer和列编辑器TableCellEditor);它的共有GUI-State Model是Selection Model(和jlist、jtree等共用)。除了GUI-State Model,还有决定显示在控件中的内容的Application-data model,jtable的Application-data model是TableModel。实现方式是先定义接口TableModel,再定义抽象类AbstractTableModel实现这个接口,然后由DefaultTableModel实现抽象类。不过一般来说用户需要自己扩展AbstractTableModel实现它的几个方法来获取和设定值。
 
②以jtable为例,它并未提供实现单元格合并的方法。所以我们需要重载它的三个方法(getCellRect:获取单元格的边界,columnAtPoint和rowAtPoint:分别返回屏幕指定位置的列和行 )。
 
③现在我们需要自己绘制jtable,所以要用到Graphics类。另大部分的swing components 并不是直接由paint()方法来渲染(render),而是使用ComponentUI对象来完成渲染的。所以我们需要找出渲染Jtable的ComponentUI对象(BasicTableUI),并且修改它(重载paint()方法)以达到我们的目的。
 
④现在开始具体实现,tame先定义了4个接口(CellAttribute、ColoredCell、CellFont、CellSpan),用DefaultCellAttribute类实现了这四个接口,包含相关表格的基本属性(颜色、字体、合并单元格的属性等),这个类将每个cell定义为一个三维数组int[][][] span,并且都初始化为1。继承自DefaultTableModel的AttributiveCellTableModel负责初始化table,至此并无特异之处。当需要合并单元格时。监听按钮将调用DefaultCellAttribute的combine方法把被覆盖的单元格的三维数组int[][][] span设置为小于1,这样在绘制的时候就可以判断哪单元格可见,哪些单元格不可见了。接着通过重载jtable的三个方法得到cell的边界以及行和列的位置,重绘表格的时候通过每个cell返回的结果(边界、位置、是否可见)等循环绘制。