画图板的实现

画图板的实现
项目截图:
画图板的实现

    这是我学习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下的组件的使用都有了成熟的经验,组件更换显示图标,组件的分布,组件的监听都有了比较感性的认识,尤其是对于对象与类的区别,封装、继承、多态等等这些基础的知识点。但同时也知道对于一个桌面应用程序来说,要做好做完善是需要不断的维护和更新的,这也要求我们自己要不断的进步和学习,这样才能开发出更优秀的软件。

你可能感兴趣的:(画图板)