11.7 在AWT中绘图

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显示了paintrepaintupdate三个方法之间的调用关系
这里有一张图片

重写repaint方法实现重绘 重写paint方法实现绘图

从图11.27中可以看出,程序不应该主动调用组件的paintupdate方法,这两个方法都由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参数,它可以使用RGBCMYK等方式设置一个颜色;而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
shape = RECT_SHAPE;
// 重画MyCanvas对象,即调用它的repaint()方法
drawArea.repaint();
});
oval.addActionListener(e -> {
// 设置shape变量为OVAL_SHAPE
shape = OVAL_SHAPE;
// 重画MyCanvas对象,即调用它的repaint()方法
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 {
// 重写Canvas的paint方法,实现绘画
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;
// 返回一个-0.5~0.5的比率,用于控制小球的运行方向。
private double xyRate = rand.nextDouble() - 0.5;
// 小球横向的运行速度
private int xSpeed = (int) (ySpeed * xyRate * 2);
// ballX和ballY代表小球的坐标
private int ballX = rand.nextInt(200) + 20;
private int ballY = rand.nextInt(10) + 20;
// racketX代表球拍的水平位置
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;
}
}
};
// 为窗口和tableArea对象分别添加键盘监听器
f.addKeyListener(keyProcessor);
tableArea.addKeyListener(keyProcessor);
// 定义每0.1秒执行一次的事件监听器。
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();
// 设置游戏是否结束的旗标为true。
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 {

// 重写Canvas的paint方法,实现绘画
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对应的组件,使用SwingPanel组件作为画布即可