11.7 在AWT中绘图
很多程序如各种小游戏都需要在窗口中绘制各种图形,除此之外,即使在开发Java EE
项目时,有时候也必须“动态”地向客户端生成各种图形、图表,比如图形验证码、统计图等,这都需要利用AWT
的绘图功能。
11.7.1 画图的实现原理
Component类绘图相关方法
在Component
类里提供了和绘图有关的三个方法:
绘图方法 |
描述 |
paint(Graphics g) |
绘制组件的外观。 |
update(Graphics g) |
调用paint 方法,刷新组件外观。 |
repaint() |
调用update 方法,刷新组件外观。 |
上面三个方法的调用关系为:repaint
方法调用update
方法;update
方法调用paint
方法。
Container的update方法
Component
类的子类Container
类中的update
方法先以组件的背景色填充整个组件区域,然后调用paint
方法重画组件。
Container
类的update
方法代码如下
1 2 3 4 5 6 7 8 9
| public void update(Graphics g) { if (isShowing()){ if(!(peer instanceof LightweightPeer)){ g.clearRect(0,0, width, height); paint(g); } } }
|
普通组件的update
方法则直接调用paint
方法:
1 2 3
| public void update(Graphics g){ paint(g); }
|
图11.27显示了paint
、repaint
和update
三个方法之间的调用关系
重写repaint方法实现重绘 重写paint方法实现绘图
从图11.27中可以看出,程序不应该主动调用组件的paint
和update
方法,这两个方法都由AWT
系统负责调用。如果程序希望AWT
系统重新绘制该组件,则调用该组件的repaint
方法即可。而paint
)和update
方法通常被重写。在通常情况下,程序通过重写paint
方法实现在AWT
组件上绘图。
11.7.2 使用Graphics类
Graphics
是一个抽象的画笔对象,Graphics
可以在组件上绘制丰富多彩的几何图形和位图。Graphics
类提供了如下几个方法用于绘制几何图形和位图:
方法 |
描述 |
drawLine() |
绘制直线。 |
drawString() |
绘制字符串 |
drawRect() |
绘制矩形。 |
drawRoundRect() |
绘制圆角矩形。 |
drawOval() |
绘制椭圆形状。 |
drawPolygon() |
绘制多边形边框。 |
drawArc() |
绘制一段圆弧(可能是椭圆的圆弧)。 |
drawPolyline() |
绘制折线。 |
fillRect() |
填充一个矩形区域。 |
fillroundRect() |
填充一个圆角矩形区域。 |
fillOval() |
填充椭圆区域。 |
fillPolygon() |
填充一个多边形区域 |
fillArc() |
填充圆弧和圆弧两个端点到中心连线所包围的区域。 |
drawImage() |
绘制位图。 |
除此之外,Graphics
还提供了setColor()
和setFont()
两个方法用于设置画笔的颜色和字体(仅当绘制字符串时有效),其中setColor()
方法需要传入一个Color
参数,它可以使用RGB
、CMYK
等方式设置一个颜色;而setFont()
方法需要传入一个Font
参数,Font
参数需要指定字体名、字体样式、字体大小三个属性。
普通AWT组件如何设置 前景色 背景色 字体
实际上,不仅Graphics
对象可以使用setColor()
和setFont()
方法来设置画笔的颜色和字体。
AWT
普通组件也可以通过Color()
和Font()
方法来改变它的前景色和字体。
除此之外,所有组件都有一个setBackground()
方法用于设置组件的背景色
AWT
专门提供一个Canvas
类作为绘图的画布,程序可以通过创建Canvas
的子类,并重写它的paint
方法来实现绘图。
程序示例
下面程序示范了一个简单的绘图程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| import java.awt.BorderLayout; import java.awt.Button; import java.awt.Canvas; import java.awt.Color; import java.awt.Dimension; import java.awt.Frame; import java.awt.Graphics; import java.awt.Panel; import java.util.Random; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent;
public class SimpleDraw { private final String RECT_SHAPE = "rect"; private final String OVAL_SHAPE = "oval"; private Frame f = new Frame("简单绘图"); private Button rect = new Button("绘制矩形"); private Button oval = new Button("绘制圆形"); private MyCanvas drawArea = new MyCanvas(); private String shape = "";
public void init() { Panel p = new Panel(); rect.addActionListener(e -> { shape = RECT_SHAPE; drawArea.repaint(); }); oval.addActionListener(e -> { shape = OVAL_SHAPE; drawArea.repaint(); }); p.add(rect); p.add(oval); drawArea.setPreferredSize(new Dimension(250, 180)); f.add(drawArea); f.add(p, BorderLayout.SOUTH); f.pack(); f.setVisible(true); f.addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { System.exit(0); } }); }
public static void main(String[] args) { new SimpleDraw().init(); }
class MyCanvas extends Canvas { public void paint(Graphics g) { Random rand = new Random(); if (shape.equals(RECT_SHAPE)) { g.setColor(new Color(220, 100, 80)); g.drawRect(rand.nextInt(200), rand.nextInt(120), 40, 60); } if (shape.equals(OVAL_SHAPE)) { g.setColor(new Color(80, 100, 200)); g.fillOval(rand.nextInt(200), rand.nextInt(120), 50, 40); } } } }
|
上面程序定义了一个MyCanvas
类,它继承了Canvas
类,重写了Canvas
类的paint
方法,该方法根据shape
变量值随机地绘制矩形或填充椭圆区域。
窗口中还定义了两个按钮,当用户单击任意一个按钮时,程序调用了drawArea
对象的repaint
方法,该方法导致画布重绘(即调用drawArea
对象的update
方法,该方法再调用paint()
方法
运行上面程序,单击“绘制圆形”按钮,将看到如图11.28所示的窗口
运行上面程序时,如果改变窗口大小,或者让该窗口隐藏后重新显示都会导致drawArea
重新绘制形状,这是因为这些动作都会触发组件的update
方法。
动画
Java
也可用于开发一些动画。所谓动画,就是间隔一定的时间(通常小于0.1秒)重新绘制新的图像两次绘制的图像之间差异较小,肉眼看起来就成了所谓的动画。
定时器Timer
为了实现间隔一定的时间就重新调用组件的repaint
方法,可以借助于Swing
提供的Timer
类,Timer
类是一个定时器,它有如下一个构造器
方法 |
描述 |
Timer(int delay, ActionListener listener) |
每间隔delay 毫秒,系统自动触发ActionListener 监听器里的事件处理器,也就是actionPerformed() 方法 |
程序示例 弹球游戏
下面程序示范了一个简单的弹球游戏,其中小球和球拍分别以圆形区域和矩形区域代替,小球开始以随机速度向下运动,遇到边框或球拍时小球反弹;球拍则由用户控制,当用户按下向左、向右键时,球拍将会向左、向右移动。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
| import java.util.Random; import java.awt.*; import javax.swing.*; import java.awt.event.*;
public class PinBall { private final int TABLE_WIDTH = 300; private final int TABLE_HEIGHT = 400; private final int RACKET_Y = 340; private final int RACKET_HEIGHT = 20; private final int RACKET_WIDTH = 60; private final int BALL_SIZE = 16; private Frame f = new Frame("弹球游戏"); Random rand = new Random(); private int ySpeed = 10; private double xyRate = rand.nextDouble() - 0.5; private int xSpeed = (int) (ySpeed * xyRate * 2); private int ballX = rand.nextInt(200) + 20; private int ballY = rand.nextInt(10) + 20; private int racketX = rand.nextInt(200); private MyCanvas tableArea = new MyCanvas(); Timer timer; private boolean isLose = false;
public void init() { tableArea.setPreferredSize(new Dimension(TABLE_WIDTH, TABLE_HEIGHT)); f.add(tableArea); KeyAdapter keyProcessor = new KeyAdapter() { public void keyPressed(KeyEvent ke) { if (ke.getKeyCode() == KeyEvent.VK_LEFT) { if (racketX > 0) racketX -= 10; } if (ke.getKeyCode() == KeyEvent.VK_RIGHT) { if (racketX < TABLE_WIDTH - RACKET_WIDTH) racketX += 10; } } }; f.addKeyListener(keyProcessor); tableArea.addKeyListener(keyProcessor); ActionListener taskPerformer = evt -> { if (ballX <= 0 || ballX >= TABLE_WIDTH - BALL_SIZE) { xSpeed = -xSpeed; } if (ballY >= RACKET_Y - BALL_SIZE && (ballX < racketX || ballX > racketX + RACKET_WIDTH)) { timer.stop(); isLose = true; tableArea.repaint(); } else if (ballY <= 0 || (ballY >= RACKET_Y - BALL_SIZE && ballX > racketX && ballX <= racketX + RACKET_WIDTH)) { ySpeed = -ySpeed; } ballY += ySpeed; ballX += xSpeed; tableArea.repaint(); }; timer = new Timer(100, taskPerformer); timer.start(); f.addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { System.exit(0); } }); f.pack(); f.setVisible(true); }
public static void main(String[] args) { new PinBall().init(); }
class MyCanvas extends Canvas {
public void paint(Graphics g) { if (isLose) { g.setColor(new Color(255, 0, 0)); g.setFont(new Font("Times", Font.BOLD, 30)); g.drawString("游戏已结束!", 50, 200); } else { g.setColor(new Color(240, 240, 80)); g.fillOval(ballX, ballY, BALL_SIZE, BALL_SIZE); g.setColor(new Color(80, 80, 200)); g.fillRect(racketX, RACKET_Y, RACKET_WIDTH, RACKET_HEIGHT); } } } }
|
运行上面程序,将看到一个简单的弹球游戏,运行效果如图11.29所示。
弹球游戏升级思路
上面的弹球游戏还比较简陋,如果为该游戏增加位图背景,使用更逼真的小球位图代替小球,更逼真的球拍位图代替球拍,并在弹球桌面増加一些障碍物,整个弹球游戏将会更有趣味性。细心的读者可能会发现上面的游戏有轻微的闪烁,这是由于AWT
组件的绘图没有采用双缓冲技术,当重写paint
方法来绘制图形时,所有的图形都是直接绘制到GUI
组件上的,所以多次重新调用paint()
方法进行绘制会发生闪烁现象。使用Swing
组件就可避免这种闪烁,Swing组件没有提供Canvas对应的组件,使用Swing的Panel
组件作为画布即可