您的位置:首页 > 编程语言 > Java开发

[疯狂Java]AWT:绘图、动画

2016-03-08 13:15 423 查看
1. 绘图三方法:

1) AWT绘图的核心就是三个方法:paint、update、repaint;

2) 三者的调用关系是repaint调用update,update调用paint;

3) 首先看paint,就是画出一个组件的外观,比如一个按钮,那就是按钮上的文字、边框、纹理、3D效果等,整个组件的外观都由paint来绘制;

4) 接下来是update,就是更新组件的画面,其实就是重画。设想当一个组件的位置改变了(比如挪动到了另一个位置),或者发生了伸缩(认为的改变其大小),组件当然是要重画了,那难道没有其他工作要做了吗?比如你重画之前总要擦掉之前位置上的组件吧,update的目的就是这个,让组件本身的绘制完全交给paint,而update要做的工作就是先把重画前的组件抹掉,而这个抹掉并不是简单的删掉,而是用组件的背景色(组件所在容器的底色)填充原来组件的区域,然后再调用paint重画组件;

5) update的实现:

i. 对于容器类组件,像Panel、Canvas(画布)等的更新(重画)必定要先将其中的组件先抹去,然后再重绘其中的组件,因此对于容器类组件的update第一步是先用容器的底色填充其中组件的原始区域,然后再调用各个组件的paint重画其中的组件;

ii. 对于普通组件,由于它们不是容器,里面没有其它组件,因此其update方法就只有一句,就是调用paint方法;

6) repaint就是重画组件外观,里面主动调用了update,通常用户就调用repaint重画就行了,paint和update都是有AWT系统负责调用,这两个方法通常需要用户自己覆盖;

7) 触发update的时机:

i. 组件大小改变(发生伸缩);

ii. 组件隐藏后被显示出来或者第一次显示出来;

iii. 组件位置移动;

iv. 人为地主动调用repaint:主动调用repaint就是程序控制重画的唯一手段,上面三个都是自动触发的(操作系统中断触发);

2. 为什么要覆盖paint和update:

1) paint的覆盖很容易想到,比如画布Canvas的paint就必须要实现,它定义了画布中要画什么东西,画的内容肯定是要由用户自己决定的;

!!但是想普通组件(按钮)之类的paint往往不需要覆盖,系统定义好了已经,除非你想自制,但一般没有这个需要;

2) update的覆盖:即使你不服该AWT也提供了默认的实现,那就是先用底色填充整个容器区域,然后再调用各个组件的paint绘制组件,但是这种实现效率不高,因为

i. 有时候没必要填充整个容器区域,也许里面就只有一个组件,那么只要填充那一小个组件的区域就行了;

ii. 其次是有可能只有里面的一个组件需要重绘(比如就那个组件位置改变了),但是默认会重绘里面的所有组件,这就可能导致不需要重绘的无端重绘了;

!!种种原因导致重绘效率低下,在一些效率要求较高的情况下明显不符合要求,因此覆盖update的主要目的就是提高重绘的效率;

3. Graphics类:

1) 先看一下绘图三方法的原型:

i. public void Component.paint(Graphics g);

ii. public void Component.update(Graphics g);

iii. public void Component.repaint();

2) 可以看到前两个都传入了Graphics类参数g,该参数就是类似MFC中的CDC类,即上下文设备,可以利用该类的对象进行绘图;

3) 而repaint中显然是创建了一个Graphics类对象作为参数连续传递给update和paint来进行绘图;

4) Graphics类有很多绘图方法,主要有两类,一类是画,即以draw打头,另一类是填充,以fill打头:

i. draw有drawLine、drawRect、drawString、drawImage等等,有画线、画图形、画字符串、画位图等;

ii. fill有fillRect、fillArc等,用来填充封闭的图形区域;

5) 设置颜色、字体:

i. 颜色:void Graphics.setColor(Color c); // 设置画笔的颜色,画的时候是画笔,填充的时候叫画刷

ii. 字体:void Graphics.setFont(Font font); // 设置画字符串时的字体

!其中最常用的是设置颜色了,Color的构造器是:Color(int r, int g, int b); // 即RGB三原色,各范围是0-255

4. 示例:在画布中画圆画方

public class AwtTest extends WindowAdapter {

@Override
public void windowClosing(WindowEvent e) {
// TODO Auto-generated method stub
// super.windowClosing(e);
System.exit(0);
}

private final String RECT = "rect";
private final String OVAL = "oval";
private String shape = "";

class MyCanvas extends Canvas {

@Override
public void paint(Graphics g) {
// TODO Auto-generated method stub
// super.paint(g);
Random rand = new Random();
if (shape.equals(RECT)) {
g.setColor(new Color(220, 100, 80));
g.drawRect(rand.nextInt(200), rand.nextInt(120), 40, 60);
}
else if (shape.equals(OVAL)) {
g.setColor(new Color(80, 100, 200));
g.fillOval(rand.nextInt(200), rand.nextInt(120), 50, 40);
}
}

}

private Frame f = new Frame("Graphics Test");
private MyCanvas cva = new MyCanvas();
private Panel p = new Panel();
private Button btnRect = new Button("Draw Rect");
private Button btnOval = new Button("Fill Oval");

public void init() {
cva.setPreferredSize(new Dimension(250, 180));
f.add(cva);
p.add(btnRect);
p.add(btnOval);
f.add(p, BorderLayout.SOUTH);

btnRect.addActionListener(e -> {
shape = RECT;
cva.repaint();
});
btnOval.addActionListener(e -> {
shape = OVAL;
cva.repaint();
});
f.addWindowListener(this);

f.pack();
f.setVisible(true);
}

public static void main(String[] args) {
new AwtTest().init();
}

}
!!两种图形的位置都是随机挑选的;

!!所有的绘图方法的用法直接查看手册就行了,这里就不一一讲解了;

5. AWT实现动画效果:

1) 秘诀就是使用Swing组件提供的Timer类,其构造器:public Timer(int delay, ActionListener listener); // 意思就是每个delay毫秒就触发ActionListener的actionPerfomed方法;

2) 因此可以在actionPerformed中定义如何绘制图形,这样就每个delay毫秒画出一个图形,在如此之短的时间间隔下画图便能实现动画效果;

3) 示例:一个简单的桌面弹球游戏

public class AwtTest extends WindowAdapter {

@Override
public void windowClosing(WindowEvent e) {
// TODO Auto-generated method stub
// super.windowClosing(e);
System.exit(0);
}

private Random rand = new Random(); // 随机序列种子

private Frame f = new Frame("Welcome to Ball Game!"); // 框架窗口
private Button btn = new Button("Restart");

// 球桌的配置
// 球桌的宽和高
private final int TABLE_WIDTH = 300;
private final int TABLE_HEIGHT = 400;
private BallCanvas table = new BallCanvas();

// 小球的配置
private final int BALL_SIZE = 16; // 球的直径
// 小球的起始位置,要保证在球桌范围内(球的左上角)
private int ballX; // = rand.nextInt(200) + 20;
private int ballY; // = rand.nextInt(10) + 20;
// 小球的启示速率(为单位时间在两个方向上运动多少距离)
// 其中x和y方向上稍有比率差别,这样更加和谐(模拟随机过程)
// 负值表示反向
private int ySpeed = 10;
private int xSpeed = (int)(ySpeed * 2.0 * (rand.nextDouble() - 0.5));

// 球拍的配置
// 球拍的宽和高
private final int RACKET_WIDTH = 60;
private final int RACKET_HEIGHT = 20;
// 球拍的起始位置(矩形左上角),要保证在球桌范围之内
private int racketX; // = rand.nextInt(200);
private final int racketY = 340; // 球拍的Y坐标固定在340的位置
private int racketSpeed = 10; // 球拍移动速度固定为10,球拍只横向运动

private boolean isLose = false; // 是否输球

Timer timer;

class BallCanvas extends Canvas {

@Override
public void paint(Graphics g) {
// TODO Auto-generated method stub
// super.paint(g);
if (isLose) {
g.setColor(new Color(255, 0, 0));
g.setFont(new Font("Times", Font.BOLD, 30));
g.drawString("Game Over!", 50, 200);
}
else {
int x = ballX;
int y = ballY;

if (ballX < 0) {
x = 0;
}
if (ballX + BALL_SIZE > TABLE_WIDTH) {
x = TABLE_WIDTH - BALL_SIZE;
}
if (ballY <= 0) {
y = 0;
}
if (ballY + BALL_SIZE > racketY &&
ballX + BALL_SIZE / 2 >= racketX &&
ballX + BALL_SIZE / 2 <= racketX + RACKET_WIDTH) {
y = racketY - BALL_SIZE;
}

g.setColor(new Color(240, 240, 80));
g.fillOval(x, y, BALL_SIZE, BALL_SIZE);

g.setColor(new Color(80, 80, 200));
g.fillRect(racketX, racketY, RACKET_WIDTH, RACKET_HEIGHT);
}
}

}

private void initTable() {
ballX = rand.nextInt(200) + 20;
ballY = rand.nextInt(10) + 20;
racketX = rand.nextInt(200);

isLose = false;
timer.start();
}

public void init() {

f.addWindowListener(this);

KeyAdapter keyCtrl = new KeyAdapter() { // 监听左右按键控制球拍动向

@Override
public void keyPressed(KeyEvent e) {
// TODO Auto-generated method stub
// super.keyPressed(e);

// 球桌宽度刚好是球拍速度的整数倍,因此不用判断是否出界
// 只需要防止穿墙即可
if (e.getKeyCode() == KeyEvent.VK_LEFT) {
if (racketX > 0) {
racketX -= racketSpeed;
}
}
else if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
if (racketX + RACKET_WIDTH < TABLE_WIDTH) {
racketX += racketSpeed;
}
}
else if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
System.exit(0);
}
}

};
f.addKeyListener(keyCtrl); // 窗口和球桌同时监听按键,两者任意一个获得焦点都可进行游戏
table.addKeyListener(keyCtrl);

timer = new Timer(100, e -> {
// 检测横向撞击
if (ballX <= 0 || ballX + BALL_SIZE >= TABLE_WIDTH) { // 横向撞边就要反向
xSpeed = -xSpeed;
}
// 检测纵向撞击
if (ballY <= 0 || // 撞顶边,接下来检测是否撞拍
ballY + BALL_SIZE >= racketY &&
ballY + BALL_SIZE <= racketY + RACKET_HEIGHT &&
ballX + BALL_SIZE / 2 >= racketX && // (2) 并且球心位置在球拍范围之内
ballX + BALL_SIZE / 2 <= racketX + RACKET_WIDTH) { // 满足这两个条件就代表撞拍了
// 纵向撞边或拍也要反向
ySpeed = -ySpeed;
}

// 是否输球
if (ballY + BALL_SIZE > racketY + RACKET_HEIGHT) { // 只要球的下边沿超过拍的上边沿就输了
isLose = true;
timer.stop();
table.repaint();
}

// 球位移然后重画
ballX += xSpeed;
ballY += ySpeed;
table.repaint();
});

btn.addActionListener(e -> {
table.requestFocus();
initTable();
});

table.setPreferredSize(new Dimension(TABLE_WIDTH, TABLE_HEIGHT));
f.add(table);
f.add(btn, BorderLayout.SOUTH);

initTable();

f.pack();
f.setVisible(true);

table.requestFocus();
}

public static void main(String[] args) {
new AwtTest().init();
}

}
!Timer的两个方法,start用来启动计时器,stop来停止计时器,两个方法的原型:

a. public void Timer.start();

b. public void Timer.stop();

6. AWT的弱点——缓冲:

1) AWT的绘图都是直接绘制在屏幕上的,比如要画两根线,那就先将第一根直接画在屏幕上,再把第二根直接画在屏幕上,这就是典型的无缓冲绘图;

2) 缓冲绘图在上面的例子中就先在内存中写好两根线的画法,然后直接将内存中的镜像一次性滑到屏幕上,而不是分两条线两次画到屏幕上,这就是MFC的CompatibleDC的原理,即先将要绘制的图形“画”在内存中(或者显卡中),然后直接将显卡中的内容冲到屏幕上,内存中实际并不存在图像,而是图像的数据表示,因此叫做内存镜像;

!!由于在屏幕上绘制图形的过程要比CPU和内存的速度慢很多,因此AWT实现动画时反复多次的屏幕直写导致画质不好、屏幕闪烁(即速度慢),因此缓存技术显得尤为重要;

3) Swing的绘图就采用双缓存技术,即如果绘制的内容很多,那就现在内存中(显卡)中写好要绘制的所有图形的数据,然后一次性将镜像刷至屏幕,大大提高了绘图速率;

4) AWT绘图基本使用Canvas,而Canvas无缓冲,效率极低,而Swing的GUI组件都采用了双缓存技术,Swing没有提供Canvas画布组件,可以直接在Panel上绘图;
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: