• 1. 第9章 Java在多媒体中的应用 9.1 利用AWT绘图 9.2 Graphics类的使用 9.3 Font类的使用 9.4 图像处理 9.5 动画图像处理
  • 2. 9.1 利用AWT绘图 这一节中我们主要介绍如何使用Java语言提供的AWT包绘图。java.awt包中提供了用于绘图的API,我们通常称之为2D API。不要以为只有设计绘图程序或游戏软件才会用到Java 2D,其实Java 2D的用途可能远比你想象的更广泛。
  • 3. 其实,只要你的程序有GUI,就很可能会用到Java 2D。因为AWT和Swing的组件常常无法完全适合我们,这个时候自己绘制一部分的GUI就有绝对的必要。甚至,我们还可以用Java 2D来绘制自己的组件。实际上,AWT和Swing组件都是透过Java 2D来进行绘制的。
  • 4. Java 2D API增强了AWT的图形、文本和图像功能,可以开发更为强大的用户接口和新型的Java应用程序。除了更为强大的图形、字体和图像API外,Java 2D API还改进了颜色的定义与复合及对任意几何形状和文本的选中检测,并为打印机和显示设备提供了统一的绘制模式。Java 2D API还可以创建高级图形库,并可创建图像和图形文件读/写过滤器。当与Java媒体框架 (JMF) 和其他Java媒体应用程序配合使用时,Java 2D API还可用来创建和显示动画和其他多媒体演示稿。
  • 5. 到底Java 2D API有多强大?这一点我们可以通过SUN JDK包中提供的例程来了解。在命令行窗口下输入: c:\>cd \jdk1.2\demo\jfc\Java2D c:\jdk1.2\demo\jfc\Java2D>java -classpath Java2Demo.jar Java2Demo 或 c:\jdk1.2\demo\jfc\Java2D>appletviewer java2demo.html 看到了吗?Java 2D神奇的威力!
  • 6. 下面我们来学习如何使用Java 2D绘图。上面看到的例子虽然功能十分强大,但是它的实现非常复杂。下面,我们先从简单一点的入手。 从java.awt.Component类(所有窗口对象的基类)继承的类提供了一个名为paint()的方法,在需要重新绘制组件时,可调用该方法。 paint()方法只有一个参数,该参数是Graphics类的实例。如果在某个继承了Component的类中覆盖了该方法,那么就可以使用该方法来控制在控制区域着何种颜色。例如,下面的类创建了一个带有蓝背景的面板。
  • 7. 例9.1 BluePanel.java import java.awt.*; class BluePanel extends Panel { public static void main(String[] args) { Frame f = new Frame(); BluePanel p = new BluePanel(); f.add(p); f.setSize(300,100); f.setVisible(true);}
  • 8. // Invoked when the panel needs to be repainted public void paint(Graphics g) { // Get the rectangle that represents the viewable area // of the panel Rectangle rect = g.getClipBounds(); // Set the context to paint in a pre-defined color g.setColor(Color.blue); // Fill the rectangle with the current color g.fillRect(rect.x, rect.y, rect.width, rect.height);} } 程序运行结果如图9.1所示。
  • 9. 图 9.1
  • 10. 9.2 Graphics类的使用 java.awt中提供了一系列的类用于绘制图形。其中,Color类包含了编辑颜色的方法和常量;Font类包含了编辑字体的方法和常量;FontMetrics类包含了获取字体信息的方法;Polygon类包含了创建多边形的方法;Toolkit类提供了从系统获得图形信息的方法,例如可显示的字体集和屏幕分辨率等等;Graphics类包含了绘制字符串、线条以及各种几何图形的方法。
  • 11. Graphics是一个抽象类,其作用是定义一个真正的工具,用来接受图形操作。在该类中,有47个公用方法,可以用作显示图像和文本、绘制和填充形状、剪贴图像操作等等。 9.2.1 绘制字符串、字符和字节 用于绘制字符串、字符和字节的方法分别为 ● public abstract void drawString(String string, int x, int y)在坐标(x,y)处以当前字体和颜色绘制指定的字符串string。
  • 12. ● public void drawChars(char[] chars, int offset, int number, int x, int y) 在坐标(x,y)处以当前字体和颜色绘制指定的一系列字符。chars为要绘制的字符组,offset为位置的坐标,number为要绘制的元素个数。 ● public void drawBytes(byte[] bytes, int offset, int number, int x, int y) 在坐标(x,y)处以当前字体和颜色绘制指定的一系列字节。bytes为要绘制的字节数组,offset为位置的坐标,number为要绘制的元素个数。
  • 13. 下面的例子展示了如何使用了drawString(),drawChars()和drawBytes()三个方法: 例9.2 DrawSCB.java import java.awt.*; public class DrawSCB extends Frame { String s = "Using drawString!"; char[] c = {'c', 'h', 'a', 'r', 's', ' ', '8'}; byte[] b = {'b', 'y', 't', 'e', 1, 2, 3};
  • 14. public static void main(String[] args) { DrawSCB frame = new DrawSCB(); frame.setSize(300, 100); frame.setVisible(true); } public void paint(Graphics g) { g.drawString(s, 100, 40); g.drawChars(c, 0, 7, 100, 65); g.drawBytes(b, 0, 5, 100, 90); } } 程序运行结果如图9.2所示。
  • 15. 图 9.2
  • 16. 9.2.2 颜色控制 Color类定义了颜色常量和方法。每种颜色都是从RGB(红/绿/蓝)值创建出来的。一个RGB值有三部分,都是从0~255的整数值,分别代表着三种颜色的含量。因此,实际上我们可以使用256×256×256种颜色,即224种颜色。这就是我们常说的24位真彩色。但不是任何计算机都可以显示所有的颜色,就目前来说,大部分计算机都可以显示24位甚至超过24位的彩色。显然,我们很难记住一种颜色的RGB值,因而,Color类将一些最常用的颜色预定义为常量以方便我们使用。表9.1列出了预定义的颜色常量。
  • 17. 表9.1 Color类中定义的颜色常量 颜色常量 颜色 RGB值 public final static Color orange 橙色 255,200,0 public final static Color pink 粉红色 255,175,175 public final static Color cyan 青色 0,255,255 public final static Color magenta 火红色 255,0,255 public final static Color yellow 黄色 255,255,0 public final static Color black 黑色 0,0,0 public final static Color white 白色 255,255,255
  • 18. public final static Color gray 灰色 128,128,128 public final static Color lightGray 浅灰色 192,192,192 public final static Color darkGray 深灰色 64,64,64 public final static Color red 红色 255,0,0 public final static Color green 绿色 0,255,0 public final static Color blue 蓝色 0,0,255
  • 19. Color类中常用的一些方法如下: ● public Color(int r, int g, int b):创建指定RGB值的颜色。 ● public int getRed():返回红色含量的值。 ● public int getBlue():返回蓝色含量的值。 ● public int getGreen():返回绿色含量的值。 Graphics类与Color类相关的常用方法有: ● public abstact Color getColor():返回图形上下文的当前颜色。 ● public abstract void setColor(Color c):设置图形上下文的当前颜色。
  • 20. 下面来看一个颜色控制的实例。 例9.3 ShowColor.java import java.awt.*; public class ShowColor extends Frame { int red, green, blue; public static void main(String[] args) { ShowColor frame = new ShowColor(); frame.setSize(300, 100); frame.setVisible(true); } public ShowColor()
  • 21. { red = 200; green = 100; blue = 0; } public void paint(Graphics g) { g.setColor(new Color(red, green, blue)); g.drawString("Draw color string.", 50, 40); Color color = g.getColor();
  • 22. g.drawString("Red:"+color.getRed(), 10, 70); g.drawString("Green:"+color.getGreen(), 100, 70); g.drawString("Blue:"+color.getBlue(), 200, 70); } } 程序运行结果如图9.3所示。
  • 23. 图 9.3
  • 24. 9.2.3 绘制几何图形 Graphics类中用于绘制几何图形的方法如下: ● public abstract void drawLine(int x1, int y1, int x2, int y2) 在点(x1,y1)和(x2,y2)之间用当前颜色绘制线段。 ● public void drawRect(int x, int y, int width, int height) 以点(x,y)为左上角坐标,width和height为宽度和高度,用当前颜色画矩形。 ● public abstact void fillRect(int x, int y, int width, int height)
  • 25. 以点(x,y)为左上角坐标,width和height为宽度和高度,用当前颜色画一个实心的矩形。 ● public abstact void clearRect(int x, int y, int width, int height) 以点(x,y)为左上角坐标,width和height为宽度和高度,用当前背景颜色画一个实心的矩形。此方法用于清除某个矩形显示区域。 ● public abstact void drawRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) 以点(x,y)为左上角坐标,width和height为宽度和高度,用当前颜色画圆角矩形。
  • 26. ● public abstact void fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) 以点(x,y)为左上角坐标,width和height为宽度和高度,用当前颜色画一个实心的圆角矩形。 ● public void draw3Drect(int x, int y, int width, int height, boolean b) 用指定的width和height,以当前颜色画一个三维矩形。矩形的左上角坐标为(x,y)。当b为true时,矩形为凸的;b为false时,矩形为凹的。
  • 27. ● public void fill3Drect(int x, int y, int width, int height, boolean b) 用指定的width和height,以当前颜色画一个填充的三维矩形。矩形的左上角坐标为(x,y)。当b为true时,矩形为凸的;b为false时,矩形为凹的。 ● public abstract void drawOval(int x, int y, int width, int height) 用指定的width和height,以当前颜色画椭圆。外切矩形的左上角坐标为(x,y)。 ● public abstract void fillOval(int x, int y, int width, int height) 用指定的width和height,以当前颜色画实心椭圆。外切矩形的左上角坐标为(x,y)。
  • 28. ● public abstract void drawArc(int x, int y, int width, int height, int startAngle, int arcAngle) 参考外切矩形的左上角坐标(x,y),用指定的width和height,以当前颜色绘制一段弧。弧段从起始角startAngle开始一直到张角arcAngle。 ● public abstract void fillArc(int x, int y, int width, int height, int startAngle, int arcAngle) 参考外切矩形的左上角坐标(x,y),用指定的width和height,以当前颜色绘制一段实心的弧。弧段从起始角startAngle开始一直到张角arcAngle。
  • 29. ● public abstract void drawPolygon(int[] xPoints, int[] yPoints, int points) 以当前颜色画一个具有points个顶点的多边形。xPoint和yPoint数组分别指定了每个顶点的x和y坐标。 ● public void drawPolygon(Polygon p) 以当前颜色画指定的多边形。
  • 30. ● public abstract void fillPolygon(int[] xPoints, int[], yPoints, int points) 以当前颜色画一个具有points个顶点的填充多边形。xPoint和yPoint数组分别指定了每个顶点的x和y坐标。 下面我们通过一个例子来学习这些方法。
  • 31. 例9.4 DrawDemo.java import java.awt.*; public class DrawDemo extends Frame { public static void main(String[] args) { DrawDemo frame = new DrawDemo(); frame.setSize(400, 500); frame.setVisible(true); } public void paint(Graphics g)
  • 32. { // draw a line g.drawLine(10, 30, 200, 40); // draw a rectangle g.drawRect(10, 50, 100, 50); // draw a filled rectangle g.fillRect(150, 50, 100, 50); // draw a rounded rectangle g.drawRoundRect(10, 120, 50, 50, 10, 20); // draw a filled rounded rectangle g.fillRoundRect(80, 120, 50, 50, 40, 20); // draw a ellipse
  • 33. g.drawRoundRect(150, 130, 80, 20, 70, 70); // draw a filled square g.drawRoundRect(250, 120, 50, 50, 0, 0); // draw a circle g.drawRoundRect(330, 120, 50, 50, 50, 50); g.setColor(Color.yellow); // draw a raised 3D rectangle g.draw3DRect(10, 190, 50, 50, true); // draw a sunk 3D rectangle g.draw3DRect(100, 190, 50, 50, false);
  • 34. // draw a filled raised 3D rectangle g.fill3DRect(200, 190, 50, 50, true); // draw a filled sunk 3D rectangle g.fill3DRect(300, 190, 50, 50, false); g.setColor(Color.black); // draw an oval g.drawOval(10, 260, 70, 50); // draw an filled oval g.fillOval(200, 260, 50, 70); // draw an arc g.drawArc(10, 350, 60, 60, 0, 180);
  • 35. // draw solid arc g.fillArc(100, 350, 50, 60, 0, 270); g.fillArc(200, 350, 50, 60, 0, -110); g.fillArc(300, 350, 40, 60, 0, -360); // draw a polygon int[] xPoints1 = {10, 30, 40, 20, 10, 5, 10}; int[] yPoints1 = {430, 430, 440, 460, 460, 440, 430}; g.drawPolygon(xPoints1, yPoints1, 7);
  • 36. // draw a filled polygon int[] xPoints2 = {100, 120, 130, 110, 100, 95, 90, 100}; int[] yPoints2 = {430, 430, 440, 460, 460, 440, 420, 430}; g.fillPolygon(xPoints2, yPoints2, 8); } }   程序运行结果如图9.4所示。
  • 37. 图 9.4
  • 38. 9.2.4 屏幕操作 本节介绍一种实现屏幕操作的Graphics方法copyArea()。copyArea()方法用于复制屏幕的一个区域,并将它放置在屏幕的另一个位置上。copyArea方法的定义如下:public abstract void copyArea(int x, int y, int width, int height, int dx, int dy)其中,参数x和y指定了复制区域的左上角坐标;width和height为区域的宽和高;dx和dy指定了与点(x,y)的水平和垂直偏移量,复制的区域放置在该偏移量相对于(x,y)的位置上。
  • 39. 例9.5 CopyAreaDemo.java import java.awt.*; public class CopyAreaDemo extends Frame { public static void main(String[] args) { CopyAreaDemo frame = new CopyAreaDemo(); frame.setSize(300, 200); frame.setVisible(true); } public void paint(Graphics g)
  • 40. { g.drawRect(20, 30, 50, 50); g.fillOval(50, 100, 50, 70); g.copyArea(20, 30, 100, 170, 150, 20);} } 程序运行结果如图9.5所示。
  • 41. 图 9.5
  • 42. 9.2.5 绘图模式 绘图模式(paint mode)用于描述如何绘图。默认的绘图模式是覆盖模式(overwrite),当一个图形画在另一个图形上面时,旧的图形将被覆盖,看到的将只是新的图形。另外,还有一种异或绘图模式(XOR),使用这种模式可以看到互相重叠的所有图形。使用异或绘图模式可以通过调用Graphics的方法: public abstract void setXORMode(Color c)
  • 43. 来实现。该方法的参数为一个Color对象。这个颜色称作XORMode颜色。由于XOR绘图模式实际上是对新旧两种颜色的二进制值做异或操作,所以当重叠的部分颜色相同时,将恢复为背景颜色。这时候可以通过设置XORMode颜色,指定用某颜色来替代。例如: 例9.6 XORModeDemo.java import java.awt.*; public class XORModeDemo extends Frame { public static void main(String[] args) { XORModeDemo frame = new XORModeDemo();
  • 44. frame.setSize(300, 100); frame.setVisible(true); } public void paint(Graphics g) { // draw a pink oval g.setColor(Color.pink); g.fillOval(20, 30, 100, 50); // draw a yellow rectangle over part of the oval g.setColor(Color.yellow); g.fillRect(100, 30, 100, 50); // draw an orange rectangle g.setColor(Color.orange);
  • 45. g.fillOval(190, 30, 80, 50); // set XOR mode to yellow g.setXORMode(Color.yellow); g.fillOval(180, 45, 60, 20); // draw a blue arc g.setColor(Color.blue); g.fillArc(150, 40, 20, 20, 0, 360); // draw a red square g.setColor(Color.red); g.fillRect(120, 45, 20, 20);} }
  • 46. 程序绘制了六种不同的形状,前三种为覆盖模式,后三种为异或模式。大家可以通过运行结果中图形重叠部分看出两种不同绘图模式的效果。程序运行结果如图9.6所示。
  • 47. 图 9.6
  • 48. 9.3 Font类的使用 Java 2D具有复杂文本的输出能力。Java 2D和一个重新设计的字体引擎支持使用属性集对字符串的单个字符进行操作。 9.3.1 字体 字体是一套具有一个点尺寸和外观的字符类型集合,例如,所有10点Helvetica英文字符和符号组成一个字体。文本所使用的字体定义特定的外观、大小和式样(黑体、斜体或者普通体)。
  • 49. 字体如何定义特定外观呢?字体是从字形(glyphs)创建的,一个字形是一个位映像图像,它定义字体中的字符和符号的外观。同一字体家族的字体都有相似的外观,因为他们使用同一个字形创建。同样的,不同的字体家族使用不同的字形得到相互区分的外观。 一个字体家族不但由具有相似外观的字体组成,还包括不同的大小和式样。Helvetica 10 point黑体和Helvetica 12 point斜体是同一家族中的两个不同字体,而Times Roman 8 point黑体和Times Roman 10 point普通体是另一个家族的两个不同字体。
  • 50. 为了使用一个字体,需要创建一个Font对象,而为了做到这个,则需要知道系统中有什么字体可用以及它们的名字。字体有逻辑名、家族名和字体名。 逻辑名是被映射到平台上,可用的特定字体的名字。调用java.awt.Font.getName类可以得到一个Font对象的逻辑名。
  • 51. 家族名是字体家族的名字,它通过不同的外观决定排版图案,例如,Helvetica或者Times New Roman。要得到一个Font对象的家族名,须调用java.awt.Font.getFamily类。 字体名代表家族中的特定字体,例如Helvetica Bold。要得到一个Font对象的字体名,须调用java.awt.Font.getFontName类。要决定系统上哪些字体可用,须调用java.awt.GraphicsEnvironment.getAllFonts类。
  • 52. 9.3.2 创建和派生字体 创建一个字体的最简单的方法是指定字体名、大小和式样。一旦你有一个Font对象,你就可以通过调用Font.deriveFont方法在存在的字体上派生任意个新Font对象,并指定新的大小、样式、变换(位置、倾斜、缩放或者旋转)或者属性映射。例如 Font boldFont = new Font("Helvetica", Font.BOLD, 12); Font italicDerived = boldFont.deriveFont(Font.ITALIC, 12); Font plainDerived = boldFont.deriveFont(Font.PLAIN, 14);
  • 53. 一旦你有一个字体,你就可以用它创建一个TextLayout对象并绘制艺术字。java.awt.Font.TextLayout类可以让你使用字符、字体和属性集创建艺术字。一旦被创建,TextLayout对象就不可编辑,但是它的方法可以让你访问布局、字体、脱字符,选择和点击测试信息。 下面的代码使用Font,TextLayout和 FontRenderContext 对象绘制了一个简单的文本,使用的是24 point Times黑体。
  • 54. 例9.7 TimesB.java import java.awt.*; import java.awt.event.*; import java.awt.geom.*; import java.awt.Font.*; public class TimesB extends Canvas { private Image img; publicc TimesB() {setBackground(Color.white); } public void paint(Graphics g)
  • 55. { Graphics2D g2; g2 = (Graphics2D)g; g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); ontRenderContext frc = g2.getFontRenderContext(); Font f = new Font("Times",Font.BOLD, 24); String s = new String("24 Point Times Bold"); TextLayout tl = new TextLayout(s, f, frc); Dimension theSize= getSize();
  • 56. g2.setColor(Color.green); tl.draw(g2, theSize.width/30, theSize.height/2); } public static void main(String s[]) { WindowListener l = new WindowAdapter() {public void windowClosing(WindowEvent e) {System.exit(0);} public void windowClosed(WindowEvent e) {System.exit(0);} };
  • 57. Frame f = new Frame("2D Text"); f.addWindowListener(l); f.add("Center", new TimesB()); f.pack(); f.setSize(new Dimension(400, 300)); f.show(); } } 程序运行结果如图9.7所示。
  • 58. 图 9.7
  • 59. 程序中,java.awt.Font代表系统中可用字体的一个实例;java.awt.TextLayout代表不可变的艺术字数据;java.awt.Font.FontRenderContext包含绘制文本时需要的正确测量和定位的信息。
  • 60. 9.4 图 像 处 理 正如上一节所介绍的,Graphics类中确实提供了不少绘制图形的方法,但如果用它们在Applet运行过程中实时地绘制一幅较复杂的图形,就好比是在用斧头和木块去制造航天飞机。因此,对于复杂图形,一般都事先用专用的绘图软件将其绘制好,或者是用其他截取图像的工具(如扫描仪、视效卡等)获取图像的数据信息,再将它们按一定的格式存入图像文件中。程序运行时,只要找到图像文件存贮的位置,将它装载到内存里,然后在适当的时机将它显示在屏幕上就可以了。
  • 61. 9.4.1 加载和显示图像 在AWT中,java.awt.image类用于描述图像,它通过传递一个Image类对象的引用给Graphics.drawImage(Image, int, int, ImageObserver)方法,就可以将图像在画布(Canvas)或是其他可视组件中显示出来。
  • 62. Java目前所支持的图像文件格式只有两种,分别是GIF和JPEG格式(带有 .GIF、.JPG、.JPEG后缀名的文件)。因此,若图像文件是其他格式,就须先将它们转换为这两种格式。 java.awt.image是一个抽象类,它定义的方法提供对图像信息的访问。下面通过一个例子来看看如何利用Image类来显示一幅图像。
  • 63. 例9.8 ImageTestApplication.java import java.awt.*; import java.awt.event.*; public class ImageTestApplication extends Frame { Insets insets; Image im; static public void main(String args[]) { ImageTestApplication app = new ImageTestApplication();
  • 64. app.show(); } public ImageTestApplication() { super("Image Test"); im = Toolkit.getDefaultToolkit().getImage("tiger.gif"); addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent event) { dispose(); System.exit(0);} }); }
  • 65. public void addNotify() { super.addNotify(); // peer is created here insets = getInsets(); setBounds(100, 100, 217 + insets.left, 321 + insets.top); } public void paint(Graphics g) { g.drawImage(im, insets.left, insets.top, this); System.out.println("drawing image..."); System.out.println(g.drawImage(im, insets.left, insets.top, this)); } }
  • 66. 在应用程序中加载图像必须调用Toolkit类的静态方法getImage(),该方法返回一个指定图像文件的Image对象描述,然后在paint()方法中调用Graphics类的drawImage()方法,就可以将图片显示在当前容器中。
  • 67. 需要注意的是addNotify()方法,覆盖这个方法是为了得到框架窗口空白区域的引用,并用它来设置框架窗口的大小。这样做是因为框架窗口的左上角(0,0)坐标是从标题栏开始计算的,如果将图从(0,0)开始画,空白区域以外(即标题栏覆盖的部分)将会被裁减,所以必须从坐标(insets.left,insets.top)的位置开始画图。程序的运行结果如图9.8所示。
  • 68. 图 9.8
  • 69. 在上面的例子中,查看控制台会发现,paint()方法被调用了很多次,这是因为getImage()方法是启动一个线程来加载图像的,所以paint()方法被调用的时候不一定已经载入了整张图片,每次只绘出已经加载的部分。
  • 70. Java这样采用线程的做法虽然会提高性能,但是也为编程带来了一些问题。例如,上例中的setBounds()方法中的尺寸是硬编码(直接写入数值)的,这种方法缺乏通用性,是明确不被推荐的做法。较好的方法是直接取图像的尺寸,通过调用Image.getWidth()和Image.getHeight()方法可以做到。因为在图像被完全加载以前,它们的返回值都是-1,所以要等到图像加载完才能调用它们。
  • 71. 如何知道图像有没有被加载完呢?AWT包为此提供了MediaTracker类用于监控图像的加载过程。使用MediaTracker类分为三步: (1) 创建MediaTracker对象。 (2) 使用MediaTracker.addImage()指明要监控的图像对象。 (3) 创建try/catch块,等待和指定与ID相关的图像被完全加载。 现在采用MediaTracker类来改写上面的例子。  
  • 72. 例9.9 ImageTestApplication.java import java.awt.*; import java.awt.event.*; public class ImageTestApplication extends Frame { Insets insets; Image im; int width, height; static public void main(String args[]) { ImageTestApplication app = new ImageTestApplication();
  • 73. app.show(); } public ImageTestApplication() { super("Image Test"); MediaTracker tracker = new MediaTracker(this); im = Toolkit.getDefaultToolkit().getImage("tiger.gif"); tracker.addImage(im, 0); try { tracker.waitForID(0); } catch (InterruptedException e) { e.printStackTrace(); } width = im.getWidth(this); height = im.getHeight(this);
  • 74. addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent event) { dispose(); System.exit(0);} }); } public void addNotify() { super.addNotify(); // peer is created here insets = getInsets(); setBounds(100, 100, width + insets.left, height + insets.top); }
  • 75. public void paint(Graphics g) { g.drawImage(im, insets.left, insets.top, this); System.out.println("drawing image..."); System.out.println(g.drawImage(im,insets.left, insets.top, this)); } } 再来看看控制台,可以看到paint()方法只被执行了一次。这说明图像是被完全加载以后才调用paint()方法显示的。
  • 76. 9.4.2 图像生成 AWT除了提供用于描述图像的java.awt.image类外,还提供了用于图像处理的java.awt.image包,这个包的所有类几乎都和生产和消费图像有关。图像生产者负责产生图像的位,而图像消费者用于接收图像的位。 注:用于描述图像的是java.awt包中的Image类,它为图像提供引用,而java.awt.image包中的类则用于图像处理,不要将它们混淆。
  • 77. 在java.awt.image包中,提供了图像源生产者接口ImageProducer,以及用于像素抓取和图像过滤器的消费者接口ImageConsumer。 实际上,和图像相关联的位并不存在java.awt.image中,而是每个图像都和一个ImageProducer接口相关联,这个ImageProducer真正负责生产图像的位。
  • 78. AWT组件除了可以显示图像,还可以创建图像。要生成一幅图像就必须调用AWT组件类提供的方法createImage(ImageProducer)或CreateImage(int width, int height)。第一个方法通过给定一个提供图像位的ImageProducer参数来创建图像;第二个方法则通过指定图像大小来生成图像。
  • 79. 此外,java.awt.Toolkit类也拥有创建图像的能力。它提供了三种创建图像的方法: ① createImage(ImageProducer) ② createImage(byte[] bits) ③ createImage(byte[] bits, int offset, int length) 和AWT一样,Toolkit类在创建图像时,也可以通过给定一个ImageProducer参数来实现。另外,它还提供了两种方法,从一个byte数组创建图像,该方法是使用位数组创建图像,即我们常用的内存图像。
  • 80. 9.4.3 图像处理 在AWT中,提供了大量的方法支持图像处理,特别是在java.awt.image包中,为我们提供了一些十分有用的图像过滤器。这一节将向大家介绍如何使用这些奇妙的图像过滤器。 前面已经提到,生产者接口ImageProducer用于产生图像位并把它们传递给图像消费者。对于每个Image对象,都有一个和它对应的图像生产者,其作用是用来重构图像和生产随时需要的图像位。
  • 81. 图像生产者保持一个图像消费者列表,他们得到的图像数据都来自图像生产者。ImageProducer接口提供用来在列表中增加或删除图像消费者的方法,而且同时还用于判断一个消费者是否已经向生产者注册。在java.awt.image软件包中,有两个类实现ImageProducer接口,它们是:FilteredImageSource类和MemoryImageSource类。
  • 82. 图像消费者ImageConsumer接口用于从图像生产者接收图像数据,java.awt.image包中也提供了两个类实现ImageConsumer接口,它们是:ImageFilter类和PixelGrabber类。 下面我们来介绍如何使用java.awt.image包中的图像处理工具 —— 图像过滤器。 java.awt.image 包提供了下面几种图像过滤器: ● CorpImageFilter:从一副已知图像中裁剪出一个特殊的矩形,要裁剪的矩形形状由过滤器的构造器来决定。
  • 83. ● ReplicateScaleFilter:使用一个简单的算法缩放图像,例如复制图像数据的行或列进行放大;删除图像数据的行或列进行缩小。 ● AreaAveragingScaleFilter:是ReplicateScaleFilter的一个扩展,它在缩放时采用了一个比较高级的算法。两者的使用方式相同,但AreaAveragingSealeFilter缩放图像的质量要好些。
  • 84. ● RGBImageFilter:是一类抽象过滤器,其作用是传输专用像素到它在RGB颜色模式中的扩展,用于实现自定义图像过滤器。 这里我们主要讨论CorpImageFilter和RGBImageFilter的使用。我们先来看一个使用CropImageFilter实现图像裁减的例子。
  • 85. 例9.10 CropTest.java import java.awt.*; import java.awt.image.*; import java.net.*; public class CropTest extends Frame { private Image im; private Image cropped; private Insets insets; private int width, height; public static void main(String[] agrs)
  • 86. { CropTest frame = new CropTest(); frame.setVisible(true); } public CropTest() { MediaTracker mt = new MediaTracker(this); im = Toolkit.getDefaultToolkit().getImage("pic.jpg"); mt.addImage(im, 0); try { mt.waitForID(0);} catch(Exception e) {e.printStackTrace(); } width = im.getWidth(this); height = im.getHeight(this);
  • 87. ImageFilter filter = new CropImageFilter(110,5,100,100); FilteredImageSource fis = new FilteredImageSource(im.getSource(), filter); cropped = createImage(fis); } public void addNotify() { super.addNotify(); // peer is created here insets = getInsets(); setBounds(100, 100, width+insets.left+120, height+insets.top); }
  • 88. public void paint(Graphics g) { g.drawImage(im, insets.left, insets.top, this); g.drawImage(cropped, width+insets.left+20, insets.top, this);} } 上面的程序中,通过创建一个CropImageFilter的对象来指定要裁减的区域为左上角是(100,5),宽和高都是100的一个矩形区域。
  • 89. 接下来构造一个FilteredImageSource对象,并通过Image.getSource()方法得到原图的图像生产者的引用,传递给FilteredImageSource的对象。最后,调用Component.createImage()方法生成裁减后的图像,并将FilteredImageSource对象作为参数传递给该方法。这是因为FilteredImageSource实现了ImageProducer接口,所以它可以作为createImage()方法的合法参数。程序的运行结果如图9.9所示。
  • 90. 图 9.9
  • 91. RGBImageFilter是一个抽象类,它定义了如下一个抽象方法: int filterRGB(int x, int y, int rgb) 改方法在缺省的RGB颜色模式中,向filterRGB()方法传递像素的位置和RGB颜色的一个整数表示,用于检验像素的RGB表示,返回相同的RGB整数表示或被修改的像素颜色表示。下面是一个使用RGBImageFilter实现自定义的溶解过滤器DissolveFilter的例子。
  • 92. 例9.11 RGBImageFilter.java import java.net.URL; import java.awt.*; import java.awt.event.*; import java.awt.image.*; public class DissolveFilterTest extends Frame { Image orig, dissolved; public DissolveFilterTest() { super("Dissolve Filter");
  • 93. MediaTracker mt = new MediaTracker(this); URL url = getClass().getResource("tiger.gif"); try { orig = createImage((ImageProducer)url.getContent()); mt.addImage(orig, 0); mt.waitForID(0);} catch(Exception e) { e.printStackTrace(); } FilteredImageSource fis = new FilteredImageSource( orig.getSource(),
  • 94. new DissolveFilter(50)); dissolved = createImage(fis); } public void update(Graphics g) { paint(g); } public static void main(String args[]) { final Frame f = new DissolveFilterTest(); f.setBounds(100,100,730, 380); f.setVisible(true); f.addWindowListener(new WindowAdapter() {
  • 95. public void windowClosing(WindowEvent e) { f.dispose(); System.exit(0);}}); } public void paint(Graphics g) { Insets i = getInsets(); int ow = orig.getWidth(this); // ow = Original Width g.drawImage(orig, i.left, i.top, this); g.drawImage(dissolved, i.left + ow + 20, i.top, this);}
  • 96. } class DissolveFilter extends RGBImageFilter { private int alpha; public DissolveFilter() { this(0); } public DissolveFilter(int alpha) { canFilterIndexColorModel = true; if(alpha < 0 && alpha > 255) throw new IllegalArgumentException("bad alpha");
  • 97. this.alpha = alpha; } public int filterRGB(int x, int y, int rgb) { DirectColorModel cm = (DirectColorModel)ColorModel.getRGBdefault(); int alpha = cm.getAlpha(rgb); int red = cm.getRed (rgb);
  • 98. int green = cm.getGreen(rgb); int blue = cm.getBlue (rgb); alpha = alpha == 0 ? 0 : this.alpha; return alpha << 24 | red << 16 | green << 8 | blue; } } 这个程序实现了一个溶解过滤器DissolveFilter,它是通过扩展RGBImageFilter得到的。
  • 99. 在DissolveFilter构造器中,接收被过滤图像中每个颜色的直接颜色模式中的alpha值(直接颜色模式即直接指明颜色值比特位)。alpha值的取值范围是0~255。alpha值为0表示该图像是完全透明的,而值255则表示图像完全不透明(即原图)。将载入的图像中的每个点通过DissolveFilter.filterRGB( )处理后,重构图像,就得到了如图9.10所示的运行结果。
  • 100. 图 9.10
  • 101. 9.5 动画图像处理 9.5.1 使用线程设计动画 在Java中,实现动画有很多种办法,但它们实现的基本原理是一样的,即在屏幕上画出一系列的帧来造成运动的视觉效果。在此,我们先构造一个程序的框架,再慢慢扩展,使之功能比较齐备。
  • 102. 为了每秒钟多次更新屏幕,必须创建一个线程来实现动画的循环,这个循环要跟踪当前帧并响应周期性的屏幕更新要求。实现线程的方法有两种:创建一个类Thread的派生类,或在当前类上实现一个Runnable接口。一个较容易犯的错误是将动画循环放在paint()中,这样就会占据主AWT线程,而主线程是用于负责所有的绘图和事件处理的,这将导致界面对事件响应迟钝。下面我们来看一个使用线程实现动画的例子。
  • 103. 例9.12 Animation.java import java.awt.*; public class Animation extends Frame implements Runnable { int totalImages = 19; int currentImage = 0; int delay = 50; int x; Image[] spin; Thread animator;
  • 104. public static void main(String[] args) { Animation frame = new Animation(); frame.setSize(500, 150); frame.setBackground(Color.yellow); frame.setVisible(true); } public Animation() { spin = new Image[totalImages]; Toolkit defaultTk = Toolkit.getDefaultToolkit(); MediaTracker tracker = new MediaTracker(this); for (int i = 0; i < totalImages; i++)
  • 105. { spin[i] = defaultTk.getImage("spin"+i+".gif"); tracker.addImage(spin[i], i); try { tracker.waitForID(i);} catch (InterruptedException e) { e.printStackTrace(); } } x = 0; animator = new Thread(this); animator.start(); }
  • 106. public void run() { while (Thread.currentThread() == animator) { repaint(); try { Thread.sleep(delay);} catch (InterruptedException e) { e.printStackTrace(); } } }
  • 107. public void paint(Graphics g) { g.drawImage(spin[currentImage], 10+x, 30, this); x = (x+5) % 400; currentImage = ++currentImage % totalImages;} } 此程序的原理是利用一系列的图片的更替来实现动画,run()方法中不断的调用repaint()方法,repaint()方法又会导致paint()方法被调用,每次调用paint()方法都会绘制一副图片,并且将currentImage指针移动到下一幅图片的位置,横坐标也随之平移。这样的效果就是出现一个不断移动的小人,如图9.11所示。
  • 108. 图 9.11
  • 109. 9.5.2 避免闪烁 大家可能已经发现,上例的画面有些闪动。闪烁有两个原因:一是绘制每一帧花费的时间太长(因为重绘时要求的计算量大),二是在每次调用pait()前整个背景被清除,当在进行下一帧的计算时,用户看到的是背景。清除背景和绘制图形间的短暂时间被用户看见,就是闪烁。在有些平台,如PC机上,闪烁比在X Window上明显,这是因为X Window的图像被缓存过,使得闪烁的时间比较短。有两种办法可以明显地减弱闪烁:重载update()或使用双缓冲。
  • 110. 这里我们先讨论重载update()的方法。当AWT接收到一个重绘(repaint)请求时,它就先调用update()清除背景,而update()方法又会调用paint()。重载update(),手动调用paint()方法,避免每次重绘时将整个区域清除,即背景不再自动清除。这需要我们自己在update()中编写代码来完成。在绘制新的图像之前手动将原来的旧图擦除,这样可以有效地避免闪烁。我们来看修改以后的例子。
  • 111. 例9.13 Animation1.java import java.awt.*; public class Animation1 extends Frame implements Runnable { int totalImages = 19; int currentImage = 0; int delay = 50; int x, x1; Image[] spin; Thread animator;
  • 112. public static void main(String[] args) { Animation1 frame = new Animation1(); frame.setSize(500, 150); frame.setBackground(Color.yellow); frame.setVisible(true); } public Animation1() { spin = new Image[totalImages]; Toolkit defaultTk = Toolkit.getDefaultToolkit(); MediaTracker tracker = new MediaTracker(this);
  • 113. for (int i = 0; i < totalImages; i++) { spin[i] = defaultTk.getImage("spin"+i+".gif"); tracker.addImage(spin[i], i); try { tracker.waitForID(i);} catch (InterruptedException e) { e.printStackTrace(); }} x = 0; x1 = 0; animator = new Thread(this); animator.start(); }
  • 114. public void run() {while (Thread.currentThread() == animator) { repaint(); try { Thread.sleep(delay);} catch (InterruptedException e) { e.printStackTrace(); }} } public void update(Graphics g)
  • 115. { g.setColor(Color.yellow); g.fillRect(x1, 30, spin[0].getWidth(this), spin[0].getHeight(this)); paint(g); } public void paint(Graphics g) { g.drawImage(spin[currentImage], 10+x, 30, this); x1 = x; x = (x+5) % 400; currentImage = ++currentImage % totalImages; } }
  • 116. 9.5.3 双缓冲技术 另一种减小帧之间的闪烁的方法就是使用双缓冲,它在许多动画Applet中被使用。双缓冲技术主要原理是创建一个后台图像,将一帧画入图像,然后调用drawImage()将整个图像一次画到屏幕上去。好处是大部分绘制是离屏的,将离屏图像一次绘至屏幕上比直接在屏幕上绘制要有效得多。 双缓冲可以使动画平滑,但有一个缺点:要分配一张后台图像,如果此图像相当大,将需要很大一块内存。当使用双缓冲技术时,应重载update()。下面我们来看看如何继续修改Animation实现双缓冲。
  • 117. 例9.14 Animation2.java import java.awt.*; public class Animation2 extends Frame implements Runnable { int totalImages = 19; int currentImage = 0; int delay = 50; int x, x1; Image[] spin; Thread animator; Image offscreen;
  • 118. public static void main(String[] args) { Animation2 frame = new Animation2(); frame.setSize(500, 150); frame.setBackground(Color.yellow); frame.setVisible(true); } public Animation2() { spin = new Image[totalImages]; Toolkit defaultTk = Toolkit.getDefaultToolkit();
  • 119. MediaTracker tracker = new MediaTracker(this); for (int i = 0; i < totalImages; i++) { spin[i] = defaultTk.getImage("spin"+i+".gif"); tracker.addImage(spin[i], i); try { tracker.waitForID(i);} catch (InterruptedException e) { e.printStackTrace(); }} x = 0; x1 = 0;
  • 120. animator = new Thread(this); animator.start();} public void run() { while (Thread.currentThread() == animator) { repaint(); try { Thread.sleep(delay);} catch (InterruptedException e) { e.printStackTrace(); }} }
  • 121. public void update(Graphics g) { paint(g); } public void paint(Graphics g) {if (offscreen == null) offscreen = createImage(getSize().width, getSize().height); Graphics og = offscreen.getGraphics(); og.setClip(0, 0, getSize().width, getSize().height); og.setColor(Color.yellow); og.fillRect(x1, 30, spin[0].getWidth(this),
  • 122. spin[0].getHeight(this)); og.drawImage(spin[currentImage], 10+x, 30, this); super.paint(og); g.drawImage(offscreen, 0, 0, null); x1 = x; x = (x+5) % 400; currentImage = ++currentImage % totalImages; } }
  • 123. 程序中先创建了一个图形上下文,这个图形上下文并不是在屏幕上显示图像的那一个,所以也称为离屏图像。将要显示的图片先画到这个图形上下文中,然后将这个离屏图像一次性贴到屏幕上。这样的速度要比直接在屏幕上画图快的多,所以对消除闪烁是十分有效的。