服务热线:13616026886

技术文档 欢迎使用技术文档,我们为你提供从新手到专业开发者的所有资源,你也可以通过它日益精进

位置:首页 > 技术文档 > JAVA > 新手入门 > 基础入门 > 查看文档

java指南之使用图形:自定义绘图概览


  自定义绘图概览
如果你还没有读过 绘图 一节,请现在就看看。那节描述了swing 组件是如何被绘制的 --如果你要写自定义绘图代码,那些就是基础知识。
在实现一个自定义绘图的组件前首先请确认你真的需要这样做。你可能可以使用 标签, 按钮或者 文本组件 的功能代替。记住,你可以使用 边界自定义组件的外部边界。

如果你真的需要进行自定义绘图,那么就需要决定使用哪个超类。我们推荐要么扩展jpanel要么使用一个更特殊的swing组件。例如,如果你想创建一个自定义按钮,你也许应该通过扩展一个像jbutton 或者 jtogglebutton 这样的按钮类来实现它。用那个方法,你就可以继承那些类提供的状态管理功能。如果你正创建一个在图像上绘图的组件,你也许想创建一个jlabel 的子类。另一方面,如果你正实现一个在空白的或者透明的背景上产生和显示图表的组件,那么你可能想使用jpanel作为超类。

在实现自定义绘图代码的时候,记住两件事:

你的绘图代码应该在一个名字为 paintcomponent的方法里面。
你可以 -- 而且可能是应该 -- 使用一个边界绘制你的组件的外边缘。
自定义绘图的一个范例
下面的代码给出了一个自定义绘图的范例。它将一个图像显示两次,一次以图像的原始大小一次非常宽。
class imagepanel extends jpanel {
...
public void paintcomponent(graphics g) {
super.paintcomponent(g); //paint background

//首先以图像的原始大小显示。
g.drawimage(image, 0, 0, this); //85x62 image

//现在显示缩放的图像。
g.drawimage(image, 90, 0, 300, 62, this);
}
}

下面是结果:


这个图片是该applet的gui。要运行那个applet,单击图片。该applet将在一个新浏览窗口显示。

范例代码来自 imagedisplayer.java,它的更进一步的讨论在 显示图像中。那个范例示范了在组件进行自定义绘图时的一些新规则:

绘图代码执行一些标准的swing组件所没有的动作。如果我们只想将图像以它的原始大小显示一次,我们应该使用jlabel 对象而不是使用自定义组件。
自定义组件是jpanel的子类。这是自定义组件的一个常用的超类。
所有的自定义绘图代码都在paintcomponent方法里面。
在执行任何自定义绘图前,通过调用super.paintcomponent让组件绘制自己的背景。如果你没有调用它,要么自己的代码绘制组件的背景,要么对组件调用setopaque(false)。使用后者将通知swing绘图系统在透明的组件后面的组件可能是可见的因此应该被绘制。
这个组件没有考虑的一个事情就是边界。它不仅没有使用边界,而且没有调整它的绘图坐标以考虑有边界的情况。一个产品级的组件应该像下一小节描述的那样为边界进行调整。


坐标系统
每个组件都有自己的整型坐标系统,范围从(0, 0) 到 (width - 1, height - 1),单位是象素。像下面的图片显示的那样,组件的绘图区的左上角是(0, 0)。x坐标向右增加而y坐标向下增加。



在绘制一个组件时,你不仅要考虑组件的尺寸而且在需要时还要考虑组件的边界的尺寸。例如组件周围绘制一个象素宽的边界将左上角的坐标从(0,0)变成(1,1)而且将绘图区的宽度和高度各减小2个象素(每边一个象素)。下面的图片说明了这个:




要得到组件的宽度和高度可以使用它的getwidth和 getheight 方法。要得到边界的尺寸,使用getinsets方法。下面是一个组件决定自定义绘图区的可用宽度和高度的可能的代码:
public void paintcomponent(graphics g) {
...
insets insets = getinsets();
int currentwidth = getwidth() - insets.left - insets.right;
int currentheight = getheight() - insets.top - insets.bottom;
...
.../* 第一次绘图发生在(x,y), x不能小于
insets.left, 而y不能小于insets.height。 */...
}

为了让你自己熟悉坐标系统,你可以运行下面的applet。无论你在框架区域的任何地方点击都将绘制一个点而且下面的标签会显示该点的坐标。如果你点击边界,点就不是很清楚,因为组件的边界是在执行自定义绘图后被绘制的。如果你不想要这个效果,一个简单的解决方法是将组件的边界从它移动到一个新建的包含该组件的jpanel对象上。




这是applet的gui的一个截图。要运行这个applet,单击图片。applet将在一个新窗口中

这个程序在 coordinatesdemo.java中被实现。虽然这个范例代码没有在任何地方被讨论,但是它和 rectangledemo 程序的代码很相似,那个程序将在稍后的 绘制形状中讨论。
repaint方法的参数
记住调用组件的repaint 方法请求组件被预定去绘制自己。当绘图系统不能跟上repaint请求的要求时(译者注:主要是因为过于频繁的调用repaint,例如在paint方法中调用repaint(1),每隔一毫秒重绘一次),它会将多个请求合并为一个。
repaint方法有两个有用的形式:

void repaint()
请求整个组件被重绘。
void repaint(int, int, int, int)
请求组件中指定的区域被重绘。参数指定区域的左上角的x,y坐标和区域的宽度和高度。
虽然使用四参数形式的repaint方法通常没有实用价值,但是它可以显著的提高绘图性能。下面的图片显示的程序在频繁请求绘制以显示用户当前选定区域时使用四参数的repaint方法。这样做避免了绘制从上次的绘图操作后没有被改变的区域。



这是applet的gui的一个截图。要运行这个applet,单击图片。applet将在一个新窗口中

这个程序在 selectiondemo.java中实现。下面是计算绘制区域并绘制它的代码:
class selectionarea extends jlabel {
...
public selectionarea(imageicon image, ...) {
super(image); //使这个组件显示一个图像。
...
}

...//在一个鼠标拖动事件处理器中:
rectangle totalrepaint = recttodraw.union(previousrectdrawn);
repaint(totalrepaint.x, totalrepaint.y,
totalrepaint.width, totalrepaint.height);
...
public void paintcomponent(graphics g) {
super.paintcomponent(g); //绘制背景和图像
...
//在图像上绘制一个矩形。
g.setcolor(color.white);
g.drawrect(recttodraw.x, recttodraw.y,
recttodraw.width - 1, recttodraw.height - 1);
...
}
...
}

就像你看到的,这个自定义组件扩展jlabel,因此它继承了显示图像的能力。用户可以通过拖动鼠标选定一个矩形区域。组件连续的显示一个矩形以指出当前选定的尺寸。为了提高绘制速度,组件的鼠标拖动事件处理器为repaint指定一个绘制区域。
通过限制重绘的区域,事件处理器避免了不必要的重绘图像外的区域。对于这个小图像,这个策略没有显著的性能上的提高。但是对于一个巨大的图像,这可能就有真正的好处了。并且如果是用从文件里面绘制图像的情况代替,你还必须计算在矩形下面绘制什么--例如,在一个拖动程序中计算形状--然后使用绘制区域的知识限制你执行的计算可能会显著的提升性能。

指定给repaint方法的区域不光包括要被绘制的区域,还有所有需要擦除的区域。否则原来被绘制的东西保持可见直到另外的绘制操作碰巧擦除了它。前面的代码通过结合将被绘制的矩形和先前绘制的矩形来计算总的区域。

为repaint方法指定的绘制区域被反映到传递给paintcomponent方法的graphics对象中。你可以使用getclipbounds 方法决定哪个矩形区域被绘制。下面是一个使用剪切区域的例子:


public void paintcomponent(graphics g) {
rectangle cliprect = g.getclipbounds();
if (cliprect != null) {
//如果它有效,只绘制由cliprect指定的区域。
//最左上角坐标为 (cliprect.x, cliprect.y)
//宽度,高度为 cliprect.width, cliprect.height
} else {
//绘制整个组件
}
}

graphics对象
被传递到paintcomponent方法的 graphics 对象提供绘制环境和执行绘制的方法。那些方法将在稍后讨论,他们有诸如 drawimage, drawstring, drawrect和 fillrect这样的名字。
图形环境由诸如当前绘图色,当前字体和当前绘制区(就像你已经见过的)这样的状态组成,颜色和字体被初始化为在调用paintcomponent前的背景色和组件的字体。你可以使用getcolor和getfont方法得到它们,用setcolor 和 setfont方法设置它们。

如果你愿意,你可以安全的忽略当前的绘制区,这对组件的坐标系统没有任何影响,任何区域外的绘图被忽略。然而如果在绘图区域减小时你的绘图代码包括可以被简化的复杂的操作,那么你应该使用绘图区的知识帮助提高绘图的性能。就像前面的代码显示的那样,你通过调用getclipbounds方法从graphics 对象得到绘图区的矩形范围。

你可以使用两种方法减小绘图区。首先是在任何可能的情况下指定repaint 的参数。另一个就是实现paintcomponent,让它调用graphics 对象的setclip 方法。如果你使用setclip,确保在返回前恢复原始的绘图区。否则组件可能被不正确的绘制。下面是一个减小然后恢复绘图区的例子:

rectangle oldclipbounds = g.getclipbounds();
rectangle clipbounds = new rectangle(...);
g.setclip(clipbounds);

...//执行自定义绘制...

g.setclip(oldclipbounds);

在写你的绘图代码时记住你不能依赖除了提供的graphics对象外的任何图形环境。例如你不能依赖你对repaint指定的绘图区和随后调用的paintcomponent中的绘图区一样。一种情况是多个重绘请求可以被合并到一个paintcomponent 调用,对应的绘图区进行调整。另一种情况是绘图系统有时候自己会调用 paintcomponent方法,没有从你的程序中调用任何重绘请求。一个例子是绘图系统在第一次显示组件的gui时调用组件的paintcomponent 方法,同样当gui被其它的窗口覆盖而又出现时,绘图系统调用paintcomponent 方法绘制最近出现的部分。

swing绘图方法
paintcomponent是jcomponent对象用于绘制自身的三个绘图方法之一,三个方法的调用顺序是:
paintcomponent -- 绘图的主要方法。 缺省时,如果组件不透明它首先绘制背景,然后它执行其它自定义绘图操作。
paintborder -- 告诉组件的边界(如果有的话)进行绘制。 不用调用或者重写这个方法。
paintchildren -- 告诉这个组件包含的所有组件绘制它们自己。 不用调用或者重写这个方法。


--------------------------------------------------------------------------------
注意: 不要重写或者调用调用了paintxxx方法的方法:paint。虽然重写paint方法在先前的swing组件中是合法的,但是通常在一个从jcomponent派生的组件中这样做不是一件好事。除非你非常小心,重写paint 很容易搞乱绘图系统,绘图系统依赖jcomponent实现的 paint 方法进行正确的绘图、性能增强和诸如双缓冲这样的特性。
--------------------------------------------------------------------------------

标准的swing组件将它们的look-and-feel-specific绘制委托给一个称为ui delegate(ui代理)的对象。当这样一个组件的paintcomponent 方法被调用时,方法请求ui 代理绘制组件。通常,ui代理首先检查组件是否是不透明的,如果是,绘制组件的整个背景。然后ui代理执行任何look-and-feel-specific绘制。

我们推荐扩展jpanel而不是jcomponent的原因是jcomponent类目前没有设置一个ui代理 -- 只有它的子类设置了。这意味着如果你扩展jcomponent,你的组件在你自己不绘制的情况下不会被绘制。当你扩展jpanel并且在你的paintcomponent方法的开始调用super.paintcomponent方法时,那么面板的ui代理在组件不透明的情况下绘制组件的背景。

如果你需要关于绘图的更多信息,参看 awt和swing中的绘图。它是 swing connection中的一篇深入讨论绘图的复杂细节的论文。

扫描关注微信公众号