11.8 处理位图

11.8 处理位图

如果仅仅绘制一些简单的几何图形,程序的图形效果依然比较单调。AWT也允许在组件上绘制位图,Graphics提供了drawImage方法用于绘制位图,该方法需要一个Image参数代表位图,通过该方法就可以绘制出指定的位图。

11.8.1 Image抽象类和BufferedImage实现类

Image类代表位图,但它是一个抽象类,无法直接创建Image对象,为此Java为它提供了一个BufferedImage子类,这个子类是一个可访问图像数据缓冲区的Image实现类。该类提供了一个简单的构造器,用于创建一个Bufferedlmage对象。

方法 描述
BufferedImage(int width, int height, int imageType) 创建指定大小、指定图像类型的BufferedImage对象,其中imageType可以是BufferedImage.TYPE_INT_RGBBufferedImage.TYPE_BYTE_GRAY等值。

在位图上绘图

除此之外,BufferedImage还提供了一个getGraphics()方法返回该对象的Graphics对象,从而允许通过该Graphics对象向Image中添加图形。

通过BufferedImageAWT上使用缓冲

借助BufferedImage可以在AWT中实现缓冲技术:当需要向GUI组件上绘制图形时,不要直接绘制到该GUI组件上,而是先将图形绘制到BufferedImage对象中,然后再调用组件的drawImage方法一次性地将BufferedImage对象绘制到特定组件上。

程序示例 简单的手绘程序

下面程序通过BufferedImage类实现了图形缓冲,并实现了一个简单的手绘程序。

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
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;

public class HandDraw {
// 画图区的宽度
private final int AREA_WIDTH = 500;
// 画图区的高度
private final int AREA_HEIGHT = 400;
// 下面的preX、preY保存了上一次鼠标拖动事件的鼠标坐标
private int preX = -1;
private int preY = -1;
// 定义一个右键菜单用于设置画笔颜色
PopupMenu pop = new PopupMenu();
MenuItem redItem = new MenuItem("红色");
MenuItem greenItem = new MenuItem("绿色");
MenuItem blueItem = new MenuItem("蓝色");
// 定义一个BufferedImage对象
BufferedImage image = new BufferedImage(AREA_WIDTH, AREA_HEIGHT, BufferedImage.TYPE_INT_RGB);
// 获取image对象的Graphics
Graphics g = image.getGraphics();
private Frame f = new Frame("简单手绘程序");
private DrawCanvas drawArea = new DrawCanvas();
// 用于保存画笔颜色
private Color foreColor = new Color(255, 0, 0);

public void init() {
// 定义右键菜单的事件监听器。
ActionListener menuListener = e -> {
if (e.getActionCommand().equals("绿色")) {
foreColor = new Color(0, 255, 0);
}
if (e.getActionCommand().equals("红色")) {
foreColor = new Color(255, 0, 0);
}
if (e.getActionCommand().equals("蓝色")) {
foreColor = new Color(0, 0, 255);
}
};
// 为三个菜单添加事件监听器
redItem.addActionListener(menuListener);
greenItem.addActionListener(menuListener);
blueItem.addActionListener(menuListener);
// 将菜单项组合成右键菜单
pop.add(redItem);
pop.add(greenItem);
pop.add(blueItem);
// 将右键菜单添加到drawArea对象中
drawArea.add(pop);
// 将image对象的背景色填充成白色
g.fillRect(0, 0, AREA_WIDTH, AREA_HEIGHT);
drawArea.setPreferredSize(new Dimension(AREA_WIDTH, AREA_HEIGHT));
// 监听鼠标移动动作
drawArea.addMouseMotionListener(new MouseMotionAdapter() {
// 实现按下鼠标键并拖动的事件处理器
public void mouseDragged(MouseEvent e) {
// 如果preX和preY大于0
if (preX > 0 && preY > 0) {
// 设置当前颜色
g.setColor(foreColor);
// 绘制从上一次鼠标拖动事件点到本次鼠标拖动事件点的线段
g.drawLine(preX, preY, e.getX(), e.getY());
}
// 将当前鼠标事件点的X、Y坐标保存起来
preX = e.getX();
preY = e.getY();
// 重绘drawArea对象
drawArea.repaint();
}
});
// 监听鼠标事件
drawArea.addMouseListener(new MouseAdapter() {
// 实现鼠标松开的事件处理器
public void mouseReleased(MouseEvent e) {
// 弹出右键菜单
if (e.isPopupTrigger()) {
pop.show(drawArea, e.getX(), e.getY());
}
// 松开鼠标键时,把上一次鼠标拖动事件的X、Y坐标设为-1。
preX = -1;
preY = -1;
}
});
f.add(drawArea);
f.pack();
f.addWindowListener(new WindowAdapter(){
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
f.setVisible(true);
}

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

class DrawCanvas extends Canvas {
// 重写Canvas的paint方法,实现绘画
public void paint(Graphics g) {
// 将image绘制到该组件上
g.drawImage(image, 0, 0, null);
}
}
}

实现手绘功能其实是一种假象:表面上看起来可以随鼠标移动自由画曲线,实际上依然利用GraphicsdrawLine()方法画直线,每条直线都是从上一次鼠标拖动事件发生点画到本次鼠标拖动事件发生点。当鼠标拖动时,两次鼠标拖动事件发生点的距离很小,多条极短的直线连接起来,肉眼看起来就是鼠标拖动的轨迹了。

上面程序还增加了右键菜单来选择画笔颜色。

运行上面程序,出现一个空白窗口,用户可以使用鼠标在该窗口上拖出任意的曲线

上面程序进行手绘时只能选择红、绿、蓝三种颜色,不能调岀像Windows的颜色选择对话框那种“专业”的颜色选择工具。实际上,Swing提供了对颜色选择对话框的支持,如果结合Swing提供的颜色选择对话框,就可以选择任意的颜色进行画图,并可以提供些按钮让用户选择绘制直线、折线、多边形等几何图形。如果为该程序分别建立多个BufferedImage对象,就可实现多图层效果(每个BufferedImage代表一个图层。

11.8.2 Java9增强的ImageIO

如果希望可以访问磁盘上的位图文件,例如GIFJPG等格式的位图,则需要利用ImageIO工具类。

ImageIO利用ImageReaderImageWriter读写图形文件,通常程序无须关心该类底层的细节,只需要利用该工具类来读写图形文件即可

ImageIO类并不支持读写全部格式的图形文件,程序可以通过ImageIO类的如下几个静态方法来访问该类所支持读写的图形文件格式

方法 描述
static String[] getReaderFileSuffixes() 返回一个String数组,该数组列出ImageIO所有能读的图形文件的文件后缀。
static String[] getReaderFormatNames() 返回一个String数组,该数组列出``所有能读的图形文件的非正式格式名称
static String[] getWriterFileSuffixes() 返回一个String数组,该数组列出ImageIO所有能写的图形文件的文件后缀。
static String[] getWriterFormatNames() 返回一个String数组,该数组列出ImageIO所有能写的图形文件的非正式格式名称。

程序示例 ImageIO所支持读写的全部文件格式

下面程序测试了ImageIO所支持读写的全部文件格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import javax.imageio.*;

public class ImageIOTest
{
public static void main(String[] args)
{
String[] readFormat = ImageIO.getReaderFormatNames();
System.out.println("-----Image能读的所有图形文件格式-----");
for (String tmp : readFormat)
{
System.out.println(tmp);
}
String[] writeFormat = ImageIO.getWriterFormatNames();
System.out.println("-----Image能写的所有图形文件格式-----");
for (String tmp : writeFormat)
{
System.out.println(tmp);
}
}
}

运行结果

运行上面程序就可以看到Java所支持的图形文件格式:

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
-----Image能读的所有图形文件格式-----
JPG
jpg
bmp
BMP
gif
GIF
WBMP
png
PNG
jpeg
wbmp
JPEG
-----Image能写的所有图形文件格式-----
JPG
jpg
bmp
BMP
gif
GIF
WBMP
png
PNG
jpeg
wbmp
JPEG

AWT不支持.ico图标格式

通过运行结果可以看出,AWT并不支持ico等图标格式。因此,如果需要在Java程序中为按钮、菜单等指定图标,也不要使用ico格式的图标文件,而应该使用JPGGIF等格式的图形文件

Java9增强了ImageIO的功能,ImageIO可以读写TIFF(Tag Image File Format)格式的图片。
ImageIO类包含两个静态方法:read()write(),通过这两个方法即可完成对位图文件的读写:
调用wirte方法输出图形文件时需要指定输出的图形格式,例如GIFJPEG等。

程序示例 压缩位图

下面程序可以将一个原始位图缩小成另一个位图后输出。

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
import java.io.*;
import java.awt.*;
import java.awt.image.*;
import javax.imageio.*;

public class ZoomImage {
// 下面两个常量设置缩小后图片的大小
private final int WIDTH = 80;
private final int HEIGHT = 60;
// 定义个BuffedImage对象,用于保存缩小后的位图
BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();

public void zoom() throws Exception {
// 读取原始位图
Image srcImage = ImageIO.read(new File("image/board.jpg"));
// 将原始位图缩小后绘制到image图像中
g.drawImage(srcImage, 0, 0, WIDTH, HEIGHT, null);
// 将image图像文件输出到磁盘文件中。
ImageIO.write(image, "jpeg", new File(System.currentTimeMillis() + ".jpg"));
}

public static void main(String[] args) throws Exception {
new ZoomImage().zoom();
}
}

上面程序中先从磁盘中读取一个位图文件,然后将原始位图按指定大小绘制到Image对象中,接着将Image对象输出,这就完成了位图的缩小(实际上不一定是缩小,程序总是将原始位图缩放到WIDTHHEIGHT常量指定的大小)并输出。

缩放位图的作用

上面程序总是使用board.jpg文件作为原始图片文件,总是缩放到80×60的尺寸,且总是以当前时间作为文件名来输出该文件,这是为了简化该程序。如果为该程序增加图形界面,允许用户选择需要缩放的原始图片文件和缩放后的目标文件名,并可以设置缩放后的尺寸,该程序将具有更好的实用性。

对位图文件进行缩放是非常实用的功能,大部分Web应用都允许用户上传的图片,而Web应用则需要对用户上传的位图生成相应的缩略图,这就需要对位图进行缩放

利用ImageIO读取磁盘上的位图,然后将这图绘制在AWT组件上,就可以做出更加丰富多彩的图形界面程序。

下面程序再次改写第4章的五子棋游戏,为该游戏增加图形用户界面,这种改写很简单,只需要改变如下两个地方即可:

  • 原来是在控制台打印棋盘和棋子,现在改为使用位图在窗口中绘制棋盘和棋子。
  • 原来是靠用户输入下棋坐标,现在改为当用户单击鼠标键时获取下棋坐标,此处需要将鼠标事件的X、Y坐标转换为棋盘数组的坐标。

程序示例 AWT五子棋

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
import java.awt.*;
import javax.swing.*;
import java.awt.event.*;
import java.awt.image.*;
import javax.imageio.*;
import java.io.*;

public class Gobang {
// 下面三个位图分别代表棋盘、黑子、白子
BufferedImage table;
BufferedImage black;
BufferedImage white;
// 当鼠标移动时候的选择框
BufferedImage selected;
// 定义棋盘的大小
private static int BOARD_SIZE = 15;
// 定义棋盘宽、高多少个像素
private final int TABLE_WIDTH = 535;
private final int TABLE_HETGHT = 536;
// 定义棋盘坐标的像素值和棋盘数组之间的比率。
private final int RATE = TABLE_WIDTH / BOARD_SIZE;
// 定义棋盘坐标的像素值和棋盘数组之间的偏移距。
private final int X_OFFSET = 5;
private final int Y_OFFSET = 6;
// 定义一个二维数组来充当棋盘
private String[][] board = new String[BOARD_SIZE][BOARD_SIZE];
// 五子棋游戏的窗口
JFrame f = new JFrame("五子棋游戏");
// 五子棋游戏棋盘对应的Canvas组件
ChessBoard chessBoard = new ChessBoard();
// 当前选中点的坐标
private int selectedX = -1;
private int selectedY = -1;

public void init() throws Exception {
table = ImageIO.read(new File("image/board.jpg"));
black = ImageIO.read(new File("image/black.gif"));
white = ImageIO.read(new File("image/white.gif"));
selected = ImageIO.read(new File("image/selected.gif"));
// 把每个元素赋为"╋","╋"代表没有棋子
for (int i = 0; i < BOARD_SIZE; i++) {
for (int j = 0; j < BOARD_SIZE; j++) {
board[i][j] = "╋";
}
}
chessBoard.setPreferredSize(new Dimension(TABLE_WIDTH, TABLE_HETGHT));
chessBoard.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
// 将用户鼠标事件的坐标转换成棋子数组的坐标。
int xPos = (int) ((e.getX() - X_OFFSET) / RATE);
int yPos = (int) ((e.getY() - Y_OFFSET) / RATE);
board[xPos][yPos] = "●";
/*
* 电脑随机生成两个整数,作为电脑下棋的坐标,赋给board数组。 还涉及: 1.如果下棋的点已经有棋子,不能重复下棋。 2.每次下棋后,需要扫描谁赢了
*/
chessBoard.repaint();
}

// 当鼠标退出棋盘区后,复位选中点坐标
public void mouseExited(MouseEvent e) {
selectedX = -1;
selectedY = -1;
chessBoard.repaint();
}
});
chessBoard.addMouseMotionListener(new MouseMotionAdapter() {
// 当鼠标移动时,改变选中点的坐标
public void mouseMoved(MouseEvent e) {
selectedX = (e.getX() - X_OFFSET) / RATE;
selectedY = (e.getY() - Y_OFFSET) / RATE;
chessBoard.repaint();
}
});
f.add(chessBoard);
f.pack();
f.setVisible(true);
}

public static void main(String[] args) throws Exception {
Gobang gb = new Gobang();
gb.init();
}

class ChessBoard extends JPanel {
// 重写JPanel的paint方法,实现绘画
public void paint(Graphics g) {
// 将绘制五子棋棋盘
g.drawImage(table, 0, 0, null);
// 绘制选中点的红框
if (selectedX >= 0 && selectedY >= 0)
g.drawImage(selected, selectedX * RATE + X_OFFSET, selectedY * RATE + Y_OFFSET, null);
// 遍历数组,绘制棋子。
for (int i = 0; i < BOARD_SIZE; i++) {
for (int j = 0; j < BOARD_SIZE; j++) {
// 绘制黑棋
if (board[i][j].equals("●")) {
g.drawImage(black, i * RATE + X_OFFSET, j * RATE + Y_OFFSET, null);
}
// 绘制白棋
if (board[i][j].equals("○")) {
g.drawImage(white, i * RATE + X_OFFSET, j * RATE + Y_OFFSET, null);
}
}
}
}
}
}

省略…

使用Swing组件以避免闪烁

上面程序为了避免游戏时产生闪烁感,将棋盘所用的画图区改为继承JPanel类,游戏窗口改为使用JFrame类,这两个类都是Swing组件,Swing组件的绘图功能提供了双缓冲技术,可以避免图像闪烁

标记落子点

上面游戏界面中还有一个红色选中框,提示用户鼠标所在的落棋点,这是通过监听鼠标移动事件实现的——当鼠标在游戏界面移动时,程序根据鼠标移动事件发生的坐标来绘制红色选中框。