在Swing中的绘画
Swing起步于AWT基本绘画模式,并且作了进一步的扩展以获得最大化的性能以及改善可扩展性能。象AWT一样,Swing支持回调绘画以及使用repaint()
促使部件更新。另外,Swing提供了内置的双缓冲(double-buffering)并且作了改变以支持Swing的其它结构(象边框(border)和UI代理)。最后,Swing为那些想更进一步定制绘画机制的程序提供了RepaintManager
API。
对双缓冲的支持
Swing的最引人注目的特性之一就是把对双缓冲的支持整个儿的内置到工具包。通过设置javax.swing.JComponent
的"doubleBuffered"属性就可以使用双缓冲:
public boolean isDoubleBuffered()
public void setDoubleBuffered(boolean o)
当缓冲激活的时候,Swing的双缓冲机制为每个包容层次(通常是每个最高层的窗体)准备一个单独的屏外缓冲。并且,尽管这个属性可以基于部件而设置,对一个特定的容器上设置这个属性,将会影响到这个容器下面的所有轻量级部件把自己的绘画提交给屏外缓冲,而不管它们各自的"双缓冲"属性值
默认地,所有Swing部件的该属性值为true
。不过对于JRootPane
这种设置确实有些问题,因为这样就使所有位于这个上层Swing部件下面的所有部件都使用了双缓冲。对于大多数的Swing程序,不需要作任何特别的事情就可以使用双缓冲,除非你要决定这个属性是开还是关(并且为了使GUI能够平滑呈现,你需要打开这个属性)。Swing保证会有适宜的Graphics
对象(或者是为双缓冲使用的屏外映像的Graphics
,或者是正规的Graphics
)传递给部件的绘画回调函数,所以,部件需要做的所有事情仅仅就是使用这个Graphics
画图。本文的后面,在绘制的处理过程这一章会详细解释这个机制。
其他的绘画属性
为了改善内部的绘画算法性能,Swing另外引进了几个JComponent
的相互有关联的属性。引入这些属性为的是处理下面两个问题,这两个问题有可能导致轻量级部件的绘画成本过高:
- 透明(Transparency): 当一个轻量级部件的绘画结束时,如果该部件的一部分或者全部透明,那么它就可能不会把所有与其相关的像素位都涂上颜色;这就意味着不管它什么时候重画,它底层的部件必须首先重画。这个技术需要系统沿着部件的包容层次去找到最底层的重量级祖先,然后从它开始、从后向前地执行绘画。
- 重叠的部件(Overlapping components): 当一个轻量级部件的绘画结束是,如果有一些其他的轻量级部件部分地叠加在它的上方;就是说,不管最初的轻量级部件什么时候画完,只要有叠加在它上面的其它部件(裁剪区与叠加区相交),这些叠加的部件必须也要部分地重画。这需要系统在每次绘画时要遍历大量的包容层次,以检查与之重叠的部件。
遮光性
在一般情况下部件是不透明的,为了提高改善性能,Swing增加了读写javax.swing.JComponent
的遮光(opaque)
属性的操作:
public boolean isOpaque()
public void setOpaque(boolean o)
这些设置是:
-
true
:部件同意在它的矩形范围包含的里所有像素位上绘画。 -
false
:部件不保证其矩形范围内所有像素位上绘画。
遮光(opaque)
属性允许Swing的绘图系统去检测是否一个对指定部件的重画请求会导致额外的对其底层祖先的重画。每个标准Swing部件的默认(遮光)opaque
属性值由当前的视-感UI对象设定。而对于大多数部件,该值为true
。
部件实现中的一个最常见的错误是它们允许遮光(opaque)
属性保持其默认值true
,却又不完全地呈现它们所辖的区域,其结果就是没有呈现的部分有时会造成屏幕垃圾。当一个部件设计完毕,应该仔细的考虑所控制的遮光(opaque)
属性,既要确保透的使用是明智的,因为它会花费更多的绘画时间,又要确保与绘画系统之间的协约履行。
遮光(opaque)
属性的意义经常被误解。有时候被用来表示“使部件的背景透明”。然而这不是Swing对遮光的精确解释。一些部件,比如按钮,为了给部件一个非矩形的外形可能会把“遮光”设置为false,或者为了短时间的视觉效果使用一个矩形框围住部件,例如焦点指示框。在这些情况下,部件不遮光,但是其背景的主要部分仍然需要填充。
如先前的定义,遮光属性的本质是一个与负责重画的系统之间订立的契约。如果一个部件使用遮光属性去定义怎样使部件的外观透明,那么该属性的这种使用就应该备有证明文件。(一些部件可能更合适于定义额外的属性控制外观怎样怎样增加透明度。例如,javax.swing.AbstractButton
提供ContentAreaFilled
属性就是为了达到这个目的。)
另一个毫无价值的问题是遮光属性与Swing部件的边框(border)
属性有多少联系。在一个部件上,由Border
对象呈现的区域从几何意义上讲仍是部件的一部分。就是说如果部件遮光,它就有责任去填充边框所占用的空间。(然后只需要把边框放到该不透明的部件之上就可以了)。
如果你想使一个部件允许其底层部件能透过它的边框范围而显示出来 -- 即,通过isBorderOpaque()
判断border是否支持透明而返回值为false
-- 那么部件必须定义自身的遮光属性为false并且确保它不在边框的范围内绘图。
"最佳的"绘画方案
部件重叠的问题有些棘手。即使没有直接的兄弟部件叠加在该部件之上,也总是可能有非直系继承关系(比如"堂兄妹"或者"姑婶")的部件会与它交叠。这样的情况下,处于一个复杂层次中的每个部件的重画工作都需要一大堆的树遍历来确保'正确地'绘画。为了减少不必要的遍历,Swing为javax.swing.JComponent
增加一个只读的isOptimizedDrawingEnabled
属性:
public boolean isOptimizedDrawingEnabled()
这些设置是:
-
true
:部件指示没有直接的子孙与其重叠。 -
false
: 部件不保证有没有直接的子孙与之交叠。
通过检查isOptimizedDrawingEnabled
属性,Swing在重画时可以快速减少对交叠部件的搜索。
因为isOptimizedDrawingEnabled
属性是只读的,于是部件改变默认值的唯一方法是在其子类覆盖(override)这个方法来返回所期望的值。除了JLayeredPane,JDesktopPane
,和JViewPort
外,所有标准Swing部件对这个属性返回true
。
绘画方法
适应于AWT的轻量级部件的规则同样也适用于Swing部件 -- 举一个例子,在部件需要呈现的时候就会调用paint()
-- 只是Swing更进一步地把paint()
的调用分解为3个分立的方法,以下列顺序依次执行:
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)对象(叫做"UI代理")实现的观-感。这意味着标准部件把大多数或者所有的绘画委派给UI代理,并且出现在下面的途径:
-
paint()
触发paintComponent()
方法。 - 如果
ui
属性为non-null,paintComponent()
触发ui.update()。
- 如果部件的
遮光
属性为true,ui.udpate()
方法使用背景颜色填充部件的背景并且触发ui.paint()
。 -
ui.paint()
呈现部件的内容。
这意味着Swing部件的拥有UI代理的子类(相对于JComponent
的直系子类),应该在它们所覆盖的paintComponent
方法中触发super.paintComponent()
。
|
如果因为某些原因部件的扩展类不允许UI代理去执行绘画(是如果,例如,完全更换了部件的外观),它可以忽略对super.paintComponent()
的调用,但是它必须负责填充自己的背景,如果遮光(opaque)
属性为true
的话,如前面在遮光(opaque)
属性一章讲述的。
绘画的处理过程
Swing处理"repaint"请求的方式与AWT有稍微地不同,虽然对于应用开发人员来讲其本质是相同的 -- 同样是触发paint()
。Swing这么做是为了支持它的RepaintManager
API (后面介绍),就象改善绘画性能一样。在Swing里的绘画可以走两条路,如下所述:
(A) 绘画需求产生于第一个重量级祖先(通常是JFrame、JDialog、JWindow
或者JApplet
):
- 事件分派线程调用其祖先的
paint().
-
Container.paint()
的默认实现会递归地调用任何轻量级子孙的paint()
方法。 - 当到达第一个Swing部件时,
JComponent.paint()
的默认执行做下面的步骤:
- 如果部件的
双缓冲
属性为true
并且部件的RepaintManager
上的双缓冲已经激活,将把Graphics
对象转换为一个合适的屏外Graphics
。 - 调用
paintComponent()
(如果使用双缓冲就把屏外Graphics传递进去)。 - 调用
paintChildren()
(如果使用双缓冲就把屏外Graphics传递进去),该方法使用裁剪并且遮光
和optimizedDrawingEnabled
等属性来严密地判定要递归地调用哪些子孙的paint()
。 - 如果部件的
双缓冲
属性为true
并且在部件的RepaintManager
上的双缓冲已经激活,使用最初的屏幕Graphics
对象把屏外映像拷贝到部件上。
注意:JComponent.paint()
步骤#1和#5在对paint()
的递归调用中被忽略了(由于paintChildren()
,在步骤#4中介绍了),因为所有在swing窗体层次中的轻量级部件将共享同一个用于双缓冲的屏外映像。
(B) 绘画需求从一个javax.swing.JComponent
扩展类的repaint()
调用上产生:
-
JComponent.repaint()
注册一个针对部件的RepaintManager
的异步的重画需求,该操作使用invokeLater()
把一个Runnable
加入事件队列以便稍后执行在事件分派线程上的需求。 - 该Runnable在事件分派线程上执行并且导致部件的
RepaintManager
调用该部件上paintImmediately()
,该方法执行下列步骤:
- 使用裁剪框以及
遮光
和optimizedDrawingEnabled
属性确定“根”部件,绘画一定从这个部件开始(处理透明以及潜在的重叠部件)。 - 如果根部件的
双缓冲
属性为true
,并且根部件的RepaintManager
上的双缓冲已激活,将转换Graphics
对象到适当的屏外Graphics
。 - 调用根部件(该部件执行上述(A)中的
JComponent.paint()
步骤#2-4)上的paint()
,导致根部件之下的、与裁剪框相交的所有部件被绘制。 - 如果根部件的
doubleBuffered
属性为true
并且根部件的RepaintManager
上的双缓冲已经激活,使用原始的Graphics
把屏外映像拷贝到部件。
注意:如果在重画没有完成之前,又有发生多起对部件或者任何一个其祖先的repaint()
调用,所有这些调用会被折叠到一个单一的调用 回到paintImmediately()
on topmostSwing部件 on which 其repaint()
被调用。例如,如果一个JTabbedPane
包含了一个JTable
并且在其包容层次中的现有的重画需求完成之前两次发布对repaint()
的调用,其结果将变成对该JTabbedPane
部件的paintImmediately()
方法的单一调用,会触发两个部件的paint()
的执行。
这意味着对于Swing部件来说,update()
不再被调用。
虽然repaint()
方法导致了对paintImmediately()
的调用,它不考虑"回调"绘图,并且客户端的绘画代码也不会放置到paintImmediately()
方法里面。实际上,除非有特殊的原因,根本不需要超载paintImmediately()
方法。
同步绘图
象我们在前面章节所讲述的,paintImmediately()
表现为一个入口,用来通知Swing部件绘制自身,确认所有需要的绘画都能适当地产生。这个方法也可能用来安排同步的绘图需求,就象它的名字所暗示的,即一些部件有时候需要保证它们的外观实时地与其内部状态保持一致(例如,在JScrollPane
执行滚定操作的时候确实需要这样并且也做到了)。
程序不应该直接调用这个方法,除非有合理实时绘画需要。这是因为异步的repaint()
可以使多个重复的需求得到有效的精简,反之直接调用paintImmediately()
则做不到这点。另外,调用这个方法的规则是它必须由事件分派线程中的进程调用;它也不是为能以多线程运行你的绘画代码而设计的。关于Swing单线程模式的更多信息,参考一起归档的文章"Threads and Swing."
RepaintManager
Swing的RepaintManager
类的目的是最大化地提高Swing包容层次上的重画执行效率,同时也实现了Swing的'重新生效'机制(作为一个题目,将在其它文章里介绍)。它通过截取所有Swing部件的重画需求(于是它们不再需要经由AWT处理)实现了重画机制,并且在需要更新的情况下维护其自身的状态(我们已经知道的"dirty regions")。最后,它使用invokeLater()
去处理事件分派线程中的未决需求,如同在"Repaint Processing"一节中描述的那样(B选项).
对于大多数程序来讲,RepaintManager
可以看做是Swing的内部系统的一部分,并且甚至可以被忽略。然而,它的API为程序能更出色地控制绘画中的几个要素提供了选择。
"当前的"RepaintManager
RepaintManager
设计 is designed to be dynamically plugged, 虽然 有一个单独的接口。下面的静态方法允许程序得到并且设置"当前的"RepaintManager
:
public static RepaintManager currentManager(Component c) public static RepaintManager currentManager(JComponent c) public static void setCurrentManager(RepaintManager aRepaintManager)
更换"当前的"RepaintManager
总的说来,程序通过下面的步骤可能会扩展并且更换RepaintManager
:
RepaintManager.setCurrentManager(new MyRepaintManager());
你也可以参考RepaintManagerDemo ,这是个简单的举例说明RepaintManager
加载的例子,该例子将把有关正在执行重画的部件的信息打印出来。
扩展和替换RepaintManager
的一个更有趣的动机是可以改变对重画的处理方式。当前,默认的重画实现所使用的来跟踪dirty regions的内部状态值是包内私有的并且因此不能被继承类访问。然而,程序可以实现它们自己的跟踪dirty regions的机制并且通过超载下面的方法对重画需求的缩减:
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) {
addDirtyRegion()
方法是在调用Swing部件的repaint()
的之后被调用的,因此可以用作钩子来捕获所有的重画需求。如果程序超载了这个方法(并且不调用super.addDirtyRegion()
),那么它改变了它的职责,而使用invokeLater()
把Runnable
放置到EventQueue
,该队列将在合适的部件上调用paintImmediately()
(translation: not for the faint of heart).
从全局控制双缓冲
RepaintManager
提供了从全局中激活或者禁止双缓冲的API:
public void setDoubleBufferingEnabled(boolean aFlag)
public boolean isDoubleBufferingEnabled()
这个属性在绘画处理的时候,在JComponent
的内部检查过以确定是否使用屏外缓冲显示部件。这个属性默认为true
,但是如果程序希望在全局范围为所有Swing部件关闭双缓冲的使用,可以按照下面的步骤做:
RepaintManager.currentManager(mycomponent). setDoubleBufferingEnabled(false);
注意:因为Swing的默认实现要初始化一个单独的RepaintManager
实例,mycomponent
参数与此不相关。
Swing绘画准则
Swing开发人员在写绘画代码时应该理解下面的准则:
- 对于Swing部件,不管是系统-触发还是程序-触发的请求,总会调用
paint()
方法;而update()
不再被Swing部件调用。 - 程序可以通过
repaint()
触发一个异步的paint()
调用,但是不能直接调用paint()
。 - 对于复杂的界面,应该调用带参数的
repaint()
,这样可以仅仅更新由该参数定义的区域;而不要调用无参数的repaint()
,导致整个部件重画。 - Swing中实现
paint()
的3个要素是调用3个分离的回调方法:
-
paintComponent()
-
paintBorder()
-
paintChildren()
- Swing部件的子类,如果想执行自己的绘画代码,应该把自己的绘画代码放在
paintComponent()
方法的范围之内。(不要放在paint()
里面)。
- Swing引进了两个属性来最大化的改善绘画的性能:
-
opaque
: 部件是否要重画它所占据范围中的所有像素位? -
optimizedDrawingEnabled
: 是否有这个部件的子孙与之交叠?
- 如故Swing部件的
(遮光)opaque
属性设置为true
,那就表示它要负责绘制它所占据的范围内的所有像素位(包括在paintComponent()
中清除它自己的背景),否则会造成屏幕垃圾。 - 把一个部件设置为
遮光(opaque)
同时又把它的optimizedDrawingEnabled
属性设置为false
,将导致在每个绘画操作中要执行更多的处理,因此我们推荐的明智的方法是同时使用透明并且交叠部件。 - 使用UI代理(包括
JPanel
)的Swing部件的扩展类的典型作法是在它们自己的paintComponent()
的实现中调用super.paintComponent()
。因为UI代理可以负责清除一个遮光部件的背景,这将照顾到#5. - Swing通过
JComponent
的doubleBuffered
属性支持内置的双缓冲,所有的Swing部件该属性默认值是true
,然而把Swing容器的遮光设置为true
有一个整体的构思,把该容器上的所有轻量级子孙的属性打开,不管它们各自的设定。 - 强烈建议为所有的Swing部件使用双缓冲。
- 界面复杂的部件应该灵活地运用剪切框来,只对那些与剪切框相交的区域进行绘画操作,从而减少工作量。
总结
不管AWT还是Swing都提供了方便的编程手段使得部件内容能够正确地显示到屏幕上。虽然对于大多数的GUI需要我们推荐使用Swing,但是理解AWT的绘画机制也会给我们带来帮助,因为Swing建立在它的基础上。
关于AWT和Sing的特点就介绍到这里,应用开发人员应该尽力按照本文中介绍的准则来撰写代码,充分发挥这些API功能,使自己的程序获得最佳性能。