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上绘图;