Java画图程序设计
本文讲述一个画图板应用程序的设计,屏幕抓图如下:
『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,定义新的图形显示代码。
Java画图程序设计的更多相关文章
- 20165235 实验二Java面向对象程序设计
20165235 Java面向对象程序设计 姓名:祁瑛 学号:20165235 班级:1652 实验课程:JAVA程序设计 实验名称:Java面向对象程序设计 实验时间:2018.4.14 指导老师: ...
- Java面向对象程序设计
北京电子科技学院(BESTI) 实 ...
- 20172308 实验二《Java面向对象程序设计 》实验报告
20172308 2017-2018-2 <程序设计与数据结构>实验2报告 课程:<程序设计与数据结构> 班级: 1723 姓名: 周亚杰 学号:20172308 实验教师:王 ...
- java基础学习03(java基础程序设计)
java基础程序设计 一.完成的目标 1. 掌握java中的数据类型划分 2. 8种基本数据类型的使用及数据类型转换 3. 位运算.运算符.表达式 4. 判断.循环语句的使用 5. break和con ...
- 实验二 Java面向对象程序设计
实验二 Java面向对象程序设计 实验内容 1. 初步掌握单元测试和TDD 2. 理解并掌握面向对象三要素:封装.继承.多态 3. 初步掌握UML建模 4. 熟悉S.O.L.I.D原则 5. 了解设计 ...
- 20145212《Java程序程序设计》课程总结
20145212<Java程序程序设计>课程总结 一.每周读书笔记链接汇总 第一周读书笔记 第二周读书笔记 第三周读书笔记 第四周读书笔记 第五周读书笔记 第六周读书笔记 第七周读书笔记 ...
- 20145212《Java程序设计》实验报告二 《 Java面向对象程序设计》
20145212 实验二< Java面向对象程序设计> 实验内容 单元测试 三种代码 伪代码 百分制转五分制: 如果成绩小于60,转成"不及格" 如果成绩在60与70之 ...
- 20145213《Java程序设计》实验二Java面向对象程序设计实验报告
20145213<Java程序设计>实验二Java面向对象程序设计实验报告 实验内容 初步掌握单元测试和TDD 理解并掌握面向对象三要素:封装,继承,多态 初步掌握UML建模 熟悉S.O. ...
- 20145206《Java程序设计》实验二Java面向对象程序设计实验报告
20145206<Java程序设计>实验二Java面向对象程序设计实验报告 实验内容 初步掌握单元测试和TDD 理解并掌握面向对象三要素:封装.继承.多态 初步掌握UML建模 熟悉S.O. ...
随机推荐
- a new blog from MarsEdit
终于在网上找到了一款mac 下的blog写作和发布工具了. 先测试一下.
- python的with...as用法
with...as叫做上下文管理器,作用是进入一个对象的作用域和离开时,可以执行执行一定的操作.这个操作是可以自己 设定的. 写个例子学习一下: class test(): def __init__( ...
- jdk动态代理实现
1.jdk动态代理的简单实现类 package com.proxy; import java.lang.reflect.InvocationHandler; import java.lang.refl ...
- JLINK通过JFLASH烧写bin文件报错处理方法
错误原因:烧写开始地址出错,打开BIN文件后弹出的设置开始地址不正确不能为0 解决措施:用J-FLASH LITE或者将开始地址设置成正确的地址(KEILMDK中IROM1的开始地址
- Linux文件权限概念
一.Linux文件属性 1.第一列代表这个文件的类型与权限(permission): 共有10个字符 第一个字符代表这个文件的类型,是"目录,文件或链接文件等": [d]----& ...
- 从感知器到SVM
这篇文章主要是分析感知器和SVM处理分类问题的原理,不涉及求解 感知器: 感知器要解决的是这样的一个二分类问题:给定了一个线性可分的数据集,我们需要找到一个超平面,将该数据集分开.这个超平面的描述如下 ...
- centos 安装 python2.7 运行webpy 项目
1.服务器是centos5,在virtualbox里装的.网络选择桥接,ip与主机在一个网段类.主机ip为xxx.xxx.xxx.69,服务器ip定义为xxx.xxx.xxx.66,GATEWAY与N ...
- hadoop 完全分布式 下 datanode无法启动解决方法
问题描述: 在集群模式下更改节点后,启动集群发现 datanode一直启动不起来. 我集群配置:有5个节点,分别为master slave1-5 . 在master以Hadoop用户执行:start- ...
- struts的标签库出现Failed to load or instantiate TagExtraInfo class
使用struts的标签库出现Failed to load or instantiate TagExtraInfo class 最近在使用struts标签库的时候,在eclipse开发环境中是正常的,放 ...
- 高质量C++/C编程指南(林锐)
推荐-高质量C++/C编程指南(林锐) 版本/状态 作者 参与者 起止日期 备注 V 0.9 草稿文件 林锐 2001-7-1至 2001-7-18 林锐起草 V 1.0 正式文件 林锐 20 ...