荔园在线

荔园之美,在春之萌芽,在夏之绽放,在秋之收获,在冬之沉淀

[回到开始] [上一篇][下一篇]


发信人: jjksam (Linux,c/c++,java), 信区: Java
标  题: [转载] 【好文推荐】AWT 和 Swing 的绘制(转寄)
发信站: 荔园晨风BBS站 (Thu Nov 29 11:53:34 2001), 转信

【 以下文字转载自 jjksam 的信箱 】
【 原文由 jjksam@smth.org 所发表 】
发信人: SuperMMX (笑天子*不再喝可乐), 信区: Java
标  题: 【好文推荐】AWT 和 Swing 的绘制
发信站: BBS 水木清华站 (Thu Nov 29 11:24:14 2001)

代码和英文版可以在 http://SuperMMX.dhs.org/forum 中找到

From java.sun.com

         [b]AWT 和 Swing 中的绘制[/b]

好的重绘代码是应用程序性能的关键

By Amy Fowler

翻译 by SuperMMX

在一个图形系统中, 一个窗口工具包通常提供了一个框架, 这样对于一个图形用
户接口(GUI), 很容易在正确的时候在屏幕上绘制正确的内容.

AWT (Abstract Windowing Toolkit) 和 Swing 就提供了这样一个框架. 但是一
些开发者不能很好地理解它们实现的 API -- 一个导致程序员不能做得跟他们本
来能做得那样好的问题.

这篇文章详细解释了 AWT 和 Swing 的绘制机制. 目的是帮助开发者写出正确和有效
的 GUI 绘制代码. 虽然这篇文章包含了一般的绘制机制(什么时候在哪里绘制), 它
没有告诉你怎么使用 Swing 的 API 来画出正确的结果. 学习怎样画出好的图形, 请
访问 Java 2D 网站. (http://java.sun.com/products/java-media/2D/index.html)

这篇文章包含的话题有:

* 绘制系统的发展
* AWT 中的绘制
**  系统触发的绘制 vs. 应用程序触发的绘制
**  Paint 方法
**  绘制和轻量级控件
**  "智能"绘制
**  AWT 的绘制指南
* Swing 中的绘制
**  双缓冲支持
**  附加的绘制属性
**  Paint 方法
**  绘制过程
**  重绘管理器 RepaintManager
**  Swing 绘制指南
* 概要

[b][u]Swing 绘制系统的发展[/u][/b]

当最初的 AWT API 为 JDK 1.0 开发的时候, 只存在重量级的控件("重量级" 意
思是控件有它自己的不透明的本地窗口). 这就使 AWT 很大程度上依赖于每一个
本地平台的绘制子系统. 这个方案管理所有的细节, 象破坏检测, 裁剪计算以及
Z-次序. 在 JDK 1.1 中轻量级控件(一个"轻量级"的控件就是重用了它最近的重
量级祖先的本地窗口的控件)的出现, AWT 需要用 Java 的共享代码实现轻量级
控件的绘制, 接下来, 绘制在重量级和轻量级控件的不同就有了.

JDK 1.1 之后, 当 Swing 工具包发布时, 它引入了它自己的控件绘制机制. 大
的方面来说, Swing 的绘制机制类似于并且基于 AWT的. 但是它也引入了一些机
制上的不同, 以及使应用程序很容易定制绘制工作的新 API.

-----------------------------------------------------------------------

[b][u]AWT 中的绘制[/u][/b]

要理解 AWT 的绘制 API 是怎么工作的, 需要知道在一个窗口环境中什么触发了
一个绘制操作. 在 AWT 中, 有两种类型的绘制操作: 系统触发的绘制, 应用程
序触发的绘制.

系统触发的绘制

在一个系统触发的绘制操作中, 系统要求一个控件绘制它的内容, 通常是由于以
下几个原因:

1, 控件是第一次在屏幕上可见
2, 控件改变大小
3, 控件被破坏了, 需要修复. (例如, 原来遮住这个控件的一些东西移动了, 这
个控件原来被遮住的部分现在可见了)

应用程序触发的绘制

在一个应用程序触发的绘制操作中, 控件决定需要更新它的内容, 因为它的内部
状态改变了. (例如, 一个按钮检测到一个鼠标键按下了, 就决定它需要画一个"
按下" 的按钮形式)

[u]Paint 方法[/u]

不管一个绘制的请求是怎样触发的, AWT 使用一种"回调"机制来绘制, 这个机制
对于重量级和轻量级控件都是一样的. 这就是说一个程序需要在一个特殊的重写
(override) 的函数中代替控件的绘制代码, 工具包就会在需要绘制的时候调用
这个方法. 这个方法就是java.awt.Component 的 paint 方法:

public void paint(Graphics g)

当 AWT 调用这个方法的时候, 为了在这个特殊的控件上画, 参数 Graphics 对
象已经预先配置了:

1, Graphics 对象的颜色设置为这个控件的 foreground 属性.
2, Graphics 对象的字体设置为这个控件的 font 属性.
3, Graphics 对象的坐标映射设置为坐标 (0, 0) 代表控件的左上角.
4, Graphics 对象的裁剪矩形设置为控件所需要重绘的区域.

程序必须使用这个 Graphics 对象(或者从它继承的)来绘制. 在需要的时候可以
随便改变Graphics 对象的状态.

这里有一个 paint 的回调函数的例子, 在一个控件的范围内画一个实心圆:

[code]
public void paint(Graphics g)
{
        // Dynamically calculate size information
        Dimension size = getSize();
        // diameter
        int d = Math.min(size.width, size.height);
        int x = (size.width - d)/2;
        int y = (size.height - d)/2;

        // draw circle (color already set to foreground)
        g.fillOval(x, y, d, d);
        g.setColor(Color.black);
        g.drawOval(x, y, d, d);
 }
[/code]

刚接触 AWT 的开发者应该看看 PaintDemo 例子, 提供了一个可以运行的程序,
演示了怎样在一个 AWT 程序中使用 paint 回调函数.

一般来说, 程序应该避免把绘制代码放在 paint 回调函数可以调用以外的其他
地方. 为什么? 因为这样的代码可能在它不应该绘制的时候调用 -- 例如, 在控
件可见之前或者访问一个有效的 Graphics 对象. 不推荐程序直接调用
paint().

要使程序触发的绘制可用, AWT 提供了一下的 java.awt.Component 方法来允许
程序异步地请求一个绘制操作:
[code]
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)
[/code]
接下来的代码提供了一个例子, 演示了一个鼠标监听器, 在鼠标按下或者释放的
时候使用 repaint() 来触发一个理论上的按钮的更新.

[code]
MouseListener l = new MouseAdapter() {
            public void mousePressed(MouseEvent e) {
                MyButton b = (MyButton)e.getSource();
                b.setSelected(true);
                b.repaint(); }

            public void mouseReleased(MouseEvent e) {
                MyButton b = (MyButton)e.getSource();
                b.setSelected(false);
                b.repaint(); }
        };
[/code]
绘制复杂结果的控件应该调用带有定义了需要更新区域的参数的 repaint() 方
法.  一个通常的错误就是总是调用没有参数的 repaint() 方法, 就导致整个控
件的重绘.  经常导致不必要的绘制过程.

paint() vs. update()

为什么我们需要区别"系统触发的绘制"和"程序触发的绘制"呢?  因为 AWT 在处理
重量级控件的时候, 在两种情况有一点不一样(轻量级控件的情况我们迟些讨论),
不幸的是, 这是巨大混淆的一个原因.

对于重量级控件, 这两种类型的绘制使用两种不同的方法, 依赖于一个绘制操作
是系统触发的还是程序触发的.

系统触发的绘制

这是一个系统触发的绘制操作怎么发生的:

1, AWT 决定是控件部分还是全部需要绘制.
2, AWT 引起事件分配线程在这个控件上调用 paint().

应用程序触发的绘制

一个应用程序触发的绘制这样发生:

1, 程序根据控件内部的状态改变决定控件是部分还是全部重绘.

2, 程序在这个控件上调用 repaint(), 就向 AWT 注册了一个异步的请求, 这个
空间需要重绘.

3, AWT 引起事件分配线程调用控件的 update().

注意: 如果在最初的重绘请求处理之前, 这个控件有多个 repaint() 的调用,
多个请求可能合并为一个 update() 的调用. 决定什么时候请求应该合并的算法
是和实现相关的. 如果多个请求合并了, 结果更新的矩形等于包含在合并了的请
求的矩形的并集.

4, 如果控件没有重写 update(), update() 缺省的实现是清除控件的背景(如果
它不是一个轻量级控件), 接着只简单调用 paint().

既然在缺省情况下, 最后的结果是一样的(调用了 paint()), 许多人完全不明白
有一个分离的 update() 的目的. 虽然缺省的实现是调用 paint(), 这个更新的
"钩子"使一个程序能够在需要的时候不一样地处理应用程序触发的绘制. 一个程
序必须假设, 一个 paint() 的调用暗示了 grphic 定义的裁剪矩形被破坏了,
必须完全重绘, 但是一个 update() 的调用没有这个意思, 只是使程序做一些增
量的绘制.

增量绘制在一个程序想要在一个控件已经存在的部分之上再画另外一些东西的时
候很有用. UpdateDemo 例子演示了一个使用 update() 做增量绘制的程序.

事实上, GUI 控件的绝大部分不需要做增量绘制, 所以大多数程序可以忽略
update() 方法, 只简单地重写 paint() 来在一个控件当前的基础上绘制. 这是
说对于大多数控件的实现来说系统触发的绘制和应用程序触发的绘制最终是等同
的.

[u]绘制和轻量级控件[/u]

从一个应用程序开发人员的角度来看, 轻量级控件的绘制 API 跟重量级控件的
一样(也就是说, 你只重写 paint(), 调用 repaint() 来触发更新). 但是, 因
为 AWT 的轻量级控件框架完全用通常的 Java 代码来写的, 所以在轻量级的实现
机制中有一些细微的不同.

轻量级控件是怎样绘制的

一个轻量级控件的存在, 需要在它的包含结构的某个地方有一个重量级控件, 才
有一个地方来绘制. 当这个重量级的祖先被通知需要绘制它的窗口, 它必须把
paint 的调用转换为它所有轻量级的子孙的 paint 调用. java.awt.Container
的 paint() 方法来处理这些, 对它所有可见的, 轻量级的子孙调用 paint() 来
绘制它们和需要更新的矩形的交集.
[code]
public class MyContainer extends Container {
        public void paint(Graphics g) {
            // paint my contents first...
            // then, make sure lightweight children paint
            super.paint(g);
    }
}
[/code]
如果没有 super.paint() 的调用, 那么容器的轻量级后代不会显示(当 JDK 1.1
首次引入轻量级时的一个常见的问题).

缺省的 Container.update() 实现没有在它的轻量级后代上递归调用 update()
或者 paint() 是没有价值的. 这意味着任何使用 update() 来做增量绘制的重
量级容器的子类必须确信轻量级后代在需要的时候递归地被重绘. 幸运的是,
很少的重量级容器需要增量绘制, 所以这个问题不会影响大多数的程序.

轻量级控件和系统触发的绘制

为轻量级控件实现窗口行为(显示, 隐藏, 移动, 改变大小等等)的轻量级框架代
码是完全用 Java 写的. 通常, 在这些功能的 Java 实现中, AWT 必须明确地告
诉各种轻量级控件来绘制(尤其是系统触发的绘制, 甚至它不再是来源于本地系
统). 但是, 轻量级框架使用 repaint() 告诉控件需要绘制, 我们前面已经解释
了结果是调用了 update(), 而不是直接调用 paint(). 所以, 对于轻量级控件,
系统触发的绘制遵循下面两种途径:


1, 系统触发的绘制请求来自于本地系统(i.e. 轻量级控件的重量级祖先首先
显示出来), 结果是一个 paint() 的直接调用.

2, 系统触发的绘制请求来自于轻量级框架(i.e. 轻量级控件改变了大小), 结果是
调用了 update(), 缺省情况下是调用 paint().

在一个小容器中, 这意味着对于轻量级控件在 update() 和 paint() 之间没有
真正的区别, 进一步说明对于轻量级控件增量绘制技术不应该使用.

轻量级控件和透明性

因为轻量级控件从一个重量级祖先"借"了真正的屏幕资源, 它们支持透明的特性.
这个会起作用, 因为轻量级控件上从后往前绘制的, 所以如果一个轻量级控件留
下一些或者全部的部分没有绘制, 那么它下面的控件就"显示"出来了. 这也是
update() 的缺省实现中如果这个控件是轻量级的话就不清除背景的原因.

LightweightDemo 例子程序演示了轻量级控件的透明特性.


[u]智能绘制[/u]

虽然 AWT 试图使绘制控件的过程尽可能的有效, 一个控件的 paint() 实现它自
己就有可能给性能带来冲击. 两个影响这个过程的关键地方是:

1, 使用裁剪区域来缩小需要绘制的范围.
2, 使用内部的布局支持来缩小子女们要绘制的范围(只适用于轻量级控件)

如果你的控件很简单 -- 例如, 它是一个按钮 -- 为了只画出和裁剪矩形相交的
部分, 它不需要花费很大的工夫; 只画出整个控件, 让 graphics 正确裁剪可能
更合适一点. 但是, 如果你创建了一个画出复杂结果的控件, 象一个文本控件,
你的代码使用裁剪信息来缩小需要绘制的数量就非常重要了.

进一步, 如果你在写一个复杂的轻量级容器, 包含了许多控件, 这个控件或者它
的布局管理器有关于布局的信息, 它就需要布局的知识来更智能地确定哪个孩子
需要绘制. Container.paint() 的缺省实现只按顺序浏览所有的孩子, 检查可见
性和交集 -- 这个操作对于一些布局来可能是一个不必要的不是有效的操作. 例
如, 一个容器把控件放在一个 100x100 的格子中, 格子的信息可以用来更快地
确定那些 10,000 个控件中哪些和裁剪矩形的交集以及确实需要绘制.

[u]AWT 绘制指南[/u]

AWT 提供了简单回调 API 来绘制控件. 当你使用它时, 可以参考一下指南:

1, 大多数程序来说, 所有的客户端绘制代码应该在控件的 paint() 方法的范围
之内.

2, 程序可以通过调用 repaint() 来触发一个后来的 paint() 调用, 不应该直
接调用paint().

3, 对于带有复杂输出的控件, repaint() 的调用应该是带有定义了需要更新矩
形的参数的版本, 而不是没有参数的, 否则会导致这个控件重绘.

4, 因为 repaint() 的调用首先结果是一个 update() 的调用, 缺省传递给
paint(), 重量级控件可能在需要的时候重写 update() 来做增量绘制(轻量级控
件不支持增量绘制).

5, java.awt.Container 的扩展重写了 paint(), 应该调用 super.paint() 确
信子女重绘.

6, 画出复杂结果的控件应该智能地使用裁剪矩形来缩小需要绘制的区域.

--------------------------------------------------------------------

[b][u]Swing 中的绘制[/u][/b]

Swing 是基于 AWT 的基本绘制模型的, 做了更大的扩展来提高性能和扩展
性. 象 AWT 一样, Swing 支持 paint 回调函数, 使用 repaint() 来触发更新,
还有, Swing 提供了双缓冲的内在支持以及为了支持 Swing 的附加结构(象边界
和 UI 代表)所做的改变. 最后, Swing 为那些想要自定义绘制机制的程序提供
了 RepaintManager API.

[u]双缓冲支持[/u]

Swing 的一个最有名的特性就是它把双缓冲加入了工具包. 它通过在
javax.swing.JComponent 中加一个 "doubleBuffered" 属性来做到:

public boolean isDoubleBuffered()
    public void setDoubleBuffered(boolean o)

Swing 的双缓冲机制对于每个容器结构(通常是每个最顶层的窗口)使用了一个后
台的缓冲区, 这个窗口中双缓冲是可用的. 虽然这个属性对于每个控件可以设置,
在一个特定的容器中设置的结果是使这个容器下面的所有轻量级控件都在后台的
缓冲区中绘制, 而不管它们各自的 "doubleBuffered" 属性的值.

缺省情况下, 所有 Swing 控件的这个属性设置为 true. 但是真正起作用的是在
JRootPane 中, 因为这个设置使所有在最高层 Swing 控件下面的控件的双缓冲
可用. 大部分情况来说, Swing 程序不需要做任何特殊的事来处理双缓冲, 除了
需要决定它是否应该打开或者关闭(要得到平滑的 GUI 效果, 你会想让它打开的!)
Swing 确信合适的 Graphics(双缓冲的后台 image Graphics, 其他就是通常的
Graphics) 传递给控件的 paint 回调函数, 这样控件所需要做的就是用它来画.
这种机制在这篇文章的后面来详细解释, 在绘制过程小节中.

[u]附加的绘制属性[/u]

Swing 给 JComponent 引入了一些附加的属性, 来提高内部绘制算法的效率. 这
些属性用来处理下面两方面内容, 这两方面导致绘制轻量级控件是一个代价高的
操作:


1, 透明性: 如果绘制一个轻量级控件, 可能它是半透明或者全透明的, 就不需
要绘制它所有的东西; 这是说不管它什么时候重绘, 它下面的东西必须先重绘.
这需要系统来遍历容器结构来找到第一个下面的重量级祖先, 然后再从后往前绘
制.

2, 重叠的控件: 如果绘制了一个轻量级控件, 可能一些其他的轻量级控件覆盖
了它; 这是所不管什么时候绘制原来的轻量级控件, 任何覆盖了这个控件(裁剪
矩形和覆盖区域的交)的其他轻量级控件也需要部分重绘. 这需要系统横贯容器
结构, 在每个绘制操作中检查覆盖的控件.

不透明性

在不透明控件的例子中, 要提高性能, Swing 给 javax.swing.JComponent 加了
一个可读写的 opaque 属性:

public boolean isOpaque()
public void setOpaque(boolean o)


设置是:

1, true: 控件同意绘制它的矩形范围内所有的东西.
2, false: 控件不保证绘制它的矩形范围内所有的东西

opaque 属性允许 Swing 的绘制系统检测是否一个特定控件的重绘请求要求它下
面的祖先附加的重绘. 每个标准的 Swing 控件的 opaque 属性的缺省值设置为
当前的 look and feel UI 对象. 大多数控件是 true.

控件实现的一个最常见的错误就是它们允许 opaque 属性缺省是 true, 但是它
们没有完全地绘制它们边界内的区域, 结果是在未绘制的区域内有偶然性的屏幕
垃圾. 当设计一个控件的时候, 应该仔细考虑 opaque 属性的处理, 要确信明智
地使用透明性, 因为它耗费更多的绘制时间, 还有注意绘制系统.

opaque 属性的意思常常被误解. 有时候被理解为 "使控件的背景透明". 但是,
这不是 Swing 关于不透明性的严格的解释. 一些控件, 象一个按钮, 可以设置
opaque 属性为 false 来给控件一个非矩形的形状, 或者在控件周围留下短暂的
视觉效应, 象一个焦点标志. 在这些情况下, 控件不是不透明的, 但是它背景的
大部分仍然填充了.

就象前面所定义的, opaque 属性主要是和重绘系统的一个协议. 如果一个控件
也使用了 opaque 属性来定义透明性怎样应用到一个控件的视觉效应, 那么属性
的这种使用方法应该写入文档. (对一些控件来说使用另外的一些属性来控制透
明性怎么应用到视觉方面可能更好一些. 例如, javax.swing.AbstractButton
提供了 ContentAreaFilled 属性就是这个目的).

另一个没有意义的事情就是不透明性和一个 Swing 控件的边界怎样联系的. 一
个 Board 对象给一个控件所画的区域仍然认为是这个控件几何上的一部分. 这
就是说如果一个控件是不透明的, 它仍然要负责填充边界所占的区域. (边界只
是在不透明控件的基础上绘制).

如果你想让一个控件允许它下面的控件通过它的边界区域显示出来 -- 也就是,
如果边界通过 isBorderOpaque() 返回 false 支持透明性 -- 接着控件定义自
己为非-不透明的, 然后它留下边界区域没有绘制.

"优化的"绘制

覆盖的控件部分更有技巧. 甚至如果没有一个这个控件直接的兄弟覆盖了它, 它
也常常可能一个非祖先的亲戚(象一个"表兄妹" 或者 "阿姨")覆盖了它. 在这种
情况下在一个复杂结构中单独绘制一个控件可能需要许多遍历来确信正确的绘制.
要减少不必要的遍历, Swing 给 javax.swing.JComponent 加了一个只读的
isOptimizedDrawingEnabled 属性:
[code]
public boolean isOptimizedDrawingEnabled()
[/code]
设置是:

1, true: 控件指明它的直接子女没有一个覆盖.

2, false: 控件不保证它的子女没有覆盖.

通过检查 isOptimizedDrawingEnabled 属性, Swing 能够在重绘时候快速地缩
小它的搜索覆盖控件范围.

因为 isOptimizedDrawingEnabled 属性是只读的, 所以控件可以修改它的缺省
值的唯一方法就是继承它, 重写这个方法, 返回需要的值. 所有标准的 Swing
控件对于这个属性返回 true, 除了 JLayeredPane, JDesktopPane 和
JViewPort.

[u]Paint 方法[/u]

AWT 轻量级控件适用的规则对于 Swing 控件也使用 -- 例如, paint() 在需要
绘制的时候调用 -- 除了 Swing 把 paint() 调用分为三个分开的方法, 按照以
下顺序调用:

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

Swing 程序应该重写 paintComponent() 而不是重写 paint(). 虽然 API 允许
这样做, 通常没有理由要重写 paintBorder() 或者 paintComponents() (如果
你做了, 确信你知道自己在做什么). 这个代理使程序简单地重写绘制过程中它
们需要扩展的部分. 例如, 这个解决了前面提到的 AWT 的问题, 没有调用
super.paint() 阻止了轻量级子女的显示.

SwingPaintDemo 例子程序演示了 Swing 的 paintComponent() 回调函数的简单
使用.

绘制和 UI 代表

大多数标准的 Swing 控件有它们的 look and feel, 通过分离的
look-and-feel(叫做 "UI 代表")实现, 是 Swing 的可拆卸的 look and feel
特性. 这就是说标准控件的大多数或者全部绘制都通过 UI 代理来做, 通过以下
方法来做:

1, paint() 调用 paintComponent().

2, 如果 ui 属性是非 null, paintComponent() 调用 ui.update().

3, 如果控件的 opaque 属性是 true, ui.update() 用背景色填充控件的背景,
然后调用 ui.paint().

4, ui.paint() 绘制出控件的内容.

这就是说带有 UI 代理的 Swing 控件的子类(vs. JComponent 的直接子类), 应
该在重写的 paintComponent() 中调用 super.paintComponent().
[code]
public class MyPanel extends JPanel {
        protected void paintComponent(Graphics g) {
            // Let UI delegate paint first
            // (including background filling, if I'm opaque)
            super.paintComponent(g);
// paint my contents next....
        }
    }
[/code]
如果由于一些原因控件扩展不想允许 UI 代理来绘制(举个例子, 它要完全代替
控件的视觉效果), 它可能跳过调用 super.paintComponent(), 但是如果
opaque 属性是 true, 它必须负责填充它自己的背景, 就象在关于 opaque 属性
的小节中讨论的一样.

[u]绘制过程[/u]

Swing 重绘请求过程和 AWT 有点不一样. 虽然对于应用程序员来说是一样的 --
调用了 paint(). Swing 通过它的 RepaintManager API (在后面讨论)做到这些.
提高了绘制的性能. 在 Swing 中, 绘制有下面描述的两种方法:

(A) 绘制请求来自于第一个重量级祖先(通常是 JFrame, JDialog, JWindow, 或
者 JApplet):

1, 事件分配线程在这个祖先上调用 paint().

2, Container.paint() 的缺省实现递归地调用任何轻量级子孙的 paint().

3, 当到达第一个 Swing 控件时, JComponent.paint() 的缺省实现做这些:

3.1 如果控件的 doubleBuffered 属性是 true, 并且控件的 RepaintManager
中双缓冲可用, 就要把 Graphics 对象转换为一个合适的后台 graphics.

3.2 调用 paintComponent() (如果是双缓冲就传递一个后台的 graphics)

3.3 调用 paintBorder() (如果是双缓冲就传递一个后台的 graphics)

3.4 调用 paintChildren() (如果是双缓冲就传递一个后台的 graphics), 使用
了 clip 和 opaque , optimizedDrawingEnabled 属性来精确确定哪些子孙需要
递归调用paint().

3.5 如果控件的 doubleBuffered 属性是 true, 并且控件的 RepaintManager
中双缓冲可用, 就把后台的图形拷贝到使用原来的前台的 Graphics 对象.

注意: JComponent().paint() 步骤 3.1 和 3.5 在递归调用 paint() 时跳过了,
(从 paintChildren(), 在步骤 3.4 中描述), 因为在一个 Swing 窗口结构中所
有的轻量级控件共享同一个后台图像来做双缓冲.

(B) 绘制请求来自于 javax.swing.JComponent 的 repaint() 的调用:

1, JComponent.repaint() 在控件的 RepaintManager 中注册了一个异步的重绘
请求, RepaintManager 调用 invokeLater() 来把一个 Runnable 排列在事件分
配线程队列中来处理请求.

2, runnable 在事件分配线程中运行, 引起控件的 RepaintManager 在这个控件
上调用 paintImmediately(), 做如下一些工作:

2.1 使用裁剪矩形和 opaque 和 optimizedDrawingEnabled 属性来确定绘制开
始的"根"控件, (要处理透明性和潜在的覆盖控件).

2.2 如果根控件的 doubleBuffered 属性是 true, 并且根控件的
RepaintManager 中双缓冲可用, 就要把 Graphics 对象转换为一个合适的后台
graphics.

2.3 在根控件上调用 paint() (执行 (A) 中 JComponent.paint() 步骤 2-4),
引起根下所有和裁剪矩形相交的东西重绘.

2.4 如果根控件的 doubleBuffered 属性是 true, 并且根控件的
RepaintManager 中双缓冲可用, 就把后台的图形拷贝到使用原来的前台的
Graphics 对象.

注意: 如果在重绘请求处理之前, 多个 repaint() 的调用发生在一个控件或者
它的 Swing 祖先的任何一个, 那些多个请求可能合并为一个单独的 repaint()
方法所被调用的最高层 Swing 控件的 paintImmediately() 调用. 例如, 如果
一个 JTabbedPane 包含一个 JTable, 在任何未诀的重绘请求之前两个都调用了
repaint(), 结果会是一个 JTabbedPane 单独的 paintImmediately() 调用, 是
因为 JTabbedPane 导致了在两个控件上运行 paint().

这就是说, 对于 Swing 控件, update() 永远不会被调用.

虽然 repaint() 的结果是 paintImmediately() 的调用, 它没有被认为是绘制
的回调函数, 客户端的绘制代码也不应该放在 paintImmediately() 中. 实际上,
完全没有理由重写 paintImmediately().

同步绘制

在前一个小节中描述的, paintImmediately() 就象是一个入口告诉一个 Swing
的控件来绘制它自己, 确信所有需要的绘制正确发生. 这个方法也可能用于做同
步绘制请求. 就象它的名字所暗示的那样, 有时候控件需要实时地根据它的内部
状态来改变它的外观. (e.g. 就象一个 JScrollPane 做滚动操作的时候).

程序不应该直接调用这个方法, 除非有实时绘制的需要. 这是因为异步的
repaint() 会使多个重叠的请求有效地合并, 而 paintImmediately() 的直接调
用不会. 另外, 调用这个方法的规则是它必须从事件分配线程中调用; 它不是为
你的多线程绘制代码所设计的! 要看到 Swing 的单线程模式的细节, 请参见收集
的文章 "线程和 Swing".

[u]RepaintManager[/u]

Swing 的 RepaintManager 类的目的是使一个 Swing 容器结构的重绘过程最有
效, 也为了实现 Swing 的 "重确认" 的机制(后者可以是另外一篇文章的主题了).
它通过解释 Swing 控件中所有的重绘请求(所以它们不再被 AWT 处理)实现了重
绘机制, 也保存了它自己的关于是否需要更新(叫做"脏的区域")的状态. 最后,
它在事件分配线程中使用 invokeLater() 来处理未决的请求, 就象在关于"重绘
过程"小节(B 项)中提到的.

对于大多数程序, RepaintManager 可以看做是 Swing 内部系统的一部分, 实际
上可以忽略. 但是, 它的 API 给程序提供了绘制特定方面的更好的控制.

"当前"的 RepaintManager

RepaintManager 设计成为可以动态插入的, 虽然缺省只有一个实例. 下面的静
态方法允许程序得到和设置"当前"的 RepaintManager:
[code]
public static RepaintManager currentManager(Component c)
public static RepaintManager currentManager(JComponent c)
public static void
         setCurrentManager(RepaintManager aRepaintManager)
[/code]
替换"当前"的 RepaintManager

一个程序可以用如下方法全局性地扩展和代替 RepaintManager():
[code]
RepaintManager.setCurrentManager(new MyRepaintManager());
[/code]
你也可以看看 RepaintManagerDemo, 安装了一个只打印出正在重绘的信息的
RepaintManager 的简单例子.

扩展和替换 RepaintManager 的一个更有趣的原因可能是改变它如何处理重绘请
求. 现在的缺省实现把用来跟踪脏区域的内部状态包装成 private, 所以不能被
子类所访问. 但是程序可以实现它们自己的跟踪脏区域和合并请求的机制, 只要
重写如下方法:
[code]
public synchronized void
      addDirtyRegion(JComponent c, int x, int y, int w, int h)
public Rectangle getDirtyRegion(JComponent aComponent)
public void markCompletelyDirty(JComponent aComponent)
public void markCompletelyClean(JComponent aComponent)
[/code]

addDirtyRegion() 方法是当一个 Swing 控件调用 repaint() 时调用的, 可以
用来捕获所有的重绘请求. 如果一个程序重写了这个方法(而没有调用
super.addDirtyRegion())它就有责任调用 invokeLater() 来把一个 Runnable 放在
EventQueue 中, 以后会在合适的控件上调用 paintImmediately().

双缓冲的全局控制

RepaintManager 提供了一个 API 来全局地使双缓冲可用或者不可用.
[code]
public void setDoubleBufferingEnabled(boolean aFlag)
    public boolean isDoubleBufferingEnabled()
[/code]

这个属性在绘制操作的过程中在 JComponent 的内部检查, 来确定是否需要后台
缓冲区来画. 这个属性缺省是 true, 但是程序希望全局地给所有的 Swing 控件
禁止双缓冲, 可以这样做:
[code]
RepaintManager.currentManager(mycomponent).
                  setDoubleBufferingEnabled(false);
[/code]

注意: 因为 Swing 的缺省实现是实例化单个的 RepaintManager 实例,
mycomponent 参数是无关的.

[u]Swing 绘制指南[/u]

Swing 程序在写绘制代码的时候应该理解这些指南:

1, 对于 Swing 控件来说, paint() 作为系统触发的和程序触发的绘制请求的结
果, 它总会被调用; update() 在 Swing 控件中从来不会调用.

2, 程序可以通过调用 repaint() 来触发将来的 paint() 调用, 但不应该直接
调用 paint().

3, 对于有复杂输出的控件, 应该调用带有定义了需要更新区域的参数 的
repaint() 方法, 而不是没有参数的, 否则会引起这个控件的重绘.

4, Swing 的 paint() 通过 3 个分别的回调方法来实现;

4.1 paintComponent()
4.2 paintBorder()
4.3 paintChildren()

希望实现它们自己的绘制代码的 Swing 扩展控件应该把它们的代码放在
paintComponent() 的范围内(而不是 paint()).

5, Swing 引入了两个属性来提高绘制的效率:

* opaque: 控件是否需要绘制它全部的内容

* optimizedDrawingEnabled: 这个控件的任何子女可以覆盖吗?

6, 如果 Swing 控件的 opaque 属性设置为 true, 那么它就同意绘制包含在它
的边界之内(包括在 paintComponent() 中清除它自己的背景)的所有内容, 不然
屏幕上会有垃圾.

7, 把一个控件的 opaque 或者 optimizeDrawingEnabled 属性设置为 false,
会导致对每个绘制操作更多的处理过程, 所以我们建议聪明地使用透明的和覆盖
的控件.

8, 带有 UI 代理的 Swing 扩展控件(包括 JPanel), 应该在它们自己的
paintComponent() 实现中显式地调用 super.paintComponent(). 因为 UI 代理
负责清除不透明的控件的背景, 需要注意 #5.

9, Swing 通过 JComponent 的 doubleBuffered 属性内在支持双缓冲, 对于所
有的 Swing 控件缺省值是 true, 但是对于一个 Swing 容器把它设为 true, 对
于这个容器的所有轻量级的子孙来说效果好象是这个属性打开一样, 而不管它们
各自的属性设置.

10, 强烈推荐对于所有的 Swing 控件把双缓冲打开.

11, 那些画出复杂输出的控件应该智能地使用裁剪矩形来减少那些和裁剪矩形相
交的区域的绘制操作.

----------------------------------------------------------------------

[b][u]概要[/u][/b]

AWT 和 Swing 都提供了 API 来使程序更容易地把内容正确画到屏幕上. 虽然我
们推荐开发者对于大多数 GUI 需求使用 Swing, 但是理解 AWT 的绘制机制也非
常有用, 因为 Swing 是建立在 它的基础上的.

要从这些 API 中得到最好的性能, 应用程序必须使用这篇文章所列出来的指南.

--
爱的反面是什么? 恨吗? 不是, 如果一个人恨你一辈子,
这也很难得, 或许他(她)就是爱了你一辈子, 不曾忘记.
爱的反面是冷漠. 见面而如同陌路, 爱一个人, 不会忘
记, 那一份感觉会永远存在.
      SuperMMX.dhs.org 自由 SuperMMX
讨论自由软件, 包括 linux 和 java 等方面.


※ 来源:·BBS 水木清华站 smth.org·[FROM: 162.105.30.83]
--
※ 转载:·荔园晨风BBS站 bbs.szu.edu.cn·[FROM: 192.168.0.146]


[回到开始] [上一篇][下一篇]

荔园在线首页 友情链接:深圳大学 深大招生 荔园晨风BBS S-Term软件 网络书店