如何实现复合数据类型的组件的渲染呢?最直接的是在paint方法中一个一个地根据数据类型画出每一个组件,但这种方法很显然代码复用率很低,大量重复了相应标量型组件的代码,代码的维护和同步会非常困难,也不容易实现皮肤切换。
为解决此问题,Swing体系中提出了所谓渲染器(Renderer)的概念,其核心思想是使用接口,封装和复用已有标量型组件的渲染代码,降低代码重复率,提高组件的可扩展性。
渲染器是如何工作的呢?我们以JTable的渲染代码来演示渲染器的工作原理。下面代码是从JTable的UI类javax.swing.plaf.basic.BasicTableUI中摘出的部分代码(JDK
6):
//注:为清晰期间,只显示了关键代码
public void paint(Graphics g,
JComponent c) {
......
//计算当前可显示区域的行列范围
......
//
画出表格的分割线
paintGrid(g, rMin, rMax, cMin, cMax);
//
画出表格单元格的内容
paintCells(g, rMin, rMax,
cMin, cMax);
//如果正在拖动列,画出正在被拖动的线
paintDropLines(g);
}
paintCells实现所有单元格的渲染过程:
private void
paintCells(Graphics g, int rMin, int rMax, int cMin, int
cMax) {
......
for(int row = rMin; row <= rMax; row++)
{
cellRect = table.getCellRect(row, cMin,
false);
for(int column = cMin; column <= cMax; column++)
{
//遍历表格的每一个单元格
.......
paintCell(g, cellRect, row, column);
cellRect.x += columnWidth;
}
}
......
}
渲染单元格的方法paintCell的实现:
private void
paintCell(Graphics g, Rectangle cellRect, int row, int
column) {
if
(如果当前格是正在编辑的) {
//重新设置编辑器的边框
......
}
else
{
//获取当前格的渲染器
TableCellRenderer renderer = table.getCellRenderer(row,
column);
//配置当前的渲染器
Component component = table.prepareRenderer(renderer,
row, column);
//使用渲染器来渲染当前表格
rendererPane.paintComponent(g, component, table,
cellRect.x, cellRect.y,
cellRect.width, cellRect.height, true);
}
}
public Component
prepareRenderer(TableCellRenderer renderer, int row, int
column) {
Object
value = getValueAt(row, column);
......
//调用渲染器的配置方法配置并获取合适的渲染组件
return
renderer.getTableCellRendererComponent(this,
value,
isSelected, hasFocus,
row, column);
}
如何使用渲染器返回的组件渲染当前的单元格呢?JTable在自己内部隐藏了一个所谓的CellRendererPane组件,该组件是一个“零实现”的容器组件。虽然被添加到JTable上,但它是不可见的,其paint和update方法都为空,仅仅作为临时容纳渲染组件的容器,目的是将渲染组件粘合到JTable组件树上,使得渲染组件有效化,以便使它们达到渲染前的正确状态。下面代码演示了CellRendererPane的概要结构:
public class CellRendererPane extends
Container implements
Accessible
{
//构造函数
public CellRendererPane()
{
super();
//注意CellRendererPane的布局管理器为空,后面渲染时有用!
setLayout(null);
//不可见,使之不被显示在JTable上
setVisible(false);
}
//零实现
public
void invalidate() { }
//零实现
public void paint(Graphics
g) { }
//零实现
public void update(Graphics g) { }
......
//下面是CellRendererPane的paintComponent方法:
public void paintComponent(Graphics g, Component c,
Container p, int x, int y, int w, int h, boolean
shouldValidate)
{
......
if (c.getParent() != this)
{//如果渲染组件c还没有添加当前CellRendererPane中
//添加进去
this.add(c);
}
//构造函数将布局管理器设置为空,setBounds将渲染组件设置成相应的位置和尺寸<
/p>
c.setBounds(x, y, w,
h);
//将渲染组件在JTable的组件树上有效化,目的是如果渲染组件是一个有内部结构的复合组件,比如一个JTextField
和一个JButton构成的一个复合框组件,该过程将会使内部组件进行布局,并递归此过程,使得该组件达到正常显示应该达到的效果。<
/font>
if(shouldValidate)
{
c.validate();
}
//下面主要处理双缓冲问题,可略去
......
//准备图形对象
Graphics cg = g.create(x,
y, w, h);
try {
//调用渲染组件的重画方法渲染,注意由于cg其实是JTable的图形对象,因此其效果是将该组件渲染到JTable上相应单元格,从而达到了代码复用。
c.paint(cg);
}
......
}
......
}
渲染器的核心思想都体现在上面红色代码标注的部分。将JTable的图形对象传递给组件的paint的方法,产生的结果是将组件画到了JTable上。其实Swing打印的原理也大抵如此,只不过这儿的图形对象变成了打印机的图形对象。虽然大部分Swing组件都专门对打印进行了专门的处理(主要是因为有一些图形元素不希望被打印的,比如填充的内容往往不希望打印,可能是太耗墨了),但基本过程是一样的。
渲染器的思想很像是摄像机、镜子等成像原理。作个比喻,如果你想获取某人的图像,一种方法是将此人一点点用笔画出来。另种方法是通过光线将此人照到镜子里或用照相机拍摄下来。其好处是不管是什么物体,都可以映射出来,具有很强的可扩展性。比如JTable中,表格中不仅可以使用JLabel、JCheckBox、JComboBox等简单组件作为渲染器,而且可以使用其它任何的Swing组件进行渲染,包括复杂的组件JTable(比如实现表格套表的风格)、自定义的组件(比如嵌入图片)。渲染器方法带来的好处不仅仅是组件代码的复用,更带来了无限的可扩展性!
渲染器思想在Swing中有着广泛的应用。除利用它们实现JTable、JList、JTree和JComboBox等标准组件,还可以实现界面设计工具中属性页、类似UML设计图、类似于MS
Excel风格的电子表格等更为复杂的界面组件,甚至IDE中常见的界面设计工具也是利用了渲染器的思想,它把整个组件树当作一个大渲染器,渲染出当前图形用户界面的设计效果。
渲染器是Swing展现复杂数据结构的利器。但是Swing组件不仅仅被用作展现数据,通常还是编辑数据的地方。实际上纯粹展现数据的Swing组件很少,标准组件中也许只有JLabel。复合数据类型的组件往往使用渲染器原理实现组件的渲染,使用所谓in-place
editor实现组件的编辑。渲染器Renderer和Editor的结合赋予了Swing强大的灵活性,JTable、等组件这两种原理结合的代表。后续文章将讲述in-place
editor在Swing中的使用。