简易画板
- UI界面设置
- 监听器类
- 图形保存
github源代码下载: 画图工具源代码.
UI界面设置
根据我们想实现的画板要求,可以分步骤进行解决。在创建画板之前,首先创建窗体框架,利用java提供的工具包中的JFrame窗口类就可以创建一个基本的框架。这个框架是之后添加画板和画笔的最顶层的容器。就好比吃饭的桌子,没有这个桌子,盘子和碗筷都无法放置。当我们把这个顶层容器设置好后,我们就可以进一步添加我们的相应功能组件了,也就是在这个桌子上放东西。创建窗体代码如下:
JFrame jf = new JFrame();
jf.setTitle("画图板");
jf.setLocationRelativeTo(null);//设置窗体为正中央
jf.setSize(700, 600);//设置窗体大小
jf.setLayout(new FlowLayout()); 设置布局为流式布局
jf.setVisible(true); //设置窗体可见
当我窗体创建好后,接下来就要添加东西了,首先就是画板。在此之前我想先简要介绍一下JPanel类,JPanel类也是java提供的一个工具类,根据它我们可以创建一个面板容器添加到我们的窗体当中,来帮助我们更好的组织我们接下来要放置的元素组件。可以简单理解为用一个个餐垫将桌子的放置区域划分的更细更紧凑。
关于组件的添加位置,还要先了解一下Jframe的层次结构,当我们向窗体添加组件时是添加到 containPane内容面板。直接添加到窗体中实质上就是添加到containPane上。
JFrame jf = new JFrame();
jf.add(component);//等价
jf.getContentPane().add(component);
链接: jframe层次结构.
同样的方法,可以利用工具包中的JButton创建功能按钮,并且添加到窗体中,创建方法同上,这里就不赘述了。
至此已经实现了画板,然后就可以创建画笔了,JPanel可以调用父类中getGraphics方法可以获得画笔,类型为Graphics。画笔的操作对象是图形坐标系的像素点。
(说明一点:所有组件的添加要放在jf.setVisible(true)之前,而getGraphics()一定要在jf.setVisible(true)之后再调用,否则编译时不会报错,但在运行时画笔会报空指针异常)
在图形坐标系中,其原点位于构件的左上角,坐标轴沿向下和右的方向增长,小圈代表坐标,正方形代表像素,坐标位于像素之间:
获取画笔方法如下:
//jf.setVisible(true)
JPanel jp=new JPanel();
Graphics gf = jp.getGraphics();
至此我们的基本画板界面就可以创建好了:
public class Paint extends JPanel{
public static void main(String[] args) {
Paint p = new Paint();
}
JFrame jf = new JFrame();
public Paint() {
//实现一个窗体
jf.setTitle("画图板");
jf.setSize(700, 600);
jf.setLocation(600,250);//窗口左上角起点
jf.setLayout(new FlowLayout()); //设置布局为流式布局
//声明两个数组,存储按钮指示
String[] command = { "开始", "清除", "直线", "曲线", "多边形", "矩形","圆形" };
Color[] color = { Color.BLACK,Color.BLUE,Color.YELLOW, Color.RED, Color.GREEN };
//将按钮添加到窗体中
for (int i = 0; i < command.length; i++) {
JButton jb = new JButton(command[i]);
jf.add(jb);
}
for (int i = color.length - 1; i >= 0; i--) {
JButton jb = new JButton();
jb.setBackground(color[i]);//设置背景颜色
Dimension dm = new Dimension(20, 20);//设置大小
jb.setPreferredSize(dm);
jf.add(jb);
}
this.setPreferredSize(new Dimension(1600, 1400));
this.setBackground(Color.LIGHT_GRAY);//画板背景颜色
//将JPanel对象添加进入jf窗体对象中,让他生效
jf.add(this);
jf.setDefaultCloseOperation(3);
jf.setVisible(true); //设置窗体可见
Graphics gf = this.getGraphics(); //获取画笔,我们的this代表当前类的对象,正好是一个JPanel的对象
}
}
监听器类
接下来就可以开始实现画图功能了:
我们都用过windows自带的画图工具,假如要画一条直线,鼠标点击左键不松开,拖动一段距离,松开左键,这时一条直线就画好了。这里先引入一个概念:鼠标监听。
在JPanel中可以调用父类中的一个方法addMouseListener(lis),顾名思义,添加鼠标监听器,监听对象就是JPanel的实例对象(这里指的是画板),并且鼠标会根据在监听对象上的做的举动做出响应事件(例如:在画板上拖动鼠标则画出线条)。
说明一下重写方法public void mouseClicked(MouseEvent e) 调用的时间:鼠标按下并松开后(pressed和realeased方法均已发生),如果鼠标的始末的坐标不变,则clicked被调用,否则不被调用。这是容易搞错的地方。
除此之外,还有其他监听方法addMouseMotionListener(lis),addActionListener(lis)读者可自行了解。这里再说明一下,调用这些方法的时候,发现括号中形参列表需要传入的参数是接口类型,然而接口是无法直接创建对象的,所以要创建相应的接口子类对象lis来实现。之后在以上代码相应地方为监听对象添加这些方法就行了。
现在就根据不同的监听方法来设置我们的响应事件。
创建一个Listener接口子类来实现响应事件:
public class Listener implements ActionListener,MouseListener,MouseMotionListener {
//一定要将接口的所有方法重写
private Graphics gr;
private String command="";
private Paint p;
int x0=0,y0=0,//曲线的起始坐标
x1=0,y1=0,x2=0,y2=0,//其他图形
x3=0,x4=0,y3=0,y4=0,
start_x=0,start_y=0;//多边形起点
//初始化数据
//基本数据类型向形参传递的是副本,引用数据类型(类,接口,数组)向形参传递的地址,如下传递jp
public void set_gr(Graphics G) {
this.gr = G;
}
public void set_p(Paint p)
{
this.p=p;
}
/*
* 这里必须先点击被命名的按钮(即要画什么图形),将"command"初始化,
* 否则后面的监听回应方法都不能执行,如果不点击颜色按钮则不会为画笔setcolor默认黑色
*/
public void actionPerformed(ActionEvent e)
{
// TODO Auto-generated method stub
//只不过每次使用之前把画笔的颜色修改
x1 = 200; y1 = 200;
if (e.getActionCommand()=="") {
//j暂存颜色按钮
//getSource得到的组件的名称,而getActionCommand得到的是标签。
JButton j = (JButton)e.getSource();
//将画笔设置为按钮的背景颜色
gr.setColor(j.getBackground());
}
else {
command = e.getActionCommand();
if("清除".equals(command))
{
//JFrame和JPanel的repaint方法是同一个,都是继承自 JComponent的,对该方法的调用执行的都是同样的代码的。
p.repaint();
}
}
}
//鼠标按键在组件上按下(长按)并拖动时(在拖动的过程中)调用。 (处理鼠标拖动事件)
public void mouseDragged(MouseEvent e) {
if("曲线".equals(command)) {
x1 = x0; y1 = y0; //鼠标按下的时候就获得了起始坐标,调用了mousePressed
x0 = e.getX(); y0 = e.getY();
gr.drawLine(x1,y1,x0,y0);
}
}
// Override鼠标监听//点击鼠标点击时间,用来绘画多边形
public void mouseClicked(MouseEvent e) //是点击和松开两步完成后再调用 在此之前,pressed 和released都已经被调用
{
// TODO Auto-generated method stub
if("多边形".equals(command))
{
if(x4==0&&y4==0)
{
x4 = e.getX(); y4 = e.getY();
start_x = x4; start_y = y4;
//记录多边形的起点位置
}
else {
x3 = x4; y3 = y4;//这里的x3,y3为上一条边的终点
x4 = e.getX(); y4 = e.getY();
gr.drawLine(x3, y3, x4, y4);//将这点与上一条边的终点连起来
}
if(e.getClickCount()==2)
{
x4 =0; y4=0;//这里设置为零是为了能都再次执行if语句
gr.drawLine(start_x, start_y, e.getX(), e.getY());//将双击的终点位置和开始记录的起点位置相连
}
}
}
//当鼠标按下的时候,获取起点的坐标
public void mousePressed(MouseEvent e)
{
// System.out.println("按下");
if("曲线".equals(command)){
x0 = e.getX();
y0 = e.getY();
}
// TODO Auto-generated method stub
x1 = e.getX();
y1 = e.getY();
}
//当鼠标松开的时候,获取终点的坐标
public void mouseReleased(MouseEvent e)
{
// TODO Auto-generated method stub
x2 = e.getX();
y2 = e.getY();
// System.out.println("松开");
if("直线".equals(command)){
gr.drawLine(x1, y1, x2, y2);
}
else if("矩形".equals(command)) {
gr.drawRect(Math.min(x1, x2), Math.min(y1, y2), Math.abs(x1-x2), Math.abs(y1-y2));
}
else if("圆形".equals(command)) {
//Math.min(x1, x2), Math.min(y1, y2),确定画图的方向起点
//drawoval 和drawrect 都是以某个点 向着坐标点增大的方向画图
//Math.abs(x1-x2)求绝对值
gr.drawOval(Math.min(x1, x2), Math.min(y1, y2), Math.abs(x1-x2), Math.abs(y1-y2));
}
}
//鼠标进入窗口
public void mouseEntered(MouseEvent e)
{
}
//鼠标退出窗口
public void mouseExited(MouseEvent e)
{
}
//鼠标在移动的过程中调用(没有点击)
public void mouseMoved(MouseEvent e)
{
}
}
现在我们的画板基本算是完成了
图形保存
但是会发现,在隐藏窗口或者拉动窗口的时候图案又消失了,这是因为窗体会默认自动调用一个父类的paint()方法,他会重新绘制组件,但是不会重新绘制图形,所以我们就要想到重写父类的paint()方法,在重绘组件的基础上添加重绘图形方法。重绘图形就要用到之前绘制的图案的基本信息,因此我们还需要创建一个类用来存储之前画的图案的信息(图案名称,坐标点,颜色等)
如下:
public class Shape{
private int x1,y1,x2,y2;
private String name;
private Graphics g;
private Color color;
public Shape(int x1,int y1,int x2,int y2,String name,Color color)
{
this.x1=x1;
this.x2=x2;
this.y1=y1;
this.y2=y2;
this.name=name;
this.color=color;
}
public Color getcolor()
{
return color;
}
public void Repaint(Graphics g)
{
switch(name)
{
case "直线":
g.drawLine(x1, y1, x2, y2);
break;
case "矩形":
g.drawRect(Math.min(x1, x2), Math.min(y1 ,y2), Math.abs(x1-x2), Math.abs(y1-y2));
break;
case "圆形":
g.drawOval(Math.min(x1, x2), Math.min(y1 ,y2), Math.abs(x1-x2), Math.abs(y1-y2));
break;
case "曲线":
g.drawLine(x1, y1, x2, y2);
break;
case "多边形":
g.drawLine(x1, y1, x2, y2);
break;
}
}
}
这样就可以问题就解决了,再补充一个要点,在重写了父类paint()方法后,响应类清除图案这里就不能直接调用repaint()方法了,因为repaint()方法调用的就是之前重写过的paint()方法。所以需要重写创建一个paint1()方法,这个方法里只有父类重绘组件的功能,然后在响应类里面调用。
下面附上源代码:
Paint类:
public class Paint extends JPanel{
//建立一个很大的数组,用来存放图形元素,
private Shape[] shape=new Shape[10000];
public static void main(String[] args) {
Paint p = new Paint();
}
JFrame jf = new JFrame();
//实例化一个ButtonListener对象,实现了多种接口
Listener lis= new Listener();
public Paint() {
//实现一个窗体
jf.setTitle("画图板");
jf.setLocation(600,250);//窗口左上角起点
jf.setSize(700, 600);
jf.setLayout(new FlowLayout());
//声明两个数组,包含各种指令
String[] command = { "开始", "清除", "直线", "曲线", "多边形", "矩形","圆形" };
Color[] color = { Color.BLACK, Color.BLUE, Color.YELLOW, Color.RED, Color.GREEN };
/*在下面的两个循环中将各种按钮添加进入动作监听器中,其中addActionListener参数为lis,lis是一个Buttonlistener的对象*/
for (int i = 0; i < command.length; i++) {
JButton jb = new JButton(command[i]);
jb.addActionListener(lis);
jf.add(jb);
}
for (int i = color.length - 1; i >= 0; i--) {
JButton jb = new JButton();
jb.setBackground(color[i]);//设置背景颜色
Dimension dm = new Dimension(20, 20);//设置大小
jb.setPreferredSize(dm);
jb.addActionListener(lis);
jf.add(jb);
}
//将JPanel对象添加进入jf窗体对象中,让他生效
//jpanel画板的大小和颜色
this.setPreferredSize(new Dimension(1600, 1400));
this.setBackground(Color.LIGHT_GRAY);
jf.add(this);
jf.setDefaultCloseOperation(3);
jf.setVisible(true); //设置可见
Graphics gf = this.getGraphics(); //获取画笔,我们的this代表当前类的对象,正好是一个JPanel的对象
this.addMouseListener(lis);
this.addMouseMotionListener(lis);
lis.set_gr(gf);
lis.set_shape(shape);
lis.set_p(this);
}
//这里是重写jpanel中的paint
public void paint(Graphics g)
{
super.paint(g);//这是原先的父类方法 本来只要绘制组件的功能
//所以现在我们要加一个绘制图形的功能如下
for(int i=0;i<=shape.length;i++)
{
if(shape[i]!=null)
{
g.setColor(shape[i].getcolor());
shape[i].Repaint(g);
}
else
break;
}
}
public void paint1(Graphics g)
{
super.paint(g);
}
}
Listener类:
public class Listener implements ActionListener,MouseListener,MouseMotionListener {
//一定要将接口的所有方法重写
private int index=0;
private Graphics gr;
private String command="";
private Shape[] shape=new Shape[10000];
private Paint p;
int x0=0,y0=0,//曲线的起始坐标
x1=0,y1=0,x2=0,y2=0,//其他图形
x3=0,x4=0,y3=0,y4=0,
start_x=0,start_y=0;//多边形起点
public void set_gr(Graphics G) {
this.gr = G;
}
public void set_shape(Shape[] shape)
{
this.shape=shape;
}
public void set_p(Paint p)
{
this.p=p;
}
/*
* 这里必须先点击被命名的按钮(即要画什么图形),将"zhiling"初始化, 否则后面的监听回应方法都不能执行,如果不点击颜色按钮
* 则不会为画笔setcolor 默认黑色
*/
public void actionPerformed(ActionEvent e)
{
// TODO Auto-generated method stub
//每次使用之前把画笔的颜色修改
x1 = 200; y1 = 200;
if (e.getActionCommand()=="") {
//j暂存颜色按钮
//getSource得到的组件的名称,而getActionCommand得到的是标签
JButton j = (JButton)e.getSource();
//将画笔设置为按钮的背景颜色
gr.setColor(j.getBackground());
}
else {
command = e.getActionCommand();
if("清除".equals(command))
{
p.paint1(gr);
}
}
}
//鼠标按键在组件上按下(长按)并拖动时(在拖动的过程中)调用。 (处理鼠标拖动事件)
public void mouseDragged(MouseEvent e) {
if("曲线".equals(command)) {
x1 = x0; y1 = y0; //鼠标按下的时候就获得了起始坐标
x0 = e.getX(); y0 = e.getY();
gr.drawLine(x1,y1,x0,y0);
shape[index++]= new Shape(x1,y1,x0,y0,command,gr.getColor());
}
}
// Override鼠标监听//点击鼠标点击时间,用来绘画多边形
public void mouseClicked(MouseEvent e) //是点击和松开两步完成后再调用 在此之前,pressed 和released都已经被调用
{
// TODO Auto-generated method stub
if("多边形".equals(command))
{
if(x4==0&&y4==0)
{
x4 = e.getX(); y4 = e.getY();
start_x = x4; start_y = y4;
//记录多边形的起点位置
}
else {
x3 = x4; y3 = y4;//这里的x3,y3为上一条边的终点
x4 = e.getX(); y4 = e.getY();
gr.drawLine(x3, y3, x4, y4);//将这点与上一条边的终点连起来
shape[index++]= new Shape(x3,y3,x4,y4,command,gr.getColor());
}
if(e.getClickCount()==2)
{
x4 =0; y4=0;//这里设置为零是为了能都再次执行if语句
gr.drawLine(start_x, start_y, e.getX(), e.getY());//将双击的终点位置和开始记录的起点位置相连
shape[index++]= new Shape(start_x, start_y, e.getX(), e.getY(),command,gr.getColor());
}
}
}
//当鼠标按下的时候,获取起点的坐标
public void mousePressed(MouseEvent e)
{
if("曲线".equals(command)){
x0 = e.getX();
y0 = e.getY();
}
// TODO Auto-generated method stub
x1 = e.getX();
y1 = e.getY();
}
//当鼠标松开的时候,获取终点的坐标
public void mouseReleased(MouseEvent e)
{
// TODO Auto-generated method stub
x2 = e.getX();
y2 = e.getY();
if("直线".equals(command)){
gr.drawLine(x1, y1, x2, y2);
shape[index++]= new Shape(x1,y1,x2,y2,command,gr.getColor());
}
else if("矩形".equals(command)) {
gr.drawRect(Math.min(x1, x2), Math.min(y1, y2), Math.abs(x1-x2), Math.abs(y1-y2));
shape[index++]= new Shape(x1,y1,x2,y2,command,gr.getColor());
}
else if("圆形".equals(command)) {
//Math.min(x1, x2), Math.min(y1, y2),
//drawoval 和drawrect 都是以某个点 向着坐标点增大的方向画图
//Math.abs(x1-x2)求绝对值
gr.drawOval(Math.min(x1, x2), Math.min(y1, y2), Math.abs(x1-x2), Math.abs(y1-y2));
shape[index++]= new Shape(x1,y1,x2,y2,command,gr.getColor());
}
}
//鼠标进入窗口
public void mouseEntered(MouseEvent e)
{
}
//鼠标退出窗口
public void mouseExited(MouseEvent e)
{
}
//鼠标在移动的过程中调用(没有点击)
public void mouseMoved(MouseEvent e)
{
}
}
Shape类:
public Shape(int x1,int y1,int x2,int y2,String name,Color color)
{
this.x1=x1;
this.x2=x2;
this.y1=y1;
this.y2=y2;
this.name=name;
this.color=color;
}
public Color getcolor()
{
return color;
}
public void Repaint(Graphics g)
{
switch(name)
{
case "直线":
g.drawLine(x1, y1, x2, y2);
break;
case "矩形":
g.drawRect(Math.min(x1, x2), Math.min(y1 ,y2), Math.abs(x1-x2), Math.abs(y1-y2));
break;
case "圆形":
g.drawOval(Math.min(x1, x2), Math.min(y1 ,y2), Math.abs(x1-x2), Math.abs(y1-y2));
break;
case "曲线":
g.drawLine(x1, y1, x2, y2);
break;
case "多边形":
g.drawLine(x1, y1, x2, y2);
break;
}
}
}
问题bug修改记录:日期:2021.7.25
①:拖动窗口之后再画图,发现画笔画图的位置和我们当前鼠标拖动的位置不一致,那是因为在创建jframe的时候就已经创建并获得了jframe的画笔graphics g,那么后面无论在哪里调用g,他的使用范围就限制在了初始化的窗口大小。
解决方法:在jframe上真正开始画图的时候才获得画笔,以获得和改变窗口之后的大小相同的绘画范围。
改进代码如下:
//当鼠标按下的时候,获取起点的坐标
public void mousePressed(MouseEvent e)
{
gr=p.getGraphics();
gr.setColor(color);
System.out.println("pressed事件发生");
if("曲线".equals(command)){
x0 = e.getX();
y0 = e.getY();
}
// TODO Auto-generated method stub
x1 = e.getX();
y1 = e.getY();
}
清除功能也是如此,(窗口变大后,只清除了原先窗口大小的内的图像),也需要在当前获得一个新的画笔。
改进代码:
public void actionPerformed(ActionEvent e)
{
if (e.getActionCommand()=="") {
JButton j = (JButton)e.getSource();
//将画笔设置为按钮的背景颜色
//前面设置了当前类的全局变量private Color color=Color.black;
color=j.getBackground();
}
else {
command = e.getActionCommand();
if("清除".equals(command))
{ //改进代码:
Graphics cleang=p.getGraphics();
p.paint1(cleang);
p.shape=new Shape[10000];
index=0;
}
}
}
其他相应位置,读者可以自行调试修改。