
简介基于JavaFX开发的流程图设计器源码工程面向学习Java与JavaFX课程的高校学生尤其适合作为期末大作业参考或图形界面编程实战案例。压缩包内共21个文件包含13个Java源文件、6个XML配置文件、1个说明文档及Git忽略文件整包仅26KB属于轻量级源码项目。Java源文件覆盖了流程图元素的绘制、交互编辑与布局算法XML配置则包含Maven的pom.xml及开发环境信息便于快速掌握项目结构与依赖管理。目前已有65人学习下载资源保留了完整的src/main源码目录和IDE配置可直接导入主流Java开发环境运行调试。通过该项目读者能深入理解JavaFX中Canvas绘图、Stage/Scene界面搭建、事件监听与图形节点操作等关键机制同时掌握流程图设计器从创建、编辑到保存导出的完整实现思路对完成同类期末设计或提升桌面应用开发能力都很有帮助。1. JavaFX流程图设计器为什么说它是期末大作业里性价比最高的一类选题「JavaFX流程图设计器」是一个放在期末大作业清单里容易被低估的选题。它比管理系统多出看得见的交互——节点能拖、能连线、能选中删除又比游戏简单所有图形都用 Rectangle、Circle、Text 这些 JavaFX 基础组件拼出来不碰物理引擎和动画循环。用一句话概括核心玩法在画布上摆节点在节点之间拉连线最后把整张图保存成工程文件再打开。这个选题能一次性覆盖面向对象设计、集合管理、事件驱动和文件持久化四块课程重点演示效果好答辩也好讲。适合不愿意卷算法、也不想碰 Android 方向的 Java 课学生也适合想拿 JavaFX 做内部小工具的工程师练手。下面按从零搭这类工具的顺序展开最小工程、可拖拽节点、连线交互再到避坑和答辩加分写法。2. 先立骨架Maven JavaFX 最小工程与启动参数配置2.1 为什么我不用 IDE 自带的 JavaFX 模板而用 Maven多数课程的上机环境是 Eclipse 或 IDEA点 New Project 里确实有 JavaFX 选项但那个模板依赖 IDE 帮你配置的本地 SDK 路径换一台机器、换一个 IDE 版本路径就失效重新导包能折腾一下午。我一般拿到别人给的 JavaFX 工程第一件事也是看有没有 pom.xml没有就先按 Maven 结构重建。Maven 的好处是把依赖从中央仓库拉下来pom.xml 写清楚坐标和版本Eclipse、IDEA、命令行都能跑交作业时把 src 和 pom.xml 一起压缩就行不需要附带几十 MB 的 SDK。前提是 JDK 17 以上且 JAVA_HOME 配置正确——这一步对环境变量不熟的人最容易卡住后面第 5 章会专门说启动失败怎么排查。有人会问JavaFX 不是 JDK 里自带吗那是 JDK 8 及之前的旧事。从 JDK 9 开始JavaFX 被从 JDK 中拆出去独立维护JDK 11 之后的发行版里根本没有 javafx 包。这就是为什么很多同学把代码从 JDK 8 项目复制到 JDK 17一编译满屏红叉。2.2 pom.xml 里的 javafx-controls 依赖与模块化两种写法先看一份能直接跑通的最小 pom.xml只引入 javafx-controls 一个模块它会把图形、事件、场景相关的类全部带进来。project xmlnshttp://maven.apache.org/POM/4.0.0 modelVersion4.0.0/modelVersion groupIdcom.flowdesigner/groupId artifactIdflow-designer/artifactId version1.0/version packagingjar/packaging properties maven.compiler.source17/maven.compiler.source maven.compiler.target17/maven.compiler.target javafx.version21.0.2/javafx.version /properties dependencies dependency groupIdorg.openjfx/groupId artifactIdjavafx-controls/artifactId version${javafx.version}/version /dependency /dependencies build plugins !-- javafx-maven-plugin 负责把模块路径传给 JVM解决“JavaFX runtime components are missing” -- plugin groupIdorg.openjfx/groupId artifactIdjavafx-maven-plugin/artifactId version0.0.8/version configuration mainClasscom.flowdesigner.MainApp/mainClass /configuration /plugin /plugins /build /project逻辑说明javafx-controls是 JavaFX 最核心的桌面 UI 模块javafx-maven-plugin的职责是帮你算出--module-path并启动应用。如果你只需要弹窗、画布、按钮、文本这一个依赖足够等做到导出图片那一步可以再补javafx-swing或直接用javafx.graphics里的Snapshot功能。参数说明里有三处要留意。第一maven.compiler.source/target要和本机 JDK 版本匹配写成 17 是最稳妥的JavaFX 21 要求 JDK 17 以上低了会直接报错。第二mainClass如果用了module-info.java要写成模块名/全类名的格式否则只写全类名。第三JavaFX 版本 17 和 21 都是最常见的 LTS21.0.2 能同时兼容 JDK 17 和 JDK 21省得来回换。是否写module-info.java是期末作业里最容易纠结的点。不写模块描述文件也能跑此时 javafx-maven-plugin 用 classpath 模式启动适合绝大多数课程演示写了模块描述文件代码更规范但要把requires javafx.controls;写对稍有不慎就变成启动失败。我的习惯是不写把精力留在功能上答辩时反而能解释清楚两种模式的区别。2.3 最小启动类从 Application.launch 到首屏窗口JavaFX 的入口跟普通 main 方法不一样需要继承Application类并实现start方法。下面是最小可运行版本先把窗口和一块空画布立起来。package com.flowdesigner; import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.layout.AnchorPane; import javafx.stage.Stage; public class MainApp extends Application { Override public void start(Stage stage) { // 用 AnchorPane 当画布容器子节点靠 setLayoutX/setLayoutY 自由定位 AnchorPane canvas new AnchorPane(); canvas.setPrefSize(1280, 800); canvas.setStyle(-fx-background-color: #f7f8fa;); Scene scene new Scene(canvas); stage.setTitle(流程图设计器 - JavaFX 期末大作业); stage.setScene(scene); stage.show(); } public static void main(String[] args) { launch(args); } }逻辑说明launch(args)是 JavaFX 框架的真正入口它会初始化图形环境然后在 JavaFX 应用线程上回调start方法。AnchorPane是最适合做自由画布的容器因为子节点不参与布局管理坐标完全由自己控制换成BorderPane或VBox反而要处理布局约束徒增麻烦。参数说明里有一个容易被忽略的事实Scene不设宽高时默认跟随根节点的PrefSize所以canvas.setPrefSize(1280, 800)实际决定了首屏大小。做期末作业时建议把画布再做大一点比如 1600×1000因为流程图一旦连了十几条线800 的高度会显得很挤。编译运行命令如下mvn clean javafx:run如果 IDE 里直接跑main方法报「JavaFX runtime components are missing」不要硬刚回到命令行用mvn javafx:run这是这个插件存在的意义。2.4 fxml、图片、字体放进 resources 后的加载路径坑不少作业会配合 FXML 做界面FXML 文件本身没毛病但资源加载路径有讲究。用getClass().getResource(/view/main.fxml)时路径最前面必须带斜杠表示从 classpath 根目录开始找写成getResource(view/main.fxml)就会去当前类的包目录里找十有八九返回 null然后抛NullPointerException。哪怕你暂时不用 FXMLCSS 样式表也会踩同一个坑。scene.getStylesheets().add(getClass().getResource(/css/style.css).toExternalForm())才是正解。打包成 jar 之后文件系统的相对路径会全部失效只有 classpath 路径还能用所以从一开始就养成带前导斜杠的习惯能省掉答辩前夜一次莫名其妙的修 bug。3. 把节点做活拖拽、选中与画布坐标换算3.1 用 Group 把 Rectangle 和 Text 合成流程图节点流程图里的每个节点本质上是一块圆角矩形、一行文字和两个端口圆点。用Group把这些组件包起来拖拽时只需要移动 Group 本身内部的图形自动跟随这是最省事的设计。public class DraggableNode extends Group { private final String nodeId; private final Rectangle body; private final Text label; public DraggableNode(String nodeId, String text, double x, double y) { this.nodeId nodeId; body new Rectangle(140, 56); body.setArcWidth(12); body.setArcHeight(12); body.setFill(Color.web(#ffffff)); body.setStroke(Color.web(#8a93a6)); body.setStrokeWidth(1.5); label new Text(text); label.setX(12); label.setY(30); label.setFont(Font.font(System, 14)); getChildren().addAll(body, label); setLayoutX(x); setLayoutY(y); } public String getNodeId() { return nodeId; } }逻辑说明Group不同于普通容器它不强制子节点参与布局所有子节点坐标都是相对 Group 内部的移动 Group 本身最直接。body是节点的可视底板label是节点文字。节点 ID 单独存一份后面做连线和 JSON 存取时靠它索引不能依赖对象引用。参数说明里值得关注的是setLayoutX/setLayoutY与setTranslateX/setTranslateY的差别。前者是节点在父容器中的布局位置后者是叠加的偏移量。做拖拽时优先改layoutX/layoutY因为保存工程文件时读这两个值最直观用translate的话保存前还得做一次加法容易漏。3.2 三个鼠标事件让节点“跟手”按下记偏移、拖动改坐标、松开落定位拖拽最忌讳的做法是拖动时不断用getLayoutX() 位移那样会把上一次的漂移累积进去。正确做法是在按下时记录一次基准值拖动时用「基准值 鼠标总位移」重新计算。private double pressSceneX; private double pressSceneY; private double layoutStartX; private double layoutStartY; setOnMousePressed(e - { // 按下瞬间记录鼠标场景坐标和节点当前布局坐标 pressSceneX e.getSceneX(); pressSceneY e.getSceneY(); layoutStartX getLayoutX(); layoutStartY getLayoutY(); toFront(); }); setOnMouseDragged(e - { // 布局坐标 按下时的布局坐标 鼠标移动量 setLayoutX(layoutStartX e.getSceneX() - pressSceneX); setLayoutY(layoutStartY e.getSceneY() - pressSceneY); }); setOnMouseReleased(e - { // 松开后不需要做额外处理坐标已经写进 layoutX/layoutY });逻辑说明e.getSceneX()是鼠标在整个窗口中的坐标getSceneX() - pressSceneX是本次拖动的总位移。把位移加到按下时的布局坐标上无论之前怎么拖都不会产生累积误差。toFront()把被拖的节点抬到最上层避免节点间遮挡导致拖不动。这里有个关键细节不要在MouseDragged里读e.getX()或e.getY()那是相对于当前节点的坐标节点一动它就变用来定位会越算越乱。这属于典型的「看着能跑、跑起来飞」的写法。3.3 场景坐标转画布坐标为什么不能直接 setLayoutX(e.getSceneX())很多同学写拖拽时直接setLayoutX(e.getSceneX())结果是越拖越偏最后节点跑到窗口外面去。原因在于场景坐标和父容器坐标不是同一个坐标系。Scene坐标的原点在窗口客户区左上角而AnchorPane的坐标原点是画布左上角。当画布直接作为 Scene 的根节点并铺满窗口时两者原点基本重合一旦画布外面套了菜单栏、工具栏或标题栏Y 方向就会差几十像素。正确做法是用父节点的坐标系转换Parent parent getParent(); if (parent ! null) { Point2D local parent.sceneToLocal(e.getSceneX(), e.getSceneY()); setLayoutX(local.getX()); setLayoutY(local.getY()); }逻辑说明sceneToLocal把场景坐标换算成当前父容器坐标系下的坐标自动抵消标题栏、菜单栏带来的偏移。如果你的画布就是窗口里唯一的根节点直接getSceneX()碰巧能跑但拿掉窗口标题栏或加一个顶栏就会翻车。我一律用sceneToLocal省得后面排查玄学漂移。3.4 选中描边与拖动阈值防止点击和拖拽打架流程图设计器需要反馈「你选中了哪个节点」。最轻量的做法是改描边颜色和粗细选中为蓝色粗边取消选中恢复灰色。setOnMouseClicked(e - { // 只有没有明显位移的点击才算选中避免拖完自动变选中 if (Math.abs(e.getSceneX() - pressSceneX) 3 Math.abs(e.getSceneY() - pressSceneY) 3) { body.setStroke(Color.web(#2b7df7)); body.setStrokeWidth(2.0); toFront(); } });逻辑说明如果不对位移做判断拖拽结束时也会触发MouseClicked导致每个节点拖完都变成选中状态。加一个 3 像素的拖动阈值小于它就当作点击这是一个纯鼠标交互的默认容差。选中之后还要维护「上一次选中节点」的取消逻辑。我一般让画布持有一个DraggableNode selectedNode字段新节点选中时把旧节点的描边和描边宽度还原。这一步不做多个节点同时高亮流程图看起来就像装修现场。4. 把连线做通锚点、折线与删除交互4.1 给节点加输入/输出端口两个 Circle 锚点怎么放只有节点没有端口连线就没有落点。流程图设计器里最常见的端口约定是输入端口在顶部中点输出端口在底部中点。这样读图顺序天然是从上到下、从左到右。端口用Circle实现直径 12 像素加一圈边线让它在白色节点底板上看得清。private final Circle inputPort; private final Circle outputPort; // 在 DraggableNode 构造器里追加 inputPort new Circle(70, 0, 6); // 顶部中点 inputPort.setFill(Color.web(#ffffff)); inputPort.setStroke(Color.web(#2b7df7)); inputPort.setStrokeWidth(1.5); outputPort new Circle(70, 56, 6); // 底部中点 outputPort.setFill(Color.web(#ffffff)); outputPort.setStroke(Color.web(#2b7df7)); outputPort.setStrokeWidth(1.5); getChildren().addAll(inputPort, outputPort);逻辑说明Circle(70, 0, 6)的前两个参数是圆心坐标第三个是半径。节点宽 140所以中点在 x70高 56顶部中点在 y0底部中点在 y56。端口放在 Group 内部随节点一起移动连线时不需要重新计算端口位置直接用localToScene换算一次即可。端口半径影响连线的视觉精度。6 像素的半径意味着鼠标点到端口附近 6 像素内都算命中手感比较友好。如果做得太细比如 3 像素用户连四五条线就会烦。4.2 四段式折线连线从源端口到目标端口的 Path 怎么画两个节点之间的连线不能是直线直线会穿过中间的节点而且视觉上像蜘蛛网。工业流程设计器普遍用正交折线我习惯用一个四段式Path先水平出去再垂直拐弯再水平进来。public class LinkLine extends Path { public LinkLine(double sx, double sy, double tx, double ty) { double bend Math.max(20, Math.abs(sx - tx) / 4); getElements().add(new MoveTo(sx, sy)); getElements().add(new HLineTo(sx bend)); getElements().add(new VLineTo(ty)); getElements().add(new HLineTo(tx)); getStyleClass().add(link-line); } public void update(double sx, double sy, double tx, double ty) { double bend Math.max(20, Math.abs(sx - tx) / 4); getElements().clear(); getElements().add(new MoveTo(sx, sy)); getElements().add(new HLineTo(sx bend)); getElements().add(new VLineTo(ty)); getElements().add(new HLineTo(tx)); } }逻辑说明路径元素从左到右依次执行。MoveTo把画笔移到源端口坐标HLineTo(sx bend)先向右画一段水平线bend是拐角余量VLineTo(ty)垂直到达目标点的 Y 坐标最后HLineTo(tx)水平进入目标端口。整个路径是一条「先右、再下、再左」的正交折线。参数说明里最容易误解的是bend的计算。固定写死 30 像素会出问题当起点和终点水平距离很近甚至终点在源端口左侧时固定 bend 会导致路径往回折形成 Z 字形很丑。把它设成Math.abs(sx - tx) / 4并且限制最小值 20能保证拐弯长度始终小于连线总长的一半路径不会出现回头弯。4.3 连线跟随节点移动监听 layoutXProperty 而不是手动刷新拖节点时已连好的线必须跟着动。最笨的办法是每次MouseDragged里遍历所有连线重新算坐标但那样要处理大量查找逻辑。更简洁的方案是让连线监听节点的layoutXProperty和layoutYProperty。// 创建 line 之后给源节点和目标节点各挂一个监听器 source.layoutXProperty().addListener((obs, oldVal, newVal) - line.update( sourceNodeCenterX(), sourceNodeOutputY(), targetNodeCenterX(), targetNodeInputY() )); source.layoutYProperty().addListener((obs, oldVal, newVal) - line.update(...)); target.layoutXProperty().addListener((obs, oldVal, newVal) - line.update(...)); target.layoutYProperty().addListener((obs, oldVal, newVal) - line.update(...));逻辑说明layoutXProperty是 JavaFX 的属性系统当setLayoutX改变坐标时监听器自动触发。节点 Group 内部有端口源端口在节点底部目标端口在节点顶部所以更新时要把端口相对节点中心的偏移量加进去。监听器里只改Path的元素不会反向修改节点的坐标因此不存在循环触发的问题。有个实际坑如果 Line 存的是「建立时的坐标快照」而不是「对节点的引用」监听器根本没有数据来源。所以连线模型里必须持有sourceNode和targetNode的引用而不是只存sourceX/sourceY。这一点在后续做 JSON 存取时要特别区分界面运行用对象引用保存文件用节点 ID。4.4 删线与右键菜单别让悬空线留在画布上流程图连错线是常态删除操作必须顺手。常见做法有两个选中连线后按 Delete 键或者在连线上点右键弹出删除菜单。对期末作业来说Delete 键最直观。scene.setOnKeyPressed(e - { if (e.getCode() KeyCode.DELETE selectedLine ! null) { canvas.getChildren().remove(selectedLine.getPath()); edgeList.remove(selectedLine); selectedLine null; } });逻辑说明selectedLine是画布维护的当前选中连线对象。删除时要做两件事把Path从画布的children里移除把LinkLine从数据列表里移除。只删界面不删数据保存工程文件时会发现幽灵线只删数据不删界面画布上会残留一条永远点不中的线。右键菜单可以用ContextMenu给Path挂setOnContextMenuRequested菜单里放一个「删除连线」项。这比 Delete 键更符合鼠标操作习惯但实现时要注意Path本身比较细右键不容易点中我一般把描边宽度调成 3 并在setPickOnBounds(true)让鼠标在连线附近 3 像素内都能命中手感会好很多。setPickOnBounds(true)是命中判定里最有用的参数默认Path只响应路径本体的像素范围把整个包围盒变成可点击区域后用户不需要精确到线上这对小目标交互是质的提升。5. 避坑实录JavaFX流程图设计器最常见的 5 个翻车现场以下五个问题是我自己和学生项目里反复出现的按「现象 → 原因 → 解决」的顺序写你遇到时可以直接对号入座。5.1 启动失败找不到 JavaFX 运行组件现象用mvn clean javafx:run启动后控制台报JavaFX runtime components are missing或者 IDE 里跑main方法直接抛NoClassDefFoundError: javafx/application/Application。原因JDK 9 之后不再内置 JavaFX也没有把 JavaFX 的 jar 放进 classpath。IDE 直接运行main方法时没有--module-path参数JVM 就找不到javafx.application.Application这个类。常见于从 JDK 8 项目迁移过来、或者手贱删掉了 pom 里的插件。解决pom.xml 里必须同时有javafx-controls依赖和javafx-maven-plugin然后用mvn clean javafx:run启动不要用 IDE 的 Run 按钮。如果你非要 IDE 运行需要手动加 VM 参数--module-path /path/to/javafx-sdk/lib --add-modules javafx.controls注意javafx-maven-plugin的mainClass配置如果写错包名会报「找不到主类」排查时先确认全类名和实际代码一致这是最蠢也最容易犯的错。5.2 拖拽漂移节点“飞掉”或越拖越偏现象鼠标按住节点拖动节点要么一下子跳到左上角要么每拖一次就偏离鼠标一段距离拖到后面节点和鼠标之间隔了大半个屏幕。原因用了setLayoutX(e.getSceneX())直接把场景坐标当布局坐标或者拖动时不断累加getLayoutX() e.getX()。前者忽略了窗口标题栏和菜单栏导致的坐标系偏移后者把每次拖动的微小误差反复叠加最终漂移成非线性。解决按下时记录基准值拖动时计算位移量。代码参考第三章 3.2 小节核心是setLayoutX(layoutStartX e.getSceneX() - pressSceneX)。实在要处理容器偏移就调用parent.sceneToLocal(e.getSceneX(), e.getSceneY())这是最稳的换算没有之一。5.3 连线不规整斜线、穿节点、回头折现象两个节点之间的连线是一条歪斜的对角线甚至从节点正中间穿过水平距离很近时连线出现一个明显的 Z 字形回折。原因直接用Line从端口圆心连到端口圆心没有考虑正交折线或者折线的拐角偏移量用固定值没做边界处理。对角线在流程图上看起来非常业余访客级别的人一眼就能看出来是赶工产物。解决改用第四章的Path四段式折线并且bend Math.max(20, Math.abs(sx - tx) / 4)。这个公式保证拐弯长度不超过连线水平总长的一半两端距离再近也不会回折。节点边框要有圆角要确保端点坐标不在矩形内部——从端口圆心而不是矩形角出发就行了。5.4 导出 PNG 全白全黑、只截到一块儿现象把画布截图保存成图片打开一看整张全白或只有左上角一小块内容节点和线全部消失。原因用node.snapshot时传了不存在的节点或者对Scene的根节点做快照时画布还没完成布局。JavaFX 的snapshot执行的是「当前画面渲染」如果节点尚未出现在窗口可见区域或者使用了ScrollPane而内容超出视口快照就会丢东西。这个问题的外在表现很像玄学实际就是快照目标选错了。解决对scene.getRoot()做快照导出前先调用root.applyCss()和root.layout()强制完成布局再用WritableImage接收WritableImage image new WritableImage((int) root.getBoundsInParent().getWidth(), (int) root.getBoundsInParent().getHeight()); root.snapshot(new SnapshotParameters(), image); ImageIO.write(SwingFXUtils.fromFXImage(image, null), png, file);逻辑说明applyCss让样式表先计算完毕layout让所有节点的坐标落定快照才能拿到完整像素。这里需要javafx-swing模块配合把 FX 图像转成BufferedImage记得在 pom 里补依赖org.openjfx:javafx-swing:21.0.2。5.5 关窗口进程不退界面没了Java 进程还挂着现象点掉窗口右上角界面消失了但 IDE 控制台的小红方块还在进程没退出任务管理器里能看到一个 java 进程占着内存。原因JavaFX 窗口关闭后应用线程自动结束的前提是没有非守护线程存活。常见的元凶是AnimationTimer、ScheduledExecutorService、或者你手写的new Thread(() - { while(true) ... })。期末作业里最常见的就是为了做闪烁提示或动画轮询起了线程忘了停。解决在stop()方法里做清理。stop是Application生命周期里窗口关闭后、进程退出前会回调的方法把定时器和动画线程在里面显式关闭Override public void stop() throws Exception { if (timer ! null) timer.stop(); if (executor ! null) executor.shutdownNow(); super.stop(); }逻辑说明AnimationTimer.stop()只停止动画帧回调不会结束 JavaFX 线程ScheduledExecutorService必须shutdownNow否则线程池里的线程一直活着进程自然退不掉。养成在 stop 里做清理的习惯这个坑基本能 100% 避开。6. 答辩加分项撤销重做 JSON 工程存取的两个落地写法6.1 撤销/重做把画布快照压栈比逐条记录操作省事实现撤销最省心的做法不是记录具体命令而是给画布做「快照」。每次操作前把当前状态深拷贝压进撤销栈操作完成后清空重做栈。撤销时弹栈恢复同时把当前状态压进重做栈。代码骨架如下DequeGraphSnapshot undoStack new ArrayDeque(); DequeGraphSnapshot redoStack new ArrayDeque(); private void pushUndo() { undoStack.push(captureSnapshot()); if (undoStack.size() 50) { undoStack.removeLast(); // 防止快照太多把内存吃垮 } redoStack.clear(); } private void undo() { if (undoStack.isEmpty()) return; redoStack.push(captureSnapshot()); restoreSnapshot(undoStack.pop()); }逻辑说明captureSnapshot遍历画布上的节点和连线把节点 ID、坐标、文本、连线关系收集到一个普通 POJO 里restoreSnapshot清空画布再按快照数据重建。快照栈上限 50 是经验值流程图节点多时一次快照可能要保存上百条数据无限压栈内存会涨得很难看。答辩时演示撤销的正确姿势是连续拖三个节点、连两条线然后「CtrlZ 三连」每一步画面都回退一次视觉效果比任何口头讲解都有说服力。这一步不做被评委问到「支持撤销吗」就只能尬住。6.2 JSON 存取用 ObjectMapper 把工程存成 .flow.json工程文件的保存不要用 Java 序列化也不要直接拿 JavaFX 组件去存。正确做法是定义两个纯数据类GraphNode存节点 ID、坐标、文本GraphEdge存源节点 ID 和目标节点 ID整个工程用GraphDocument包起来然后用 Jackson 的ObjectMapper读写。public class GraphDocument { public String name; public ListGraphNode nodes; public ListGraphEdge edges; } ObjectMapper mapper new ObjectMapper(); GraphDocument doc mapper.readValue(file, GraphDocument.class); // 加载时根据 doc 重建节点对象再根据 edges 重建连线逻辑说明GraphNode是普通 POJO只有id、x、y、text四个字段不持有 JavaFX 组件引用。保存时从界面节点取getLayoutX()写入x加载时用new DraggableNode(id, text, x, y)重建。JSON 文件天然可读、可 diff、可提交到 git答辩现场用文本编辑器打开工程文件展示数据结构会显得你对持久化理解很到位。这个方向值得做。快照、撤销、JSON 存取这三件事共用同一套数据结构写一次等于做了三件事。我最后一次做类似项目时把撤销栈的深度限制忽略了演示时撤销超过 20 步内存肉眼可见地涨当场被评委追问后来所有画布类工具我都先写 UndoManager 再写界面。希望帮到你。本文还有配套的精品资源点击获取