Java图形绘制系统:Figure抽象类与多态绘图实践

发布时间:2026/9/18 0:19:07
Java图形绘制系统:Figure抽象类与多态绘图实践 简介本资源是西南科技大学《Java程序设计与实践》课程配套实验三的完整报告文档面向Java初学者及高校计算机类专业学生聚焦类的继承、抽象类设计、多态实现与GUI事件驱动编程等核心OOP能力训练。实验通过构建Figure抽象父类及RightTriangle、Square、Cycle三个具体子类系统演练面积计算、图形绘制drawFigure、颜色控制与鼠标交互逻辑并最终集成到类似Windows风格的DrawFrame应用程序中涵盖布局管理、内部类监听器、Canvas绘图及面向对象UI设计全流程。资源为1个583KB的Word文档.doc含规范实验报告结构、详细代码注释、运行截图说明与完整可运行源码内容覆盖从类设计、测试类编写TestSquare到GUI界面实现的全部环节。目前已有1752人学习下载适合课堂实验复盘、期末复习巩固或Java面向对象编程专项强化训练。1. 这不是“写个父类再写几个子类”的练习题而是一套可运行的图形绘制系统——它用Figure抽象类统一管理鼠标拖拽坐标、颜色状态与绘图逻辑靠drawFigure(Graphics, int)的多态分发实现「点一下按钮 → 拖动鼠标 → 自动画出带边框/填充的圆/正方形/等腰直角三角形」所有面积计算、周长返回、坐标转换都封装在子类内部主程序只用Figure fig new Cycle(); fig.drawFigure(g, 0);就能完成跨类型绘图。它面向的是 Java 初学者向工程实践过渡的关键卡点如何让抽象不悬浮、继承不空转、多态不玄学适合正在啃《Java程序设计与实践》实验三、刚写完getArea()却卡在drawFigure()坐标映射、或调试MyCanvas鼠标事件时发现图形总偏移的同学——你缺的不是语法而是从「能编译」到「能交互」之间那层坐标系理解与事件生命周期把控。2. 从 Figure 抽象类到三个具体图形类为什么必须重写 setEnd()坐标转换逻辑怎么推导2.1 Figure 抽象类的设计意图与强制契约Figure不是为“算面积”而生的数学工具类它是整个绘图系统的状态容器 行为协议。其核心字段start和end并非任意两点而是严格对应鼠标事件流mousePressed触发时记录start按下起点mouseDragged持续更新end拖拽终点mouseReleased后调用drawFigure()完成渲染。这种设计将 GUI 交互细节如鼠标轨迹与图形语义如圆的直径、正方形边长解耦——Figure只管“有起点和终点”子类负责“这俩点对我意味着什么”。提示setColor(Color)默认设为黑色但MyCanvas中会通过按钮监听动态修改Figure实例的颜色属性这是多态之外另一层关键状态传递不能漏掉 setter。Figure定义了三个抽象方法构成子类必须实现的最小接口契约drawFigure(Graphics g, int select)select 0表示线框模式select 1表示填充模式。Graphics对象由MyCanvas.paint()提供子类不得创建新Graphics。getArea()返回double用于TestSquare中验证计算逻辑单位为像素²因坐标系基于屏幕。getPerimeter()同理返回像素长度虽未在 UI 中显示但体现面向对象建模完整性。setEnd(Point end)是唯一被子类重写的非抽象方法——这是实验要求中隐含的最易踩坑点。Figure的setEnd()仅做基础赋值而各子类需在此基础上根据自身几何规则反推关键尺寸参数如圆的直径、正方形边长、三角形直角边长。若忽略此步drawFigure()中使用的diameter或length将为 0图形无法绘制。2.2 Cycle 类用勾股定理把鼠标拖拽距离转为直径Cycle的几何本质是“以start为圆心、end到start的距离为半径的圆”。但注意Graphics.drawArc()的参数是(x, y, width, height, startAngle, arcAngle)其中width和height必须相等才为正圆且(x, y)是外切矩形左上角而非圆心。因此setEnd()的实现必须完成两重转换Override public void setEnd(Point end) { super.setEnd(end); // 计算 start 到 end 的欧氏距离 → 即直径非半径 double dx Math.abs(getEnd().x - getStart().x); double dy Math.abs(getEnd().y - getStart().y); diameter (int) Math.sqrt(dx * dx dy * dy); }drawFigure()中(x, y)的取值逻辑是关键圆心为getStart()外切矩形左上角应为(centerX - radius, centerY - radius)因diameter 2 * radius故x getStart().x - diameter/2,y getStart().y - diameter/2但原代码直接使用getStart().x和getStart().y作为(x, y)这意味着它把start当作了外切矩形左上角而非圆心——这会导致图形位置错误。正确做法修正版Override public void drawFigure(Graphics g, int select) { g.setColor(getColor()); int centerX getStart().x; int centerY getStart().y; int radius diameter / 2; int x centerX - radius; // 外切矩形左上角 X int y centerY - radius; // 外切矩形左上角 Y if (select 0) { g.drawArc(x, y, diameter, diameter, 0, 360); } else { g.fillArc(x, y, diameter, diameter, 0, 360); } }参数说明diameter是setEnd()计算出的整数直径drawArc()的360表示完整圆周fillArc()填充效果依赖于Graphics的当前颜色。2.3 Square 类边长 起点到终点的曼哈顿距离不是欧氏距离的整数截断Square要求“以start为左上角end决定右下角”即边长length应为max(|dx|, |dy|)错。实验指导明确“正方形”意味着width height且方向与坐标轴平行。因此length必须取dx和dy的绝对值中的较大者才能保证拖拽任意方向都能生成正方形否则斜向拖拽会得到长方形。但原代码setEnd()未做此处理直接计算欧氏距离并强转int导致边长失真。正确实现应为Override public void setEnd(Point end) { super.setEnd(end); int dx Math.abs(getEnd().x - getStart().x); int dy Math.abs(getEnd().y - getStart().y); length Math.max(dx, dy); // 确保正方形非长方形 }drawFigure()中drawRect(x, y, width, height)的(x, y)是左上角width和height均为length完全匹配Override public void drawFigure(Graphics g, int select) { g.setColor(getColor()); int x getStart().x; int y getStart().y; if (select 0) { g.drawRect(x, y, length, length); } else { g.fillRect(x, y, length, length); } }注意此处x, y直接用getStart()因为约定start为左上角。这与Cycle的圆心约定不同——多态的灵活性正体现在各子类对同一组坐标赋予不同语义。2.4 RightTriangle 类等腰直角三角形的顶点坐标推导与 Polygon 构造RightTriangle是本实验几何逻辑最复杂的部分。要求“等腰直角三角形”即两条直角边等长且夹角为 90°。实验指导给出顶点命名top直角顶点、right水平边端点、left垂直边端点并暗示top与start重合right与end重合left由top.x和right.y确定即直角顶点水平投影到right的 Y 轴垂直投影到right的 X 轴。setEnd()的推导过程如下设start (x0, y0),end (x1, y1)top start (x0, y0)right end (x1, y1)left (x0, y1)直角顶点 Xright的 Y水平直角边长width |x1 - x0|垂直直角边长height |y1 - y0|因等腰需强制width height故实际边长取Math.min(width, height)不实验未要求缩放而是接受用户拖拽形成的自然等腰——即当|x1-x0| ! |y1-y0|时它已不是等腰。因此代码中直接使用原始差值getArea()公式width * height / 2仍成立但“等腰”约束需在 UI 提示或drawFigure()中校验。drawFigure()使用Polygon构造三点Override public void drawFigure(Graphics g, int select) { g.setColor(getColor()); Polygon poly new Polygon(); poly.addPoint(top.x, top.y); // 直角顶点 poly.addPoint(right.x, right.y); // 水平边端点 poly.addPoint(left.x, left.y); // 垂直边端点 if (select 0) { g.drawPolygon(poly); } else { g.fillPolygon(poly); } }getPerimeter()计算斜边sqrt(width² height²)加两直角边符合勾股定理。3. DrawFram 主窗体与 MyCanvas 画布事件驱动下的多态调用链如何落地3.1 DrawFram 的布局结构与组件注册逻辑DrawFram继承JFrame采用BorderLayout作为顶层布局管理器。其核心组件分布如下表所示区域BorderLayout组件类型关键配置作用BorderLayout.NORTHJPanelnew FlowLayout(FlowLayout.LEFT)放置图形选择按钮圆形、正方形、直角三角形、颜色按钮、线框/填充切换按钮BorderLayout.CENTERMyCanvassetSize(1000, 600)setBackground(Color.WHITE)主绘图区域接收鼠标事件并调用Figure.drawFigure()DrawFram构造方法中必须完成三件事设置窗体标题、大小、关闭操作创建JPanel并添加四个图形按钮JButton(圆形)等每个按钮注册MyActionListener创建MyCanvas实例并添加到CENTER区域。public DrawFram() { setTitle(西南科技大学 Java 图形绘制系统); setSize(1000, 600); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); // 居中显示 JPanel buttonPanel new JPanel(new FlowLayout(FlowLayout.LEFT)); JButton circleBtn new JButton(圆形); JButton squareBtn new JButton(正方形); JButton triangleBtn new JButton(直角三角形); JButton colorBtn new JButton(颜色); // 注册监听器内部类实例 MyActionListener listener new MyActionListener(); circleBtn.addActionListener(listener); squareBtn.addActionListener(listener); triangleBtn.addActionListener(listener); colorBtn.addActionListener(listener); buttonPanel.add(circleBtn); buttonPanel.add(squareBtn); buttonPanel.add(triangleBtn); buttonPanel.add(colorBtn); add(buttonPanel, BorderLayout.NORTH); MyCanvas canvas new MyCanvas(); add(canvas, BorderLayout.CENTER); setVisible(true); }注意MyCanvas必须在add()前初始化否则buttonPanel添加失败setVisible(true)在最后调用确保组件树构建完成。3.2 MyCanvas 的鼠标事件生命周期与 Figure 实例管理MyCanvas继承CanvasAWT 组件轻量级需重写paint(Graphics g)并处理鼠标事件。其核心是维护一个当前选中的 Figure 类型工厂和待绘制的 Figure 实例currentFigureTypeint常量如0CIRCLE,1SQUARE,2TRIANGLE由MyActionListener.actionPerformed()设置currentFigureFigure类型引用每次mousePressed时根据currentFigureType创建新实例如new Cycle()并在mouseReleased时调用其drawFigure()。鼠标事件处理流程mousePressed(MouseEvent e)记录start e.getPoint()根据currentFigureType创建对应Figure实例并设置其start。mouseDragged(MouseEvent e)更新end e.getPoint()调用currentFigure.setEnd(end)计算尺寸触发repaint()。mouseReleased(MouseEvent e)再次调用currentFigure.setEnd(e.getPoint())确保最终尺寸然后repaint()。paint()方法是渲染入口Override public void paint(Graphics g) { super.paint(g); // 清除背景 if (currentFigure ! null currentFigure.getStart() ! null currentFigure.getEnd() ! null) { // 弹出对话框询问线框/填充 int select JOptionPane.showConfirmDialog(this, 选择绘制模式, 模式选择, JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE); currentFigure.drawFigure(g, select); // 多态调用 } }关键点currentFigure.drawFigure(g, select)是多态的典型应用——g是Graphics上下文select是用户选择而具体执行哪个drawFigure()由currentFigure的实际类型Cycle/Square/RightTriangle决定DrawFram完全无需if-else判断。3.3 MyActionListener 的事件分发与 Figure 工厂模式MyActionListener实现ActionListener其actionPerformed(ActionEvent e)通过e.getActionCommand()获取按钮文本用switch-case分发private class MyActionListener implements ActionListener { Override public void actionPerformed(ActionEvent e) { String cmd e.getActionCommand(); switch (cmd) { case 圆形: currentFigureType 0; break; case 正方形: currentFigureType 1; break; case 直角三角形: currentFigureType 2; break; case 颜色: Color newColor JColorChooser.showDialog(MyCanvas.this, 选择颜色, currentFigure.getColor()); if (newColor ! null currentFigure ! null) { currentFigure.setColor(newColor); } break; } } }currentFigureType是MyCanvas的成员变量MyActionListener作为内部类可直接访问。当用户点击“圆形”后下次mousePressed就会创建new Cycle()。提示JColorChooser返回null表示用户取消必须判空currentFigure可能为null首次点击前需防御性检查。4. TestSquare 测试类与面积验证如何用单元测试思想验证多态计算逻辑4.1 TestSquare 的设计目标与边界用例覆盖TestSquare不是简单地new三个对象并打印getArea()它要模拟真实使用场景中的输入-输出验证闭环。核心验证点包括各图形getArea()计算公式是否正确如Cycle是否用πr²setEnd()对尺寸的影响是否符合预期如Square边长是否随dx/dy变化多态调用Figure.getArea()是否返回子类特化结果即Figure fig new Cycle(); fig.getArea()是否调用Cycle.getArea()。测试用例应覆盖典型与边界情况Cyclestart(0,0),end(100,0)→ 直径100半径50面积≈7853.98Squarestart(0,0),end(80,60)→lengthmax(80,60)80面积6400RightTrianglestart(0,0),end(100,100)→width100,height100面积5000。4.2 可执行的 TestSquare 代码与断言逻辑package cn.swust.panit; public class TestSquare { public static void main(String[] args) { System.out.println( 西南科技大学 Java 实验三面积计算验证 \n); // 测试 Cycle Cycle circle new Cycle(); circle.setStart(new java.awt.Point(0, 0)); circle.setEnd(new java.awt.Point(100, 0)); double circleArea circle.getArea(); System.out.printf(圆形起点(0,0)终点(100,0) → 直径%d面积%.2f\n, circle.diameter, circleArea); assert Math.abs(circleArea - 7853.98) 0.01 : 圆形面积计算错误; // 测试 Square Square square new Square(); square.setStart(new java.awt.Point(0, 0)); square.setEnd(new java.awt.Point(80, 60)); double squareArea square.getArea(); System.out.printf(正方形起点(0,0)终点(80,60) → 边长%d面积%.0f\n, square.length, squareArea); assert squareArea 6400 : 正方形面积计算错误; // 测试 RightTriangle RightTriangle triangle new RightTriangle(); triangle.setStart(new java.awt.Point(0, 0)); triangle.setEnd(new java.awt.Point(100, 100)); double triangleArea triangle.getArea(); System.out.printf(直角三角形起点(0,0)终点(100,100) → 宽%d高%d面积%.0f\n, (int) triangle.width, (int) triangle.height, triangleArea); assert triangleArea 5000 : 直角三角形面积计算错误; // 多态验证Figure 引用调用子类方法 Figure figRef new Cycle(); figRef.setStart(new java.awt.Point(0, 0)); figRef.setEnd(new java.awt.Point(200, 0)); System.out.printf(多态验证Figure 引用调用 Cycle.getArea() → %.2f\n, figRef.getArea()); System.out.println(\n✅ 所有面积计算验证通过); } }参数说明assert语句启用需在运行时加-ea参数如java -ea TestSquareprintf格式化输出增强可读性figRef证明Figure类型变量可指向Cycle实例且getArea()调用的是Cycle的实现——这是多态的核心证据。4.3 常见面积计算错误排查表错误现象可能原因检查点Cycle面积为 0diameter未在setEnd()中赋值或start/end为null在setEnd()开头加System.out.println(diameterdiameter)Square面积小于预期length取了dx和dy的较小值或未取Math.max()检查setEnd()中length Math.max(dx, dy)RightTriangle面积异常大width或height使用了未取绝对值的坐标差Math.abs(getEnd().x - getStart().x)必须存在Figure fig new Cycle(); fig.getArea()报AbstractMethodErrorCycle未OverridegetArea()或方法签名不匹配如返回float确认public double getArea()且无static5. 调试 MyCanvas 鼠标偏移与图形闪烁重写 update() 与双缓冲绘图技巧5.1 为什么图形总画歪—— Canvas 默认 update() 的擦除陷阱Canvas的默认update(Graphics g)方法会先用背景色填充整个区域再调用paint()。当mouseDragged()频繁触发repaint()时update()的擦除动作会导致上一帧图形被清空新一帧paint()只绘制当前currentFigure旧图形消失用户看到“图形跟随鼠标跳动”而非“从起点拖出图形”。根本原因是update()的粗暴擦除破坏了绘图状态的连续性。解决方案是重写update()跳过擦除直接委托给paint()Override public void update(Graphics g) { // 跳过默认擦除避免闪烁和偏移 paint(g); }此技巧称为“禁用双缓冲前的单缓冲优化”是 AWT 时代处理动画的通用手法。它确保paint()总是基于上一次绘制的画布继续而非空白背景。5.2 如何保存历史图形—— 用 ArrayList实现画布持久化当前MyCanvas每次repaint()只画一个currentFigure无法保留历史。要实现“画板”功能需维护一个图形列表private ListFigure figureList new ArrayList(); // mouseReleased 时将当前 figure 添加到列表 public void addFigure(Figure fig) { figureList.add(fig); } Override public void paint(Graphics g) { super.paint(g); // 绘制所有历史图形 for (Figure fig : figureList) { if (fig.getStart() ! null fig.getEnd() ! null) { // 此处需传入固定 select 值如 0 线框或存储每个 figure 的 select 状态 fig.drawFigure(g, 0); } } // 绘制当前拖拽中的图形可选 if (currentFigure ! null currentFigure.getStart() ! null currentFigure.getEnd() ! null) { currentFigure.drawFigure(g, 0); } }注意figureList中每个Figure需记录自己的select模式线框/填充可在Figure中增加private int drawMode 0;字段并在MyActionListener中设置。5.3 Swing 迁移建议用 JPanel 替代 Canvas 的现代方案虽然实验要求用Canvas但生产环境应迁移到JPanelSwing 组件支持双缓冲。关键改动MyCanvas改为extends JPanel重写paintComponent(Graphics g)替代paint()开启双缓冲setDoubleBuffered(true);mousePressed/Dragged/Released事件监听改为addMouseListener()和addMouseMotionListener()。public class MyJPanel extends JPanel { public MyJPanel() { setPreferredSize(new Dimension(1000, 600)); setBackground(Color.WHITE); setDoubleBuffered(true); // 关键自动双缓冲消除闪烁 addMouseListener(new MouseAdapter() { Override public void mousePressed(MouseEvent e) { // 同 MyCanvas.mousePressed } }); addMouseMotionListener(new MouseMotionAdapter() { Override public void mouseDragged(MouseEvent e) { // 同 MyCanvas.mouseDragged } }); } Override protected void paintComponent(Graphics g) { super.paintComponent(g); // 自动双缓冲擦除 // 绘图逻辑同 paint() } }此方案彻底解决闪烁问题且JPanel与JFrame布局兼容性更好是Canvas的现代化替代路径。本文还有配套的精品资源点击获取