表格(单元格合并)
JTable的单个单元格最复杂的操作也就是Renderer渲染和Editor编辑,然后增加事件处理和悬浮框提示,最多再加点特殊显示效果,在前面的例子里都已经讲过了,这里就剩下最后一个关于单元格的操作了,单元格的合并和拆分.
JTable的单元格可编辑时可以把它看做一个JTextField,不可操作时可以看做一个JLabel,对于单元格的合并和拆分操作来说就是把JLabel或JTextField进行合并和拆分的过程.JTable单元格的合并简单来说就是把你选定的要合并的单元格的边线擦掉,然后调整宽度和高度,再在这几个合并的单元格外围画一个新的边线,然后设置JTable的UI,刷新就可以了.
先看完成后的界面:
然后是工程的目录结构:
其中CustomCell就合并后的Cell,它储存了当前那几个Cell被合并,以及合并后的宽和高用于计算.CustomCellRenderer就是合并后的Cell的Renderer了,它用于合并后Cell内容的显示,CustomCellUI则是合并Cell的UI了,这是主要的合并Cell用到的类,剩下的两个接口ICellAttribute和ICellSpan则是合并Cell的接口,里面定义Cell增删和更新的方法,而SpanCellTablePanel类是因为做成的合并单元格JTable很难使用,提供的辅助类,传入数据则把JTable创建出来,并根据数据实现了合并单元格.
先看接口IcellSpan,它提供设置单元格是否可见,单元格的合并和拆分方法,单元格拆分的Index方法需要实现类完成.
/**
*theinterfacethataboutcellspan.
*/
publicinterface ICellSpan {
方法如下:
/**
*getcellSpan.
*/
publicint[] getSpan(int row, int column);
/**
*setcellspan.
*/
publicvoid setSpan(int[] span, int row, int column);
这两个方法是提供JTable真正的行和列和合并后的行和列的对应关系.
/**
*iscellvisible.
*/
publicboolean isVisible(int row, int column);
/**
*wherecombine.
*/
publicvoid combine(int[] rows, int[] columns);
/**
*wheresplit.
*/
publicvoid split(int row, int column);
提供当前Jtable那几个行和列合并;以及合并后的某一行和列的拆分.
然后是IcellAttribute接口,它提供增加行、增加列、以及插入行、取得和设置大小(不是物理大小,是相对于JTable的大小)的方法:
/**
*theinterfacethataboutcellattribute.
*/
publicinterface ICellAttribute {
再看方法:
/**
*addcolumntocell.
*/
publicvoid addColumn();
/**
*addrowtocell.
*/
publicvoid addRow();
/**
*insertrowtocell
*/
publicvoid insertRow(int row);
这三个方法提供行和列的增加操作
/**
*getcellsize.
*/
public Dimension getSize();
/**
*setcellsize.
*/
publicvoid setSize(Dimension size);
这两个方法提供单元格大小的设置和取得.
再看实现这两个接口的类CustomCell,它是合并后的单元格的信息保存类:
/**
*setcellAttributespanandsoon.
*/
publicclass CustomCell implements ICellAttribute, ICellSpan {
有三个属性:
/**cellwidth.*/
privateintrowSize = 0;
/**cellheight.*/
privateintcolumnSize = 0;
/**cellspans.*/
privateint[][][] span = null;
分别保存合并后单元格的宽度和高度(这个宽和高的意思是在两个方向上有几个JTable的单元格大小),以及合并的单元格是由原本JTable的那几个行和列组成的.
先是构造函数:
/**
*setcellattribute.
*/
public CustomCell() {
this(1, 1);
}
setSize(new Dimension(columnSize, rowSize));
在setSize方法里,初始化属性:
@Override
publicvoid setSize(Dimension size) {
columnSize = size.width;
rowSize = size.height;
span = newint[rowSize][columnSize][2];
initValue();
}
然后是初始化方法,
/**
*setcellinit.
*/
privatevoid initValue() {
for (int i = 0; i < span.length; i++) {
for (int j = 0; j < span[i].length; j++) {
span[i][j][ICellSpan.COLUMN] = 1;
span[i][j][ICellSpan.ROW] = 1;
}
}
}
然后就是实现接口的方法,
@Override
publicint[] getSpan(int row, int column) {
返回span数组对应的行和列:
returnspan[row][column];
@Override
publicvoid setSpan(int[] span, int row, int column) {
设置span数组对应的行和列:
this.span[row][column] = span;
@Override
publicvoid addColumn() {
@Override
publicvoid addRow() {
@Override
publicvoid insertRow(int row) {
这三个方法很类似,都是先取得旧有的span:
int[][][] oldSpan = span;
int numRows = oldSpan.length;
int numColumns = oldSpan[0].length;
然后创建新的:
span = newint[numRows + 1][numColumns][2];
System.arraycopy(oldSpan, 0, span, 0, numRows);
最后赋予新的值:
for (int i = 0; i < numColumns; i++) {
span[numRows][i][ICellSpan.COLUMN] = 1;
span[numRows][i][ICellSpan.ROW] = 1;
}
最后是比较重要的合并和拆分方法,它根据传入的需要合并和拆分的行和列计算,得出数组新的值:
@Override
publicvoid combine(int[] rows, int[] columns) {
先取得开始比较的起点:
int rowSpan = rows.length;
int columnSpan = columns.length;
int startRow = rows[0];
int startColumn = columns[0];
对于需要修改的值比较并赋予新的:
for (int i = 0, ii = 0; i < rowSpan; i++, ii--) {
for (int j = 0, jj = 0; j < columnSpan; j++, jj--) {
span[startRow + i][startColumn + j][ICellSpan.COLUMN] = jj;
span[startRow + i][startColumn + j][ICellSpan.ROW] = ii;
}
}
最后设置新的数组值:
span[startRow][startColumn][ICellSpan.COLUMN] = columnSpan;
span[startRow][startColumn][ICellSpan.ROW] = rowSpan;
这样新的span就形成了,当画面repaint时,UI会根据新的span的值决定那个单元格的Border需要绘制,那个需要擦去,这样就单元格方面完成了拆分,至于持分后内容的显示则是通过Rebderer控制的.
这里我们的Rebderer类十分简单,我们就不实现任何效果了,只确定合并后的显示问题:
publicclass CustomCellRenderer extends JLabel implements TableCellRenderer {
它的接口实现方法
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
除了定义颜色背景色的基本属性外,最主要的是设置显示:
setText((value == null) ? "" : value.toString());
然后就是很重要的CustomCellUI类了,它根据前面CustomCell类的span来绘制合并单元格后的显示问题:
/**
*BasicTableUIimplementation,paintthespantableui.
*/
publicclass CustomCellUI extends BasicTableUI {
它重写BasicTableUI的paint方法进行自己的UI绘制,
/**
*Paintarepresentationofthetableinstancethatwasset
*ininstallUI().
*/
@Override
publicvoid paint(Graphics g, JComponent c) {
首先取得旧的绘制边框:
Rectangle oldClipBounds = g.getClipBounds();
Rectangle clipBounds = new Rectangle(oldClipBounds);
然后根据JTable的宽度比较决定绘制的宽度,
int tableWidth = table.getColumnModel().getTotalColumnWidth();
clipBounds.width = Math.min(clipBounds.width, tableWidth);
g.setClip(clipBounds);
取得行的边框
Rectangle rowRect = new Rectangle(0, 0, tableWidth, table
.getRowHeight() + table.getRowMargin());
rowRect.y = firstIndex * rowRect.height;
然后开始绘制行:
先去的需要绘制的合并单元格的属性:
CustomTableModel tableModel = (CustomTableModel) table
.getModel();
ICellSpan cellAtt = (ICellSpan) tableModel.getCellAttribute();
然后算出本行内那几个列需要合并:
cellRow = row + cellAtt.getSpan(row, column)[ICellSpan.ROW];
cellColumn = column
+ cellAtt.getSpan(row, column)[ICellSpan.COLUMN];
最后就是绘制具体的单元格了:
Color c = g.getColor();
g.setColor(table.getGridColor());
g.drawRect(cellRect.x, cellRect.y, cellRect.width - 1,
cellRect.height - 1);
g.setColor(c);
cellRect.setBounds(cellRect.x + spacingWidth / 2, cellRect.y
+ spacingHeight / 2, cellRect.width - spacingWidth,
cellRect.height - spacingHeight);
不仅如此还需要控制编辑状态和普通状态有Renderer的显示问题:
Component component = table.getEditorComponent();
component.setBounds(cellRect);
component.validate();
//renderer
rendererPane.paintComponent(g, component, table, cellRect.x,
cellRect.y, cellRect.width, cellRect.height, true);
到这里单元格合并后的显示就完成了,还有就是JTable的显示和TableModel的数据设置,先看TableModel:
我们继承DefaultTableModel类,
publicclass CustomTableModel extends DefaultTableModel {
它的构造函数和DefaultTableModel一样,最终都会转换为Vector,
public CustomTableModel(Vector<Vector<?>> data, Vector<?> columnNames) {
addDataVector(data, columnNames);
}
在它的addDataVector方法里,不但要给TableModel的数据集赋:
dataVector = new Vector<Vector<?>>(0);
setColumnIdentifiers(columnNames);
dataVector = newData;
还要根据行和列做成我们的CustomCell:
cellAtt = new CustomCell (dataVector.size(), columnIdentifiers
.size());
同样的TableModel的addColumn、addRow、insertRow方法,我们都需要复写修改,加上我们自己Cell的增加方法:
cellAtt.addRow();
cellAtt.insertRow(row);
cellAtt.addColumn();
最后我们修改的是JTable类,我们继承它,设置它的UI和鼠标行和列的响应:
publicclass CustomTabel extends JTable {
构造函数和JTable相同,只不过我们需要设置自己的UI,
setUI(new CustomCellUI());
然后复写rowAtPoint和columnAtPoint得到鼠标点击时我们真正的行列:
@Override
publicint rowAtPoint(Point point) {
首先是取得当前所在行列和合并单元格的值:
int row = point.y / (rowHeight + rowMargin);
int column = getColumnModel().getColumnIndexAtX(point.x);
ICellSpan cellAtt = (ICellSpan) ((CustomTableModel) getModel())
.getCellAttribute();
然后取得实际的:
retValue[ICellSpan.COLUMN] = column
+ cellAtt.getSpan(row, column)[ICellSpan.COLUMN];
retValue[ICellSpan.ROW] = row
+ cellAtt.getSpan(row, column)[ICellSpan.ROW];
最后需要重写的是方法,它保证了合并单元格后的行和列和Header的对应,不会因为去掉了单元格的Boder线使不能对齐:
@Override
public Rectangle getCellRect(int row, int column, boolean includeSpacing) {
和UI里的paint方法一样就是计算补足没有Border的间隙,一般每合并一个加上1就可以了.
到这里为止,合并单元格的JTable就算完成了,但是比较麻烦的是因为它并不知道我们合并那个,不会主动给我们合并,需要我们自己去调用combine方法,比较复杂,而实际使用的时候,我们想告诉数据是什么样子的希望JTable自己合并,因此写了一个SpanCellTablePanel类,只需传入数据就可以自己合并了.
publicclass SpanCellTablePanel extends JPanel {
继承Jpanel确保我们可以和一个普通的JPanel一样使用它.
它初始化TableModel并构建了JTable:
CustomTableModel model = new CustomTableModel(datas, convertToVector(columnTitle));
CustomTabel table = new CustomTabel(model)
提供了我们取得JTable的方法:
public JTable getTable() {
returntable;
}
提供一个我们给定数据转换为合并的数据的方法:
privateint[][] combineSpanData(Vector<Vector<?>> datas) {
然后根据数据合并单元格显示出来:
for (int i = 0; i < columns.length; i++) {
for (int t = 0; t < spanArray.length; t++) {
((ICellSpan) cellAtt).combine(spanArray[t],
newint[] { columns[i] });
}
}
table.clearSelection();
table.revalidate();
table.repaint();
最后就是使用了,只需要要传入我们的数据就可以构建出可合并的JTable了:
spanTablePanel = new SpanCellTablePanel(createTestData());
当我们实际使用时可能希望选中任何一个单元格都选中目前处于的最大列全选择,简单的加个
SelectionListener就可以了
spanTablePanel.getTable().getSelectionModel().addListSelectionListener(
在事件里处理选中:
@Override
publicvoid valueChanged(ListSelectionEvent e) {
spanTablePanel.getTable().getSelectionModel()
.setSelectionInterval(allSelectRows[0],
llSelectRows[allSelectRows.length - 1]);
到此为之,对单元格的操作就基本结束了,以后看到或者想到别的再补充,JTable剩下的比较复杂的就是JtableHeader了,它也可以设置Rendere和Editor,也可以合并和拆分,可以设置特殊组件,下次就开始JtableHeader.