本文讲述一个画图板应用程序的设计,屏幕抓图如下:

『IShape』

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

view plaincopy to clipboardprint?
  1. public void draw(java.awt.Graphics2D g);
  2. //每个实现IShape的类都在这个方法里面指定它的图形显示代码。
  3. public void processCursorEvent(java.awt.event.MouseEvent evt, int type);
  4. /*
  5. 这个方法是在图形(被用户)绘制过程中,发生相关的鼠标点击和移动事件时调用的。
  6. 第一个参数就是所发生的鼠标事件对象;
  7. 第二个参数取值于IShape所定义的三个常数:RIGHT_PRESSED, LEFT_RELEASED,和CURSOR_DRAGGED。
  8. */

下面这个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。

view plaincopy to clipboardprint?
  1. public PointsSet();
  2. 用默认的初始容量(10)创建一个对象。
  3. public PointsSet(int initCap);
  4. 用指定的初始容量(initCap)创建一个对象。
  5. public void addPoint(int x, int y);
  6. 加入一个新的点到这个集合的末端;如果旧的末端点跟新的点重合,则不重复加入。
  7. public int[][] getPoints();
  8. 将所有点以一个二维数组(int[2][n])返回。第一行是x坐标,第二行是y坐标。
  9. public int[][] getPoints(int x, int y);
  10. 类似上一个方法,只是最后将参数指定的点加在末尾(无论是否跟集合末端的点重合);
  11. 这个方法只被PolyGon用到。

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

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

view plaincopy to clipboardprint?
  1. import java.awt.*;
  2. import java.awt.event.MouseEvent;
  3. public abstract class FreeShape implements IShape {
  4. protected Color color;
  5. protected Stroke stroke;
  6. protected PointsSet pointsSet;
  7. protected FreeShape(Color c, Stroke s, int x, int y) {
  8. pointsSet = new PointsSet(50);
  9. color = c;
  10. stroke = s;
  11. pointsSet.addPoint(x, y);
  12. }
  13. public void processCursorEvent(MouseEvent e, int t) {
  14. if (t != IShape.CURSOR_DRAGGED)
  15. return;
  16. pointsSet.addPoint(e.getX(), e.getY());
  17. }
  18. }

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

view plaincopy to clipboardprint?
  1. public void draw(Graphics2D g) {
  2. g.setColor(color);
  3. g.setStroke(stroke);
  4. int[][] points = pointsSet.getPoints();
  5. int s = points[0].length;
  6. if (s == 1) {
  7. int x = points[0][0];
  8. int y = points[1][0];
  9. g.drawLine(x, y, x, y);
  10. } else {
  11. g.drawPolyline(points[0], points[1], s);
  12. }
  13. }

这个方法里面有一个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里面才行。同时画图工具的切换也需要外部的控件来处理。不过这些都比较简单了,就不多说了。

view plaincopy to clipboardprint?
  1. import java.awt.*;
  2. import java.awt.event.*;
  3. import javax.swing.*;
  4. import java.util.ArrayList;
  5. public class TestBoard extends JPanel
  6. implements MouseListener, MouseMotionListener {
  7. //定义一些常量
  8. public static final int TOOL_PENCIL = 1;
  9. public static final int TOOL_ERASER = 2;
  10. public static final Stroke STROKE = new BasicStroke(1.0f);
  11. public static final Stroke ERASER_STROKE = new BasicStroke(15.0f);
  12. private ArrayList shapes;     //保存所有的图形对象(IShape)
  13. private IShape currentShape;  //指向当前还未完成的图形
  14. private int tool; //代表当前使用的画图工具(TOOL_PENCIL或TOOL_ERASER)
  15. public TestBoard() {
  16. //进行一些初始化
  17. shapes = new ArrayList();
  18. tool = TOOL_PENCIL;
  19. currentShape = null;
  20. //安装鼠标监听器
  21. addMouseListener(this);
  22. addMouseMotionListener(this);
  23. }
  24. //外部的控制界面可以通过这个方法切换画图工具
  25. public void setTool(int t) {
  26. tool = t;
  27. }
  28. //override JPanel的方法。通过调用IShape.draw(Graphics2D)方法来显示图形
  29. protected void paintComponent(Graphics g) {
  30. super.paintComponent(g);
  31. int size = shapes.size();
  32. Graphics2D g2d = (Graphics2D) g;
  33. for (int i=0; i < size; i++) {
  34. ((IShape) shapes.get(i)).draw(g2d);
  35. }
  36. }
  37. public void mousePressed(MouseEvent e) {
  38. /* 当左键点击时,currentShape肯定指向null。根据当前画图工具创建相应图形对象,
  39. 将currentShape指向它,并把这个对象加入到对象集合(shapes)中。另外,调用
  40. repaint()方法将画图板的画面更新一下。 */
  41. if (e.getButton() == MouseEvent.BUTTON1) {
  42. switch (tool) {
  43. case TOOL_PENCIL:
  44. currentShape = new PolyLine(getForeground(),
  45. STROKE, e.getX(), e.getY());
  46. break;
  47. case TOOL_ERASER:
  48. currentShape = new Eraser(this, ERASER_STROKE,
  49. e.getX(), e.getY());
  50. break;
  51. }
  52. shapes.add(currentShape);
  53. repaint();
  54. /* 当右键点击并且currentShape不指向null时,调用currentShape的
  55. processCursorEvent(MouseEvent,int)方法,类型参数是
  56.       IShape.RIGHT_PRESSED。 repaint()*/
  57. } else if (e.getButton() == MouseEvent.BUTTON3 && currentShape != null) {
  58. currentShape.processCursorEvent(e, IShape.RIGHT_PRESSED);
  59. repaint();
  60. }
  61. }
  62. public void mouseDragged(MouseEvent e) {
  63. /* 当鼠标拖拽并且currentShape不指向null时(这种情况下,左键肯定处于
  64. 按下状态),调用currentShape的processCursorEvent(MouseEvent,int)方法,
  65. 类型参数是IShape.CURSOR_DRAGGED。 repaint()*/
  66. if (currentShape != null) {
  67. currentShape.processCursorEvent(e, IShape.CURSOR_DRAGGED);
  68. repaint();
  69. }
  70. }
  71. public void mouseReleased(MouseEvent e) {
  72. /* 当左键被松开并且currentShape不指向null时(这个时候,currentShape
  73. 肯定不会指向null的,多检查一次,保险),调用currentShape的
  74. processCursorEvent(MouseEvent,int)方法,类型参数是
  75. IShape.CURSOR_DRAGGED。 repaint()*/
  76. if (e.getButton() == MouseEvent.BUTTON1 && currentShape != null) {
  77. currentShape.processCursorEvent(e, IShape.LEFT_RELEASED);
  78. currentShape = null;
  79. repaint();
  80. }
  81. }
  82. //对下面这些事件不感兴趣
  83. public void mouseClicked(MouseEvent e) {}
  84. public void mouseEntered(MouseEvent e) {}
  85. public void mouseExited(MouseEvent e) {}
  86. public void mouseMoved(MouseEvent e) {}
  87. }

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

『RectBoundedShape』

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

view plaincopy to clipboardprint?
  1. import java.awt.*;
  2. import java.awt.event.MouseEvent;
  3. public abstract class RectBoundedShape implements IShape {
  4. protected Color color;
  5. protected Stroke stroke;
  6. protected int startX, startY, endX, endY;
  7. protected RectBoundedShape(Color c, Stroke s, int x, int y) {
  8. color = c;
  9. stroke = s;
  10. startX = endX = x;
  11. startY = endY = y;
  12. }
  13. public void processCursorEvent(MouseEvent e, int t) {
  14. if (t != IShape.CURSOR_DRAGGED)
  15. return;
  16. int x = e.getX();
  17. int y = e.getY();
  18. if (e.isShiftDown()) {
  19. regulateShape(x, y);
  20. } else {
  21. endX = x;
  22. endY = y;
  23. }
  24. }
  25. protected void regulateShape(int x, int y) {
  26. int w = x - startX;
  27. int h = y - startY;
  28. int s = Math.min(Math.abs(w), Math.abs(h));
  29. if (s == 0) {
  30. endX = startX;
  31. endY = startY;
  32. } else {
  33. endX = startX + s * (w / Math.abs(w));
  34. endY = startY + s * (h / Math.abs(h));
  35. }
  36. }
  37. }

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

view plaincopy to clipboardprint?
  1. public void processCursorEvent(MouseEvent e, int t) {
  2. if (t != IShape.CURSOR_DRAGGED)
  3. return;
  4. int x = e.getX();
  5. int y = e.getY();
  6. if (e.isShiftDown()) {
  7. //这个情况单独处理,不然就要除以0了
  8. if (x - startX == 0) { //竖直
  9. endX = startX;
  10. endY = y;
  11. } else {
  12. //由于对称性,只要算斜率的绝对值
  13. float slope = Math.abs(((float) (y - startY)) / (x - startX));
  14. //小于30度,变成水平的
  15. if (slope < 0.577) {
  16. endX = x;
  17. endY = startY;
  18. //介于30度跟60度中间的,变成45度,利用父类的regulateShape(int,int)完成
  19. } else if (slope < 1.155) {
  20. regulateShape(x, y);
  21. //大于60度,变成竖直的
  22. } else {
  23. endX = startX;
  24. endY = y;
  25. }
  26. }
  27. //如果shift键没有按下,跟父类一样处理
  28. } else {
  29. endX = x;
  30. endY = y;
  31. }
  32. }

『PolyGon』

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

view plaincopy to clipboardprint?
  1. import java.awt.*;
  2. import java.awt.event.MouseEvent;
  3. public class PolyGon implements IShape {
  4. //类似于FreeShape和RectBoundedShape的变量
  5. private Color color;
  6. private Stroke stroke;
  7. //记录所有顶点坐标,姑且称之为顶点集
  8. private PointsSet pointsSet;
  9. //记录多边形是否完成。true表示完成
  10. private boolean finalized;
  11. //记录画图过程中鼠标被拖拽到的点,姑且称之为浮点吧^_^
  12. private int currX, currY;
  13. public PolyGon(Color c, Stroke s, int x, int y) {
  14. pointsSet = new PointsSet();
  15. color = c;
  16. stroke = s;
  17. pointsSet.addPoint(x, y);
  18. //刚开始先把浮点设置到起始顶点
  19. currX = x;
  20. currY = y;
  21. finalized = false;
  22. }
  23. public void processCursorEvent(MouseEvent e, int t) {
  24. //首先更新浮点坐标
  25. currX = e.getX();
  26. currY = e.getY();
  27. //右键按下时,将浮点加入到顶点集里
  28. if (t == IShape.RIGHT_PRESSED) {
  29. pointsSet.addPoint(currX, currY);
  30. //左键按下时,设置多边形到完成状态,并且将浮点加入顶点集中
  31. } else if (t == IShape.LEFT_RELEASED) {
  32. finalized = true;
  33. pointsSet.addPoint(currX, currY);
  34. }
  35. /* 注意:上面的if-else结构只包含了RIGHT_PRESSED和LEFT_RELEASED两种情况,
  36. 不过,这个方法也处理了CURSOR_DRAGGED这种情况,就是更新浮点坐标 */
  37. }
  38. public void draw(Graphics2D g) {
  39. g.setColor(color);
  40. g.setStroke(stroke);
  41. if (finalized) {
  42. //一旦图形完成,浮点就不再用到了
  43. int[][] points = pointsSet.getPoints();
  44. int s = points[0].length;
  45. //这部分跟PolyLine类似
  46. if (s == 1) {
  47. int x = points[0][0];
  48. int y = points[1][0];
  49. g.drawLine(x, y, x, y);
  50. } else {
  51. g.drawPolygon(points[0], points[1], s);
  52. }
  53. } else { //图形没完成的情况下,显示的时候要用到浮点
  54. int[][] points = pointsSet.getPoints(currX, currY);
  55. g.drawPolyline(points[0], points[1], points[0].length);
  56. }
  57. }
  58. }

『其他』

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

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

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

转载于:https://www.cnblogs.com/jufu/p/4182295.html

Java画图程序设计相关推荐

  1. 《JAVA语言程序设计》期末考试试题及答案

    文章目录 <JAVA语言程序设计>期末考试试题及答案1(应考必备题库) 一.单选择题 二.填空题 三.写出下面程序的运行结果 <JAVA语言程序设计>期末考试试题及答案2(应考 ...

  2. 2020年8月Java语言程序设计(一)试题及答案

    Java语言程序设计(一) (课程代码04747) 注意事项: 1.本试卷分为两部分,第-部分为选择题,第二部分为非选择题. 2.应考省必须按试题顺序在答题卡(纸)指定位置上作答,答在试卷上无效. 3 ...

  3. 四川大学java试题_四川大学2013年计算机(软件)学院Java语言程序设计期末考试试题B卷...

    四川大学期末考试试题(闭卷) (2013 -2014学年第1学期) 课程号:课程名称: Java语言程序设计(B 卷)任课教师: 适用专业年级:学号:姓名: 一.单项选择题(本大题共20小题,每小题2 ...

  4. java语言程序设计期末复习综合练习题_Java语言程序设计期末复习综合练习题答案...

    Java语言程序设计期末复习综合练习题 一.选择题. 2. main方法是Java Application程序执行的入口点,关于main方法的方法头以下哪项是合法的( )? A.public stat ...

  5. 2018年10月自考java_请注意!2018年自考《Java语言程序设计(一)》课程全国统一命题考试...

    为组织好高等教育自学考试<Java语言程序设计(一)>课程的全国统一考试命题工作,根据全国统一命题课程的有关规定,特制定本说明. 一.考试原则 1.考试标准 本课程考试参照全日制普通高校同 ...

  6. 20155328 《Java程序设计》 实验二(Java面向对象程序设计) 实验报告

    20155328 <Java程序设计> 实验二(Java面向对象程序设计) 实验报告 单元测试 一.单元测试和TDD 编程时需理清思路,将编程需求等想好,再开始编.此部分可用伪代码实现. ...

  7. java语言程序设计考点_计算机二级考试Java语言程序设计考点:关键字

    大家回忆一下我们在学习汉语的时候,开始学的是什么?肯定是先学一些单个的字,只有认识了单个的字,然后才能组成词,然后才能慢慢的到句子,然后到文章.学习同计算机交流跟这个过程是一样的,首先我们得学习一些计 ...

  8. 2012年1月java_全国2012年1月自考Java语言程序设计(一)试题及答案.doc

    全国2012年1月自考Java语言程序设计(一)试题及答案 全国2012年10月自考Java语言程序设计(一)试题 课程代码:04747 选择题部分 一.单项选择题 本大题共10小题,每小题1分,共1 ...

  9. java程序设计试题_《Java语言程序设计》期末考试模拟试题——填空题和编程题...

    一.根据题意,填写出空格中的内容 Java平台包括三个技术方向,其中J2ME代表____________.J2SE代表___________.J2EE代表____________.2.面向对象的四大概 ...

最新文章

  1. 数字信号处理与音频处理(使用Audition)
  2. MyBatis—insert语句返回主键和selectKey标签
  3. 数值格式化,每隔三位加一个逗号
  4. Struts 2的基石——拦截器(Interceptor)
  5. 2019-10-12 拉普拉斯变换的理解
  6. Spark编程指南V1.4.0(翻译)
  7. leveldb - sstable格式
  8. Lesson 3 Part 2 logistic regression
  9. Atitit 音频资源管理法 与教程 音频资源分类法 卡拉ok功能 人声消除给你教程 Atitit 音频功能常见工具与类库 Atitit 调整播放速率 Atitit、 ffmpeg录音 atit
  10. docker 停止、启动、删除镜像指令操作总结
  11. LPVOID 没有类型的指针
  12. 这就是CDN回源原理和CDN多级缓存啊!
  13. Linux 上安装Realtek瑞昱网卡驱动
  14. 麒麟V10系统-系统激活点击按钮无响应
  15. 我的世界刷猪人塔java版_我的世界猪人塔怎么做 5款猪人塔详解教程
  16. 2021年西式面点师(初级)考试题及西式面点师(初级)免费试题
  17. plt.scatter 中cmap参数详解
  18. 华为eNSP配置防火墙进入web界面
  19. 辞旧迎新,2013年年终感言
  20. el-upload 上传压缩图片

热门文章

  1. c语言 求两个数的平均值
  2. Spring整合JPA
  3. 推荐一款超级下载利器工具,突破网盘的下载限制
  4. CF76A.Gift [最小生成树]
  5. python学习笔记3-解析配置文件ConfigParser模块
  6. 面试 cocos2dx
  7. 高压之下依然能高效发挥才是真本事
  8. alin的学习之路:共享内存
  9. MUI移动端开发,嵌入ifream,页面返回问题解决
  10. public、private、protected的区别