1. AWT位图处理机制:
1) 如果AWT只能绘制简单的几何图形那会显得过于单调,因此AWT允许绘制位图,可以将位图作为组件的外观;
2) AWT表现位图的机制:
i. 因为位图比较复杂,最好是一次性画到屏幕上,比如之前讲过的例子,如果位图里有两条线,那么你一次将两条线同时画到屏幕上一定要比分别画两条线到屏幕上要快很多;
ii. 因此位图必须先在内存/显卡中先准备花(即先在内存中“画”好),然后一次性冲刷到屏幕上;
iii. 总结:即位图采用内存传送的机制,必须现在内存中画好,然后一次性传送到屏幕上显示;
3) 那么关键问题就是如何在内存中“画”位图呢?我们知道在屏幕上画图直接用Graphics类对象就行了,那内存里也可以用Graphics吗?答案是可以的,我们知道在MFC中设备环境CDC的绘制对象可以不停变化,可以在屏幕绘图、在打印机内存中绘图、直接在内存中绘图等等,Graphics也一样,在paint、update中获得的g就是屏幕环境,而通过BufferedImage类的getGraphics方法获得的对象g就是内存环境了,可以通过g调用和屏幕Graphics相同的绘图方法在内存中绘图,只不过在内存中绘制的并不是真实的图像,而仅仅是图像的数据表示,但不过调用的方法都还是一样的,这在AWT底层隐藏了两者的区别而已;
2. 使用BufferedImage在内存中绘制位图:
1) 位图在AWT中对应的类是Image类,只不过该类是一个纯抽象类,因为没有实用价值,要绘制位图就必须使用Image的子类BufferedImage;
2) 顾名思义BufferedImage就是缓冲位图的意思,即它所表示的位图存在于内存中,等到位图在内存中绘制完毕后再通过其他方法将其刷到屏幕上显示;
3) 构造器:BufferedImage(int width, int height, int imageType); // 前两个参数表示位图的宽和高
4) imageType是BufferedImage定义的静态常量,表示用什么方式绘制位图,共有两种,一种是TYPE_INT_RGB(4字节三原色),另一种是TYPE_BYTE_GRAY(单字节灰度值);
5) 但是构造器仅仅就是定义了一个内存中的位图对象,仅仅就是开辟了一个内存镜像而已,但什么都没有画,是一个空的位图,要绘制图形就必须使用BufferedImage的getGraphics方法得到内存环境设备描述符:Graphics getGraphics(); // 返回的对象就是可以直接在内存中绘图的Graphics对象了
!!绘图时跟屏幕Graphics没有任何区别,什么drawLine之类的完全一样,只不过不是画在屏幕上,而是画在内存镜像中;
3. 位图传送:
1) 内存镜像中的位图已经画好了,就等待传送至屏幕上显示了;
2) 位图要绘制在哪个组件中就调用那个组件的屏幕Graphics的drawImage方法即可,将准备好的BufferedImage作为该方法的参数传入即可,就直接将内存镜像传送至屏幕了;
3) 原型:boolean Graphics.drawImage(Image img, int x, int y, ImageObserver observer);
i. img就是要传送到屏幕上的位图,直接传BufferedImage的对象即可;
ii. (x, y)就是位图在组件中的坐标(组件左上角为原点);
iii. observer是位图传送时的监督者,监督正在传送的时候内存中的位图是否还正在绘制,这是什么意思呢?就是说有些应用程序要求极高的绘图效率,因此可能会采用多线程绘图,比如内存中已经绘制了1/2了,还有剩下1/2没画好,那么就先将前1/4线冲到屏幕上,就是要如此高效的利用CPU时间,尽量减少等待,因此内存绘图和位图传送就使用两个线程进行,因此有可能遇到传送比绘制更快的情形,即要绘制的那个像素点仍然还在改变(被绘制),因此遇到这种冲突是drawImage就会返回false并终端drawImage方法,正常情况下就返回true;
iv. 这里我们先不用多线程,因此没有监督者,该参数传null即可,这是就默认单线程,即必须等待内存镜像画完后才能传送;
4. 示例:手绘程序
1) 秘诀就是MouseMotionListener监听器的mouseDragged方法,在鼠标拖拽时会触发该方法,可以利用MouseEvent的getX和getY方法获取拖拽轨迹上的坐标,根据改坐标画线,由于拖拽时触发该事件的事件间隔非常短,每段间隔画一条线,连起来就无限近似曲线了,非常和谐;
2) 只不过画线是画在内存镜像中的,而每画一条线就进行一次位图传送,对,这非常低效,但仅仅是为了演示位图传送罢了;
public class AwtTest extends WindowAdapter {
private Frame f = new Frame("Hand Draw");
private final int DRAW_WIDTH = 500;
private final int DRAW_HEIGHT = 400;
private DrawCanvas drawArea = new DrawCanvas();
private BufferedImage bufImg = new BufferedImage(DRAW_WIDTH, DRAW_HEIGHT, BufferedImage.TYPE_INT_RGB);
private Graphics gmm = bufImg.getGraphics();
private Color foreColor = new Color(255, 0, 0); // 当前画笔的颜色
class DrawCanvas extends Canvas {
@Override
public void paint(Graphics g) { // 通过位图传送的方式将图像传送至画布在屏幕上显示
// 因此我们的手绘程序其实并不是直接在屏幕上手绘,而是现在内存中手绘,然后传送至屏幕
// TODO Auto-generated method stub
// super.paint(g);
g.drawImage(bufImg, 0, 0, null);
}
}
private PopupMenu pop = new PopupMenu();
private MenuItem red = new MenuItem("red");
private MenuItem green = new MenuItem("green");
private MenuItem blue = new MenuItem("blue");
// 上一次鼠标拖动事件发生时鼠标的位置
private int preX = -1;
private int preY = -1;
public void init() {
f.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
// TODO Auto-generated method stub
// super.windowClosing(e);
System.exit(0);
}
});
drawArea.addMouseMotionListener(new MouseMotionAdapter() {
@Override
public void mouseDragged(MouseEvent e) {
// TODO Auto-generated method stub
// super.mouseDragged(e);
if (preX > 0 && preY > 0) { // 如果上次鼠标拖动的终点已存在
gmm.setColor(foreColor); // 那就直接在内存中画线(要先设置当前颜色,可能已经通过右键菜单改变)
gmm.drawLine(preX, preY, e.getX(), e.getY());
}
// 将本次拖拽的终点记录下来
preX = e.getX();
preY = e.getY();
drawArea.repaint(); // 画好后进行位图传送
// 这里的策略是画一条传送一次,效率仍然很低,因为要求实时显示画线轨迹,这里只是为了演示位图传送而已
}
});
drawArea.addMouseListener(new MouseAdapter() {
@Override
public void mouseReleased(MouseEvent e) {
// TODO Auto-generated method stub
// super.mouseReleased(e);
if (e.isPopupTrigger()) {
pop.show(drawArea, e.getX(), e.getY());
}
// 鼠标松开意味着可能发生了两种事件的一种
// 第一种就是右键松开,表示弹出右键菜单,即上面的代码
// 另一种就是画图键(左键)松开,表示一段手绘的结束
// 这两种事件都意味着一段手绘的结束,为了重新开始下一段手绘要把preX和preY初始化
preX = -1;
preY = -1;
}
});
ActionListener menuListener = e -> {
if (e.getActionCommand().equals("red")) {
foreColor = new Color(255, 0, 0);
}
else if (e.getActionCommand().equals("green")) {
foreColor = new Color(0, 255, 0);
}
else if (e.getActionCommand().equals("blue")) {
foreColor = new Color(0, 0, 255);
}
};
red.addActionListener(menuListener);
green.addActionListener(menuListener);
blue.addActionListener(menuListener);
f.add(drawArea);
drawArea.setPreferredSize(new Dimension(DRAW_WIDTH, DRAW_HEIGHT));
gmm.fillRect(0, 0, DRAW_WIDTH, DRAW_HEIGHT); // 初始时默认背景色为白色
drawArea.add(pop);
pop.add(red);
pop.add(green);
pop.add(blue);
f.pack();
f.setVisible(true);
}
public static void main(String[] args) {
new AwtTest().init();
}
}