JAVA Painting-Swing实现纪要一
首先推荐<Painting in AWT and Swing>by Amy Fowler。
Sun在JDK 1.0最初发布了图形API包,代号AWT (abstract windowing toolkit),里面除对GUI基本支持(如结合各OS的事件分发机制等)外,自有一套重量级开发GUI的思路,并提供了一组常规使用的重量级组件。所谓重量级组件就是每个组件都引用一个本地对等体peer成员对象,这个对等体对象利用本地系统GUI API绘制组件。后来在JDK1.1,AWT包中引进了一套轻量级开发GUI的新思路,并提供了一组轻量级组件。所谓轻量级组件就是自身没有本地对等体,而借助重量级组件作为容器来绘制组件。JDK 1.1之后,sun在开发GUI思路上,在效率,扩展性等方面给出了很多创新,并基于这种新思路推出一套丰富的新组件(轻量级组件),sun为此打出一个新的响亮的代号---Swing,并推荐以后的GUI开发都应该基于SWING的GUI开发思路开展,应该使用或扩展这套SWING的组件。
不论是AWT模式还是SWING模式,Sun的GUI开发思路都是纯OO的。开发人员总是构建多个组件对象实例来组合建立GUI,这些对象是因不同的输入输出表现被封装为多种组件类的实例,而这些组件类是有合理的继承关系因而容易扩展的“套件”。而且两种模式最基本的统一的程序运行思路都是:
1.通过建立各种组件的实例来负责GUI的工作。
2. 约定出GUI变化时机—java应用程序随需发出请求调用或对操作系统级某种操作的监听(如暴露被遮挡的窗口内容)。
3. 在时机到来时由“框架程序”来判断并调用应该调用的目标组件实例所提供的各种形式的paint方法(各组件在此方法里通过java 2d API包来实现自己的具体绘制逻辑)来完成各组件绘制。
4. 在GUI的整个生命周期里,通过以上的123模式来完成整个应用界面的随需而变。
下文将主要分析SWING模式。
Swing式 开发GUI的基本约定包括:SWING提供4个顶层容器JFrame,JDialog,JApplet,JWindow,如果是桌面应用,则GUI必须要有一个JFrame,如果是浏览器应用,则GUI必须要有一个JApplet。其他swing组件,或自定义开发的Swing组件都扩展自JComponent,并且其实例要存在于顶层容器的层次树中。下面是一个符合约定的GUI的运行分析。
import javax.swing.JFrame;
import javax.swing.JLabel;
public class BasicSwing {
public static void main(String[] args) {
javax.swing.SwingUtilities.invokeLater(new Runnable() {
public void run() {
createAndShowGUI();
}
private void createAndShowGUI() {
JFrame frame = new JFrame("BasicSwing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JLabel label=new JLabel("hello world");
frame.getContentPane().add(label);
frame.setSize(100,200);
frame.setVisible(true);
}
});
}
}
invokeLater方法在执行时首先会延迟创建getToolkit---系统属性awt.toolkit给出了要加载的类,在windows平台下即为WToolkit。WToolkit在初始化时会启动AWT-WINDOWS线程(setDaemon-true),该线程将一直负责从win32系统中获取底层事件并简接挂到EventQueue事件队列中;同时激活AWT-Shutdown线程(setDaemon-false),该线程一直监测是否满足关闭GUI的条件(peerMap is null;AWT-WINDOWS is busy;EDT is busy),若是则主动要求关闭EDT也就是GUI最终退出(因为GUI环境下只有EDT是非daemon线程);WToolkit还有就是加载sun.java2d.Disposer类,其将在类加载初始化时启动Java2D Disposer线程(setDaemon-true, MAX_PRIORITY),该线程将一直跟踪监测被废弃的注册记录(WToolkit算一个,还有各种peer),监测到后执行对应的Dispose类来完成相应的资源回收。
invokeLater方法同时会创建EventQueue挂在AppContext里,并马上向EventQueue提交InvocationEvent以执行上面例子中的Runable,这将导致启动第一个AWT-EventQueue-N线程(EDT-(setDaemon-false))。
EDT启动后将一直从EventQueue获取AWTEVENT进行dispatch分发处理,处理过程中若遇到某些意外或被强制中断都有可能导致EDT熄火,此时AWT-Shutdown被notify检测彻底终止AWT的时机是否到来,若不满足条件新的EDT:AWT-EventQueue-N+1将被启动。
以上将建立起界面GUI的基本运行框架。上述例子的main线程很快退出,而EDT线程将处理InvocationEvent,处理过程即是执行Runable.run方法体。
在EDT中,JFrame被构造,在其构造过程中,会追加dispose记录即
(addRecord(JFrame.anchorObject, WindowDisposerRecord(appContext,this));)进Java2D Disposer以在失去引用时释放窗口资源。
随后JFrame被setvisible,在setvisible过程中,将通过WToolkit createFramePeer,并注册在AWT-Shutdown的peerMap中以支持AWT-AutoShutDown机制。
Setvisible中将促使调用peer.pShow-native代码,即发送给win32请求显示窗口,窗口被打开后awt_windows线程在eventloop中得到wm_paint消息进行处理,这是一个异步过程。
awt_windows处理中将有选择地通过RepaintManager加入重画记录区几何区域
RepaintManager. nativeAddDirtyRegio并调度重画线程单位在EDT中进行绘制
postEvent-InvocationEvent(ProcessingRunnable),ProcessingRunnable随后
在EDT中run时将根据重画区记录执行可能的窗口内容绘制--即各子组件回调paint过程。
上述是SWING顶层重量级容器组件的一个绘制场景,可以看到是经由awt-windows eventloop到了底层事件后触发paint绘制;然而对轻量级swing组件,其paint都是通过java代码中对repaint调用而触发,其会向RepaintManager.addDirtyRegion,同时scheduleProcessingRunnable。这是整个GUI生命周期内对绘制的两种不同的触发方式,但触发后的处理都是交由RepaintManager。
回过头去看,JFrame被构造的时候就会创建root pane, layered pane,content pane, glass pane等,这些没有对等体的轻量级Swing组件在构造时都将repaint。虽然在创建windows对等窗口之前这些Swing组件就已经在要求绘制,但是RepaintManager能够协调好这个步调(具体即是当收到repaint请求时要判断情况,像这时的请求因为顶层容器还没有绘制则不会记录到重画区)。所以最终效果就是在peer.pshow的时候只能看到一个空窗口,随后底层消息到来后通过paint回调画这些子组件,最后hello world才显示出来。如果眼神好,能够看出这有一个“闪烁”。
这是一个最简单的swing应用程序的基本运行机制分析,下面再具体分析。
Swing的GUI总是由顶层容器组件和轻量级swing组件组合建立,顶层容器和其他组件区别主要在于顶层容器没有自身的paint逻辑。
所有顶层容器都是通过使用底层系统API来绘制对等体的方式进行paint,自身没有java2d的paint逻辑实现,对等体画成什么样顶层容器就是什么样,它只是可以控制对等体的一些可配显示属性。所以效果就是比如在windows平台上画一个jframe,除在桌面上显示一个窗口还会在任务栏上显示一个条目。Swing的4个顶层容器都是在addNotify时才会getToolkit().createPeer(this)(Frame/Dialog/Window),而addNotify并不是在构造时被调用,而是在pack/show或setvisible(这3个所谓的realized具现化方法)时被调用。创建了对等体peer后还要通过peer.pShow(show/setVisible(true)调用)调用才会要求底层系统进行显示(所以只有pack是不会显示窗口的)。在显示窗口后底层消息队列得到通知,此后随着窗口被最小化后恢复或被遮盖后恢复等系统操作后同样能从底层消息得到通知,这时的监听处理将有选择地通知给RepaintManager一个重画请求进行窗口内容-子组件重画。
而轻量级swing组件将绘制有关的职责都委托给了ui成员对象,ui对象使用JAVA2D API 进行绘制,paint成什么样那就是这个组件的样子。具体就是在构造的时候即要updateUI{setUI(UIManger.getUI(this))}。UIManger会根据当前L&F的选择,根据this.uiClassID来得到ui成员类并建立实例,以后的paint回调等都推托给ui成员类paint,这也算是一种策略模式。Setui的过程中除了保存这个ui实例外,将repaint来通知RepaintManager进行paint回调完成组件绘制。轻量级swing组件在addNotify时也会去创建对等体getToolkit().createPeer(this)( LightWeightPeer),但这个peer的实现(NullComponentPeer)是个空壳子,只是作为一个轻量级组件的标记,以后的很多事件处理等都要判断peer是否instance of LightWeightPeer从而能够进行不同处理。同样的Addnotify也不是在构造时被调用,而是在被加入container时被调用。
注意:构造方法本身就是状态模式的第一状态,所以GUI组件的构造方法里就应该要努力完成自身的绘制来符合自己的地位。轻量级组件就是按这个意义在构造方法里去通知repaintmanager进行自身绘制的,但是顶层容器却将真正的绘制意图createPeer延迟到了具现方法里。这是因为首先一个合乎思维的表达逻辑是先有容器,再将子组件向容器里添加, 所以最顶层容器总是先行构造出来,然后再被一层层地追加轻量级子组件。如果最顶层容器在构造时就去具现,则就要求后续的构造都应该在EDT中进行,而且每次add子组件都要导致revalidate;但若将最顶层容器的绘制分离延迟到具现方法里,则可以表达是在容器里盛满了要显示的子组件后再一股脑具现绘制出来的概念,类似于在进行一次web页面的完整加载,然后注意在具现方法执行后如果要操作组件都在EDT中进行即可,而且顶层容器提供一个特有的pack方法,用来一次性对所有子组件验证大小位置进行重布局,pack之后再show,这样的一次性计算展现是最有效率的。
顶层容器和轻量级组件就是这样诞生并绘制的,在此后的生命周期里,都将按事件监听机制完成GUI随需而变,无论是系统事件,还是因为repaint调用主动post事件,事件到来后再在EDT中执行监听器里的paint绘制。Swing已经提供的顶层容器和轻量级组件因各自的定义已经注册了各自的paint监听,开发人员可以再行维护或按此模式开发新组件从而满足应用的需要。比如,jbutton默认有mousepress listener,在mousepress事件到来后,监听响应中会设置鼠标颜色加深来表示按下,然后再调用repaint要求重画,随后在EDT中执行jbutton的paint回调,此时按深颜色绘制,于是一个被按下的效果就出来了。
下面在具体分析各类事件的处理。
对于顶层容器的受底层事件消息的触发,当得到的通知是因为expose暴露隐藏区(暴露被遮蔽的部分或恢复最小化或第一次绘制等)时,处理过程会涉及到双缓存的处理,即如果可能,直接使用缓存中的旧图像信息进行覆盖而不再重新绘制。
所谓双缓存机制是将一整片的显示内容暂时写入一张内存空间里,然后一次性内存拷入显示区来进行显示,这样处理是因为如果直接写入显示区,随着显示区被该写入线程逐渐写入,可能经历多次屏幕刷新,导致每次刷新都形成过程图像,给人眼造成闪烁感觉;同时一个副收益就是可以针对每个窗口都做缓存待用(而不仅仅是针对一个屏幕双缓存),当窗口被遮挡的部分重现时直接拷贝缓存来覆盖,不用再执行绘画逻辑,提高了效率。
现在的OS一般都提供双缓存机制支持,如果底层系统自身支持以每个窗口为单位做双缓存,则该expose消息将被本地处理,不需要通知进行子组件的绘制;如果底层不支持,则该消息会到达wcomponetpeer.handleexpose中进行回调处理,此时swing机制下有一个参数控制的双缓存机制可以提供。这里的参数控制需要从RepaintManager的构造过程说起。
首先RepaintManager可以通过static setCurrentManager(SomeCurrentManager)来进行全局指定。默认情况使用currentRepaintManager(){new RepaintManager(BUFFER_STRATEGY_TYPE)}得到一个延迟创建的单例。RepaintManager有一段静态类初始化过程,涉及到双缓存设置:
static {
nativeDoubleBuffering = "true".equals(AccessController.doPrivileged(
new GetPropertyAction("awt.nativeDoubleBuffering")));//JVM的启动参数控制,默认false
String bs = AccessController.doPrivileged(
new GetPropertyAction("swing.bufferPerWindow"));//是否每窗口缓存。
if (headless) {
BUFFER_STRATEGY_TYPE = BUFFER_STRATEGY_SPECIFIED_OFF;
}
else if (bs == null) {
BUFFER_STRATEGY_TYPE = BUFFER_STRATEGY_NOT_SPECIFIED;
}
else if ("true".equals(bs)) {
BUFFER_STRATEGY_TYPE = BUFFER_STRATEGY_SPECIFIED_ON;
}
else {
BUFFER_STRATEGY_TYPE = BUFFER_STRATEGY_SPECIFIED_OFF;
}
}
private RepaintManager(short bufferStrategyType) {
// If native doublebuffering is being used, do NOT use
// Swing doublebuffering.
doubleBufferingEnabled = !nativeDoubleBuffering;
this.bufferStrategyType = bufferStrategyType;
}
public void setDoubleBufferingEnabled(boolean aFlag) {
doubleBufferingEnabled = aFlag;
doubleBufferingEnabled(开启双缓存),nativeDoubleBuffering(利用本地双缓存机制),bufferStrategyType(每窗口双缓存策略)
这几个参数将影响到RepaintManager的成员对象paintManager的选择,也算是一个策略模式,该paintManager是负责绘制的核心类。
private synchronized PaintManager getPaintManager() {
if (paintManager == null) {
PaintManager paintManager = null;
if (doubleBufferingEnabled && !nativeDoubleBuffering) {
switch (bufferStrategyType) {
case BUFFER_STRATEGY_NOT_SPECIFIED:
if (((SunToolkit)Toolkit.getDefaultToolkit()).
useBufferPerWindow()) {//windows下是否禁用vista dwm,在没有声明bufferPerWindow的情况下由windows系统特性确定paintmanager。
paintManager = new BufferStrategyPaintManager();
}
break;
case BUFFER_STRATEGY_SPECIFIED_ON:
paintManager = new BufferStrategyPaintManager();
break;
default:
break;
}
}
// null case handled in setPaintManager
setPaintManager(paintManager);
}
return paintManager;
}
void setPaintManager(PaintManager paintManager) {
if (paintManager == null) {
paintManager = new PaintManager();
}
}
回到上文,当handleexpose时,通过getPaintEventDispatcher 来createPaintEvent,在UIManager.initialize根据RepaintManager.HANDLE_TOP_LEVEL_PAINT(属性swing.handleTopLevelPaint)确定是SwingPaintEventDispatcher还是直接使用PaintEventDispatcher。
若为false,在PaintEventDispatcher中,将直接创建PaintEvent-PAINT提交,此后该事件经合并后将由wcomponentpeer.handleEvent,该处理将通过一个自身维护的paintArea几何脏区域进行重画区域优化,最终委托给Container进行子组件绘制,这是非SWING模式-即AWT模式,没有双缓存的概念。
补充:在Swing和它的RepainManager出现以前,GUI的模式-AWT模式总是要先形成一个PaintEvent(触发可能来自底层消息-PAINT类型,也可能来自repaint-UPDATE类型),post给EventQueue,并组织一次合并:
public abstract class WComponentPeer{
void handlePaint(int x, int y, int w, int h) {
System.out.println("handlePaint>>>"+x+":"+y+":"+w+":"+h);
postPaintIfNecessary(x, y, w, h);
}
private void postPaintIfNecessary(int x, int y, int w, int h) {
if ( !ComponentAccessor.getIgnoreRepaint( (Component) target) ) {
PaintEvent event = PaintEventDispatcher.getPaintEventDispatcher().
createPaintEvent((Component)target, x, y, w, h);
if (event != null) {
postEvent(event);
}
}
}
public class PaintEventDispatcher {
public PaintEvent createPaintEvent(Component target, int x, int y, int w,
int h) {
return new PaintEvent((Component)target, PaintEvent.PAINT,
new Rectangle(x, y, w, h));
}
public abstract class Component{
public void repaint(long tm, int x, int y, int width, int height) {
if (this.peer instanceof LightweightPeer) {
~~~
parent.repaint(tm, px, py, pwidth, pheight);
}
} else {
if (isVisible() && (this.peer != null) &&
(width > 0) && (height > 0)) {
PaintEvent e = new PaintEvent(this, PaintEvent.UPDATE,
new Rectangle(x, y, width, height));
Toolkit.getEventQueue().postEvent(e);
}
}
}
public class EventQueue{
private void postEvent(AWTEvent theEvent, int priority) {
if (coalesceEvent(theEvent, priority)) {//post之前总是需要合并
return;
}
private boolean coalesceEvent(AWTEvent e, int priority) {
if (e instanceof PaintEvent) {
return coalescePaintEvent((PaintEvent)e);//对paintevent进行一轮合并处理,导致同一重量级组件的多次paintevent被合并为一个paintevent等待dispatch。以提高效率
}
然后EDT中在Component.dispatchImpl中委托给wcomponentpeer处理。
public abstract class Component{
dispatchEventImpl{
/*
* 9. Allow the peer to process the event.
* Except KeyEvents,
*/
if (tpeer != null) {
tpeer.handleEvent(e);
}
public abstract class WComponentPeer{
public void handleEvent(AWTEvent e) {
switch(id) {
case PaintEvent.PAINT:
// Got native painting
paintPending = false;
// Fallthrough to next statement
case PaintEvent.UPDATE:
// Skip all painting while layouting and all UPDATEs
// while waiting for native paint
if (!isLayouting && ! paintPending) {
paintArea.paint(target,shouldClearRectBeforePaint());
}
return;
default:
break;
}
Peer处理过程中将利用自身维护的PaintArea进行重画区域的优化,并执行子组件paint回调。
/**
* Invokes paint and update on target Component with optimal
* rectangular clip region.
* If PAINT bounding rectangle is less than
* MAX_BENEFIT_RATIO times the benefit, then the vertical and horizontal unions are
* painted separately. Otherwise the entire bounding rectangle is painted.
*
* @param target Component to <code>paint</code> or <code>update</code>
* @since 1.4
*/
public void paint(Object target, boolean shouldClearRectBeforePaint) {
Component comp = (Component)target;
~~~
if (ra.paintRects[HORIZONTAL] != null && ra.paintRects[VERTICAL] != null) {
Rectangle paintRect = ra.paintRects[HORIZONTAL].union(ra.paintRects[VERTICAL]);
int square = paintRect.width * paintRect.height;
int benefit = square - ra.paintRects[HORIZONTAL].width
* ra.paintRects[HORIZONTAL].height - ra.paintRects[VERTICAL].width
* ra.paintRects[VERTICAL].height;
// if benefit is comparable with bounding box
if (MAX_BENEFIT_RATIO * benefit < square) {
ra.paintRects[HORIZONTAL] = paintRect;
ra.paintRects[VERTICAL] = null;
}
}
for (int i = 0; i < paintRects.length; i++) {
if (ra.paintRects[i] != null
&& !ra.paintRects[i].isEmpty())
{
// Should use separate Graphics for each paint() call,
// since paint() can change Graphics state for next call.
Graphics g = comp.getGraphics();
if (g != null) {
try {
g.setClip(ra.paintRects[i]);
if (i == UPDATE) {
updateComponent(comp, g);
} else {
if (shouldClearRectBeforePaint) {
g.clearRect( ra.paintRects[i].x,
ra.paintRects[i].y,
ra.paintRects[i].width,
ra.paintRects[i].height);
}
paintComponent(comp, g);
}
} finally {
g.dispose();
}
}
}
}
}
若为true,在SwingPaintEventDispatcher.createPaintEvent,
if (component instanceof RootPaneContainer) {//如果是顶层容器
AppContext appContext = SunToolkit.targetToAppContext(component);
RepaintManager rm = RepaintManager.currentManager(appContext);
if (!SHOW_FROM_DOUBLE_BUFFER ||//参数swing.showFromDoubleBuffer控制,默认true确定swing//是否会考虑双缓存支持
!rm.show((Container)component, x, y, w, h)) {
rm.nativeAddDirtyRegion(appContext, (Container)component,
x, y, w, h);
}
return new IgnorePaintEvent(component, PaintEvent.PAINT,
new Rectangle(x, y, w, h));//返回一个将被忽略的假事件提交
如果SHOW_FROM_DOUBLE_BUFFER 考虑双缓存支持,将进行rm.show,其交给getPaintManager().show,这时的paintmanager是经过了前面所说的几个参数选择的,也就是说,考虑当前是否当前正使能双缓存doubleBufferingEnabled,是否不使用本地双缓存nativeDoubleBuffering, BUFFER_STRATEGY_TYPE是否指定了每窗口缓存的双缓存支持策略,如果没有指定策略是否或本地windows系统环境没有开启vista dwm效果,如果都满足将使用BufferStrategyPaintManager,借由swing提供每窗口双缓存机制,检查swing记录中是否具有有效缓存,若存在则会要求该区直接拷贝flip即可,如果没有成功执行双缓存拷贝,则将加入Repaintmanager重画区域进行swing模式的重画。
顶层容器除了在对等体发过消息后处理paint,也具有自己的repaint方法去主动创造绘画时机。
public void repaint(long time, int x, int y, int width, int height) {
if (RepaintManager.HANDLE_TOP_LEVEL_PAINT) {//属性swing.handleTopLevelPaint确定,默认true
RepaintManager.currentManager(this).addDirtyRegion(
this, x, y, width, height);
}
else {
super.repaint(time, x, y, width, height);
}
}
这里的repaint将首先确定RepaintManager.HANDLE_TOP_LEVEL_PAINT-如果不支持将委托给Component.repaint,形成PaintEvent并进行提交走AWT模式。支持的话将促使RepaintManager加入重画区后通过调度走SWING模式。SWING模式就是走RepaintManager的方式。自身的repaint不会去考虑每窗口双缓存直接拷贝区域,因为这时的需求就是要求重新绘画。
轻量级swing组件在自己的repaint方法去主动创造绘画时机。
JComponent.Repaint{RepaintManager.currentManager(this).addDirtyRegion}走SWING模式处理。
SWING模式都是借由RepaintManager来安排绘画,它维护了一个几何区域并负责重画的框架。外界总是要求先加入RepaintManager重绘区,在加入的同时激发起一个调度重画的
SunToolkit.getSystemEventQueueImplPP(context).
postEvent(new InvocationEvent(Toolkit.getDefaultToolkit(),
processingRunnable))
InvocationEvent。
注意,通过上文分析,对于顶层容器处理底层消息的触发时,走swing处理模式而通过swingpaintEventdispatcher去创建painitevent时除向repaintmanager登记脏区(如果不使用每窗口双缓存策略)外,还要额外post一个IgnorePaintEvent。该paintevent在随后的EDT里按awt模式走peer处理时并没有加入awt的重画脏区,实际上忽略掉了绘制意义,这样做避免了在swing和awt两种模式的重复绘制,但同时形成依然将paint事件通知到组件的效果。
public void coalescePaintEvent(PaintEvent e) {
Rectangle r = e.getUpdateRect();
if (!(e instanceof IgnorePaintEvent)) {
paintArea.add(r, e.getID());
}
---------------------------------------------------------------------------------------
——使你疲劳的不是远方的高山,而是你鞋里一粒沙子!