读书人

java学习人迹:深入java绘图机制

发布时间: 2013-10-14 12:54:46 作者: rapoo

java学习脚印:深入java绘图机制

java学习脚印:深入java绘图机制

写在前面

封装性越好的类在使用时,只要清楚接口即可,而不应该让程序员了解其内部结构;

对于平常的绘图来讲,java绘图机制无需了解太多,但是朦胧容易产生错误,绘图操作包括了整个GUI的显示问题,遂花了一些时间来学习、整理下,本文主要基于[1][2]以及众多资料整理以及自我理解和实践加工而成(如有错误,请纠正我)。这里所讲的很多技术可能已经过时了,你可以略过这些部分,但是像下面关键概念以及绘图指导意见部分还是应该掌握。


1.javaGUI的背景

java GUI开发库,经过最初的AWT,发展到Swing,SWT以及现在的Jface。这些GUI框架各有不同。

在AWT在JDK 1.0时引入,此时系统中只有重量级组件(heavyweight component)(见下文解释),这种组件采用"对等机制",对本地系统依赖性很大。 同时,采用这种方式时要处理诸如脏区脏检测,计算裁剪区和Z次序(damage detection, clip calculation, and z-ordering.
)。
在JDK 1.1 中轻量级组件(lightweight component)被引入,AWT需要在共享的java 代码中实现它们的绘制过程。对于轻量级和重量级组件,AWT处理略有不同。


在JDK 1.1 之后引入了Swing,Swing绘图机制和AWT类似并且依赖于AWT,但是Swing在绘图机制上也与AWT有差别。AWT采用"对等机制",调用本地操作系统的控件。Swing只为诸如窗口和框架之类的顶层组件调用操作系统控件。大部分组件都是使用纯Java代码来模拟的。Swing也引入了新的API来简化绘图工作。

Swing与AWT的依赖关系可以参见下图(来自[3]) :


java学习人迹:深入java绘图机制


java类库中Swing与AWT类间关系如下图所示:



java学习人迹:深入java绘图机制

2. 关键概念

以下介绍以下java GUI编程中基本的概念,它们需要你了解,如果你未曾熟悉的话。

2.1 顶层窗口与窗口包含关系树

在java GUI中,任何想要显示的组件必须存在于一个窗口包含阶层(containment hierarchy)中。这个概念类似于树(或者像文件目录系统一样的递归结构),每个组件类似于树中的中间节点和叶子节点,因此这里我们可以把它理解为包含关系树。这个树的树根必须是顶层窗口,在Swing中顶层窗口包括:JFrame, JDialog, 和 JApplet.

每一个组件只能被包含一次,如果之前已经加入到一个组件当中了,那么之后加到其他组件时,它就处在这个最近添加到的位置里面。

比如我们在JFrame上面放置了一个文本框和按钮,那么这个包含关系树如下图所示:


java学习人迹:深入java绘图机制

注意这里JFrame是顶层窗口,实际上每个顶层窗口都会默认的包含一个content pane来容纳其中的组件。

2.2 重量级与轻量级组件

java语言中,有两种组件,一种是重量级(heavyweight)组件,一种是轻量级(lightweight)组件。

1)重量级组件与它的本地屏幕资源相关联,通常称之为peer(java源码中很多时候,出现对peer的判断).来自于java.awt包里面的组件,像Button 、Label都是重量级组件。

一些重量级的第三方包,像

Java Binding for the OpenGL API、JDesktop Integration Components (JDIC) project、JRex, a browser component created in the Java language

也变得越来越流行起来(参考自[4])。

2)轻量级组件,没有它自身的屏幕资源,因此更"轻"。一个轻量级组件依赖于它在包含关系阶层中的祖先窗口的屏幕资源,例如JFrame的。

java.awt包中的Component 和Container子类以及所有的Swing组件都是轻量级的。

3)在 java.awt.Component库中,包含了一个用于判断组件到底是不是轻量级的方法:

//true if this component has a lightweight peer; false if it has a native peer or no peer

public boolean isLightweight() {
return getPeer() instanceof LightweightPeer;
}

注意,isLightweight()函数在组件不可显示时,是不能确定组件是不是轻量级的。

后面我们将会看到,绘图机制在轻量级与重量级组件处理上的不同。

2.3 EDT(Event Dispatch Thread) 绘图事件派发线程

1) 是谁在什么时候调用我们的事件处理方法?

我们在编写GUI中响应代码时,可能会这样写:

JButton btn = new JButton("Submit");
btn.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
// do something
}
});

那么到底是谁会调用这个方法呢?又是什么时候调用呢?

实际上在java 处理GUI任务时会生成一个唯一线程从事件队列事件中抽取事件,并转发给应用程序定义的事件处理器。这个单线程就是所谓的事件派发线程EDT,这个概念可参见下图(来自[3]):

java学习人迹:深入java绘图机制


2)为什么是一个单线程在执行GUI任务?

多线程编程虽然可以带来性能上的提升,充分利用系统空闲资源,提高用户响应性;但是维护多线程的安全性和可靠性,缺相当困难。不仅仅是java 的GUI框架采用单线程的,还有像Qt, NextStep, MacOS Cocoa, X Windows等等都是单线程化的(参考[5])。

由于大多数的Swing组件的方法并不是线程安全的,因此通过使用单线程来保证它的线程安全,因为多线程的GUI会尤其受死锁的影响,维护这些线程安全代价太大。

单线程的事件派发线程,让GUI任务像一个一个短任务在GUI线程中串行执行。

这里并不打算深入讨论线程安全问题,只给出我们在编写事件处理代码时的注意事项:

总是通过事件派发线程来处理我们的用户界面在事件派发线程中执行的任务,应该比较简单,如果过于复杂,那么事件队列中事件堆积将会造成界面无响应;过于复杂的任务应该在另外一个线程中执行,例如运用SwingWorker来产生一个后台线程。不要在事件处理代码中添加诸如Thread.sleep(), Object.wait(),Condition.await()这样的阻塞代码。

更多关于GUI并发的问题,可参见java官网教程。

2.4 系统触发的绘图与程序触发的绘图

在GUI程序中,窗口或者组件的重绘请求有两个来源,一个是系统触发的,另一个是应用程序自身触发的。

1)系统触发的重绘请求

组件第一次显示在屏幕上组件大小被调整了组件有坏点(damage)需要被修复,例如之前蒙在组件之上的东西被移走,组件之前被蒙住的部分现在显现出来了。

2)程序触发的重绘请求

在程序触发的重绘中,由于组件的内部状态改变了因而决定更新自己的内容,比如,一个按钮监听到鼠标按下了,它会调整为按下状态的视觉。

对于系统触发和程序触发的重绘,处理有一些差别,稍后将会见到。


3.AWT中绘图机制3.1 在哪里书写我们的绘图代码 ?

不管一个绘画请求是由系统触发的还是程序触发的,AWT中使用回调机制来处理绘画,并且这种机制对轻量级组件和重量级组件相同。这意味着我们需要把渲染组件的代码写在一个可覆盖可的特殊的方法中,那么绘图工具箱就会在绘制时触发这些操作代码。在AWT中这个要覆盖的方法是:

public void paint(Graphics g);

Graphics是图形上下文对象(类似于MFC里面的Device context,用来完成GDI绘制),用来完成具体的绘制。当AWT绘图方法被触发时,系统已经给这个对象,预置了很多属性,包括绘制组件的颜色、字体、坐标转换和适合组件重绘大小的裁剪区。

下面我们给出AWT中覆盖paint方法,绘制一个Karel机器人(MIT Karel 与java 公开课中的机器人形象),我给出简洁版本:

例1: Karel in AWT

对于paint方法书写要注意两点:

绘制组件的代码不能随意放置,一般都应该放在paint方法中;否则,你的代码在绘制时有可能没有可用的Graphics对象。不要直接调用paint方法,paint方法应该由AWT框架调用或者由程序自身通过repaint方法来调用;

repaint方法,用于异步请求重绘操作;这个方法有四个重载版本,如下:

public void repaint() //整个区域重绘
public void repaint(long tm) //指定重绘时间
public void repaint(int x, int y, int width, int height)
public void repaint(long tm, int x, int y, int width, int height)
分别对应不同情况.

注意: 我们在请求组件重绘时,一定要记住,我们尽量使用带参数版本来缩小重绘区域,减轻系统压力。

3.2 处理重量级组件绘制时,系统触发和程序触发的请求有差异

对于系统触发的请求:

由AWT框架是组件的部分还是整个需要重绘?AWT框架引起EDT线程触发组件上的paint()方法

程序触发的请求:

程序决定组件响应内部状态变化时是部分还是整个重绘? 程序触发repaint()方法来向AWT框架注册一个异步重绘请求? AWT框架引起EDT触发组件上的update()方法
如果组件没有覆盖update()方法,默认实现是擦出组件的背景,并简单的调用paint()方法。注意,如果不需要默认擦除背景可以覆盖update()方法。

对于大多数组件来说,默认情况下最终调用paint()方法,因此,我们也不用细加区分系统触发和程序触发的不同点,但是注意一点就是重量级组件可以利用update()方法实现增量作图(incremental painting),这是一项优点(轻量级组件不能实现增量作图)。增量作图,我通过实验,感觉就是不用擦除原来的背景,继续往组件上添加新内容,可以在一定程度上,减少闪烁。

3.3 保证所有轻量级子组件被绘制

虽然默认下,最终都是调用paint()方法来绘制组件;但是 AWT框架中轻量级组件的实现代码全部由java实现,因此与重量级组件还是有区别的。

轻量级组件的绘制依赖与包含关系阶层中的重量级祖先组件,当这个祖先组件被通知绘制时,它将把绘制请求转化为绘制自身上任何可见的子孙组件,这个方法是由java.awt.Container's paint() 方法来完成的,因此任何Container的子类,在覆盖paint方法时一定要记得调用super.paint()来保证(这是错误的一个来源),它上面的轻量级子孙组件都被绘制到了。
写代码时可以这样架构:

public class MyContainer extends Container {
public void paint(Graphics g) {
// 先绘制自身内容
// 然后确保轻量级子组件被绘制
super.paint(g);
}
}

3.3 轻量级组件处理系统触发的请求有两种方法一种是系统触发的请求,由本地系统发起,例如轻量级组件的重量级祖先组件第一次显示,将会调用paint()方法;一种是系统触发的请求,由轻量级框架发起,例如轻量级组件大小被调整,将会调用update(),这个方法默认调用paint()方法。

这意味着轻量级组件的update和paint没有什么区别,因此不能用于增量作图。

3.4 轻量级组件的透明特性

轻量级组件借用了重量级组件的屏幕资源,它们支持透明特性。因为,轻量级组件是从后面向前面绘制的,如果它们的某些像素没有绘制,那么这部分下面的组件就会“穿透”显示出来,这也是轻量级组件的update()方法默认没有清除背景的原因了。

轻量级组件的透明特性,可以参看例2: Moon in Nightsky ,来体会。这个例子中,月亮组件只绘制了一半的区域,后面的夜色背景透过月亮组件未绘制的区域显示出来,表现了轻量级组件的透明特性。


例2: Moon in Nightsky

3.4 AWT绘图指导意见

前面的这些分析,有些帮助,但是不够具体实用,还是看看这些指导意见。

对于大多数程序,图形绘制代码应该覆盖在组件的paint()方法里;
程序中应该使用repaint()来触发异步的paint(),而不是直接去调用paint()方法;
repaint()方法的调用要考虑效率问题,尽量使用带参数版本来缩小重绘区域; 重量级组件可以覆盖update()方法来做增量绘图,而轻量级组件不支持增量作图; java.awt.Container 的子类如果覆盖了paint()方法必须要加上super.paint()来保证所有轻量级子组件被绘制; 处理复杂绘图的组件,应该明智的使用裁剪矩形(clip rectangle ),来把绘图区域缩小到与裁剪区(clip area)相交的区域;
4.Swing绘图机制

Swing以AWT的基本绘图模型为基础,拓展了它从而让绘图的性能最好,并提高绘图灵活性。像AWT一样,Swing也采用paint回调机制和repaint来触发更新。

另外,Swing还增加了诸如,内置双缓冲作图,UI代理,以及用于自定义绘图机制的RepaintManager API .

4.1 Swing中绘图代码该放在哪里?

Swing中当组件要绘制时,依然需要回调机制,但是不是调用paint()方法,而是把paint()方法分解成三个方法:

protected void paintComponent(Graphics g)
protected void paintBorder(Graphics g)
protected void paintChildren(Graphics g)

这样的分解增加了灵活性,解决了诸如AWT中覆盖paint()方法漏掉super.paint()的风险。

Swing程序应该覆盖paintComponent()来实现图形绘制,而不是覆盖paint()方法;另外paintBorder和paintChidren方法通常也不应该覆盖。

这里我们给出Swing组件中绘制karel机器人的简洁代码,与上文AWT中代码作参照:

例3: Karel in Swing


注意,在代码中我们注释了paint()覆盖的代码,如果直接覆盖paint()方法,我们将看不到蓝色边框和标签子组件。

4.2 UI 代理如何实现组件风格和外观个性化

Swing提供了将组件绘制的过程委托给代理的方式来支持组件的外观和风格,使组件的外观和风格变得可灵活变换;这意味着组件绘制过程变成:

paint()触发paintComponent()方法;paintComponent()方法中判断ui是否为空,不为空 则执行ui.update();如果组件的不透明属性设置为真,ui.update()方法用组件的背景色填充组件并触发ui.paint()方法;ui.paint()方法渲染组件中的内容;

可以参考java源码(免责声明:代码归属Oracle及其附属组织所有,这里只是学习):

javax.swing.JComponent.paintComponent

为了帮助理解双缓冲,这里还是再做一个实验(代码部分参卡自[7],[8]),如例4 : Double-buffer in AWT 所示:

例4 : Double-buffer in AWT

测试时,移动鼠标,观察屏幕闪烁即可得出结论。

在Swing中,默认所有组件都具有双缓冲功能,所以代码就像书写这个没有使用屏幕外绘图对象时的代码一样简洁。可以通过javax.swing.JComponent:类的
public boolean isDoubleBuffered()
public void setDoubleBuffered(boolean o)

方法来设置和获取当前组件是否使用双缓冲。注意,如果祖先组件设置为使用双缓冲则它里面的组件将都会使用双缓冲,而忽略子组件的设置。

4.4 Swing中的新特性

Swing为了提高绘图性能,引入了不透明度(Opacity),优化作图选项(OptimizedDrawingEnabled)以及重绘管理器(RepaintManager)等特性。

4.4.1 不透明度

javax.swing.JComponent提供了
public boolean isOpaque()

public void setOpaque(boolean o)来设置不透明度;不透明度指示组件是否保证在它的矩形边界内的像素都会绘制。当这个属性为true时,将会绘制组件边界内全部像素,否则则不保证全部绘制。这个值默认是由外观UI代理设定的,对于大多数组件为true.

关于这个属性注意三点:

组件在设计时要注意,通常组件该属性默认为真,但是组件却不绘制边界内所有区域,这些区域偶尔导致屏幕垃圾;组件透明性特性已经广泛应用,因此应该注意与绘图系统的协定;不透明性值设置为false,并不与组件透明对等;有些组件设置它为false是为了视觉或者为了改变形状,这种时候组件背景一部分还是填充了。不透明属性用于透明度的特性时应该用文档加以记录。如果组件通过isBorderOpaque()判断边框不透明时返回false,那么意味着组件背后的组件可以透过边框显示出来,此时组件应该把自己定义为透明的以便让边框没有被绘制。4.4.2 优化作图选项

这个选项用来解决组件重叠的棘手问题。如果组件被同层次的组件或者与祖先组件不相干的组件重叠的话,绘制一个组件需要很多的遍历来确保组件被正确的绘制。

为了减少这种遍历,Swing为组件添加了这个只读属性,可以通过 javax.swing.JComponent的
public boolean isOptimizedDrawingEnabled() 来判断是否可以优化作图。

该方法返回true表明中间组件没有重叠,否则组件不能确保中间组件是否重叠。

通过检查这个属性,Swing框架可以缩小在绘图时需要搜索重叠组件的范围。

这个属性是只读的,子类更改它的唯一方法是覆盖这个方法并返回想要的值.所有标准Swing组件返回true,除了JLayeredPane, JDesktopPane,和JViewPort.

4.4.3 重绘管理器

重绘管理器的提出是为了提升Swing包含关系阶层的绘图性能和实现Swing的验证机制(revalidation mechanism )。它通过截获所有Swing组件的重绘请求(这些请求将不再由AWT处理)和维护那些指示哪里需要更新的自身状态(俗称为"脏区""dirty regions")来实现重绘机制,最后通过 invokeLater() 在EDT上来处理截获的请求。

用户可以查看、替换、实现自己的重绘管理器。

重绘管理器对双缓冲机制有着全局控制,使用

public void setDoubleBufferingEnabled(boolean aFlag)
public boolean isDoubleBufferingEnabled()

来设置启用或者关闭双缓冲,这个属性默认为true。如果不想默认使用双缓冲的话可以通过:

RepaintManager.currentManager(mycomponent).
setDoubleBufferingEnabled(false);来关闭双缓冲,

注意mycomponent时哪个具体组件不重要。


RepaintManager在实践中暂时还未用上,关于重绘管理器更高级的话题这里就不做讨论。

4.5 Swing绘图指导意见

Swing绘图处理相对AWT更加灵活,性能更好,但是使用起来注意事项也更多,要获得更好总是要付出代价的,我们来看:

对于Swing组件来说,不管是系统还是程序触发的绘图请求,paint()总是被调用的,update永远没有调用;程序可以通过repaint来触发一个异步的paint(),但是不要直接调用paint();在输出复杂图形的组件上,应该使用带参数的repaint()来调用,这个参数知名哪些区域需要更新,而不是不带参数版本的一股脑全部更新;Swing默认的paint方法被分解为独立的回调方法paintComponent()、paintBorder()和paintChildren();拓展Swing组件的用户代码应该在覆盖paintComponent()的方法体中书写,而不是在paint()中;
Swing引入了不透明度和优化绘图选项来提高绘图性能;其中不透明属性指定组件是部分还是全部绘制,优化绘图选项(optimizedDrawingEnabled)用来确定这个组件的子组件是否重叠;将一个组件的优化绘图选项设置为假,会导致每一次绘图操作都需要更多处理;因此我们希望你明智地使用透明和组件重叠;从包含UI代理的Swing组件拓展时,注意在覆盖paintComponent()方法时,调用super.paintComponent()来保证不透明组件的背景被清除了。Swing支持内置的双缓冲机制,可以通过JComponent的doubleBuffered属性来设置,默认时对于所有swing组件该属性改为真;注意,在容器组件中设置它的值为真时,将会影响到它所包含的所有轻量级子孙组件,让它们全都具有双缓冲性质,忽略这些子孙组件它们自身的doubleBuffered属性值;
强烈建议为所有Swing组件开启双缓冲特性; 处理复杂绘图的组件,应该明智的使用裁剪矩形(clip rectangle ),来把绘图区域缩小到与裁剪区(clip area)相交的区域;


5.参考资料

[1]: http://www.oracle.com Painting in AWT and Swing

[2] csdn jxsfreedom的专栏 http://www.realapplets.com Chapter 4 - Advanced Topics1. Double-Buffering.


6.最后的建议

GUI绘图,java 2D绘图等问题,有些方面是比较复杂且浪费精力的,建议除了从事图形界面编程、视觉处理的专业外,对绘图机制有个大致了解能编写简单应用即可,不要花费过多精力研究这方面的技术,遇到问题需要时再去翻字典,因为还有更重要的技术在等待我们去学习。


读书人网 >编程

热点推荐