`
djsl6071
  • 浏览: 595245 次
  • 性别: Icon_minigender_1
  • 来自: 厦门
社区版块
存档分类
最新评论

一个Java画图板程序的设计

阅读更多

本文讲述一个画图板应用程序的设计,屏幕抓图如下。这篇文章带有三个附件,其中两个jar文件都是j2sdk1.4.2_08编译打包,包含源代码,可执行,如下表:

附件名称及链接 详情
jDraw_basic.jar 本文是基于这个基本版本的,屏幕抓图显示的也是这个基本版本的界面。
jDraw_extended.jar 在基础版本上稍加扩展,加入文件读存功能,即可将所画的图存入一个模型文件(特定的格式,见下)或者从文件中读取,也可以将其导出到一个PNG格式的文件。由于扩展功能不是本文的重点,并且也不复杂,所以文中就不在对其进行阐述。它的源代码只是在基本版本上增加了一些内容。
jdraw_demo.zip 屏幕抓图中的图形的模型文件,属于纯文本格式,为了节省空间,将其压缩了一下,解压缩取出其中的jdraw_demo.jdw文件后再使用。按理说,SGML/XML的格式才是正途,不过这只是个简单的应用,不用那么大动干戈了,就走个“邪道”吧:)

『IShape』

这是所有图形类(此后称作模型类)都应该实现接口,外部的控制类,比如画图板类就通过这个接口跟模型类“交流”。名字开头的I表示它是一个接口(Interface),这是eclipse用的一个命名法则,觉得挺有用的,就借鉴来了。这个接口定义了两个方法:

public void draw(java.awt.Graphics2D g);
每个实现IShape的类都在这个方法里面指定它的图形显示代码。

public void processCursorEvent(java.awt.event.MouseEvent evt, int type);
这个方法是在图形(被用户)绘制过程中,发生相关的鼠标点击和移动事件时调用的。
第一个参数就是所发生的鼠标事件对象;
第二个参数取值于IShape所定义的三个常数:RIGHT_PRESSED, LEFT_RELEASED,和CURSOR_DRAGGED。

下面这个class diagram显示了所有图形类的结构图。FreeShape, RectBoundedShape,和PolyGon这三个类直接实现了IShape接口。其中,FreeShape和RectBoundedShape是抽象类,分别代表不规则图形(比如铅笔画图)和以一个长方形为边界的规则图形,由于分属于这两个类别的图形对于鼠标事件的处理基本上都是一致的,所以就抽象出来这两个父类,避免重复代码。PolyGon是一个具体类,它的命名没有采用Polygon是为了避免同java.awt.Polygon重名。它代表的图形是多边形,由于它独特的鼠标处理方式,它不属于上面两种类型图形的任何一种,所以它直接实现了IShape接口。

IShape接口所定义的两个方法到底是怎么被用到的呢?这个问题现在还不能立刻解答。在下面的部分,我们先讲述FreeShape所定义的不规则图形及其两个具体子类PolyLine和Eraser,然后在这个基础上讲述一个缩略版的画图板类,到那个时候,上面问题的答案也就自然揭晓了。之后,我们再继续讲述其他的图形类。

『FreeShape』

讲到FreeShape,我们不得不先说一下PointsSet这个类。这是一个util类,被FreeShape和PolyGon用到,代表一个有序的点集合,并提供方便的方法来加入新的点和读取点坐标。为了方便对模型类代码的理解,这里列出PointsSet类的API。

public PointsSet();
用默认的初始容量(10)创建一个对象。

public PointsSet(int initCap);
用指定的初始容量(initCap)创建一个对象。

public void addPoint(int x, int y);
加入一个新的点到这个集合的末端;如果旧的末端点跟新的点重合,则不重复加入。

public int[][] getPoints();
将所有点以一个二维数组(int[2][n])返回。第一行是x坐标,第二行是y坐标。

public int[][] getPoints(int x, int y);
类似上一个方法,只是最后将参数指定的点加在末尾(无论是否跟集合末端的点重合);
这个方法只被PolyGon用到。

好了,来看下面代码中FreeShape对IShape接口的实现。FreeShape有三个属性变量:color, stroke,和pointsSet。权限设成protected当然是给子类用啦。color就是色彩了,stroke用来指定使用线条的粗细(当然,Stroke类的对象还可以指定交接点形状之类的属性,不过这里都使用其默认值了),pointsSet当然就是包含了所有控制点(这里叫控制点似乎不太恰当,因为其实无法利用这些点来“控制”的,不过也想不到其他恰当的名字,就这么叫吧)集合。值得注意的是构造函数里面包含了起始点的坐标,这个点在函数里面被加到了控制点集中。

这类图形对鼠标事件的处理很简单,它只对IShape.CURSOR_DRAGGED类型的事件感兴趣,每当发生这类事件的时候,就把鼠标拖拽到的新的点加入到控制点集中。当然了,根据上面看到的PointsSet.addPoint(int,int)这个方法的“个性”,这个点是否真的被加入还要看它是否跟旧的末端点重合。

import java.awt.*;
import java.awt.event.MouseEvent;

public abstract class FreeShape implements IShape {
    
    protected Color color;
    protected Stroke stroke;
    protected PointsSet pointsSet;
  
    protected FreeShape(Color c, Stroke s, int x, int y) {
        pointsSet = new PointsSet(50);
        color = c;
        stroke = s;
        pointsSet.addPoint(x, y);
    }
    
    public void processCursorEvent(MouseEvent e, int t) {
        if (t != IShape.CURSOR_DRAGGED)
            return;
        pointsSet.addPoint(e.getX(), e.getY());
    }

}

FreeShape类没有实现IShape接口的draw(Graphics2D)方法,很明显,这个方法是留给子类来完成的。PolyLine和Eraser继承了FreeShape,分别代表铅笔绘出的图形和橡皮擦。其中PolyLine的构造函数结构跟其父类相似,直接调用父类的super方法来完成;相比之下,Eraser类就有点“叛逆”了,它的参数里面用一个JComponent替换了Color。Eraser类是通过画出跟画图板背景色彩一致的线条来掩盖原有图形而实现橡皮擦的效果的,但由于画图板的背景色是可以调的(见抓图的Color Settings部分),直接给Eraser的构造函数一个色彩对象不太合适,所以干脆将画图板自己(JComponent)传了进来,这样,每次Eraser设定图形色彩时,都直接问画图板要它的背景色。来看一下PolyLine对draw(Graphics2D)方法的实现:

    public void draw(Graphics2D g) {
        g.setColor(color);
        g.setStroke(stroke);
        int[][] points = pointsSet.getPoints();
        int s = points[0].length;
        if (s == 1) {
            int x = points[0][0];
            int y = points[1][0];
            g.drawLine(x, y, x, y);
        } else {
            g.drawPolyline(points[0], points[1], s);
        }
    }

这个方法里面有一个if-else结构,由于构造函数里面已经将起始点加入控制点集中,所以pointsSet.getPoints()会至少返回一个点。利用Graphics.drawPolyline(int[],int[],int)画图时,如果只有一个点,它是不会画出来东西的,所以检查一下点数,如果只有一个,则改用Graphics.drawLine(int,int,int,int)将这个点画出来。Eraser的draw(Graphics2D)方法跟上面基本上完全一样,只是传给Graphics.setColor(Color)的参数是通过JComponent.getBackground()得到的。

『TestBoard』

现在就来看一个精简版的画图板类:TestBoard。下面的代码,是通过代码注释进行解释的。需要注意的是,TestBoard本身还不能直接运行,需要把它放到一个JFrame里面才行。同时画图工具的切换也需要外部的控件来处理。不过这些都比较简单了,就不多说了。

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.util.ArrayList;

public class TestBoard extends JPanel 
                         implements MouseListener, MouseMotionListener {
    
    //定义一些常量
    public static final int TOOL_PENCIL = 1;
    public static final int TOOL_ERASER = 2;
    public static final Stroke STROKE = new BasicStroke(1.0f);
    public static final Stroke ERASER_STROKE = new BasicStroke(15.0f);

    private ArrayList shapes;     //保存所有的图形对象(IShape)
    private IShape currentShape;  //指向当前还未完成的图形
    private int tool; //代表当前使用的画图工具(TOOL_PENCIL或TOOL_ERASER)

    public TestBoard() {
        //进行一些初始化
        shapes = new ArrayList();
        tool = TOOL_PENCIL;
        currentShape = null;
        
        //安装鼠标监听器
        addMouseListener(this);
        addMouseMotionListener(this);
    }
    
    //外部的控制界面可以通过这个方法切换画图工具
    public void setTool(int t) {
        tool = t;
    }
    
    //override JPanel的方法。通过调用IShape.draw(Graphics2D)方法来显示图形
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        int size = shapes.size();
        Graphics2D g2d = (Graphics2D) g;
        for (int i=0; i<size; i++) {
            ((IShape) shapes.get(i)).draw(g2d);
        }
    }
    
    public void mousePressed(MouseEvent e) {
        /* 当左键点击时,currentShape肯定指向null。根据当前画图工具创建相应图形对象,
           将currentShape指向它,并把这个对象加入到对象集合(shapes)中。另外,调用
           repaint()方法将画图板的画面更新一下。 */
        if (e.getButton() == MouseEvent.BUTTON1) {
            switch (tool) {
            case TOOL_PENCIL:
                currentShape = new PolyLine(getForeground(), 
                                               STROKE, e.getX(), e.getY());
                break;
            case TOOL_ERASER:
                currentShape = new Eraser(this, ERASER_STROKE, 
                                               e.getX(), e.getY());
                break;
            }
            shapes.add(currentShape);
            repaint();
        /* 当右键点击并且currentShape不指向null时,调用currentShape的
          processCursorEvent(MouseEvent,int)方法,类型参数是
      IShape.RIGHT_PRESSED。 repaint()*/
        } else if (e.getButton() == MouseEvent.BUTTON3 && currentShape != null) {
            currentShape.processCursorEvent(e, IShape.RIGHT_PRESSED);
            repaint();
        }
    }
    
    public void mouseDragged(MouseEvent e) {
        /* 当鼠标拖拽并且currentShape不指向null时(这种情况下,左键肯定处于
          按下状态),调用currentShape的processCursorEvent(MouseEvent,int)方法,
          类型参数是IShape.CURSOR_DRAGGED。 repaint()*/
        if (currentShape != null) {
            currentShape.processCursorEvent(e, IShape.CURSOR_DRAGGED);
            repaint();
        }
    }
    
    public void mouseReleased(MouseEvent e) {
        /* 当左键被松开并且currentShape不指向null时(这个时候,currentShape
          肯定不会指向null的,多检查一次,保险),调用currentShape的
          processCursorEvent(MouseEvent,int)方法,类型参数是
          IShape.CURSOR_DRAGGED。 repaint()*/
        if (e.getButton() == MouseEvent.BUTTON1 && currentShape != null) {
            currentShape.processCursorEvent(e, IShape.LEFT_RELEASED);
            currentShape = null;
            repaint();
        }
    }
    
    //对下面这些事件不感兴趣
    public void mouseClicked(MouseEvent e) {}
    public void mouseEntered(MouseEvent e) {}
    public void mouseExited(MouseEvent e) {}
    public void mouseMoved(MouseEvent e) {}
    
}

至此,整个程序的流程就很清楚了,文章开头部分的问题也被解开了。接下来,就继续来看其他的模型类。

『RectBoundedShape』

RectBoundedShape构造函数的结构跟FreeShape一样,在色彩和线条的运用上也是一样的,也只对鼠标拖拽事件感兴趣。不过,它只有两个控制点,起始点和结束点,所以,不需要用到PointsSet。本来,RectBoundedShape这个类是比FreeShape简单的,在处理鼠标拖拽事件时只要将结束点设置到新拖拽到的点就可以了。不过,这里我们多加入一个的功能,就是在shift键按下的情况下,让图形的边界是个正方形(取原边界中较短的那条边)。这个功能是由regulateShape(int,int)这个方法来完成的,它的代码相当简短,就不多做解释了 。

import java.awt.*;
import java.awt.event.MouseEvent;

public abstract class RectBoundedShape implements IShape {
    
    protected Color color;
    protected Stroke stroke;
  
    protected int startX, startY, endX, endY;
    
    protected RectBoundedShape(Color c, Stroke s, int x, int y) {
        color = c;

        stroke = s;
        startX = endX = x;
        startY = endY = y;
    }
    
    public void processCursorEvent(MouseEvent e, int t) {
        if (t != IShape.CURSOR_DRAGGED)
            return;
        int x = e.getX();
        int y = e.getY();
        if (e.isShiftDown()) {
            regulateShape(x, y);
        } else {
            endX = x;
            endY = y;
        }
    }
    
    protected void regulateShape(int x, int y) {
        int w = x - startX;
        int h = y - startY;
        int s = Math.min(Math.abs(w), Math.abs(h));
        if (s == 0) {
            endX = startX;
            endY = startY;
        } else {
            endX = startX + s * (w / Math.abs(w));
            endY = startY + s * (h / Math.abs(h));
        }
    }
    
}

有了RectBoundedShape这个父类打下的基础,它下面的子类所要做的事情就是画图啦。所有子类的构造函数跟父类都是一样的结构,基本上也都是直接调用super的构造函数,只是Diamond这个类为了提高画图效率,“私下”定义了一个数组。RectBoundedShape的子类包括Line, Rect, Oval, 和Diamond。除了Diamond需要根据边界长方形进行稍微计算求得菱形的四个点外,它们的图形都可以直接利用Graphics类提供的方法很方便的画出来,详情可以参看源代码,就不多说了。现在看一下Line这个类。不同于其它几个类,在shift键按下的情况下,根据角度不同,我们想画出45度线,水平线,或者竖直线。所以,Line这个类不使用其父类定义的processCursorEvent(MouseEvent,int)方法,而是自己定义了一套。父类中regulateShape(int,int)方法的权限设成protected也是为了给Line用的。代码如下:

    public void processCursorEvent(MouseEvent e, int t) {
        if (t != IShape.CURSOR_DRAGGED)
            return;
        int x = e.getX();
        int y = e.getY();

        if (e.isShiftDown()) {
            //这个情况单独处理,不然就要除以0了
            if (x - startX == 0) { //竖直
                endX = startX;
                endY = y;
            } else {
                //由于对称性,只要算斜率的绝对值
                float slope = Math.abs(((float) (y - startY)) / (x - startX));
                //小于30度,变成水平的
                if (slope < 0.577) {
                    endX = x;
                    endY = startY;
                //介于30度跟60度中间的,变成45度,利用父类的regulateShape(int,int)完成
                } else if (slope < 1.155) {
                    regulateShape(x, y);
                //大于60度,变成竖直的
                } else {
                    endX = startX;
                    endY = y;
                }
            }
        //如果shift键没有按下,跟父类一样处理
        } else {
            endX = x;
            endY = y;
        }
    }

『PolyGon』

用户画多边形的步骤是这样的,先在一点按下鼠标左键,定义一个顶点,然后将鼠标拖拽到多边形的下一个顶点,点鼠标右键将这个点记录,之后重复这个步骤直到所有顶点都记录,松开左键,多边形完成。在多边形完成前,显示出来的不是闭合图形,当左键松开时,图形自动闭合。对于最后一个顶点,用户不用点右键也会被自动记录的。好了,来看一下这个过程是怎么来完成的。方便起见,直接用注释在代码上解释了。

import java.awt.*;
import java.awt.event.MouseEvent;

public class PolyGon implements IShape {
    
    //类似于FreeShape和RectBoundedShape的变量
    private Color color;
    private Stroke stroke;

    //记录所有顶点坐标,姑且称之为顶点集
    private PointsSet pointsSet;
  
    //记录多边形是否完成。true表示完成
    private boolean finalized;
    
    //记录画图过程中鼠标被拖拽到的点,姑且称之为浮点吧^_^
    private int currX, currY;
    
    public PolyGon(Color c, Stroke s, int x, int y) {
        pointsSet = new PointsSet();
        color = c;
        stroke = s;
        pointsSet.addPoint(x, y);
        //刚开始先把浮点设置到起始顶点
        currX = x;
        currY = y;
        finalized = false;
    }
    
    public void processCursorEvent(MouseEvent e, int t) {
        //首先更新浮点坐标

        currX = e.getX();
        currY = e.getY();
        //右键按下时,将浮点加入到顶点集里
        if (t == IShape.RIGHT_PRESSED) {
            pointsSet.addPoint(currX, currY);
        //左键按下时,设置多边形到完成状态,并且将浮点加入顶点集中
        } else if (t == IShape.LEFT_RELEASED) {
            finalized = true;
            pointsSet.addPoint(currX, currY);
        }
        /* 注意:上面的if-else结构只包含了RIGHT_PRESSED和LEFT_RELEASED两种情况,
           不过,这个方法也处理了CURSOR_DRAGGED这种情况,就是更新浮点坐标 */

    }
    
    public void draw(Graphics2D g) {
        g.setColor(color);
        g.setStroke(stroke);
        if (finalized) {
            //一旦图形完成,浮点就不再用到了
            int[][] points = pointsSet.getPoints();
            int s = points[0].length;
            //这部分跟PolyLine类似
            if (s == 1) {
                int x = points[0][0];
                int y = points[1][0];
                g.drawLine(x, y, x, y);
            } else {
                g.drawPolygon(points[0], points[1], s);
            }
        } else { //图形没完成的情况下,显示的时候要用到浮点
            int[][] points = pointsSet.getPoints(currX, currY);
            g.drawPolyline(points[0], points[1], points[0].length);
        }
    }
        
}

『其他』

DrawingBoard(extends JPanel)是附件程序中用的画图板类,它是在TestBoard类上的一个扩展,加入了其他的模型类。另外,它提供了一些方法让外部控制界面来设置绘图色,画图板背景色,画图线条,橡皮擦大小(也是通过改变线条实现的)。这些就不再一一赘述了。

AppFrame(extends JFrame)用来放画图板和控制面板。

此外,在稍微变动代码的情况下,还可以加入新的图形类,当然这些类要实现IShape接口,比如,直接继承RectBoundedShape,定义新的图形显示代码。 

分享到:
评论
1 楼 effort_fan 2010-11-29  
学习了。

相关推荐

    java画图板程序代码

    Java 画图板程序设计与实现 Java 画图板是使用 Java 语言编写的图形绘制程序,提供了基本的绘图功能,如画笔、直线、椭圆、矩形等,用户可以使用该程序绘制简单的图形。下面我们将对 Java 画图板程序进行详细的分析...

    java 画图板 windows画图板简单实现

    通过这个简单的Java画图板项目,初学者可以了解到GUI编程的基本原理,事件监听器的使用,以及如何利用Java的绘图功能来创建动态的用户交互界面。同时,这也是一个良好的起点,可以进一步扩展功能,如添加更多的绘图...

    Java画图板课程设计报告

    这份报告的目的是让学生在实践中掌握Java程序设计的基本原理和方法,通过设计一个用户友好的画图应用程序,提高其编程和软件开发能力。 1. **课程设计选题**:画图板软件开发和设计,旨在让学习者了解图形用户界面...

    Java画图板程序的设计

    Java画图板程序设计是一个涉及图形用户界面(GUI)开发、事件处理、图形绘制等多个方面的IT技术实践。在Java中,我们可以使用内置的Java AWT(Abstract Window Toolkit)和Swing库来创建这样的程序。下面将详细阐述这...

    java 画图板程序

    此画图板程序运用了类似photoshop界面,功能十分强大,界面优美,代码中带有解释易于理解,课程设计的不二选择。代码在workspace/oceanking/src中,只有一个文件,很适于初学者使用。

    一个Java实现的画图板程序

    在本项目中,“一个Java实现的画图板程序”是一个基于Java开发的图形用户界面应用,它允许用户进行基本的绘图操作,如绘制线条、形状、填充颜色等。这个程序通常会利用Java的AWT(Abstract Window Toolkit)或Swing...

    java画图板程序,功能齐全,代码简单

    总的来说,"java画图板程序"是一个涵盖多种Java GUI编程技术的项目,对于学习和理解Java图形用户界面设计和事件处理有很好的实践价值。通过分析和研究这个程序的源代码,开发者不仅可以提升自己的编程技能,还能深入...

    java画图板小程序,包括多种移动,复制,换色等功能

    Java画图板小程序是一个基于Java编程语言开发的图形用户界面应用,它提供了丰富的绘图功能,如移动、复制和换色等,旨在为用户提供一个简单而直观的创作平台。这个程序的设计采用了MVC(Model-View-Controller)架构...

    Java画图板程序设计报告.pdf

    Java 画图板程序设计报告 本报告详细介绍了 Java 画图板应用程序的设计与实现,涵盖了程序的总体设计、详细设计与实现、系统测试等方面。报告中首先介绍了程序的设计要求和环境,包括程序的功能需求、开发平台和...

    java实现小型函数画图板(含源代码、报告、打包的jar文件)

    java课程设计的一个题目。本程序可以画各种函数曲线,功能有: 1.新建、打开、保存画图 2.多种方式输入函数作图 3.可设置画笔颜色、大小 4.可以放大、缩小 5.可以移动坐标和图像 6.实时显示鼠标经过的坐标 jar使用:...

    java画图板系统

    【Java画图板系统】是一个基于Java编程语言开发的图形用户界面(GUI)应用程序,它允许用户在屏幕上绘制图形和图像。这个系统可能是为教育、设计或者简单的绘图练习而设计的,它提供了基本的绘图工具,如选择颜色、...

    Java简易画图板

    总的来说,"Java简易画图板"项目涵盖了Java GUI编程、图形绘制、事件处理以及基础的用户交互设计等多个方面,是学习和实践Java图形编程的一个良好实例。通过这个项目,开发者可以深入了解Java在图形应用中的应用,...

    Java画图板课程设计报告只有报告没有完整源代码

    Java画图板课程设计报告只有报告没有完整源代码 但是报告十分详细,值得参考。

    java学习小总结——画图板制作(附代码)

    在Java学习过程中,创建一个简单的画图板是一个经典的练习项目,它可以帮助我们深入理解图形用户界面(GUI)的构建和事件处理机制。本篇小结将聚焦于如何使用Java实现一个基本的画图板,同时提供相关的源码分析。 1...

    JAVA_画图板实现

    在Java编程语言中,"JAVA_画图板实现"是一个常见的项目,用于学习图形用户界面(GUI)设计和事件处理。这个项目的核心是利用Java的Swing或JavaFX库来创建一个可交互的画布,用户可以在上面进行绘图操作。下面我们将...

    java画图板

    在Java编程语言中,开发一个简单的画图板是一项常见...综上所述,这个“java画图板”项目涵盖了GUI设计、事件处理、图形绘制等多个Java编程的核心概念。对于学习和提升Java GUI编程技能来说,这是一个很好的实践项目。

    Java画图板程序设计报告.doc

    【Java画图板程序设计】 本设计报告详细阐述了一款基于Java编程语言的画图板应用程序的开发过程。设计目标是构建一个具有多种图形绘制、编辑功能的小型画图工具,用户可以在此平台上进行自由创作。 1. **设计要求*...

    java swing 画图板

    这个"java swing 画图板"项目展示了如何利用Java Swing构建一个具有基本绘图功能的应用程序,涵盖了GUI设计、图形绘制、事件处理等多个核心概念。通过学习和实践这样的项目,开发者可以深入理解Java Swing框架及其在...

    JAVA应用程序 画图板

    【JAVA应用程序 画图板】是一个使用JAVA编程语言实现的简单图形用户界面(GUI)应用,它提供了一个画布供用户进行绘画操作。这个程序的核心是利用JAVA的Swing或JavaFX库来创建图形界面,并处理用户的输入事件,如...

Global site tag (gtag.js) - Google Analytics