级别: 初级 张民 (zhang_min@263.net),
2001 年 5 月 13 日 在图形界面的应用系统中,经常会遇到的需求是界面上的元素可以自由拖动,整个界面具有较为美观的背景。但原有AWT包提供的组件不能直接完成此功能,此外,拖动过程中的闪烁及对象选中策略等问题也是实际应用中需要解决的问题。本文将会介绍一个基于AWT包的界面显示框架,完成上述功能。
整体介绍
在整个框架中,我们的显示区域定义为一个类,名为
DemoPane,它是java.awt.Panel类的子类。在DemoPane上显示的元素为类
DisplayUnit 的实例。DisplayUnit是java.awt.component
的子类,是基本的显示单元。在DemoPane上,可以设置一图像背景,对加入其它的显示单元(图像或文字)将完成基本的显示(包括透明图片的显示),而对其中的某些单元可以完成拖动操作。(
详见演示程序)。
DisplayUnit
从面向对象的设计角度出发,在显示区域上的每一个元素都应该有自己的属性,能独立完成一定的功能。在此框架中主要就是能够完成绘制工作的能力。也就是说,在我们希望对它说,“你可以将你自己画在画布上了”,然后我们给它块画布,它就在上面将自己画出来了。AWT中就是这样做的了。在所有
AWT 组件的父类中有一个方法,paint(Graphics
g),它的作用就是利用AWT中Graphics类提供的能力,在传入的 g
上面完成对自己的绘制工作。
DisplayUnit 扩展了
java.awt.component,那么它与其它的component的子类有什么不同之处呢?首先,我们希望它可以显示文字或者图片,或者两者的组合。因而我们的
DisplayUnit 就应有两个属性:Image
image(用来表示元素的图片)和String
text(用来表示元素上所显示的文字),而且与之相关的属性也是很容易想到的,包括字体与文字颜色,以及文字绘制的位置。第二,显示单元可以具有一定的“活跃性”,即它可以被“激活”,被“移动”。在被激活时它的显示可能会有所不同,此属性用
activable 来描述;同时,用 movable
属性来描述其是否可以被拖动。这在以后的显示中会被其“容器”,也就是
DemoPane
用到。第三,在其处于“激活”状态时可以将其显示做一些修改,这种修改可以从图片的改变或文字的改变反映出来,我们以
altImage
属性来代表在某些情况下所显示的替代图片,(默认的替代显示是对图像做一些处理,即以较小的尺寸显示原图)。
AWT绘制的基本原理与基本控制
AWT的绘制与界面更新使用了一个单独的线程,称为AWT线程。这个线程可以在两种情形下更新显示。
一种情况是界面“显露”,这可能会发生在界面首次显示时,或者界面某一部分由于被其它窗口遮盖后重新显示时。界面显露的处理是AWT自动进行的,。
第二种情况是程序在显示内容有所改变时进行界面的更新,而这一般是由应用程序的逻辑来控制的。
上面的两种情况最终都将作用于某个paint()方法,更准确地说是具有调用关系的一组
paint() 方法。如果是一个component的刷新,则它的
paint()方法被调用;如果是界面容器(Container)的刷新,则它的paint()方法将按某种策略/顺序依次调用加入其中的每个component的paint()方法。paint
方法的声明形式为:
public void paint(Graphics g);
paint() 方法所传入的参数――一个 java.awt.Graphics
对象将是一个经裁剪的相关显示区的图像代表(而不会是整个显示区)。我们对可显示图形元素的绘制就是在通过重写
paint()方法,在其中对传入的Graphics 对象g进行操作完成的。
当我们应用程序的逻辑要对系统界面进行更新时,调用 repaint()
方法来通知AWT线程进行刷新操作。repaint() 方法实际会让
AWT线程去调用另外一个方法,update。update方法在默认情况下会做两件事,一是清除当前区域内容,二是调用其
paint()方法完成实际绘制工作。paint、repaint、update
三个方法关系如图所示:
双缓冲与绘制优化
看看我们框架中的绘制。在拖动时要动态地进行界面更新,主要思路是在鼠标按下及拖动时改变当前作用的显示单元的坐标位置,再调用
repaint() 进行界面的刷新。通过上面的 AWT
绘制介绍,可以看出,我们要做的第一个优化工作就是在DemoPane 中重写
update()
方法,也就是不对当前区域进行清除工作,而直接进行绘制。当然这要求我们要对整个区域进行重绘,否则一般情况下局部重绘会导致不正确的显示。update()方法如下:
public void update(Graphics g) {
paint(g) ;
}
|
在DemoPane中,用backImage表示显示区域的背景图片,以一个数组保存所有加入的显示单元的引用。前面已经说过,对于
DisplayUnit,我们重写了它的 paint
方法,那么在DemoPane的paint()方法中,首先绘制背景,然后对加入的每一个
DisplayUnit 调用其paint() 就可以完成显示。DemoPane的 paint()
方法代码如下:
public void paint(Graphics g) {
g.drawImage(backImage, 0, 0,width, height, this);
for ( int i=0 ; i<nunits ; i++ ) {
getUnit(i).paint(g) ;
}
}
|
但这个方法实现的结果还是会让人感觉到闪烁,其原因是我们现在是直接对代表屏幕对象的Graphics
对象进行操作,而这个操作是比较费时的操作。解决的办法是采用“双缓冲”,即我们创建一个绘制缓冲区,以
bufImage
表示,先将主要的图形元素一个一个地绘制到此缓冲图像上,再将此缓冲图像一次性绘到代表屏幕的
Graphics 对象,即 paint() 方法传入的“g”上。则代码改变为:
public void paint(Graphics g) {
bufImage = prepareBufImage(); // 准备缓冲图像
g.drawImage(bufImage, 0, 0,width, height, this);
}
|
再做进一步的考察我们可以发现,我们在对某一选中的显示单元进行拖动操作时,背景和其它所有的显示单元都没有改变,也就是说只有当前正在操作的显示单元需要重绘。我们将正在进行拖动操作的显示单元称为“活动单元”,那么在拖动时将活动单元与其它所有元素(包括背景和其它显示单元)分开绘制,则可大大提高绘制效率,减少无用操作。在程序中我们通过DemoPane的方法getUnit(index)可以获得相应显示单元,而以activeIndex记录当前的活动单元,则经过再次绘制优化的代码如下:
protected Image prepareBufImage() {
boolean redraw=false ;
if (lastActIndex!=activeIndex) { // lastActIndex代表上次的活动单元
redraw=true ; //与上次活动单元不一致,说明缓冲图像需要全部重绘
lastActIndex = activeIndex ;
}
if (redraw) {
Graphics g = bufImage.getGraphics() ;
g.clearRect(0,0,width,height) ;
g.drawImage(backImage, 0,0,width,height,this) ;
for ( int i=0 ; i<nunits ; i++ ) {
if (i!=activeIndex) {
getUnit(i).paint(g) ;
}
}
}
return bufImage ;
}
public void paint(Graphics g) {
bufImage = prepareBufImage() ;
g.drawImage(bufImage, 0,0,getSize().width,getSize().height,this) ;
DisplayUnit u = getUnit(activeIndex) ;
if (u!=null) {
u.paint(g) ;
}
}
|
绘制缓冲及优化如图所示。
显示单元的选中
对于拖动操作来说,首先要在鼠标按下时判断哪一个显示单元可能被选中,并且进而将其状态置为“activated”以表示它是当前“活动”的显示单元。我们可以这样解决,在处理鼠标动作的mousePressed方法中,先取得鼠标按下时的坐标,再逐一获得显示单元的显示区域,判断是否在其范围之中。但这种做法实现起来代码较显杂乱,且模式不够清晰。在此我们使用另一种模式,即在DisplayUnit中增加一个方法,而在需要时由DemoPane调用此方法来确定某一点是否可能是处于此显示单元的选取区域。inSelectArea方法定义如下:
public boolean inSelectArea(int _x, int _y){
if (!isActivable() )
return false ;
if ( (_x>=x)&&( _x< x+width ) && (_y>=y) && (_y<y+height) )
return true ;
else
return false ;
}
|
此方法让DisplayUnit自己判断给定的坐标(_x,
_y)是否处于其“选取区域”。返回true说明当前点位于选取此显示单元的区域之中。这种实现方法更好地体现了面向对象的思想,即DemoPane调用由此接口获得此单元选中与否的结果,而允许显示单元自己确定一种选中策略,从而给程序增加了灵活性与可扩展性。目前我们对于此方法的判断算法是考察点是否位于显示单元左上角与长、高所确定的矩形当中,如果我们需要使选取区域不局限于规则的正方形时,我们只要对相应显示单元的
inSelectArea方法进行重写就可以了,而DemoPane中代码无需做任何改动。
框架的功能扩展设想
-
此框架只是提供了一个基本的图形界面元素拖动应用解决模板,在此基础上可以很容易地进行许多方面的扩展。
-
将DisplayUnit扩展为“MediaUnit”。目前DisplayUnit可以在激活状态下对显示进行一些改变,而在此基础上只要对鼠标事件的响应增加声音处理就成为了一个多媒体单元。
-
在DemoPane中加入布局策略,这样在加入DisplayUnit时可以更方便地设置其显示位置,但此时要配合使用setDragBounds方法来限定显示单元可移动的区域。
-
多个显示单元同时选取和拖动。这要求DemoPane要具有同时激活多个显示单元,并保存这些激活的显示单元的能力。
下载源程序
源代码下载
关于作者
对本文的评价
|