本文摘自:精通Java Web动态图表编程 一书,版权归作者所有;
在现实工作中,我们希望在Web图表最终输出结果中包含一些图标或者图像。这些图标和图像可以是公司的徽标,也可以是软件的徽标(如微软的Windows操作系统的徽标、JSP Web服务器Tomcat的徽标等),也可能是任意一幅图片。如果这些徽标或者图像都需要我们自己使用Java相关的绘制方法来完成,那么效率实在是太低了。对于一些已经绘制完成的图像,我们可以直接使用Java从文件中读取并显示图像。
Java中有多种方法都可以加载并显示图像文件,如下所述:
Ø 调用Image类的相关方法。
Ø 调用ImageIcon类的相关方法。
Java J2SE 5.0版之前的旧版Java处理格式基本上分为以下三类:
Ø 联合图像专家组(Joint Photographic Experts Group)格式,即JPG/JPEG格式。
Ø 图形交错格式(Graphics Interchange Format格式),即GIF格式。
Ø 可移植的网络图像(Portable Network Graphics)格式,即PNG格式。
J2SE 5.0后的版本就支持了微软的BMP格式图像。BMP图像作为Windows环境下主要的图像格式之一,以其格式简单、适应性强而备受欢迎。因此在J2SE 5.0中,Sun公司也将其纳入Java的支持范围。但BMP文件格式,在处理单色图像和真彩色图像的时候,无论图像数据多么庞大,都不对图像数据进行任何压缩处理,这就使得BMP在存储单色图像或者真彩色图像时,文件体积过于庞大。所以在网络上,一般不采用BMP的文件格式来进行传送。
表2.19列出了java.swing包中的ImageIcon类中的部分构造器。
表2.19 ImageIcon类中的部分构造器
ImageIcon() |
生成尚未初始化的ImageIcon对象,在使用之前必须使用Image对象初始化 |
ImageIcon(String filename) |
以filename指定的文件生成一个ImageIcon对象,filename既可以表示文件的绝对路径,也可以是当前目录的相对路径 |
ImageIcon(String filename, String description) |
同上面构造器一样,从filename指定的文件生成一个ImageIcon对象,第二个参数指定图像的说明。该图像将被MediaTracker类的对象预先装载用来监视图像的加载状况 |
ImageIcon(URL location) |
以URL参数指定的源文件对象创建一个ImageIcon对象。URL类表示统一资源定位器,它指出World Wide Web上的资源。这里的URL可以是因特网上的地址,也可以是本地计算机 |
ImageIcon(URL location, String description) |
URL指定图像文件的位置,description指定文件的名称。这里的URL可以是因特网上的地址,也可以是本地计算机 |
ImageIcon(Image image) |
以Image的对象来创建一个ImageIcon对象 |
ImageIcon(Image image, String description) |
以Image的对象来创建一个ImageIcon对象,第二个参数提供了图像的说明
|
ImageObserver负责处理从URL制定的资源上加载图像数据的过程。当这个图像数据在因特网上,而且图像文件比较大的时候,图像的加载过程就可能有比较长的延迟。为解决此问题,ImageObserver接口还声明了imageUpdate的方法,该方法会在前面请求有关图像的信息可用时自动调用,并在组件上绘制该图像。
我们阐述了两种加载并显示图像的方法,但是采用异步的方式,即程序在后台完成图像的加载和显示,而主进程继续执行。这种情况可能在图像还未被完全加载完毕时造成不完整的显示。在某些情况下,我们需要在图像完全加载完毕后再进行显示,要得到这种功能就需要了解本节介绍的另外一种加载图像的方法,即调用媒体跟踪器(Media Tracker)来加载图像。
把Image类的对象image添加到当前媒体跟踪器要追踪的图像列表中,整型对象id表示该image的标识 |
public void waitForAll() |
初始化加载过程并等待所有被跟踪的图像加载 |
public boolean waitForAll(long ms) |
指定初始化加载过程并等待图像加载的时间,时间用毫秒表示 |
Public boolean checkID(int id) |
检查指定的标识为id的Image对象image是否加载完毕,如果已经加载完毕,则返回真 |
Public boolean checkAll() |
检查所有被跟踪的Image对象image是否加载完毕,如果已经加载完毕,则返回真 |
Public boolean isErrorAny() |
检查所有被跟踪的Image对象image的错误状况,没有错误则返回假 |
Public boolean isErrorID(int id) |
检查指定的标识为id的Image对象image在加载过程中是否发生错误,没有错误则返回假 |
媒体跟踪器是MediaTracker类型的对象,专门用来跟踪图像的加载。这个类在java.awt包中定义,目前只管理图像的载入,以后可能会扩展其功能用来跟踪其他类型的媒体资源的加载。MediaTracker只有一个构造器,需要一个组件引用的参数,这个组件对象就是装入图像的组件。表2.21列出了媒体跟踪器的构造器及其相关方法。
public MediaTracker(Component comp) |
构造器:接收一个用于加载图像的Component对象 |
public void addImage(Image image, int id) |
程序MediaTrackerApplet.java(/chap02/Fig2.11/Fig2.11_02)演示了如何在Applet中调用MediaTracker类以及跟踪图像,运行结果如图2.43所示。
图2.43 用媒体跟踪器(MediaTracker)加载外部图像文件
程序第9行~第11行:
private Image[] images = new Image[3];
private Image bg; // 背景图像
private MediaTracker mt = null;
声明了1个Image类的数组对象images,其中包含3个image对象,1个Image类的对象bg,表示本Applet将要加载的背景图像,还声明了一个MediaTracker类的对象mt。
程序第14行~第27行,在init方法内,对上述变量进行初始化。
程序第16行:
bg = getImage(getCodeBase(), "images/background.gif");
用Applet类getImage方法得到一个Image类的对象引用,并传递给Applet。注意,在getImage的方法内,第2个参数的使用方法:
"images/background.gif"
表示图像文件的位置在Java Applet当前目录下的images子目录中,因此这里表示加载当前目录中的images子目录下的background.gif图像文件。
程序第19行:
mt = new MediaTracker(this);
初始化MediaTracker的实例mt。MediaTracker只有一个构造器,需要一个组件引用的参数,这个组件对象就是装入图像的组件。这里的this表示本容器,也就是在本Applet中创建图像。
程序第20行:
mt.addImage(bg, 0);
使用MediaTracker类的addImage方法,指定要跟踪的图像对象。媒体跟踪器可以同时跟踪多个图像,多个图像可以拥有相同的标识,标识的值决定加载图像的优先级,具有相对较低标识值的图像优先加载。这里,图像bg的标识指定为0。
程序第22行~第25行:
for (int i = 0; i < images.length; i++)
{
images[i] = getImage(getCodeBase(), "images/logo" + (i + 1) + ".jpg");
mt.addImage(images[i], i + 1);
}
使用了一个循环,在用getImage方法获得图像的引用后,将当前目录下的images子目录中logo1.jpg、logo2以及logo3.jpg图像文件,依次添加到当前媒体跟踪器mt要跟踪的图像列表中。标识在这里分别设定为1、2、3。
init方法结束,进入paint方法中。
程序第32行~第40行:
try
{
mt.waitForAll();
}
catch (InterruptedException e)
{
System.out.println(e);
return ;
}
程序在第34行,调用waitForAll方法初始化加载过程,并等待所有被跟踪的图像加载完毕后返回。如果要加载某个特定标识的图像对象,可以调用跟踪器的waitForID方法,并传递一个标识给该方法,开始加载与某个特定标识相关的所有图像对象,并等待所有与该标识相关的图像加载完成后才返回。例如,希望跟踪标识为1的图像,在上面的语句块中,只需要更改程序第34行:
mt.waitForID(1);
如果我们只希望跟踪某一个特定的图像对象,就赋予该媒体跟踪器中所跟踪的每个图像对象都具有一个惟一的标识。这样,一旦出现异常,我们可以通过相应的方法获得出错图像的标识,从而找到发生错误的地方。
这里介绍的waitForID和waitForAll方法将无限期进行等待。因为这些方法可能会抛出异常,所以将其放入一个try…catch语句块中。还有另外一个版本的waitForAll方法,该方法需要一个长整型变量,指定初始化加载过程以及等待图像加载的时间,时间用毫秒表示,由这个长整型的参数决定。但是使用这种方法,我们并不知道在该方法返回时,图像是否真的装载完毕了,还是等待的时间到了;也许图像早就加载完毕了,但因为设置的等待时间还没有结束,因此媒体跟踪器仍旧在等待,这样会严重浪费系统资源。所以我们推荐使用前面介绍的第1和第2种方法,而不要随意使用需要接收一个等待时间参数的waitForAll方法。
如果waitForID和waitForAll方法在无限期的等待中抛出异常,将被程序第36行~第40行的catch语句块所捕获,然后在命令控制台输出一条有关的异常信息,之后,主线程返回,Applet上没有任何显示。
waitForAll方法执行完毕后,虽然所有被跟踪的图像都已经加载完毕,但并不说明这些图像的加载过程中没有任何错误(例如,图像的路径有错、或者图像的文件名有误、或者文件名和路径都正确,但却是Java不支持的图像格式),所以,我们必须明确地检查错误,因此程序第42行~第53行的代码就负责此项检查工作:
if (mt.isErrorAny())
{
for (int i = 0; i <= 4; i++)
{
if (mt.isErrorID(i))
{
g.drawString("图像产生错误状况!文件标志为:" + i, 10, 40 + 20 * i);
}
}
return ;
}
程序第42行,调用了isErrorAny方法,用来检查并确定任何被追踪的图像是否产生了错误。如果方法返回false,则说明没有错误发生;如果图像产生了错误,isErrorAny方法返回真。我们在程序第44行~第49行的代码中,对其进行进一步的检查。
程序第44行~第50行,编写了一段循环代码来检查并确定到底是哪一个被追踪的图像产生了错误。程序第46行,我们将循环语句中的局部变量i作为参数并调用isErrorID方法。如果isErrorID方法返回true,则说明至少在加载一个图像标志与变量i相关的图像发生了错误。因为每个图像都指定了惟一的标识,所以一旦发生错误,可以轻松地找到错误的图像。找到发生错误的图像后,程序将在Applet上绘制出一条与该错误相关的信息。
当确定图像的加载没有错误时,就可以显示这些图像了。
程序第55行:
g.drawImage(bg, 0, 0, this);
我们前面介绍过的Graphics类的dwawImage方法,在Applet上绘制图像bg,如图 2.43①所示。
图像bg(background.gif)的宽度和高度分别是565磅和470磅。为达到将该图作为Applet背景的目的,在调用MediaTrackerApplet.class的HTML文档中,我们指定该Media TrackerApplet.class的宽度和高度正好与图像bg(background.gif)的宽度和高度相同。例如,MediaTrackerApplet.html文档的第4行~第5行所示:
<APPLET CODE = "MediaTrackerApplet.class" WIDTH = "565" HEIGHT = "470">
</APPLET>
程序第57行以及第59行:
g.setFont(new Font("汉真广标", Font.PLAIN, 30));
g.drawString("MediaTracker 的使用", 15, 30);
用“汉真广标”(30磅,普通风格)这种字体在Applet上的坐标(15,30)处绘制文本“MediaTracker的使用”。Java的绘图模式默认为覆盖模式,所以,“MediaTracker的使用”这段文本将显示在已经绘制完成的图像bg上,见图2.43②所示。反之,如果程序第57、59行的位置互换,将无法看到文本“MediaTracker的使用”,因为图像对象bg将它覆盖了。
程序第61行~第64行:
for (int i = 0; i < 3; i++)
{
g.drawImage(images[i], 410, 40+i * 100, this);
}
循环绘制出Image的数组对象images中的每一个图像对象。因为,每个图像对象的绘制横坐标都为410像素(磅),而纵坐标依次递增100像素(磅),所以,显示效果呈现纵向排列。如图2.43③、2.43④、2.43⑤所示。
为了测试MediaTrackerApplet.java程序中的异常捕获功能是否正常工作,我们将images子目录下的background.gif,以及logo2.jpg分别重新改名为bf.gif和logo2bak.jpg。重新启动IE,再调用MediaTrackerApplet.html文档,我们可以看到,程序中的异常捕获功能已经能够正常工作了。因为,每个图像对象都定义了惟一的标识,所以在输出结果上,可以看到标志为“0”和“2”的图像对象出现了错误,而这两个图像文件正是我们刚刚更名前的background.gif以及logo2.jpg(标志分别为0和2,见源程序第20、25行)。出现异常后的程序输出结果,如图2.44所示。
双缓冲技术绘制图像:
读者可能已经发现,在运行前面第2.8.4节所讲述的绘制圆柱体的源程序,以及运行2.10.5节所介绍的绘制平行四边形及立方体源程序的时候,当我们将IE从当前窗口转变成非当前窗口状态,再从非当前窗口恢复到当前窗口状态,有时,某些绘制好的图像会消失,除非我们重新刷新IE窗口,显示才会恢复正常。此外,当我们移动IE窗口或者其他的窗口在IE上移动的时候,图像会有些闪烁。但运行2.11.1节的加载并绘制图像文件的源程序,却没有这种现象。这是怎么一回事呢?这就要涉及到Java Applet中的paint方法的绘图机制了。产生这种现象的主要原因是:
Ø 由于在显示所绘制的图像时,调用了repaint方法。repaint方法被调用时,需要清除整个背景,然后才调用paint方法显示画面。这样,在清除背景和绘制图像的短暂时间间隔内被用户看见的就是闪烁。
Ø 由于paint()方法需要进行复杂的计算,图像中包含着多个图形,不同图形的复杂程度及其所需要的绘制时间不同,因此,图像中的各个像素值不能同时产生,使得图形的生成频率低于显示器的刷新频率,从而造成闪烁。
提示:运行Java编写的动画程序时,发生不连贯或闪烁现象时,可参考下文所介绍的方法加以改善。
下面两种方法可以明显地消除或减弱闪烁:
Ø 重载update方法
当AWT接收到Applet重新绘制的请求时,调用Applet的update方法。默认情况下,update方法清除Applet的背景,然后调用paint方法。重载update方法,就可以将以前在paint方法中的绘图代码包含在update方法中,从而避免每次重新绘制时将整个区域清除。
Ø 双缓冲技术
双缓冲技术在很多动画Applet中被采用。主要原理是创建一幅后台图像,将每一帧画入图像,然后调用drawImage方法,将整个后台图像一次画到屏幕上去。这种方法的优点在于大部分绘制是在后台进行的。将后台绘制的图像一次绘制到屏幕上。在创建后台图像前,首先通过调用createImage方法生成合适的后台缓冲区,然后获得在缓冲区的绘图环境(即Graphics类对象)。
综上所述,改善前面我们写的一些Java Applet源程序的思路是:不直接在paint方法中调用各种绘制方法,而是采用重载update方法及双缓冲技术,生成一个图像的缓冲区,获得该缓冲区中的绘图环境后,将该绘图环境读入内存。paint方法不再负责图像的绘制工作,即paint方法不再装入任何的图像绘制代码。我们在paint方法中,直接调用update方法,在内存缓冲区的绘图环境下进行图像的绘制工作,当所有的图像绘制工作完成后,最后将缓冲区的内容一次性地写入Applet并在Applet窗口中直接显示出来。这种方法很巧妙地解决了图像丢失和闪烁的问题。
现在我们就遵循上面的思路,重新改写第2.8.4和第2.10.5节所介绍的源程序。
首先看改写的2.8.4节介绍的绘制圆柱体的源程序DrawBufferedCylinder.java (/ chap02/Fig2.11/Fig2.11_03)。
DrawBufferedCylinder.java与第2.8.4节所介绍的源程序DrawCylinderApplet.java在绘制圆柱体时的不同之处在于,前者是先绘制在内存缓冲区中的,然后显示在Applet窗口中,而后者是直接在Applet上进行绘制。
程序第16行~第17行:
Image offImage;
Graphics offGraphics;
声明了一个名为offImage的Image对象,表示缓冲区的Image对象。然后声明了一个名为offGraphics的Graphics对象,offGraphics是标识缓冲区的图像绘制环境。这两个系统成员变量在这里仅仅是声明,并没有初始化,我们将在init方法中对其进行初始化。
程序第19行:
int appltWidth = 370, appletHeight = 420;
增加了两个类的数据成员变量appletWidth和appletHeight,分别表示本Applet的宽度和高度。appletWidth和appletHeight的值,与调用本Applet的HTML文档DrawBuffered Cylinder.html中的Applet属性中的Width和Height相同。
程序首先运行第22行~第26行的init方法:
public void init()
{
offImage = createImage(appltWidth, appletHeight);
offGraphics = offImage.getGraphics();
}
使用Component类的createImage方法,创建了一个Image类的实例,并将该实例对象赋给offImage。Component类的createImage方法,返回的是一个后台的(off-screen),用于双缓冲的图像对象。它接收两个整型参数,分别指定该图像对象的宽度和高度,如果该Component是不可显示的部件,则返回的结果为null。
接着,用Image类的getGraphics方法,创建了一个名为offGraphics的Graphics对象,并获得图形环境。后台的(off-screen)缓冲图像将由它来产生,因为这里画的是内存缓冲区图像,所以Applet窗口上不会有显示。
然后,程序执行第68行的paint方法,我们强制其执行程序第73行~第91行的 update方法。
程序第75行~第77行:
offGraphics.setColor(Color.BLACK);
offGraphics.setFont(new Font("汉真广标", Font.PLAIN, 35));
offGraphics.drawString("圆柱体画法", 10, 40);
前面例程都是调用paint方法中的参数Graphics对象g的相关绘制方法,对文本和其他图形进行绘制操作。上面的代码是在缓冲区图像的图形环境中绘制,所以,我们不再调用g的相关方法,而是调用offGraphics的相关方法进行绘图作业。因为offGraphics是缓冲区图像的图形环境对象,所以每当Applet调用offGraphics方法时,绘制工作都将在内存中的缓冲区中进行。这几行代码设置了当前的绘图颜色、字体并绘制一条文本“圆柱体画法”。
程序第80行:
drawCylinder(offGraphics);
调用drawCylinder方法,并将offGraphics作为参数传递给该方法。因此,程序第46行~第66行:
public void drawCylinder(Graphics g)
{
// 计算椭圆的坐标
calculateOvalCoordinate();
// 绘制下方椭圆
g.setColor(fillColor);
g.fillOval(ovalX2, ovalY2, ovalWidth, ovalHeight);
g.setColor(outlineColor);
g.drawOval(ovalX2, ovalY2, ovalWidth, ovalHeight);
// 绘制中间的矩形
g.setColor(fillColor);
g.fillRect(rectX, rectY, rectWidth, rectHeight);
// 绘制上方的椭圆
g.setColor(g.getColor().darker());
g.fillOval(ovalX1, ovalY1, ovalWidth, ovalHeight);
g.setColor(outlineColor);
g.drawOval(ovalX1, ovalY1, ovalWidth, ovalHeight);
}
因为,传递的参数是缓冲区的图形绘制环境对象offGraphics,所以,drawCylinder方法同样是在offGraphics上进行绘制。这里需要注意的是程序第90行,update方法中的最后一条语句:
g.drawImage(offImage, 0, 0, null);
在调用drawImage方法时把null作为第4个参数,这样可以防止drawImage调用update方法。因为图像的所有内容都已装入内存,所以,图像在Applet窗口的显示就一气呵成了。
现在我们检测程序的运行结果。将IE从当前窗口转变成非当前窗口状态,再从非当前窗口恢复到当前窗口状态,我们会发现,绘制好的图像不再消失了,也不需要重新刷新IE窗口了。