画图板的实现
项目截图:
这是我学习Java做的第一个小软件,这是一个仿照Windows XP系统下的画图板自制的画板,功能比较简单,这个项目仅仅是我们学习如何使用java.swing、java.even、java.awt、java.io包下的组件,以及对于一个面板框架的布局。
因此,这次给大家分享这个小项目非常简单,希望大家一看就能懂。
一、关键技术点
a)窗体布局
b)组件的添加(略,这个参考api文档中的java.swing下的组件就OK啦,很简单!)
c)工具组件方法的实现——动态多态实现
d)颜色组件方法的实现
e)监听技术——鼠标监听器、动作监听器
f)面板重绘——二维数组与队列存储的比较
g)保存/打开
h)BMP文件格式
二、关键技术点的实现
a)窗体布局
在这个项目中,我运用到了两种窗体布局BorderLayout边框布局和FlowLayout流式布局,下面就简单的介绍一下这两种布局。
BorderLayout边框布局
Up up = new Up(); //菜单栏面板
JMenuBar bar = up.setUp(); //菜单栏对象
Left left = new Left();//工具栏面板容器
Center center = new Center();//绘图区面板容器
Down down = new Down();//颜料区面板容器
首先我创建了这四个面板,这四个面板的创建方法我都把它们封闭在一个类里了,在这里只能看到这四行实现化对象的过程,但从这些对象名可以很容易区分出来,up是菜单栏的面板,它的大小面板中的内容都已经在这个面板类中初始化完成了,其它left/center/down,想必大家都明白。那么最终只需要组装这几个面板就行了。
this.setJMenuBar(bar);//添加菜单栏
this.add(left.setLeft(), java.awt.BorderLayout.WEST);
this.add(down.setDown(), java.awt.BorderLayout.SOUTH);
this.add(center.setCenter(), java.awt.BorderLayout.CENTER);
FlowLayout流式布局
流式布局就是按行排列,一行放满了就换到下行去,这个布局是在各个面板容器中去实现的,通过上面的项目截图应该就很清楚的明白我是设置的从左对齐,当然也可以设置居中对齐、右对齐;例如:java.awt.FlowLayout f2 = new java.awt.FlowLayout(FlowLayout.LEFT); // 画板流式布局;其中FlowLayout.LEFT这个参数就是控制了对齐方式,我们可以改成居中对齐FlowLayout.CENTER右对齐FlowLayout.RIGHT.
b)组件的添加(略)
c)工具组件方法的实现——动态多态实现
通过Windows XP的画板我们就可以知道,左面板上有许多的工具,有的是选择工具,有的是画图工具,有的是辅助设计工具,其实要实现这些工具都不难,下面我们透过几个简介的画图工具来试试。下面要介绍的这个技术点是在Java面向对象设计语言中,非常具有代表的一项特性,java语言的主要特性有三:封装、继承、多态;
那么简单的描述一下什么是多态:一个事物可呈现出多种形态。那么具体接口的多种不同的实现方式即为多态。下面我们具体到实例中来看吧。
package MyPaint;
import java.awt.Color;
import java.awt.Graphics;
/**
* 绘图的方法体组合
*/
public abstract class Shapenes {
public int x1,y1,x2,y2,type;
public Color color;
public Shapenes(int x1, int y1, int x2, int y2,int type,Color color) {
super();
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
this.type = type;
this.color = color;
}
public abstract void draw(Graphics peaper);
}
这是一个抽象类,其实刚才讲的接口,也就是一个纯抽象类,但抽象类是介于抽象类与普通类之间的数据结构。这里我们可以看到这个抽象类中,有变量的声明,构造方法,还有一个很重要的元素,就是这个抽象方法abstract,何为抽象方法,就是没有实现的方法,在没有定义之前它不是真正存在的;只有当其它类继承了这个抽象类,实现了抽象类中的draw这个抽象方法,这个方法才是真正存在。所有下面我们可以再来构造其它的画图工具来继承这个抽象类,那可能讲了这么多,大家还是不明白我为什么要这样做;其实后面很简单,我就是要用一个对象名去调用不同对象的方法,请看实例:
/**
* 创建绘图工具
*/
//图形的对象的存储,实现了一个对象可以接受各种不同的
Shapenes shape = new Drawline(x1,y1,x2,y2,0,color);
if (Button_name.equals("Line")){
shape = new Drawline(x1,y1,x2,y2,0,color);
}
else if (Button_name.equals("Rect"))
{shape = new DrawRect(x1,y1,x2,y2,1,color);}
else if (Button_name.equals("Oval"))
{shape = new DrawOval(x1,y1,x2,y2,2,color);}
else if (Button_name.equals("fillOval"))
{shape = new DrawFOval(x1,y1,x2,y2,3,color);}
else if (Button_name.equals("fill3DRect"))
{shape = new Draw3DRect(x1,y1,x2,y2,4,color);}
else if (Button_name.equals("save"))
{System.out.println("save");}
else if (Button_name.equals("open"))
{System.out.println("open");}
shape.draw(peaper);
list.add(shape);
}
从代码中可以看出,shape可以根据判断的不同,赋予的对象也不同,而最后我都只管调用这一个方法shape.draw(peaper);最终可以画出我想要的各种图形。因此这里就是实现了动态多态技术。如果还不清晰,我向大家举一个例子:抽象类在这里发挥出了巨大的优势,这个类就好像一个铅笔盒,可以放入各种学习用品,铅笔、钢笔、橡皮擦、尺子……当铅笔盒盖上的时候它是一个整体,但取出来的时候又可以变成许多的个体,有效的节约了物理空间;那么在程序中就相当于简化了程序。
d)颜色组件方法的实现
自认为本项目的颜料盒做得最差,但这确实是时间的原因,在这里只是意思一下了,而如果真正仿照Windows XP中的画板,这里的颜料盒应该使用GridLayout网格布局,每一格是一种颜色,这样成和Windows XP中的画板是一模一样的;在这里只能简单的介绍一下,后面再接着去完善。
e)监听技术——鼠标监听器、动作监听器
鼠标监听器:鼠标监听器是用于监听鼠标在中间画布的操作,我们可看看MouseDragGestureRecognizer鼠标监听器中的方法,以下是其中的一部分方法:
public void mouseClicked(MouseEvent e) { }
public void mousePressed(MouseEvent e) { }
public void mouseReleased(MouseEvent e) { }
public void mouseEntered(MouseEvent e) { }
public void mouseExited(MouseEvent e) { }
public void mouseDragged(MouseEvent e) { }
public void mouseMoved(MouseEvent e) { }
这几个方法的功能从这些方法名称就可以明白,因此这里不多解释了,在这些方法中我们可以进行相应的操作,来达到我们想到的操作,例如:MouseEvent e可以获取鼠标的坐标,这样就可确定图形绘制的位置,不同的图形可能需要的坐标点的数量不同,但没关系是可以解决的。
动作监听器
是用来监听工具面板和颜料面板上的按钮动作,当选中某个按钮时在监听器中自动触发相应的动作,当然前提是这些面板上的对象都是在这个监听器的范围之内;所以我们还需要注意无论是鼠标监听器、动作监听器,它只是一个附加功能,最终都需要被对象附加上去。
下面请看一下动作监听器ActionListener接口中的方法:
public void actionPerformed(ActionEvent e);
这个接口中只含有一个方法,并且这个方法还没有实现,那么我们使用这个功能时就必需要先实现这个方法,这个方法如何实现,就和鼠标监听器的方法差不多,那我们要看一下,哪些功能器件要使用到动作监听器,刚才例出了工具面板中的对象,颜料面板中的对象,其实还有一个是菜单栏中的菜单选项。具体实例不看了。
f)面板重绘——二维数组与队列存储的比较
面板重绘是指将缓存中的数据保存到内存中去,如果没有重绘操作你会发现,当你在绘图区绘图之后,只要窗体发现改变,刚刚绘制的图形就消失了,这就是因为刚刚绘制的图形在缓存中,还没有被保存到内存中去,那么重绘就是要来解决这个问题,首先要把画布上的内容使用数据容器,保存到内存中去,当窗体发生变化时,面板重新从内存中调取保存的内容绘制在面板上,这样就达到了面板恢复的效果,但是绘画区的内容暂时保存到内存中去也不能保证万无一失,内存中的内容也不是永久性的,如果计算机发现断电时,内存中的数据会消失,这样依然无法保证我们的成果。这就需要通过文件存取的方式保存到外存中,那样我们就可以永存数据不至于丢失。文件操作在下一节中讲,这里我们暂时来实现,将缓存中的数据保存到内存中去。
刚讲了把画布上的内容使用数据容器,保存到内存中去,那么这个关键技术点就在于这个数据容器上了,如果一个数据容器设计得巧合,那么对于项目操作肯定是有益的。那么想想在Java中数据容器有很多:Set 、List 、Map ……,那哪些是更适合的呢?这里我将针对两种数据容器来使用比较两者的优劣性。
第一:队列
想必大家对队列已经很熟悉了,那么我直接分享队列在保存中的使用,其实在这里队列还涉及到一个很重要的知道点,那就是泛型,如果有不清楚的可以自己去查阅资料;首先我们设想一下,我们可以将每次画上去的图形为一个对象,保存到队列中去,这个对象可能涉及到坐标、颜色、以及是个什么图形。
好,根据提出的这些设想,那么我们就开始实现了,请看实例:
package MyPaint;
import java.awt.Color;
import java.awt.Graphics;
/**
* 绘图的方法体组合
*/
public abstract class Shapenes {
public int x1,y1,x2,y2,type;
public Color color;
public Shapenes(int x1, int y1, int x2, int y2,int type,Color color) {
super();
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
this.type = type;
this.color = color;
}
public abstract void draw(Graphics peaper);
}
这是我们在之前在实现工具的动态多态中所使用的abstract抽象方法,在这里我们又可能用到它了,我们将它作为每个图形的对象,保存到队列中去,从这个类中可以看出来,它保存了对象的两个坐标点、颜色、图形的类型;就是这三个关键元素构成了我们想到的图形,我们既然可以通过这三个元素画出相应的图形,我们保存这三个元素当然能够还原相应的图形。确定这一点后,我们可以继续往下走:
//图形的对象的存储,实现了一个对象可以接受各种不同的
Shapenes shape = new Drawline(x1,y1,x2,y2,0,color);
if (Button_name.equals("Line")){
shape = new Drawline(x1,y1,x2,y2,0,color);
}
else if (Button_name.equals("Rect"))
{shape = new DrawRect(x1,y1,x2,y2,1,color);}
else if (Button_name.equals("Oval"))
{shape = new DrawOval(x1,y1,x2,y2,2,color);}
else if (Button_name.equals("fillOval"))
{shape = new DrawFOval(x1,y1,x2,y2,3,color);}
else if (Button_name.equals("fill3DRect"))
{shape = new Draw3DRect(x1,y1,x2,y2,4,color);}
else if (Button_name.equals("save"))
{System.out.println("save");}
else if (Button_name.equals("open"))
{System.out.println("open");}
shape.draw(peaper);
list.add(shape);
}
这个方法在是鼠标监听器public void mouseReleased(MouseEvent e)中实现的,在这里我们又可以看到动态多态的优势了,我们绘制不同的图形,最终我们不仅仅是用这一个对象还调用绘画方法,而且用需要保存这一个对象就可以完成对所有不同图形的保存。这个设计模式是太完美了。保存完图形之后,我们还没有完成重点操作,就是实现面板的重绘,要将这些保存的数据与重绘方法结合起来,让这个数据可以随着重绘操作实现更新面板。
这个重绘操作是在中间面板中来完成的,我们要把保存在队列中的数据传到这个面板类中来,在JPanel中有一个重绘方法,我们需要重写这个方法,请看实例:
class MyJPanel extends JPanel {
public void paint(Graphics g){
super.paint(g);
ListSave<Shapenes> list = mouselisterner.list;///调用子类的变量,需要将mouselisterner定义成子类才行
for(int i=0;i<list.size();i++){
Shapenes shape = (Shapenes) list.get(i); ///新定义一个变量来接收队列中的元素
shape.draw(g);
}
}
}
这里我们定义了一个MyJPanel内部类,那这个类继承了javax.swing.JPanel这个类,重写了public void paint(Graphics g)这个方法,这个方法正式我们要用到的重绘方法,先要调用super.paint(g);父类中的绘图方法,把之前的内容也绘制出来,不然之后绘制的图形会刷新面板使得图形无法显示。在调用完父类中的绘图方法之后,再把队列中的数据绘制上去,这样就实现了重绘操作,之后无论你是“最小化”、“移动窗体”操作都不会对面板中的图形有影响。
第二:二维数组
看完二维数组的保存方法之后,你也许会惊讶二维数组的方便。可以分析一下,我们每幅图画都可以看成是一个矩形,那么矩形岂不是有长和宽,那么在这个矩形区域由许多的像素点组成,而在计算机中每个像素点是可以用一个Int数据类型来表示,这样我们利用图片的分辨率还定义数组,数组内的元素都是存在了像素点的颜色Int值。请看代码实例:
/**
* 将图像存入数组中
*/
// 创建机器人来截取图像
try {
// 创建机器人对象
Robot robot = new Robot();
// 获取画布对象
JPanel center_peaper = (JPanel) e.getSource();
// 获取画布的位置
Point point = center_peaper.getLocationOnScreen();
// 获取画面的高度宽度
Dimension dimension = center_peaper.getPreferredSize();
// 设置截图的位置、高、宽
Rectangle screenRect = new Rectangle(point, dimension);
// 初始化二维数组
saveImage = new int[dimension.height][dimension.width];
imageR = new int[dimension.height][dimension.width];
imageG = new int[dimension.height][dimension.width];
imageB = new int[dimension.height][dimension.width];
// 机器人开始截图
BufferedImage bufferedImage = robot.createScreenCapture(screenRect);
// 将图像数据存入二维数组中
for (int i = dimension.height - 1; i >= 0; i--)
for (int j = 0; j < dimension.width; j++) { // 自定义文件格式保存
int image = bufferedImage.getRGB(j, i);
saveImage[i][j] = image;
filetype = 1;
}
}
思路很清晰了,通过代码实例来看,我们所做的无非就是把画板上的图像截取下来,按照图像的分辨率实例化了一个二维数组,用这个数组来保存截图每个点上的颜色值;但这里还是有几个重要的技术点,一个是Robot机器人类的使用,这个不难可以参考API文档,另外一个就是这个图像数据存入二维数组时,要注意图像的长度与二维数组的下标相对应,如果对应错了图像是会发现错误的。通过二维数组保存之外就可以传入重绘方法中去,让重绘方法调用数组中的数据进行重绘。操作就是这么简单,具体Robot的操作在上面的代码中都体现出来了,希望大家一看就懂!通过使用这两种数据容器来保存数据,想必大家会有青睐的一种,无论哪一种,我觉得都有它的优劣性,具体也不分析了,总之学会一种方式就很好了!
g)保存/打开
刚刚我们实现了重绘,那么这个环节我们把内存中的数据再写到外存中去,进行永久的保存,我们依然可以选择其中一种数据容器来进行保存,那么我们要怎么要写入文件呢?拿这个队列为例:
public void actionPerformed(ActionEvent e) {
String command = e.getActionCommand();
/**
* 文件保存操作
*/
if (command.equals("save")) {
// 文件保存对象
FileSave filesave = new FileSave();
try {
/**
* 将队列保存到文件中去
*/
filesave.output(lis.list);
} catch (Exception e1) {
e1.printStackTrace();
}
}
/**
* 文件打开操作
*/
else if (command.equals("open")) {
// 文件打开对象
FileOpen fileopen = new FileOpen();
try {
/**
* 将文件从文件中读到列队中来
*/
lis.list = fileopen.input();
center_peaper.updateUI();
} catch (Exception e1) {
e1.printStackTrace();
}
}
}
这个方法是在动作监听器中实现的,我们要在单击了“保存”或“打开”这个选项时,我们才会触发相应的动作,这个时候如果单击了“保存”,那么我们首先要创建一个文件,文件名由用户自定义,如果文件创建成功则调用这个文件保存output方法:
public void output(ListSave<Shapenes> list) throws Exception{
//文件输出流
FileOutputStream outputfile = new FileOutputStream("g:\\ss\\savepaint.txt");
//原始数据输出流
DataOutputStream outstream = new DataOutputStream(outputfile);
//写入图形对象的个数,即文件头
outstream.write(list.size());
for(int i=0;i<list.size();i++){
Shapenes shape = list.get(i);
outstream.writeInt(shape.x1);
outstream.writeInt(shape.y1);
outstream.writeInt(shape.x2);
outstream.writeInt(shape.y2);
outstream.writeInt(shape.type);
outstream.writeInt(shape.color.getRGB());
}
//强制输出
outstream.flush();
//关闭文件输出流
outputfile.close();
}
通过这个过程,我们就可以把我们绘制的作品写入到文件中去了,那么打开操作就是保存互逆的一个过程;如果保存实现了,打开就没必要再多分享了。写入文件后要记得flush一下,close一下。
h)BMP文件格式
其实在这个项目中,还实现了BMP文件格式的解析,在项目截图就可看到绘图板上的那幅图像是BMP的一张图片,BMP是Windows中自带的一种图像文件格式,这种文件格式在网上已经被公开,只要知道这种文件的格式就很好读取出来,显示在我们自制的绘画板上,同样我们也可将我们绘制的图像保存成为BMP格式,这样我们的作品就要以与Windows直接交互使用。
BMP的文件格式在这里不去分析了,只是将我所写的代码分享如下!
public class ButtonListen implements ActionListener {
private int[][] saveImage;
public static int[][] imageR;
public static int[][] imageG;
public static int[][] imageB;
public static int image_width;
public static int image_heigh;
public void actionPerformed(ActionEvent e) {
/**
* 画板内容的保存
*/
if (e.getActionCommand().equals("save")) {
// 创建保存对话框
JFileChooser filechooser = new JFileChooser();
int num = filechooser.showSaveDialog(null);
if (num == 0) {
String path = filechooser.getSelectedFile().getAbsolutePath();
ButtonListen file = new ButtonListen();
try {
file.savefile(path);
} catch (Exception e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
} else if (e.getActionCommand().equals("open")) {
// 创建打开对话框
JFileChooser filechooser = new JFileChooser();
int num = filechooser.showOpenDialog(null);
if (num == 0) {
String path = filechooser.getSelectedFile().getAbsolutePath();
ButtonListen file = new ButtonListen();
try {
file.openfile(path);
} catch (Exception e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
}
}
private void openfile(String path) throws Exception{
// TODO Auto-generated method stub
//获取文件名
File filename =new File(path);
String name = filename.getName().toLowerCase();
String key = ".bmp";
// 创建文件输入流
FileInputStream infile = new FileInputStream(path);
// 创建原始数据输入流
DataInputStream instream = new DataInputStream(infile);
if(name.indexOf(key)!=-1){///用BMP格式打开
//读入BMP文件头,读取14个字节
int fileheadsize = 14;
byte filehead [] = new byte[fileheadsize];
instream.read(filehead, 0, fileheadsize);
//读入位图信息头,读取40个字节
int filenewssize = 40;
byte filenews [] = new byte[filenewssize];
instream.read(filenews, 0, filenewssize);
//从位图信息头中获取重要数据
image_width = ChangeInt(filenews,7);
image_heigh = ChangeInt(filenews,11);
int image_bit = (((int)filenews[15]&0xff)<<8)|(int)filenews[14]&0xff; //图像的色深位数
int image_size =ChangeInt(filenews,23);//源图的大小
//读取位图中图像的数据
int skip_width = 0;
if(!(image_width*3%4==0)){
skip_width = 4-image_width*3%4;
}///后面补0的情况处理
//装载RGB颜色的数据数组
imageR = new int [image_heigh][image_width];
imageG = new int [image_heigh][image_width];
imageB = new int [image_heigh][image_width];
//按行读取,H,W倒着读
for(int h=image_heigh-1;h>=0;h--)
for(int w=0;w<image_width;w++)
{
int blue = instream.read();
int green = instream.read();
int red = instream.read();
imageB[h][w] = blue;
imageG[h][w] = green;
imageR[h][w] = red;
if(w == 0){
System.out.println(instream.skipBytes(skip_width));
}
}
infile.close();
}else {////用自定义格式打开
int h = instream.readInt();
int w = instream.readInt();
saveImage = new int[h][w];
for (int i = 0; i <h; i++)
for (int j = 0; j <w; j++) {
saveImage[i][j] = instream.readInt();
}
infile.close();
//将读取到的数据传入重绘数组
Mouse_Lis.saveImage = saveImage;}
}
三、关键技术点的总结
这个项目的主要技术点已经分享完了,最后还做一个小小的总结,这个项目如果有时间我会继续改进;通过这次项目的演练,我对于Swing、Awt下的组件的使用都有了成熟的经验,组件更换显示图标,组件的分布,组件的监听都有了比较感性的认识,尤其是对于对象与类的区别,封装、继承、多态等等这些基础的知识点。但同时也知道对于一个桌面应用程序来说,要做好做完善是需要不断的维护和更新的,这也要求我们自己要不断的进步和学习,这样才能开发出更优秀的软件。