news 2026/9/18 16:59:55

Java Swing 汉诺塔图形课设:六类拆分与递归自动演示

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java Swing 汉诺塔图形课设:六类拆分与递归自动演示

简介:这份资源是面向Java初学者与高校课程设计学生的汉诺塔(Hannoi塔)游戏课程设计报告,围绕递归算法与Swing图形界面开发展开,适合正在完成Java程序设计课程设计、需要参考完整项目实现思路与文档写作规范的读者。包内共1个doc文档,约606KB,内容涵盖课程设计目的、系统需求分析、系统概要设计与详细设计、开发环境(J2SDK与Eclipse)等章节。文档以HannoiWindow、Tower、TowerPoint、Disc、HandleMouse、AutoMoveDisc六个类的组合关系为主线,讲解盘子拖动、大小规则校验、盘子数量与颜色调整、背景音乐控制、自动移动与重新开始等功能的实现细节,并附有UML类图与类间关系说明,同时梳理了递归边界条件、GUI布局事件处理、非法操作提示等常见难点的解决思路。已有111人学习下载,可供读者对照参考,快速理清面向对象设计与递归求解的完整开发流程。

1. 从一份拆成 6 个类的 Hannoi 塔课设说起

汉诺塔的递归三行代码谁都会背,难的是把它做成能拖、能校验、能自动演示的图形程序。这份课设的思路值得借鉴:它没有把所有逻辑塞进一个 main 方法,而是切成 HannoiWindow、Tower、TowerPoint、Disc、HandleMouse、AutoMoveDisc 六个类,各管一段——窗口管菜单和按钮,Tower 管三个座和坐标,TowerPoint 管每个可落位置,Disc 干脆继承 JButton 当盘子用,鼠标逻辑和自动求解各自独立成类。对刚学完 Java 面向对象、准备做课设的在校生来说,这是一份能直接跑起来、也能顺手改造的模板;对已经写了几年业务代码的人,回头看这套「用坐标和状态位模拟物理规则」的朴素做法,反而能看清 GUI 事件驱动模型的骨架。

2. HannoiWindow 窗口装配与三档难度的状态复位

主窗口是整个程序唯一的入口,它不做游戏逻辑,只负责组装面板、发号施令。新手最容易在这里犯的错是把盘子的创建、坐标计算、鼠标监听全写进构造函数,结果窗口一改尺寸整个程序就崩。合理的边界是:窗口只持有 Tower 的引用,通过几个 set 方法把参数递进去。

2.1 窗口骨架与布局参数

public class HannoiWindow extends JFrame implements ActionListener { Tower tower = null; int amountOfDisc = 3; char[] towerName = {'A', 'B', 'C'}; JMenuBar bar; JMenu menuGrade; JMenuItem oneGradeItem, twoGradeItem, threeGradeItem; JButton renew = null, autoButton = null; HannoiWindow() { tower = new Tower(towerName); // 先设参数,再放置盘子,顺序不能反 tower.setAmountOfDisc(amountOfDisc); tower.setMaxDiscWidth(120); tower.setMinDiscWidth(50); tower.setDiscHeight(16); tower.putDiscOnTower(); add(tower, BorderLayout.CENTER); // Tower 是 JPanel 子类,放中间 bar = new JMenuBar(); menuGrade = new JMenu("选择级别"); oneGradeItem = new JMenuItem("初级"); twoGradeItem = new JMenuItem("中级"); threeGradeItem = new JMenuItem("高级"); menuGrade.add(oneGradeItem); menuGrade.add(twoGradeItem); menuGrade.add(threeGradeItem); bar.add(menuGrade); setJMenuBar(bar); // 菜单项与按钮共用同一个监听器,靠 getSource() 区分 oneGradeItem.addActionListener(this); twoGradeItem.addActionListener(this); threeGradeItem.addActionListener(this); renew = new JButton("重新开始"); renew.addActionListener(this); autoButton = new JButton("自动演示"); autoButton.addActionListener(this); JPanel north = new JPanel(); north.add(renew); north.add(autoButton); add(north, BorderLayout.NORTH); setResizable(false); setBounds(60, 60, 460, 410); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setVisible(true); validate(); } }

这段代码里真正需要留意的是四个几何参数,它们互相咬合,改一个就得核对另外三个。

参数初值作用改动后的连带影响
amountOfDisc3盘子个数决定 Disc 数组和三个塔点数组的长度,最少 2^3-1=7 步
maxDiscWidth120最大盘宽度同时决定三个座的水平基准,塔点 x 分别是它的 1、2、3 倍
minDiscWidth50最小盘宽度与 max 一起决定相邻盘的宽度差
discHeight16盘子高度决定塔点的纵向间距、竖线起止点和底部横条位置

setBounds(60, 60, 460, 410)的宽必须留够 3 倍 maxDiscWidth 加左右余量,否则 C 座会被裁掉一截,鼠标永远点不到最右边的盘子。

2.2 三档难度切换的复位路径

菜单项的响应逻辑短,但顺序有讲究。

public void actionPerformed(ActionEvent e) { if (e.getSource() == oneGradeItem) { amountOfDisc = 3; tower.setAmountOfDisc(amountOfDisc); tower.putDiscOnTower(); // 内部先 removeDisk 再重建 } else if (e.getSource() == twoGradeItem) { amountOfDisc = 4; tower.setAmountOfDisc(amountOfDisc); tower.putDiscOnTower(); } else if (e.getSource() == threeGradeItem) { amountOfDisc = 5; tower.setAmountOfDisc(amountOfDisc); tower.putDiscOnTower(); } else if (e.getSource() == renew) { tower.setAmountOfDisc(amountOfDisc); // 沿用当前档位,不清零 amountOfDisc tower.putDiscOnTower(); } else if (e.getSource() == autoButton) { tower.setAmountOfDisc(amountOfDisc); tower.putDiscOnTower(); int x = getBounds().x + getBounds().width; int y = getBounds().y; tower.getAutoMoveDisc().setLocation(x, y); tower.getAutoMoveDisc().setSize(280, getBounds().height); tower.getAutoMoveDisc().setVisible(true); } validate(); }

「重新开始」这里有个细节:它不能重置amountOfDisc,否则用户在高级档玩到一半点重开会被打回三盘。盘子数量是窗口级别的状态,Tower 只是执行者,这个职责划分要守住。

2.3 自动演示对话框的定位

自动演示按钮做三件事:复位棋盘、把 AutoMoveDisc 对话框贴到主窗口右侧、设成可见。getBounds().x + getBounds().width让对话框紧贴主窗口右边沿,不会盖住棋盘;高度直接取主窗口高度,这样步数列表和四个按钮都能排下。如果窗口被拖到屏幕最右侧,对话框会跑到屏幕外,常见做法是判断x + 280是否超出屏幕宽度再回退到主窗口左侧。

3. Tower 塔点模型与盘子几何参数的计算

Tower 是整个项目的重心,它同时承担了两件事:维护三根柱子上的盘子分布,以及把这份分布翻译成像素坐标。把这两件事混在一起写是课设里最常见的结构问题,但在这个规模下,用「塔点数组」做中间层已经足够清晰。

3.1 TowerPoint 与坐标系的对应关系

TowerPoint 只存四个东西:坐标 x、y,占位标志 haveDisc,以及压在上面的 Disc 引用。

public class TowerPoint { int x, y; boolean haveDisc; Disc disc = null; public boolean isHaveDisc() { return haveDisc; } public void setHaveDisc(boolean boo) { haveDisc = boo; } // 把盘子压到该点:容器的添加顺序决定叠放顺序 public void putDisc(Component com, Container con) { int w = con.getWidth(); int h = con.getHeight(); disc = (Disc) com; setHaveDisc(true); disc.setPoint(this); // 盘子中心对齐塔点,而不是左上角对齐 con.add(disc); disc.setLocation(x - disc.getWidth() / 2, y - disc.getHeight() / 2); con.setComponentZOrder(disc, 0); } public Disc getDiscOnPoint() { return disc; } public void removeDisc(Component com, Container con) { if (disc != null && con != null) con.remove(disc); disc = null; setHaveDisc(false); } }

关键在putDisc里的居中:塔点的 (x, y) 是盘子中心,不是左上角。少了x - disc.getWidth()/2这一步,所有盘子会向右偏移半个身位,越往下越明显,看起来像是斜着堆的。

3.2 塔点生成与盘子宽度递推

public void putDiscOnTower() { removeDisk(); // 重建前必须清场 int n = (maxDiscWidth - minDiscWidth) / amountOfDisc; disc = new Disc[amountOfDisc]; for (int i = 0; i < disc.length; i++) { disc[i] = new Disc(); disc[i].setNumber(i); // 编号越小盘子越窄 int diskWidth = minDiscWidth + i * n; disc[i].setSize(diskWidth, discHeight); disc[i].addMouseListener(handleMouse); disc[i].addMouseMotionListener(handleMouse); } pointA = new TowerPoint[amountOfDisc]; pointB = new TowerPoint[amountOfDisc]; pointC = new TowerPoint[amountOfDisc]; int verticalDistance = discHeight; for (int i = 0; i < pointA.length; i++) { pointA[i] = new TowerPoint(maxDiscWidth, 100 + verticalDistance); verticalDistance += discHeight; } // pointB、pointC 同理,x 分别取 2*maxDiscWidth 和 3*maxDiscWidth for (int i = 0; i < pointA.length; i++) { pointA[i].putDisc(disc[i], this); // 初始全部堆在 A 座 } handleMouse.setPointA(pointA); handleMouse.setPointB(pointB); handleMouse.setPointC(pointC); autoMoveDisc = new AutoMoveDisc(this); autoMoveDisc.setTowerName(towerName); autoMoveDisc.setAmountOfDisc(amountOfDisc); autoMoveDisc.setPointA(pointA); autoMoveDisc.setPointB(pointB); autoMoveDisc.setPointC(pointC); validate(); repaint(); }

宽度公式minDiscWidth + i * n有个取整偏差:当 amountOfDisc=5 时,n = (120-50)/5 = 14,最大盘只有 50+4×14 = 106,用不满 120。想让它铺满,把除法改成先乘后除,或者把 n 定义为(maxDiscWidth - minDiscWidth) / (amountOfDisc - 1)并单独处理只有一盘的情况。

另外注意,塔点数组下标 0 在顶部,下标 amountOfDisc-1 在底部。初始化时pointA[i].putDisc(disc[i], this)把编号大的宽盘放在了大下标处,视觉上自然形成上小下大。这个「下标越大越靠下」的约定会被绘制、拖拽、自动求解三处复用,不能中途改。

3.3 绘制顺序与竖线锚点

paintComponent负责画三根柱子的竖线、底座横条、每个塔点的小圆点和座标文字。它的调用时机是repaint(),所以每次盘子移动后都要触发一次。绘制的顺序也有讲究:

绘制内容坐标来源说明
竖线point[i][0] 到 point[i][n-1]y 各向外扩 discHeight/2,把盘子包住
底座横条pointA[n-1] 到 pointC[n-1]蓝色 fillRect,高度固定 6
塔点圆点每个塔点中心直径 5,画在盘子之后会被遮住
座名文字point[i][n-1].getY()+50"A座""B座""C座"

注意事项:paintComponent里直接访问pointA[0],如果 Tower 还未执行过putDiscOnTower()就被重绘,会抛 NullPointerException。稳妥写法是在方法开头加一句if (pointA == null) return;,或者在构造 Tower 时就完成一次初始化。

4. HandleMouse 拖拽判定与 AutoMoveDisc 递归演示

鼠标拖拽和自动求解是两条互不干扰的通道,共用同一套塔点数据。前者解决「人怎么操作」,后者解决「机器怎么演示」,放在两个类里是对的。

4.1 鼠标三阶段的事件分工

方法触发时机职责
mousePressed按下左键记录盘子原占塔点,记录鼠标在盘内的相对偏移
mouseDragged按住拖动让盘子按偏移量跟着鼠标走,实时 setLocation
mouseReleased松开左键求最近合法落点,能放就落,不能放就弹回原塔点
public void mousePressed(MouseEvent e) { Disc disc = (Disc) e.getSource(); startPoint = disc.getPoint(); // 记住原位置,非法时回退用 x = e.getX(); // 相对偏移,避免盘子跳到鼠标左上角 y = e.getY(); } public void mouseDragged(MouseEvent e) { Disc disc = (Disc) e.getSource(); disc.setLocation(disc.getX() + e.getX() - x, disc.getY() + e.getY() - y); } public void mouseReleased(MouseEvent e) { Disc disc = (Disc) e.getSource(); endPoint = findNearestPoint(disc); // 在三个座里找距离最近的塔点 if (endPoint != null && canPut(disc, endPoint)) { startPoint.removeDisc(disc, con); // 先摘下来,再压到新位置 endPoint.putDisc(disc, con); } else { startPoint.putDisc(disc, con); // 非法则原位放回 } con.repaint(); }

xy这两个偏移量是必须的。如果拖拽时直接disc.setLocation(e.getX(), e.getY()),鼠标点在盘子哪个角落,盘子就跳到哪,手感会非常跳。

4.2 合法落点的两条判定

规则只有一条:不能把大盘放在小盘上面。落到代码里要拆成两个条件——落点必须是该座当前第一个空位,且该空位下面那张盘的编号要比本盘小(编号小=盘小=应在上方)。

private boolean canPut(Disc disc, TowerPoint target) { if (target.isHaveDisc()) return false; // 位置已被占 // 校验它下方的塔点:如果下方有盘,本盘必须更小 for (TowerPoint[] tower : new TowerPoint[][]{pointA, pointB, pointC}) { int idx = indexOf(tower, target); if (idx >= 0) { if (idx + 1 < tower.length && tower[idx + 1].isHaveDisc()) { Disc below = tower[idx + 1].getDiscOnPoint(); return disc.getNumber() > below.getNumber(); } return true; // 到底了,随便放 } } return false; }

反过来写判断也行:below.getNumber() < disc.getNumber()说明下面那张更宽,摆在它上面合法。这里最容易错的是把编号方向记反了,建议在 Disc 上加一句注释「number 越小越窄」,省得三天后自己都看不懂。

4.3 递归步骤预生成与定时播放

自动演示不能真的在新线程里递归调用移动方法,那样界面会被瞬间刷完,看不到过程。常见做法是先用递归把每一步压进列表,再用javax.swing.Timer按固定间隔逐条回放。

// 预生成移动序列:把 n 个盘从 a 经 c 移到 b private void buildSteps(int n, char a, char b, char c) { if (n == 1) { steps.add(a + "->" + b); return; } buildSteps(n - 1, a, c, b); // n-1 个先挪到辅助座 steps.add(a + "->" + b); // 最大的那个直接到位 buildSteps(n - 1, c, b, a); // 再把 n-1 个挪过去 } // Timer 每触发一次执行一步 private void playNext() { if (moveStep >= steps.size()) { time.stop(); return; } String s = steps.get(moveStep++); char from = s.charAt(0), to = s.charAt(3); TowerPoint src = topPointOf(from); // 取出该座最上面的盘 TowerPoint dst = emptyPointOf(to); // 该座第一个空位 Disc d = src.getDiscOnPoint(); src.removeDisc(d, tower); dst.putDisc(d, tower); tower.repaint(); showStep.append(s + "\n"); // 步数同步滚进文本区 }

步数总量是 2^n - 1,这个数字决定了演示时长,也决定了文本区需要多大。

盘子数最少步数500ms 间隔下的时长
37约 3.5 秒
415约 7.5 秒
531约 15.5 秒

Timer 的间隔建议放在 300ms 到 800ms 之间。低于 200ms 人眼跟不上,高于 1000ms 又显得卡。对话框上的暂停按钮对应time.stop(),继续按钮对应time.start(),但要注意暂停后moveStep必须保留,否则会从头开始。

5. 编译链路、坐标越界与演示稳定性的排查技巧

这个项目的编译命令很朴素,但在中文环境里有几个坑值得提前说清楚。六个源文件放在同一目录下,用javac *.java批量编译,再运行主类:

# 指定编码,避免注释里的中文把编译搞崩 javac -encoding UTF-8 HannoiWindow.java Tower.java Disc.java TowerPoint.java HandleMouse.java AutoMoveDisc.java java HannoiWindow

-encoding不能省。用 GBK 保存的源码在 UTF-8 环境下编译会报「编码 GBK 的不可映射字符」,反过来也一样。Eclipse 里对应的是 Window → Preferences → General → Workspace 的 Text file encoding,设成和文件实际编码一致,这个项目里六个文件要保持统一。

几个高频故障和对应处理:

现象根因处理
启动即抛 NullPointerException,堆栈指向 paintComponentpointA 还是 null 就触发重绘在 paintComponent 开头判空,或先 putDiscOnTower 再 setVisible
盘子能拖但松手后消失removeDisc 里 con.remove 了盘,但没重新 add落点为空时把盘子放回 startPoint,别只调 remove
高难度下 C 座点不到窗口宽度小于 3×maxDiscWidth调大 setBounds 的宽,或调小 maxDiscWidth
点一次「自动演示」后速度越来越快每次点击都 new 一个 Timer,多个计时器叠加在按钮响应里先判断 time != null 再 stop,然后重建
盘子被底座横条盖住组件添加顺序晚于绘制用 setComponentZOrder 把 Disc 提到最上层

再补一个验证递归正确性的土办法:把buildSteps产出的序列打印出来,检查三个座上的盘子数是否始终满足「上小下大」,以及最终是否全部落到目标座。三盘应该正好 7 步,四盘 15 步,五盘 31 步,数量对不上就说明递归的边界条件写错了——通常是n == 1那条分支被写成了n == 0,导致每层多算一步。

如果要继续往外扩,比较值得做的是把手动操作的每一步也记进一个操作栈,提供「撤销」和「回放」;再进一步,可以在每次落子后判断当前局面是否仍处在最短解路径上,给出提示。这两个改动都只需在 HandleMouse 的落点分支里加几行记录代码,塔点模型本身不用动。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 16:58:24

工程板工装吊顶选哪家强,2026实力口碑榜出炉,零套路备选不踩坑

很多有工装项目需求的工程方、装修负责人&#xff0c;找供应商的时候第一个问题就是&#xff1a;做工程板工装吊顶的公司有哪些比较好?毕竟工装项目对吊顶材料的需求量大&#xff0c;要求也更严格&#xff1a;不仅要尺寸精准、品质稳定&#xff0c;还要能跟上项目施工节点&…

作者头像 李华
网站建设 2026/9/18 16:57:33

Scrapy爬虫框架实战:从异步原理到分布式部署

简介&#xff1a;这是一份讲解开源Python网络爬虫框架Scrapy的PDF文档&#xff0c;专为Python爬虫初学者和希望系统掌握Scrapy架构的开发者准备。文档首先介绍网络爬虫的基本概念&#xff0c;然后围绕Scrapy引擎、调度器、下载器、蜘蛛、项目管道、下载器中间件、蜘蛛中间件等核…

作者头像 李华
网站建设 2026/9/18 16:57:10

招聘信息可视化分析:从爬虫到Word报告的一站式Python实践

简介&#xff1a;docx文档《基于Python语言的招聘信息可视化分析》面向数据分析初学者、互联网行业求职者及人力资源从业者&#xff0c;利用招聘平台数据讲解从采集到决策的完整链路。文档从Python基础工具链入手&#xff0c;介绍Pandas、NumPy、Matplotlib、Seaborn、Scrapy、…

作者头像 李华
网站建设 2026/9/18 16:56:09

查重报告里 AI 率飘红?TaoToken 这样改 Codex 的模型通道再复检

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华