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_RGB 、BufferedImage.TYPE_BYTE_GRAY 等值。 |
在位图上绘图
除此之外,BufferedImage
还提供了一个getGraphics()
方法返回该对象的Graphics
对象,从而允许通过该Graphics
对象向Image
中添加图形。
通过BufferedImage
在AWT
上使用缓冲
借助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; 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 image = new BufferedImage(AREA_WIDTH, AREA_HEIGHT, BufferedImage.TYPE_INT_RGB); 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.add(pop); 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) { if (preX > 0 && preY > 0) { g.setColor(foreColor); g.drawLine(preX, preY, e.getX(), e.getY()); } preX = e.getX(); preY = e.getY(); drawArea.repaint(); } }); drawArea.addMouseListener(new MouseAdapter() { public void mouseReleased(MouseEvent e) { if (e.isPopupTrigger()) { pop.show(drawArea, e.getX(), e.getY()); } 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 { public void paint(Graphics g) { g.drawImage(image, 0, 0, null); } } }
|
实现手绘功能其实是一种假象:表面上看起来可以随鼠标移动自由画曲线,实际上依然利用Graphics
的drawLine()
方法画直线,每条直线都是从上一次鼠标拖动事件发生点画到本次鼠标拖动事件发生点。当鼠标拖动时,两次鼠标拖动事件发生点的距离很小,多条极短的直线连接起来,肉眼看起来就是鼠标拖动的轨迹了。
上面程序还增加了右键菜单来选择画笔颜色。
运行上面程序,出现一个空白窗口,用户可以使用鼠标在该窗口上拖出任意的曲线
上面程序进行手绘时只能选择红、绿、蓝三种颜色,不能调岀像Windows
的颜色选择对话框那种“专业”的颜色选择工具。实际上,Swing
提供了对颜色选择对话框的支持,如果结合Swing
提供的颜色选择对话框,就可以选择任意的颜色进行画图,并可以提供些按钮让用户选择绘制直线、折线、多边形等几何图形。如果为该程序分别建立多个BufferedImage
对象,就可实现多图层效果(每个BufferedImage
代表一个图层。
11.8.2 Java9增强的ImageIO
如果希望可以访问磁盘上的位图文件,例如GIF
、JPG
等格式的位图,则需要利用ImageIO
工具类。
ImageIO
利用ImageReader
和ImageWriter
读写图形文件,通常程序无须关心该类底层的细节,只需要利用该工具类来读写图形文件即可
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
格式的图标文件,而应该使用JPG
、GIF
等格式的图形文件。
Java9
增强了ImageIO
的功能,ImageIO
可以读写TIFF
(Tag Image File Format)
格式的图片。
ImageIO
类包含两个静态方法:read()
和write()
,通过这两个方法即可完成对位图文件的读写:
调用wirte
方法输出图形文件时需要指定输出的图形格式,例如GIF
、JPEG
等。
程序示例 压缩位图
下面程序可以将一个原始位图缩小成另一个位图后输出。
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; 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")); g.drawImage(srcImage, 0, 0, WIDTH, HEIGHT, null); ImageIO.write(image, "jpeg", new File(System.currentTimeMillis() + ".jpg")); }
public static void main(String[] args) throws Exception { new ZoomImage().zoom(); } }
|
上面程序中先从磁盘中读取一个位图文件,然后将原始位图按指定大小绘制到Image
对象中,接着将Image
对象输出,这就完成了位图的缩小(实际上不一定是缩小,程序总是将原始位图缩放到WIDTH
、HEIGHT
常量指定的大小)并输出。
缩放位图的作用
上面程序总是使用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("五子棋游戏"); 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] = "●";
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 { 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组件的绘图功能提供了双缓冲技术,可以避免图像闪烁。
标记落子点
上面游戏界面中还有一个红色选中框,提示用户鼠标所在的落棋点,这是通过监听鼠标移动事件实现的——当鼠标在游戏界面移动时,程序根据鼠标移动事件发生的坐标来绘制红色选中框。