1、大致思路:
- 画图板类Panel
要制作画图板的大致框架,首先需要一个画板(JPanel),然后是选择要画的图形、画笔的颜色、粗细以及后退、清除的按钮(JButton),最后将这个画板和所有按钮以流式布局的方式加到最终的大框架(JFrame)上,画图板的大致结构就完成了,主要需要用到的就是以上括号里的三个类。
需要注意的是,关于我自己写的这个画图板类Panel到底应该继承JFrame类还是继承JPanel类有两种想法:
1、继承JFrame,作为一个新型的框架类,在此类中编写显示界面的方法时再new一个JPanel类的画板加到this上,显示this。
2、继承JPanel,作为一个新型画板类,在此类中编写显示界面的方法时再new一个JFrame类的框架,将this加到这个框架上,显示这个框架。
个人觉得应该用第二种想法,因为到后面会涉及重写用于重绘的paint()函数,既然图形是画在画板JPanel上的,那么重绘也必然要针对画板JPanel,也就是说我要重写的是JPanel中的paint()函数,而不是JFrame中的,那么就只有继承JPanel,才能重写JPanel中的paint()函数,从而达到针对性重绘的目的。
在设置画图板显示之后,再利用Graphics graphics = this.getGraphics()在自己写的这个新型画板类的对象上获得画笔,并将画笔传到监听器类Listener中,在监听器中监听鼠标的各种行为并用此画笔画出对应的图形。注意:一定要在画图板设置可见之后再获得画笔,否则得到的画笔是空指针。
- 监听器类Listener
点击各按钮时,监听按钮点击事件需要用到ActionListener接口,画图时需要用到鼠标,因此也需要监听鼠标行为的相关监听器接口。使用时无论是定点位置监听鼠标行为的MouseListener,还是监听鼠标移动拖动的MouseMotionListener,还是两者兼而有之,都可以只用MouseAdapter来代替。MouseAdapter是一个类,其中含有MouseListener接口和MouseMotionListener接口的所有方法,所以继承了MouseAdapter类之后,不仅可以使用监听鼠标行为的所有常用方法,还不用像继承那两个接口那样必须把不用的方法也重写一遍,岂不美哉!
想清楚需要继承的接口与类之后,就要开始思考画每一个图形的每一步细节。画这个图形的每一步时,鼠标在这一刻的行为是什么?这个行为对应的是我需要重写的哪个函数?我在重写这个函数时,要在里面得到哪些坐标,还要做哪些其他的事以帮助完成整个图形的绘制?
- 画直线:按下——拖拽——释放。
1、首先要在画板上某个点按下鼠标(对应mousePressed(MouseEvent e){}函数),点击的同时在对应的行为函数中获得该点的坐标X1,Y1。
public void mousePressed(MouseEvent e){
if("直线".equals(shapename)){
x1 = e.getX(); y1 = e.getY(); //得到头点坐标
}
}
2、从这一点开始,用鼠标朝某一个方向进行拖拽,直到另一个点,然后释放鼠标(对应mouseReleased(MouseEvent e)函数),释放的同时在对应的行为函数中获取释放点的坐标X2,Y2,此时已经获得此线段的头坐标(X1,Y1)和(X2,Y2),调用画笔画直线的方法drawLine( X1, Y1, X2, Y2)即可画出一条直线。
public void mouseReleased(MouseEvent e){
if("直线".equals(shapename)){
x2 = e.getX(); y2 = e.getY(); //得到尾点坐标
graphics.drawLine(x1, y1, x2, y2); //连接头尾两点画直线
}
}
- 画三角形:画直线——点击(对应mouseClicked(MouseEvent e)函数)第三个点连接这条直线的首尾两点。
注意:由于点击 = 按下 + 释放,当鼠标点击时,监听器监听到鼠标行为的顺序依次是:按下——释放——点击,系统会先执行按下和释放所对应的的函数,最后再执行点击函数,因此为了使得点击的操作不受按下和释放的影响,在最后点击第三点画出三角形时,必须想办法避开前面的按下和释放操作,只执行点击操作,最简单的方法就是设置一个变量,画三角形时每完成一步,就让该变量+1,每次根据这个变量的大小来判断该进行哪一步,该避开哪一步,最后一个三角形画完后,使该变量回归初始值即可。
private int triangle = 1; //设置变量
public void mousePressed(MouseEvent e){
if("三角形".equals(shapename) && triangle==1){
x1 = e.getX(); y1 = e.getY();
triangle++; //执行完第一步,变量+1
}
}
public void mouseReleased(MouseEvent e){
if"三角形".equals(shapename) && triangle==2){ //如果变量为2的话才执行此操作
x2 = e.getX(); y2 = e.getY();
graphics.drawLine(x1, y1, x2, y2); //连接头尾两点画直线
triangle++; //执行完第二步,变量+1
}
}
public void mouseClicked(MouseEvent e){
if("三角形".equals(shapename) && triangle==3){ //如果变量为3的话才执行此操作
x = e.getX(); y = e.getY(); //得到第三点的坐标
graphics.drawLine(x, y, x1, y1);
graphics.drawLine(x, y, x2, y2); //连接第三点与已画直线的首尾两点形成三角形
triangle = 1; //三角形画完,变量回归初始值
}
}
- 画多边形:画直线——for(i=0;i<n;i++){点击某点,连接该点与上一直线的尾点形成新的直线}——双击某点(e.getClickCount()==2),连接上一直线的尾点以及第一条直线的首点,形成封闭多边形。
注意:与三角形一样,在点击和双击时画多边形涉及计算机执行按下——释放——点击操作的顺序问题,解决的方法同样是设置一个对应的变量,使得该变量随着画图进程的前进而+1,根据此变量的大小在画图进程的不同时刻执行唯一的操作,最后再让其回归初始值。
private int triangle = 1 //设置变量
public void mousePressed(MouseEvent e){
if("多边形".equals(shapename) && polygon==1){
x1 = e.getX(); y1 = e.getY();
polygon++; 执行完第一步,变量+1
}
}
public void mouseReleased(MouseEvent e){
if("多边形".equals(shapename) && polygon==2){ //当变量为2时才执行此操作
x2 = e.getX(); y2 = e.getY();
graphics.drawLine(x1, y1, x2, y2); //连接头尾两点画直线
polygon++; 执行完第二步,变量+1
}
}
public void mouseClicked(MouseEvent e){
if("多边形".equals(shapename) && polygon>2){ //当变量大于2时才执行此操作
x = e.getX(); y = e.getY();
graphics.drawLine(x, y, x2, y2);
x2 = x; y2 = y; //每多画一条边,就将坐标变换,以便下一次点击时画下一条边
polygon++; //每多画一条边,变量+1
}
if("多边形".equals(shapename) && e.getClickCount()==2){ //双击时
x = e.getX(); y = e.getY();
graphics.drawLine(x, y, x1, y1);
graphics.drawLine(x, y, x2, y2); //连接头尾两点,形成封闭多边形
polygon = 1; //变量回归初始值
}
}
画三角形与多边形的方法都是在画直线方法的基础上演化而来的,只是多涉及了点击、双击和每一步坐标的变换,在此不做过于详细的讲解。
- 画矩形:鼠标操作方法与画直线相同,得到X1,Y1,X2,Y2。画矩形方法为drawRect(x,y,width,height),其中x,y是最终画出的矩形左上角的顶点坐标,因此无论你是朝哪个方向画的矩形,一定要得到其左上角的顶点坐标,即((X1,X2中的最小值),(Y1,Y2中的最小值))。而宽width和高height则分别对应X1与X2之差的绝对值和Y1与Y2之差的绝对值。
public void mousePressed(MouseEvent e){
if("直线".equals(shapename)){
x1 = e.getX(); y1 = e.getY(); //得到头点坐标
}
}
public void mouseReleased(MouseEvent e){
if("直线".equals(shapename)){
x2 = e.getX(); y2 = e.getY(); //得到尾点坐标
graphics.drawRect(Math.min(x1, x2), Math.min(y1, y2),Math.abs(x1-x2), Math.abs(y1-y2)); //画矩形
}
}
- 画椭圆:与画矩形类似,只是方法为drawOval(x,y,width,height)。
public void mousePressed(MouseEvent e){
if("椭圆".equals(shapename)){
x1 = e.getX(); y1 = e.getY(); //得到头点坐标
}
}
public void mouseReleased(MouseEvent e){
if("椭圆".equals(shapename)){
x2 = e.getX(); y2 = e.getY(); //得到尾点坐标
graphics.drawOval(Math.min(x1, x2), Math.min(y1, y2),Math.abs(x1-x2), Math.abs(y1-y2)); //画椭圆
}
}
此外还有填充矩形、填充椭圆和3D矩形等类似图形,都是与最基础的椭圆、矩形画法同一模式,只是方法参数不同而已,在此不做过于详细的解释。
- 画曲线:点击——不断拖动。
从微元法的角度思考,曲线可以看做是由很多条小直线收尾连接而成的,因此在鼠标拖动(对应mouseDragged(MouseEvent e)函数)时,可以在对应行为函数中先画一条直线,然后立刻此直线的尾坐标(X2,Y2)转变为下一直线的头坐标(X1,Y1),这样一来,在鼠标拖动过程中,系统会不断执行mouseDragged(MouseEvent e)函数,也就会不断地画首尾相连的小直线,最终连成一条曲线。
public void mousePressed(MouseEvent e){
if("曲线".equals(shapename)){
x1 = e.getX(); y1 = e.getY(); //得到头点坐标
}
}
public void mouseDragged(MouseEvent e){
if("曲线".equals(shapename)){
x2 = e.getX(); y2 = e.getY(); //得到尾点坐标
graphics.drawLine(x1, y1, x2, y2); //连接头尾两点画微元小直线
x1 = x2; y1 = y2; //调换坐标,准备循环画微元小直线
}
}
- 图形类Shape
1、图形的重绘:
按理说,画笔传到了监听器类中,就可以直接一步一步画出各种图形,不用再增加其它类了。但之所以还要增加一个图形类,是因为画图板的重绘函数paint()并没有重绘出你自己画出的这些图形,导致在画图的过程中,一旦界面稍微动一下,所有图形都会全部消失。paint()函数的主要作用是在界面移动或者发生某些形变(缩小、最小化)时,系统会自动调用paint()函数,以保证paint()函数中的内容不会随着界面的变化而消失。这时就需要将你已经画过的所有图形的数据、信息以某种形式保存起来,并且重写paint()函数,将所有画过的图形在重写的paint()函数中再重绘一遍,你画过的图形就不会随着界面的变化而消失了。保存的形式,就是再写一个图形类Shape,将不同图形的绘制方法写在Shape类的画图方法中,并用这个类创建一个图形数组,每画一个图形,就将它存在这个Shape数组中,最后在重绘函数paint()中调用数组中每个图形对象对应的画图方法,即可重绘出图形。
public void paint(Graphics graphics){
super.paint(graphics); //必须先调用父类的paint()函数
for(int i=0;i<shapeArray.length;i++){
if(shapeArray[i]!=null){
shapeArray[i].DrawShape(graphics); //重绘
}
}
}
2、后退和清空的实现:
一旦监听器监听到点击了“后退”按钮,那么就将已经保存的图形数组中的最后一个图形对象设置为null,在paint()函数中重绘时加上一个条件语句if(a[]!=null),然后想办法调用paint()函数,使得所有图形重绘一次,那么你所画得最后一个图形就不会被重绘出,当场消失,后退功能就得以实现。然而paint()函数不可以被人为直接调用,只有在界面发生变化时才会触发它的调用,如果要人为调用paint()函数,只能通过调用repaint()函数来间接调用paint()函数。因此,将画板传到监听器类,并在“后退”的最后调用其repaint()函数,就可以人为实现重绘,最终达到后退目的。
至于清空,和后退一个道理,只需将图形数组中的所有图形设置为null,再用repaint()人为重绘即可。
public void actionPerformed(ActionEvent e){
if("后退".equals(e.getActionCommand())){
shapeArray[num-1] = null;
panel.repaint();
num--;
}else if("清空".equals(e.getActionCommand())){
for(int i=0;i<num;i++){
shapeArray[i] = null;
}
panel.repaint();
num = 0;
}
}
补充:关于设置粗线、细线与橡皮擦:
为一个画笔graphics设置线条粗细的方法:利用Graphics2D类。此类可理解为二维画笔,将Graphics类的普通画笔强制转型为Graphics2D类的二维画笔之后,就可以为此画笔设置粗细。具体代码为:
Graphics2D graphics2d = (Graphics2D)graphics; //强制转型为二维画笔
graphics2d.setStroke(new BasicStroke(1f)); //设置粗细为1f
设置完粗细后,后面用画笔时无论用graphics2d还是用graphics都可以,此时这两个画笔已经是同一个画笔了,而且都是二维画笔。
至于橡皮擦,想简单点无非就是要使鼠标划过的地方被画板的背景色覆盖,其实不就是用颜色为画板背景色的画笔来画曲线吗?所以当执行橡皮擦按钮的命令时,就将画笔颜色设置为画板背景色,并且调用画曲线的方法就可以了,同时还要注意用Graphics2D将这个“橡皮擦画笔”的粗细设置得粗一点,这样这个橡皮擦能一次性擦干净的面积就更大。
2、具体代码:
- 画图板类Panel
public class Panel extends JPanel{
public static void main(String args[]){
Panel panel = new Panel();
panel.showP();
}
public Shape []shapeArray = new Shape [1000000]; //设置保存图形的数组
public void showP(){
JFrame jFrame = new JFrame("画图板");
jFrame.setSize(900, 800);
jFrame.setResizable(false);
jFrame.setLocationRelativeTo(null);
jFrame.setDefaultCloseOperation(3);
FlowLayout flowLayout = new FlowLayout();
jFrame.setLayout(flowLayout);
Listener listener = new Listener();
//加图形选择按钮
String name[] = {"直线","三角形","矩形","椭圆","多边形","曲线","填充矩形","3D矩形","填充椭圆"};
for(int i=0;i<9;i++){
JButton jButton = new JButton(name[i]);
jButton.setPreferredSize(new Dimension(90, 30));
jFrame.add(jButton);
jButton.addActionListener(listener);
}
//加线与橡皮擦选择按钮
String line[] = {"细线","粗线","橡皮擦"};
for(int i=0;i<3;i++){
JButton jButton = new JButton(line[i]);
jButton.setPreferredSize(new Dimension(90, 30));
jFrame.add(jButton);
jButton.addActionListener(listener);
}
//加颜色选择按钮
Color color[] = {Color.BLACK,Color.RED,Color.PINK,Color.ORANGE,Color.YELLOW,Color.GREEN,Color.CYAN,Color.BLUE,Color.GRAY};
for(int i=0;i<9;i++){
JButton jButton = new JButton();
jButton.setPreferredSize(new Dimension(40, 40));
jButton.setBackground(color[i]);
jFrame.add(jButton);
jButton.addActionListener(listener);
}
//加后退清空按钮
JButton jButton001 = new JButton("后退");
jButton001.setPreferredSize(new Dimension(60, 30));
JButton jButton002 = new JButton("清空");
jButton002.setPreferredSize(new Dimension(60, 30));
jFrame.add(jButton001);
jFrame.add(jButton002);
jButton001.addActionListener(listener);
jButton002.addActionListener(listener);
//将画板加到界面上
this.setPreferredSize(new Dimension(888, 700));
this.setBackground(Color.WHITE);
jFrame.add(this);
this.addMouseListener(listener);
this.addMouseMotionListener(listener);
jFrame.setVisible(true);
//生成画笔并进行相关初始设置
Graphics graphics = this.getGraphics();
Graphics2D graphics2d = (Graphics2D)graphics;
graphics2d.setStroke(new BasicStroke(1f));
graphics.setColor(Color.BLACK);
//传相关参数到监听器类
listener.Pass(graphics, shapeArray, this);
}
//重写重绘函数
public void paint(Graphics graphics){
super.paint(graphics);
for(int i=0;i<shapeArray.length;i++){
if(shapeArray[i]!=null){
shapeArray[i].DrawShape(graphics);
}
}
}
}
- 监听器类Listener
public class Listener extends MouseAdapter implements ActionListener{
private Graphics graphics;
private Color color ;
private String shapename;
private int x1,y1,x2,y2,x,y;
private Shape shapeArray [];
private int num = 0;
private int triangle = 1;
private int polygon = 1;
String line = "细线";
Panel panel = new Panel();
//传递参数函数
public void Pass(Graphics graphics, Shape shapeArray[], Panel panel){
this.graphics = graphics ;
this.shapeArray = shapeArray;
this.panel = panel;
}
//按钮被点击
public void actionPerformed(ActionEvent e){
if("".equals(e.getActionCommand())){
JButton jButton = new JButton();
jButton = (JButton)e.getSource();
color = jButton.getBackground();
graphics.setColor(color);
}else if("橡皮擦".equals(e.getActionCommand())){
line = "橡皮擦";
}else if("粗线".equals(e.getActionCommand())){
line = "粗线";
}else if("细线".equals(e.getActionCommand())){
line = "细线";
}else if("后退".equals(e.getActionCommand())){
shapeArray[num-1] = null;
panel.repaint();
num--;
}else if("清空".equals(e.getActionCommand())){
for(int i=0;i<num;i++){
shapeArray[i] = null;
}
panel.repaint();
num = 0;
}else{
shapename = e.getActionCommand();
}
}
//鼠标点击
public void mouseClicked(MouseEvent e){
if("三角形".equals(shapename) && triangle==3){
x = e.getX(); y = e.getY();
Shape shape = new Shape(shapename, line, color, x1, y1, x2, y2, x, y);
shape.DrawShape(graphics);
shapeArray[num++] = shape;
triangle = 1;
}
if("多边形".equals(shapename) && polygon>2){
x = e.getX(); y = e.getY();
Shape shape = new Shape(shapename, line, color, x1, y1, x2, y2, x, y);
shape.DrawShape(graphics);
shapeArray[num++] = shape;
x2 = x; y2 = y;
polygon++;
}
if("多边形".equals(shapename) && e.getClickCount()==2){
x = e.getX(); y = e.getY();
Shape shape = new Shape("三角形", line, color, x1, y1, x2, y2, x, y);
shape.DrawShape(graphics);
shapeArray[num++] = shape;
polygon = 1;
}
}
//鼠标按下
public void mousePressed(MouseEvent e){
if("直线".equals(shapename)||"矩形".equals(shapename)||"填充矩形".equals(shapename)||"3D矩形".equals(shapename)||
"椭圆".equals(shapename)||"填充椭圆".equals(shapename)||"曲线".equals(shapename)||"橡皮擦".equals(line)){
x1 = e.getX(); y1 = e.getY();
}
if("三角形".equals(shapename) && triangle==1){
x1 = e.getX(); y1 = e.getY();
triangle++;
}
if("多边形".equals(shapename) && polygon==1){
x1 = e.getX(); y1 = e.getY();
polygon++;
}
}
//鼠标释放
public void mouseReleased(MouseEvent e){
if("直线".equals(shapename)){
x2 = e.getX(); y2 = e.getY();
Shape shape = new Shape(shapename,line,color,x1,y1,x2,y2);
shape.DrawShape(graphics);
shapeArray[num++] = shape;
}
if("三角形".equals(shapename) && triangle==2){
x2 = e.getX(); y2 = e.getY();
Shape shape = new Shape("直线",line,color,x1,y1,x2,y2);
shape.DrawShape(graphics);
shapeArray[num++] = shape;
triangle++;
}
if("矩形".equals(shapename)||"填充矩形".equals(shapename)||"3D矩形".equals(shapename)){
x2 = e.getX(); y2 = e.getY();
Shape shape = new Shape(shapename,line,color,x1,y1,x2,y2);
shape.DrawShape(graphics);
shapeArray[num++] = shape;
}
if("椭圆".equals(shapename)||"填充椭圆".equals(shapename)){
x2 = e.getX(); y2 = e.getY();
Shape shape = new Shape(shapename,line,color,x1,y1,x2,y2);
shape.DrawShape(graphics);
shapeArray[num++] = shape;
}
if("多边形".equals(shapename) && polygon==2){
x2 = e.getX(); y2 = e.getY();
Shape shape = new Shape("直线",line,color,x1,y1,x2,y2);
shape.DrawShape(graphics);
shapeArray[num++] = shape;
polygon++;
}
}
//鼠标拖拽
public void mouseDragged(MouseEvent e){
if("曲线".equals(shapename)){
x2 = e.getX(); y2 = e.getY();
Shape shape = new Shape("直线",line,color,x1,y1,x2,y2);
shape.DrawShape(graphics);
shapeArray[num++] = shape;
x1 = x2; y1 = y2;
}else if("橡皮擦".equals(line)){
x2 = e.getX(); y2 = e.getY();
Shape shape = new Shape("直线",line,color,x1,y1,x2,y2);
shape.DrawShape(graphics);
x1 = x2; y1 = y2;
}
}
}
- 图形类Shape
public class Shape {
private int x1,y1,x2,y2,x,y;
public String name,line;
Color color;
//构造函数一
public Shape(String name,String line,Color color,int x1,int y1,int x2,int y2){
this.name = name;
this.line = line;
this.color = color;
this.x1 = x1; this.y1 = y1;
this.x2 = x2; this.y2 = y2;
}
//构造函数二
public Shape(String name,String line,Color color,int x1,int y1,int x2,int y2,int x,int y){
this.name = name;
this.line = line;
this.color = color;
this.x1 = x1; this.y1 = y1;
this.x2 = x2; this.y2 = y2;
this.x = x; this.y = y;
}
//画图方法
public void DrawShape(Graphics graphics){
graphics.setColor(color);
Graphics2D graphics2d = (Graphics2D)graphics;
switch (line) {
case "橡皮擦": graphics2d.setStroke(new BasicStroke(30f));
graphics2d.setColor(Color.WHITE); break;
case "粗线": graphics2d.setStroke(new BasicStroke(3f)); break;
case "细线": graphics2d.setStroke(new BasicStroke(1f)); break;
}
switch (name){
case "直线": graphics.drawLine( x1, y1, x2, y2); break;
case "三角形": graphics.drawLine(x, y, x1, y1);
graphics.drawLine(x, y, x2, y2); break;
case "矩形": graphics.drawRect(Math.min(x1, x2), Math.min(y1, y2),Math.abs(x1-x2), Math.abs(y1-y2)); break;
case "填充矩形": graphics.fillRect(Math.min(x1, x2), Math.min(y1, y2),Math.abs(x1-x2), Math.abs(y1-y2)); break;
case "3D矩形": graphics.fill3DRect(Math.min(x1, x2), Math.min(y1, y2),Math.abs(x1-x2), Math.abs(y1-y2),true); break;
case "椭圆": graphics.drawOval(Math.min(x1, x2), Math.min(y1, y2),Math.abs(x1-x2), Math.abs(y1-y2)); break;
case "填充椭圆": graphics.fillOval(Math.min(x1, x2), Math.min(y1, y2),Math.abs(x1-x2), Math.abs(y1-y2)); break;
case "多边形": graphics.drawLine(x, y, x2, y2); break;
}
}
}
3、最终效果: